1use is_executable::IsExecutable;
2use itertools::Itertools;
3use path_slash::PathBufExt;
4use std::fmt;
5use std::fmt::Display;
6use std::io;
7use std::path::Path;
8use std::path::PathBuf;
9use thiserror::Error;
10use which::which;
11
12use crate::build::external_dependency::to_lib_name;
13use crate::build::external_dependency::ExternalDependencyInfo;
14use crate::build::utils::{c_lib_extension, format_path};
15use crate::config::external_deps::ExternalDependencySearchConfig;
16use crate::lua_rockspec::ExternalDependencySpec;
17use crate::lua_version::LuaVersion;
18use crate::lua_version::LuaVersionUnset;
19use crate::operations;
20use crate::operations::BuildLuaError;
21use crate::progress::Progress;
22use crate::progress::ProgressBar;
23use crate::tree::InstallTree;
24use crate::variables::GetVariableError;
25use crate::{config::Config, package::PackageVersion, variables::HasVariables};
26use lazy_static::lazy_static;
27use tokio::sync::Mutex;
28
29lazy_static! {
31 static ref NEW_MUTEX: Mutex<i32> = Mutex::new(0i32);
32 static ref INSTALL_MUTEX: Mutex<i32> = Mutex::new(0i32);
33}
34
35#[derive(Debug)]
36pub struct LuaInstallation {
37 pub version: LuaVersion,
38 dependency_info: ExternalDependencyInfo,
39 pub(crate) bin: Option<PathBuf>,
41}
42
43#[derive(Debug, Error)]
44pub enum LuaBinaryError {
45 #[error("neither `lua` nor `luajit` found on the PATH")]
46 LuaBinaryNotFound,
47 #[error(transparent)]
48 DetectLuaVersion(#[from] DetectLuaVersionError),
49 #[error(
50 r#"
51{} -v (= {}) does not match expected Lua version: {}.
52
53Try setting
54
55```toml
56[variables]
57LUA = "/path/to/lua_binary"
58```
59
60in your config, or use `-v LUA=/path/to/lua_binary`.
61 "#,
62 lua_cmd,
63 installed_version,
64 lua_version
65 )]
66 LuaVersionMismatch {
67 lua_cmd: String,
68 installed_version: PackageVersion,
69 lua_version: LuaVersion,
70 },
71 #[error("{0} not found on the PATH")]
72 CustomBinaryNotFound(String),
73}
74
75#[derive(Error, Debug)]
76pub enum DetectLuaVersionError {
77 #[error("failed to run {0}: {1}")]
78 RunLuaCommand(String, io::Error),
79 #[error("failed to parse Lua version from output: {0}")]
80 ParseLuaVersion(String),
81 #[error(transparent)]
82 PackageVersionParse(#[from] crate::package::PackageVersionParseError),
83 #[error(transparent)]
84 LuaVersion(#[from] crate::lua_version::LuaVersionError),
85}
86
87#[derive(Error, Debug)]
88pub enum LuaInstallationError {
89 #[error("could not find a Lua installation and failed to build Lua from source:\n{0}")]
90 Build(#[from] BuildLuaError),
91 #[error(transparent)]
92 LuaVersionUnset(#[from] LuaVersionUnset),
93}
94
95impl LuaInstallation {
96 pub async fn new_from_config(
97 config: &Config,
98 progress: &Progress<ProgressBar>,
99 ) -> Result<Self, LuaInstallationError> {
100 Self::new(LuaVersion::from(config)?, config, progress).await
101 }
102
103 pub async fn new(
104 version: &LuaVersion,
105 config: &Config,
106 progress: &Progress<ProgressBar>,
107 ) -> Result<Self, LuaInstallationError> {
108 let _lock = NEW_MUTEX.lock().await;
109 if let Some(lua_intallation) = Self::probe(version, config.external_deps()) {
110 return Ok(lua_intallation);
111 }
112 let output = Self::root_dir(version, config);
113 let include_dir = output.join("include");
114 let lib_dir = output.join("lib");
115 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
116 if include_dir.is_dir() && lua_lib_name.is_some() {
117 let bin_dir = Some(output.join("bin")).filter(|bin_path| bin_path.is_dir());
118 let bin = bin_dir
119 .as_ref()
120 .and_then(|bin_path| find_lua_executable(bin_path, version));
121 let lib_dir = output.join("lib");
122 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
123 let include_dir = Some(output.join("include"));
124 Ok(LuaInstallation {
125 version: version.clone(),
126 dependency_info: ExternalDependencyInfo {
127 include_dir,
128 lib_dir: Some(lib_dir),
129 bin_dir,
130 lib_info: None,
131 lib_name: lua_lib_name,
132 },
133 bin,
134 })
135 } else {
136 Self::install(version, config, progress).await
137 }
138 }
139
140 pub(crate) fn probe(
141 version: &LuaVersion,
142 search_config: &ExternalDependencySearchConfig,
143 ) -> Option<Self> {
144 let pkg_name_probes = match version {
145 LuaVersion::Lua51 => vec!["lua5.1", "lua-5.1"],
146 LuaVersion::Lua52 => vec!["lua5.2", "lua-5.2"],
147 LuaVersion::Lua53 => vec!["lua5.3", "lua-5.3"],
148 LuaVersion::Lua54 => vec!["lua5.4", "lua-5.4"],
149 LuaVersion::Lua55 => vec!["lua5.5", "lua-5.5"],
150 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit"],
151 };
152
153 let mut dependency_info = pkg_name_probes
154 .iter()
155 .map(|pkg_name| {
156 ExternalDependencyInfo::probe(
157 pkg_name,
158 &ExternalDependencySpec::default(),
159 search_config,
160 )
161 })
162 .find_map(Result::ok);
163
164 if let Some(info) = &mut dependency_info {
165 let bin = info.lib_dir.as_ref().and_then(|lib_dir| {
166 lib_dir
167 .parent()
168 .map(|parent| parent.join("bin"))
169 .filter(|dir| dir.is_dir())
170 .and_then(|bin_path| find_lua_executable(&bin_path, version))
171 });
172 let lua_lib_name = info
173 .lib_dir
174 .as_ref()
175 .and_then(|lib_dir| get_lua_lib_name(lib_dir, version));
176 info.lib_name = lua_lib_name;
177 dependency_info.map(|dependency_info| Self {
178 version: version.clone(),
179 dependency_info,
180 bin,
181 })
182 } else {
183 None
184 }
185 }
186
187 pub async fn install(
188 version: &LuaVersion,
189 config: &Config,
190 progress: &Progress<ProgressBar>,
191 ) -> Result<Self, LuaInstallationError> {
192 let _lock = INSTALL_MUTEX.lock().await;
193
194 let target = Self::root_dir(version, config);
195
196 operations::BuildLua::new()
197 .lua_version(version)
198 .install_dir(&target)
199 .config(config)
200 .progress(progress)
201 .build()
202 .await?;
203
204 let include_dir = target.join("include");
205 let lib_dir = target.join("lib");
206 let bin_dir = Some(target.join("bin")).filter(|bin_path| bin_path.is_dir());
207 let bin = bin_dir
208 .as_ref()
209 .and_then(|bin_path| find_lua_executable(bin_path, version));
210 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
211 Ok(LuaInstallation {
212 version: version.clone(),
213 dependency_info: ExternalDependencyInfo {
214 include_dir: Some(include_dir),
215 lib_dir: Some(lib_dir),
216 bin_dir,
217 lib_info: None,
218 lib_name: lua_lib_name,
219 },
220 bin,
221 })
222 }
223
224 pub fn includes(&self) -> Vec<&PathBuf> {
225 self.dependency_info.include_dir.iter().collect_vec()
226 }
227
228 pub fn bin(&self) -> &Option<PathBuf> {
229 &self.bin
230 }
231
232 fn root_dir(version: &LuaVersion, config: &Config) -> PathBuf {
233 if let Some(lua_dir) = config.lua_dir() {
234 return lua_dir.clone();
235 } else if let Ok(tree) = config.user_tree(version.clone()) {
236 return tree.root().join(".lua");
237 }
238 config.data_dir().join(".lua").join(version.to_string())
239 }
240
241 #[cfg(not(target_env = "msvc"))]
242 fn lua_lib(&self) -> Option<String> {
243 self.dependency_info
244 .lib_name
245 .as_ref()
246 .map(|name| format!("{}.{}", name, c_lib_extension()))
247 }
248
249 #[cfg(target_env = "msvc")]
250 fn lua_lib(&self) -> Option<String> {
251 self.dependency_info.lib_name.clone()
252 }
253
254 pub(crate) fn define_flags(&self) -> Vec<String> {
255 self.dependency_info.define_flags()
256 }
257
258 pub(crate) fn lib_link_args(&self, compiler: &cc::Tool) -> Vec<String> {
260 if cfg!(target_os = "macos") {
261 Vec::new()
263 } else {
264 self.dependency_info.lib_link_args(compiler)
265 }
266 }
267
268 pub(crate) fn lua_binary_or_config_override(&self, config: &Config) -> Option<String> {
271 config.variables().get("LUA").cloned().or(self
272 .bin
273 .clone()
274 .or(LuaBinary::new(self.version.clone(), config).try_into().ok())
275 .map(|bin| bin.to_slash_lossy().to_string()))
276 }
277}
278
279impl HasVariables for LuaInstallation {
280 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
281 Ok(match input {
282 "LUA_INCDIR" => self
283 .dependency_info
284 .include_dir
285 .as_ref()
286 .map(|dir| format_path(dir)),
287 "LUA_LIBDIR" => self
288 .dependency_info
289 .lib_dir
290 .as_ref()
291 .map(|dir| format_path(dir)),
292 "LUA_BINDIR" => self
293 .bin
294 .as_ref()
295 .and_then(|bin| bin.parent().map(format_path)),
296 "LUA" => self
297 .bin
298 .clone()
299 .or(LuaBinary::Lua {
300 lua_version: self.version.clone(),
301 }
302 .try_into()
303 .ok())
304 .map(|lua| format_path(&lua)),
305 "LUALIB" => self.lua_lib().or(Some("".into())),
306 _ => None,
307 })
308 }
309}
310
311#[derive(Clone)]
312pub enum LuaBinary {
313 Lua { lua_version: LuaVersion },
315 Custom(String),
317}
318
319impl Display for LuaBinary {
320 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
321 match self {
322 LuaBinary::Lua { lua_version } => write!(f, "lua {lua_version}"),
323 LuaBinary::Custom(cmd) => write!(f, "{cmd}"),
324 }
325 }
326}
327
328impl LuaBinary {
329 pub fn new(lua_version: LuaVersion, config: &Config) -> Self {
332 match config.variables().get("LUA").cloned() {
333 Some(lua) => Self::Custom(lua),
334 None => Self::Lua { lua_version },
335 }
336 }
337}
338
339impl From<PathBuf> for LuaBinary {
340 fn from(value: PathBuf) -> Self {
341 Self::Custom(value.to_string_lossy().to_string())
342 }
343}
344
345impl TryFrom<LuaBinary> for PathBuf {
346 type Error = LuaBinaryError;
347
348 fn try_from(value: LuaBinary) -> Result<Self, Self::Error> {
349 match value {
350 LuaBinary::Lua { lua_version } => {
351 if let Some(lua_binary) =
352 LuaInstallation::probe(&lua_version, &ExternalDependencySearchConfig::default())
353 .and_then(|lua_installation| lua_installation.bin)
354 {
355 return Ok(lua_binary);
356 }
357 if lua_version.is_luajit() {
358 if let Ok(path) = which("luajit") {
359 return Ok(path);
360 }
361 }
362 match which(format!("lua{}", lua_version)) {
363 Ok(path) => Ok(path),
364 Err(_) => detect_default_lua_bin(lua_version),
365 }
366 }
367 LuaBinary::Custom(bin) => match which(&bin) {
368 Ok(path) => Ok(path),
369 Err(_) => Err(LuaBinaryError::CustomBinaryNotFound(bin)),
370 },
371 }
372 }
373}
374
375fn detect_default_lua_bin(lua_version: LuaVersion) -> Result<PathBuf, LuaBinaryError> {
376 match which("lua") {
377 Ok(path) => {
378 let installed_version = detect_installed_lua_version_from_path(&path)?;
379 if lua_version
380 .clone()
381 .as_version_req()
382 .matches(&installed_version)
383 {
384 Ok(path)
385 } else {
386 Err(LuaBinaryError::LuaVersionMismatch {
387 lua_cmd: path.to_slash_lossy().to_string(),
388 installed_version,
389 lua_version,
390 })?
391 }
392 }
393 Err(_) => Err(LuaBinaryError::LuaBinaryNotFound),
394 }
395}
396
397pub fn detect_installed_lua_version() -> Option<LuaVersion> {
398 which("lua")
399 .ok()
400 .or(which("luajit").ok())
401 .and_then(|lua_cmd| {
402 detect_installed_lua_version_from_path(&lua_cmd)
403 .ok()
404 .and_then(|version| LuaVersion::from_version(version).ok())
405 })
406}
407
408fn find_lua_executable(bin_path: &Path, version: &LuaVersion) -> Option<PathBuf> {
409 std::fs::read_dir(bin_path).ok().and_then(|entries| {
410 let bin_files = entries
411 .filter_map(Result::ok)
412 .map(|entry| entry.path().to_path_buf())
413 .collect_vec();
414
415 #[cfg(windows)]
416 let ext = ".exe";
417
418 #[cfg(not(windows))]
419 let ext = "";
420
421 let lua_version_bin = format!("lua{}{}", version, ext);
424 if let Some(lua_bin) = bin_files
425 .iter()
426 .filter(|file| {
427 file.is_executable()
428 && file
429 .file_name()
430 .is_some_and(|name| name.to_string_lossy() == lua_version_bin)
431 })
432 .collect_vec()
433 .first()
434 .cloned()
435 {
436 Some(lua_bin.clone())
437 } else {
438 bin_files
440 .into_iter()
441 .filter(|file| {
442 file.is_executable()
443 && file.file_name().is_some_and(|name| {
444 matches!(
445 name.to_string_lossy().to_string().as_str(),
446 "lua" | "luajit" | "lua.exe" | "luajit.exe"
447 )
448 })
449 })
450 .collect_vec()
451 .first()
452 .cloned()
453 }
454 })
455}
456
457fn is_lua_lib_name(name: &str, lua_version: &LuaVersion) -> bool {
458 let prefixes = match lua_version {
459 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit", "lua"],
460 _ => vec!["lua"],
461 };
462 let version_str = lua_version.version_compatibility_str();
463 let version_suffix = version_str.replace(".", "");
464 #[cfg(target_family = "unix")]
465 let name = name.trim_start_matches("lib");
466 prefixes
467 .iter()
468 .any(|prefix| name == format!("{}.{}", *prefix, c_lib_extension()))
469 || prefixes.iter().any(|prefix| name.starts_with(*prefix))
470 && (name.contains(&version_str) || name.contains(&version_suffix))
471}
472
473fn get_lua_lib_name(lib_dir: &Path, lua_version: &LuaVersion) -> Option<String> {
474 std::fs::read_dir(lib_dir)
475 .ok()
476 .and_then(|entries| {
477 entries
478 .filter_map(Result::ok)
479 .map(|entry| entry.path().to_path_buf())
480 .filter(|file| file.extension().is_some_and(|ext| ext == c_lib_extension()))
481 .filter(|file| {
482 file.file_name()
483 .is_some_and(|name| is_lua_lib_name(&name.to_string_lossy(), lua_version))
484 })
485 .collect_vec()
486 .first()
487 .cloned()
488 })
489 .map(|file| to_lib_name(&file))
490}
491
492fn detect_installed_lua_version_from_path(
493 lua_cmd: &Path,
494) -> Result<PackageVersion, DetectLuaVersionError> {
495 let output = match std::process::Command::new(lua_cmd).arg("-v").output() {
496 Ok(output) => Ok(output),
497 Err(err) => Err(DetectLuaVersionError::RunLuaCommand(
498 lua_cmd.to_string_lossy().to_string(),
499 err,
500 )),
501 }?;
502 let output_vec = if output.stderr.is_empty() {
503 output.stdout
504 } else {
505 output.stderr
507 };
508 let lua_output = String::from_utf8_lossy(&output_vec).to_string();
509 parse_lua_version_from_output(&lua_output)
510}
511
512fn parse_lua_version_from_output(
513 lua_output: &str,
514) -> Result<PackageVersion, DetectLuaVersionError> {
515 let lua_version_str = lua_output
516 .trim_start_matches("Lua")
517 .trim_start_matches("JIT")
518 .split_whitespace()
519 .next()
520 .map(|s| s.to_string())
521 .ok_or(DetectLuaVersionError::ParseLuaVersion(
522 lua_output.to_string(),
523 ))?;
524 Ok(PackageVersion::parse(&lua_version_str)?)
525}
526
527#[cfg(test)]
528mod tests {
529 use crate::{config::ConfigBuilder, progress::MultiProgress};
530
531 use super::*;
532
533 #[tokio::test]
534 async fn parse_luajit_version() {
535 let luajit_output =
536 "LuaJIT 2.1.1713773202 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/";
537 parse_lua_version_from_output(luajit_output).unwrap();
538 }
539
540 #[tokio::test]
541 async fn parse_lua_51_version() {
542 let lua_output = "Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio";
543 parse_lua_version_from_output(lua_output).unwrap();
544 }
545
546 #[tokio::test]
547 async fn lua_installation_bin() {
548 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
549 println!("Skipping impure test");
550 return;
551 }
552 let config = ConfigBuilder::new().unwrap().build().unwrap();
553 let lua_version = config.lua_version().unwrap();
554 let progress = MultiProgress::new(&config);
555 let bar = progress.map(MultiProgress::new_bar);
556 let lua_installation = LuaInstallation::new(lua_version, &config, &bar)
557 .await
558 .unwrap();
559 assert!(lua_installation.bin.is_some());
561 let lua_binary: LuaBinary = lua_installation.bin.unwrap().into();
562 let lua_bin_path: PathBuf = lua_binary.try_into().unwrap();
563 let pkg_version = detect_installed_lua_version_from_path(&lua_bin_path).unwrap();
564 assert_eq!(&LuaVersion::from_version(pkg_version).unwrap(), lua_version);
565 }
566
567 #[cfg(not(target_env = "msvc"))]
568 #[tokio::test]
569 async fn test_is_lua_lib_name() {
570 assert!(is_lua_lib_name("lua.a", &LuaVersion::Lua51));
571 assert!(is_lua_lib_name("lua-5.1.a", &LuaVersion::Lua51));
572 assert!(is_lua_lib_name("lua5.1.a", &LuaVersion::Lua51));
573 assert!(is_lua_lib_name("lua51.a", &LuaVersion::Lua51));
574 assert!(!is_lua_lib_name("lua-5.2.a", &LuaVersion::Lua51));
575 assert!(is_lua_lib_name("luajit-5.2.a", &LuaVersion::LuaJIT52));
576 assert!(is_lua_lib_name("lua-5.2.a", &LuaVersion::LuaJIT52));
577 assert!(is_lua_lib_name("liblua.a", &LuaVersion::Lua51));
578 assert!(is_lua_lib_name("liblua-5.1.a", &LuaVersion::Lua51));
579 assert!(is_lua_lib_name("liblua53.a", &LuaVersion::Lua53));
580 assert!(is_lua_lib_name("liblua-54.a", &LuaVersion::Lua54));
581 assert!(is_lua_lib_name("liblua-55.a", &LuaVersion::Lua55));
582 }
583
584 #[cfg(target_env = "msvc")]
585 #[tokio::test]
586 async fn test_is_lua_lib_name() {
587 assert!(is_lua_lib_name("lua.lib", &LuaVersion::Lua51));
588 assert!(is_lua_lib_name("lua-5.1.lib", &LuaVersion::Lua51));
589 assert!(!is_lua_lib_name("lua-5.2.lib", &LuaVersion::Lua51));
590 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
591 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
592 assert!(is_lua_lib_name("luajit-5.2.lib", &LuaVersion::LuaJIT52));
593 assert!(is_lua_lib_name("lua-5.2.lib", &LuaVersion::LuaJIT52));
594 }
595}