Skip to main content

lux_lib/build/compile/
c_modules.rs

1use std::{collections::HashMap, path::Path};
2
3use itertools::Itertools;
4use miette::Diagnostic;
5use path_slash::PathExt;
6use target_lexicon::Triple;
7use thiserror::Error;
8
9use crate::{
10    build::external_dependency::ExternalDependencyInfo,
11    config::Config,
12    fs,
13    lua_installation::LuaInstallation,
14    lua_rockspec::{LuaModule, ModulePaths},
15    variables::{self, Environment, VariableSubstitutionError},
16};
17
18use super::{c_link, CCExt};
19
20// TODO: (#261): special cases for mingw/cygwin?
21
22#[derive(Error, Debug, Diagnostic)]
23#[non_exhaustive]
24pub enum Error {
25    #[error(transparent)]
26    #[diagnostic(transparent)]
27    Fs(#[from] fs::Error),
28    #[error("failed to compile intermediates from C modules")]
29    #[diagnostic(help(
30        r#"check the compiler output above for missing headers or syntax errors.
31ensure the C compiler has access to the required include paths."#
32    ))]
33    CompileIntermediates(#[source] cc::Error),
34    #[error("error compiling C modules (compilation failed)")]
35    #[diagnostic(help(
36        r#"check the compiler output above for details.
37if no C compiler was found, run `lx debug toolchains` to verify your build tools."#
38    ))]
39    Compilation(#[from] cc::Error),
40    #[error("error compiling C modules (linking failed)")]
41    #[diagnostic(forward(0))]
42    Link(#[from] c_link::Error),
43    #[error(transparent)]
44    #[diagnostic(transparent)]
45    VariableSubstitution(#[from] VariableSubstitutionError),
46}
47
48/// Compiles a set of C files (with extra metadata) to a given destination.
49/// # Panics
50/// Panics if no filename for the target path can be determined.
51#[tracing::instrument(
52    name = "Compiling C modules",
53    level = "info",
54    skip_all,
55    fields(profile = config.build_profile().to_string()),
56)]
57pub(crate) async fn compile(
58    data: &ModulePaths,
59    source_dir: &Path,
60    target_module: &LuaModule,
61    target_dir: &Path,
62    lua: &LuaInstallation,
63    external_dependencies: &HashMap<String, ExternalDependencyInfo>,
64    config: &Config,
65) -> Result<(), Error> {
66    let target = target_dir.join(target_module.to_lib_path());
67
68    let target_parent_dir = target.parent().unwrap_or_else(|| {
69        unreachable!(
70            "couldn't determine build target parent directory:\n{}",
71            target.display()
72        )
73    });
74
75    fs::tokio::create_dir_all(target_parent_dir).await?;
76
77    let host = Triple::host();
78
79    let mut build = cc::Build::new();
80    let source_files = data
81        .sources
82        .iter()
83        .map(|dir| {
84            variables::substitute(
85                &[lua, external_dependencies, &Environment {}, config],
86                &dir.to_slash_lossy(),
87            )
88        })
89        .map(|dir| dir.map(|dir| source_dir.join(dir)))
90        .try_collect::<_, Vec<_>, _>()?;
91    let include_dirs = data
92        .incdirs
93        .iter()
94        .map(|dir| {
95            variables::substitute(
96                &[lua, external_dependencies, &Environment {}, config],
97                &dir.to_slash_lossy(),
98            )
99        })
100        .map(|dir| dir.map(|dir| source_dir.join(dir)))
101        .chain(
102            external_dependencies
103                .values()
104                .filter_map(|dep| dep.include_dir.clone())
105                .map(Result::Ok),
106        )
107        .try_collect::<_, Vec<_>, _>()?
108        .into_iter()
109        .unique() // Some rocks specify the include dirs via variable substitution.
110        .collect_vec();
111
112    let intermediate_dir = fs::tempfile::tempdir()?;
113    let build = build
114        .cargo_output(config.verbose())
115        .cargo_debug(config.verbose())
116        .cargo_metadata(config.verbose())
117        .cargo_warnings(config.verbose())
118        .warnings(config.verbose())
119        .files(source_files)
120        .host(&host.to_string())
121        .target(&host.to_string())
122        .includes(&include_dirs)
123        .includes(lua.includes())
124        .includes(
125            external_dependencies
126                .values()
127                .filter_map(|dep| dep.include_dir.as_ref()),
128        )
129        .opt_level(config.build_profile().opt_level())
130        .out_dir(intermediate_dir);
131
132    let compiler = build.try_get_compiler()?;
133    let is_msvc = compiler.is_like_msvc();
134    // Suppress all warnings
135    if is_msvc {
136        build.flag("-W0");
137    } else {
138        build.flag("-w");
139    }
140    for arg in lua.define_flags() {
141        build.flag(&arg);
142    }
143
144    // `cc::Build` has no `defines()` function, so we manually feed in the
145    // definitions in a verbose loop
146    for (name, value) in &data.defines {
147        build.define(name, value.as_deref());
148    }
149
150    let target_file_name = target
151        .file_name()
152        .unwrap_or_default()
153        .to_string_lossy()
154        .to_string();
155    // See https://github.com/rust-lang/cc-rs/issues/594#issuecomment-2110551057
156    let objects = build
157        .try_compile_objects(config)
158        .map_err(Error::CompileIntermediates)?;
159
160    let libdir_args = data
161        .libdirs
162        .iter()
163        .map(|dir| {
164            variables::substitute(
165                &[lua, external_dependencies, &Environment {}, config],
166                &dir.to_slash_lossy(),
167            )
168        })
169        .map(|libdir| {
170            libdir.map(|libdir| {
171                if is_msvc {
172                    format!("/LIBPATH:{}", source_dir.join(libdir).display())
173                } else {
174                    format!("-L{}", source_dir.join(libdir).display())
175                }
176            })
177        })
178        .try_collect::<_, Vec<_>, _>()?;
179
180    let library_args = data
181        .libraries
182        .iter()
183        .map(|dir| {
184            variables::substitute(
185                &[lua, external_dependencies, &Environment {}, config],
186                &dir.to_slash_lossy(),
187            )
188        })
189        .map(|library| {
190            library.map(|library| {
191                if is_msvc {
192                    format!("{}.lib", library)
193                } else {
194                    format!("-l{}", library)
195                }
196            })
197        })
198        .try_collect::<_, Vec<_>, _>()?;
199
200    c_link::artifacts(
201        build,
202        lua,
203        external_dependencies,
204        &target_file_name,
205        libdir_args,
206        library_args,
207        target_module,
208        target_parent_dir,
209        objects,
210        config,
211    )
212    .await?;
213    Ok(())
214}