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