Skip to main content

lux_lib/config/
mod.rs

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