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