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::config::external_deps::ExternalDependencySearchConfig;
16use crate::fs;
17use crate::lua_rockspec::ExternalDependencySpec;
18use crate::lua_version;
19use crate::lua_version::LuaVersion;
20use crate::lua_version::LuaVersionUnset;
21use crate::operations;
22use crate::operations::BuildLuaError;
23use crate::package;
24use crate::tree::InstallTree;
25use crate::variables::GetVariableError;
26use crate::{config::Config, variables::HasVariables};
27use crate::{path, target};
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, Diagnostic)]
46pub enum LuaBinaryError {
47    #[error("neither `lua` nor `luajit` found on the PATH")]
48    LuaBinaryNotFound,
49    #[error("neither `luau` nor `lune` found on the PATH.")]
50    #[diagnostic(
51        help("ensure luau is installed or run `lx install-lua --lua-version luau`"),
52        url("https://github.com/luau-lang/luau/releases")
53    )]
54    LuauBinaryNotFound,
55    #[error(transparent)]
56    #[diagnostic(transparent)]
57    DetectLuaVersion(#[from] DetectLuaVersionError),
58    #[error(
59        r#"
60{} -v (= {}) does not match expected Lua version: {}.
61
62Try setting
63
64```toml
65[variables]
66LUA = "/path/to/lua_binary"
67```
68
69in your config, or use `-v LUA=/path/to/lua_binary`.
70    "#,
71        lua_cmd,
72        installed_version,
73        lua_version
74    )]
75    LuaVersionMismatch {
76        lua_cmd: String,
77        installed_version: package::Version,
78        lua_version: LuaVersion,
79    },
80    #[error("{0} not found on the PATH")]
81    CustomBinaryNotFound(String),
82}
83
84#[derive(Error, Debug, Diagnostic)]
85#[non_exhaustive]
86pub enum DetectLuaVersionError {
87    #[error("failed to run {0}: {1}")]
88    RunLuaCommand(String, io::Error),
89    #[error("failed to parse Lua version from output: {0}")]
90    ParseLuaVersion(String),
91    #[error(transparent)]
92    #[diagnostic(transparent)]
93    PackageVersionParse(#[from] package::version::ParseError),
94    #[error(transparent)]
95    #[diagnostic(transparent)]
96    LuaVersion(#[from] lua_version::Error),
97}
98
99#[derive(Error, Debug, Diagnostic)]
100#[non_exhaustive]
101pub enum Error {
102    #[error("could not find a Lua installation and failed to build Lua from source")]
103    #[diagnostic(forward(0))]
104    Build(#[from] BuildLuaError),
105    #[error(transparent)]
106    #[diagnostic(transparent)]
107    LuaVersionUnset(#[from] LuaVersionUnset),
108    #[error("`luau` binary not found after building Luau from source")]
109    #[diagnostic(help("this is probably a bug"))]
110    LuauBinaryNotFound,
111}
112
113impl LuaInstallation {
114    #[tracing::instrument(level = "trace", skip_all)]
115    pub async fn new_from_config(config: &Config) -> Result<Self, Error> {
116        Self::new(LuaVersion::from(config)?, config).await
117    }
118
119    #[tracing::instrument(level = "trace", skip(config))]
120    pub async fn new(version: &LuaVersion, config: &Config) -> Result<Self, Error> {
121        let _lock = NEW_MUTEX.lock().await;
122        if let Some(lua_intallation) = Self::probe(version, config.external_deps()) {
123            return Ok(lua_intallation);
124        }
125        if version.is_luau() {
126            if let Ok(bin) = PathBuf::try_from(LuaBinary::new(version.clone(), config)) {
127                return Ok(Self::from_bin_path(version.clone(), bin));
128            }
129            return match Self::luau_from_root_dir(&Self::root_dir(version, config)) {
130                Some(lua_installation) => Ok(lua_installation),
131                None => Self::install(version, config).await,
132            };
133        }
134        let output = Self::root_dir(version, config);
135        let include_dir = output.join("include");
136        let lib_dir = output.join("lib");
137        let lua_lib_name = get_lua_lib_name(&lib_dir, version);
138        if include_dir.is_dir() && lua_lib_name.is_some() {
139            let bin_dir = Some(output.join("bin")).filter(|bin_path| bin_path.is_dir());
140            let bin = bin_dir
141                .as_ref()
142                .and_then(|bin_path| find_lua_executable(bin_path, version));
143            let lib_dir = output.join("lib");
144            let lua_lib_name = get_lua_lib_name(&lib_dir, version);
145            let include_dir = Some(output.join("include"));
146            Ok(LuaInstallation {
147                version: version.clone(),
148                dependency_info: ExternalDependencyInfo {
149                    include_dir,
150                    lib_dir: Some(lib_dir),
151                    bin_dir,
152                    lib_info: None,
153                    lib_name: lua_lib_name,
154                },
155                bin,
156            })
157        } else {
158            Self::install(version, config).await
159        }
160    }
161
162    #[tracing::instrument(level = "trace")]
163    pub(crate) fn probe(
164        version: &LuaVersion,
165        search_config: &ExternalDependencySearchConfig,
166    ) -> Option<Self> {
167        let pkg_name_probes = match version {
168            LuaVersion::Lua51 => vec!["lua5.1", "lua-5.1"],
169            LuaVersion::Lua52 => vec!["lua5.2", "lua-5.2"],
170            LuaVersion::Lua53 => vec!["lua5.3", "lua-5.3"],
171            LuaVersion::Lua54 => vec!["lua5.4", "lua-5.4"],
172            LuaVersion::Lua55 => vec!["lua5.5", "lua-5.5"],
173            LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit"],
174            // NOTE: lune is a standalone binary with no pkg-config metadata or C API
175            LuaVersion::Luau => vec!["luau"],
176        };
177
178        let mut dependency_info = pkg_name_probes
179            .iter()
180            .map(|pkg_name| {
181                ExternalDependencyInfo::probe(
182                    pkg_name,
183                    &ExternalDependencySpec::default(),
184                    search_config,
185                )
186            })
187            .find_map(Result::ok);
188
189        if let Some(info) = &mut dependency_info {
190            if info.include_dir.is_none() || info.lib_dir.is_none() {
191                return None;
192            }
193            let bin = info.lib_dir.as_ref().and_then(|lib_dir| {
194                lib_dir
195                    .parent()
196                    .map(|parent| parent.join("bin"))
197                    .filter(|dir| dir.is_dir())
198                    .and_then(|bin_path| find_lua_executable(&bin_path, version))
199            });
200            let lua_lib_name = info
201                .lib_dir
202                .as_ref()
203                .and_then(|lib_dir| get_lua_lib_name(lib_dir, version));
204            info.lib_name = lua_lib_name;
205            dependency_info.map(|dependency_info| Self {
206                version: version.clone(),
207                dependency_info,
208                bin,
209            })
210        } else {
211            None
212        }
213    }
214
215    #[tracing::instrument(level = "trace", skip(config))]
216    pub async fn install(version: &LuaVersion, config: &Config) -> Result<Self, Error> {
217        let _lock = INSTALL_MUTEX.lock().await;
218
219        let target = Self::root_dir(version, config);
220
221        operations::BuildLua::new()
222            .lua_version(version)
223            .install_dir(&target)
224            .config(config)
225            .build()
226            .await?;
227
228        if version.is_luau() {
229            return Self::luau_from_root_dir(&target).ok_or(Error::LuauBinaryNotFound);
230        }
231
232        let include_dir = target.join("include");
233        let lib_dir = target.join("lib");
234        let bin_dir = Some(target.join("bin")).filter(|bin_path| bin_path.is_dir());
235        let bin = bin_dir
236            .as_ref()
237            .and_then(|bin_path| find_lua_executable(bin_path, version));
238        let lua_lib_name = get_lua_lib_name(&lib_dir, version);
239        Ok(LuaInstallation {
240            version: version.clone(),
241            dependency_info: ExternalDependencyInfo {
242                include_dir: Some(include_dir),
243                lib_dir: Some(lib_dir),
244                bin_dir,
245                lib_info: None,
246                lib_name: lua_lib_name,
247            },
248            bin,
249        })
250    }
251
252    pub fn includes(&self) -> Vec<&PathBuf> {
253        self.dependency_info.include_dir.iter().collect_vec()
254    }
255
256    pub fn bin(&self) -> &Option<PathBuf> {
257        &self.bin
258    }
259
260    fn luau_from_root_dir(root: &Path) -> Option<Self> {
261        let bin_dir = root.join("bin");
262        let bin = find_lua_executable(&bin_dir, &LuaVersion::Luau)?;
263        Some(Self {
264            version: LuaVersion::Luau,
265            dependency_info: ExternalDependencyInfo {
266                include_dir: None,
267                lib_dir: None,
268                bin_dir: bin.parent().map(Path::to_path_buf),
269                lib_info: None,
270                lib_name: None,
271            },
272            bin: Some(bin),
273        })
274    }
275
276    /// A Lua installation referencing a binary found on the PATH,
277    /// without probing for headers/libraries or building from source.
278    /// Used for installations that don't link against the Lua C API.
279    fn from_bin_path(version: LuaVersion, bin: PathBuf) -> Self {
280        Self {
281            version,
282            dependency_info: ExternalDependencyInfo {
283                include_dir: None,
284                lib_dir: None,
285                bin_dir: bin.parent().map(Path::to_path_buf),
286                lib_info: None,
287                lib_name: None,
288            },
289            bin: Some(bin),
290        }
291    }
292
293    fn root_dir(version: &LuaVersion, config: &Config) -> PathBuf {
294        if let Some(lua_dir) = config.lua_dir() {
295            return lua_dir.clone();
296        } else if let Ok(tree) = config.user_tree(version.clone()) {
297            return tree.root().join(".lua");
298        }
299        config.data_dir().join(".lua").join(version.to_string())
300    }
301
302    #[cfg(not(target_env = "msvc"))]
303    fn lua_lib(&self) -> Option<String> {
304        self.dependency_info
305            .lib_name
306            .as_ref()
307            .map(|name| format!("{}.{}", name, target::c_lib_extension()))
308    }
309
310    #[cfg(target_env = "msvc")]
311    fn lua_lib(&self) -> Option<String> {
312        self.dependency_info.lib_name.clone()
313    }
314
315    pub(crate) fn define_flags(&self) -> Vec<String> {
316        let mut defines = self.dependency_info.define_flags();
317        if self.version.is_luajit() {
318            defines.push("-DLUAJIT_VERSION".into());
319        }
320        defines
321    }
322
323    pub(crate) fn lib_link_args(&self, compiler: &cc::Tool) -> Vec<String> {
324        self.dependency_info.lib_link_args(compiler)
325    }
326
327    /// Get the Lua binary (if present), prioritising
328    /// a potentially overridden value in the config.
329    pub(crate) fn lua_binary_or_config_override(&self, config: &Config) -> Option<String> {
330        let variable = if self.version.is_luau() {
331            "LUAU"
332        } else {
333            "LUA"
334        };
335        config.variables().get(variable).cloned().or(self
336            .bin
337            .clone()
338            .or(LuaBinary::new(self.version.clone(), config).try_into().ok())
339            .map(|bin| bin.to_slash_lossy().to_string()))
340    }
341}
342
343impl HasVariables for LuaInstallation {
344    #[tracing::instrument(level = "trace")]
345    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
346        Ok(match input {
347            "LUA_INCDIR" => self
348                .dependency_info
349                .include_dir
350                .as_ref()
351                .map(|dir| path::quoted(dir)),
352            "LUA_LIBDIR" => self
353                .dependency_info
354                .lib_dir
355                .as_ref()
356                .map(|dir| path::quoted(dir)),
357            "LUA_BINDIR" => self
358                .bin
359                .as_ref()
360                .and_then(|bin| bin.parent().map(path::quoted)),
361            "LUA" => self
362                .bin
363                .clone()
364                .or(LuaBinary::Lua {
365                    lua_version: self.version.clone(),
366                }
367                .try_into()
368                .ok())
369                .map(|lua| path::quoted(&lua)),
370            "LUAU" if self.version.is_luau() => self.bin.as_ref().map(|bin| path::quoted(bin)),
371            "LUALIB" => self.lua_lib().or(Some("".into())),
372            _ => None,
373        })
374    }
375}
376
377#[derive(Clone)]
378pub enum LuaBinary {
379    /// The regular Lua interpreter.
380    Lua { lua_version: LuaVersion },
381    /// Custom Lua interpreter.
382    Custom(String),
383}
384
385impl Display for LuaBinary {
386    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
387        match self {
388            LuaBinary::Lua { lua_version } => write!(f, "lua {lua_version}"),
389            LuaBinary::Custom(cmd) => write!(f, "{cmd}"),
390        }
391    }
392}
393
394impl LuaBinary {
395    /// Construct a new `LuaBinary` for the given `LuaVersion`,
396    /// potentially prioritising an overridden value in the config.
397    pub fn new(lua_version: LuaVersion, config: &Config) -> Self {
398        let variable = if lua_version.is_luau() { "LUAU" } else { "LUA" };
399        match config.variables().get(variable).cloned() {
400            Some(bin) => Self::Custom(bin),
401            None => Self::Lua { lua_version },
402        }
403    }
404}
405
406impl From<PathBuf> for LuaBinary {
407    fn from(value: PathBuf) -> Self {
408        Self::Custom(value.to_string_lossy().to_string())
409    }
410}
411
412impl TryFrom<LuaBinary> for PathBuf {
413    type Error = LuaBinaryError;
414
415    fn try_from(value: LuaBinary) -> Result<Self, Self::Error> {
416        match value {
417            LuaBinary::Lua { lua_version } => {
418                if lua_version.is_luau() {
419                    return which("luau")
420                        .or_else(|_| which("lune"))
421                        .map_err(|_| LuaBinaryError::LuauBinaryNotFound);
422                }
423                if let Some(lua_binary) =
424                    LuaInstallation::probe(&lua_version, &ExternalDependencySearchConfig::default())
425                        .and_then(|lua_installation| lua_installation.bin)
426                {
427                    return Ok(lua_binary);
428                }
429                if lua_version.is_luajit() {
430                    if let Ok(path) = which("luajit") {
431                        return Ok(path);
432                    }
433                }
434                match which(format!("lua{}", lua_version)) {
435                    Ok(path) => Ok(path),
436                    Err(_) => detect_default_lua_bin(lua_version),
437                }
438            }
439            LuaBinary::Custom(bin) => match which(&bin) {
440                Ok(path) => Ok(path),
441                Err(_) => Err(LuaBinaryError::CustomBinaryNotFound(bin)),
442            },
443        }
444    }
445}
446
447fn detect_default_lua_bin(lua_version: LuaVersion) -> Result<PathBuf, LuaBinaryError> {
448    match which("lua") {
449        Ok(path) => {
450            let installed_version = detect_installed_lua_version_from_path(&path)?;
451            if lua_version
452                .clone()
453                .as_version_req()
454                .matches(&installed_version)
455            {
456                Ok(path)
457            } else {
458                Err(LuaBinaryError::LuaVersionMismatch {
459                    lua_cmd: path.to_slash_lossy().to_string(),
460                    installed_version,
461                    lua_version,
462                })?
463            }
464        }
465        Err(_) => Err(LuaBinaryError::LuaBinaryNotFound),
466    }
467}
468
469pub fn detect_installed_lua_version() -> Option<LuaVersion> {
470    which("lua")
471        .ok()
472        .or(which("luajit").ok())
473        .and_then(|lua_cmd| {
474            detect_installed_lua_version_from_path(&lua_cmd)
475                .ok()
476                .and_then(|version| LuaVersion::from_version(version).ok())
477        })
478}
479
480/// Determine the Lua version to use with the `nvim` preset.
481/// Prefers LuaJIT if installed, falling back to Lua 5.1.
482pub fn nvim_lua_version() -> Option<LuaVersion> {
483    if LuaInstallation::probe(&LuaVersion::LuaJIT, &Default::default()).is_some() {
484        Some(LuaVersion::LuaJIT)
485    } else if LuaInstallation::probe(&LuaVersion::Lua51, &Default::default()).is_some() {
486        Some(LuaVersion::Lua51)
487    } else {
488        None
489    }
490}
491
492fn find_lua_executable(bin_path: &Path, version: &LuaVersion) -> Option<PathBuf> {
493    fs::sync::read_dir(bin_path).ok().and_then(|entries| {
494        let bin_files = entries
495            .filter_map(Result::ok)
496            .map(|entry| entry.path().to_path_buf())
497            .collect_vec();
498
499        #[cfg(windows)]
500        let ext = ".exe";
501
502        #[cfg(not(windows))]
503        let ext = "";
504
505        if version.is_luau() {
506            let luau_bin = format!("luau{ext}");
507            return bin_files
508                .into_iter()
509                .filter(|file| {
510                    file.is_executable()
511                        && file
512                            .file_name()
513                            .is_some_and(|name| name.to_string_lossy() == luau_bin)
514                })
515                .collect_vec()
516                .first()
517                .cloned();
518        }
519
520        // Prioritise Lua binaries with version suffix
521        // (see https://github.com/lumen-oss/lux/issues/1215)
522        let lua_version_bin = format!("lua{}{}", version, ext);
523        if let Some(lua_bin) = bin_files
524            .iter()
525            .filter(|file| {
526                file.is_executable()
527                    && file
528                        .file_name()
529                        .is_some_and(|name| name.to_string_lossy() == lua_version_bin)
530            })
531            .collect_vec()
532            .first()
533            .cloned()
534        {
535            Some(lua_bin.clone())
536        } else {
537            // Fall back to Lua binary without version suffix
538            bin_files
539                .into_iter()
540                .filter(|file| {
541                    file.is_executable()
542                        && file.file_name().is_some_and(|name| {
543                            matches!(
544                                name.to_string_lossy().to_string().as_str(),
545                                "lua" | "luajit" | "lua.exe" | "luajit.exe"
546                            )
547                        })
548                })
549                .collect_vec()
550                .first()
551                .cloned()
552        }
553    })
554}
555
556fn is_lua_lib_name(name: &str, lua_version: &LuaVersion) -> bool {
557    let prefixes = match lua_version {
558        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit", "lua"],
559        _ => vec!["lua"],
560    };
561    let version_str = lua_version.version_compatibility_str();
562    let version_suffix = version_str.replace(".", "");
563    #[cfg(target_family = "unix")]
564    let name = name.trim_start_matches("lib");
565    prefixes
566        .iter()
567        .any(|prefix| name == format!("{}.{}", *prefix, target::c_lib_extension()))
568        || prefixes.iter().any(|prefix| name.starts_with(*prefix))
569            && (name.contains(&version_str) || name.contains(&version_suffix))
570}
571
572fn get_lua_lib_name(lib_dir: &Path, lua_version: &LuaVersion) -> Option<String> {
573    fs::sync::read_dir(lib_dir)
574        .ok()
575        .and_then(|entries| {
576            entries
577                .filter_map(Result::ok)
578                .map(|entry| entry.path().to_path_buf())
579                .filter(|file| {
580                    file.extension()
581                        .is_some_and(|ext| ext == target::c_lib_extension())
582                })
583                .filter(|file| {
584                    file.file_name()
585                        .is_some_and(|name| is_lua_lib_name(&name.to_string_lossy(), lua_version))
586                })
587                .collect_vec()
588                .first()
589                .cloned()
590        })
591        .map(|file| to_lib_name(&file))
592}
593
594fn detect_installed_lua_version_from_path(
595    lua_cmd: &Path,
596) -> Result<package::Version, DetectLuaVersionError> {
597    let output = match std::process::Command::new(lua_cmd).arg("-v").output() {
598        Ok(output) => Ok(output),
599        Err(err) => Err(DetectLuaVersionError::RunLuaCommand(
600            lua_cmd.to_string_lossy().to_string(),
601            err,
602        )),
603    }?;
604    let output_vec = if output.stderr.is_empty() {
605        output.stdout
606    } else {
607        // Yes, Lua 5.1 prints to stderr (-‸ლ)
608        output.stderr
609    };
610    let lua_output = String::from_utf8_lossy(&output_vec).to_string();
611    parse_lua_version_from_output(&lua_output)
612}
613
614fn parse_lua_version_from_output(
615    lua_output: &str,
616) -> Result<package::Version, DetectLuaVersionError> {
617    let lua_version_str = lua_output
618        .trim_start_matches("Lua")
619        .trim_start_matches("JIT")
620        .split_whitespace()
621        .next()
622        .map(|s| s.to_string())
623        .ok_or(DetectLuaVersionError::ParseLuaVersion(
624            lua_output.to_string(),
625        ))?;
626    Ok(package::Version::parse(&lua_version_str)?)
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632    #[cfg(feature = "impure_tests")]
633    use flaky_test::flaky_test;
634
635    #[tokio::test]
636    async fn parse_luajit_version() {
637        let luajit_output =
638            "LuaJIT 2.1.1713773202 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/";
639        parse_lua_version_from_output(luajit_output).unwrap();
640    }
641
642    #[tokio::test]
643    async fn parse_lua_51_version() {
644        let lua_output = "Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio";
645        parse_lua_version_from_output(lua_output).unwrap();
646    }
647
648    #[cfg(feature = "impure_tests")]
649    #[flaky_test(tokio, times = 5)]
650    async fn lua_installation_bin() {
651        let config = crate::config::ConfigBuilder::new()
652            .unwrap()
653            .build()
654            .unwrap();
655        let lua_version = config.lua_version().unwrap();
656        let lua_installation = LuaInstallation::new(lua_version, &config).await.unwrap();
657        // FIXME: This fails when run in the nix checkPhase
658        assert!(lua_installation.bin.is_some());
659        let lua_binary: LuaBinary = lua_installation.bin.unwrap().into();
660        let lua_bin_path: PathBuf = lua_binary.try_into().unwrap();
661        let pkg_version = detect_installed_lua_version_from_path(&lua_bin_path).unwrap();
662        assert_eq!(&LuaVersion::from_version(pkg_version).unwrap(), lua_version);
663    }
664
665    #[cfg(not(target_env = "msvc"))]
666    #[tokio::test]
667    async fn test_is_lua_lib_name() {
668        assert!(is_lua_lib_name("lua.a", &LuaVersion::Lua51));
669        assert!(is_lua_lib_name("lua-5.1.a", &LuaVersion::Lua51));
670        assert!(is_lua_lib_name("lua5.1.a", &LuaVersion::Lua51));
671        assert!(is_lua_lib_name("lua51.a", &LuaVersion::Lua51));
672        assert!(!is_lua_lib_name("lua-5.2.a", &LuaVersion::Lua51));
673        assert!(is_lua_lib_name("luajit-5.2.a", &LuaVersion::LuaJIT52));
674        assert!(is_lua_lib_name("lua-5.2.a", &LuaVersion::LuaJIT52));
675        assert!(is_lua_lib_name("liblua.a", &LuaVersion::Lua51));
676        assert!(is_lua_lib_name("liblua-5.1.a", &LuaVersion::Lua51));
677        assert!(is_lua_lib_name("liblua53.a", &LuaVersion::Lua53));
678        assert!(is_lua_lib_name("liblua-54.a", &LuaVersion::Lua54));
679        assert!(is_lua_lib_name("liblua-55.a", &LuaVersion::Lua55));
680    }
681
682    #[cfg(target_env = "msvc")]
683    #[tokio::test]
684    async fn test_is_lua_lib_name() {
685        assert!(is_lua_lib_name("lua.lib", &LuaVersion::Lua51));
686        assert!(is_lua_lib_name("lua-5.1.lib", &LuaVersion::Lua51));
687        assert!(!is_lua_lib_name("lua-5.2.lib", &LuaVersion::Lua51));
688        assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
689        assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
690        assert!(is_lua_lib_name("luajit-5.2.lib", &LuaVersion::LuaJIT52));
691        assert!(is_lua_lib_name("lua-5.2.lib", &LuaVersion::LuaJIT52));
692    }
693}