1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use miette::Diagnostic;
6use serde::{Deserialize, Serialize, Serializer};
7use std::{collections::HashMap, env, path::PathBuf, time::Duration};
8use thiserror::Error;
9use tree::RockLayoutConfig;
10use url::Url;
11
12use crate::fs;
13use crate::lua_version::LuaVersion;
14use crate::package::RemotePackageTypeFilterSpec;
15use crate::project::TomlDeError;
16use crate::tree::{Tree, TreeError};
17use crate::variables::GetVariableError;
18use crate::{build::utils, variables::HasVariables};
19
20pub mod external_deps;
21pub mod tree;
22
23const DEV_PATH: &str = "dev/";
24const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
25
26#[derive(Error, Debug, Diagnostic)]
27#[error("could not find a valid home directory")]
28#[diagnostic(
29 code(lux_lib::no_home_directory),
30 help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
31)]
32pub struct NoValidHomeDirectory;
33
34#[derive(Debug, Clone)]
38pub struct Config {
39 enable_development_packages: bool,
40 server: Url,
41 extra_servers: Vec<Url>,
42 namespace: Option<String>,
43 lua_dir: Option<PathBuf>,
44 lua_version: Option<LuaVersion>,
45 user_tree: PathBuf,
46 verbose: bool,
47 no_progress: bool,
49 no_prompt: bool,
51 timeout: Duration,
52 max_jobs: usize,
53 variables: HashMap<String, String>,
54 external_deps: ExternalDependencySearchConfig,
55 entrypoint_layout: RockLayoutConfig,
56
57 cache_dir: PathBuf,
58 data_dir: PathBuf,
59 vendor_dir: Option<PathBuf>,
60
61 user_agent: String,
62
63 generate_luarc: bool,
64 luarc_file_name: String,
65 wrap_bin_scripts: bool,
66 package_types: RemotePackageTypeFilterSpec,
67 no_tfa: bool,
68}
69
70impl Config {
71 fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
73 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
74 }
75
76 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
78 let project_dirs = Config::project_dirs()?;
79 Ok(project_dirs.cache_dir().to_path_buf())
80 }
81
82 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
84 let project_dirs = Config::project_dirs()?;
85 Ok(project_dirs.data_local_dir().to_path_buf())
86 }
87
88 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
90 Self {
91 lua_version: Some(lua_version),
92 ..self
93 }
94 }
95
96 pub fn with_tree(self, tree: PathBuf) -> Self {
98 Self {
99 user_tree: tree,
100 ..self
101 }
102 }
103
104 pub fn server(&self) -> &Url {
106 &self.server
107 }
108
109 pub fn extra_servers(&self) -> &Vec<Url> {
111 self.extra_servers.as_ref()
112 }
113
114 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
116 let mut enabled_dev_servers = Vec::new();
117 if self.enable_development_packages {
118 let config_file = ConfigBuilder::config_file()
119 .map(|p| p.to_string_lossy().to_string())
120 .unwrap_or_default();
121 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
122 ConfigError::UrlParseError {
123 source,
124 help: Some(format!("check the `server` URL in {config_file}")),
125 }
126 })?);
127 for server in self.extra_servers() {
128 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
129 ConfigError::UrlParseError {
130 source,
131 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
132 }
133 })?);
134 }
135 }
136 Ok(enabled_dev_servers)
137 }
138
139 pub fn namespace(&self) -> Option<&String> {
141 self.namespace.as_ref()
142 }
143
144 pub fn lua_dir(&self) -> Option<&PathBuf> {
146 self.lua_dir.as_ref()
147 }
148
149 pub fn lua_version(&self) -> Option<&LuaVersion> {
151 self.lua_version.as_ref()
152 }
153
154 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
157 Tree::new(self.user_tree.clone(), version, self)
158 }
159
160 pub fn verbose(&self) -> bool {
162 self.verbose
163 }
164
165 pub fn no_progress(&self) -> bool {
167 self.no_progress
168 }
169
170 pub fn no_prompt(&self) -> bool {
172 self.no_prompt
173 }
174
175 pub fn timeout(&self) -> &Duration {
178 &self.timeout
179 }
180
181 pub fn max_jobs(&self) -> usize {
184 self.max_jobs
185 }
186
187 pub fn make_cmd(&self) -> String {
189 match self.variables.get("MAKE") {
190 Some(make) => make.clone(),
191 None => "make".into(),
192 }
193 }
194
195 pub fn cmake_cmd(&self) -> String {
197 match self.variables.get("CMAKE") {
198 Some(cmake) => cmake.clone(),
199 None => "cmake".into(),
200 }
201 }
202
203 pub fn variables(&self) -> &HashMap<String, String> {
207 &self.variables
208 }
209
210 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
211 &self.external_deps
212 }
213
214 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
217 &self.entrypoint_layout
218 }
219
220 pub fn cache_dir(&self) -> &PathBuf {
222 &self.cache_dir
223 }
224
225 pub fn data_dir(&self) -> &PathBuf {
227 &self.data_dir
228 }
229
230 pub fn vendor_dir(&self) -> Option<&PathBuf> {
234 self.vendor_dir.as_ref()
235 }
236
237 pub fn user_agent(&self) -> &str {
239 &self.user_agent
240 }
241
242 pub fn generate_luarc(&self) -> bool {
244 self.generate_luarc
245 }
246
247 pub fn luarc_file_name(&self) -> &str {
249 &self.luarc_file_name
250 }
251
252 pub fn wrap_bin_scripts(&self) -> bool {
256 self.wrap_bin_scripts
257 }
258
259 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
261 &self.package_types
262 }
263 pub fn no_tfa(&self) -> bool {
265 self.no_tfa
266 }
267}
268
269impl HasVariables for Config {
270 #[tracing::instrument(level = "trace")]
271 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
272 Ok(self.variables.get(input).cloned())
273 }
274}
275
276#[derive(Error, Debug, Diagnostic)]
277pub enum ConfigError {
278 #[error(transparent)]
279 #[diagnostic(transparent)]
280 Fs(#[from] fs::FsError),
281 #[error(transparent)]
282 #[diagnostic(transparent)]
283 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
284 #[error("error parsing {config_file}")]
285 Deserialize {
286 config_file: String,
287 #[diagnostic_source]
288 source: TomlDeError,
289 },
290 #[error("error parsing URL: {source}")]
291 UrlParseError {
292 source: url::ParseError,
293 #[help]
294 help: Option<String>,
295 },
296}
297
298#[derive(Debug, Clone, Default, Deserialize, Serialize)]
305pub struct ConfigBuilder {
306 #[serde(
307 default,
308 deserialize_with = "deserialize_url",
309 serialize_with = "serialize_url"
310 )]
311 server: Option<Url>,
312 #[serde(
313 default,
314 deserialize_with = "deserialize_url_vec",
315 serialize_with = "serialize_url_vec"
316 )]
317 extra_servers: Option<Vec<Url>>,
318 namespace: Option<String>,
319 lua_version: Option<LuaVersion>,
320 user_tree: Option<PathBuf>,
321 lua_dir: Option<PathBuf>,
322 cache_dir: Option<PathBuf>,
323 data_dir: Option<PathBuf>,
324 vendor_dir: Option<PathBuf>,
325 enable_development_packages: Option<bool>,
326 verbose: Option<bool>,
327 no_progress: Option<bool>,
328 no_prompt: Option<bool>,
329 timeout: Option<Duration>,
330 max_jobs: Option<usize>,
331 variables: Option<HashMap<String, String>>,
332 #[serde(default)]
333 external_deps: ExternalDependencySearchConfig,
334 #[serde(default)]
335 entrypoint_layout: RockLayoutConfig,
336 user_agent: Option<String>,
337 generate_luarc: Option<bool>,
338 luarc_file_name: Option<String>,
339 wrap_bin_scripts: Option<bool>,
340 package_types: Option<RemotePackageTypeFilterSpec>,
341 no_tfa: Option<bool>,
342}
343
344impl ConfigBuilder {
346 pub fn new() -> Result<Self, ConfigError> {
349 let config_file = Self::config_file()?;
350 if config_file.is_file() {
351 let config_file_name = config_file.to_string_lossy().to_string();
352 let content = fs::sync::read_to_string(&config_file)?;
353 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
354 ConfigError::Deserialize {
355 config_file: config_file_name,
356 source,
357 }
358 })
359 } else {
360 Ok(Self::default())
361 }
362 }
363
364 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
366 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
367 .ok_or(NoValidHomeDirectory)?;
368 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
369 }
370
371 pub fn dev(self, dev: Option<bool>) -> Self {
374 Self {
375 enable_development_packages: dev.or(self.enable_development_packages),
376 ..self
377 }
378 }
379
380 pub fn server(self, server: Option<Url>) -> Self {
383 Self {
384 server: server.or(self.server),
385 ..self
386 }
387 }
388
389 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
391 Self {
392 extra_servers: extra_servers.or(self.extra_servers),
393 ..self
394 }
395 }
396
397 pub fn namespace(self, namespace: Option<String>) -> Self {
399 Self {
400 namespace: namespace.or(self.namespace),
401 ..self
402 }
403 }
404
405 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
407 Self {
408 lua_dir: lua_dir.or(self.lua_dir),
409 ..self
410 }
411 }
412
413 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
416 Self {
417 lua_version: lua_version.or(self.lua_version),
418 ..self
419 }
420 }
421
422 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
424 Self {
425 user_tree: tree.or(self.user_tree),
426 ..self
427 }
428 }
429
430 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
434 Self {
435 variables: variables.or(self.variables),
436 ..self
437 }
438 }
439
440 pub fn verbose(self, verbose: Option<bool>) -> Self {
443 Self {
444 verbose: verbose.or(self.verbose),
445 ..self
446 }
447 }
448
449 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
452 Self {
453 no_progress: no_progress.or(self.no_progress),
454 ..self
455 }
456 }
457
458 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
461 Self {
462 no_prompt: no_prompt.or(self.no_prompt),
463 ..self
464 }
465 }
466
467 pub fn timeout(self, timeout: Option<Duration>) -> Self {
471 Self {
472 timeout: timeout.or(self.timeout),
473 ..self
474 }
475 }
476
477 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
481 Self {
482 max_jobs: max_jobs.or(self.max_jobs),
483 ..self
484 }
485 }
486
487 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
489 Self {
490 cache_dir: cache_dir.or(self.cache_dir),
491 ..self
492 }
493 }
494
495 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
497 Self {
498 data_dir: data_dir.or(self.data_dir),
499 ..self
500 }
501 }
502
503 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
507 Self {
508 vendor_dir: vendor_dir.or(self.vendor_dir),
509 ..self
510 }
511 }
512
513 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
516 Self {
517 entrypoint_layout: rock_layout,
518 ..self
519 }
520 }
521
522 pub fn user_agent(self, user_agent: Option<String>) -> Self {
525 Self {
526 user_agent: user_agent.or(self.user_agent),
527 ..self
528 }
529 }
530
531 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
534 Self {
535 generate_luarc: generate.or(self.generate_luarc),
536 ..self
537 }
538 }
539
540 pub fn luarc_file_name(self, file: Option<String>) -> Self {
543 Self {
544 luarc_file_name: file.or(self.luarc_file_name),
545 ..self
546 }
547 }
548
549 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
555 Self {
556 wrap_bin_scripts: generate.or(self.generate_luarc),
557 ..self
558 }
559 }
560 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
563 Self {
564 no_tfa: tfa.or(self.no_tfa),
565 ..self
566 }
567 }
568
569 #[tracing::instrument(level = "trace")]
570 pub fn build(self) -> Result<Config, ConfigError> {
571 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
572 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
573 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
574
575 let lua_version = self
576 .lua_version
577 .or(crate::lua_installation::detect_installed_lua_version());
578
579 Ok(Config {
580 enable_development_packages: self.enable_development_packages.unwrap_or(false),
581 server: self.server.unwrap_or_else(|| unsafe {
582 Url::parse("https://luarocks.org/").unwrap_unchecked()
583 }),
584 extra_servers: self.extra_servers.unwrap_or_default(),
585 namespace: self.namespace,
586 lua_dir: self.lua_dir,
587 lua_version,
588 user_tree,
589 verbose: self.verbose.unwrap_or(false),
590 no_progress: self.no_progress.unwrap_or(false),
591 no_prompt: self.no_prompt.unwrap_or(false),
592 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
593 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
594 0 => usize::MAX,
595 max_jobs => max_jobs,
596 },
597 variables: default_variables()
598 .chain(self.variables.unwrap_or_default())
599 .collect(),
600 external_deps: self.external_deps,
601 entrypoint_layout: self.entrypoint_layout,
602 cache_dir,
603 data_dir,
604 vendor_dir: self.vendor_dir,
605 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
606 generate_luarc: self.generate_luarc.unwrap_or(true),
607 luarc_file_name: self
608 .luarc_file_name
609 .unwrap_or_else(|| ".luarc.json".to_string()),
610 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
611 package_types: self.package_types.unwrap_or_default(),
612 no_tfa: self.no_tfa.unwrap_or(false),
613 })
614 }
615}
616
617impl From<Config> for ConfigBuilder {
619 fn from(value: Config) -> Self {
620 ConfigBuilder {
621 enable_development_packages: Some(value.enable_development_packages),
622 server: Some(value.server),
623 extra_servers: Some(value.extra_servers),
624 namespace: value.namespace,
625 lua_dir: value.lua_dir,
626 lua_version: value.lua_version,
627 user_tree: Some(value.user_tree),
628 verbose: Some(value.verbose),
629 no_progress: Some(value.no_progress),
630 no_prompt: Some(value.no_prompt),
631 timeout: Some(value.timeout),
632 max_jobs: if value.max_jobs == usize::MAX {
633 None
634 } else {
635 Some(value.max_jobs)
636 },
637 variables: Some(value.variables),
638 cache_dir: Some(value.cache_dir),
639 data_dir: Some(value.data_dir),
640 vendor_dir: value.vendor_dir,
641 external_deps: value.external_deps,
642 entrypoint_layout: value.entrypoint_layout,
643 user_agent: Some(value.user_agent),
644 generate_luarc: Some(value.generate_luarc),
645 luarc_file_name: Some(value.luarc_file_name),
646 wrap_bin_scripts: Some(value.wrap_bin_scripts),
647 package_types: Some(value.package_types),
648 no_tfa: Some(value.no_tfa),
649 }
650 }
651}
652
653fn default_variables() -> impl Iterator<Item = (String, String)> {
654 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
655 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
656 vec![
657 ("MAKE".into(), "make".into()),
658 ("CMAKE".into(), "cmake".into()),
659 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
660 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
661 ("CFLAGS".into(), cflags),
662 ("LDFLAGS".into(), ldflags),
663 ("LIBFLAG".into(), utils::default_libflag().into()),
664 ]
665 .into_iter()
666}
667
668fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
669where
670 D: serde::Deserializer<'de>,
671{
672 let s = Option::<String>::deserialize(deserializer)?;
673 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
674 .transpose()
675}
676
677fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
678where
679 S: Serializer,
680{
681 match url {
682 Some(url) => serializer.serialize_some(url.as_str()),
683 None => serializer.serialize_none(),
684 }
685}
686
687fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
688where
689 D: serde::Deserializer<'de>,
690{
691 let s = Option::<Vec<String>>::deserialize(deserializer)?;
692 s.map(|v| {
693 v.into_iter()
694 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
695 .try_collect()
696 })
697 .transpose()
698}
699
700fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
701where
702 S: Serializer,
703{
704 match urls {
705 Some(urls) => {
706 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
707 serializer.serialize_some(&url_strings)
708 }
709 None => serializer.serialize_none(),
710 }
711}