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::{info_span, span, Instrument};
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.9";
34const LUA54_HASH: &str = "sha256-IzW2xYKlJlT5RhK/ENL0ZygF0FMpqmVosdjNnlxvuOY=";
35const LUA55_VERSION: &str = "5.5.1";
36const LUA55_HASH: &str = "sha256-HEtAaNZwYfKiIxrStUIud6zqFIfqmJD2Mgr2FPQ3Pc4=";
37const LUAJIT_MM_VERSION: &str = "2.1";
40const LUAU_VERSION: &str = "0.744";
41
42#[derive(Builder)]
43#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
44pub struct BuildLua<'a> {
45 lua_version: &'a LuaVersion,
46 install_dir: &'a Path,
47 config: &'a Config,
48}
49
50#[derive(Debug, Error, Diagnostic)]
51pub enum BuildLuaError {
52 #[error(transparent)]
53 Request(#[from] reqwest::Error),
54 #[error(transparent)]
55 Io(#[from] io::Error),
56 #[error(transparent)]
57 #[diagnostic(transparent)]
58 Fs(#[from] fs::FsError),
59 #[error(transparent)]
60 #[diagnostic(transparent)]
61 Unpack(#[from] UnpackError),
62 #[error(transparent)]
63 Git(#[from] git2::Error),
64 #[error(transparent)]
65 CC(#[from] cc::Error),
66 #[error("failed to find cl.exe")]
67 ClNotFound,
68 #[error("expected `{name}` in the Luau build output")]
69 #[diagnostic(help("this usually indicates a broken or incomplete Luau source build"))]
70 LuauBinaryNotFound { name: String },
71 #[error("failed to run `{cmd}`")]
72 CmakeSpawn { cmd: String, source: io::Error },
73 #[error("failed to find LINK.exe")]
74 LinkNotFound,
75 #[error(
76 r#"source integrity mismatch.
77- source: {src}
78- expected: {expected}
79- got: {actual}"#
80 )]
81 #[diagnostic(help(
82 r#"the source may have been modified or a tag may have been moved.
83check the source, then rerun the command with `--no-lock` to update the hash."#
84 ))]
85 SourceIntegrityMismatch {
86 src: String,
87 expected: Integrity,
88 actual: Integrity,
89 },
90 #[error("{name} failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
91 CommandFailure {
92 name: String,
93 status: ExitStatus,
94 stdout: String,
95 stderr: String,
96 },
97}
98
99impl<State: build_lua_builder::State + build_lua_builder::IsComplete> BuildLuaBuilder<'_, State> {
100 pub async fn build(self) -> Result<(), BuildLuaError> {
101 let args = self._build();
102 let lua_version = args.lua_version;
103 match lua_version {
104 LuaVersion::Lua51
105 | LuaVersion::Lua52
106 | LuaVersion::Lua53
107 | LuaVersion::Lua54
108 | LuaVersion::Lua55 => do_build_lua(args).await,
109 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => do_build_luajit(args).await,
110 LuaVersion::Luau => do_build_luau(args).await,
111 }
112 }
113}
114
115#[tracing::instrument(
116 name = "Building LuaJIT",
117 level = "info",
118 skip_all,
119 fields(version = LUAJIT_MM_VERSION),
120)]
121async fn do_build_luajit(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
122 let build_dir = fs::tempfile::tempdir()?;
123 let luajit_url = "https://github.com/LuaJIT/LuaJIT.git";
126
127 {
128 let span = span!(
129 tracing::Level::INFO,
130 "Cloning LuaJIT sources",
131 url = luajit_url,
132 );
133 let _enter = span.enter(); let mut fetch_options = FetchOptions::new();
137 fetch_options.update_fetchhead(false);
138 let mut repo_builder = RepoBuilder::new();
139 repo_builder.fetch_options(fetch_options);
140 let repo = repo_builder.clone(luajit_url, build_dir.path())?;
141 let (object, _) = repo.revparse_ext(&format!("v{LUAJIT_MM_VERSION}"))?;
142 repo.checkout_tree(&object, None)?;
143 }
144 if cfg!(target_env = "msvc") {
145 do_build_luajit_msvc(args, build_dir.path()).await
146 } else {
147 do_build_luajit_unix(args, build_dir.path()).await
148 }
149}
150
151#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
152async fn do_build_luajit_unix(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
153 let lua_version = args.lua_version;
154 let config = args.config;
155 let install_dir = args.install_dir;
156
157 let host = Triple::host();
158
159 let mut cc = cc::Build::new();
160 cc.cargo_output(false)
161 .cargo_metadata(false)
162 .cargo_warnings(false)
163 .warnings(config.verbose())
164 .opt_level(3)
165 .host(&host.to_string())
166 .target(&host.to_string());
167 let compiler = cc.try_get_compiler()?;
168 let compiler_path = compiler.path().to_slash_lossy().to_string();
169 let mut make_cmd = Command::new(config.make_cmd());
170 make_cmd.current_dir(build_dir.join("src"));
171 make_cmd.arg("-e");
172 make_cmd.stdout(Stdio::piped());
173 make_cmd.stderr(Stdio::piped());
174 let target = host.to_string();
175 match target.as_str() {
176 "x86_64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
177 make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.11");
178 }
179 "aarch64-apple-darwin" if env::var_os("MACOSX_DEPLOYMENT_TARGET").is_none() => {
180 make_cmd.env("MACOSX_DEPLOYMENT_TARGET", "11.0");
181 }
182 _ if target.contains("linux") => {
183 make_cmd.env("TARGET_SYS", "Linux");
184 }
185 _ => {}
186 }
187 let compiler_path = which::which(&compiler_path)
188 .map_err(|err| io::Error::other(format!("cannot find {}:\n{}", compiler_path, err)))?;
189 let compiler_path = compiler_path.to_slash_lossy().to_string();
190 let compiler_args = compiler.cflags_env();
191 let compiler_args = compiler_args.to_string_lossy();
192 if env::var_os("STATIC_CC").is_none() {
193 make_cmd.env("STATIC_CC", format!("{compiler_path} {compiler_args}"));
194 }
195 if env::var_os("TARGET_LD").is_none() {
196 make_cmd.env("TARGET_LD", format!("{compiler_path} {compiler_args}"));
197 }
198 let mut xcflags = vec!["-fPIC"];
199 if lua_version == &LuaVersion::LuaJIT52 {
200 xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
201 }
202 if cfg!(debug_assertions) {
203 xcflags.push("-DLUA_USE_ASSERT");
204 xcflags.push("-DLUA_USE_APICHECK");
205 }
206 make_cmd.env("BUILDMODE", "static");
207 make_cmd.env("XCFLAGS", xcflags.join(" "));
208
209 match make_cmd.output().await {
210 Ok(output) if output.status.success() => utils::trace_command_output(&output),
211 Ok(output) => {
212 return Err(BuildLuaError::CommandFailure {
213 name: "build".into(),
214 status: output.status,
215 stdout: String::from_utf8_lossy(&output.stdout).into(),
216 stderr: String::from_utf8_lossy(&output.stderr).into(),
217 });
218 }
219 Err(err) => {
220 return Err(BuildLuaError::Io(io::Error::other(format!(
221 "Failed to run `{} build`:\n{}",
222 config.make_cmd(),
223 err
224 ))));
225 }
226 };
227
228 match Command::new(config.make_cmd())
229 .current_dir(build_dir)
230 .stdout(Stdio::piped())
231 .stderr(Stdio::piped())
232 .arg("install")
233 .arg(format!(r#"PREFIX="{}""#, install_dir.display()))
234 .output()
235 .await
236 {
237 Ok(output) if output.status.success() => utils::trace_command_output(&output),
238 Ok(output) => {
239 return Err(BuildLuaError::CommandFailure {
240 name: "install".into(),
241 status: output.status,
242 stdout: String::from_utf8_lossy(&output.stdout).into(),
243 stderr: String::from_utf8_lossy(&output.stderr).into(),
244 });
245 }
246 Err(err) => {
247 return Err(BuildLuaError::Io(io::Error::other(format!(
248 "Failed to run `{} install`:\n{}",
249 config.make_cmd(),
250 err
251 ))));
252 }
253 };
254 move_luajit_includes(install_dir).await?;
255 Ok(())
256}
257
258async fn move_luajit_includes(install_dir: &Path) -> io::Result<()> {
261 let include_dir = install_dir.join("include");
262 let include_subdir = include_dir.join(format!("luajit-{LUAJIT_MM_VERSION}"));
263 if !include_subdir.is_dir() {
264 return Ok(());
265 }
266 let mut dir = fs::tokio::read_dir(&include_subdir)
267 .await
268 .map_err(|err| io::Error::other(err.to_string()))?;
269 while let Some(entry) = dir.next_entry().await? {
270 let file_name = entry.file_name();
271 let src_path = entry.path();
272 let dest_path = include_dir.join(&file_name);
273 fs::tokio::copy(&src_path, &dest_path)
274 .await
275 .map_err(|err| io::Error::other(err.to_string()))?;
276 }
277 fs::tokio::remove_dir_all(&include_subdir)
278 .await
279 .map_err(|err| io::Error::other(err.to_string()))?;
280 Ok(())
281}
282
283#[tracing::instrument(name = "Compiling LuaJIT", skip_all)]
287async fn do_build_luajit_msvc(args: BuildLua<'_>, build_dir: &Path) -> Result<(), BuildLuaError> {
288 let lua_version = args.lua_version;
289 let install_dir = args.install_dir;
290 let lib_dir = install_dir.join("lib");
291 fs::tokio::create_dir_all(&lib_dir)
292 .await
293 .map_err(|err| io::Error::other(err.to_string()))?;
294 let include_dir = install_dir.join("include");
295 fs::tokio::create_dir_all(&include_dir)
296 .await
297 .map_err(|err| io::Error::other(err.to_string()))?;
298 let bin_dir = install_dir.join("bin");
299 fs::tokio::create_dir_all(&bin_dir)
300 .await
301 .map_err(|err| io::Error::other(err.to_string()))?;
302
303 let src_dir = build_dir.join("src");
304 let mut msvcbuild = Command::new(src_dir.join("msvcbuild.bat"));
305 msvcbuild.current_dir(&src_dir);
306 if lua_version == &LuaVersion::LuaJIT52 {
307 msvcbuild.arg("lua52compat");
308 }
309 let host = Triple::host();
310 let target = host.to_string();
311 let cl = cc::windows_registry::find_tool(&target, "cl.exe").ok_or(BuildLuaError::ClNotFound)?;
312 for (k, v) in cl.get_envs() {
313 msvcbuild.env(k, v);
314 }
315 fs::tokio::create_dir_all(&install_dir)
316 .await
317 .map_err(|err| io::Error::other(err.to_string()))?;
318 match msvcbuild.output().await {
319 Ok(output) if output.status.success() => utils::trace_command_output(&output),
320 Ok(output) => {
321 return Err(BuildLuaError::CommandFailure {
322 name: "build".into(),
323 status: output.status,
324 stdout: String::from_utf8_lossy(&output.stdout).into(),
325 stderr: String::from_utf8_lossy(&output.stderr).into(),
326 });
327 }
328 Err(err) => {
329 return Err(BuildLuaError::Io(io::Error::other(format!(
330 "Failed to run msvcbuild.bat:\n{}",
331 err
332 ))))
333 }
334 };
335
336 copy_includes(&src_dir, &include_dir).await?;
337 fs::tokio::copy(src_dir.join("lua51.lib"), lib_dir.join("luajit.lib"))
338 .await
339 .map_err(|err| io::Error::other(err.to_string()))?;
340 fs::tokio::copy(src_dir.join("luajit.exe"), bin_dir.join("luajit.exe"))
341 .await
342 .map_err(|err| io::Error::other(err.to_string()))?;
343 fs::tokio::copy(src_dir.join("lua51.dll"), bin_dir.join("lua51.dll"))
344 .await
345 .map_err(|err| io::Error::other(err.to_string()))?;
346 fs::tokio::copy(src_dir.join("lua51.dll"), lib_dir.join("lua51.dll"))
347 .await
348 .map_err(|err| io::Error::other(err.to_string()))?;
349 Ok(())
350}
351
352async fn do_build_luau(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
353 let install_dir = args.install_dir;
354 let build_dir = fs::tempfile::tempdir()?;
355
356 let luau_url = "https://github.com/luau-lang/luau.git";
357 {
358 let span = info_span!("Cloning Luau sources", url = luau_url);
359 let _enter = span.enter(); let mut fetch_options = FetchOptions::new();
362 fetch_options.update_fetchhead(false);
363 let mut repo_builder = RepoBuilder::new();
364 repo_builder.fetch_options(fetch_options);
365 let repo = repo_builder.clone(luau_url, build_dir.path())?;
366 let (object, _) = repo.revparse_ext(LUAU_VERSION)?;
367 repo.checkout_tree(&object, None)?;
368 }
369
370 let cmake_build_dir = build_dir.path().join("build");
371 fs::tokio::create_dir_all(&cmake_build_dir).await?;
372
373 let src_dir_arg = build_dir.path().to_slash_lossy().to_string();
374 let build_dir_arg = cmake_build_dir.to_slash_lossy().to_string();
375 let build_type = if cfg!(debug_assertions) {
376 "Debug"
378 } else {
379 "Release"
380 };
381 run_cmake(
382 build_dir.path(),
383 &[
384 "-S".into(),
385 src_dir_arg,
386 "-B".into(),
387 build_dir_arg.clone(),
388 format!("-DCMAKE_BUILD_TYPE={build_type}"),
389 "-DLUAU_BUILD_TESTS=OFF".into(),
390 "-DLUAU_BUILD_WEB=OFF".into(),
391 ],
392 args.config,
393 )
394 .await?;
395 run_cmake(
396 build_dir.path(),
397 &[
398 "--build".into(),
399 build_dir_arg,
400 "--config".into(),
401 build_type.into(),
402 "--parallel".into(),
403 "--target".into(),
404 "Luau.Repl.CLI".into(),
405 "Luau.Analyze.CLI".into(),
406 ],
407 args.config,
408 )
409 .await?;
410
411 let bin_candidates = [cmake_build_dir.join(build_type), cmake_build_dir];
414 let bin_dir = install_dir.join("bin");
415 fs::tokio::create_dir_all(&bin_dir).await?;
416
417 #[cfg(windows)]
418 const CLI_BINARIES: [&str; 2] = ["luau.exe", "luau-analyze.exe"];
419 #[cfg(not(windows))]
420 const CLI_BINARIES: [&str; 2] = ["luau", "luau-analyze"];
421
422 for name in CLI_BINARIES {
423 let src = bin_candidates
424 .iter()
425 .map(|dir| dir.join(name))
426 .find(|path| path.is_file())
427 .ok_or(BuildLuaError::LuauBinaryNotFound {
428 name: name.to_string(),
429 })?;
430 fs::tokio::copy(&src, bin_dir.join(name)).await?;
431 }
432 Ok(())
433}
434
435async fn run_cmake(cwd: &Path, args: &[String], config: &Config) -> Result<(), BuildLuaError> {
436 let mut cmd = Command::new(config.cmake_cmd());
437 cmd.current_dir(cwd);
438 cmd.args(args);
439 cmd.stdout(Stdio::piped());
440 cmd.stderr(Stdio::piped());
441 match cmd.output().await {
442 Ok(output) if output.status.success() => {
443 utils::trace_command_output(&output);
444 Ok(())
445 }
446 Ok(output) => Err(BuildLuaError::CommandFailure {
447 name: config.cmake_cmd(),
448 status: output.status,
449 stdout: String::from_utf8_lossy(&output.stdout).into(),
450 stderr: String::from_utf8_lossy(&output.stderr).into(),
451 }),
452 Err(err) => Err(BuildLuaError::CmakeSpawn {
453 cmd: config.cmake_cmd(),
454 source: err,
455 }),
456 }
457}
458
459#[tracing::instrument(
460 name = "Building Lua",
461 level = "info",
462 skip_all,
463 fields(version = args.lua_version.to_string()),
464)]
465async fn do_build_lua(args: BuildLua<'_>) -> Result<(), BuildLuaError> {
466 let lua_version = args.lua_version;
467 let build_dir = fs::tempfile::tempdir()?;
468
469 let (source_integrity, pkg_version): (Integrity, &str) = unsafe {
470 match lua_version {
471 LuaVersion::Lua51 => (LUA51_HASH.parse().unwrap_unchecked(), LUA51_VERSION),
472 LuaVersion::Lua52 => (LUA52_HASH.parse().unwrap_unchecked(), LUA52_VERSION),
473 LuaVersion::Lua53 => (LUA53_HASH.parse().unwrap_unchecked(), LUA53_VERSION),
474 LuaVersion::Lua54 => (LUA54_HASH.parse().unwrap_unchecked(), LUA54_VERSION),
475 LuaVersion::Lua55 => (LUA55_HASH.parse().unwrap_unchecked(), LUA55_VERSION),
476 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 | LuaVersion::Luau => unreachable!(),
477 }
478 };
479
480 let file_name = format!("lua-{pkg_version}.tar.gz");
481
482 let source_url: Url = unsafe {
483 format!("https://www.lua.org/ftp/{file_name}")
484 .parse()
485 .unwrap_unchecked()
486 };
487
488 let response = crate::reqwest::https_client(args.config)?
489 .get(source_url.clone())
490 .send()
491 .instrument(tracing::info_span!("Downloading Lua"))
492 .await?
493 .error_for_status()?
494 .bytes()
495 .instrument(tracing::trace_span!("getting response body"))
496 .await?;
497
498 let hash = response.hash().await?;
499
500 if hash.matches(&source_integrity).is_none() {
501 return Err(BuildLuaError::SourceIntegrityMismatch {
502 src: source_url.to_string(),
503 expected: source_integrity,
504 actual: hash,
505 });
506 }
507
508 let cursor = Cursor::new(response);
509 let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
510 operations::unpack::unpack(mime_type, cursor, true, file_name, build_dir.path()).await?;
511
512 if cfg!(target_env = "msvc") {
513 do_build_lua_msvc(args, build_dir.path(), lua_version, pkg_version).await
514 } else {
515 do_build_lua_unix(args, build_dir.path(), lua_version, pkg_version).await
516 }
517}
518
519#[tracing::instrument(name = "Compiling Lua", skip_all)]
520async fn do_build_lua_unix(
521 args: BuildLua<'_>,
522 build_dir: &Path,
523 lua_version: &LuaVersion,
524 _pkg_version: &str,
525) -> Result<(), BuildLuaError> {
526 tracing::debug!(message = "Compiling Lua (Unix)...");
527 let config = args.config;
528 let install_dir = args.install_dir;
529
530 let build_target = if cfg!(target_os = "linux") {
531 if matches!(&lua_version, LuaVersion::Lua54) {
533 "linux-readline"
534 } else {
535 "linux"
536 }
537 } else if cfg!(target_os = "macos") {
538 "macosx"
539 } else if cfg!(target_os = "freebsd") {
540 "freebsd"
541 } else {
542 "generic"
543 };
544 match Command::new(config.make_cmd())
545 .current_dir(build_dir)
546 .stdout(Stdio::piped())
547 .stderr(Stdio::piped())
548 .arg(build_target)
549 .output()
550 .await
551 {
552 Ok(output) if !output.status.success() && build_target != "generic" => {
556 let fallback_output = Command::new(config.make_cmd())
557 .current_dir(build_dir)
558 .stdout(Stdio::piped())
559 .stderr(Stdio::piped())
560 .arg("generic")
561 .output()
562 .await;
563 guard_success(fallback_output, config, "build (generic)")?;
564 }
565 output => guard_success(output, config, &format!("build ({build_target})"))?,
566 };
567
568 match Command::new(config.make_cmd())
569 .current_dir(build_dir)
570 .stdout(Stdio::piped())
571 .stderr(Stdio::piped())
572 .arg("install")
573 .arg(format!(r#"INSTALL_TOP="{}""#, install_dir.display()))
574 .output()
575 .await
576 {
577 Ok(output) if output.status.success() => utils::trace_command_output(&output),
578 Ok(output) => {
579 return Err(BuildLuaError::CommandFailure {
580 name: "install".into(),
581 status: output.status,
582 stdout: String::from_utf8_lossy(&output.stdout).into(),
583 stderr: String::from_utf8_lossy(&output.stderr).into(),
584 });
585 }
586 Err(err) => {
587 return Err(BuildLuaError::Io(io::Error::other(format!(
588 "Failed to run `{} install`:\n{}",
589 config.make_cmd(),
590 err
591 ))))
592 }
593 };
594
595 Ok(())
596}
597
598#[tracing::instrument(name = "Compiling Lua", skip_all)]
599async fn do_build_lua_msvc(
600 args: BuildLua<'_>,
601 build_dir: &Path,
602 lua_version: &LuaVersion,
603 _pkg_version: &str,
604) -> Result<(), BuildLuaError> {
605 tracing::debug!(message = "Compiling Lua (MSVC)...");
606 let config = args.config;
607 let install_dir = args.install_dir;
608
609 let lib_dir = install_dir.join("lib");
610 fs::tokio::create_dir_all(&lib_dir)
611 .await
612 .map_err(|err| io::Error::other(err.to_string()))?;
613 let include_dir = install_dir.join("include");
614 fs::tokio::create_dir_all(&include_dir)
615 .await
616 .map_err(|err| io::Error::other(err.to_string()))?;
617 let bin_dir = install_dir.join("bin");
618 fs::tokio::create_dir_all(&bin_dir)
619 .await
620 .map_err(|err| io::Error::other(err.to_string()))?;
621
622 let src_dir = build_dir.join("src");
623
624 let lua_bin_name = "lua";
625 let lua_c_bin_name = "luac";
626
627 let dll_name = match lua_version {
628 LuaVersion::Lua51 => "lua51",
629 LuaVersion::Lua52 => "lua52",
630 LuaVersion::Lua53 => "lua53",
631 LuaVersion::Lua54 => "lua54",
632 LuaVersion::Lua55 => "lua55",
633 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 | LuaVersion::Luau => unreachable!(),
634 };
635
636 let lib_name = match lua_version {
637 LuaVersion::Lua51 => "lua5.1",
638 LuaVersion::Lua52 => "lua5.2",
639 LuaVersion::Lua53 => "lua5.3",
640 LuaVersion::Lua54 => "lua5.4",
641 LuaVersion::Lua55 => "lua5.5",
642 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 | LuaVersion::Luau => unreachable!(),
643 };
644
645 let host = Triple::host();
646 let mut cc = cc::Build::new();
647 cc.cargo_output(false)
648 .cargo_metadata(false)
649 .cargo_warnings(false)
650 .warnings(config.verbose())
651 .opt_level(3)
652 .host(&host.to_string())
653 .target(&host.to_string());
654
655 cc.define("LUA_USE_WINDOWS", None);
656 cc.define("LUA_BUILD_AS_DLL", None);
657
658 let mut lib_c_files = Vec::new();
659 let mut read_dir = fs::tokio::read_dir(&src_dir)
660 .await
661 .map_err(|err| io::Error::other(err.to_string()))?;
662 while let Some(entry) = read_dir.next_entry().await? {
663 let path = entry.path();
664 if path.extension().is_some_and(|ext| ext == "c")
665 && path
666 .with_extension("")
667 .file_name()
668 .is_some_and(|name| name != "lua" && name != "luac")
669 {
670 lib_c_files.push(path);
671 }
672 }
673
674 let lib_objects = cc
675 .include(&src_dir)
676 .files(lib_c_files)
677 .out_dir(&lib_dir)
678 .try_compile_objects(config)?;
679
680 let bin_objects = cc
681 .include(&src_dir)
682 .file(src_dir.join(format!("{lua_bin_name}.c")))
683 .file(src_dir.join(format!("{lua_c_bin_name}.c")))
684 .out_dir(&src_dir)
685 .try_compile_objects(config)?;
686
687 let lua_bin_objects = bin_objects.iter().filter(|file| {
688 file.file_stem().is_some_and(|fname| {
689 fname
690 .to_string_lossy()
691 .ends_with(&format!("-{lua_bin_name}"))
692 })
693 });
694
695 let lua_c_bin_objects = bin_objects.iter().filter(|file| {
696 file.file_stem().is_some_and(|fname| {
697 fname
698 .to_string_lossy()
699 .ends_with(&format!("-{lua_c_bin_name}"))
700 })
701 });
702
703 let target = host.to_string();
704 let link =
705 cc::windows_registry::find_tool(&target, "link.exe").ok_or(BuildLuaError::LinkNotFound)?;
706
707 let dll_path = bin_dir.join(format!("{dll_name}.dll"));
708 let lua_bin_path = bin_dir.join(format!("{lua_bin_name}.exe"));
709 let lua_c_bin_path = bin_dir.join(format!("{lua_c_bin_name}.exe"));
710
711 let implib_path = lib_dir.join(format!("{lib_name}.lib"));
712
713 guard_success(
715 Command::new(link.path())
716 .arg("/DLL")
717 .arg(format!("/OUT:{}", dll_path.display()))
718 .arg(format!("/IMPLIB:{}", implib_path.display()))
719 .args(&lib_objects)
720 .output()
721 .await,
722 config,
723 &format!("link {dll_name}.dll"),
724 )?;
725
726 guard_success(
728 Command::new(link.path())
729 .arg(format!("/OUT:{}", lua_bin_path.display()))
730 .arg(&implib_path)
731 .args(lua_bin_objects)
732 .output()
733 .await,
734 config,
735 &format!("link {}", lua_bin_path.display()),
736 )?;
737
738 guard_success(
740 Command::new(link.path())
741 .arg(format!("/OUT:{}", lua_c_bin_path.display()))
742 .args(&lib_objects)
743 .args(lua_c_bin_objects)
744 .output()
745 .await,
746 config,
747 &format!("link {}", lua_c_bin_path.display()),
748 )?;
749
750 copy_includes(&src_dir, &include_dir).await?;
751
752 Ok(())
753}
754
755fn guard_success(
756 output: io::Result<std::process::Output>,
757 config: &Config,
758 cmd_name: &str,
759) -> Result<(), BuildLuaError> {
760 match output {
761 Ok(output) if output.status.success() => {
762 utils::trace_command_output(&output);
763 Ok(())
764 }
765 Ok(output) => Err(BuildLuaError::CommandFailure {
766 name: cmd_name.to_string(),
767 status: output.status,
768 stdout: String::from_utf8_lossy(&output.stdout).into(),
769 stderr: String::from_utf8_lossy(&output.stderr).into(),
770 }),
771 Err(err) => Err(BuildLuaError::Io(io::Error::other(format!(
772 "Failed to run `{} build`:\n{}",
773 config.make_cmd(),
774 err
775 )))),
776 }
777}
778
779async fn copy_includes(src_dir: &Path, include_dir: &Path) -> Result<(), io::Error> {
780 for f in &[
781 "lauxlib.h",
782 "lua.h",
783 "luaconf.h",
784 "luajit.h",
785 "lualib.h",
786 "lua.hpp",
787 ] {
788 let src_file = src_dir.join(f);
789 if src_file.is_file() {
790 fs::tokio::copy(&src_file, include_dir.join(f))
791 .await
792 .map_err(|err| io::Error::other(err.to_string()))?;
793 }
794 }
795 Ok(())
796}