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::{external_dependency::ExternalDependencyInfo, utils},
10    config::{external_deps::ExternalDependencySearchConfig, Config, LuaVersion},
11    hash::HasIntegrity,
12    lua_rockspec::ExternalDependencySpec,
13    operations::{self, UnpackError},
14    progress::{Progress, ProgressBar},
15};
16use bon::Builder;
17use git2::{build::RepoBuilder, FetchOptions};
18use ssri::Integrity;
19use target_lexicon::Triple;
20use tempdir::TempDir;
21use thiserror::Error;
22use tokio::{fs, process::Command};
23use url::Url;
24
25const LUA51_VERSION: &str = "5.1.5";
26const LUA51_HASH: &str = "sha256-JkD8VqeV8p0o7xXhPDSkfiI5YLAkDoywqC2bBzhpUzM=";
27const LUA52_VERSION: &str = "5.2.4";
28const LUA52_HASH: &str = "sha256-ueLkqtZ4mztjoFbUQveznw7Pyjrg8fwK5OlhRAG2n0s=";
29const LUA53_VERSION: &str = "5.3.6";
30const LUA53_HASH: &str = "sha256-/F/Wm7hzYyPwJmcrG3I12mE9cXfnJViJOgvc0yBGbWA=";
31const LUA54_VERSION: &str = "5.4.8";
32const LUA54_HASH: &str = "sha256-TxjdrhVOeT5G7qtyfFnvHAwMK3ROe5QhlxDXb1MGKa4=";
33// XXX: there's no tag with lua 5.2 compatibility, so we have to use the v2.1 branch for now
34// this is unstable and might break the build.
35const LUAJIT_MM_VERSION: &str = "2.1";
36
37#[derive(Builder)]
38#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
39pub struct BuildLua<'a> {
40    lua_version: &'a LuaVersion,
41    install_dir: &'a Path,
42    config: &'a Config,
43    progress: &'a Progress<ProgressBar>,
44}
45
46#[derive(Debug, Error)]
47pub enum BuildLuaError {
48    #[error(transparent)]
49    Request(#[from] reqwest::Error),
50    #[error(transparent)]
51    Io(#[from] io::Error),
52    #[error(transparent)]
53    Unpack(#[from] UnpackError),
54    #[error(transparent)]
55    Git(#[from] git2::Error),
56    #[error(transparent)]
57    CC(#[from] cc::Error),
58    #[error("failed to find cl.exe")]
59    ClNotFound,
60    #[error("failed to find LINK.exe")]
61    LinkNotFound,
62    #[error("source integrity mismatch.\nExpected: {expected},\nbut got: {actual}")]
63    SourceIntegrityMismatch {
64        expected: Integrity,
65        actual: Integrity,
66    },
67    #[error("{name} failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
68    CommandFailure {
69        name: String,
70        status: ExitStatus,
71        stdout: String,
72        stderr: String,
73    },
74}
75
76impl<State: build_lua_builder::State + build_lua_builder::IsComplete> BuildLuaBuilder<'_, State> {
77    pub async fn build(self) -> Result<(), BuildLuaError> {
78        let args = self._build();
79        let lua_version = args.lua_version;
80        match lua_version {
81            LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53 | LuaVersion::Lua54 => {
82                do_build_lua(args).await
83            }
84            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => do_build_luajit(args).await,
85        }
86    }
87}
88
89async fn do_build_luajit(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
90    let progress = args.progress;
91
92    let build_dir = TempDir::new("lux_luajit_build_dir")
93        .expect("failed to create lua_installation temp directory")
94        .into_path();
95    // XXX luajit.org responds with an invalid content-type, so we'll use the github mirror for now.
96    // let luajit_url = "https://luajit.org/git/luajit.git";
97    let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
98    progress.map(|p| p.set_message(format!("🦠 Cloning {luajit_url}")));
99    {
100        // We create a new scope because we have to drop fetch_options before the await
101        let mut fetch_options = FetchOptions::new();
102        fetch_options.update_fetchhead(false);
103        let mut repo_builder = RepoBuilder::new();
104        repo_builder.fetch_options(fetch_options);
105        let repo = repo_builder.clone(luajit_url, &build_dir)?;
106        let (object, _) = repo.revparse_ext(&format!("v{LUAJIT_MM_VERSION}"))?;
107        repo.checkout_tree(&object, None)?;
108    }
109    if cfg!(target_env = "msvc") {
110        do_build_luajit_msvc(args, &build_dir).await
111    } else {
112        do_build_luajit_unix(args, &build_dir).await
113    }
114}
115
116async fn do_build_luajit_unix(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
117    let lua_version = args.lua_version;
118    let config = args.config;
119    let install_dir = args.install_dir;
120    let progress = args.progress;
121    progress.map(|p| p.set_message(format!("🛠️ Building Luajit {LUAJIT_MM_VERSION}")));
122
123    let host = Triple::host();
124
125    let mut cc = cc::Build::new();
126    cc.cargo_output(false)
127        .cargo_metadata(false)
128        .cargo_warnings(false)
129        .warnings(config.verbose())
130        .opt_level(3)
131        .host(std::env::consts::OS)
132        .target(&host.to_string());
133    let compiler = cc.try_get_compiler()?;
134    let compiler_path = compiler.path().to_str().unwrap();
135    let mut make_cmd = Command::new(config.make_cmd());
136    make_cmd.current_dir(build_dir.join("src"));
137    make_cmd.arg("-e");
138    make_cmd.stdout(Stdio::piped());
139    make_cmd.stderr(Stdio::piped());
140    let target = host.to_string();
141    match target.as_str() {
142        "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
143            make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.11");
144        }
145        "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
146            make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
147        }
148        _ if target.contains("linux") => {
149            make_cmd.env("TARGET_SYS", "Linux");
150        }
151        _ => {}
152    }
153    let compiler_path =
154        which::which(compiler_path).unwrap_or_else(|_| panic!("cannot find {compiler_path}"));
155    let compiler_path = compiler_path.to_str().unwrap();
156    let compiler_args = compiler.cflags_env();
157    let compiler_args = compiler_args.to_str().unwrap();
158    if env::var_os("STATIC_CC").is_none() {
159        make_cmd.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
160    }
161    if env::var_os("TARGET_LD").is_none() {
162        make_cmd.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
163    }
164    let mut xcflags = vec!["-fPIC"];
165    if lua_version == &LuaVersion::LuaJIT52 {
166        xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
167    }
168    if cfg!(debug_assertions) {
169        xcflags.push("-DLUA_USE_ASSERT");
170        xcflags.push("-DLUA_USE_APICHECK");
171    }
172    make_cmd.env("BUILDMODE", "static");
173    make_cmd.env("XCFLAGS", xcflags.join(" "));
174
175    match make_cmd.output().await {
176        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
177        Ok(output) => {
178            return Err(BuildLuaError::CommandFailure {
179                name: "build".into(),
180                status: output.status,
181                stdout: String::from_utf8_lossy(&output.stdout).into(),
182                stderr: String::from_utf8_lossy(&output.stderr).into(),
183            });
184        }
185        Err(err) => return Err(BuildLuaError::Io(err)),
186    };
187
188    progress.map(|p| p.set_message(format!("💻 Installing Luajit {LUAJIT_MM_VERSION}")));
189
190    match Command::new(config.make_cmd())
191        .current_dir(build_dir)
192        .stdout(Stdio::piped())
193        .stderr(Stdio::piped())
194        .arg("install")
195        .arg(format!("PREFIX={}", install_dir.display()))
196        .output()
197        .await
198    {
199        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
200        Ok(output) => {
201            return Err(BuildLuaError::CommandFailure {
202                name: "install".into(),
203                status: output.status,
204                stdout: String::from_utf8_lossy(&output.stdout).into(),
205                stderr: String::from_utf8_lossy(&output.stderr).into(),
206            });
207        }
208        Err(err) => return Err(BuildLuaError::Io(err)),
209    };
210    move_luajit_includes(install_dir).await?;
211    Ok(())
212}
213
214/// luajit installs the includes to a subdirectory.
215/// For consistency, we want them in the `include` directory
216async fn move_luajit_includes(install_dir: &Path) -> io::Result<()> {
217    let include_dir = install_dir.join("include");
218    let include_subdir = include_dir.join(format!("luajit-{LUAJIT_MM_VERSION}"));
219    if !include_subdir.is_dir() {
220        return Ok(());
221    }
222    let mut dir = fs::read_dir(&include_subdir).await?;
223    while let Some(entry) = dir.next_entry().await? {
224        let file_name = entry.file_name();
225        let src_path = entry.path();
226        let dest_path = include_dir.join(&file_name);
227        fs::copy(&src_path, &dest_path).await?;
228    }
229    fs::remove_dir_all(&include_subdir).await?;
230    Ok(())
231}
232
233async fn do_build_luajit_msvc(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
234    let lua_version = args.lua_version;
235    let config = args.config;
236    let install_dir = args.install_dir;
237    let lib_dir = install_dir.join("lib");
238    fs::create_dir_all(&lib_dir).await?;
239    let include_dir = install_dir.join("include");
240    fs::create_dir_all(&include_dir).await?;
241    let bin_dir = install_dir.join("bin");
242    fs::create_dir_all(&bin_dir).await?;
243
244    let progress = args.progress;
245
246    progress.map(|p| p.set_message(format!("🛠️ Building Luajit {LUAJIT_MM_VERSION}")));
247
248    let src_dir = build_dir.join("src");
249    let mut msvcbuild = Command::new(src_dir.join("msvcbuild.bat"));
250    msvcbuild.current_dir(&src_dir);
251    if lua_version == &LuaVersion::LuaJIT52 {
252        msvcbuild.arg("lua52compat");
253    }
254    msvcbuild.arg("static");
255    let host = Triple::host();
256    let target = host.to_string();
257    let cl = cc::windows_registry::find_tool(&target, "cl.exe").ok_or(BuildLuaError::ClNotFound)?;
258    for (k, v) in cl.env() {
259        msvcbuild.env(k, v);
260    }
261    fs::create_dir_all(&install_dir).await?;
262    match msvcbuild.output().await {
263        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
264        Ok(output) => {
265            return Err(BuildLuaError::CommandFailure {
266                name: "build".into(),
267                status: output.status,
268                stdout: String::from_utf8_lossy(&output.stdout).into(),
269                stderr: String::from_utf8_lossy(&output.stderr).into(),
270            });
271        }
272        Err(err) => return Err(BuildLuaError::Io(err)),
273    };
274
275    progress.map(|p| p.set_message(format!("💻 Installing Luajit {LUAJIT_MM_VERSION}")));
276    copy_includes(&src_dir, &include_dir).await?;
277    fs::copy(src_dir.join("lua51.lib"), lib_dir.join("luajit.lib")).await?;
278    fs::copy(src_dir.join("luajit.exe"), bin_dir.join("luajit.exe")).await?;
279    Ok(())
280}
281
282async fn do_build_lua(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
283    let lua_version = args.lua_version;
284    let progress = args.progress;
285
286    let build_dir = TempDir::new("lux_lua_build_dir")
287        .expect("failed to create lua_installation temp directory")
288        .into_path();
289
290    let (source_integrity, pkg_version): (Integrity, &str) = match lua_version {
291        LuaVersion::Lua51 => (LUA51_HASH.parse().unwrap(), LUA51_VERSION),
292        LuaVersion::Lua52 => (LUA52_HASH.parse().unwrap(), LUA52_VERSION),
293        LuaVersion::Lua53 => (LUA53_HASH.parse().unwrap(), LUA53_VERSION),
294        LuaVersion::Lua54 => (LUA54_HASH.parse().unwrap(), LUA54_VERSION),
295        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
296    };
297
298    let file_name = format!("lua-{pkg_version}.tar.gz");
299
300    let source_url: Url = format!("https://www.lua.org/ftp/{file_name}")
301        .parse()
302        .unwrap();
303
304    progress.map(|p| p.set_message(format!("📥 Downloading {}", &source_url)));
305
306    let response = reqwest::get(source_url)
307        .await?
308        .error_for_status()?
309        .bytes()
310        .await?;
311
312    let hash = response.hash()?;
313
314    if hash.matches(&source_integrity).is_none() {
315        return Err(BuildLuaError::SourceIntegrityMismatch {
316            expected: source_integrity,
317            actual: hash,
318        });
319    }
320
321    let cursor = Cursor::new(response);
322    let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
323    operations::unpack::unpack(mime_type, cursor, true, file_name, &build_dir, progress).await?;
324
325    if cfg!(target_env = "msvc") {
326        do_build_lua_msvc(args, &build_dir, lua_version, pkg_version).await
327    } else {
328        do_build_lua_unix(args, &build_dir, lua_version, pkg_version).await
329    }
330}
331
332async fn do_build_lua_unix(
333    args: BuildLua<'_>,
334    build_dir: &Path,
335    lua_version: &LuaVersion,
336    pkg_version: &str,
337) -> Result<(), BuildLuaError> {
338    let config = args.config;
339    let progress = args.progress;
340    let install_dir = args.install_dir;
341
342    progress.map(|p| p.set_message(format!("🛠️ Building Lua {}", &pkg_version)));
343
344    let readline_spec = ExternalDependencySpec {
345        header: Some("readline/readline.h".into()),
346        library: None,
347    };
348    let build_target = match ExternalDependencyInfo::probe(
349        "readline",
350        &readline_spec,
351        &ExternalDependencySearchConfig::default(),
352    ) {
353        Ok(_) => {
354            // NOTE: The Lua < 5.4 linux targets depend on readline
355            if cfg!(target_os = "linux") {
356                if matches!(&lua_version, LuaVersion::Lua54) {
357                    "linux-readline"
358                } else {
359                    "linux"
360                }
361            } else if cfg!(target_os = "macos") {
362                "macosx"
363            } else if matches!(&lua_version, LuaVersion::Lua54) {
364                "linux"
365            } else {
366                "generic"
367            }
368        }
369        _ => "generic",
370    };
371
372    match Command::new(config.make_cmd())
373        .current_dir(build_dir)
374        .stdout(Stdio::piped())
375        .stderr(Stdio::piped())
376        .arg(build_target)
377        .output()
378        .await
379    {
380        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
381        Ok(output) => {
382            return Err(BuildLuaError::CommandFailure {
383                name: "build".into(),
384                status: output.status,
385                stdout: String::from_utf8_lossy(&output.stdout).into(),
386                stderr: String::from_utf8_lossy(&output.stderr).into(),
387            });
388        }
389        Err(err) => return Err(BuildLuaError::Io(err)),
390    };
391
392    progress.map(|p| p.set_message(format!("💻 Installing Lua {}", &pkg_version)));
393
394    match Command::new(config.make_cmd())
395        .current_dir(build_dir)
396        .stdout(Stdio::piped())
397        .stderr(Stdio::piped())
398        .arg("install")
399        .arg(format!("INSTALL_TOP={}", install_dir.display()))
400        .output()
401        .await
402    {
403        Ok(output) if output.status.success() => utils::log_command_output(&output, config),
404        Ok(output) => {
405            return Err(BuildLuaError::CommandFailure {
406                name: "install".into(),
407                status: output.status,
408                stdout: String::from_utf8_lossy(&output.stdout).into(),
409                stderr: String::from_utf8_lossy(&output.stderr).into(),
410            });
411        }
412        Err(err) => return Err(BuildLuaError::Io(err)),
413    };
414
415    Ok(())
416}
417
418async fn do_build_lua_msvc(
419    args: BuildLua<'_>,
420    build_dir: &Path,
421    lua_version: &LuaVersion,
422    pkg_version: &str,
423) -> Result<(), BuildLuaError> {
424    let config = args.config;
425    let progress = args.progress;
426    let install_dir = args.install_dir;
427
428    progress.map(|p| p.set_message(format!("🛠️ Building Lua {}", &pkg_version)));
429
430    let lib_dir = install_dir.join("lib");
431    fs::create_dir_all(&lib_dir).await?;
432    let include_dir = install_dir.join("include");
433    fs::create_dir_all(&include_dir).await?;
434    let bin_dir = install_dir.join("bin");
435    fs::create_dir_all(&bin_dir).await?;
436
437    let src_dir = build_dir.join("src");
438
439    let lib_name = match lua_version {
440        LuaVersion::Lua51 => "lua5.1",
441        LuaVersion::Lua52 => "lua5.2",
442        LuaVersion::Lua53 => "lua5.3",
443        LuaVersion::Lua54 => "lua5.4",
444        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => unreachable!(),
445    };
446    let host = Triple::host();
447    let mut cc = cc::Build::new();
448    cc.cargo_output(false)
449        .cargo_metadata(false)
450        .cargo_warnings(false)
451        .warnings(config.verbose())
452        .opt_level(3)
453        .host(std::env::consts::OS)
454        .target(&host.to_string());
455
456    cc.define("LUA_USE_WINDOWS", None);
457
458    let mut lib_c_files = Vec::new();
459    let mut read_dir = fs::read_dir(&src_dir).await?;
460    while let Some(entry) = read_dir.next_entry().await? {
461        let path = entry.path();
462        if path.extension().is_some_and(|ext| ext == "c")
463            && path
464                .with_extension("")
465                .file_name()
466                .is_some_and(|name| name != "lua" && name != "luac")
467        {
468            lib_c_files.push(path);
469        }
470    }
471    cc.include(&src_dir)
472        .files(lib_c_files)
473        .out_dir(&lib_dir)
474        .try_compile(lib_name)?;
475
476    let bin_objects = cc
477        .include(&src_dir)
478        .file(src_dir.join("lua.c"))
479        .file(src_dir.join("luac.c"))
480        .out_dir(&src_dir)
481        .try_compile_intermediates()?;
482
483    progress.map(|p| p.set_message(format!("💻 Installing Lua {}", &pkg_version)));
484
485    let target = host.to_string();
486    let link =
487        cc::windows_registry::find_tool(&target, "link.exe").ok_or(BuildLuaError::LinkNotFound)?;
488
489    for name in ["lua", "luac"] {
490        let bin = bin_dir.join(format!("{name}.exe"));
491        let objects = bin_objects.iter().filter(|file| {
492            file.with_extension("").file_name().is_some_and(|fname| {
493                fname
494                    .to_string_lossy()
495                    .to_string()
496                    .ends_with(&format!("-{name}"))
497            })
498        });
499        match Command::new(link.path())
500            .arg(format!("/OUT:{}", bin.display()))
501            .args(objects)
502            .arg(format!("{}.lib", lib_dir.join(lib_name).display()))
503            .output()
504            .await
505        {
506            Ok(output) if output.status.success() => utils::log_command_output(&output, config),
507            Ok(output) => {
508                return Err(BuildLuaError::CommandFailure {
509                    name: format!("install {name}.exe"),
510                    status: output.status,
511                    stdout: String::from_utf8_lossy(&output.stdout).into(),
512                    stderr: String::from_utf8_lossy(&output.stderr).into(),
513                });
514            }
515            Err(err) => return Err(BuildLuaError::Io(err)),
516        };
517    }
518
519    copy_includes(&src_dir, &include_dir).await?;
520
521    Ok(())
522}
523
524async fn copy_includes(src_dir: &Path, include_dir: &Path) -> Result<(), io::Error> {
525    for f in &[
526        "lauxlib.h",
527        "lua.h",
528        "luaconf.h",
529        "luajit.h",
530        "lualib.h",
531        "lua.hpp",
532    ] {
533        let src_file = src_dir.join(f);
534        if src_file.is_file() {
535            fs::copy(src_file, include_dir.join(f)).await?;
536        }
537    }
538    Ok(())
539}