Skip to main content

lux_lib/config/
mod.rs

1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use serde::{Deserialize, Serialize, Serializer};
6use std::{collections::HashMap, env, io, path::PathBuf, time::Duration};
7use thiserror::Error;
8use tree::RockLayoutConfig;
9use url::Url;
10
11use crate::lua_version::LuaVersion;
12use crate::tree::{Tree, TreeError};
13use crate::variables::GetVariableError;
14use crate::{build::utils, variables::HasVariables};
15
16pub mod external_deps;
17pub mod tree;
18
19const DEV_PATH: &str = "dev/";
20const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
21
22#[derive(Error, Debug)]
23#[error("could not find a valid home directory")]
24pub struct NoValidHomeDirectory;
25
26/// The resolved configuration for a Lux session.
27/// Can be constructed via [`ConfigBuilder`], which supports layering multiple
28/// configuration sources (config file, CLI flags, environment variables).
29#[derive(Debug, Clone)]
30pub struct Config {
31    enable_development_packages: bool,
32    server: Url,
33    extra_servers: Vec<Url>,
34    only_sources: Option<String>,
35    namespace: Option<String>,
36    lua_dir: Option<PathBuf>,
37    lua_version: Option<LuaVersion>,
38    user_tree: PathBuf,
39    verbose: bool,
40    /// Don't display progress bars
41    no_progress: bool,
42    /// Skip prompts (choosing the default choice)
43    no_prompt: bool,
44    timeout: Duration,
45    max_jobs: usize,
46    variables: HashMap<String, String>,
47    external_deps: ExternalDependencySearchConfig,
48    /// The rock layout for entrypoints of new install trees.
49    /// Does not affect existing install trees or dependency rock layouts.
50    entrypoint_layout: RockLayoutConfig,
51
52    cache_dir: PathBuf,
53    data_dir: PathBuf,
54    vendor_dir: Option<PathBuf>,
55
56    /// The user agent to set when making web requests.
57    /// Default: "lux-lib/<version>".
58    user_agent: String,
59
60    generate_luarc: bool,
61    /// Whether to wrap installed Lua bin scripts to be executed with
62    /// the detected or configured Lua installation.
63    /// Setting this to `false` disables wrapping globally.
64    /// If set to `true`, individual rocks can still disable wrapping of their own bin scripts.
65    /// Default: `true`.
66    wrap_bin_scripts: bool,
67}
68
69impl Config {
70    pub fn get_project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
71        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
72    }
73
74    pub fn get_default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
75        let project_dirs = Config::get_project_dirs()?;
76        Ok(project_dirs.cache_dir().to_path_buf())
77    }
78
79    pub fn get_default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
80        let project_dirs = Config::get_project_dirs()?;
81        Ok(project_dirs.data_local_dir().to_path_buf())
82    }
83
84    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
85        Self {
86            lua_version: Some(lua_version),
87            ..self
88        }
89    }
90
91    pub fn with_tree(self, tree: PathBuf) -> Self {
92        Self {
93            user_tree: tree,
94            ..self
95        }
96    }
97
98    pub fn server(&self) -> &Url {
99        &self.server
100    }
101
102    pub fn extra_servers(&self) -> &Vec<Url> {
103        self.extra_servers.as_ref()
104    }
105
106    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
107        let mut enabled_dev_servers = Vec::new();
108        if self.enable_development_packages {
109            enabled_dev_servers.push(self.server().join(DEV_PATH)?);
110            for server in self.extra_servers() {
111                enabled_dev_servers.push(server.join(DEV_PATH)?);
112            }
113        }
114        Ok(enabled_dev_servers)
115    }
116
117    pub fn only_sources(&self) -> Option<&String> {
118        self.only_sources.as_ref()
119    }
120
121    pub fn namespace(&self) -> Option<&String> {
122        self.namespace.as_ref()
123    }
124
125    pub fn lua_dir(&self) -> Option<&PathBuf> {
126        self.lua_dir.as_ref()
127    }
128
129    // TODO(vhyrro): Remove `LuaVersion::from(&config)` and keep this only.
130    pub fn lua_version(&self) -> Option<&LuaVersion> {
131        self.lua_version.as_ref()
132    }
133
134    /// The tree in which to install rocks.
135    /// If installing packges for a project, use `Project::tree` instead.
136    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
137        Tree::new(self.user_tree.clone(), version, self)
138    }
139
140    pub fn verbose(&self) -> bool {
141        self.verbose
142    }
143
144    pub fn no_progress(&self) -> bool {
145        self.no_progress
146    }
147
148    pub fn no_prompt(&self) -> bool {
149        self.no_prompt
150    }
151
152    pub fn timeout(&self) -> &Duration {
153        &self.timeout
154    }
155
156    pub fn max_jobs(&self) -> usize {
157        self.max_jobs
158    }
159
160    pub fn make_cmd(&self) -> String {
161        match self.variables.get("MAKE") {
162            Some(make) => make.clone(),
163            None => "make".into(),
164        }
165    }
166
167    pub fn cmake_cmd(&self) -> String {
168        match self.variables.get("CMAKE") {
169            Some(cmake) => cmake.clone(),
170            None => "cmake".into(),
171        }
172    }
173
174    pub fn variables(&self) -> &HashMap<String, String> {
175        &self.variables
176    }
177
178    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
179        &self.external_deps
180    }
181
182    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
183        &self.entrypoint_layout
184    }
185
186    pub fn cache_dir(&self) -> &PathBuf {
187        &self.cache_dir
188    }
189
190    pub fn data_dir(&self) -> &PathBuf {
191        &self.data_dir
192    }
193
194    pub fn vendor_dir(&self) -> Option<&PathBuf> {
195        self.vendor_dir.as_ref()
196    }
197
198    pub fn user_agent(&self) -> &str {
199        &self.user_agent
200    }
201
202    pub fn generate_luarc(&self) -> bool {
203        self.generate_luarc
204    }
205
206    pub fn wrap_bin_scripts(&self) -> bool {
207        self.wrap_bin_scripts
208    }
209}
210
211impl HasVariables for Config {
212    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
213        Ok(self.variables.get(input).cloned())
214    }
215}
216
217#[derive(Error, Debug)]
218pub enum ConfigError {
219    #[error(transparent)]
220    Io(#[from] io::Error),
221    #[error(transparent)]
222    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
223    #[error("error deserializing lux config: {0}")]
224    Deserialize(#[from] toml::de::Error),
225    #[error("error parsing URL: {0}")]
226    UrlParseError(#[from] url::ParseError),
227    #[error("error initializing compiler toolchain: {0}")]
228    CompilerToolchain(#[from] cc::Error),
229}
230
231/// Incrementally builds a [`Config`] by layering configuration sources.
232///
233/// - Call [`ConfigBuilder::default`] to start with a blank slate,
234///   or call [`ConfigBuilder::new`] to start from a deserialised configuration file.
235/// - Populate the fields from overriding sources (e.g. CLI arguments).
236/// - Finish with [`ConfigBuilder::build`].
237#[derive(Clone, Default, Deserialize, Serialize)]
238pub struct ConfigBuilder {
239    #[serde(
240        default,
241        deserialize_with = "deserialize_url",
242        serialize_with = "serialize_url"
243    )]
244    server: Option<Url>,
245    #[serde(
246        default,
247        deserialize_with = "deserialize_url_vec",
248        serialize_with = "serialize_url_vec"
249    )]
250    extra_servers: Option<Vec<Url>>,
251    only_sources: Option<String>,
252    namespace: Option<String>,
253    lua_version: Option<LuaVersion>,
254    user_tree: Option<PathBuf>,
255    lua_dir: Option<PathBuf>,
256    cache_dir: Option<PathBuf>,
257    data_dir: Option<PathBuf>,
258    vendor_dir: Option<PathBuf>,
259    enable_development_packages: Option<bool>,
260    verbose: Option<bool>,
261    no_progress: Option<bool>,
262    no_prompt: Option<bool>,
263    timeout: Option<Duration>,
264    max_jobs: Option<usize>,
265    variables: Option<HashMap<String, String>>,
266    #[serde(default)]
267    external_deps: ExternalDependencySearchConfig,
268    /// The rock layout for new install trees.
269    /// Does not affect existing install trees.
270    #[serde(default)]
271    entrypoint_layout: RockLayoutConfig,
272    user_agent: Option<String>,
273    generate_luarc: Option<bool>,
274    /// Whether to wrap installed Lua bin scripts to be executed with
275    /// the detected or configured Lua installation.
276    /// Setting this to `false` disables wrapping globally.
277    /// If set to `true`, individual rocks can still disable wrapping of their own bin scripts.
278    /// Default: `true`.
279    wrap_bin_scripts: Option<bool>,
280}
281
282/// A builder for the lux `Config`.
283impl ConfigBuilder {
284    /// Create a new `ConfigBuilder` from a config file by deserializing from a config file
285    /// if present, or otherwise by instantiating the default config.
286    pub fn new() -> Result<Self, ConfigError> {
287        let config_file = Self::config_file()?;
288        if config_file.is_file() {
289            Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
290        } else {
291            Ok(Self::default())
292        }
293    }
294
295    /// Get the path to the lux config file.
296    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
297        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
298            .ok_or(NoValidHomeDirectory)?;
299        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
300    }
301
302    pub fn dev(self, dev: Option<bool>) -> Self {
303        Self {
304            enable_development_packages: dev.or(self.enable_development_packages),
305            ..self
306        }
307    }
308
309    pub fn server(self, server: Option<Url>) -> Self {
310        Self {
311            server: server.or(self.server),
312            ..self
313        }
314    }
315
316    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
317        Self {
318            extra_servers: extra_servers.or(self.extra_servers),
319            ..self
320        }
321    }
322
323    pub fn only_sources(self, sources: Option<String>) -> Self {
324        Self {
325            only_sources: sources.or(self.only_sources),
326            ..self
327        }
328    }
329
330    pub fn namespace(self, namespace: Option<String>) -> Self {
331        Self {
332            namespace: namespace.or(self.namespace),
333            ..self
334        }
335    }
336
337    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
338        Self {
339            lua_dir: lua_dir.or(self.lua_dir),
340            ..self
341        }
342    }
343
344    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
345        Self {
346            lua_version: lua_version.or(self.lua_version),
347            ..self
348        }
349    }
350
351    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
352        Self {
353            user_tree: tree.or(self.user_tree),
354            ..self
355        }
356    }
357
358    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
359        Self {
360            variables: variables.or(self.variables),
361            ..self
362        }
363    }
364
365    pub fn verbose(self, verbose: Option<bool>) -> Self {
366        Self {
367            verbose: verbose.or(self.verbose),
368            ..self
369        }
370    }
371
372    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
373        Self {
374            no_progress: no_progress.or(self.no_progress),
375            ..self
376        }
377    }
378
379    pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
380        Self {
381            no_prompt: no_prompt.or(self.no_prompt),
382            ..self
383        }
384    }
385
386    pub fn timeout(self, timeout: Option<Duration>) -> Self {
387        Self {
388            timeout: timeout.or(self.timeout),
389            ..self
390        }
391    }
392
393    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
394        Self {
395            max_jobs: max_jobs.or(self.max_jobs),
396            ..self
397        }
398    }
399
400    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
401        Self {
402            cache_dir: cache_dir.or(self.cache_dir),
403            ..self
404        }
405    }
406
407    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
408        Self {
409            data_dir: data_dir.or(self.data_dir),
410            ..self
411        }
412    }
413
414    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
415        Self {
416            vendor_dir: vendor_dir.or(self.vendor_dir),
417            ..self
418        }
419    }
420
421    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
422        Self {
423            entrypoint_layout: rock_layout,
424            ..self
425        }
426    }
427
428    pub fn user_agent(self, user_agent: Option<String>) -> Self {
429        Self {
430            user_agent: user_agent.or(self.user_agent),
431            ..self
432        }
433    }
434
435    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
436        Self {
437            generate_luarc: generate.or(self.generate_luarc),
438            ..self
439        }
440    }
441
442    pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
443        Self {
444            wrap_bin_scripts: generate.or(self.generate_luarc),
445            ..self
446        }
447    }
448
449    pub fn build(self) -> Result<Config, ConfigError> {
450        let data_dir = self.data_dir.unwrap_or(Config::get_default_data_path()?);
451        let cache_dir = self.cache_dir.unwrap_or(Config::get_default_cache_path()?);
452        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
453
454        let lua_version = self
455            .lua_version
456            .or(crate::lua_installation::detect_installed_lua_version());
457
458        Ok(Config {
459            enable_development_packages: self.enable_development_packages.unwrap_or(false),
460            server: self.server.unwrap_or_else(|| unsafe {
461                Url::parse("https://luarocks.org/").unwrap_unchecked()
462            }),
463            extra_servers: self.extra_servers.unwrap_or_default(),
464            only_sources: self.only_sources,
465            namespace: self.namespace,
466            lua_dir: self.lua_dir,
467            lua_version,
468            user_tree,
469            verbose: self.verbose.unwrap_or(false),
470            no_progress: self.no_progress.unwrap_or(false),
471            no_prompt: self.no_prompt.unwrap_or(false),
472            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
473            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
474                0 => usize::MAX,
475                max_jobs => max_jobs,
476            },
477            variables: default_variables()
478                .chain(self.variables.unwrap_or_default())
479                .collect(),
480            external_deps: self.external_deps,
481            entrypoint_layout: self.entrypoint_layout,
482            cache_dir,
483            data_dir,
484            vendor_dir: self.vendor_dir,
485            user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
486            generate_luarc: self.generate_luarc.unwrap_or(true),
487            wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
488        })
489    }
490}
491
492/// Useful for printing the current config
493impl From<Config> for ConfigBuilder {
494    fn from(value: Config) -> Self {
495        ConfigBuilder {
496            enable_development_packages: Some(value.enable_development_packages),
497            server: Some(value.server),
498            extra_servers: Some(value.extra_servers),
499            only_sources: value.only_sources,
500            namespace: value.namespace,
501            lua_dir: value.lua_dir,
502            lua_version: value.lua_version,
503            user_tree: Some(value.user_tree),
504            verbose: Some(value.verbose),
505            no_progress: Some(value.no_progress),
506            no_prompt: Some(value.no_prompt),
507            timeout: Some(value.timeout),
508            max_jobs: if value.max_jobs == usize::MAX {
509                None
510            } else {
511                Some(value.max_jobs)
512            },
513            variables: Some(value.variables),
514            cache_dir: Some(value.cache_dir),
515            data_dir: Some(value.data_dir),
516            vendor_dir: value.vendor_dir,
517            external_deps: value.external_deps,
518            entrypoint_layout: value.entrypoint_layout,
519            user_agent: Some(value.user_agent),
520            generate_luarc: Some(value.generate_luarc),
521            wrap_bin_scripts: Some(value.wrap_bin_scripts),
522        }
523    }
524}
525
526fn default_variables() -> impl Iterator<Item = (String, String)> {
527    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
528    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
529    vec![
530        ("MAKE".into(), "make".into()),
531        ("CMAKE".into(), "cmake".into()),
532        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
533        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
534        ("CFLAGS".into(), cflags),
535        ("LDFLAGS".into(), ldflags),
536        ("LIBFLAG".into(), utils::default_libflag().into()),
537    ]
538    .into_iter()
539}
540
541fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
542where
543    D: serde::Deserializer<'de>,
544{
545    let s = Option::<String>::deserialize(deserializer)?;
546    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
547        .transpose()
548}
549
550fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
551where
552    S: Serializer,
553{
554    match url {
555        Some(url) => serializer.serialize_some(url.as_str()),
556        None => serializer.serialize_none(),
557    }
558}
559
560fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
561where
562    D: serde::Deserializer<'de>,
563{
564    let s = Option::<Vec<String>>::deserialize(deserializer)?;
565    s.map(|v| {
566        v.into_iter()
567            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
568            .try_collect()
569    })
570    .transpose()
571}
572
573fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
574where
575    S: Serializer,
576{
577    match urls {
578        Some(urls) => {
579            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
580            serializer.serialize_some(&url_strings)
581        }
582        None => serializer.serialize_none(),
583    }
584}