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