Skip to main content

lux_lib/config/
mod.rs

1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use miette::Diagnostic;
6use serde::{Deserialize, Serialize, Serializer};
7use std::{collections::HashMap, env, path::PathBuf, time::Duration};
8use thiserror::Error;
9use tree::RockLayoutConfig;
10use url::Url;
11
12use crate::fs;
13use crate::lua_version::LuaVersion;
14use crate::package::RemotePackageTypeFilterSpec;
15use crate::project::TomlDeError;
16use crate::tree::{Tree, TreeError};
17use crate::variables::GetVariableError;
18use crate::{build::utils, variables::HasVariables};
19
20pub mod external_deps;
21pub mod tree;
22
23const DEV_PATH: &str = "dev/";
24const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
25
26#[derive(Error, Debug, Diagnostic)]
27#[error("could not find a valid home directory")]
28#[diagnostic(
29    code(lux_lib::no_home_directory),
30    help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
31)]
32pub struct NoValidHomeDirectory;
33
34/// The resolved configuration for a Lux session.
35/// Can be constructed via [`ConfigBuilder`], which supports layering multiple
36/// configuration sources (config file, CLI flags, environment variables).
37#[derive(Debug, Clone)]
38pub struct Config {
39    enable_development_packages: bool,
40    server: Url,
41    extra_servers: Vec<Url>,
42    namespace: Option<String>,
43    lua_dir: Option<PathBuf>,
44    lua_version: Option<LuaVersion>,
45    user_tree: PathBuf,
46    verbose: bool,
47    /// Don't display progress bars
48    no_progress: bool,
49    /// Skip prompts (choosing the default choice)
50    no_prompt: bool,
51    timeout: Duration,
52    max_jobs: usize,
53    variables: HashMap<String, String>,
54    external_deps: ExternalDependencySearchConfig,
55    entrypoint_layout: RockLayoutConfig,
56
57    cache_dir: PathBuf,
58    data_dir: PathBuf,
59    vendor_dir: Option<PathBuf>,
60
61    user_agent: String,
62
63    generate_luarc: bool,
64    luarc_file_name: String,
65    wrap_bin_scripts: bool,
66    package_types: RemotePackageTypeFilterSpec,
67}
68
69impl Config {
70    /// Lux application directories
71    fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
72        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
73    }
74
75    /// Lux cache directory
76    fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
77        let project_dirs = Config::project_dirs()?;
78        Ok(project_dirs.cache_dir().to_path_buf())
79    }
80
81    /// Lux data directory
82    fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
83        let project_dirs = Config::project_dirs()?;
84        Ok(project_dirs.data_local_dir().to_path_buf())
85    }
86
87    /// Create a copy of this config for the specified Lua version
88    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
89        Self {
90            lua_version: Some(lua_version),
91            ..self
92        }
93    }
94
95    /// Create a copy of this config with the specified install tree
96    pub fn with_tree(self, tree: PathBuf) -> Self {
97        Self {
98            user_tree: tree,
99            ..self
100        }
101    }
102
103    /// The luarocks repository server
104    pub fn server(&self) -> &Url {
105        &self.server
106    }
107
108    /// Additional luarocks repository servers
109    pub fn extra_servers(&self) -> &Vec<Url> {
110        self.extra_servers.as_ref()
111    }
112
113    /// Enabled luarocks repository servers that provide dev/scm rocks
114    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
115        let mut enabled_dev_servers = Vec::new();
116        if self.enable_development_packages {
117            let config_file = ConfigBuilder::config_file()
118                .map(|p| p.to_string_lossy().to_string())
119                .unwrap_or_default();
120            enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
121                ConfigError::UrlParseError {
122                    source,
123                    help: Some(format!("check the `server` URL in {config_file}")),
124                }
125            })?);
126            for server in self.extra_servers() {
127                enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
128                    ConfigError::UrlParseError {
129                        source,
130                        help: Some(format!("check the `extra_servers` URLs in {config_file}")),
131                    }
132                })?);
133            }
134        }
135        Ok(enabled_dev_servers)
136    }
137
138    /// The luarocks server namespace to use
139    pub fn namespace(&self) -> Option<&String> {
140        self.namespace.as_ref()
141    }
142
143    /// The directory in which to install Lua{n} if not found
144    pub fn lua_dir(&self) -> Option<&PathBuf> {
145        self.lua_dir.as_ref()
146    }
147
148    // TODO(vhyrro): Remove `LuaVersion::from(&config)` and keep this only.
149    pub fn lua_version(&self) -> Option<&LuaVersion> {
150        self.lua_version.as_ref()
151    }
152
153    /// The tree in which to install rocks.
154    /// If installing packages for a project, use `Project::tree` instead.
155    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
156        Tree::new(self.user_tree.clone(), version, self)
157    }
158
159    /// Whether to display verbose output of commands executed
160    pub fn verbose(&self) -> bool {
161        self.verbose
162    }
163
164    /// Whether to disable printing progress bars and spinners
165    pub fn no_progress(&self) -> bool {
166        self.no_progress
167    }
168
169    /// Whether to skip prompts, selecting the default option
170    pub fn no_prompt(&self) -> bool {
171        self.no_prompt
172    }
173
174    /// Timeout on network operations, in seconds.
175    /// 0 means no timeout (wait forever).
176    pub fn timeout(&self) -> &Duration {
177        &self.timeout
178    }
179
180    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
181    /// 0 means no limit.
182    pub fn max_jobs(&self) -> usize {
183        self.max_jobs
184    }
185
186    /// Command to use for running `make` builds
187    pub fn make_cmd(&self) -> String {
188        match self.variables.get("MAKE") {
189            Some(make) => make.clone(),
190            None => "make".into(),
191        }
192    }
193
194    /// Command to use for running `cmake` builds
195    pub fn cmake_cmd(&self) -> String {
196        match self.variables.get("CMAKE") {
197            Some(cmake) => cmake.clone(),
198            None => "cmake".into(),
199        }
200    }
201
202    /// Variable names, mapped to their values.
203    /// Lux populates variables in the `lux.toml` and in RockSpecs
204    /// with these before building.
205    pub fn variables(&self) -> &HashMap<String, String> {
206        &self.variables
207    }
208
209    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
210        &self.external_deps
211    }
212
213    /// The rock layout for entrypoints of new install trees.
214    /// Does not affect existing install trees or dependency rock layouts.
215    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
216        &self.entrypoint_layout
217    }
218
219    /// The Lux cache directory
220    pub fn cache_dir(&self) -> &PathBuf {
221        &self.cache_dir
222    }
223
224    /// The Lux data directory
225    pub fn data_dir(&self) -> &PathBuf {
226        &self.data_dir
227    }
228
229    /// Specifies a directory with locally vendored sources and RockSpecs.
230    /// When building or installing a package with this flag,
231    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
232    pub fn vendor_dir(&self) -> Option<&PathBuf> {
233        self.vendor_dir.as_ref()
234    }
235
236    /// The user agent to use when making web requests.
237    pub fn user_agent(&self) -> &str {
238        &self.user_agent
239    }
240
241    /// Whether to generate a `.luarc.json` on build.
242    pub fn generate_luarc(&self) -> bool {
243        self.generate_luarc
244    }
245
246    // Lua runtime configuration file name
247    pub fn luarc_file_name(&self) -> &str {
248        &self.luarc_file_name
249    }
250
251    /// Whether to wrap installed Lua bin scripts to be executed with
252    /// the detected or configured Lua installation.
253    /// If `true`, individual rocks can still disable wrapping of their own bin scripts.
254    pub fn wrap_bin_scripts(&self) -> bool {
255        self.wrap_bin_scripts
256    }
257
258    /// Filter specification for package types to include in searches.
259    pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
260        &self.package_types
261    }
262}
263
264impl HasVariables for Config {
265    #[tracing::instrument(level = "trace")]
266    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
267        Ok(self.variables.get(input).cloned())
268    }
269}
270
271#[derive(Error, Debug, Diagnostic)]
272pub enum ConfigError {
273    #[error(transparent)]
274    #[diagnostic(transparent)]
275    Fs(#[from] fs::FsError),
276    #[error(transparent)]
277    #[diagnostic(transparent)]
278    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
279    #[error("error parsing {config_file}")]
280    Deserialize {
281        config_file: String,
282        #[diagnostic_source]
283        source: TomlDeError,
284    },
285    #[error("error parsing URL: {source}")]
286    UrlParseError {
287        source: url::ParseError,
288        #[help]
289        help: Option<String>,
290    },
291}
292
293/// Incrementally builds a [`Config`] by layering configuration sources.
294///
295/// - Call [`ConfigBuilder::default`] to start with a blank slate,
296///   or call [`ConfigBuilder::new`] to start from a deserialised configuration file.
297/// - Populate the fields from overriding sources (e.g. CLI arguments).
298/// - Finish with [`ConfigBuilder::build`].
299#[derive(Debug, Clone, Default, Deserialize, Serialize)]
300pub struct ConfigBuilder {
301    #[serde(
302        default,
303        deserialize_with = "deserialize_url",
304        serialize_with = "serialize_url"
305    )]
306    server: Option<Url>,
307    #[serde(
308        default,
309        deserialize_with = "deserialize_url_vec",
310        serialize_with = "serialize_url_vec"
311    )]
312    extra_servers: Option<Vec<Url>>,
313    namespace: Option<String>,
314    lua_version: Option<LuaVersion>,
315    user_tree: Option<PathBuf>,
316    lua_dir: Option<PathBuf>,
317    cache_dir: Option<PathBuf>,
318    data_dir: Option<PathBuf>,
319    vendor_dir: Option<PathBuf>,
320    enable_development_packages: Option<bool>,
321    verbose: Option<bool>,
322    no_progress: Option<bool>,
323    no_prompt: Option<bool>,
324    timeout: Option<Duration>,
325    max_jobs: Option<usize>,
326    variables: Option<HashMap<String, String>>,
327    #[serde(default)]
328    external_deps: ExternalDependencySearchConfig,
329    #[serde(default)]
330    entrypoint_layout: RockLayoutConfig,
331    user_agent: Option<String>,
332    generate_luarc: Option<bool>,
333    luarc_file_name: Option<String>,
334    wrap_bin_scripts: Option<bool>,
335    package_types: Option<RemotePackageTypeFilterSpec>,
336}
337
338/// A builder for the lux `Config`.
339impl ConfigBuilder {
340    /// Create a new `ConfigBuilder` by deserializing from a config file
341    /// if present, or otherwise by instantiating the default config.
342    pub fn new() -> Result<Self, ConfigError> {
343        let config_file = Self::config_file()?;
344        if config_file.is_file() {
345            let config_file_name = config_file.to_string_lossy().to_string();
346            let content = fs::sync::read_to_string(&config_file)?;
347            crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
348                ConfigError::Deserialize {
349                    config_file: config_file_name,
350                    source,
351                }
352            })
353        } else {
354            Ok(Self::default())
355        }
356    }
357
358    /// Get the path to the lux config file.
359    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
360        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
361            .ok_or(NoValidHomeDirectory)?;
362        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
363    }
364
365    /// Whether to enable development packages
366    /// Default: `false`
367    pub fn dev(self, dev: Option<bool>) -> Self {
368        Self {
369            enable_development_packages: dev.or(self.enable_development_packages),
370            ..self
371        }
372    }
373
374    /// Fetch rocks/rockspecs from this luarocks server
375    /// Default: `"https://luarocks.org/"`
376    pub fn server(self, server: Option<Url>) -> Self {
377        Self {
378            server: server.or(self.server),
379            ..self
380        }
381    }
382
383    /// Fetch rocks/rockspecs from these servers in addition to the main server
384    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
385        Self {
386            extra_servers: extra_servers.or(self.extra_servers),
387            ..self
388        }
389    }
390
391    /// The luarocks server namespace to use
392    pub fn namespace(self, namespace: Option<String>) -> Self {
393        Self {
394            namespace: namespace.or(self.namespace),
395            ..self
396        }
397    }
398
399    /// The directory in which to install Lua if not found
400    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
401        Self {
402            lua_dir: lua_dir.or(self.lua_dir),
403            ..self
404        }
405    }
406
407    /// Which Lua version to use.
408    /// Default: The installed Lua version, if detected
409    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
410        Self {
411            lua_version: lua_version.or(self.lua_version),
412            ..self
413        }
414    }
415
416    /// Which tree to operate on
417    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
418        Self {
419            user_tree: tree.or(self.user_tree),
420            ..self
421        }
422    }
423
424    /// Variable names, mapped to their values.
425    /// Lux populates variables in the `lux.toml` and in RockSpecs
426    /// with these before building.
427    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
428        Self {
429            variables: variables.or(self.variables),
430            ..self
431        }
432    }
433
434    /// Whether to display verbose output of commands executed.
435    /// Default: `false`
436    pub fn verbose(self, verbose: Option<bool>) -> Self {
437        Self {
438            verbose: verbose.or(self.verbose),
439            ..self
440        }
441    }
442
443    /// Whether to disable printing progress bars and spinners
444    /// Default: `false`
445    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
446        Self {
447            no_progress: no_progress.or(self.no_progress),
448            ..self
449        }
450    }
451
452    /// Whether to disable user prompts
453    /// Default: `false`
454    pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
455        Self {
456            no_prompt: no_prompt.or(self.no_prompt),
457            ..self
458        }
459    }
460
461    /// Timeout on network operations, in seconds.
462    /// 0 means no timeout (wait forever).
463    /// Default: 30 s
464    pub fn timeout(self, timeout: Option<Duration>) -> Self {
465        Self {
466            timeout: timeout.or(self.timeout),
467            ..self
468        }
469    }
470
471    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
472    /// 0 means no limit.
473    /// Default: 0
474    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
475        Self {
476            max_jobs: max_jobs.or(self.max_jobs),
477            ..self
478        }
479    }
480
481    /// The cache directory, e.g. for luarocks manifests.
482    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
483        Self {
484            cache_dir: cache_dir.or(self.cache_dir),
485            ..self
486        }
487    }
488
489    /// The data directory, in which the default user install tree resides.
490    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
491        Self {
492            data_dir: data_dir.or(self.data_dir),
493            ..self
494        }
495    }
496
497    /// Specifies a directory with locally vendored sources and RockSpecs.
498    /// When building or installing a package with this flag,
499    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
500    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
501        Self {
502            vendor_dir: vendor_dir.or(self.vendor_dir),
503            ..self
504        }
505    }
506
507    /// The rock layout for entrypoints of new install trees.
508    /// Does not affect existing install trees or dependency rock layouts.
509    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
510        Self {
511            entrypoint_layout: rock_layout,
512            ..self
513        }
514    }
515
516    /// The user agent to set when making web requests.
517    /// Default: "lux-lib/<version>".
518    pub fn user_agent(self, user_agent: Option<String>) -> Self {
519        Self {
520            user_agent: user_agent.or(self.user_agent),
521            ..self
522        }
523    }
524
525    /// Whether to generate a `.luarc.json` on build.
526    /// Default: `true`
527    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
528        Self {
529            generate_luarc: generate.or(self.generate_luarc),
530            ..self
531        }
532    }
533
534    /// Lua runtime configuration file name
535    /// Default: `.luarc.json`
536    pub fn luarc_file_name(self, file: Option<String>) -> Self {
537        Self {
538            luarc_file_name: file.or(self.luarc_file_name),
539            ..self
540        }
541    }
542
543    /// Whether to wrap installed Lua bin scripts to be executed with
544    /// the detected or configured Lua installation.
545    /// Setting this to `false` disables wrapping globally.
546    /// If set to `true`, individual rocks can still disable wrapping of their own bin scripts.
547    /// Default: `true`.
548    pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
549        Self {
550            wrap_bin_scripts: generate.or(self.generate_luarc),
551            ..self
552        }
553    }
554
555    #[tracing::instrument(level = "trace")]
556    pub fn build(self) -> Result<Config, ConfigError> {
557        let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
558        let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
559        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
560
561        let lua_version = self
562            .lua_version
563            .or(crate::lua_installation::detect_installed_lua_version());
564
565        Ok(Config {
566            enable_development_packages: self.enable_development_packages.unwrap_or(false),
567            server: self.server.unwrap_or_else(|| unsafe {
568                Url::parse("https://luarocks.org/").unwrap_unchecked()
569            }),
570            extra_servers: self.extra_servers.unwrap_or_default(),
571            namespace: self.namespace,
572            lua_dir: self.lua_dir,
573            lua_version,
574            user_tree,
575            verbose: self.verbose.unwrap_or(false),
576            no_progress: self.no_progress.unwrap_or(false),
577            no_prompt: self.no_prompt.unwrap_or(false),
578            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
579            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
580                0 => usize::MAX,
581                max_jobs => max_jobs,
582            },
583            variables: default_variables()
584                .chain(self.variables.unwrap_or_default())
585                .collect(),
586            external_deps: self.external_deps,
587            entrypoint_layout: self.entrypoint_layout,
588            cache_dir,
589            data_dir,
590            vendor_dir: self.vendor_dir,
591            user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
592            generate_luarc: self.generate_luarc.unwrap_or(true),
593            luarc_file_name: self
594                .luarc_file_name
595                .unwrap_or_else(|| ".luarc.json".to_string()),
596            wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
597            package_types: self.package_types.unwrap_or_default(),
598        })
599    }
600}
601
602/// Useful for printing the current config
603impl From<Config> for ConfigBuilder {
604    fn from(value: Config) -> Self {
605        ConfigBuilder {
606            enable_development_packages: Some(value.enable_development_packages),
607            server: Some(value.server),
608            extra_servers: Some(value.extra_servers),
609            namespace: value.namespace,
610            lua_dir: value.lua_dir,
611            lua_version: value.lua_version,
612            user_tree: Some(value.user_tree),
613            verbose: Some(value.verbose),
614            no_progress: Some(value.no_progress),
615            no_prompt: Some(value.no_prompt),
616            timeout: Some(value.timeout),
617            max_jobs: if value.max_jobs == usize::MAX {
618                None
619            } else {
620                Some(value.max_jobs)
621            },
622            variables: Some(value.variables),
623            cache_dir: Some(value.cache_dir),
624            data_dir: Some(value.data_dir),
625            vendor_dir: value.vendor_dir,
626            external_deps: value.external_deps,
627            entrypoint_layout: value.entrypoint_layout,
628            user_agent: Some(value.user_agent),
629            generate_luarc: Some(value.generate_luarc),
630            luarc_file_name: Some(value.luarc_file_name),
631            wrap_bin_scripts: Some(value.wrap_bin_scripts),
632            package_types: Some(value.package_types),
633        }
634    }
635}
636
637fn default_variables() -> impl Iterator<Item = (String, String)> {
638    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
639    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
640    vec![
641        ("MAKE".into(), "make".into()),
642        ("CMAKE".into(), "cmake".into()),
643        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
644        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
645        ("CFLAGS".into(), cflags),
646        ("LDFLAGS".into(), ldflags),
647        ("LIBFLAG".into(), utils::default_libflag().into()),
648    ]
649    .into_iter()
650}
651
652fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
653where
654    D: serde::Deserializer<'de>,
655{
656    let s = Option::<String>::deserialize(deserializer)?;
657    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
658        .transpose()
659}
660
661fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
662where
663    S: Serializer,
664{
665    match url {
666        Some(url) => serializer.serialize_some(url.as_str()),
667        None => serializer.serialize_none(),
668    }
669}
670
671fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
672where
673    D: serde::Deserializer<'de>,
674{
675    let s = Option::<Vec<String>>::deserialize(deserializer)?;
676    s.map(|v| {
677        v.into_iter()
678            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
679            .try_collect()
680    })
681    .transpose()
682}
683
684fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
685where
686    S: Serializer,
687{
688    match urls {
689        Some(urls) => {
690            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
691            serializer.serialize_some(&url_strings)
692        }
693        None => serializer.serialize_none(),
694    }
695}