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