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 workspace_tree_root: Option<PathBuf>,
47 verbose: bool,
48 no_progress: bool,
50 no_prompt: bool,
52 timeout: Duration,
53 max_jobs: usize,
54 variables: HashMap<String, String>,
55 external_deps: ExternalDependencySearchConfig,
56 entrypoint_layout: RockLayoutConfig,
57
58 cache_dir: PathBuf,
59 data_dir: PathBuf,
60 vendor_dir: Option<PathBuf>,
61
62 user_agent: String,
63
64 generate_luarc: bool,
65 luarc_file_name: String,
66 wrap_bin_scripts: bool,
67 package_types: RemotePackageTypeFilterSpec,
68 no_tfa: bool,
69}
70
71impl Config {
72 pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
74 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
75 }
76
77 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
79 let project_dirs = Config::project_dirs()?;
80 Ok(project_dirs.cache_dir().to_path_buf())
81 }
82
83 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
85 let project_dirs = Config::project_dirs()?;
86 Ok(project_dirs.data_local_dir().to_path_buf())
87 }
88
89 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
91 Self {
92 lua_version: Some(lua_version),
93 ..self
94 }
95 }
96
97 pub fn with_tree(self, tree: PathBuf) -> Self {
99 Self {
100 user_tree: tree,
101 ..self
102 }
103 }
104
105 pub fn with_workspace_tree_root(self, tree: Option<PathBuf>) -> Self {
107 Self {
108 workspace_tree_root: tree,
109 ..self
110 }
111 }
112
113 pub fn server(&self) -> &Url {
115 &self.server
116 }
117
118 pub fn extra_servers(&self) -> &Vec<Url> {
120 self.extra_servers.as_ref()
121 }
122
123 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
125 let mut enabled_dev_servers = Vec::new();
126 if self.enable_development_packages {
127 let config_file = ConfigBuilder::config_file()
128 .map(|p| p.to_string_lossy().to_string())
129 .unwrap_or_default();
130 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
131 ConfigError::UrlParseError {
132 source,
133 help: Some(format!("check the `server` URL in {config_file}")),
134 }
135 })?);
136 for server in self.extra_servers() {
137 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
138 ConfigError::UrlParseError {
139 source,
140 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
141 }
142 })?);
143 }
144 }
145 Ok(enabled_dev_servers)
146 }
147
148 pub fn namespace(&self) -> Option<&String> {
150 self.namespace.as_ref()
151 }
152
153 pub fn lua_dir(&self) -> Option<&PathBuf> {
155 self.lua_dir.as_ref()
156 }
157
158 pub fn lua_version(&self) -> Option<&LuaVersion> {
160 self.lua_version.as_ref()
161 }
162
163 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
166 Tree::new(self.user_tree.clone(), version, self)
167 }
168
169 pub(crate) fn workspace_tree_root(&self) -> Option<&PathBuf> {
171 self.workspace_tree_root.as_ref()
172 }
173
174 pub fn verbose(&self) -> bool {
176 self.verbose
177 }
178
179 pub fn no_progress(&self) -> bool {
181 self.no_progress
182 }
183
184 pub fn no_prompt(&self) -> bool {
186 self.no_prompt
187 }
188
189 pub fn timeout(&self) -> &Duration {
192 &self.timeout
193 }
194
195 pub fn max_jobs(&self) -> usize {
198 self.max_jobs
199 }
200
201 pub fn make_cmd(&self) -> String {
203 match self.variables.get("MAKE") {
204 Some(make) => make.clone(),
205 None => "make".into(),
206 }
207 }
208
209 pub fn cmake_cmd(&self) -> String {
211 match self.variables.get("CMAKE") {
212 Some(cmake) => cmake.clone(),
213 None => "cmake".into(),
214 }
215 }
216
217 pub fn variables(&self) -> &HashMap<String, String> {
221 &self.variables
222 }
223
224 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
225 &self.external_deps
226 }
227
228 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
231 &self.entrypoint_layout
232 }
233
234 pub fn cache_dir(&self) -> &PathBuf {
236 &self.cache_dir
237 }
238
239 pub fn data_dir(&self) -> &PathBuf {
241 &self.data_dir
242 }
243
244 pub fn vendor_dir(&self) -> Option<&PathBuf> {
248 self.vendor_dir.as_ref()
249 }
250
251 pub fn user_agent(&self) -> &str {
253 &self.user_agent
254 }
255
256 pub fn generate_luarc(&self) -> bool {
258 self.generate_luarc
259 }
260
261 pub fn luarc_file_name(&self) -> &str {
263 &self.luarc_file_name
264 }
265
266 pub fn wrap_bin_scripts(&self) -> bool {
270 self.wrap_bin_scripts
271 }
272
273 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
275 &self.package_types
276 }
277 pub fn no_tfa(&self) -> bool {
279 self.no_tfa
280 }
281}
282
283impl HasVariables for Config {
284 #[tracing::instrument(level = "trace")]
285 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
286 Ok(self.variables.get(input).cloned())
287 }
288}
289
290#[derive(Error, Debug, Diagnostic)]
291pub enum ConfigError {
292 #[error(transparent)]
293 #[diagnostic(transparent)]
294 Fs(#[from] fs::FsError),
295 #[error(transparent)]
296 #[diagnostic(transparent)]
297 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
298 #[error("error parsing {config_file}")]
299 Deserialize {
300 config_file: String,
301 #[diagnostic_source]
302 source: TomlDeError,
303 },
304 #[error("error parsing URL: {source}")]
305 UrlParseError {
306 source: url::ParseError,
307 #[help]
308 help: Option<String>,
309 },
310}
311
312#[derive(Debug, Clone, Default, Deserialize, Serialize)]
319pub struct ConfigBuilder {
320 #[serde(
321 default,
322 deserialize_with = "deserialize_url",
323 serialize_with = "serialize_url"
324 )]
325 server: Option<Url>,
326 #[serde(
327 default,
328 deserialize_with = "deserialize_url_vec",
329 serialize_with = "serialize_url_vec"
330 )]
331 extra_servers: Option<Vec<Url>>,
332 namespace: Option<String>,
333 lua_version: Option<LuaVersion>,
334 user_tree: Option<PathBuf>,
335 workspace_tree: Option<PathBuf>,
336 lua_dir: Option<PathBuf>,
337 cache_dir: Option<PathBuf>,
338 data_dir: Option<PathBuf>,
339 vendor_dir: Option<PathBuf>,
340 enable_development_packages: Option<bool>,
341 verbose: Option<bool>,
342 no_progress: Option<bool>,
343 no_prompt: Option<bool>,
344 timeout: Option<Duration>,
345 max_jobs: Option<usize>,
346 variables: Option<HashMap<String, String>>,
347 #[serde(default)]
348 external_deps: ExternalDependencySearchConfig,
349 #[serde(default)]
350 entrypoint_layout: RockLayoutConfig,
351 user_agent: Option<String>,
352 generate_luarc: Option<bool>,
353 luarc_file_name: Option<String>,
354 wrap_bin_scripts: Option<bool>,
355 package_types: Option<RemotePackageTypeFilterSpec>,
356 no_tfa: Option<bool>,
357}
358
359impl ConfigBuilder {
361 pub fn new() -> Result<Self, ConfigError> {
364 let config_file = Self::config_file()?;
365 if config_file.is_file() {
366 let config_file_name = config_file.to_string_lossy().to_string();
367 let content = fs::sync::read_to_string(&config_file)?;
368 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
369 ConfigError::Deserialize {
370 config_file: config_file_name,
371 source,
372 }
373 })
374 } else {
375 Ok(Self::default())
376 }
377 }
378
379 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
381 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
382 .ok_or(NoValidHomeDirectory)?;
383 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
384 }
385
386 pub fn dev(self, dev: Option<bool>) -> Self {
389 Self {
390 enable_development_packages: dev.or(self.enable_development_packages),
391 ..self
392 }
393 }
394
395 pub fn server(self, server: Option<Url>) -> Self {
398 Self {
399 server: server.or(self.server),
400 ..self
401 }
402 }
403
404 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
406 Self {
407 extra_servers: extra_servers.or(self.extra_servers),
408 ..self
409 }
410 }
411
412 pub fn namespace(self, namespace: Option<String>) -> Self {
414 Self {
415 namespace: namespace.or(self.namespace),
416 ..self
417 }
418 }
419
420 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
422 Self {
423 lua_dir: lua_dir.or(self.lua_dir),
424 ..self
425 }
426 }
427
428 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
431 Self {
432 lua_version: lua_version.or(self.lua_version),
433 ..self
434 }
435 }
436
437 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
439 Self {
440 user_tree: tree.or(self.user_tree),
441 ..self
442 }
443 }
444
445 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
448 Self {
449 workspace_tree: tree.or(self.workspace_tree),
450 ..self
451 }
452 }
453
454 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
458 Self {
459 variables: variables.or(self.variables),
460 ..self
461 }
462 }
463
464 pub fn verbose(self, verbose: Option<bool>) -> Self {
467 Self {
468 verbose: verbose.or(self.verbose),
469 ..self
470 }
471 }
472
473 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
476 Self {
477 no_progress: no_progress.or(self.no_progress),
478 ..self
479 }
480 }
481
482 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
485 Self {
486 no_prompt: no_prompt.or(self.no_prompt),
487 ..self
488 }
489 }
490
491 pub fn timeout(self, timeout: Option<Duration>) -> Self {
495 Self {
496 timeout: timeout.or(self.timeout),
497 ..self
498 }
499 }
500
501 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
505 Self {
506 max_jobs: max_jobs.or(self.max_jobs),
507 ..self
508 }
509 }
510
511 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
513 Self {
514 cache_dir: cache_dir.or(self.cache_dir),
515 ..self
516 }
517 }
518
519 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
521 Self {
522 data_dir: data_dir.or(self.data_dir),
523 ..self
524 }
525 }
526
527 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
531 Self {
532 vendor_dir: vendor_dir.or(self.vendor_dir),
533 ..self
534 }
535 }
536
537 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
540 Self {
541 entrypoint_layout: rock_layout,
542 ..self
543 }
544 }
545
546 pub fn user_agent(self, user_agent: Option<String>) -> Self {
549 Self {
550 user_agent: user_agent.or(self.user_agent),
551 ..self
552 }
553 }
554
555 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
558 Self {
559 generate_luarc: generate.or(self.generate_luarc),
560 ..self
561 }
562 }
563
564 pub fn luarc_file_name(self, file: Option<String>) -> Self {
567 Self {
568 luarc_file_name: file.or(self.luarc_file_name),
569 ..self
570 }
571 }
572
573 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
579 Self {
580 wrap_bin_scripts: generate.or(self.generate_luarc),
581 ..self
582 }
583 }
584 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
587 Self {
588 no_tfa: tfa.or(self.no_tfa),
589 ..self
590 }
591 }
592
593 #[tracing::instrument(level = "trace")]
594 pub fn build(self) -> Result<Config, ConfigError> {
595 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
596 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
597 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
598
599 let lua_version = self
600 .lua_version
601 .or(crate::lua_installation::detect_installed_lua_version());
602
603 Ok(Config {
604 enable_development_packages: self.enable_development_packages.unwrap_or(false),
605 server: self.server.unwrap_or_else(|| unsafe {
606 Url::parse("https://luarocks.org/").unwrap_unchecked()
607 }),
608 extra_servers: self.extra_servers.unwrap_or_default(),
609 namespace: self.namespace,
610 lua_dir: self.lua_dir,
611 lua_version,
612 user_tree,
613 workspace_tree_root: self.workspace_tree,
614 verbose: self.verbose.unwrap_or(false),
615 no_progress: self.no_progress.unwrap_or(false),
616 no_prompt: self.no_prompt.unwrap_or(false),
617 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
618 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
619 0 => usize::MAX,
620 max_jobs => max_jobs,
621 },
622 variables: default_variables()
623 .chain(self.variables.unwrap_or_default())
624 .collect(),
625 external_deps: self.external_deps,
626 entrypoint_layout: self.entrypoint_layout,
627 cache_dir,
628 data_dir,
629 vendor_dir: self.vendor_dir,
630 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
631 generate_luarc: self.generate_luarc.unwrap_or(true),
632 luarc_file_name: self
633 .luarc_file_name
634 .unwrap_or_else(|| ".luarc.json".to_string()),
635 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
636 package_types: self.package_types.unwrap_or_default(),
637 no_tfa: self.no_tfa.unwrap_or(false),
638 })
639 }
640}
641
642impl From<Config> for ConfigBuilder {
644 fn from(value: Config) -> Self {
645 ConfigBuilder {
646 enable_development_packages: Some(value.enable_development_packages),
647 server: Some(value.server),
648 extra_servers: Some(value.extra_servers),
649 namespace: value.namespace,
650 lua_dir: value.lua_dir,
651 lua_version: value.lua_version,
652 user_tree: Some(value.user_tree),
653 workspace_tree: value.workspace_tree_root,
654 verbose: Some(value.verbose),
655 no_progress: Some(value.no_progress),
656 no_prompt: Some(value.no_prompt),
657 timeout: Some(value.timeout),
658 max_jobs: if value.max_jobs == usize::MAX {
659 None
660 } else {
661 Some(value.max_jobs)
662 },
663 variables: Some(value.variables),
664 cache_dir: Some(value.cache_dir),
665 data_dir: Some(value.data_dir),
666 vendor_dir: value.vendor_dir,
667 external_deps: value.external_deps,
668 entrypoint_layout: value.entrypoint_layout,
669 user_agent: Some(value.user_agent),
670 generate_luarc: Some(value.generate_luarc),
671 luarc_file_name: Some(value.luarc_file_name),
672 wrap_bin_scripts: Some(value.wrap_bin_scripts),
673 package_types: Some(value.package_types),
674 no_tfa: Some(value.no_tfa),
675 }
676 }
677}
678
679fn default_variables() -> impl Iterator<Item = (String, String)> {
680 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
681 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
682 vec![
683 ("MAKE".into(), "make".into()),
684 ("CMAKE".into(), "cmake".into()),
685 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
686 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
687 ("CFLAGS".into(), cflags),
688 ("LDFLAGS".into(), ldflags),
689 ("LIBFLAG".into(), utils::default_libflag().into()),
690 ]
691 .into_iter()
692}
693
694fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
695where
696 D: serde::Deserializer<'de>,
697{
698 let s = Option::<String>::deserialize(deserializer)?;
699 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
700 .transpose()
701}
702
703fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
704where
705 S: Serializer,
706{
707 match url {
708 Some(url) => serializer.serialize_some(url.as_str()),
709 None => serializer.serialize_none(),
710 }
711}
712
713fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
714where
715 D: serde::Deserializer<'de>,
716{
717 let s = Option::<Vec<String>>::deserialize(deserializer)?;
718 s.map(|v| {
719 v.into_iter()
720 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
721 .try_collect()
722 })
723 .transpose()
724}
725
726fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
727where
728 S: Serializer,
729{
730 match urls {
731 Some(urls) => {
732 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
733 serializer.serialize_some(&url_strings)
734 }
735 None => serializer.serialize_none(),
736 }
737}