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