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