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