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::{self, CCExt},
10    config::Config,
11    fs,
12    hash::HasIntegrity,
13    lua_version::LuaVersion,
14    operations::{self, UnpackError},
15};
16use bon::Builder;
17use git2::{build::RepoBuilder, FetchOptions};
18use miette::Diagnostic;
19use path_slash::PathExt;
20use ssri::Integrity;
21use target_lexicon::Triple;
22use thiserror::Error;
23use tokio::process::Command;
24use tracing::{info_span, span, Instrument};
25use url::Url;
26
27const LUA51_VERSION: &str = "5.1.5";
28const LUA51_HASH: &str = "sha256-JkD8VqeV8p0o7xXhPDSkfiI5YLAkDoywqC2bBzhpUzM=";
29const LUA52_VERSION: &str = "5.2.4";
30const LUA52_HASH: &str = "sha256-ueLkqtZ4mztjoFbUQveznw7Pyjrg8fwK5OlhRAG2n0s=";
31const LUA53_VERSION: &str = "5.3.6";
32const LUA53_HASH: &str = "sha256-/F/Wm7hzYyPwJmcrG3I12mE9cXfnJViJOgvc0yBGbWA=";
33const LUA54_VERSION: &str = "5.4.9";
34const LUA54_HASH: &str = "sha256-IzW2xYKlJlT5RhK/ENL0ZygF0FMpqmVosdjNnlxvuOY=";
35const LUA55_VERSION: &str = "5.5.1";
36const LUA55_HASH: &str = "sha256-HEtAaNZwYfKiIxrStUIud6zqFIfqmJD2Mgr2FPQ3Pc4=";
37// XXX: there's no tag with lua 5.2 compatibility, so we have to use the v2.1 branch for now
38// this is unstable and might break the build.
39const LUAJIT_MM_VERSION: &str = "2.1";
40const LUAU_VERSION: &str = "0.744";
41
42#[derive(Builder)]
43#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
44pub struct BuildLua<'a> {
45    lua_version: &'a LuaVersion,
46    install_dir: &'a Path,
47    config: &'a Config,
48}
49
50#[derive(Debug, Error, Diagnostic)]
51pub enum BuildLuaError {
52    #[error(transparent)]
53    Request(#[from] reqwest::Error),
54    #[error(transparent)]
55    Io(#[from] io::Error),
56    #[error(transparent)]
57    #[diagnostic(transparent)]
58    Fs(#[from] fs::FsError),
59    #[error(transparent)]
60    #[diagnostic(transparent)]
61    Unpack(#[from] UnpackError),
62    #[error(transparent)]
63    Git(#[from] git2::Error),
64    #[error(transparent)]
65    CC(#[from] cc::Error),
66    #[error("failed to find cl.exe")]
67    ClNotFound,
68    #[error("expected `{name}` in the Luau build output")]
69    #[diagnostic(help("this usually indicates a broken or incomplete Luau source build"))]
70    LuauBinaryNotFound { name: String },
71    #[error("failed to run `{cmd}`")]
72    CmakeSpawn { cmd: String, source: io::Error },
73    #[error("failed to find LINK.exe")]
74    LinkNotFound,
75    #[error(
76        r#"source integrity mismatch.
77- source: {src}
78- expected: {expected}
79- got: {actual}"#
80    )]
81    #[diagnostic(help(
82        r#"the source may have been modified or a tag may have been moved.
83check the source, then rerun the command with `--no-lock` to update the hash."#
84    ))]
85    SourceIntegrityMismatch {
86        src: String,
87        expected: Integrity,
88        actual: Integrity,
89    },
90    #[error("{name} failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
91    CommandFailure {
92        name: String,
93        status: ExitStatus,
94        stdout: String,
95        stderr: String,
96    },
97}
98
99impl<State: build_lua_builder::State + build_lua_builder::IsComplete> BuildLuaBuilder<'_, State> {
100    pub async fn build(self) -> Result<(), BuildLuaError> {
101        let args = self._build();
102        let lua_version = args.lua_version;
103        match lua_version {
104            LuaVersion::Lua51
105            | LuaVersion::Lua52
106            | LuaVersion::Lua53
107            | LuaVersion::Lua54
108            | LuaVersion::Lua55 => do_build_lua(args).await,
109            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => do_build_luajit(args).await,
110            LuaVersion::Luau => do_build_luau(args).await,
111        }
112    }
113}
114
115#[tracing::instrument(
116    name = "Building LuaJIT",
117    level = "info",
118    skip_all,
119    fields(version = LUAJIT_MM_VERSION),
120)]
121async fn do_build_luajit(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
122    let build_dir = fs::tempfile::tempdir()?;
123    // XXX luajit.org responds with an invalid content-type, so we'll use the github mirror for now.
124    // let luajit_url = "https://luajit.org/git/luajit.git";
125    let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
126
127    {
128        let span = span!(
129            tracing::Level::INFO,
130            "Cloning LuaJIT sources",
131            url = luajit_url,
132        );
133        let _enter = span.enter(); // OK because we're not in an async block
134
135        // We create a new scope because we have to drop fetch_options before the await
136        let mut fetch_options = FetchOptions::new();
137        fetch_options.update_fetchhead(false);
138        let mut repo_builder = RepoBuilder::new();
139        repo_builder.fetch_options(fetch_options);
140        let repo = repo_builder.clone(luajit_url, build_dir.path())?;
141        let (object, _) = repo.revparse_ext(&format!("v{LUAJIT_MM_VERSION}"))?;
142        repo.checkout_tree(&object, None)?;
143    }
144    if cfg!(target_env = "msvc") {
145        do_build_luajit_msvc(args, build_dir.path()).await
146    } else {
147        do_build_luajit_unix(args, build_dir.path()).await
148    }
149}
150
151#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
152async fn do_build_luajit_unix(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
153    let lua_version = args.lua_version;
154    let config = args.config;
155    let install_dir = args.install_dir;
156
157    let host = Triple::host();
158
159    let mut cc = cc::Build::new();
160    cc.cargo_output(false)
161        .cargo_metadata(false)
162        .cargo_warnings(false)
163        .warnings(config.verbose())
164        .opt_level(3)
165        .host(&host.to_string())
166        .target(&host.to_string());
167    let compiler = cc.try_get_compiler()?;
168    let compiler_path = compiler.path().to_slash_lossy().to_string();
169    let mut make_cmd = Command::new(config.make_cmd());
170    make_cmd.current_dir(build_dir.join("src"));
171    make_cmd.arg("-e");
172    make_cmd.stdout(Stdio::piped());
173    make_cmd.stderr(Stdio::piped());
174    let target = host.to_string();
175    match target.as_str() {
176        "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
177            make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.11");
178        }
179        "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
180            make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
181        }
182        _ if target.contains("linux") => {
183            make_cmd.env("TARGET_SYS", "Linux");
184        }
185        _ => {}
186    }
187    let compiler_path = which::which(&compiler_path)
188        .map_err(|err| io::Error::other(format!("cannot find {}:\n{}", compiler_path, err)))?;
189    let compiler_path = compiler_path.to_slash_lossy().to_string();
190    let compiler_args = compiler.cflags_env();
191    let compiler_args = compiler_args.to_string_lossy();
192    if env::var_os("STATIC_CC").is_none() {
193        make_cmd.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
194    }
195    if env::var_os("TARGET_LD").is_none() {
196        make_cmd.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
197    }
198    let mut xcflags = vec!["-fPIC"];
199    if lua_version == &LuaVersion::LuaJIT52 {
200        xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
201    }
202    if cfg!(debug_assertions) {
203        xcflags.push("-DLUA_USE_ASSERT");
204        xcflags.push("-DLUA_USE_APICHECK");
205    }
206    make_cmd.env("BUILDMODE", "static");
207    make_cmd.env("XCFLAGS", xcflags.join(" "));
208
209    match make_cmd.output().await {
210        Ok(output) if output.status.success() => utils::trace_command_output(&output),
211        Ok(output) => {
212            return Err(BuildLuaError::CommandFailure {
213                name: "build".into(),
214                status: output.status,
215                stdout: String::from_utf8_lossy(&output.stdout).into(),
216                stderr: String::from_utf8_lossy(&output.stderr).into(),
217            });
218        }
219        Err(err) => {
220            return Err(BuildLuaError::Io(io::Error::other(format!(
221                "Failed to run `{} build`:\n{}",
222                config.make_cmd(),
223                err
224            ))));
225        }
226    };
227
228    match Command::new(config.make_cmd())
229        .current_dir(build_dir)
230        .stdout(Stdio::piped())
231        .stderr(Stdio::piped())
232        .arg("install")
233        .arg(format!(r#"PREFIX="{}""#, install_dir.display()))
234        .output()
235        .await
236    {
237        Ok(output) if output.status.success() => utils::trace_command_output(&output),
238        Ok(output) => {
239            return Err(BuildLuaError::CommandFailure {
240                name: "install".into(),
241                status: output.status,
242                stdout: String::from_utf8_lossy(&output.stdout).into(),
243                stderr: String::from_utf8_lossy(&output.stderr).into(),
244            });
245        }
246        Err(err) => {
247            return Err(BuildLuaError::Io(io::Error::other(format!(
248                "Failed to run `{} install`:\n{}",
249                config.make_cmd(),
250                err
251            ))));
252        }
253    };
254    move_luajit_includes(install_dir).await?;
255    Ok(())
256}
257
258/// luajit installs the includes to a subdirectory.
259/// For consistency, we want them in the `include` directory
260async fn move_luajit_includes(install_dir: &Path) -> io::Result<()> {
261    let include_dir = install_dir.join("include");
262    let include_subdir = include_dir.join(format!("luajit-{LUAJIT_MM_VERSION}"));
263    if !include_subdir.is_dir() {
264        return Ok(());
265    }
266    let mut dir = fs::tokio::read_dir(&include_subdir)
267        .await
268        .map_err(|err| io::Error::other(err.to_string()))?;
269    while let Some(entry) = dir.next_entry().await? {
270        let file_name = entry.file_name();
271        let src_path = entry.path();
272        let dest_path = include_dir.join(&file_name);
273        fs::tokio::copy(&src_path, &dest_path)
274            .await
275            .map_err(|err| io::Error::other(err.to_string()))?;
276    }
277    fs::tokio::remove_dir_all(&include_subdir)
278        .await
279        .map_err(|err| io::Error::other(err.to_string()))?;
280    Ok(())
281}
282
283// NOTE: LuaJIT must be built as a shared library on MSVC.
284// https://luajit.org/install.html: "You may link LuaJIT statically on Windows
285// only if you don't intend to load Lua/C modules at runtime."
286#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
287async fn do_build_luajit_msvc(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
288    let lua_version = args.lua_version;
289    let install_dir = args.install_dir;
290    let lib_dir = install_dir.join("lib");
291    fs::tokio::create_dir_all(&lib_dir)
292        .await
293        .map_err(|err| io::Error::other(err.to_string()))?;
294    let include_dir = install_dir.join("include");
295    fs::tokio::create_dir_all(&include_dir)
296        .await
297        .map_err(|err| io::Error::other(err.to_string()))?;
298    let bin_dir = install_dir.join("bin");
299    fs::tokio::create_dir_all(&bin_dir)
300        .await
301        .map_err(|err| io::Error::other(err.to_string()))?;
302
303    let src_dir = build_dir.join("src");
304    let mut msvcbuild = Command::new(src_dir.join("msvcbuild.bat"));
305    msvcbuild.current_dir(&src_dir);
306    if lua_version == &LuaVersion::LuaJIT52 {
307        msvcbuild.arg("lua52compat");
308    }
309    let host = Triple::host();
310    let target = host.to_string();
311    let cl = cc::windows_registry::find_tool(&target, "cl.exe").ok_or(BuildLuaError::ClNotFound)?;
312    for (k, v) in cl.get_envs() {
313        msvcbuild.env(k, v);
314    }
315    fs::tokio::create_dir_all(&install_dir)
316        .await
317        .map_err(|err| io::Error::other(err.to_string()))?;
318    match msvcbuild.output().await {
319        Ok(output) if output.status.success() => utils::trace_command_output(&output),
320        Ok(output) => {
321            return Err(BuildLuaError::CommandFailure {
322                name: "build".into(),
323                status: output.status,
324                stdout: String::from_utf8_lossy(&output.stdout).into(),
325                stderr: String::from_utf8_lossy(&output.stderr).into(),
326            });
327        }
328        Err(err) => {
329            return Err(BuildLuaError::Io(io::Error::other(format!(
330                "Failed to run msvcbuild.bat:\n{}",
331                err
332            ))))
333        }
334    };
335
336    copy_includes(&src_dir, &include_dir).await?;
337    fs::tokio::copy(src_dir.join("lua51.lib"), lib_dir.join("luajit.lib"))
338        .await
339        .map_err(|err| io::Error::other(err.to_string()))?;
340    fs::tokio::copy(src_dir.join("luajit.exe"), bin_dir.join("luajit.exe"))
341        .await
342        .map_err(|err| io::Error::other(err.to_string()))?;
343    fs::tokio::copy(src_dir.join("lua51.dll"), bin_dir.join("lua51.dll"))
344        .await
345        .map_err(|err| io::Error::other(err.to_string()))?;
346    fs::tokio::copy(src_dir.join("lua51.dll"), lib_dir.join("lua51.dll"))
347        .await
348        .map_err(|err| io::Error::other(err.to_string()))?;
349    Ok(())
350}
351
352async fn do_build_luau(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
353    let install_dir = args.install_dir;
354    let build_dir = fs::tempfile::tempdir()?;
355
356    let luau_url = "https://github.com/luau-lang/luau.git";
357    {
358        let span = info_span!("Cloning Luau sources", url = luau_url);
359        let _enter = span.enter(); // OK because we're not in an async block
360
361        let mut fetch_options = FetchOptions::new();
362        fetch_options.update_fetchhead(false);
363        let mut repo_builder = RepoBuilder::new();
364        repo_builder.fetch_options(fetch_options);
365        let repo = repo_builder.clone(luau_url, build_dir.path())?;
366        let (object, _) = repo.revparse_ext(LUAU_VERSION)?;
367        repo.checkout_tree(&object, None)?;
368    }
369
370    let cmake_build_dir = build_dir.path().join("build");
371    fs::tokio::create_dir_all(&cmake_build_dir).await?;
372
373    let src_dir_arg = build_dir.path().to_slash_lossy().to_string();
374    let build_dir_arg = cmake_build_dir.to_slash_lossy().to_string();
375    let build_type = if cfg!(debug_assertions) {
376        // Speeds up the build in integration tests
377        "Debug"
378    } else {
379        "Release"
380    };
381    run_cmake(
382        build_dir.path(),
383        &[
384            "-S".into(),
385            src_dir_arg,
386            "-B".into(),
387            build_dir_arg.clone(),
388            format!("-DCMAKE_BUILD_TYPE={build_type}"),
389            "-DLUAU_BUILD_TESTS=OFF".into(),
390            "-DLUAU_BUILD_WEB=OFF".into(),
391        ],
392        args.config,
393    )
394    .await?;
395    run_cmake(
396        build_dir.path(),
397        &[
398            "--build".into(),
399            build_dir_arg,
400            "--config".into(),
401            build_type.into(),
402            "--parallel".into(),
403            "--target".into(),
404            "Luau.Repl.CLI".into(),
405            "Luau.Analyze.CLI".into(),
406        ],
407        args.config,
408    )
409    .await?;
410
411    // Single-config generators emit binaries into the build root, while
412    // multi-config generators (MSVC) put them in a per-config subdirectory.
413    let bin_candidates = [cmake_build_dir.join(build_type), cmake_build_dir];
414    let bin_dir = install_dir.join("bin");
415    fs::tokio::create_dir_all(&bin_dir).await?;
416
417    #[cfg(windows)]
418    const CLI_BINARIES: [&str; 2] = ["luau.exe", "luau-analyze.exe"];
419    #[cfg(not(windows))]
420    const CLI_BINARIES: [&str; 2] = ["luau", "luau-analyze"];
421
422    for name in CLI_BINARIES {
423        let src = bin_candidates
424            .iter()
425            .map(|dir| dir.join(name))
426            .find(|path| path.is_file())
427            .ok_or(BuildLuaError::LuauBinaryNotFound {
428                name: name.to_string(),
429            })?;
430        fs::tokio::copy(&src, bin_dir.join(name)).await?;
431    }
432    Ok(())
433}
434
435async fn run_cmake(cwd: &Path, args: &[String], config: &Config) -> Result<(), BuildLuaError> {
436    let mut cmd = Command::new(config.cmake_cmd());
437    cmd.current_dir(cwd);
438    cmd.args(args);
439    cmd.stdout(Stdio::piped());
440    cmd.stderr(Stdio::piped());
441    match cmd.output().await {
442        Ok(output) if output.status.success() => {
443            utils::trace_command_output(&output);
444            Ok(())
445        }
446        Ok(output) => Err(BuildLuaError::CommandFailure {
447            name: config.cmake_cmd(),
448            status: output.status,
449            stdout: String::from_utf8_lossy(&output.stdout).into(),
450            stderr: String::from_utf8_lossy(&output.stderr).into(),
451        }),
452        Err(err) => Err(BuildLuaError::CmakeSpawn {
453            cmd: config.cmake_cmd(),
454            source: err,
455        }),
456    }
457}
458
459#[tracing::instrument(
460    name = "Building Lua",
461    level = "info",
462    skip_all,
463    fields(version = args.lua_version.to_string()),
464)]
465async fn do_build_lua(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
466    let lua_version = args.lua_version;
467    let build_dir = fs::tempfile::tempdir()?;
468
469    let (source_integrity, pkg_version): (Integrity, &str) = unsafe {
470        match lua_version {
471            LuaVersion::Lua51 => (LUA51_HASH.parse().unwrap_unchecked(), LUA51_VERSION),
472            LuaVersion::Lua52 => (LUA52_HASH.parse().unwrap_unchecked(), LUA52_VERSION),
473            LuaVersion::Lua53 => (LUA53_HASH.parse().unwrap_unchecked(), LUA53_VERSION),
474            LuaVersion::Lua54 => (LUA54_HASH.parse().unwrap_unchecked(), LUA54_VERSION),
475            LuaVersion::Lua55 => (LUA55_HASH.parse().unwrap_unchecked(), LUA55_VERSION),
476            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 | LuaVersion::Luau => unreachable!(),
477        }
478    };
479
480    let file_name = format!("lua-{pkg_version}.tar.gz");
481
482    let source_url: Url = unsafe {
483        format!("https://www.lua.org/ftp/{file_name}")
484            .parse()
485            .unwrap_unchecked()
486    };
487
488    let response = crate::reqwest::https_client(args.config)?
489        .get(source_url.clone())
490        .send()
491        .instrument(tracing::info_span!("Downloading Lua"))
492        .await?
493        .error_for_status()?
494        .bytes()
495        .instrument(tracing::trace_span!("getting response body"))
496        .await?;
497
498    let hash = response.hash().await?;
499
500    if hash.matches(&source_integrity).is_none() {
501        return Err(BuildLuaError::SourceIntegrityMismatch {
502            src: source_url.to_string(),
503            expected: source_integrity,
504            actual: hash,
505        });
506    }
507
508    let cursor = Cursor::new(response);
509    let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
510    operations::unpack::unpack(mime_type, cursor, true, file_name, build_dir.path()).await?;
511
512    if cfg!(target_env = "msvc") {
513        do_build_lua_msvc(args, build_dir.path(), lua_version, pkg_version).await
514    } else {
515        do_build_lua_unix(args, build_dir.path(), lua_version, pkg_version).await
516    }
517}
518
519#[tracing::instrument(name = "Compiling Lua", skip_all)]
520async fn do_build_lua_unix(
521    args: BuildLua<'_>,
522    build_dir: &Path,
523    lua_version: &LuaVersion,
524    _pkg_version: &str,
525) -> Result<(), BuildLuaError> {
526    tracing::debug!(message = "Compiling Lua (Unix)...");
527    let config = args.config;
528    let install_dir = args.install_dir;
529
530    let build_target = if cfg!(target_os = "linux") {
531        // only lua 5.4 has a specific `linux-readline` target
532        if matches!(&lua_version, LuaVersion::Lua54) {
533            "linux-readline"
534        } else {
535            "linux"
536        }
537    } else if cfg!(target_os = "macos") {
538        "macosx"
539    } else if cfg!(target_os = "freebsd") {
540        "freebsd"
541    } else {
542        "generic"
543    };
544    match Command::new(config.make_cmd())
545        .current_dir(build_dir)
546        .stdout(Stdio::piped())
547        .stderr(Stdio::piped())
548        .arg(build_target)
549        .output()
550        .await
551    {
552        // The lua build may fail for some platform specific reason.
553        // Typically, the `readline` headers cannot be found. In these
554        // cases, try the build again with the `generic` platform target.
555        Ok(output) if !output.status.success() && build_target != "generic" => {
556            let fallback_output = Command::new(config.make_cmd())
557                .current_dir(build_dir)
558                .stdout(Stdio::piped())
559                .stderr(Stdio::piped())
560                .arg("generic")
561                .output()
562                .await;
563            guard_success(fallback_output, config, "build (generic)")?;
564        }
565        output => guard_success(output, config, &format!("build ({build_target})"))?,
566    };
567
568    match Command::new(config.make_cmd())
569        .current_dir(build_dir)
570        .stdout(Stdio::piped())
571        .stderr(Stdio::piped())
572        .arg("install")
573        .arg(format!(r#"INSTALL_TOP="{}""#, install_dir.display()))
574        .output()
575        .await
576    {
577        Ok(output) if output.status.success() => utils::trace_command_output(&output),
578        Ok(output) => {
579            return Err(BuildLuaError::CommandFailure {
580                name: "install".into(),
581                status: output.status,
582                stdout: String::from_utf8_lossy(&output.stdout).into(),
583                stderr: String::from_utf8_lossy(&output.stderr).into(),
584            });
585        }
586        Err(err) => {
587            return Err(BuildLuaError::Io(io::Error::other(format!(
588                "Failed to run `{} install`:\n{}",
589                config.make_cmd(),
590                err
591            ))))
592        }
593    };
594
595    Ok(())
596}
597
598#[tracing::instrument(name = "Compiling Lua", skip_all)]
599async fn do_build_lua_msvc(
600    args: BuildLua<'_>,
601    build_dir: &Path,
602    lua_version: &LuaVersion,
603    _pkg_version: &str,
604) -> Result<(), BuildLuaError> {
605    tracing::debug!(message = "Compiling Lua (MSVC)...");
606    let config = args.config;
607    let install_dir = args.install_dir;
608
609    let lib_dir = install_dir.join("lib");
610    fs::tokio::create_dir_all(&lib_dir)
611        .await
612        .map_err(|err| io::Error::other(err.to_string()))?;
613    let include_dir = install_dir.join("include");
614    fs::tokio::create_dir_all(&include_dir)
615        .await
616        .map_err(|err| io::Error::other(err.to_string()))?;
617    let bin_dir = install_dir.join("bin");
618    fs::tokio::create_dir_all(&bin_dir)
619        .await
620        .map_err(|err| io::Error::other(err.to_string()))?;
621
622    let src_dir = build_dir.join("src");
623
624    let lua_bin_name = "lua";
625    let lua_c_bin_name = "luac";
626
627    let dll_name = match lua_version {
628        LuaVersion::Lua51 => "lua51",
629        LuaVersion::Lua52 => "lua52",
630        LuaVersion::Lua53 => "lua53",
631        LuaVersion::Lua54 => "lua54",
632        LuaVersion::Lua55 => "lua55",
633        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 | LuaVersion::Luau => unreachable!(),
634    };
635
636    let lib_name = match lua_version {
637        LuaVersion::Lua51 => "lua5.1",
638        LuaVersion::Lua52 => "lua5.2",
639        LuaVersion::Lua53 => "lua5.3",
640        LuaVersion::Lua54 => "lua5.4",
641        LuaVersion::Lua55 => "lua5.5",
642        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 | LuaVersion::Luau => unreachable!(),
643    };
644
645    let host = Triple::host();
646    let mut cc = cc::Build::new();
647    cc.cargo_output(false)
648        .cargo_metadata(false)
649        .cargo_warnings(false)
650        .warnings(config.verbose())
651        .opt_level(3)
652        .host(&host.to_string())
653        .target(&host.to_string());
654
655    cc.define("LUA_USE_WINDOWS", None);
656    cc.define("LUA_BUILD_AS_DLL", None);
657
658    let mut lib_c_files = Vec::new();
659    let mut read_dir = fs::tokio::read_dir(&src_dir)
660        .await
661        .map_err(|err| io::Error::other(err.to_string()))?;
662    while let Some(entry) = read_dir.next_entry().await? {
663        let path = entry.path();
664        if path.extension().is_some_and(|ext| ext == "c")
665            && path
666                .with_extension("")
667                .file_name()
668                .is_some_and(|name| name != "lua" && name != "luac")
669        {
670            lib_c_files.push(path);
671        }
672    }
673
674    let lib_objects = cc
675        .include(&src_dir)
676        .files(lib_c_files)
677        .out_dir(&lib_dir)
678        .try_compile_objects(config)?;
679
680    let bin_objects = cc
681        .include(&src_dir)
682        .file(src_dir.join(format!("{lua_bin_name}.c")))
683        .file(src_dir.join(format!("{lua_c_bin_name}.c")))
684        .out_dir(&src_dir)
685        .try_compile_objects(config)?;
686
687    let lua_bin_objects = bin_objects.iter().filter(|file| {
688        file.file_stem().is_some_and(|fname| {
689            fname
690                .to_string_lossy()
691                .ends_with(&format!("-{lua_bin_name}"))
692        })
693    });
694
695    let lua_c_bin_objects = bin_objects.iter().filter(|file| {
696        file.file_stem().is_some_and(|fname| {
697            fname
698                .to_string_lossy()
699                .ends_with(&format!("-{lua_c_bin_name}"))
700        })
701    });
702
703    let target = host.to_string();
704    let link =
705        cc::windows_registry::find_tool(&target, "link.exe").ok_or(BuildLuaError::LinkNotFound)?;
706
707    let dll_path = bin_dir.join(format!("{dll_name}.dll"));
708    let lua_bin_path = bin_dir.join(format!("{lua_bin_name}.exe"));
709    let lua_c_bin_path = bin_dir.join(format!("{lua_c_bin_name}.exe"));
710
711    let implib_path = lib_dir.join(format!("{lib_name}.lib"));
712
713    // lua.dll
714    guard_success(
715        Command::new(link.path())
716            .arg("/DLL")
717            .arg(format!("/OUT:{}", dll_path.display()))
718            .arg(format!("/IMPLIB:{}", implib_path.display()))
719            .args(&lib_objects)
720            .output()
721            .await,
722        config,
723        &format!("link {dll_name}.dll"),
724    )?;
725
726    // lua.exe
727    guard_success(
728        Command::new(link.path())
729            .arg(format!("/OUT:{}", lua_bin_path.display()))
730            .arg(&implib_path)
731            .args(lua_bin_objects)
732            .output()
733            .await,
734        config,
735        &format!("link {}", lua_bin_path.display()),
736    )?;
737
738    // luac.exe
739    guard_success(
740        Command::new(link.path())
741            .arg(format!("/OUT:{}", lua_c_bin_path.display()))
742            .args(&lib_objects)
743            .args(lua_c_bin_objects)
744            .output()
745            .await,
746        config,
747        &format!("link {}", lua_c_bin_path.display()),
748    )?;
749
750    copy_includes(&src_dir, &include_dir).await?;
751
752    Ok(())
753}
754
755fn guard_success(
756    output: io::Result<std::process::Output>,
757    config: &Config,
758    cmd_name: &str,
759) -> Result<(), BuildLuaError> {
760    match output {
761        Ok(output) if output.status.success() => {
762            utils::trace_command_output(&output);
763            Ok(())
764        }
765        Ok(output) => Err(BuildLuaError::CommandFailure {
766            name: cmd_name.to_string(),
767            status: output.status,
768            stdout: String::from_utf8_lossy(&output.stdout).into(),
769            stderr: String::from_utf8_lossy(&output.stderr).into(),
770        }),
771        Err(err) => Err(BuildLuaError::Io(io::Error::other(format!(
772            "Failed to run `{} build`:\n{}",
773            config.make_cmd(),
774            err
775        )))),
776    }
777}
778
779async fn copy_includes(src_dir: &Path, include_dir: &Path) -> Result<(), io::Error> {
780    for f in &[
781        "lauxlib.h",
782        "lua.h",
783        "luaconf.h",
784        "luajit.h",
785        "lualib.h",
786        "lua.hpp",
787    ] {
788        let src_file = src_dir.join(f);
789        if src_file.is_file() {
790            fs::tokio::copy(&src_file, include_dir.join(f))
791                .await
792                .map_err(|err| io::Error::other(err.to_string()))?;
793        }
794    }
795    Ok(())
796}