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
29lazy_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 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 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 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 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 Lua { lua_version: LuaVersion },
379 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 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
478pub 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 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 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 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 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}