Skip to main content

luajit_src/
lib.rs

1use std::error::Error;
2use std::fmt::Display;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5use std::{env, fs, io};
6
7type DynError = Box<dyn Error + Send + Sync>;
8
9/// Represents the configuration for building LuaJIT artifacts.
10pub struct Build {
11    out_dir: Option<PathBuf>,
12    target: Option<String>,
13    host: Option<String>,
14    lua52compat: bool,
15    debug: Option<bool>,
16}
17
18/// Represents the artifacts produced by the build process.
19pub struct Artifacts {
20    include_dir: PathBuf,
21    lib_dir: PathBuf,
22    libs: Vec<String>,
23}
24
25impl Default for Build {
26    fn default() -> Self {
27        Build {
28            out_dir: env::var_os("OUT_DIR").map(PathBuf::from),
29            target: env::var("TARGET").ok(),
30            host: env::var("HOST").ok(),
31            lua52compat: false,
32            debug: None,
33        }
34    }
35}
36
37impl Build {
38    /// Creates a new `Build` instance with default settings.
39    pub fn new() -> Build {
40        Build::default()
41    }
42
43    /// Sets the output directory for the build artifacts.
44    ///
45    /// This is required if called outside of a build script.
46    pub fn out_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Build {
47        self.out_dir = Some(path.as_ref().to_path_buf());
48        self
49    }
50
51    /// Sets the target architecture for the build.
52    ///
53    /// This is required if called outside of a build script.
54    pub fn target(&mut self, target: &str) -> &mut Build {
55        self.target = Some(target.to_string());
56        self
57    }
58
59    /// Sets the host architecture for the build.
60    ///
61    /// This is optional and will default to the environment variable `HOST` if not set.
62    /// If called outside of a build script, it will default to the target architecture.
63    pub fn host(&mut self, host: &str) -> &mut Build {
64        self.host = Some(host.to_string());
65        self
66    }
67
68    /// Enables or disables Lua 5.2 limited compatibility mode.
69    pub fn lua52compat(&mut self, enabled: bool) -> &mut Build {
70        self.lua52compat = enabled;
71        self
72    }
73
74    /// Sets whether to build LuaJIT in debug mode.
75    ///
76    /// This is optional and will default to the value of `cfg!(debug_assertions)`.
77    /// If set to `true`, it also enables Lua API checks.
78    pub fn debug(&mut self, debug: bool) -> &mut Build {
79        self.debug = Some(debug);
80        self
81    }
82
83    fn cmd_make(&self) -> Command {
84        match &self.host.as_ref().expect("HOST is not set")[..] {
85            "x86_64-unknown-dragonfly" => Command::new("gmake"),
86            "x86_64-unknown-freebsd" => Command::new("gmake"),
87            _ => Command::new("make"),
88        }
89    }
90
91    /// Builds the LuaJIT artifacts.
92    pub fn build(&mut self) -> Artifacts {
93        self.try_build().expect("LuaJIT build failed")
94    }
95
96    /// Attempts to build the LuaJIT artifacts.
97    ///
98    /// Returns an error if the build fails.
99    pub fn try_build(&mut self) -> Result<Artifacts, DynError> {
100        let target = &self.target.as_ref().expect("TARGET is not set")[..];
101
102        if target.contains("msvc") {
103            return self.build_msvc();
104        }
105
106        self.build_unix()
107    }
108
109    fn build_unix(&mut self) -> Result<Artifacts, DynError> {
110        let target = &self.target.as_ref().expect("TARGET is not set")[..];
111        let host = &self.host.as_ref().expect("HOST is not set")[..];
112        let out_dir = self.out_dir.as_ref().expect("OUT_DIR is not set");
113        let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
114        let source_dir = manifest_dir.join("luajit2");
115        let build_dir = out_dir.join("luajit-build");
116        let lib_dir = out_dir.join("lib");
117        let include_dir = out_dir.join("include");
118
119        // Cleanup
120        for dir in [&build_dir, &lib_dir, &include_dir] {
121            if dir.exists() {
122                fs::remove_dir_all(dir).context(|| format!("Cannot remove {}", dir.display()))?;
123            }
124            fs::create_dir_all(dir).context(|| format!("Cannot create {}", dir.display()))?;
125        }
126        cp_r(&source_dir, &build_dir)?;
127
128        // Copy release version file
129        let relver = build_dir.join(".relver");
130        fs::copy(manifest_dir.join("luajit_relver.txt"), &relver)
131            .context(|| "Cannot copy 'luajit_relver.txt'")?;
132
133        // Fix permissions for certain build situations
134        let mut perms = (fs::metadata(&relver).map(|md| md.permissions()))
135            .context(|| format!("Cannot read permissions for '{}'", relver.display()))?;
136        #[allow(clippy::permissions_set_readonly_false)]
137        perms.set_readonly(false);
138        fs::set_permissions(&relver, perms)
139            .context(|| format!("Cannot set permissions for '{}'", relver.display()))?;
140
141        let mut cc = cc::Build::new();
142        cc.warnings(false);
143        let compiler = cc.get_compiler();
144        let compiler_path = compiler.path().to_str().unwrap();
145
146        let mut make = self.cmd_make();
147        make.current_dir(build_dir.join("src"));
148        make.arg("-e");
149
150        match target {
151            "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
152                make.env("MACOSX_DEPLOYMENT_TARGET", "10.14");
153            }
154            "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
155                make.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
156            }
157            _ if target.contains("linux") => {
158                make.env("TARGET_SYS", "Linux");
159            }
160            _ if target.contains("windows") => {
161                make.env("TARGET_SYS", "Windows");
162            }
163            _ => {}
164        }
165
166        let target_pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap();
167        if target_pointer_width == "32" && env::var_os("HOST_CC").is_none() {
168            // 32-bit cross-compilation?
169            let host_cc = cc::Build::new().target(host).get_compiler();
170            make.env("HOST_CC", format!("{} -m32", host_cc.path().display()));
171        }
172
173        // Infer ar/ranlib tools from cross compilers if the it looks like
174        // we're doing something like `foo-gcc` route that to `foo-ranlib`
175        // as well.
176        let prefix = if compiler_path.ends_with("-gcc") {
177            &compiler_path[..compiler_path.len() - 3]
178        } else if compiler_path.ends_with("-clang") {
179            &compiler_path[..compiler_path.len() - 5]
180        } else {
181            ""
182        };
183
184        let compiler_path =
185            which::which(compiler_path).context(|| format!("Cannot find {compiler_path}"))?;
186        let bindir = compiler_path.parent().unwrap();
187        let compiler_path = compiler_path.to_str().unwrap();
188        let compiler_args = compiler.cflags_env();
189        let compiler_args = compiler_args.to_str().unwrap();
190        if env::var_os("STATIC_CC").is_none() {
191            make.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
192        }
193        if env::var_os("TARGET_LD").is_none() {
194            make.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
195        }
196
197        // Find ar
198        if env::var_os("TARGET_AR").is_none() {
199            let mut ar = if bindir.join(format!("{prefix}ar")).is_file() {
200                bindir.join(format!("{prefix}ar")).into_os_string()
201            } else if compiler.is_like_clang() && bindir.join("llvm-ar").is_file() {
202                bindir.join("llvm-ar").into_os_string()
203            } else if compiler.is_like_gnu() && bindir.join("ar").is_file() {
204                bindir.join("ar").into_os_string()
205            } else if let Ok(ar) = which::which(format!("{prefix}ar")) {
206                ar.into_os_string()
207            } else {
208                panic!("cannot find {prefix}ar")
209            };
210            ar.push(" rcus");
211            make.env("TARGET_AR", ar);
212        }
213
214        // Find strip
215        if env::var_os("TARGET_STRIP").is_none() {
216            let strip = if bindir.join(format!("{prefix}strip")).is_file() {
217                bindir.join(format!("{prefix}strip"))
218            } else if compiler.is_like_clang() && bindir.join("llvm-strip").is_file() {
219                bindir.join("llvm-strip")
220            } else if compiler.is_like_gnu() && bindir.join("strip").is_file() {
221                bindir.join("strip")
222            } else if let Ok(strip) = which::which(format!("{prefix}strip")) {
223                strip
224            } else {
225                panic!("cannot find {prefix}strip")
226            };
227            make.env("TARGET_STRIP", strip);
228        }
229
230        let mut xcflags = vec!["-fPIC"];
231        if self.lua52compat {
232            xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
233        }
234
235        let debug = self.debug.unwrap_or(cfg!(debug_assertions));
236        if debug {
237            make.env("CCDEBUG", "-g");
238            xcflags.push("-DLUA_USE_ASSERT");
239            xcflags.push("-DLUA_USE_APICHECK");
240        }
241
242        make.env("BUILDMODE", "static");
243        make.env("XCFLAGS", xcflags.join(" "));
244        self.run_command(&mut make)
245            .context(|| format!("Error running '{make:?}'"))?;
246
247        Artifacts::make(&build_dir, &include_dir, &lib_dir, false)
248    }
249
250    fn build_msvc(&mut self) -> Result<Artifacts, DynError> {
251        let target = &self.target.as_ref().expect("TARGET is not set")[..];
252        let out_dir = self.out_dir.as_ref().expect("OUT_DIR is not set");
253        let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
254        let source_dir = manifest_dir.join("luajit2");
255        let build_dir = out_dir.join("luajit-build");
256        let lib_dir = out_dir.join("lib");
257        let include_dir = out_dir.join("include");
258
259        // Cleanup
260        for dir in [&build_dir, &lib_dir, &include_dir] {
261            if dir.exists() {
262                fs::remove_dir_all(dir).context(|| format!("Cannot remove {}", dir.display()))?;
263            }
264            fs::create_dir_all(dir).context(|| format!("Cannot create {}", dir.display()))?;
265        }
266        cp_r(&source_dir, &build_dir)?;
267
268        // Copy release version file
269        let relver = build_dir.join(".relver");
270        fs::copy(manifest_dir.join("luajit_relver.txt"), &relver)
271            .context(|| "Cannot copy 'luajit_relver.txt'")?;
272
273        let mut msvcbuild = Command::new(build_dir.join("src").join("msvcbuild.bat"));
274        msvcbuild.current_dir(build_dir.join("src"));
275
276        if self.lua52compat {
277            msvcbuild.arg("lua52compat");
278        }
279        if self.debug.unwrap_or(cfg!(debug_assertions)) {
280            msvcbuild.arg("debug");
281        }
282        msvcbuild.arg("static");
283
284        let cl = cc::windows_registry::find_tool(target, "cl.exe").expect("failed to find cl");
285        for (k, v) in cl.env() {
286            msvcbuild.env(k, v);
287        }
288
289        self.run_command(&mut msvcbuild)
290            .context(|| format!("Error running'{msvcbuild:?}'"))?;
291
292        Artifacts::make(&build_dir, &include_dir, &lib_dir, true)
293    }
294
295    fn run_command(&self, command: &mut Command) -> io::Result<()> {
296        let status = command.status()?;
297        if !status.success() {
298            return Err(io::Error::other(format!("exited with status {status}")));
299        }
300        Ok(())
301    }
302}
303
304fn cp_r(src: &Path, dst: &Path) -> Result<(), DynError> {
305    for f in fs::read_dir(src).context(|| format!("Cannot read directory '{}'", src.display()))? {
306        let f = f.context(|| format!("Cannot read entry in '{}'", src.display()))?;
307        let path = f.path();
308        let name = path.file_name().unwrap();
309
310        // Skip git metadata
311        if name.to_str() == Some(".git") {
312            continue;
313        }
314
315        let dst = dst.join(name);
316        if f.file_type().unwrap().is_dir() {
317            fs::create_dir_all(&dst)
318                .context(|| format!("Cannot create directory '{}'", dst.display()))?;
319            cp_r(&path, &dst)?;
320        } else {
321            let _ = fs::remove_file(&dst);
322            fs::copy(&path, &dst)
323                .context(|| format!("Cannot copy '{}' to '{}'", path.display(), dst.display()))?;
324        }
325    }
326    Ok(())
327}
328
329impl Artifacts {
330    /// Returns the directory containing the LuaJIT headers.
331    pub fn include_dir(&self) -> &Path {
332        &self.include_dir
333    }
334
335    /// Returns the directory containing the LuaJIT libraries.
336    pub fn lib_dir(&self) -> &Path {
337        &self.lib_dir
338    }
339
340    /// Returns the names of the LuaJIT libraries built.
341    pub fn libs(&self) -> &[String] {
342        &self.libs
343    }
344
345    /// Prints the necessary Cargo metadata for linking the LuaJIT libraries.
346    ///
347    /// This method is typically called in a build script to inform Cargo
348    /// about the location of the LuaJIT libraries and how to link them.
349    pub fn print_cargo_metadata(&self) {
350        println!("cargo:rerun-if-env-changed=HOST_CC");
351        println!("cargo:rerun-if-env-changed=STATIC_CC");
352        println!("cargo:rerun-if-env-changed=TARGET_LD");
353        println!("cargo:rerun-if-env-changed=TARGET_AR");
354        println!("cargo:rerun-if-env-changed=TARGET_STRIP");
355        println!("cargo:rerun-if-env-changed=MACOSX_DEPLOYMENT_TARGET");
356
357        println!("cargo:rustc-link-search=native={}", self.lib_dir.display());
358        for lib in self.libs.iter() {
359            println!("cargo:rustc-link-lib=static={lib}");
360        }
361    }
362
363    fn make(
364        build_dir: &Path,
365        include_dir: &Path,
366        lib_dir: &Path,
367        is_msvc: bool,
368    ) -> Result<Self, DynError> {
369        for f in &["lauxlib.h", "lua.h", "luaconf.h", "luajit.h", "lualib.h"] {
370            let from = build_dir.join("src").join(f);
371            let to = include_dir.join(f);
372            fs::copy(&from, &to)
373                .context(|| format!("Cannot copy '{}' to '{}'", from.display(), to.display()))?;
374        }
375
376        let lib_name = if !is_msvc { "luajit" } else { "lua51" };
377        let lib_file = if !is_msvc { "libluajit.a" } else { "lua51.lib" };
378        if build_dir.join("src").join(lib_file).exists() {
379            let from = build_dir.join("src").join(lib_file);
380            let to = lib_dir.join(lib_file);
381            fs::copy(&from, &to)
382                .context(|| format!("Cannot copy '{}' to '{}'", from.display(), to.display()))?;
383        }
384
385        Ok(Artifacts {
386            lib_dir: lib_dir.to_path_buf(),
387            include_dir: include_dir.to_path_buf(),
388            libs: vec![lib_name.to_string()],
389        })
390    }
391}
392
393trait ErrorContext<T> {
394    fn context<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError>;
395}
396
397impl<T, E: Error> ErrorContext<T> for Result<T, E> {
398    fn context<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError> {
399        self.map_err(|e| format!("{}: {e}", f()).into())
400    }
401}