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