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