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