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