Skip to main content

lux_lib/config/
mod.rs

1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use serde::{Deserialize, Serialize, Serializer};
6use std::env::current_exe;
7use std::path::Path;
8use std::{
9    collections::HashMap, env, fmt::Display, io, path::PathBuf, str::FromStr, time::Duration,
10};
11use thiserror::Error;
12use tree::RockLayoutConfig;
13use url::Url;
14
15use crate::tree::{Tree, TreeError};
16use crate::variables::GetVariableError;
17use crate::{
18    build::utils,
19    package::{PackageVersion, PackageVersionReq},
20    variables::HasVariables,
21};
22
23pub mod external_deps;
24pub mod tree;
25
26const DEV_PATH: &str = "dev/";
27
28#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
29pub enum LuaVersion {
30    #[serde(rename = "5.1")]
31    Lua51,
32    #[serde(rename = "5.2")]
33    Lua52,
34    #[serde(rename = "5.3")]
35    Lua53,
36    #[serde(rename = "5.4")]
37    Lua54,
38    #[serde(rename = "5.5")]
39    Lua55,
40    #[serde(rename = "jit")]
41    LuaJIT,
42    #[serde(rename = "jit5.2")]
43    LuaJIT52,
44    // TODO(vhyrro): Support luau?
45    // LuaU,
46}
47
48#[derive(Debug, Error)]
49pub enum LuaVersionError {
50    #[error("unsupported Lua version: {0}")]
51    UnsupportedLuaVersion(PackageVersion),
52}
53
54impl LuaVersion {
55    pub fn as_version(&self) -> PackageVersion {
56        unsafe {
57            match self {
58                LuaVersion::Lua51 => "5.1.0".parse().unwrap_unchecked(),
59                LuaVersion::Lua52 => "5.2.0".parse().unwrap_unchecked(),
60                LuaVersion::Lua53 => "5.3.0".parse().unwrap_unchecked(),
61                LuaVersion::Lua54 => "5.4.0".parse().unwrap_unchecked(),
62                LuaVersion::Lua55 => "5.5.0".parse().unwrap_unchecked(),
63                LuaVersion::LuaJIT => "5.1.0".parse().unwrap_unchecked(),
64                LuaVersion::LuaJIT52 => "5.2.0".parse().unwrap_unchecked(),
65            }
66        }
67    }
68    pub fn version_compatibility_str(&self) -> String {
69        match self {
70            LuaVersion::Lua51 | LuaVersion::LuaJIT => "5.1".into(),
71            LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2".into(),
72            LuaVersion::Lua53 => "5.3".into(),
73            LuaVersion::Lua54 => "5.4".into(),
74            LuaVersion::Lua55 => "5.5".into(),
75        }
76    }
77    pub fn as_version_req(&self) -> PackageVersionReq {
78        unsafe {
79            format!("~> {}", self.version_compatibility_str())
80                .parse()
81                .unwrap_unchecked()
82        }
83    }
84
85    /// Get the LuaVersion from a version that has been parsed from the `lua -v` output
86    pub fn from_version(version: PackageVersion) -> Result<LuaVersion, LuaVersionError> {
87        // NOTE: Special case. luajit -v outputs 2.x.y as a version
88        let luajit_version_req: PackageVersionReq = unsafe { "~> 2".parse().unwrap_unchecked() };
89        if luajit_version_req.matches(&version) {
90            Ok(LuaVersion::LuaJIT)
91        } else if LuaVersion::Lua51.as_version_req().matches(&version) {
92            Ok(LuaVersion::Lua51)
93        } else if LuaVersion::Lua52.as_version_req().matches(&version) {
94            Ok(LuaVersion::Lua52)
95        } else if LuaVersion::Lua53.as_version_req().matches(&version) {
96            Ok(LuaVersion::Lua53)
97        } else if LuaVersion::Lua54.as_version_req().matches(&version) {
98            Ok(LuaVersion::Lua54)
99        } else if LuaVersion::Lua55.as_version_req().matches(&version) {
100            Ok(LuaVersion::Lua55)
101        } else {
102            Err(LuaVersionError::UnsupportedLuaVersion(version))
103        }
104    }
105
106    pub(crate) fn is_luajit(&self) -> bool {
107        matches!(self, Self::LuaJIT | Self::LuaJIT52)
108    }
109
110    /// Searches for the path to the lux-lua library for this version
111    pub fn lux_lib_dir(&self) -> Option<PathBuf> {
112        option_env!("LUX_LIB_DIR")
113            .map(PathBuf::from)
114            .map(|path| path.join(self.to_string()))
115            .or_else(|| {
116                let lib_name = format!("lux-lua{self}");
117                pkg_config::Config::new()
118                    .print_system_libs(false)
119                    .cargo_metadata(false)
120                    .env_metadata(false)
121                    .probe(&lib_name)
122                    .ok()
123                    .and_then(|library| library.link_paths.first().cloned())
124            })
125            .or_else(|| lux_lib_resource_dir().map(|path| path.join(self.to_string())))
126    }
127}
128
129/// Searches for the lux-lib directory in a binary distribution's resources
130fn lux_lib_resource_dir() -> Option<PathBuf> {
131    if cfg!(target_env = "msvc") {
132        // The msvc .exe and .msi binary installers install lux-lua to the executable's directory.
133        current_exe()
134            .ok()
135            .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
136            .and_then(|exe_dir| {
137                let lib_dir = exe_dir.join("lux-lua");
138                if lib_dir.is_dir() {
139                    Some(lib_dir)
140                } else {
141                    None
142                }
143            })
144    } else if cfg!(target_os = "macos") {
145        // Currently, we only bundle resources with an .app ApplicationBundle
146        current_exe()
147            .ok()
148            .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
149            .and_then(|macos_dir| macos_dir.parent().map(Path::to_path_buf))
150            .and_then(|contents_dir| {
151                let lib_dir = contents_dir.join("Resources").join("lux-lua");
152                if lib_dir.is_dir() {
153                    Some(lib_dir)
154                } else {
155                    None
156                }
157            })
158    } else {
159        // .deb and AppImage packages
160        let lib_dir = PathBuf::from("/usr/share/lux-lua");
161        if lib_dir.is_dir() {
162            Some(lib_dir)
163        } else {
164            None
165        }
166    }
167}
168
169#[derive(Error, Debug)]
170#[error(
171    r#"lua version not set.
172Please provide a version through `lx --lua-version <ver> <cmd>`
173Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit' and 'jit52'.
174"#
175)]
176pub struct LuaVersionUnset;
177
178impl LuaVersion {
179    pub fn from(config: &Config) -> Result<&Self, LuaVersionUnset> {
180        config.lua_version.as_ref().ok_or(LuaVersionUnset)
181    }
182}
183
184impl FromStr for LuaVersion {
185    type Err = String;
186
187    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
188        match s {
189            "5.1" | "51" => Ok(LuaVersion::Lua51),
190            "5.2" | "52" => Ok(LuaVersion::Lua52),
191            "5.3" | "53" => Ok(LuaVersion::Lua53),
192            "5.4" | "54" => Ok(LuaVersion::Lua54),
193            "5.5" | "55" => Ok(LuaVersion::Lua55),
194            "jit" | "luajit" => Ok(LuaVersion::LuaJIT),
195            "jit52" | "luajit52" => Ok(LuaVersion::LuaJIT52),
196            _ => Err(r#"unrecognized Lua version.
197                 Supported versions: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit', 'jit52'.
198                 "#
199            .into()),
200        }
201    }
202}
203
204impl Display for LuaVersion {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        f.write_str(match self {
207            LuaVersion::Lua51 => "5.1",
208            LuaVersion::Lua52 => "5.2",
209            LuaVersion::Lua53 => "5.3",
210            LuaVersion::Lua54 => "5.4",
211            LuaVersion::Lua55 => "5.5",
212            LuaVersion::LuaJIT => "jit",
213            LuaVersion::LuaJIT52 => "jit52",
214        })
215    }
216}
217
218#[derive(Error, Debug)]
219#[error("could not find a valid home directory")]
220pub struct NoValidHomeDirectory;
221
222#[derive(Debug, Clone)]
223pub struct Config {
224    enable_development_packages: bool,
225    server: Url,
226    extra_servers: Vec<Url>,
227    only_sources: Option<String>,
228    namespace: Option<String>,
229    lua_dir: Option<PathBuf>,
230    lua_version: Option<LuaVersion>,
231    user_tree: PathBuf,
232    verbose: bool,
233    /// Don't display progress bars
234    no_progress: bool,
235    timeout: Duration,
236    max_jobs: usize,
237    variables: HashMap<String, String>,
238    external_deps: ExternalDependencySearchConfig,
239    /// The rock layout for entrypoints of new install trees.
240    /// Does not affect existing install trees or dependency rock layouts.
241    entrypoint_layout: RockLayoutConfig,
242
243    cache_dir: PathBuf,
244    data_dir: PathBuf,
245    vendor_dir: Option<PathBuf>,
246
247    generate_luarc: bool,
248}
249
250impl Config {
251    pub fn get_project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
252        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
253    }
254
255    pub fn get_default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
256        let project_dirs = Config::get_project_dirs()?;
257        Ok(project_dirs.cache_dir().to_path_buf())
258    }
259
260    pub fn get_default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
261        let project_dirs = Config::get_project_dirs()?;
262        Ok(project_dirs.data_local_dir().to_path_buf())
263    }
264
265    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
266        Self {
267            lua_version: Some(lua_version),
268            ..self
269        }
270    }
271
272    pub fn with_tree(self, tree: PathBuf) -> Self {
273        Self {
274            user_tree: tree,
275            ..self
276        }
277    }
278
279    pub fn server(&self) -> &Url {
280        &self.server
281    }
282
283    pub fn extra_servers(&self) -> &Vec<Url> {
284        self.extra_servers.as_ref()
285    }
286
287    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
288        let mut enabled_dev_servers = Vec::new();
289        if self.enable_development_packages {
290            enabled_dev_servers.push(self.server().join(DEV_PATH)?);
291            for server in self.extra_servers() {
292                enabled_dev_servers.push(server.join(DEV_PATH)?);
293            }
294        }
295        Ok(enabled_dev_servers)
296    }
297
298    pub fn only_sources(&self) -> Option<&String> {
299        self.only_sources.as_ref()
300    }
301
302    pub fn namespace(&self) -> Option<&String> {
303        self.namespace.as_ref()
304    }
305
306    pub fn lua_dir(&self) -> Option<&PathBuf> {
307        self.lua_dir.as_ref()
308    }
309
310    #[cfg(test)]
311    pub(crate) fn lua_version(&self) -> Option<&LuaVersion> {
312        self.lua_version.as_ref()
313    }
314
315    /// The tree in which to install rocks.
316    /// If installing packges for a project, use `Project::tree` instead.
317    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
318        Tree::new(self.user_tree.clone(), version, self)
319    }
320
321    pub fn verbose(&self) -> bool {
322        self.verbose
323    }
324
325    pub fn no_progress(&self) -> bool {
326        self.no_progress
327    }
328
329    pub fn timeout(&self) -> &Duration {
330        &self.timeout
331    }
332
333    pub fn max_jobs(&self) -> usize {
334        self.max_jobs
335    }
336
337    pub fn make_cmd(&self) -> String {
338        match self.variables.get("MAKE") {
339            Some(make) => make.clone(),
340            None => "make".into(),
341        }
342    }
343
344    pub fn cmake_cmd(&self) -> String {
345        match self.variables.get("CMAKE") {
346            Some(cmake) => cmake.clone(),
347            None => "cmake".into(),
348        }
349    }
350
351    pub fn variables(&self) -> &HashMap<String, String> {
352        &self.variables
353    }
354
355    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
356        &self.external_deps
357    }
358
359    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
360        &self.entrypoint_layout
361    }
362
363    pub fn cache_dir(&self) -> &PathBuf {
364        &self.cache_dir
365    }
366
367    pub fn data_dir(&self) -> &PathBuf {
368        &self.data_dir
369    }
370
371    pub fn vendor_dir(&self) -> Option<&PathBuf> {
372        self.vendor_dir.as_ref()
373    }
374
375    pub fn generate_luarc(&self) -> bool {
376        self.generate_luarc
377    }
378}
379
380impl HasVariables for Config {
381    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
382        Ok(self.variables.get(input).cloned())
383    }
384}
385
386#[derive(Error, Debug)]
387pub enum ConfigError {
388    #[error(transparent)]
389    Io(#[from] io::Error),
390    #[error(transparent)]
391    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
392    #[error("error deserializing lux config: {0}")]
393    Deserialize(#[from] toml::de::Error),
394    #[error("error parsing URL: {0}")]
395    UrlParseError(#[from] url::ParseError),
396    #[error("error initializing compiler toolchain: {0}")]
397    CompilerToolchain(#[from] cc::Error),
398}
399
400#[derive(Clone, Default, Deserialize, Serialize)]
401pub struct ConfigBuilder {
402    #[serde(
403        default,
404        deserialize_with = "deserialize_url",
405        serialize_with = "serialize_url"
406    )]
407    server: Option<Url>,
408    #[serde(
409        default,
410        deserialize_with = "deserialize_url_vec",
411        serialize_with = "serialize_url_vec"
412    )]
413    extra_servers: Option<Vec<Url>>,
414    only_sources: Option<String>,
415    namespace: Option<String>,
416    lua_version: Option<LuaVersion>,
417    user_tree: Option<PathBuf>,
418    lua_dir: Option<PathBuf>,
419    cache_dir: Option<PathBuf>,
420    data_dir: Option<PathBuf>,
421    vendor_dir: Option<PathBuf>,
422    enable_development_packages: Option<bool>,
423    verbose: Option<bool>,
424    no_progress: Option<bool>,
425    timeout: Option<Duration>,
426    max_jobs: Option<usize>,
427    variables: Option<HashMap<String, String>>,
428    #[serde(default)]
429    external_deps: ExternalDependencySearchConfig,
430    /// The rock layout for new install trees.
431    /// Does not affect existing install trees.
432    #[serde(default)]
433    entrypoint_layout: RockLayoutConfig,
434    generate_luarc: Option<bool>,
435}
436
437/// A builder for the lux `Config`.
438impl ConfigBuilder {
439    /// Create a new `ConfigBuilder` from a config file by deserializing from a config file
440    /// if present, or otherwise by instantiating the default config.
441    pub fn new() -> Result<Self, ConfigError> {
442        let config_file = Self::config_file()?;
443        if config_file.is_file() {
444            Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
445        } else {
446            Ok(Self::default())
447        }
448    }
449
450    /// Get the path to the lux config file.
451    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
452        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
453            .ok_or(NoValidHomeDirectory)?;
454        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
455    }
456
457    pub fn dev(self, dev: Option<bool>) -> Self {
458        Self {
459            enable_development_packages: dev.or(self.enable_development_packages),
460            ..self
461        }
462    }
463
464    pub fn server(self, server: Option<Url>) -> Self {
465        Self {
466            server: server.or(self.server),
467            ..self
468        }
469    }
470
471    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
472        Self {
473            extra_servers: extra_servers.or(self.extra_servers),
474            ..self
475        }
476    }
477
478    pub fn only_sources(self, sources: Option<String>) -> Self {
479        Self {
480            only_sources: sources.or(self.only_sources),
481            ..self
482        }
483    }
484
485    pub fn namespace(self, namespace: Option<String>) -> Self {
486        Self {
487            namespace: namespace.or(self.namespace),
488            ..self
489        }
490    }
491
492    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
493        Self {
494            lua_dir: lua_dir.or(self.lua_dir),
495            ..self
496        }
497    }
498
499    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
500        Self {
501            lua_version: lua_version.or(self.lua_version),
502            ..self
503        }
504    }
505
506    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
507        Self {
508            user_tree: tree.or(self.user_tree),
509            ..self
510        }
511    }
512
513    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
514        Self {
515            variables: variables.or(self.variables),
516            ..self
517        }
518    }
519
520    pub fn verbose(self, verbose: Option<bool>) -> Self {
521        Self {
522            verbose: verbose.or(self.verbose),
523            ..self
524        }
525    }
526
527    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
528        Self {
529            no_progress: no_progress.or(self.no_progress),
530            ..self
531        }
532    }
533
534    pub fn timeout(self, timeout: Option<Duration>) -> Self {
535        Self {
536            timeout: timeout.or(self.timeout),
537            ..self
538        }
539    }
540
541    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
542        Self {
543            max_jobs: max_jobs.or(self.max_jobs),
544            ..self
545        }
546    }
547
548    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
549        Self {
550            cache_dir: cache_dir.or(self.cache_dir),
551            ..self
552        }
553    }
554
555    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
556        Self {
557            data_dir: data_dir.or(self.data_dir),
558            ..self
559        }
560    }
561
562    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
563        Self {
564            vendor_dir: vendor_dir.or(self.vendor_dir),
565            ..self
566        }
567    }
568
569    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
570        Self {
571            entrypoint_layout: rock_layout,
572            ..self
573        }
574    }
575
576    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
577        Self {
578            generate_luarc: generate.or(self.generate_luarc),
579            ..self
580        }
581    }
582
583    pub fn build(self) -> Result<Config, ConfigError> {
584        let data_dir = self.data_dir.unwrap_or(Config::get_default_data_path()?);
585        let cache_dir = self.cache_dir.unwrap_or(Config::get_default_cache_path()?);
586        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
587
588        let lua_version = self
589            .lua_version
590            .or(crate::lua_installation::detect_installed_lua_version());
591
592        Ok(Config {
593            enable_development_packages: self.enable_development_packages.unwrap_or(false),
594            server: self.server.unwrap_or_else(|| unsafe {
595                Url::parse("https://luarocks.org/").unwrap_unchecked()
596            }),
597            extra_servers: self.extra_servers.unwrap_or_default(),
598            only_sources: self.only_sources,
599            namespace: self.namespace,
600            lua_dir: self.lua_dir,
601            lua_version,
602            user_tree,
603            verbose: self.verbose.unwrap_or(false),
604            no_progress: self.no_progress.unwrap_or(false),
605            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
606            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
607                0 => usize::MAX,
608                max_jobs => max_jobs,
609            },
610            variables: default_variables()
611                .chain(self.variables.unwrap_or_default())
612                .collect(),
613            external_deps: self.external_deps,
614            entrypoint_layout: self.entrypoint_layout,
615            cache_dir,
616            data_dir,
617            vendor_dir: self.vendor_dir,
618            generate_luarc: self.generate_luarc.unwrap_or(true),
619        })
620    }
621}
622
623/// Useful for printing the current config
624impl From<Config> for ConfigBuilder {
625    fn from(value: Config) -> Self {
626        ConfigBuilder {
627            enable_development_packages: Some(value.enable_development_packages),
628            server: Some(value.server),
629            extra_servers: Some(value.extra_servers),
630            only_sources: value.only_sources,
631            namespace: value.namespace,
632            lua_dir: value.lua_dir,
633            lua_version: value.lua_version,
634            user_tree: Some(value.user_tree),
635            verbose: Some(value.verbose),
636            no_progress: Some(value.no_progress),
637            timeout: Some(value.timeout),
638            max_jobs: if value.max_jobs == usize::MAX {
639                None
640            } else {
641                Some(value.max_jobs)
642            },
643            variables: Some(value.variables),
644            cache_dir: Some(value.cache_dir),
645            data_dir: Some(value.data_dir),
646            vendor_dir: value.vendor_dir,
647            external_deps: value.external_deps,
648            entrypoint_layout: value.entrypoint_layout,
649            generate_luarc: Some(value.generate_luarc),
650        }
651    }
652}
653
654fn default_variables() -> impl Iterator<Item = (String, String)> {
655    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
656    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
657    vec![
658        ("MAKE".into(), "make".into()),
659        ("CMAKE".into(), "cmake".into()),
660        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
661        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
662        ("CFLAGS".into(), cflags),
663        ("LDFLAGS".into(), ldflags),
664        ("LIBFLAG".into(), utils::default_libflag().into()),
665    ]
666    .into_iter()
667}
668
669fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
670where
671    D: serde::Deserializer<'de>,
672{
673    let s = Option::<String>::deserialize(deserializer)?;
674    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
675        .transpose()
676}
677
678fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
679where
680    S: Serializer,
681{
682    match url {
683        Some(url) => serializer.serialize_some(url.as_str()),
684        None => serializer.serialize_none(),
685    }
686}
687
688fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
689where
690    D: serde::Deserializer<'de>,
691{
692    let s = Option::<Vec<String>>::deserialize(deserializer)?;
693    s.map(|v| {
694        v.into_iter()
695            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
696            .try_collect()
697    })
698    .transpose()
699}
700
701fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
702where
703    S: Serializer,
704{
705    match urls {
706        Some(urls) => {
707            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
708            serializer.serialize_some(&url_strings)
709        }
710        None => serializer.serialize_none(),
711    }
712}