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