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