lua_src/
lib.rs

1use std::env;
2use std::error::Error;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6/// Represents the version of Lua to build.
7#[derive(Debug, PartialEq, Eq)]
8pub enum Version {
9    Lua51,
10    Lua52,
11    Lua53,
12    Lua54,
13    Lua55,
14}
15pub use self::Version::*;
16
17/// Represents the configuration for building Lua artifacts.
18pub struct Build {
19    out_dir: Option<PathBuf>,
20    target: Option<String>,
21    host: Option<String>,
22    opt_level: Option<String>,
23    debug: Option<bool>,
24}
25
26/// Represents the artifacts produced by the build process.
27#[derive(Clone, Debug)]
28pub struct Artifacts {
29    include_dir: PathBuf,
30    lib_dir: PathBuf,
31    libs: Vec<String>,
32}
33
34impl Default for Build {
35    fn default() -> Build {
36        Build {
37            out_dir: env::var_os("OUT_DIR").map(PathBuf::from),
38            target: env::var("TARGET").ok(),
39            host: None,
40            opt_level: None,
41            debug: None,
42        }
43    }
44}
45
46impl Build {
47    /// Creates a new `Build` instance with default settings.
48    pub fn new() -> Build {
49        Build::default()
50    }
51
52    /// Sets the output directory for the build artifacts.
53    ///
54    /// This is required if called outside of a build script.
55    pub fn out_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Build {
56        self.out_dir = Some(path.as_ref().to_path_buf());
57        self
58    }
59
60    /// Sets the target architecture for the build.
61    ///
62    /// This is required if called outside of a build script.
63    pub fn target(&mut self, target: &str) -> &mut Build {
64        self.target = Some(target.to_string());
65        self
66    }
67
68    /// Sets the host architecture for the build.
69    ///
70    /// This is optional and will default to the environment variable `HOST` if not set.
71    /// If called outside of a build script, it will default to the target architecture.
72    pub fn host(&mut self, host: &str) -> &mut Build {
73        self.host = Some(host.to_string());
74        self
75    }
76
77    /// Sets the optimization level for the build.
78    ///
79    /// This is optional and will default to the environment variable `OPT_LEVEL` if not set.
80    /// If called outside of a build script, it will default to `0` in debug mode and `2` otherwise.
81    pub fn opt_level(&mut self, opt_level: &str) -> &mut Build {
82        self.opt_level = Some(opt_level.to_string());
83        self
84    }
85
86    /// Sets whether to build in debug mode.
87    ///
88    /// This is optional and will default to the value of `cfg!(debug_assertions)`.
89    /// If set to `true`, it also enables Lua API checks.
90    pub fn debug(&mut self, debug: bool) -> &mut Build {
91        self.debug = Some(debug);
92        self
93    }
94
95    /// Builds the Lua artifacts for the specified version.
96    pub fn build(&self, version: Version) -> Artifacts {
97        match self.try_build(version) {
98            Ok(artifacts) => artifacts,
99            Err(err) => panic!("{err}"),
100        }
101    }
102
103    /// Attempts to build the Lua artifacts for the specified version.
104    ///
105    /// Returns an error if the build fails.
106    pub fn try_build(&self, version: Version) -> Result<Artifacts, Box<dyn Error>> {
107        let target = self.target.as_ref().ok_or("TARGET is not set")?;
108        let out_dir = self.out_dir.as_ref().ok_or("OUT_DIR is not set")?;
109        let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
110        let mut source_dir = manifest_dir.join(version.source_dir());
111        let lib_dir = out_dir.join("lib");
112        let include_dir = out_dir.join("include");
113
114        if !include_dir.exists() {
115            fs::create_dir_all(&include_dir)
116                .context(|| format!("Cannot create '{}'", include_dir.display()))?;
117        }
118
119        let mut config = cc::Build::new();
120        config.warnings(false).cargo_metadata(false).target(target);
121
122        match &self.host {
123            Some(host) => {
124                config.host(host);
125            }
126            // Host will be taken from the environment variable
127            None if env::var("HOST").is_ok() => {}
128            None => {
129                // If called outside of build script, set default host
130                config.host(target);
131            }
132        }
133
134        let mut libs = vec![version.lib_name().to_string()];
135        match target {
136            _ if target.contains("linux") => {
137                config.define("LUA_USE_LINUX", None);
138            }
139            _ if target.ends_with("bsd") => {
140                config.define("LUA_USE_LINUX", None);
141            }
142            _ if target.contains("apple-darwin") => {
143                match version {
144                    Lua51 => config.define("LUA_USE_LINUX", None),
145                    _ => config.define("LUA_USE_MACOSX", None),
146                };
147            }
148            _ if target.contains("apple-ios") => {
149                match version {
150                    Lua54 | Lua55 => config.define("LUA_USE_IOS", None),
151                    _ => config.define("LUA_USE_POSIX", None),
152                };
153            }
154            _ if target.contains("windows") => {
155                // Defined in Lua >= 5.3
156                config.define("LUA_USE_WINDOWS", None);
157            }
158            _ if target.ends_with("emscripten") => {
159                config
160                    .define("LUA_USE_POSIX", None)
161                    .cpp(true)
162                    .flag("-fexceptions"); // Enable exceptions to be caught
163
164                let cpp_source_dir = out_dir.join("cpp_source");
165                if cpp_source_dir.exists() {
166                    fs::remove_dir_all(&cpp_source_dir)
167                        .context(|| format!("Cannot remove '{}'", cpp_source_dir.display()))?;
168                }
169                fs::create_dir_all(&cpp_source_dir)
170                    .context(|| format!("Cannot create '{}'", cpp_source_dir.display()))?;
171
172                for file in fs::read_dir(&source_dir)
173                    .context(|| format!("Cannot read '{}'", source_dir.display()))?
174                {
175                    let file = file?;
176                    let filename = file.file_name();
177                    let filename = &*filename.to_string_lossy();
178                    let src_file = source_dir.join(file.file_name());
179                    let dst_file = cpp_source_dir.join(file.file_name());
180
181                    let mut content = fs::read(&src_file)
182                        .context(|| format!("Cannot read '{}'", src_file.display()))?;
183                    if ["lauxlib.h", "lua.h", "lualib.h"].contains(&filename) {
184                        content.splice(0..0, b"extern \"C\" {\n".to_vec());
185                        content.extend(b"\n}".to_vec())
186                    }
187                    fs::write(&dst_file, content)
188                        .context(|| format!("Cannot write to '{}'", dst_file.display()))?;
189                }
190                source_dir = cpp_source_dir
191            }
192            _ if target.contains("wasi") => {
193                // WASI is posix-like, but further patches are needed to the Lua
194                // source to get it compiling.
195                config.define("LUA_USE_POSIX", None);
196
197                // Bring in just enough signal-handling support to get Lua at
198                // least compiling, but WASI in general does not support
199                // signals.
200                config.define("_WASI_EMULATED_SIGNAL", None);
201                libs.push("wasi-emulated-signal".to_string());
202
203                // https://github.com/WebAssembly/wasi-sdk/blob/main/SetjmpLongjmp.md
204                // for information about getting setjmp/longjmp working.
205                config.flag("-mllvm").flag("-wasm-enable-eh");
206                config.flag("-mllvm").flag("-wasm-use-legacy-eh=false");
207                config.flag("-mllvm").flag("-wasm-enable-sjlj");
208                libs.push("setjmp".to_string());
209            }
210            _ => Err(format!("don't know how to build Lua for {target}"))?,
211        }
212
213        if let Lua54 = version {
214            config.define("LUA_COMPAT_5_3", None);
215        }
216
217        #[cfg(feature = "ucid")]
218        if let Lua54 | Lua55 = version {
219            config.define("LUA_UCID", None);
220        }
221
222        let debug = self.debug.unwrap_or(cfg!(debug_assertions));
223        if debug {
224            config.define("LUA_USE_APICHECK", None);
225            config.debug(true);
226        }
227
228        match &self.opt_level {
229            Some(opt_level) => {
230                config.opt_level_str(opt_level);
231            }
232            // Opt level will be taken from the environment variable
233            None if env::var("OPT_LEVEL").is_ok() => {}
234            None => {
235                // If called outside of build script, set default opt level
236                config.opt_level(if debug { 0 } else { 2 });
237            }
238        }
239
240        config
241            .include(&source_dir)
242            .warnings(false) // Suppress all warnings
243            .flag_if_supported("-fno-common") // Compile common globals like normal definitions
244            .add_files_by_ext(&source_dir, "c")?
245            .out_dir(&lib_dir)
246            .try_compile(version.lib_name())?;
247
248        for f in &["lauxlib.h", "lua.h", "luaconf.h", "lualib.h"] {
249            let from = source_dir.join(f);
250            let to = include_dir.join(f);
251            fs::copy(&from, &to)
252                .context(|| format!("Cannot copy '{}' to '{}'", from.display(), to.display()))?;
253        }
254
255        Ok(Artifacts {
256            include_dir,
257            lib_dir,
258            libs,
259        })
260    }
261}
262
263impl Version {
264    fn source_dir(&self) -> &'static str {
265        match self {
266            Lua51 => "lua-5.1.5",
267            Lua52 => "lua-5.2.4",
268            Lua53 => "lua-5.3.6",
269            Lua54 => "lua-5.4.8",
270            Lua55 => "lua-5.5.0",
271        }
272    }
273
274    fn lib_name(&self) -> &'static str {
275        match self {
276            Lua51 => "lua5.1",
277            Lua52 => "lua5.2",
278            Lua53 => "lua5.3",
279            Lua54 => "lua5.4",
280            Lua55 => "lua5.5",
281        }
282    }
283}
284
285impl Artifacts {
286    /// Returns the directory containing the Lua headers.
287    pub fn include_dir(&self) -> &Path {
288        &self.include_dir
289    }
290
291    /// Returns the directory containing the Lua libraries.
292    pub fn lib_dir(&self) -> &Path {
293        &self.lib_dir
294    }
295
296    /// Returns the names of the Lua libraries built.
297    pub fn libs(&self) -> &[String] {
298        &self.libs
299    }
300
301    /// Prints the necessary Cargo metadata for linking the Lua libraries.
302    ///
303    /// This method is typically called in a build script to inform Cargo
304    /// about the location of the Lua libraries and how to link them.
305    pub fn print_cargo_metadata(&self) {
306        println!("cargo:rustc-link-search=native={}", self.lib_dir.display());
307        for lib in self.libs.iter() {
308            println!("cargo:rustc-link-lib=static:-bundle={lib}");
309        }
310    }
311}
312
313trait ErrorContext<T> {
314    fn context(self, f: impl FnOnce() -> String) -> Result<T, Box<dyn Error>>;
315}
316
317impl<T, E: Error> ErrorContext<T> for Result<T, E> {
318    fn context(self, f: impl FnOnce() -> String) -> Result<T, Box<dyn Error>> {
319        self.map_err(|e| format!("{}: {e}", f()).into())
320    }
321}
322
323trait AddFilesByExt {
324    fn add_files_by_ext(&mut self, dir: &Path, ext: &str) -> Result<&mut Self, Box<dyn Error>>;
325}
326
327impl AddFilesByExt for cc::Build {
328    fn add_files_by_ext(&mut self, dir: &Path, ext: &str) -> Result<&mut Self, Box<dyn Error>> {
329        for entry in fs::read_dir(dir)
330            .context(|| format!("Cannot read '{}'", dir.display()))?
331            .filter_map(|e| e.ok())
332            .filter(|e| e.path().extension() == Some(ext.as_ref()))
333        {
334            self.file(entry.path());
335        }
336        Ok(self)
337    }
338}