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