use std::env; use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; /// Represents the version of Lua to build. #[derive(Debug, PartialEq, Eq)] pub enum Version { Lua51, } pub use self::Version::*; /// Represents the configuration for building Lua artifacts. pub struct Build { out_dir: Option, target: Option, host: Option, opt_level: Option, debug: Option, } /// Represents the artifacts produced by the build process. #[derive(Clone, Debug)] pub struct Artifacts { include_dir: PathBuf, lib_dir: PathBuf, libs: Vec, } impl Default for Build { fn default() -> Build { Build { out_dir: env::var_os("OUT_DIR").map(PathBuf::from), target: env::var("TARGET").ok(), host: None, opt_level: None, debug: None, } } } impl Build { /// Creates a new `Build` instance with default settings. pub fn new() -> Build { Build::default() } /// Sets the output directory for the build artifacts. /// /// This is required if called outside of a build script. pub fn out_dir>(&mut self, path: P) -> &mut Build { self.out_dir = Some(path.as_ref().to_path_buf()); self } /// Sets the target architecture for the build. /// /// This is required if called outside of a build script. pub fn target(&mut self, target: &str) -> &mut Build { self.target = Some(target.to_string()); self } /// Sets the host architecture for the build. /// /// This is optional and will default to the environment variable `HOST` if not set. /// If called outside of a build script, it will default to the target architecture. pub fn host(&mut self, host: &str) -> &mut Build { self.host = Some(host.to_string()); self } /// Sets the optimization level for the build. /// /// This is optional and will default to the environment variable `OPT_LEVEL` if not set. /// If called outside of a build script, it will default to `0` in debug mode and `2` otherwise. pub fn opt_level(&mut self, opt_level: &str) -> &mut Build { self.opt_level = Some(opt_level.to_string()); self } /// Sets whether to build in debug mode. /// /// This is optional and will default to the value of `cfg!(debug_assertions)`. /// If set to `true`, it also enables Lua API checks. pub fn debug(&mut self, debug: bool) -> &mut Build { self.debug = Some(debug); self } /// Builds the Lua artifacts for the specified version. pub fn build(&self, version: Version) -> Artifacts { match self.try_build(version) { Ok(artifacts) => artifacts, Err(err) => panic!("{err}"), } } /// Attempts to build the Lua artifacts for the specified version. /// /// Returns an error if the build fails. pub fn try_build(&self, version: Version) -> Result> { let target = self.target.as_ref().ok_or("TARGET is not set")?; let out_dir = self.out_dir.as_ref().ok_or("OUT_DIR is not set")?; let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let mut source_dir = manifest_dir.join(version.source_dir()); let lib_dir = out_dir.join("lib"); let include_dir = out_dir.join("include"); if !include_dir.exists() { fs::create_dir_all(&include_dir) .context(|| format!("Cannot create '{}'", include_dir.display()))?; } let mut config = cc::Build::new(); config.warnings(false).cargo_metadata(false).target(target); match &self.host { Some(host) => { config.host(host); } // Host will be taken from the environment variable None if env::var("HOST").is_ok() => {} None => { // If called outside of build script, set default host config.host(target); } } let mut libs = vec![version.lib_name().to_string()]; match target { _ if target.contains("linux") => { config.define("LUA_USE_LINUX", None); } _ if target.ends_with("bsd") => { config.define("LUA_USE_LINUX", None); } _ if target.contains("apple-darwin") => { config.define("LUA_USE_LINUX", None); } _ if target.contains("apple-ios") => { config.define("LUA_USE_POSIX", None); } _ if target.contains("windows") => { // Defined in Lua >= 5.3 config.define("LUA_USE_WINDOWS", None); } _ if target.ends_with("emscripten") => { config .define("LUA_USE_POSIX", None) .cpp(true) .flag("-fexceptions"); // Enable exceptions to be caught let cpp_source_dir = out_dir.join("cpp_source"); if cpp_source_dir.exists() { fs::remove_dir_all(&cpp_source_dir) .context(|| format!("Cannot remove '{}'", cpp_source_dir.display()))?; } fs::create_dir_all(&cpp_source_dir) .context(|| format!("Cannot create '{}'", cpp_source_dir.display()))?; for file in fs::read_dir(&source_dir) .context(|| format!("Cannot read '{}'", source_dir.display()))? { let file = file?; let filename = file.file_name(); let filename = &*filename.to_string_lossy(); let src_file = source_dir.join(file.file_name()); let dst_file = cpp_source_dir.join(file.file_name()); let mut content = fs::read(&src_file) .context(|| format!("Cannot read '{}'", src_file.display()))?; if ["lauxlib.h", "lua.h", "lualib.h"].contains(&filename) { content.splice(0..0, b"extern \"C\" {\n".to_vec()); content.extend(b"\n}".to_vec()) } fs::write(&dst_file, content) .context(|| format!("Cannot write to '{}'", dst_file.display()))?; } source_dir = cpp_source_dir } _ if target.contains("wasi") => { // WASI is posix-like, but further patches are needed to the Lua // source to get it compiling. config.define("LUA_USE_POSIX", None); // Bring in just enough signal-handling support to get Lua at // least compiling, but WASI in general does not support // signals. config.define("_WASI_EMULATED_SIGNAL", None); libs.push("wasi-emulated-signal".to_string()); // https://github.com/WebAssembly/wasi-sdk/blob/main/SetjmpLongjmp.md // for information about getting setjmp/longjmp working. config.flag("-mllvm").flag("-wasm-enable-eh"); config.flag("-mllvm").flag("-wasm-use-legacy-eh=false"); config.flag("-mllvm").flag("-wasm-enable-sjlj"); libs.push("setjmp".to_string()); } _ => Err(format!("don't know how to build Lua for {target}"))?, } let debug = self.debug.unwrap_or(cfg!(debug_assertions)); if debug { config.define("LUA_USE_APICHECK", None); config.debug(true); } match &self.opt_level { Some(opt_level) => { config.opt_level_str(opt_level); } // Opt level will be taken from the environment variable None if env::var("OPT_LEVEL").is_ok() => {} None => { // If called outside of build script, set default opt level config.opt_level(if debug { 0 } else { 2 }); } } config .include(&source_dir) .warnings(false) // Suppress all warnings .flag_if_supported("-fno-common") // Compile common globals like normal definitions .add_files_by_ext(&source_dir, "c")? .out_dir(&lib_dir) .try_compile(version.lib_name())?; for f in &["lauxlib.h", "lua.h", "luaconf.h", "lualib.h"] { let from = source_dir.join(f); let to = include_dir.join(f); fs::copy(&from, &to) .context(|| format!("Cannot copy '{}' to '{}'", from.display(), to.display()))?; } Ok(Artifacts { include_dir, lib_dir, libs, }) } } impl Version { fn source_dir(&self) -> &'static str { match self { Lua51 => "lua-5.1.5", } } fn lib_name(&self) -> &'static str { match self { Lua51 => "lua5.1", } } } impl Artifacts { /// Returns the directory containing the Lua headers. pub fn include_dir(&self) -> &Path { &self.include_dir } /// Returns the directory containing the Lua libraries. pub fn lib_dir(&self) -> &Path { &self.lib_dir } /// Returns the names of the Lua libraries built. pub fn libs(&self) -> &[String] { &self.libs } /// Prints the necessary Cargo metadata for linking the Lua libraries. /// /// This method is typically called in a build script to inform Cargo /// about the location of the Lua libraries and how to link them. pub fn print_cargo_metadata(&self) { println!("cargo:rustc-link-search=native={}", self.lib_dir.display()); for lib in self.libs.iter() { println!("cargo:rustc-link-lib=static:-bundle={lib}"); } } } trait ErrorContext { fn context(self, f: impl FnOnce() -> String) -> Result>; } impl ErrorContext for Result { fn context(self, f: impl FnOnce() -> String) -> Result> { self.map_err(|e| format!("{}: {e}", f()).into()) } } trait AddFilesByExt { fn add_files_by_ext(&mut self, dir: &Path, ext: &str) -> Result<&mut Self, Box>; } impl AddFilesByExt for cc::Build { fn add_files_by_ext(&mut self, dir: &Path, ext: &str) -> Result<&mut Self, Box> { for entry in fs::read_dir(dir) .context(|| format!("Cannot read '{}'", dir.display()))? .filter_map(|e| e.ok()) .filter(|e| e.path().extension() == Some(ext.as_ref())) { self.file(entry.path()); } // BENILLA ADDITION. Upstream emits no `rerun-if-changed` for the Lua sources, which is // harmless when they are an immutable registry copy and a genuine trap once they are a // patched fork: editing `lvm.c` then rebuilds NOTHING, the old object file is relinked, and // the change appears not to work. That cost real time on the very first patch here — six // tests failed against a stale VM and the obvious conclusion was that the patch was wrong. // `Build::build` runs inside mlua-sys's build script, so anything printed here is that // script's output and cargo honours it. // // **Emitted for EVERY file in the directory, not just the ones being compiled.** The first // version of this tracked only the `ext` matches — i.e. `.c` — and the identical trap came // straight back one file-type over: flipping `LUA_COMPAT_LSTR` in `luaconf.h` rebuilt // nothing and the new test failed against a stale object file, exactly as before. A header // is an input to every translation unit here; so is anything else that lands in this // directory, which is why this no longer filters. for entry in fs::read_dir(dir) .context(|| format!("Cannot read '{}'", dir.display()))? .filter_map(|e| e.ok()) { println!("cargo:rerun-if-changed={}", entry.path().display()); } Ok(self) } }