1use is_executable::IsExecutable;
2use itertools::Itertools;
3use miette::Diagnostic;
4use path_slash::PathBufExt;
5use std::fmt;
6use std::fmt::Display;
7use std::io;
8use std::path::Path;
9use std::path::PathBuf;
10use thiserror::Error;
11use which::which;
12
13use crate::build::external_dependency::to_lib_name;
14use crate::build::external_dependency::ExternalDependencyInfo;
15use crate::build::utils::{c_lib_extension, format_path};
16use crate::config::external_deps::ExternalDependencySearchConfig;
17use crate::fs;
18use crate::lua_rockspec::ExternalDependencySpec;
19use crate::lua_version::LuaVersion;
20use crate::lua_version::LuaVersionUnset;
21use crate::operations;
22use crate::operations::BuildLuaError;
23use crate::tree::InstallTree;
24use crate::variables::GetVariableError;
25use crate::{config::Config, package::PackageVersion, variables::HasVariables};
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, Diagnostic)]
44pub enum LuaBinaryError {
45 #[error("neither `lua` nor `luajit` found on the PATH")]
46 LuaBinaryNotFound,
47 #[error(transparent)]
48 #[diagnostic(transparent)]
49 DetectLuaVersion(#[from] DetectLuaVersionError),
50 #[error(
51 r#"
52{} -v (= {}) does not match expected Lua version: {}.
53
54Try setting
55
56```toml
57[variables]
58LUA = "/path/to/lua_binary"
59```
60
61in your config, or use `-v LUA=/path/to/lua_binary`.
62 "#,
63 lua_cmd,
64 installed_version,
65 lua_version
66 )]
67 LuaVersionMismatch {
68 lua_cmd: String,
69 installed_version: PackageVersion,
70 lua_version: LuaVersion,
71 },
72 #[error("{0} not found on the PATH")]
73 CustomBinaryNotFound(String),
74}
75
76#[derive(Error, Debug, Diagnostic)]
77#[non_exhaustive]
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 #[diagnostic(transparent)]
85 PackageVersionParse(#[from] crate::package::PackageVersionParseError),
86 #[error(transparent)]
87 #[diagnostic(transparent)]
88 LuaVersion(#[from] crate::lua_version::LuaVersionError),
89}
90
91#[derive(Error, Debug, Diagnostic)]
92#[non_exhaustive]
93pub enum LuaInstallationError {
94 #[error("could not find a Lua installation and failed to build Lua from source:\n{0}")]
95 #[diagnostic(forward(0))]
96 Build(#[from] BuildLuaError),
97 #[error(transparent)]
98 #[diagnostic(transparent)]
99 LuaVersionUnset(#[from] LuaVersionUnset),
100}
101
102impl LuaInstallation {
103 #[tracing::instrument(level = "trace", skip_all)]
104 pub async fn new_from_config(config: &Config) -> Result<Self, LuaInstallationError> {
105 Self::new(LuaVersion::from(config)?, config).await
106 }
107
108 #[tracing::instrument(level = "trace", skip(config))]
109 pub async fn new(version: &LuaVersion, config: &Config) -> 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).await
139 }
140 }
141
142 #[tracing::instrument(level = "trace")]
143 pub(crate) fn probe(
144 version: &LuaVersion,
145 search_config: &ExternalDependencySearchConfig,
146 ) -> Option<Self> {
147 let pkg_name_probes = match version {
148 LuaVersion::Lua51 => vec!["lua5.1", "lua-5.1"],
149 LuaVersion::Lua52 => vec!["lua5.2", "lua-5.2"],
150 LuaVersion::Lua53 => vec!["lua5.3", "lua-5.3"],
151 LuaVersion::Lua54 => vec!["lua5.4", "lua-5.4"],
152 LuaVersion::Lua55 => vec!["lua5.5", "lua-5.5"],
153 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit"],
154 };
155
156 let mut dependency_info = pkg_name_probes
157 .iter()
158 .map(|pkg_name| {
159 ExternalDependencyInfo::probe(
160 pkg_name,
161 &ExternalDependencySpec::default(),
162 search_config,
163 )
164 })
165 .find_map(Result::ok);
166
167 if let Some(info) = &mut dependency_info {
168 let bin = info.lib_dir.as_ref().and_then(|lib_dir| {
169 lib_dir
170 .parent()
171 .map(|parent| parent.join("bin"))
172 .filter(|dir| dir.is_dir())
173 .and_then(|bin_path| find_lua_executable(&bin_path, version))
174 });
175 let lua_lib_name = info
176 .lib_dir
177 .as_ref()
178 .and_then(|lib_dir| get_lua_lib_name(lib_dir, version));
179 info.lib_name = lua_lib_name;
180 dependency_info.map(|dependency_info| Self {
181 version: version.clone(),
182 dependency_info,
183 bin,
184 })
185 } else {
186 None
187 }
188 }
189
190 #[tracing::instrument(level = "trace", skip(config))]
191 pub async fn install(
192 version: &LuaVersion,
193 config: &Config,
194 ) -> Result<Self, LuaInstallationError> {
195 let _lock = INSTALL_MUTEX.lock().await;
196
197 let target = Self::root_dir(version, config);
198
199 operations::BuildLua::new()
200 .lua_version(version)
201 .install_dir(&target)
202 .config(config)
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> {
261 self.dependency_info.lib_link_args(compiler)
262 }
263
264 pub(crate) fn lua_binary_or_config_override(&self, config: &Config) -> Option<String> {
267 config.variables().get("LUA").cloned().or(self
268 .bin
269 .clone()
270 .or(LuaBinary::new(self.version.clone(), config).try_into().ok())
271 .map(|bin| bin.to_slash_lossy().to_string()))
272 }
273}
274
275impl HasVariables for LuaInstallation {
276 #[tracing::instrument(level = "trace")]
277 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
278 Ok(match input {
279 "LUA_INCDIR" => self
280 .dependency_info
281 .include_dir
282 .as_ref()
283 .map(|dir| format_path(dir)),
284 "LUA_LIBDIR" => self
285 .dependency_info
286 .lib_dir
287 .as_ref()
288 .map(|dir| format_path(dir)),
289 "LUA_BINDIR" => self
290 .bin
291 .as_ref()
292 .and_then(|bin| bin.parent().map(format_path)),
293 "LUA" => self
294 .bin
295 .clone()
296 .or(LuaBinary::Lua {
297 lua_version: self.version.clone(),
298 }
299 .try_into()
300 .ok())
301 .map(|lua| format_path(&lua)),
302 "LUALIB" => self.lua_lib().or(Some("".into())),
303 _ => None,
304 })
305 }
306}
307
308#[derive(Clone)]
309pub enum LuaBinary {
310 Lua { lua_version: LuaVersion },
312 Custom(String),
314}
315
316impl Display for LuaBinary {
317 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
318 match self {
319 LuaBinary::Lua { lua_version } => write!(f, "lua {lua_version}"),
320 LuaBinary::Custom(cmd) => write!(f, "{cmd}"),
321 }
322 }
323}
324
325impl LuaBinary {
326 pub fn new(lua_version: LuaVersion, config: &Config) -> Self {
329 match config.variables().get("LUA").cloned() {
330 Some(lua) => Self::Custom(lua),
331 None => Self::Lua { lua_version },
332 }
333 }
334}
335
336impl From<PathBuf> for LuaBinary {
337 fn from(value: PathBuf) -> Self {
338 Self::Custom(value.to_string_lossy().to_string())
339 }
340}
341
342impl TryFrom<LuaBinary> for PathBuf {
343 type Error = LuaBinaryError;
344
345 fn try_from(value: LuaBinary) -> Result<Self, Self::Error> {
346 match value {
347 LuaBinary::Lua { lua_version } => {
348 if let Some(lua_binary) =
349 LuaInstallation::probe(&lua_version, &ExternalDependencySearchConfig::default())
350 .and_then(|lua_installation| lua_installation.bin)
351 {
352 return Ok(lua_binary);
353 }
354 if lua_version.is_luajit() {
355 if let Ok(path) = which("luajit") {
356 return Ok(path);
357 }
358 }
359 match which(format!("lua{}", lua_version)) {
360 Ok(path) => Ok(path),
361 Err(_) => detect_default_lua_bin(lua_version),
362 }
363 }
364 LuaBinary::Custom(bin) => match which(&bin) {
365 Ok(path) => Ok(path),
366 Err(_) => Err(LuaBinaryError::CustomBinaryNotFound(bin)),
367 },
368 }
369 }
370}
371
372fn detect_default_lua_bin(lua_version: LuaVersion) -> Result<PathBuf, LuaBinaryError> {
373 match which("lua") {
374 Ok(path) => {
375 let installed_version = detect_installed_lua_version_from_path(&path)?;
376 if lua_version
377 .clone()
378 .as_version_req()
379 .matches(&installed_version)
380 {
381 Ok(path)
382 } else {
383 Err(LuaBinaryError::LuaVersionMismatch {
384 lua_cmd: path.to_slash_lossy().to_string(),
385 installed_version,
386 lua_version,
387 })?
388 }
389 }
390 Err(_) => Err(LuaBinaryError::LuaBinaryNotFound),
391 }
392}
393
394pub fn detect_installed_lua_version() -> Option<LuaVersion> {
395 which("lua")
396 .ok()
397 .or(which("luajit").ok())
398 .and_then(|lua_cmd| {
399 detect_installed_lua_version_from_path(&lua_cmd)
400 .ok()
401 .and_then(|version| LuaVersion::from_version(version).ok())
402 })
403}
404
405fn find_lua_executable(bin_path: &Path, version: &LuaVersion) -> Option<PathBuf> {
406 fs::sync::read_dir(bin_path).ok().and_then(|entries| {
407 let bin_files = entries
408 .filter_map(Result::ok)
409 .map(|entry| entry.path().to_path_buf())
410 .collect_vec();
411
412 #[cfg(windows)]
413 let ext = ".exe";
414
415 #[cfg(not(windows))]
416 let ext = "";
417
418 let lua_version_bin = format!("lua{}{}", version, ext);
421 if let Some(lua_bin) = bin_files
422 .iter()
423 .filter(|file| {
424 file.is_executable()
425 && file
426 .file_name()
427 .is_some_and(|name| name.to_string_lossy() == lua_version_bin)
428 })
429 .collect_vec()
430 .first()
431 .cloned()
432 {
433 Some(lua_bin.clone())
434 } else {
435 bin_files
437 .into_iter()
438 .filter(|file| {
439 file.is_executable()
440 && file.file_name().is_some_and(|name| {
441 matches!(
442 name.to_string_lossy().to_string().as_str(),
443 "lua" | "luajit" | "lua.exe" | "luajit.exe"
444 )
445 })
446 })
447 .collect_vec()
448 .first()
449 .cloned()
450 }
451 })
452}
453
454fn is_lua_lib_name(name: &str, lua_version: &LuaVersion) -> bool {
455 let prefixes = match lua_version {
456 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit", "lua"],
457 _ => vec!["lua"],
458 };
459 let version_str = lua_version.version_compatibility_str();
460 let version_suffix = version_str.replace(".", "");
461 #[cfg(target_family = "unix")]
462 let name = name.trim_start_matches("lib");
463 prefixes
464 .iter()
465 .any(|prefix| name == format!("{}.{}", *prefix, c_lib_extension()))
466 || prefixes.iter().any(|prefix| name.starts_with(*prefix))
467 && (name.contains(&version_str) || name.contains(&version_suffix))
468}
469
470fn get_lua_lib_name(lib_dir: &Path, lua_version: &LuaVersion) -> Option<String> {
471 fs::sync::read_dir(lib_dir)
472 .ok()
473 .and_then(|entries| {
474 entries
475 .filter_map(Result::ok)
476 .map(|entry| entry.path().to_path_buf())
477 .filter(|file| file.extension().is_some_and(|ext| ext == c_lib_extension()))
478 .filter(|file| {
479 file.file_name()
480 .is_some_and(|name| is_lua_lib_name(&name.to_string_lossy(), lua_version))
481 })
482 .collect_vec()
483 .first()
484 .cloned()
485 })
486 .map(|file| to_lib_name(&file))
487}
488
489fn detect_installed_lua_version_from_path(
490 lua_cmd: &Path,
491) -> Result<PackageVersion, DetectLuaVersionError> {
492 let output = match std::process::Command::new(lua_cmd).arg("-v").output() {
493 Ok(output) => Ok(output),
494 Err(err) => Err(DetectLuaVersionError::RunLuaCommand(
495 lua_cmd.to_string_lossy().to_string(),
496 err,
497 )),
498 }?;
499 let output_vec = if output.stderr.is_empty() {
500 output.stdout
501 } else {
502 output.stderr
504 };
505 let lua_output = String::from_utf8_lossy(&output_vec).to_string();
506 parse_lua_version_from_output(&lua_output)
507}
508
509fn parse_lua_version_from_output(
510 lua_output: &str,
511) -> Result<PackageVersion, DetectLuaVersionError> {
512 let lua_version_str = lua_output
513 .trim_start_matches("Lua")
514 .trim_start_matches("JIT")
515 .split_whitespace()
516 .next()
517 .map(|s| s.to_string())
518 .ok_or(DetectLuaVersionError::ParseLuaVersion(
519 lua_output.to_string(),
520 ))?;
521 Ok(PackageVersion::parse(&lua_version_str)?)
522}
523
524#[cfg(test)]
525mod tests {
526 use crate::config::ConfigBuilder;
527
528 use super::*;
529
530 #[tokio::test]
531 async fn parse_luajit_version() {
532 let luajit_output =
533 "LuaJIT 2.1.1713773202 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/";
534 parse_lua_version_from_output(luajit_output).unwrap();
535 }
536
537 #[tokio::test]
538 async fn parse_lua_51_version() {
539 let lua_output = "Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio";
540 parse_lua_version_from_output(lua_output).unwrap();
541 }
542
543 #[tokio::test]
544 async fn lua_installation_bin() {
545 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
546 println!("Skipping impure test");
547 return;
548 }
549 let config = ConfigBuilder::new().unwrap().build().unwrap();
550 let lua_version = config.lua_version().unwrap();
551 let lua_installation = LuaInstallation::new(lua_version, &config).await.unwrap();
552 assert!(lua_installation.bin.is_some());
554 let lua_binary: LuaBinary = lua_installation.bin.unwrap().into();
555 let lua_bin_path: PathBuf = lua_binary.try_into().unwrap();
556 let pkg_version = detect_installed_lua_version_from_path(&lua_bin_path).unwrap();
557 assert_eq!(&LuaVersion::from_version(pkg_version).unwrap(), lua_version);
558 }
559
560 #[cfg(not(target_env = "msvc"))]
561 #[tokio::test]
562 async fn test_is_lua_lib_name() {
563 assert!(is_lua_lib_name("lua.a", &LuaVersion::Lua51));
564 assert!(is_lua_lib_name("lua-5.1.a", &LuaVersion::Lua51));
565 assert!(is_lua_lib_name("lua5.1.a", &LuaVersion::Lua51));
566 assert!(is_lua_lib_name("lua51.a", &LuaVersion::Lua51));
567 assert!(!is_lua_lib_name("lua-5.2.a", &LuaVersion::Lua51));
568 assert!(is_lua_lib_name("luajit-5.2.a", &LuaVersion::LuaJIT52));
569 assert!(is_lua_lib_name("lua-5.2.a", &LuaVersion::LuaJIT52));
570 assert!(is_lua_lib_name("liblua.a", &LuaVersion::Lua51));
571 assert!(is_lua_lib_name("liblua-5.1.a", &LuaVersion::Lua51));
572 assert!(is_lua_lib_name("liblua53.a", &LuaVersion::Lua53));
573 assert!(is_lua_lib_name("liblua-54.a", &LuaVersion::Lua54));
574 assert!(is_lua_lib_name("liblua-55.a", &LuaVersion::Lua55));
575 }
576
577 #[cfg(target_env = "msvc")]
578 #[tokio::test]
579 async fn test_is_lua_lib_name() {
580 assert!(is_lua_lib_name("lua.lib", &LuaVersion::Lua51));
581 assert!(is_lua_lib_name("lua-5.1.lib", &LuaVersion::Lua51));
582 assert!(!is_lua_lib_name("lua-5.2.lib", &LuaVersion::Lua51));
583 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
584 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
585 assert!(is_lua_lib_name("luajit-5.2.lib", &LuaVersion::LuaJIT52));
586 assert!(is_lua_lib_name("lua-5.2.lib", &LuaVersion::LuaJIT52));
587 }
588}