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