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