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