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