Skip to main content

lux_lib/lua_version/
mod.rs

1use std::{
2    fmt::Display,
3    path::{Path, PathBuf},
4    str::FromStr,
5};
6
7use crate::{
8    config::Config,
9    package::{PackageVersion, PackageVersionReq},
10};
11use itertools::Itertools;
12use miette::Diagnostic;
13use serde::{Deserialize, Serialize};
14use strum::IntoEnumIterator;
15use strum_macros::EnumIter;
16use thiserror::Error;
17
18#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, EnumIter)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20pub enum LuaVersion {
21    #[serde(rename = "5.1")]
22    Lua51,
23    #[serde(rename = "5.2")]
24    Lua52,
25    #[serde(rename = "5.3")]
26    Lua53,
27    #[serde(rename = "5.4")]
28    Lua54,
29    #[serde(rename = "5.5")]
30    Lua55,
31    #[serde(rename = "jit")]
32    LuaJIT,
33    #[serde(rename = "jit5.2")]
34    LuaJIT52,
35    #[serde(rename = "luau")]
36    Luau,
37}
38
39#[derive(Debug, Error, Diagnostic)]
40pub enum LuaVersionError {
41    #[error("unsupported Lua version: '{0}'")]
42    UnsupportedLuaVersion(PackageVersion),
43}
44
45impl LuaVersion {
46    pub fn as_version(&self) -> PackageVersion {
47        unsafe {
48            match self {
49                LuaVersion::Lua51 => "5.1.0".parse().unwrap_unchecked(),
50                LuaVersion::Lua52 => "5.2.0".parse().unwrap_unchecked(),
51                LuaVersion::Lua53 => "5.3.0".parse().unwrap_unchecked(),
52                LuaVersion::Lua54 => "5.4.0".parse().unwrap_unchecked(),
53                LuaVersion::Lua55 => "5.5.0".parse().unwrap_unchecked(),
54                LuaVersion::LuaJIT => "5.1.0".parse().unwrap_unchecked(),
55                LuaVersion::LuaJIT52 => "5.2.0".parse().unwrap_unchecked(),
56                LuaVersion::Luau => "5.1.0".parse().unwrap_unchecked(),
57            }
58        }
59    }
60    pub fn version_compatibility_str(&self) -> String {
61        match self {
62            LuaVersion::Lua51 | LuaVersion::LuaJIT | LuaVersion::Luau => "5.1".into(),
63            LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2".into(),
64            LuaVersion::Lua53 => "5.3".into(),
65            LuaVersion::Lua54 => "5.4".into(),
66            LuaVersion::Lua55 => "5.5".into(),
67        }
68    }
69    pub fn as_version_req(&self) -> PackageVersionReq {
70        unsafe {
71            format!("~> {}", self.version_compatibility_str())
72                .parse()
73                .unwrap_unchecked()
74        }
75    }
76
77    /// Get the LuaVersion from a version that has been parsed from the `lua -v` output
78    pub fn from_version(version: PackageVersion) -> Result<LuaVersion, LuaVersionError> {
79        // NOTE: Special case. luajit -v outputs 2.x.y as a version
80        let luajit_version_req: PackageVersionReq = unsafe { "~> 2".parse().unwrap_unchecked() };
81        if luajit_version_req.matches(&version) {
82            Ok(LuaVersion::LuaJIT)
83        } else if LuaVersion::Lua51.as_version_req().matches(&version) {
84            Ok(LuaVersion::Lua51)
85        } else if LuaVersion::Lua52.as_version_req().matches(&version) {
86            Ok(LuaVersion::Lua52)
87        } else if LuaVersion::Lua53.as_version_req().matches(&version) {
88            Ok(LuaVersion::Lua53)
89        } else if LuaVersion::Lua54.as_version_req().matches(&version) {
90            Ok(LuaVersion::Lua54)
91        } else if LuaVersion::Lua55.as_version_req().matches(&version) {
92            Ok(LuaVersion::Lua55)
93        } else {
94            Err(LuaVersionError::UnsupportedLuaVersion(version))
95        }
96    }
97
98    pub(crate) fn is_luajit(&self) -> bool {
99        matches!(self, Self::LuaJIT | Self::LuaJIT52)
100    }
101
102    pub(crate) fn is_luau(&self) -> bool {
103        matches!(self, Self::Luau)
104    }
105
106    /// Searches for the path to the lux-lua library for this version
107    pub fn lux_lib_dir(&self) -> Option<PathBuf> {
108        option_env!("LUX_LIB_DIR")
109            .map(PathBuf::from)
110            .map(|path| path.join(self.to_string()))
111            .or_else(|| self.probe_lux_lib_dir())
112            .or_else(|| {
113                match self {
114                    LuaVersion::LuaJIT => Some(LuaVersion::Lua51),
115                    LuaVersion::LuaJIT52 => Some(LuaVersion::Lua52),
116                    _ => None,
117                }
118                .and_then(|compat_version| compat_version.probe_lux_lib_dir())
119            })
120            .or_else(|| lux_lib_resource_dir().map(|path| path.join(self.to_string())))
121    }
122
123    fn probe_lux_lib_dir(&self) -> Option<PathBuf> {
124        let lib_name = format!("lux-lua{self}");
125        pkg_config::Config::new()
126            .print_system_libs(false)
127            .cargo_metadata(false)
128            .env_metadata(false)
129            .probe(&lib_name)
130            .ok()
131            .and_then(|library| library.link_paths.first().cloned())
132    }
133}
134
135#[derive(Error, Debug, Diagnostic)]
136#[non_exhaustive]
137#[error("lua version not set")]
138pub struct LuaVersionUnset {
139    #[help]
140    help: String,
141}
142
143impl LuaVersion {
144    pub fn from(config: &Config) -> Result<&Self, LuaVersionUnset> {
145        config.lua_version().ok_or(LuaVersionUnset {
146            help: format!(
147                r#"provide a version through `lx --lua-version <ver> <cmd>`
148or make sure Lux can discover your Lua installation on your PATH.
149valid versions are: {}
150"#,
151                LuaVersion::iter().join(",")
152            ),
153        })
154    }
155}
156
157impl FromStr for LuaVersion {
158    type Err = String;
159
160    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
161        match s {
162            "5.1" | "51" => Ok(LuaVersion::Lua51),
163            "5.2" | "52" => Ok(LuaVersion::Lua52),
164            "5.3" | "53" => Ok(LuaVersion::Lua53),
165            "5.4" | "54" => Ok(LuaVersion::Lua54),
166            "5.5" | "55" => Ok(LuaVersion::Lua55),
167            "jit" | "luajit" => Ok(LuaVersion::LuaJIT),
168            "jit52" | "luajit52" => Ok(LuaVersion::LuaJIT52),
169            "luau" => Ok(LuaVersion::Luau),
170            _ => Err(format!(
171                r#"unrecognized Lua version.
172supported versions: {}
173            "#,
174                LuaVersion::iter().join(",")
175            )),
176        }
177    }
178}
179
180impl Display for LuaVersion {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.write_str(match self {
183            LuaVersion::Lua51 => "5.1",
184            LuaVersion::Lua52 => "5.2",
185            LuaVersion::Lua53 => "5.3",
186            LuaVersion::Lua54 => "5.4",
187            LuaVersion::Lua55 => "5.5",
188            LuaVersion::LuaJIT => "jit",
189            LuaVersion::LuaJIT52 => "jit52",
190            LuaVersion::Luau => "luau",
191        })
192    }
193}
194
195/// Searches for the lux-lib directory in a binary distribution's resources
196fn lux_lib_resource_dir() -> Option<PathBuf> {
197    if cfg!(target_env = "msvc") {
198        // The msvc .exe and .msi binary installers install lux-lua to the executable's directory.
199        std::env::current_exe()
200            .ok()
201            .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
202            .and_then(|exe_dir| {
203                let lib_dir = exe_dir.join("lux-lua");
204                if lib_dir.is_dir() {
205                    Some(lib_dir)
206                } else {
207                    None
208                }
209            })
210    } else if cfg!(target_os = "macos") {
211        // Currently, we only bundle resources with an .app ApplicationBundle
212        std::env::current_exe()
213            .ok()
214            .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
215            .and_then(|macos_dir| macos_dir.parent().map(Path::to_path_buf))
216            .and_then(|contents_dir| {
217                let lib_dir = contents_dir.join("Resources").join("lux-lua");
218                if lib_dir.is_dir() {
219                    Some(lib_dir)
220                } else {
221                    None
222                }
223            })
224    } else {
225        // .deb and AppImage packages
226        if let Some(appdir) = std::env::var_os("APPDIR") {
227            let lib_dir = Path::new(&appdir).join("usr/share/lux-lua");
228            if lib_dir.is_dir() {
229                return Some(lib_dir);
230            }
231        }
232        let lib_dir = PathBuf::from("/usr/share/lux-lua");
233        if lib_dir.is_dir() {
234            Some(lib_dir)
235        } else {
236            None
237        }
238    }
239}