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::build::utils::{c_lib_extension, format_path};
16use crate::config::external_deps::ExternalDependencySearchConfig;
17use crate::fs;
18use crate::lua_rockspec::ExternalDependencySpec;
19use crate::lua_version::LuaVersion;
20use crate::lua_version::LuaVersionUnset;
21use crate::operations;
22use crate::operations::BuildLuaError;
23use crate::tree::InstallTree;
24use crate::variables::GetVariableError;
25use crate::{config::Config, package::PackageVersion, variables::HasVariables};
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 LuaInstallationError {
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, LuaInstallationError> {
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, LuaInstallationError> {
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(
215 version: &LuaVersion,
216 config: &Config,
217 ) -> Result<Self, LuaInstallationError> {
218 let _lock = INSTALL_MUTEX.lock().await;
219
220 let target = Self::root_dir(version, config);
221
222 operations::BuildLua::new()
223 .lua_version(version)
224 .install_dir(&target)
225 .config(config)
226 .build()
227 .await?;
228
229 if version.is_luau() {
230 return Self::luau_from_root_dir(&target)
231 .ok_or(LuaInstallationError::LuauBinaryNotFound);
232 }
233
234 let include_dir = target.join("include");
235 let lib_dir = target.join("lib");
236 let bin_dir = Some(target.join("bin")).filter(|bin_path| bin_path.is_dir());
237 let bin = bin_dir
238 .as_ref()
239 .and_then(|bin_path| find_lua_executable(bin_path, version));
240 let lua_lib_name = get_lua_lib_name(&lib_dir, version);
241 Ok(LuaInstallation {
242 version: version.clone(),
243 dependency_info: ExternalDependencyInfo {
244 include_dir: Some(include_dir),
245 lib_dir: Some(lib_dir),
246 bin_dir,
247 lib_info: None,
248 lib_name: lua_lib_name,
249 },
250 bin,
251 })
252 }
253
254 pub fn includes(&self) -> Vec<&PathBuf> {
255 self.dependency_info.include_dir.iter().collect_vec()
256 }
257
258 pub fn bin(&self) -> &Option<PathBuf> {
259 &self.bin
260 }
261
262 fn luau_from_root_dir(root: &Path) -> Option<Self> {
263 let bin_dir = root.join("bin");
264 let bin = find_lua_executable(&bin_dir, &LuaVersion::Luau)?;
265 Some(Self {
266 version: LuaVersion::Luau,
267 dependency_info: ExternalDependencyInfo {
268 include_dir: None,
269 lib_dir: None,
270 bin_dir: bin.parent().map(Path::to_path_buf),
271 lib_info: None,
272 lib_name: None,
273 },
274 bin: Some(bin),
275 })
276 }
277
278 fn from_bin_path(version: LuaVersion, bin: PathBuf) -> Self {
282 Self {
283 version,
284 dependency_info: ExternalDependencyInfo {
285 include_dir: None,
286 lib_dir: None,
287 bin_dir: bin.parent().map(Path::to_path_buf),
288 lib_info: None,
289 lib_name: None,
290 },
291 bin: Some(bin),
292 }
293 }
294
295 fn root_dir(version: &LuaVersion, config: &Config) -> PathBuf {
296 if let Some(lua_dir) = config.lua_dir() {
297 return lua_dir.clone();
298 } else if let Ok(tree) = config.user_tree(version.clone()) {
299 return tree.root().join(".lua");
300 }
301 config.data_dir().join(".lua").join(version.to_string())
302 }
303
304 #[cfg(not(target_env = "msvc"))]
305 fn lua_lib(&self) -> Option<String> {
306 self.dependency_info
307 .lib_name
308 .as_ref()
309 .map(|name| format!("{}.{}", name, c_lib_extension()))
310 }
311
312 #[cfg(target_env = "msvc")]
313 fn lua_lib(&self) -> Option<String> {
314 self.dependency_info.lib_name.clone()
315 }
316
317 pub(crate) fn define_flags(&self) -> Vec<String> {
318 let mut defines = self.dependency_info.define_flags();
319 if self.version.is_luajit() {
320 defines.push("-DLUAJIT_VERSION".into());
321 }
322 defines
323 }
324
325 pub(crate) fn lib_link_args(&self, compiler: &cc::Tool) -> Vec<String> {
326 self.dependency_info.lib_link_args(compiler)
327 }
328
329 pub(crate) fn lua_binary_or_config_override(&self, config: &Config) -> Option<String> {
332 let variable = if self.version.is_luau() {
333 "LUAU"
334 } else {
335 "LUA"
336 };
337 config.variables().get(variable).cloned().or(self
338 .bin
339 .clone()
340 .or(LuaBinary::new(self.version.clone(), config).try_into().ok())
341 .map(|bin| bin.to_slash_lossy().to_string()))
342 }
343}
344
345impl HasVariables for LuaInstallation {
346 #[tracing::instrument(level = "trace")]
347 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
348 Ok(match input {
349 "LUA_INCDIR" => self
350 .dependency_info
351 .include_dir
352 .as_ref()
353 .map(|dir| format_path(dir)),
354 "LUA_LIBDIR" => self
355 .dependency_info
356 .lib_dir
357 .as_ref()
358 .map(|dir| format_path(dir)),
359 "LUA_BINDIR" => self
360 .bin
361 .as_ref()
362 .and_then(|bin| bin.parent().map(format_path)),
363 "LUA" => self
364 .bin
365 .clone()
366 .or(LuaBinary::Lua {
367 lua_version: self.version.clone(),
368 }
369 .try_into()
370 .ok())
371 .map(|lua| format_path(&lua)),
372 "LUAU" if self.version.is_luau() => self.bin.as_ref().map(|bin| format_path(bin)),
373 "LUALIB" => self.lua_lib().or(Some("".into())),
374 _ => None,
375 })
376 }
377}
378
379#[derive(Clone)]
380pub enum LuaBinary {
381 Lua { lua_version: LuaVersion },
383 Custom(String),
385}
386
387impl Display for LuaBinary {
388 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
389 match self {
390 LuaBinary::Lua { lua_version } => write!(f, "lua {lua_version}"),
391 LuaBinary::Custom(cmd) => write!(f, "{cmd}"),
392 }
393 }
394}
395
396impl LuaBinary {
397 pub fn new(lua_version: LuaVersion, config: &Config) -> Self {
400 let variable = if lua_version.is_luau() { "LUAU" } else { "LUA" };
401 match config.variables().get(variable).cloned() {
402 Some(bin) => Self::Custom(bin),
403 None => Self::Lua { lua_version },
404 }
405 }
406}
407
408impl From<PathBuf> for LuaBinary {
409 fn from(value: PathBuf) -> Self {
410 Self::Custom(value.to_string_lossy().to_string())
411 }
412}
413
414impl TryFrom<LuaBinary> for PathBuf {
415 type Error = LuaBinaryError;
416
417 fn try_from(value: LuaBinary) -> Result<Self, Self::Error> {
418 match value {
419 LuaBinary::Lua { lua_version } => {
420 if lua_version.is_luau() {
421 return which("luau")
422 .or_else(|_| which("lune"))
423 .map_err(|_| LuaBinaryError::LuauBinaryNotFound);
424 }
425 if let Some(lua_binary) =
426 LuaInstallation::probe(&lua_version, &ExternalDependencySearchConfig::default())
427 .and_then(|lua_installation| lua_installation.bin)
428 {
429 return Ok(lua_binary);
430 }
431 if lua_version.is_luajit() {
432 if let Ok(path) = which("luajit") {
433 return Ok(path);
434 }
435 }
436 match which(format!("lua{}", lua_version)) {
437 Ok(path) => Ok(path),
438 Err(_) => detect_default_lua_bin(lua_version),
439 }
440 }
441 LuaBinary::Custom(bin) => match which(&bin) {
442 Ok(path) => Ok(path),
443 Err(_) => Err(LuaBinaryError::CustomBinaryNotFound(bin)),
444 },
445 }
446 }
447}
448
449fn detect_default_lua_bin(lua_version: LuaVersion) -> Result<PathBuf, LuaBinaryError> {
450 match which("lua") {
451 Ok(path) => {
452 let installed_version = detect_installed_lua_version_from_path(&path)?;
453 if lua_version
454 .clone()
455 .as_version_req()
456 .matches(&installed_version)
457 {
458 Ok(path)
459 } else {
460 Err(LuaBinaryError::LuaVersionMismatch {
461 lua_cmd: path.to_slash_lossy().to_string(),
462 installed_version,
463 lua_version,
464 })?
465 }
466 }
467 Err(_) => Err(LuaBinaryError::LuaBinaryNotFound),
468 }
469}
470
471pub fn detect_installed_lua_version() -> Option<LuaVersion> {
472 which("lua")
473 .ok()
474 .or(which("luajit").ok())
475 .and_then(|lua_cmd| {
476 detect_installed_lua_version_from_path(&lua_cmd)
477 .ok()
478 .and_then(|version| LuaVersion::from_version(version).ok())
479 })
480}
481
482pub fn nvim_lua_version() -> Option<LuaVersion> {
485 if LuaInstallation::probe(&LuaVersion::LuaJIT, &Default::default()).is_some() {
486 Some(LuaVersion::LuaJIT)
487 } else if LuaInstallation::probe(&LuaVersion::Lua51, &Default::default()).is_some() {
488 Some(LuaVersion::Lua51)
489 } else {
490 None
491 }
492}
493
494fn find_lua_executable(bin_path: &Path, version: &LuaVersion) -> Option<PathBuf> {
495 fs::sync::read_dir(bin_path).ok().and_then(|entries| {
496 let bin_files = entries
497 .filter_map(Result::ok)
498 .map(|entry| entry.path().to_path_buf())
499 .collect_vec();
500
501 #[cfg(windows)]
502 let ext = ".exe";
503
504 #[cfg(not(windows))]
505 let ext = "";
506
507 if version.is_luau() {
508 let luau_bin = format!("luau{ext}");
509 return bin_files
510 .into_iter()
511 .filter(|file| {
512 file.is_executable()
513 && file
514 .file_name()
515 .is_some_and(|name| name.to_string_lossy() == luau_bin)
516 })
517 .collect_vec()
518 .first()
519 .cloned();
520 }
521
522 let lua_version_bin = format!("lua{}{}", version, ext);
525 if let Some(lua_bin) = bin_files
526 .iter()
527 .filter(|file| {
528 file.is_executable()
529 && file
530 .file_name()
531 .is_some_and(|name| name.to_string_lossy() == lua_version_bin)
532 })
533 .collect_vec()
534 .first()
535 .cloned()
536 {
537 Some(lua_bin.clone())
538 } else {
539 bin_files
541 .into_iter()
542 .filter(|file| {
543 file.is_executable()
544 && file.file_name().is_some_and(|name| {
545 matches!(
546 name.to_string_lossy().to_string().as_str(),
547 "lua" | "luajit" | "lua.exe" | "luajit.exe"
548 )
549 })
550 })
551 .collect_vec()
552 .first()
553 .cloned()
554 }
555 })
556}
557
558fn is_lua_lib_name(name: &str, lua_version: &LuaVersion) -> bool {
559 let prefixes = match lua_version {
560 LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => vec!["luajit", "lua"],
561 _ => vec!["lua"],
562 };
563 let version_str = lua_version.version_compatibility_str();
564 let version_suffix = version_str.replace(".", "");
565 #[cfg(target_family = "unix")]
566 let name = name.trim_start_matches("lib");
567 prefixes
568 .iter()
569 .any(|prefix| name == format!("{}.{}", *prefix, c_lib_extension()))
570 || prefixes.iter().any(|prefix| name.starts_with(*prefix))
571 && (name.contains(&version_str) || name.contains(&version_suffix))
572}
573
574fn get_lua_lib_name(lib_dir: &Path, lua_version: &LuaVersion) -> Option<String> {
575 fs::sync::read_dir(lib_dir)
576 .ok()
577 .and_then(|entries| {
578 entries
579 .filter_map(Result::ok)
580 .map(|entry| entry.path().to_path_buf())
581 .filter(|file| file.extension().is_some_and(|ext| ext == c_lib_extension()))
582 .filter(|file| {
583 file.file_name()
584 .is_some_and(|name| is_lua_lib_name(&name.to_string_lossy(), lua_version))
585 })
586 .collect_vec()
587 .first()
588 .cloned()
589 })
590 .map(|file| to_lib_name(&file))
591}
592
593fn detect_installed_lua_version_from_path(
594 lua_cmd: &Path,
595) -> Result<PackageVersion, DetectLuaVersionError> {
596 let output = match std::process::Command::new(lua_cmd).arg("-v").output() {
597 Ok(output) => Ok(output),
598 Err(err) => Err(DetectLuaVersionError::RunLuaCommand(
599 lua_cmd.to_string_lossy().to_string(),
600 err,
601 )),
602 }?;
603 let output_vec = if output.stderr.is_empty() {
604 output.stdout
605 } else {
606 output.stderr
608 };
609 let lua_output = String::from_utf8_lossy(&output_vec).to_string();
610 parse_lua_version_from_output(&lua_output)
611}
612
613fn parse_lua_version_from_output(
614 lua_output: &str,
615) -> Result<PackageVersion, DetectLuaVersionError> {
616 let lua_version_str = lua_output
617 .trim_start_matches("Lua")
618 .trim_start_matches("JIT")
619 .split_whitespace()
620 .next()
621 .map(|s| s.to_string())
622 .ok_or(DetectLuaVersionError::ParseLuaVersion(
623 lua_output.to_string(),
624 ))?;
625 Ok(PackageVersion::parse(&lua_version_str)?)
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 #[cfg(feature = "impure_tests")]
632 use flaky_test::flaky_test;
633
634 #[tokio::test]
635 async fn parse_luajit_version() {
636 let luajit_output =
637 "LuaJIT 2.1.1713773202 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/";
638 parse_lua_version_from_output(luajit_output).unwrap();
639 }
640
641 #[tokio::test]
642 async fn parse_lua_51_version() {
643 let lua_output = "Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio";
644 parse_lua_version_from_output(lua_output).unwrap();
645 }
646
647 #[cfg(feature = "impure_tests")]
648 #[flaky_test(tokio, times = 5)]
649 async fn lua_installation_bin() {
650 let config = crate::config::ConfigBuilder::new()
651 .unwrap()
652 .build()
653 .unwrap();
654 let lua_version = config.lua_version().unwrap();
655 let lua_installation = LuaInstallation::new(lua_version, &config).await.unwrap();
656 assert!(lua_installation.bin.is_some());
658 let lua_binary: LuaBinary = lua_installation.bin.unwrap().into();
659 let lua_bin_path: PathBuf = lua_binary.try_into().unwrap();
660 let pkg_version = detect_installed_lua_version_from_path(&lua_bin_path).unwrap();
661 assert_eq!(&LuaVersion::from_version(pkg_version).unwrap(), lua_version);
662 }
663
664 #[cfg(not(target_env = "msvc"))]
665 #[tokio::test]
666 async fn test_is_lua_lib_name() {
667 assert!(is_lua_lib_name("lua.a", &LuaVersion::Lua51));
668 assert!(is_lua_lib_name("lua-5.1.a", &LuaVersion::Lua51));
669 assert!(is_lua_lib_name("lua5.1.a", &LuaVersion::Lua51));
670 assert!(is_lua_lib_name("lua51.a", &LuaVersion::Lua51));
671 assert!(!is_lua_lib_name("lua-5.2.a", &LuaVersion::Lua51));
672 assert!(is_lua_lib_name("luajit-5.2.a", &LuaVersion::LuaJIT52));
673 assert!(is_lua_lib_name("lua-5.2.a", &LuaVersion::LuaJIT52));
674 assert!(is_lua_lib_name("liblua.a", &LuaVersion::Lua51));
675 assert!(is_lua_lib_name("liblua-5.1.a", &LuaVersion::Lua51));
676 assert!(is_lua_lib_name("liblua53.a", &LuaVersion::Lua53));
677 assert!(is_lua_lib_name("liblua-54.a", &LuaVersion::Lua54));
678 assert!(is_lua_lib_name("liblua-55.a", &LuaVersion::Lua55));
679 }
680
681 #[cfg(target_env = "msvc")]
682 #[tokio::test]
683 async fn test_is_lua_lib_name() {
684 assert!(is_lua_lib_name("lua.lib", &LuaVersion::Lua51));
685 assert!(is_lua_lib_name("lua-5.1.lib", &LuaVersion::Lua51));
686 assert!(!is_lua_lib_name("lua-5.2.lib", &LuaVersion::Lua51));
687 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
688 assert!(!is_lua_lib_name("lua53.lib", &LuaVersion::Lua53));
689 assert!(is_lua_lib_name("luajit-5.2.lib", &LuaVersion::LuaJIT52));
690 assert!(is_lua_lib_name("lua-5.2.lib", &LuaVersion::LuaJIT52));
691 }
692}