Skip to main content

lux_lib/lua_installation/
mod.rs

1use 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 thiserror::Error;
10use which::which;
11
12use crate::build::external_dependency::to_lib_name;
13use crate::build::external_dependency::ExternalDependencyInfo;
14use crate::build::utils::{c_lib_extension, format_path};
15use crate::config::external_deps::ExternalDependencySearchConfig;
16use crate::lua_rockspec::ExternalDependencySpec;
17use crate::lua_version::LuaVersion;
18use crate::lua_version::LuaVersionUnset;
19use crate::operations;
20use crate::operations::BuildLuaError;
21use crate::progress::Progress;
22use crate::progress::ProgressBar;
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)]
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        r#"
50{} -v (= {}) does not match expected Lua version: {}.
51
52Try setting
53
54```toml
55[variables]
56LUA = "/path/to/lua_binary"
57```
58
59in your config, or use `-v LUA=/path/to/lua_binary`.
60    "#,
61        lua_cmd,
62        installed_version,
63        lua_version
64    )]
65    LuaVersionMismatch {
66        lua_cmd: String,
67        installed_version: PackageVersion,
68        lua_version: LuaVersion,
69    },
70    #[error("{0} not found on the PATH")]
71    CustomBinaryNotFound(String),
72}
73
74#[derive(Error, Debug)]
75pub enum DetectLuaVersionError {
76    #[error("failed to run {0}: {1}")]
77    RunLuaCommand(String, io::Error),
78    #[error("failed to parse Lua version from output: {0}")]
79    ParseLuaVersion(String),
80    #[error(transparent)]
81    PackageVersionParse(#[from] crate::package::PackageVersionParseError),
82    #[error(transparent)]
83    LuaVersion(#[from] crate::lua_version::LuaVersionError),
84}
85
86#[derive(Error, Debug)]
87pub enum LuaInstallationError {
88    #[error("could not find a Lua installation and failed to build Lua from source:\n{0}")]
89    Build(#[from] BuildLuaError),
90    #[error(transparent)]
91    LuaVersionUnset(#[from] LuaVersionUnset),
92}
93
94impl LuaInstallation {
95    pub async fn new_from_config(
96        config: &Config,
97        progress: &Progress<ProgressBar>,
98    ) -> Result<Self, LuaInstallationError> {
99        Self::new(LuaVersion::from(config)?, config, progress).await
100    }
101
102    pub async fn new(
103        version: &LuaVersion,
104        config: &Config,
105        progress: &Progress<ProgressBar>,
106    ) -> Result<Self, LuaInstallationError> {
107        let _lock = NEW_MUTEX.lock().await;
108        if let Some(lua_intallation) = Self::probe(version, config.external_deps()) {
109            return Ok(lua_intallation);
110        }
111        let output = Self::root_dir(version, config);
112        let include_dir = output.join("include");
113        let lib_dir = output.join("lib");
114        let lua_lib_name = get_lua_lib_name(&lib_dir, version);
115        if include_dir.is_dir() && lua_lib_name.is_some() {
116            let bin_dir = Some(output.join("bin")).filter(|bin_path| bin_path.is_dir());
117            let bin = bin_dir
118                .as_ref()
119                .and_then(|bin_path| find_lua_executable(bin_path, version));
120            let lib_dir = output.join("lib");
121            let lua_lib_name = get_lua_lib_name(&lib_dir, version);
122            let include_dir = Some(output.join("include"));
123            Ok(LuaInstallation {
124                version: version.clone(),
125                dependency_info: ExternalDependencyInfo {
126                    include_dir,
127                    lib_dir: Some(lib_dir),
128                    bin_dir,
129                    lib_info: None,
130                    lib_name: lua_lib_name,
131                },
132                bin,
133            })
134        } else {
135            Self::install(version, config, progress).await
136        }
137    }
138
139    pub(crate) fn probe(
140        version: &LuaVersion,
141        search_config: &ExternalDependencySearchConfig,
142    ) -> Option<Self> {
143        let pkg_name_probes = match version {
144            LuaVersion::Lua51 => vec!["lua5.1", "lua-5.1"],
145            LuaVersion::Lua52 => vec!["lua5.2", "lua-5.2"],
146            LuaVersion::Lua53 => vec!["lua5.3", "lua-5.3"],
147            LuaVersion::Lua54 => vec!["lua5.4", "lua-5.4"],
148            LuaVersion::Lua55 => vec!["lua5.5", "lua-5.5"],
149            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit"],
150        };
151
152        let mut dependency_info = pkg_name_probes
153            .iter()
154            .map(|pkg_name| {
155                ExternalDependencyInfo::probe(
156                    pkg_name,
157                    &ExternalDependencySpec::default(),
158                    search_config,
159                )
160            })
161            .find_map(Result::ok);
162
163        if let Some(info) = &mut dependency_info {
164            let bin = info.lib_dir.as_ref().and_then(|lib_dir| {
165                lib_dir
166                    .parent()
167                    .map(|parent| parent.join("bin"))
168                    .filter(|dir| dir.is_dir())
169                    .and_then(|bin_path| find_lua_executable(&bin_path, version))
170            });
171            let lua_lib_name = info
172                .lib_dir
173                .as_ref()
174                .and_then(|lib_dir| get_lua_lib_name(lib_dir, version));
175            info.lib_name = lua_lib_name;
176            dependency_info.map(|dependency_info| Self {
177                version: version.clone(),
178                dependency_info,
179                bin,
180            })
181        } else {
182            None
183        }
184    }
185
186    pub async fn install(
187        version: &LuaVersion,
188        config: &Config,
189        progress: &Progress<ProgressBar>,
190    ) -> Result<Self, LuaInstallationError> {
191        let _lock = INSTALL_MUTEX.lock().await;
192
193        let target = Self::root_dir(version, config);
194
195        operations::BuildLua::new()
196            .lua_version(version)
197            .install_dir(&target)
198            .config(config)
199            .progress(progress)
200            .build()
201            .await?;
202
203        let include_dir = target.join("include");
204        let lib_dir = target.join("lib");
205        let bin_dir = Some(target.join("bin")).filter(|bin_path| bin_path.is_dir());
206        let bin = bin_dir
207            .as_ref()
208            .and_then(|bin_path| find_lua_executable(bin_path, version));
209        let lua_lib_name = get_lua_lib_name(&lib_dir, version);
210        Ok(LuaInstallation {
211            version: version.clone(),
212            dependency_info: ExternalDependencyInfo {
213                include_dir: Some(include_dir),
214                lib_dir: Some(lib_dir),
215                bin_dir,
216                lib_info: None,
217                lib_name: lua_lib_name,
218            },
219            bin,
220        })
221    }
222
223    pub fn includes(&self) -> Vec<&PathBuf> {
224        self.dependency_info.include_dir.iter().collect_vec()
225    }
226
227    pub fn bin(&self) -> &Option<PathBuf> {
228        &self.bin
229    }
230
231    fn root_dir(version: &LuaVersion, config: &Config) -> PathBuf {
232        if let Some(lua_dir) = config.lua_dir() {
233            return lua_dir.clone();
234        } else if let Ok(tree) = config.user_tree(version.clone()) {
235            return tree.root().join(".lua");
236        }
237        config.data_dir().join(".lua").join(version.to_string())
238    }
239
240    #[cfg(not(target_env = "msvc"))]
241    fn lua_lib(&self) -> Option<String> {
242        self.dependency_info
243            .lib_name
244            .as_ref()
245            .map(|name| format!("{}.{}", name, c_lib_extension()))
246    }
247
248    #[cfg(target_env = "msvc")]
249    fn lua_lib(&self) -> Option<String> {
250        self.dependency_info.lib_name.clone()
251    }
252
253    pub(crate) fn define_flags(&self) -> Vec<String> {
254        self.dependency_info.define_flags()
255    }
256
257    /// NOTE: In luarocks, these are behind a link_lua_explicity config option
258    pub(crate) fn lib_link_args(&self, compiler: &cc::Tool) -> Vec<String> {
259        if cfg!(target_os = "macos") {
260            // On macos, linking Lua can lead to duplicate symbol errors
261            Vec::new()
262        } else {
263            self.dependency_info.lib_link_args(compiler)
264        }
265    }
266
267    /// Get the Lua binary (if present), prioritising
268    /// a potentially overridden value in the config.
269    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    /// The regular Lua interpreter.
313    Lua { lua_version: LuaVersion },
314    /// Custom Lua interpreter.
315    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    /// Construct a new `LuaBinary` for the given `LuaVersion`,
329    /// potentially prioritising an overridden value in the config.
330    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(format!("lua{}", lua_version)) {
362                    Ok(path) => Ok(path),
363                    Err(_) => detect_default_lua_bin(lua_version),
364                }
365            }
366            LuaBinary::Custom(bin) => match which(&bin) {
367                Ok(path) => Ok(path),
368                Err(_) => Err(LuaBinaryError::CustomBinaryNotFound(bin)),
369            },
370        }
371    }
372}
373
374fn detect_default_lua_bin(lua_version: LuaVersion) -> Result<PathBuf, LuaBinaryError> {
375    match which("lua") {
376        Ok(path) => {
377            let installed_version = detect_installed_lua_version_from_path(&path)?;
378            if lua_version
379                .clone()
380                .as_version_req()
381                .matches(&installed_version)
382            {
383                Ok(path)
384            } else {
385                Err(LuaBinaryError::LuaVersionMismatch {
386                    lua_cmd: path.to_slash_lossy().to_string(),
387                    installed_version,
388                    lua_version,
389                })?
390            }
391        }
392        Err(_) => Err(LuaBinaryError::LuaBinaryNotFound),
393    }
394}
395
396pub fn detect_installed_lua_version() -> Option<LuaVersion> {
397    which("lua")
398        .ok()
399        .or(which("luajit").ok())
400        .and_then(|lua_cmd| {
401            detect_installed_lua_version_from_path(&lua_cmd)
402                .ok()
403                .and_then(|version| LuaVersion::from_version(version).ok())
404        })
405}
406
407fn find_lua_executable(bin_path: &Path, version: &LuaVersion) -> Option<PathBuf> {
408    std::fs::read_dir(bin_path).ok().and_then(|entries| {
409        let bin_files = entries
410            .filter_map(Result::ok)
411            .map(|entry| entry.path().to_path_buf())
412            .collect_vec();
413
414        #[cfg(windows)]
415        let ext = ".exe";
416
417        #[cfg(not(windows))]
418        let ext = "";
419
420        // Prioritise Lua binaries with version suffix
421        // (see https://github.com/lumen-oss/lux/issues/1215)
422        let lua_version_bin = format!("lua{}{}", version, ext);
423        if let Some(lua_bin) = bin_files
424            .iter()
425            .filter(|file| {
426                file.is_executable()
427                    && file
428                        .file_name()
429                        .is_some_and(|name| name.to_string_lossy() == lua_version_bin)
430            })
431            .collect_vec()
432            .first()
433            .cloned()
434        {
435            Some(lua_bin.clone())
436        } else {
437            // Fall back to Lua binary without version suffix
438            bin_files
439                .into_iter()
440                .filter(|file| {
441                    file.is_executable()
442                        && file.file_name().is_some_and(|name| {
443                            matches!(
444                                name.to_string_lossy().to_string().as_str(),
445                                "lua" | "luajit" | "lua.exe" | "luajit.exe"
446                            )
447                        })
448                })
449                .collect_vec()
450                .first()
451                .cloned()
452        }
453    })
454}
455
456fn is_lua_lib_name(name: &str, lua_version: &LuaVersion) -> bool {
457    let prefixes = match lua_version {
458        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit", "lua"],
459        _ => vec!["lua"],
460    };
461    let version_str = lua_version.version_compatibility_str();
462    let version_suffix = version_str.replace(".", "");
463    #[cfg(target_family = "unix")]
464    let name = name.trim_start_matches("lib");
465    prefixes
466        .iter()
467        .any(|prefix| name == format!("{}.{}", *prefix, c_lib_extension()))
468        || prefixes.iter().any(|prefix| name.starts_with(*prefix))
469            && (name.contains(&version_str) || name.contains(&version_suffix))
470}
471
472fn get_lua_lib_name(lib_dir: &Path, lua_version: &LuaVersion) -> Option<String> {
473    std::fs::read_dir(lib_dir)
474        .ok()
475        .and_then(|entries| {
476            entries
477                .filter_map(Result::ok)
478                .map(|entry| entry.path().to_path_buf())
479                .filter(|file| file.extension().is_some_and(|ext| ext == c_lib_extension()))
480                .filter(|file| {
481                    file.file_name()
482                        .is_some_and(|name| is_lua_lib_name(&name.to_string_lossy(), lua_version))
483                })
484                .collect_vec()
485                .first()
486                .cloned()
487        })
488        .map(|file| to_lib_name(&file))
489}
490
491fn detect_installed_lua_version_from_path(
492    lua_cmd: &Path,
493) -> Result<PackageVersion, DetectLuaVersionError> {
494    let output = match std::process::Command::new(lua_cmd).arg("-v").output() {
495        Ok(output) => Ok(output),
496        Err(err) => Err(DetectLuaVersionError::RunLuaCommand(
497            lua_cmd.to_string_lossy().to_string(),
498            err,
499        )),
500    }?;
501    let output_vec = if output.stderr.is_empty() {
502        output.stdout
503    } else {
504        // Yes, Lua 5.1 prints to stderr (-‸ლ)
505        output.stderr
506    };
507    let lua_output = String::from_utf8_lossy(&output_vec).to_string();
508    parse_lua_version_from_output(&lua_output)
509}
510
511fn parse_lua_version_from_output(
512    lua_output: &str,
513) -> Result<PackageVersion, DetectLuaVersionError> {
514    let lua_version_str = lua_output
515        .trim_start_matches("Lua")
516        .trim_start_matches("JIT")
517        .split_whitespace()
518        .next()
519        .map(|s| s.to_string())
520        .ok_or(DetectLuaVersionError::ParseLuaVersion(
521            lua_output.to_string(),
522        ))?;
523    Ok(PackageVersion::parse(&lua_version_str)?)
524}
525
526#[cfg(test)]
527mod test {
528    use crate::{config::ConfigBuilder, progress::MultiProgress};
529
530    use super::*;
531
532    #[tokio::test]
533    async fn parse_luajit_version() {
534        let luajit_output =
535            "LuaJIT 2.1.1713773202 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/";
536        parse_lua_version_from_output(luajit_output).unwrap();
537    }
538
539    #[tokio::test]
540    async fn parse_lua_51_version() {
541        let lua_output = "Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio";
542        parse_lua_version_from_output(lua_output).unwrap();
543    }
544
545    #[tokio::test]
546    async fn lua_installation_bin() {
547        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
548            println!("Skipping impure test");
549            return;
550        }
551        let config = ConfigBuilder::new().unwrap().build().unwrap();
552        let lua_version = config.lua_version().unwrap();
553        let progress = MultiProgress::new(&config);
554        let bar = progress.map(MultiProgress::new_bar);
555        let lua_installation = LuaInstallation::new(lua_version, &config, &bar)
556            .await
557            .unwrap();
558        // FIXME: This fails when run in the nix checkPhase
559        assert!(lua_installation.bin.is_some());
560        let lua_binary: LuaBinary = lua_installation.bin.unwrap().into();
561        let lua_bin_path: PathBuf = lua_binary.try_into().unwrap();
562        let pkg_version = detect_installed_lua_version_from_path(&lua_bin_path).unwrap();
563        assert_eq!(&LuaVersion::from_version(pkg_version).unwrap(), lua_version);
564    }
565
566    #[cfg(not(target_env = "msvc"))]
567    #[tokio::test]
568    async fn test_is_lua_lib_name() {
569        assert!(is_lua_lib_name("lua.a", &LuaVersion::Lua51));
570        assert!(is_lua_lib_name("lua-5.1.a", &LuaVersion::Lua51));
571        assert!(is_lua_lib_name("lua5.1.a", &LuaVersion::Lua51));
572        assert!(is_lua_lib_name("lua51.a", &LuaVersion::Lua51));
573        assert!(!is_lua_lib_name("lua-5.2.a", &LuaVersion::Lua51));
574        assert!(is_lua_lib_name("luajit-5.2.a", &LuaVersion::LuaJIT52));
575        assert!(is_lua_lib_name("lua-5.2.a", &LuaVersion::LuaJIT52));
576        assert!(is_lua_lib_name("liblua.a", &LuaVersion::Lua51));
577        assert!(is_lua_lib_name("liblua-5.1.a", &LuaVersion::Lua51));
578        assert!(is_lua_lib_name("liblua53.a", &LuaVersion::Lua53));
579        assert!(is_lua_lib_name("liblua-54.a", &LuaVersion::Lua54));
580        assert!(is_lua_lib_name("liblua-55.a", &LuaVersion::Lua55));
581    }
582
583    #[cfg(target_env = "msvc")]
584    #[tokio::test]
585    async fn test_is_lua_lib_name() {
586        assert!(is_lua_lib_name("lua.lib", &LuaVersion::Lua51));
587        assert!(is_lua_lib_name("lua-5.1.lib", &LuaVersion::Lua51));
588        assert!(!is_lua_lib_name("lua-5.2.lib", &LuaVersion::Lua51));
589        assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
590        assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
591        assert!(is_lua_lib_name("luajit-5.2.lib", &LuaVersion::LuaJIT52));
592        assert!(is_lua_lib_name("lua-5.2.lib", &LuaVersion::LuaJIT52));
593    }
594}