luajit_src/
lib.rs

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