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