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