Skip to main content

lux_lib/lua_installation/
mod.rs

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