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