Skip to main content

lux_lib/lua_version/
mod.rs

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