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=";
33const 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 let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
98 progress.map(|p| p.set_message(format!("🦠 Cloning {luajit_url}")));
99 {
100 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
214async 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 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}