Skip to main content

lux_lib/build/compile/
c_link.rs

1use std::{
2    collections::HashMap,
3    io,
4    path::{Path, PathBuf},
5    process::{ExitStatus, Output},
6};
7
8use itertools::Itertools;
9use miette::Diagnostic;
10use path_slash::PathExt;
11use thiserror::Error;
12use tokio::process::Command;
13
14use crate::{
15    build::external_dependency::ExternalDependencyInfo, config::Config, fs,
16    lua_installation::LuaInstallation, lua_rockspec::LuaModule,
17};
18
19#[derive(Error, Debug, Diagnostic)]
20#[non_exhaustive]
21pub enum Error {
22    #[error("IO operation failed while linking C modules")]
23    Io(#[from] io::Error),
24    #[error(transparent)]
25    #[diagnostic(transparent)]
26    Fs(#[from] fs::Error),
27    #[error(transparent)]
28    CC(#[from] cc::Error),
29    #[error("compilation failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
30    #[diagnostic(help(
31        r#"check the compiler output above for details.
32rerun with `--verbose` for more information."#
33    ))]
34    CommandFailure {
35        status: ExitStatus,
36        stdout: String,
37        stderr: String,
38    },
39    #[error("compiling C modules succeeded, but the expected library {0} was not created")]
40    #[diagnostic(help(
41        "check the rockspec or lux.toml `modules` and `lib_modules` tables for correct paths."
42    ))]
43    LibOutputNotCreated(String),
44}
45
46fn validate_output(output: &Output) -> Result<(), Error> {
47    if !output.status.success() {
48        return Err(Error::CommandFailure {
49            status: output.status,
50            stdout: String::from_utf8_lossy(&output.stdout).into(),
51            stderr: String::from_utf8_lossy(&output.stderr).into(),
52        });
53    }
54    Ok(())
55}
56
57/// On MSVC, we need to create Lua definitions manually
58#[tracing::instrument(level = "trace")]
59fn mk_def_file(
60    dir: &Path,
61    output_file_name: &str,
62    target_module: &LuaModule,
63) -> Result<PathBuf, fs::Error> {
64    let mut def_file: PathBuf = dir.join(output_file_name);
65    def_file.set_extension(".def");
66    let exported_name = target_module.to_string().replace(".", "_");
67    let exported_name = exported_name
68        .split_once('-')
69        .map(|(_, after_hyphen)| after_hyphen.to_string())
70        .unwrap_or_else(|| exported_name.clone());
71    let content = format!(
72        r#"EXPORTS
73luaopen_{exported_name}
74"#,
75    );
76    fs::sync::write(&def_file, content)?;
77    Ok(def_file)
78}
79
80#[allow(clippy::too_many_arguments)]
81pub(crate) async fn artifacts(
82    build: &mut cc::Build,
83    lua: &LuaInstallation,
84    external_dependencies: &HashMap<String, ExternalDependencyInfo>,
85    target_file_name: &str,
86    libdir_args: Vec<String>,
87    library_args: Vec<String>,
88    target_module: &LuaModule,
89    target_parent_dir: &Path,
90    objects: Vec<PathBuf>,
91    config: &Config,
92) -> Result<(), Error> {
93    let output_path =
94        std::path::absolute(target_parent_dir.join(target_file_name)).map_err(Error::Io)?;
95    let compiler = build.try_get_compiler()?;
96    // Linking from within a temp directory makes sure the linker doesn't create any build artifacts in
97    // the current working directory.
98    // See https://github.com/lumen-oss/lux/issues/1106
99    let temp_work_dir = fs::tempfile::tempdir()?;
100    let is_msvc = compiler.is_like_msvc();
101    let cmd = build.try_get_compiler()?.to_command();
102    let mut cmd: tokio::process::Command = cmd.into();
103    cmd.current_dir(temp_work_dir.path());
104    let output = if is_msvc {
105        let def_file = mk_def_file(temp_work_dir.path(), target_file_name, target_module)?;
106        cmd.args(&objects).arg("/LD");
107        add_variable_if_set(config, "LDFLAGS", &mut cmd);
108        cmd.arg("/link")
109            .arg("/NOIMPLIB")
110            .arg("/NOEXP")
111            .arg(format!("/DEF:{}", def_file.display()))
112            .arg(format!("/OUT:{}", output_path.display()))
113            .args(lua_lib_link_args(lua, &compiler))
114            .args(
115                external_dependencies
116                    .values()
117                    .flat_map(|dep| dep.lib_link_args(&compiler)),
118            )
119            .args(libdir_args)
120            .args(library_args)
121            .output()
122            .await?
123    } else {
124        add_variable_if_set(config, "LIBFLAG", &mut cmd);
125        add_variable_if_set(config, "LDFLAGS", &mut cmd);
126        cmd.args(vec!["-o".into(), output_path.to_string_lossy().to_string()])
127            .args(lua_lib_link_args(lua, &build.try_get_compiler()?))
128            .args(
129                external_dependencies
130                    .values()
131                    .flat_map(|dep| dep.lib_link_args(&compiler)),
132            )
133            .args(&objects)
134            .args(libdir_args)
135            .args(library_args)
136            .output()
137            .await?
138    };
139    crate::build::command::trace_output(&output);
140    validate_output(&output)?;
141
142    if output_path.exists() {
143        Ok(())
144    } else {
145        Err(Error::LibOutputNotCreated(
146            output_path.to_slash_lossy().to_string(),
147        ))
148    }
149}
150
151/// NOTE: In luarocks, these are behind a link_lua_explicity config option
152fn lua_lib_link_args(lua: &LuaInstallation, compiler: &cc::Tool) -> Vec<String> {
153    if cfg!(target_os = "macos") {
154        // On macos, linking Lua can lead to duplicate symbol errors
155        Vec::new()
156    } else {
157        lua.lib_link_args(compiler)
158    }
159}
160
161/// Lookup a variable in the `Config` and if set and non-empty,
162/// split it into multiple arguments and add them to the `Command`.
163fn add_variable_if_set(config: &Config, name: &str, cmd: &mut Command) {
164    let variables = config.variables();
165    if let Some(var_str) = variables.get(name) {
166        if !var_str.is_empty() {
167            let vars = var_str.split_whitespace().collect_vec();
168            cmd.args(vars);
169        }
170    }
171}