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