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