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 target_lexicon::Triple;
10use tempdir::TempDir;
11use thiserror::Error;
12use which::which;
13
14use crate::build::external_dependency::to_lib_name;
15use crate::build::external_dependency::ExternalDependencyInfo;
16use crate::build::utils::recursive_copy_dir;
17use crate::build::utils::{c_lib_extension, format_path};
18use crate::config::external_deps::ExternalDependencySearchConfig;
19use crate::lua_rockspec::ExternalDependencySpec;
20use crate::variables::GetVariableError;
21use crate::{
22 config::{Config, LuaVersion},
23 package::PackageVersion,
24 variables::HasVariables,
25};
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 "{} -v (= {}) does not match expected Lua version {}",
51 lua_cmd,
52 installed_version,
53 lua_version
54 )]
55 LuaVersionMismatch {
56 lua_cmd: String,
57 installed_version: PackageVersion,
58 lua_version: LuaVersion,
59 },
60 #[error("{0} not found on the PATH")]
61 CustomBinaryNotFound(String),
62}
63
64#[derive(Error, Debug)]
65pub enum DetectLuaVersionError {
66 #[error("failed to run {0}: {1}")]
67 RunLuaCommand(String, io::Error),
68 #[error("failed to parse Lua version from output: {0}")]
69 ParseLuaVersion(String),
70 #[error(transparent)]
71 PackageVersionParse(#[from] crate::package::PackageVersionParseError),
72 #[error(transparent)]
73 LuaVersion(#[from] crate::config::LuaVersionError),
74}
75
76#[derive(Error, Debug)]
77pub enum LuaInstallationError {
78 #[error("error building Lua from source:\n{0}")]
79 Build(String),
80}
81
82impl LuaInstallation {
83 pub async fn new(version: &LuaVersion, config: &Config) -> Result<Self, LuaInstallationError> {
84 let _lock = NEW_MUTEX.lock().await;
85 if let Some(lua_intallation) = Self::probe(version, config.external_deps()) {
86 return Ok(lua_intallation);
87 }
88 let output = Self::root_dir(version, config);
89 let include_dir = output.join("include");
90 let lib_dir = output.join("lib");
91 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
92 if include_dir.is_dir() && lua_lib_name.is_some() {
93 let bin_dir = Some(output.join("bin")).filter(|bin_path| bin_path.is_dir());
94 let bin = bin_dir
95 .as_ref()
96 .and_then(|bin_path| find_lua_executable(bin_path));
97 let lib_dir = output.join("lib");
98 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
99 let include_dir = Some(output.join("include"));
100 Ok(LuaInstallation {
101 version: version.clone(),
102 dependency_info: ExternalDependencyInfo {
103 include_dir,
104 lib_dir: Some(lib_dir),
105 bin_dir,
106 lib_info: None,
107 lib_name: lua_lib_name,
108 },
109 bin,
110 })
111 } else {
112 Self::install(version, config).await
113 }
114 }
115
116 pub(crate) fn probe(
117 version: &LuaVersion,
118 search_config: &ExternalDependencySearchConfig,
119 ) -> Option<Self> {
120 let pkg_name = match version {
121 LuaVersion::Lua51 => "lua5.1",
122 LuaVersion::Lua52 => "lua5.2",
123 LuaVersion::Lua53 => "lua5.3",
124 LuaVersion::Lua54 => "lua5.4",
125 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => "luajit",
126 };
127
128 let mut dependency_info = ExternalDependencyInfo::probe(
129 pkg_name,
130 &ExternalDependencySpec::default(),
131 search_config,
132 );
133
134 if let Ok(info) = &mut dependency_info {
135 let bin = info.lib_dir.as_ref().and_then(|lib_dir| {
136 lib_dir
137 .parent()
138 .map(|parent| parent.join("bin"))
139 .filter(|dir| dir.is_dir())
140 .and_then(|bin_path| find_lua_executable(&bin_path))
141 });
142 let lua_lib_name = info
143 .lib_dir
144 .as_ref()
145 .and_then(|lib_dir| get_lua_lib_name(lib_dir, version));
146 info.lib_name = lua_lib_name;
147 Some(Self {
148 version: version.clone(),
149 dependency_info: dependency_info.unwrap(),
150 bin,
151 })
152 } else {
153 None
154 }
155 }
156
157 pub async fn install(
159 version: &LuaVersion,
160 config: &Config,
161 ) -> Result<Self, LuaInstallationError> {
162 let _lock = INSTALL_MUTEX.lock().await;
163 let host = Triple::host();
164 let target = &host.to_string();
165 let host_operating_system = &host.operating_system.to_string();
166
167 let output = TempDir::new("lux_lua_installation")
168 .expect("failed to create lua_installation temp directory");
169
170 let (include_dir, lib_dir) = match version {
171 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => {
172 let target_pointer_width =
174 std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap_or("64".into());
175 std::env::set_var("CARGO_CFG_TARGET_POINTER_WIDTH", target_pointer_width);
176 let build = luajit_src::Build::new()
177 .target(target)
178 .host(host_operating_system)
179 .out_dir(&output)
180 .lua52compat(matches!(version, LuaVersion::LuaJIT52))
181 .build();
182
183 (
184 build.include_dir().to_path_buf(),
185 build.lib_dir().to_path_buf(),
186 )
187 }
188 _ => {
189 let build = lua_src::Build::new()
190 .target(target)
191 .host(host_operating_system)
192 .out_dir(&output)
193 .try_build(match version {
194 LuaVersion::Lua51 => lua_src::Version::Lua51,
195 LuaVersion::Lua52 => lua_src::Version::Lua52,
196 LuaVersion::Lua53 => lua_src::Version::Lua53,
197 LuaVersion::Lua54 => lua_src::Version::Lua54,
198 _ => unreachable!(),
199 })
200 .map_err(|err| LuaInstallationError::Build(err.to_string()))?;
201
202 (
203 build.include_dir().to_path_buf(),
204 build.lib_dir().to_path_buf(),
205 )
206 }
207 };
208
209 let target = Self::root_dir(version, config);
210 recursive_copy_dir(&output.into_path(), &target)
211 .await
212 .expect("error copying lua installation");
213
214 let bin_dir = Some(target.join("bin")).filter(|bin_path| bin_path.is_dir());
215 let bin = bin_dir
216 .as_ref()
217 .and_then(|bin_path| find_lua_executable(bin_path));
218 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
219 Ok(LuaInstallation {
220 version: version.clone(),
221 dependency_info: ExternalDependencyInfo {
222 include_dir: Some(include_dir),
223 lib_dir: Some(lib_dir),
224 bin_dir,
225 lib_info: None,
226 lib_name: lua_lib_name,
227 },
228 bin,
229 })
230 }
231
232 pub fn includes(&self) -> Vec<&PathBuf> {
233 self.dependency_info.include_dir.iter().collect_vec()
234 }
235
236 fn root_dir(version: &LuaVersion, config: &Config) -> PathBuf {
237 if let Some(lua_dir) = config.lua_dir() {
238 return lua_dir.clone();
239 } else if let Ok(tree) = config.user_tree(version.clone()) {
240 return tree.root().join(".lua");
241 }
242 config.data_dir().join(".lua").join(version.to_string())
243 }
244
245 #[cfg(not(target_env = "msvc"))]
246 fn lua_lib(&self) -> Option<String> {
247 self.dependency_info
248 .lib_name
249 .as_ref()
250 .map(|name| format!("{}.{}", name, c_lib_extension()))
251 }
252
253 #[cfg(target_env = "msvc")]
254 fn lua_lib(&self) -> Option<String> {
255 self.dependency_info.lib_name.clone()
256 }
257
258 pub(crate) fn define_flags(&self) -> Vec<String> {
259 self.dependency_info.define_flags()
260 }
261
262 pub(crate) fn lib_link_args(&self, compiler: &cc::Tool) -> Vec<String> {
264 self.dependency_info.lib_link_args(compiler)
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("lua") {
362 Ok(path) => {
363 let installed_version = detect_installed_lua_version_from_path(&path)?;
364 if lua_version
365 .clone()
366 .as_version_req()
367 .matches(&installed_version)
368 {
369 Ok(path)
370 } else {
371 Err(Self::Error::LuaVersionMismatch {
372 lua_cmd: path.to_slash_lossy().to_string(),
373 installed_version,
374 lua_version,
375 })?
376 }
377 }
378 Err(_) => Err(LuaBinaryError::LuaBinaryNotFound),
379 }
380 }
381 LuaBinary::Custom(bin) => match which(&bin) {
382 Ok(path) => Ok(path),
383 Err(_) => Err(LuaBinaryError::CustomBinaryNotFound(bin)),
384 },
385 }
386 }
387}
388
389pub fn detect_installed_lua_version() -> Option<LuaVersion> {
390 which("lua")
391 .ok()
392 .or(which("luajit").ok())
393 .and_then(|lua_cmd| {
394 detect_installed_lua_version_from_path(&lua_cmd)
395 .ok()
396 .and_then(|version| LuaVersion::from_version(version).ok())
397 })
398}
399
400fn find_lua_executable(bin_path: &Path) -> Option<PathBuf> {
401 std::fs::read_dir(bin_path).ok().and_then(|entries| {
402 entries
403 .filter_map(Result::ok)
404 .map(|entry| entry.path().to_path_buf())
405 .filter(|file| {
406 file.is_executable()
407 && file.file_name().is_some_and(|name| {
408 matches!(
409 name.to_string_lossy().to_string().as_str(),
410 "lua" | "luajit"
411 )
412 })
413 })
414 .collect_vec()
415 .first()
416 .cloned()
417 })
418}
419
420fn is_lua_lib_name(name: &str, lua_version: &LuaVersion) -> bool {
421 let prefixes = match lua_version {
422 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit", "lua"],
423 _ => vec!["lua"],
424 };
425 let version_str = lua_version.version_compatibility_str();
426 let version_suffix = version_str.replace(".", "");
427 #[cfg(target_family = "unix")]
428 let name = name.trim_start_matches("lib");
429 prefixes
430 .iter()
431 .any(|prefix| name == format!("{}.{}", *prefix, c_lib_extension()))
432 || prefixes.iter().any(|prefix| name.starts_with(*prefix))
433 && (name.contains(&version_str) || name.contains(&version_suffix))
434}
435
436fn get_lua_lib_name(lib_dir: &Path, lua_version: &LuaVersion) -> Option<String> {
437 std::fs::read_dir(lib_dir)
438 .ok()
439 .and_then(|entries| {
440 entries
441 .filter_map(Result::ok)
442 .map(|entry| entry.path().to_path_buf())
443 .filter(|file| file.extension().is_some_and(|ext| ext == c_lib_extension()))
444 .filter(|file| {
445 file.file_name()
446 .is_some_and(|name| is_lua_lib_name(&name.to_string_lossy(), lua_version))
447 })
448 .collect_vec()
449 .first()
450 .cloned()
451 })
452 .map(|file| to_lib_name(&file))
453}
454
455fn detect_installed_lua_version_from_path(
456 lua_cmd: &Path,
457) -> Result<PackageVersion, DetectLuaVersionError> {
458 let output = match std::process::Command::new(lua_cmd).arg("-v").output() {
459 Ok(output) => Ok(output),
460 Err(err) => Err(DetectLuaVersionError::RunLuaCommand(
461 lua_cmd.to_string_lossy().to_string(),
462 err,
463 )),
464 }?;
465 let output_vec = if output.stderr.is_empty() {
466 output.stdout
467 } else {
468 output.stderr
470 };
471 let lua_output = String::from_utf8_lossy(&output_vec).to_string();
472 parse_lua_version_from_output(&lua_output)
473}
474
475fn parse_lua_version_from_output(
476 lua_output: &str,
477) -> Result<PackageVersion, DetectLuaVersionError> {
478 let lua_version_str = lua_output
479 .trim_start_matches("Lua")
480 .trim_start_matches("JIT")
481 .split_whitespace()
482 .next()
483 .map(|s| s.to_string())
484 .ok_or(DetectLuaVersionError::ParseLuaVersion(
485 lua_output.to_string(),
486 ))?;
487 Ok(PackageVersion::parse(&lua_version_str)?)
488}
489
490#[cfg(test)]
491mod test {
492 use crate::config::ConfigBuilder;
493
494 use super::*;
495
496 #[tokio::test]
497 async fn parse_luajit_version() {
498 let luajit_output =
499 "LuaJIT 2.1.1713773202 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/";
500 parse_lua_version_from_output(luajit_output).unwrap();
501 }
502
503 #[tokio::test]
504 async fn parse_lua_51_version() {
505 let lua_output = "Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio";
506 parse_lua_version_from_output(lua_output).unwrap();
507 }
508
509 #[tokio::test]
510 async fn lua_installation_bin() {
511 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
512 println!("Skipping impure test");
513 return;
514 }
515 let config = ConfigBuilder::new().unwrap().build().unwrap();
516 let lua_version = config.lua_version().unwrap();
517 let lua_installation = LuaInstallation::new(lua_version, &config).await.unwrap();
518 assert!(lua_installation.bin.is_some());
520 let lua_binary: LuaBinary = lua_installation.bin.unwrap().into();
521 let lua_bin_path: PathBuf = lua_binary.try_into().unwrap();
522 let pkg_version = detect_installed_lua_version_from_path(&lua_bin_path).unwrap();
523 assert_eq!(&LuaVersion::from_version(pkg_version).unwrap(), lua_version);
524 }
525
526 #[cfg(not(target_env = "msvc"))]
527 #[tokio::test]
528 async fn test_is_lua_lib_name() {
529 assert!(is_lua_lib_name("lua.a", &LuaVersion::Lua51));
530 assert!(is_lua_lib_name("lua-5.1.a", &LuaVersion::Lua51));
531 assert!(is_lua_lib_name("lua5.1.a", &LuaVersion::Lua51));
532 assert!(is_lua_lib_name("lua51.a", &LuaVersion::Lua51));
533 assert!(!is_lua_lib_name("lua-5.2.a", &LuaVersion::Lua51));
534 assert!(is_lua_lib_name("luajit-5.2.a", &LuaVersion::LuaJIT52));
535 assert!(is_lua_lib_name("lua-5.2.a", &LuaVersion::LuaJIT52));
536 assert!(is_lua_lib_name("liblua.a", &LuaVersion::Lua51));
537 assert!(is_lua_lib_name("liblua-5.1.a", &LuaVersion::Lua51));
538 assert!(is_lua_lib_name("liblua53.a", &LuaVersion::Lua53));
539 assert!(is_lua_lib_name("liblua-54.a", &LuaVersion::Lua54));
540 }
541
542 #[cfg(target_env = "msvc")]
543 #[tokio::test]
544 async fn test_is_lua_lib_name() {
545 assert!(is_lua_lib_name("lua.lib", &LuaVersion::Lua51));
546 assert!(is_lua_lib_name("lua-5.1.lib", &LuaVersion::Lua51));
547 assert!(!is_lua_lib_name("lua-5.2.lib", &LuaVersion::Lua51));
548 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
549 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
550 assert!(is_lua_lib_name("luajit-5.2.lib", &LuaVersion::LuaJIT52));
551 assert!(is_lua_lib_name("lua-5.2.lib", &LuaVersion::LuaJIT52));
552 }
553}