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