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