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