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