Skip to main content

lux_lib/operations/
build_lua.rs

1use std::{
2    env,
3    io::{self, Cursor},
4    path::Path,
5    process::{ExitStatus, Stdio},
6};
7
8use crate::{
9    build::utils,
10    config::Config,
11    hash::HasIntegrity,
12    lua_version::LuaVersion,
13    operations::{self, UnpackError},
14    progress::{Progress, ProgressBar},
15};
16use bon::Builder;
17use git2::{build::RepoBuilder, FetchOptions};
18use path_slash::PathExt;
19use ssri::Integrity;
20use target_lexicon::Triple;
21use tempfile::tempdir;
22use thiserror::Error;
23use tokio::{fs, process::Command};
24use url::Url;
25
26const LUA51_VERSION: &str = "5.1.5";
27const LUA51_HASH: &str = "sha256-JkD8VqeV8p0o7xXhPDSkfiI5YLAkDoywqC2bBzhpUzM=";
28const LUA52_VERSION: &str = "5.2.4";
29const LUA52_HASH: &str = "sha256-ueLkqtZ4mztjoFbUQveznw7Pyjrg8fwK5OlhRAG2n0s=";
30const LUA53_VERSION: &str = "5.3.6";
31const LUA53_HASH: &str = "sha256-/F/Wm7hzYyPwJmcrG3I12mE9cXfnJViJOgvc0yBGbWA=";
32const LUA54_VERSION: &str = "5.4.8";
33const LUA54_HASH: &str = "sha256-TxjdrhVOeT5G7qtyfFnvHAwMK3ROe5QhlxDXb1MGKa4=";
34const LUA55_VERSION: &str = "5.5.0";
35const LUA55_HASH: &str = "sha256-V8zDK7vQBcq3W8xSREBSU1r2kXiduiuQFtXFBkDWiz0=";
36// XXX: there's no tag with lua 5.2 compatibility, so we have to use the v2.1 branch for now
37// this is unstable and might break the build.
38const LUAJIT_MM_VERSION: &str = "2.1";
39
40#[derive(Builder)]
41#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
42pub struct BuildLua<'a> {
43    lua_version: &'a LuaVersion,
44    install_dir: &'a Path,
45    config: &'a Config,
46    progress: &'a Progress<ProgressBar>,
47}
48
49#[derive(Debug, Error)]
50pub enum BuildLuaError {
51    #[error(transparent)]
52    Request(#[from] reqwest::Error),
53    #[error(transparent)]
54    Io(#[from] io::Error),
55    #[error(transparent)]
56    Unpack(#[from] UnpackError),
57    #[error(transparent)]
58    Git(#[from] git2::Error),
59    #[error(transparent)]
60    CC(#[from] cc::Error),
61    #[error("failed to find cl.exe")]
62    ClNotFound,
63    #[error("failed to find LINK.exe")]
64    LinkNotFound,
65    #[error("source integrity mismatch.\nExpected: {expected},\nbut got: {actual}")]
66    SourceIntegrityMismatch {
67        expected: Integrity,
68        actual: Integrity,
69    },
70    #[error("{name} failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
71    CommandFailure {
72        name: String,
73        status: ExitStatus,
74        stdout: String,
75        stderr: String,
76    },
77}
78
79impl<State: build_lua_builder::State + build_lua_builder::IsComplete> BuildLuaBuilder<'_, State> {
80    pub async fn build(self) -> Result<(), BuildLuaError> {
81        let args = self._build();
82        let lua_version = args.lua_version;
83        match lua_version {
84            LuaVersion::Lua51
85            | LuaVersion::Lua52
86            | LuaVersion::Lua53
87            | LuaVersion::Lua54
88            | LuaVersion::Lua55 => do_build_lua(args).await,
89            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => do_build_luajit(args).await,
90        }
91    }
92}
93
94async fn do_build_luajit(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
95    let progress = args.progress;
96
97    let build_dir = tempdir().map_err(|err| {
98        io::Error::other(format!(
99            "failed to create lua_installation temp directory:\n{}",
100            err
101        ))
102    })?;
103    // XXX luajit.org responds with an invalid content-type, so we'll use the github mirror for now.
104    // let luajit_url = "https://luajit.org/git/luajit.git";
105    let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
106    progress.map(|p| p.set_message(format!("🦠 Cloning {luajit_url}")));
107    {
108        // We create a new scope because we have to drop fetch_options before the await
109        let mut fetch_options = FetchOptions::new();
110        fetch_options.update_fetchhead(false);
111        let mut repo_builder = RepoBuilder::new();
112        repo_builder.fetch_options(fetch_options);
113        let repo = repo_builder.clone(luajit_url, build_dir.path())?;
114        let (object, _) = repo.revparse_ext(&format!("v{LUAJIT_MM_VERSION}"))?;
115        repo.checkout_tree(&object, None)?;
116    }
117    if cfg!(target_env = "msvc") {
118        do_build_luajit_msvc(args, build_dir.path()).await
119    } else {
120        do_build_luajit_unix(args, build_dir.path()).await
121    }
122}
123
124async fn do_build_luajit_unix(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
125    let lua_version = args.lua_version;
126    let config = args.config;
127    let install_dir = args.install_dir;
128    let progress = args.progress;
129    progress.map(|p| p.set_message(format!("🛠️ Building Luajit {LUAJIT_MM_VERSION}")));
130
131    let host = Triple::host();
132
133    let mut cc = cc::Build::new();
134    cc.cargo_output(false)
135        .cargo_metadata(false)
136        .cargo_warnings(false)
137        .warnings(config.verbose())
138        .opt_level(3)
139        .host(&host.to_string())
140        .target(&host.to_string());
141    let compiler = cc.try_get_compiler()?;
142    let compiler_path = compiler.path().to_slash_lossy().to_string();
143    let mut make_cmd = Command::new(config.make_cmd());
144    make_cmd.current_dir(build_dir.join("src"));
145    make_cmd.arg("-e");
146    make_cmd.stdout(Stdio::piped());
147    make_cmd.stderr(Stdio::piped());
148    let target = host.to_string();
149    match target.as_str() {
150        "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
151            make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.11");
152        }
153        "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
154            make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
155        }
156        _ if target.contains("linux") => {
157            make_cmd.env("TARGET_SYS", "Linux");
158        }
159        _ => {}
160    }
161    let compiler_path = which::which(&compiler_path)
162        .map_err(|err| io::Error::other(format!("cannot find {}:\n{}", &compiler_path, err)))?;
163    let compiler_path = compiler_path.to_slash_lossy().to_string();
164    let compiler_args = compiler.cflags_env();
165    let compiler_args = compiler_args.to_string_lossy();
166    if env::var_os("STATIC_CC").is_none() {
167        make_cmd.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
168    }
169    if env::var_os("TARGET_LD").is_none() {
170        make_cmd.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
171    }
172    let mut xcflags = vec!["-fPIC"];
173    if lua_version == &LuaVersion::LuaJIT52 {
174        xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
175    }
176    if cfg!(debug_assertions) {
177        xcflags.push("-DLUA_USE_ASSERT");
178        xcflags.push("-DLUA_USE_APICHECK");
179    }
180    make_cmd.env("BUILDMODE", "static");
181    make_cmd.env("XCFLAGS", xcflags.join(" "));
182
183    match make_cmd.output().await {
184        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
185        Ok(output) => {
186            return Err(BuildLuaError::CommandFailure {
187                name: "build".into(),
188                status: output.status,
189                stdout: String::from_utf8_lossy(&output.stdout).into(),
190                stderr: String::from_utf8_lossy(&output.stderr).into(),
191            });
192        }
193        Err(err) => {
194            return Err(BuildLuaError::Io(io::Error::other(format!(
195                "Failed to run `{} build`:\n{}",
196                config.make_cmd(),
197                err,
198            ))));
199        }
200    };
201
202    progress.map(|p| p.set_message(format!("💻 Installing Luajit {LUAJIT_MM_VERSION}")));
203
204    match Command::new(config.make_cmd())
205        .current_dir(build_dir)
206        .stdout(Stdio::piped())
207        .stderr(Stdio::piped())
208        .arg("install")
209        .arg(format!(r#"PREFIX="{}""#, install_dir.display()))
210        .output()
211        .await
212    {
213        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
214        Ok(output) => {
215            return Err(BuildLuaError::CommandFailure {
216                name: "install".into(),
217                status: output.status,
218                stdout: String::from_utf8_lossy(&output.stdout).into(),
219                stderr: String::from_utf8_lossy(&output.stderr).into(),
220            });
221        }
222        Err(err) => {
223            return Err(BuildLuaError::Io(io::Error::other(format!(
224                "Failed to run `{} install`:\n{}",
225                config.make_cmd(),
226                err,
227            ))));
228        }
229    };
230    move_luajit_includes(install_dir).await?;
231    Ok(())
232}
233
234/// luajit installs the includes to a subdirectory.
235/// For consistency, we want them in the `include` directory
236async fn move_luajit_includes(install_dir: &Path) -> io::Result<()> {
237    let include_dir = install_dir.join("include");
238    let include_subdir = include_dir.join(format!("luajit-{LUAJIT_MM_VERSION}"));
239    if !include_subdir.is_dir() {
240        return Ok(());
241    }
242    let mut dir = fs::read_dir(&include_subdir).await.map_err(|err| {
243        io::Error::other(format!(
244            "Failed to read {}:\n{}",
245            include_subdir.display(),
246            err
247        ))
248    })?;
249    while let Some(entry) = dir.next_entry().await? {
250        let file_name = entry.file_name();
251        let src_path = entry.path();
252        let dest_path = include_dir.join(&file_name);
253        fs::copy(&src_path, &dest_path).await.map_err(|err| {
254            io::Error::other(format!(
255                "error copying {} to {}:\n{}",
256                src_path.display(),
257                dest_path.display(),
258                err
259            ))
260        })?;
261    }
262    fs::remove_dir_all(&include_subdir).await.map_err(|err| {
263        io::Error::other(format!(
264            "Failed to remove {}:\n{}",
265            include_subdir.display(),
266            err
267        ))
268    })?;
269    Ok(())
270}
271
272async fn do_build_luajit_msvc(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
273    let lua_version = args.lua_version;
274    let config = args.config;
275    let install_dir = args.install_dir;
276    let lib_dir = install_dir.join("lib");
277    fs::create_dir_all(&lib_dir).await.map_err(|err| {
278        io::Error::other(format!(
279            "Failed to create directory {}:\n{}",
280            lib_dir.display(),
281            err
282        ))
283    })?;
284    let include_dir = install_dir.join("include");
285    fs::create_dir_all(&include_dir).await.map_err(|err| {
286        io::Error::other(format!(
287            "Failed to create directory {}:\n{}",
288            include_dir.display(),
289            err
290        ))
291    })?;
292    let bin_dir = install_dir.join("bin");
293    fs::create_dir_all(&bin_dir).await.map_err(|err| {
294        io::Error::other(format!(
295            "Failed to create directory {}:\n{}",
296            bin_dir.display(),
297            err
298        ))
299    })?;
300
301    let progress = args.progress;
302
303    progress.map(|p| p.set_message(format!("🛠️ Building Luajit {LUAJIT_MM_VERSION}")));
304
305    let src_dir = build_dir.join("src");
306    let mut msvcbuild = Command::new(src_dir.join("msvcbuild.bat"));
307    msvcbuild.current_dir(&src_dir);
308    if lua_version == &LuaVersion::LuaJIT52 {
309        msvcbuild.arg("lua52compat");
310    }
311    msvcbuild.arg("static");
312    let host = Triple::host();
313    let target = host.to_string();
314    let cl = cc::windows_registry::find_tool(&target, "cl.exe").ok_or(BuildLuaError::ClNotFound)?;
315    for (k, v) in cl.env() {
316        msvcbuild.env(k, v);
317    }
318    fs::create_dir_all(&install_dir).await.map_err(|err| {
319        io::Error::other(format!(
320            "Failed to create directory {}:\n{}",
321            install_dir.display(),
322            err
323        ))
324    })?;
325    match msvcbuild.output().await {
326        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
327        Ok(output) => {
328            return Err(BuildLuaError::CommandFailure {
329                name: "build".into(),
330                status: output.status,
331                stdout: String::from_utf8_lossy(&output.stdout).into(),
332                stderr: String::from_utf8_lossy(&output.stderr).into(),
333            });
334        }
335        Err(err) => {
336            return Err(BuildLuaError::Io(io::Error::other(format!(
337                "Failed to run msvcbuild.bat:\n{}",
338                err,
339            ))))
340        }
341    };
342
343    progress.map(|p| p.set_message(format!("💻 Installing Luajit {LUAJIT_MM_VERSION}")));
344    copy_includes(&src_dir, &include_dir).await?;
345    fs::copy(src_dir.join("lua51.lib"), lib_dir.join("luajit.lib"))
346        .await
347        .map_err(|err| {
348            io::Error::other(format!(
349                "Failed to rename lua51.lib to luajit.lib:\n{}",
350                err
351            ))
352        })?;
353    fs::copy(src_dir.join("luajit.exe"), bin_dir.join("luajit.exe"))
354        .await
355        .map_err(|err| {
356            io::Error::other(format!(
357                "Failed to install luajit.exe to {}:\n{}",
358                bin_dir.display(),
359                err
360            ))
361        })?;
362    Ok(())
363}
364
365async fn do_build_lua(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
366    let lua_version = args.lua_version;
367    let progress = args.progress;
368
369    let build_dir = tempdir().map_err(|err| {
370        io::Error::other(format!(
371            "failed to create lua_installation temp directory:\n{}",
372            err
373        ))
374    })?;
375
376    let (source_integrity, pkg_version): (Integrity, &str) = unsafe {
377        match lua_version {
378            LuaVersion::Lua51 => (LUA51_HASH.parse().unwrap_unchecked(), LUA51_VERSION),
379            LuaVersion::Lua52 => (LUA52_HASH.parse().unwrap_unchecked(), LUA52_VERSION),
380            LuaVersion::Lua53 => (LUA53_HASH.parse().unwrap_unchecked(), LUA53_VERSION),
381            LuaVersion::Lua54 => (LUA54_HASH.parse().unwrap_unchecked(), LUA54_VERSION),
382            LuaVersion::Lua55 => (LUA55_HASH.parse().unwrap_unchecked(), LUA55_VERSION),
383            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
384        }
385    };
386
387    let file_name = format!("lua-{pkg_version}.tar.gz");
388
389    let source_url: Url = unsafe {
390        format!("https://www.lua.org/ftp/{file_name}")
391            .parse()
392            .unwrap_unchecked()
393    };
394
395    progress.map(|p| p.set_message(format!("📥 Downloading {}", &source_url)));
396
397    let response = reqwest::Client::new()
398        .get(source_url)
399        .send()
400        .await?
401        .error_for_status()?
402        .bytes()
403        .await?;
404
405    let hash = response.hash()?;
406
407    if hash.matches(&source_integrity).is_none() {
408        return Err(BuildLuaError::SourceIntegrityMismatch {
409            expected: source_integrity,
410            actual: hash,
411        });
412    }
413
414    let cursor = Cursor::new(response);
415    let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
416    operations::unpack::unpack(
417        mime_type,
418        cursor,
419        true,
420        file_name,
421        build_dir.path(),
422        progress,
423    )
424    .await?;
425
426    if cfg!(target_env = "msvc") {
427        do_build_lua_msvc(args, build_dir.path(), lua_version, pkg_version).await
428    } else {
429        do_build_lua_unix(args, build_dir.path(), lua_version, pkg_version).await
430    }
431}
432
433async fn do_build_lua_unix(
434    args: BuildLua<'_>,
435    build_dir: &Path,
436    lua_version: &LuaVersion,
437    pkg_version: &str,
438) -> Result<(), BuildLuaError> {
439    let config = args.config;
440    let progress = args.progress;
441    let install_dir = args.install_dir;
442
443    progress.map(|p| p.set_message(format!("🛠️ Building Lua {}", &pkg_version)));
444
445    let build_target = if cfg!(target_os = "linux") && matches!(&lua_version, LuaVersion::Lua54) {
446        "linux"
447    } else {
448        // For other lua versions, the "linux" target uses readline.
449        "generic"
450    };
451
452    match Command::new(config.make_cmd())
453        .current_dir(build_dir)
454        .stdout(Stdio::piped())
455        .stderr(Stdio::piped())
456        .arg(build_target)
457        .output()
458        .await
459    {
460        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
461        Ok(output) => {
462            return Err(BuildLuaError::CommandFailure {
463                name: "build".into(),
464                status: output.status,
465                stdout: String::from_utf8_lossy(&output.stdout).into(),
466                stderr: String::from_utf8_lossy(&output.stderr).into(),
467            });
468        }
469        Err(err) => {
470            return Err(BuildLuaError::Io(io::Error::other(format!(
471                "Failed to run `{} build`:\n{}",
472                config.make_cmd(),
473                err,
474            ))))
475        }
476    };
477
478    progress.map(|p| p.set_message(format!("💻 Installing Lua {}", &pkg_version)));
479
480    match Command::new(config.make_cmd())
481        .current_dir(build_dir)
482        .stdout(Stdio::piped())
483        .stderr(Stdio::piped())
484        .arg("install")
485        .arg(format!(r#"INSTALL_TOP="{}""#, install_dir.display()))
486        .output()
487        .await
488    {
489        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
490        Ok(output) => {
491            return Err(BuildLuaError::CommandFailure {
492                name: "install".into(),
493                status: output.status,
494                stdout: String::from_utf8_lossy(&output.stdout).into(),
495                stderr: String::from_utf8_lossy(&output.stderr).into(),
496            });
497        }
498        Err(err) => {
499            return Err(BuildLuaError::Io(io::Error::other(format!(
500                "Failed to run `{} install`:\n{}",
501                config.make_cmd(),
502                err,
503            ))))
504        }
505    };
506
507    Ok(())
508}
509
510async fn do_build_lua_msvc(
511    args: BuildLua<'_>,
512    build_dir: &Path,
513    lua_version: &LuaVersion,
514    pkg_version: &str,
515) -> Result<(), BuildLuaError> {
516    let config = args.config;
517    let progress = args.progress;
518    let install_dir = args.install_dir;
519
520    progress.map(|p| p.set_message(format!("🛠️ Building Lua {}", &pkg_version)));
521
522    let lib_dir = install_dir.join("lib");
523    fs::create_dir_all(&lib_dir).await.map_err(|err| {
524        io::Error::other(format!(
525            "Failed to create directory {}:\n{}",
526            lib_dir.display(),
527            err
528        ))
529    })?;
530    let include_dir = install_dir.join("include");
531    fs::create_dir_all(&include_dir).await.map_err(|err| {
532        io::Error::other(format!(
533            "Failed to create directory {}:\n{}",
534            include_dir.display(),
535            err
536        ))
537    })?;
538    let bin_dir = install_dir.join("bin");
539    fs::create_dir_all(&bin_dir).await.map_err(|err| {
540        io::Error::other(format!(
541            "Failed to create directory {}:\n{}",
542            bin_dir.display(),
543            err
544        ))
545    })?;
546
547    let src_dir = build_dir.join("src");
548
549    let lib_name = match lua_version {
550        LuaVersion::Lua51 => "lua5.1",
551        LuaVersion::Lua52 => "lua5.2",
552        LuaVersion::Lua53 => "lua5.3",
553        LuaVersion::Lua54 => "lua5.4",
554        LuaVersion::Lua55 => "lua5.5",
555        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
556    };
557    let host = Triple::host();
558    let mut cc = cc::Build::new();
559    cc.cargo_output(false)
560        .cargo_metadata(false)
561        .cargo_warnings(false)
562        .warnings(config.verbose())
563        .opt_level(3)
564        .host(&host.to_string())
565        .target(&host.to_string());
566
567    cc.define("LUA_USE_WINDOWS", None);
568
569    let mut lib_c_files = Vec::new();
570    let mut read_dir = fs::read_dir(&src_dir).await.map_err(|err| {
571        io::Error::other(format!(
572            "Failed to read directory {}:\n{}",
573            src_dir.display(),
574            err
575        ))
576    })?;
577    while let Some(entry) = read_dir.next_entry().await? {
578        let path = entry.path();
579        if path.extension().is_some_and(|ext| ext == "c")
580            && path
581                .with_extension("")
582                .file_name()
583                .is_some_and(|name| name != "lua" && name != "luac")
584        {
585            lib_c_files.push(path);
586        }
587    }
588    cc.include(&src_dir)
589        .files(lib_c_files)
590        .out_dir(&lib_dir)
591        .try_compile(lib_name)?;
592
593    let bin_objects = cc
594        .include(&src_dir)
595        .file(src_dir.join("lua.c"))
596        .file(src_dir.join("luac.c"))
597        .out_dir(&src_dir)
598        .try_compile_intermediates()?;
599
600    progress.map(|p| p.set_message(format!("💻 Installing Lua {}", &pkg_version)));
601
602    let target = host.to_string();
603    let link =
604        cc::windows_registry::find_tool(&target, "link.exe").ok_or(BuildLuaError::LinkNotFound)?;
605
606    for name in ["lua", "luac"] {
607        let bin = bin_dir.join(format!("{name}.exe"));
608        let objects = bin_objects.iter().filter(|file| {
609            file.with_extension("").file_name().is_some_and(|fname| {
610                fname
611                    .to_string_lossy()
612                    .to_string()
613                    .ends_with(&format!("-{name}"))
614            })
615        });
616        match Command::new(link.path())
617            .arg(format!("/OUT:{}", bin.display()))
618            .args(objects)
619            .arg(format!("{}.lib", lib_dir.join(lib_name).display()))
620            .output()
621            .await
622        {
623            Ok(output) if output.status.success() => utils::log_command_output(&output, config),
624            Ok(output) => {
625                return Err(BuildLuaError::CommandFailure {
626                    name: format!("install {name}.exe"),
627                    status: output.status,
628                    stdout: String::from_utf8_lossy(&output.stdout).into(),
629                    stderr: String::from_utf8_lossy(&output.stderr).into(),
630                });
631            }
632            Err(err) => return Err(BuildLuaError::Io(err)),
633        };
634    }
635
636    copy_includes(&src_dir, &include_dir).await?;
637
638    Ok(())
639}
640
641async fn copy_includes(src_dir: &Path, include_dir: &Path) -> Result<(), io::Error> {
642    for f in &[
643        "lauxlib.h",
644        "lua.h",
645        "luaconf.h",
646        "luajit.h",
647        "lualib.h",
648        "lua.hpp",
649    ] {
650        let src_file = src_dir.join(f);
651        if src_file.is_file() {
652            fs::copy(&src_file, include_dir.join(f))
653                .await
654                .map_err(|err| {
655                    io::Error::other(format!(
656                        "Failed to copy {} to {}:\n{}",
657                        src_file.display(),
658                        include_dir.display(),
659                        err
660                    ))
661                })?;
662        }
663    }
664    Ok(())
665}