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