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
31lazy_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 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 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 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 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 Lua { lua_version: LuaVersion },
381 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 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
480pub 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 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 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 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 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}