1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use miette::Diagnostic;
6use serde::{Deserialize, Serialize, Serializer};
7use std::path::Path;
8use std::{collections::HashMap, env, path::PathBuf, time::Duration};
9use thiserror::Error;
10use tree::RockLayoutConfig;
11use url::Url;
12
13use crate::fs;
14use crate::lua_version::LuaVersion;
15use crate::package::RemotePackageTypeFilterSpec;
16use crate::project::TomlDeError;
17use crate::tree::{Tree, TreeError};
18use crate::variables::GetVariableError;
19use crate::{build::utils, variables::HasVariables};
20
21pub mod external_deps;
22pub mod tree;
23
24const DEV_PATH: &str = "dev/";
25const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
26
27#[derive(Error, Debug, Diagnostic)]
28#[error("could not find a valid home directory")]
29#[diagnostic(
30 code(lux_lib::no_home_directory),
31 help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
32)]
33pub struct NoValidHomeDirectory;
34
35#[derive(Debug, Clone)]
39pub struct Config {
40 enable_development_packages: bool,
41 server: Url,
42 extra_servers: Vec<Url>,
43 namespace: Option<String>,
44 lua_dir: Option<PathBuf>,
45 lua_version: Option<LuaVersion>,
46 user_tree: PathBuf,
47 workspace_tree: Option<PathBuf>,
48 verbose: bool,
49 no_progress: bool,
51 no_prompt: bool,
53 timeout: Duration,
54 max_jobs: usize,
55 variables: HashMap<String, String>,
56 external_deps: ExternalDependencySearchConfig,
57 entrypoint_layout: RockLayoutConfig,
58
59 cache_dir: PathBuf,
60 data_dir: PathBuf,
61 vendor_dir: Option<PathBuf>,
62
63 user_agent: String,
64
65 generate_luarc: bool,
66 luarc_file_name: String,
67 wrap_bin_scripts: bool,
68 package_types: RemotePackageTypeFilterSpec,
69 no_tfa: bool,
70}
71
72impl Config {
73 pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
75 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
76 }
77
78 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
80 let project_dirs = Config::project_dirs()?;
81 Ok(project_dirs.cache_dir().to_path_buf())
82 }
83
84 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
86 let project_dirs = Config::project_dirs()?;
87 Ok(project_dirs.data_local_dir().to_path_buf())
88 }
89
90 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
92 Self {
93 lua_version: Some(lua_version),
94 ..self
95 }
96 }
97
98 pub fn with_tree(self, tree: PathBuf) -> Self {
100 Self {
101 user_tree: tree,
102 ..self
103 }
104 }
105
106 pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
108 Self {
109 workspace_tree: tree,
110 ..self
111 }
112 }
113
114 pub fn server(&self) -> &Url {
116 &self.server
117 }
118
119 pub fn extra_servers(&self) -> &Vec<Url> {
121 self.extra_servers.as_ref()
122 }
123
124 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
126 let mut enabled_dev_servers = Vec::new();
127 if self.enable_development_packages {
128 let config_file = ConfigBuilder::config_file()
129 .map(|p| p.to_string_lossy().to_string())
130 .unwrap_or_default();
131 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
132 ConfigError::UrlParseError {
133 source,
134 help: Some(format!("check the `server` URL in {config_file}")),
135 }
136 })?);
137 for server in self.extra_servers() {
138 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
139 ConfigError::UrlParseError {
140 source,
141 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
142 }
143 })?);
144 }
145 }
146 Ok(enabled_dev_servers)
147 }
148
149 pub fn namespace(&self) -> Option<&String> {
151 self.namespace.as_ref()
152 }
153
154 pub fn lua_dir(&self) -> Option<&PathBuf> {
156 self.lua_dir.as_ref()
157 }
158
159 pub fn lua_version(&self) -> Option<&LuaVersion> {
161 self.lua_version.as_ref()
162 }
163
164 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
167 Tree::new(self.user_tree.clone(), version, self)
168 }
169
170 pub fn workspace_tree(&self) -> Option<&PathBuf> {
172 self.workspace_tree.as_ref()
173 }
174
175 pub fn verbose(&self) -> bool {
177 self.verbose
178 }
179
180 pub fn no_progress(&self) -> bool {
182 self.no_progress
183 }
184
185 pub fn no_prompt(&self) -> bool {
187 self.no_prompt
188 }
189
190 pub fn timeout(&self) -> &Duration {
193 &self.timeout
194 }
195
196 pub fn max_jobs(&self) -> usize {
199 self.max_jobs
200 }
201
202 pub fn make_cmd(&self) -> String {
204 match self.variables.get("MAKE") {
205 Some(make) => make.clone(),
206 None => "make".into(),
207 }
208 }
209
210 pub fn cmake_cmd(&self) -> String {
212 match self.variables.get("CMAKE") {
213 Some(cmake) => cmake.clone(),
214 None => "cmake".into(),
215 }
216 }
217
218 pub fn variables(&self) -> &HashMap<String, String> {
222 &self.variables
223 }
224
225 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
226 &self.external_deps
227 }
228
229 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
232 &self.entrypoint_layout
233 }
234
235 pub fn cache_dir(&self) -> &PathBuf {
237 &self.cache_dir
238 }
239
240 pub fn data_dir(&self) -> &PathBuf {
242 &self.data_dir
243 }
244
245 pub fn vendor_dir(&self) -> Option<&PathBuf> {
249 self.vendor_dir.as_ref()
250 }
251
252 pub fn user_agent(&self) -> &str {
254 &self.user_agent
255 }
256
257 pub fn generate_luarc(&self) -> bool {
259 self.generate_luarc
260 }
261
262 pub fn luarc_file_name(&self) -> &str {
264 &self.luarc_file_name
265 }
266
267 pub fn wrap_bin_scripts(&self) -> bool {
271 self.wrap_bin_scripts
272 }
273
274 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
276 &self.package_types
277 }
278 pub fn no_tfa(&self) -> bool {
280 self.no_tfa
281 }
282}
283
284impl HasVariables for Config {
285 #[tracing::instrument(level = "trace")]
286 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
287 Ok(self.variables.get(input).cloned())
288 }
289}
290
291#[derive(Error, Debug, Diagnostic)]
292pub enum ConfigError {
293 #[error(transparent)]
294 #[diagnostic(transparent)]
295 Fs(#[from] fs::FsError),
296 #[error(transparent)]
297 #[diagnostic(transparent)]
298 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
299 #[error("error parsing {config_file}")]
300 Deserialize {
301 config_file: String,
302 #[diagnostic_source]
303 source: TomlDeError,
304 },
305 #[error("error parsing URL: {source}")]
306 UrlParseError {
307 source: url::ParseError,
308 #[help]
309 help: Option<String>,
310 },
311}
312
313#[derive(Debug, Clone, Default, Deserialize, Serialize)]
320pub struct ConfigBuilder {
321 #[serde(
322 default,
323 deserialize_with = "deserialize_url",
324 serialize_with = "serialize_url"
325 )]
326 server: Option<Url>,
327 #[serde(
328 default,
329 deserialize_with = "deserialize_url_vec",
330 serialize_with = "serialize_url_vec"
331 )]
332 extra_servers: Option<Vec<Url>>,
333 namespace: Option<String>,
334 lua_version: Option<LuaVersion>,
335 user_tree: Option<PathBuf>,
336 workspace_tree: Option<PathBuf>,
337 lua_dir: Option<PathBuf>,
338 cache_dir: Option<PathBuf>,
339 data_dir: Option<PathBuf>,
340 vendor_dir: Option<PathBuf>,
341 enable_development_packages: Option<bool>,
342 verbose: Option<bool>,
343 no_progress: Option<bool>,
344 no_prompt: Option<bool>,
345 timeout: Option<Duration>,
346 max_jobs: Option<usize>,
347 variables: Option<HashMap<String, String>>,
348 #[serde(default)]
349 external_deps: ExternalDependencySearchConfig,
350 #[serde(default)]
351 entrypoint_layout: RockLayoutConfig,
352 user_agent: Option<String>,
353 generate_luarc: Option<bool>,
354 luarc_file_name: Option<String>,
355 wrap_bin_scripts: Option<bool>,
356 package_types: Option<RemotePackageTypeFilterSpec>,
357 no_tfa: Option<bool>,
358}
359
360impl ConfigBuilder {
362 pub fn new() -> Result<Self, ConfigError> {
365 let config_file = Self::config_file()?;
366 if config_file.is_file() {
367 Self::from_file(&config_file)
368 } else {
369 Ok(Self::default())
370 }
371 }
372
373 pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
374 let config_file_name = config_file.to_string_lossy().to_string();
375 let content = fs::sync::read_to_string(config_file)?;
376 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
377 ConfigError::Deserialize {
378 config_file: config_file_name,
379 source,
380 }
381 })
382 }
383
384 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
386 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
387 .ok_or(NoValidHomeDirectory)?;
388 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
389 }
390
391 pub fn dev(self, dev: Option<bool>) -> Self {
394 Self {
395 enable_development_packages: dev.or(self.enable_development_packages),
396 ..self
397 }
398 }
399
400 pub fn server(self, server: Option<Url>) -> Self {
403 Self {
404 server: server.or(self.server),
405 ..self
406 }
407 }
408
409 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
411 Self {
412 extra_servers: extra_servers.or(self.extra_servers),
413 ..self
414 }
415 }
416
417 pub fn namespace(self, namespace: Option<String>) -> Self {
419 Self {
420 namespace: namespace.or(self.namespace),
421 ..self
422 }
423 }
424
425 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
427 Self {
428 lua_dir: lua_dir.or(self.lua_dir),
429 ..self
430 }
431 }
432
433 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
436 Self {
437 lua_version: lua_version.or(self.lua_version),
438 ..self
439 }
440 }
441
442 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
444 Self {
445 user_tree: tree.or(self.user_tree),
446 ..self
447 }
448 }
449
450 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
453 Self {
454 workspace_tree: tree.or(self.workspace_tree),
455 ..self
456 }
457 }
458
459 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
463 Self {
464 variables: variables.or(self.variables),
465 ..self
466 }
467 }
468
469 pub fn verbose(self, verbose: Option<bool>) -> Self {
472 Self {
473 verbose: verbose.or(self.verbose),
474 ..self
475 }
476 }
477
478 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
481 Self {
482 no_progress: no_progress.or(self.no_progress),
483 ..self
484 }
485 }
486
487 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
490 Self {
491 no_prompt: no_prompt.or(self.no_prompt),
492 ..self
493 }
494 }
495
496 pub fn timeout(self, timeout: Option<Duration>) -> Self {
500 Self {
501 timeout: timeout.or(self.timeout),
502 ..self
503 }
504 }
505
506 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
510 Self {
511 max_jobs: max_jobs.or(self.max_jobs),
512 ..self
513 }
514 }
515
516 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
518 Self {
519 cache_dir: cache_dir.or(self.cache_dir),
520 ..self
521 }
522 }
523
524 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
526 Self {
527 data_dir: data_dir.or(self.data_dir),
528 ..self
529 }
530 }
531
532 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
536 Self {
537 vendor_dir: vendor_dir.or(self.vendor_dir),
538 ..self
539 }
540 }
541
542 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
545 Self {
546 entrypoint_layout: rock_layout,
547 ..self
548 }
549 }
550
551 pub fn user_agent(self, user_agent: Option<String>) -> Self {
554 Self {
555 user_agent: user_agent.or(self.user_agent),
556 ..self
557 }
558 }
559
560 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
563 Self {
564 generate_luarc: generate.or(self.generate_luarc),
565 ..self
566 }
567 }
568
569 pub fn luarc_file_name(self, file: Option<String>) -> Self {
572 Self {
573 luarc_file_name: file.or(self.luarc_file_name),
574 ..self
575 }
576 }
577
578 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
584 Self {
585 wrap_bin_scripts: generate.or(self.generate_luarc),
586 ..self
587 }
588 }
589 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
592 Self {
593 no_tfa: tfa.or(self.no_tfa),
594 ..self
595 }
596 }
597
598 pub fn merge(self, other: Self) -> Self {
600 Self {
601 server: other.server.or(self.server),
602 extra_servers: other.extra_servers.or(self.extra_servers),
603 namespace: other.namespace.or(self.namespace),
604 lua_version: other.lua_version.or(self.lua_version),
605 user_tree: other.user_tree.or(self.user_tree),
606 workspace_tree: other.workspace_tree.or(self.workspace_tree),
607 lua_dir: other.lua_dir.or(self.lua_dir),
608 cache_dir: other.cache_dir.or(self.cache_dir),
609 data_dir: other.data_dir.or(self.data_dir),
610 vendor_dir: other.vendor_dir.or(self.vendor_dir),
611 enable_development_packages: other
612 .enable_development_packages
613 .or(self.enable_development_packages),
614 verbose: other.verbose.or(self.verbose),
615 no_progress: other.no_progress.or(self.no_progress),
616 no_prompt: other.no_prompt.or(self.no_prompt),
617 timeout: other.timeout.or(self.timeout),
618 max_jobs: other.max_jobs.or(self.max_jobs),
619 variables: other.variables.or(self.variables),
620 external_deps: other.external_deps,
621 entrypoint_layout: other.entrypoint_layout,
622 user_agent: other.user_agent.or(self.user_agent),
623 generate_luarc: other.generate_luarc.or(self.generate_luarc),
624 luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
625 wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
626 package_types: other.package_types.or(self.package_types),
627 no_tfa: other.no_tfa.or(self.no_tfa),
628 }
629 }
630
631 #[tracing::instrument(level = "trace")]
632 pub fn build(self) -> Result<Config, ConfigError> {
633 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
634 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
635 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
636
637 let lua_version = self
638 .lua_version
639 .or(crate::lua_installation::detect_installed_lua_version());
640
641 Ok(Config {
642 enable_development_packages: self.enable_development_packages.unwrap_or(false),
643 server: self.server.unwrap_or_else(|| unsafe {
644 Url::parse("https://luarocks.org/").unwrap_unchecked()
645 }),
646 extra_servers: self.extra_servers.unwrap_or_default(),
647 namespace: self.namespace,
648 lua_dir: self.lua_dir,
649 lua_version,
650 user_tree,
651 workspace_tree: self.workspace_tree,
652 verbose: self.verbose.unwrap_or(false),
653 no_progress: self.no_progress.unwrap_or(false),
654 no_prompt: self.no_prompt.unwrap_or(false),
655 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
656 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
657 0 => usize::MAX,
658 max_jobs => max_jobs,
659 },
660 variables: default_variables()
661 .chain(self.variables.unwrap_or_default())
662 .collect(),
663 external_deps: self.external_deps,
664 entrypoint_layout: self.entrypoint_layout,
665 cache_dir,
666 data_dir,
667 vendor_dir: self.vendor_dir,
668 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
669 generate_luarc: self.generate_luarc.unwrap_or(true),
670 luarc_file_name: self
671 .luarc_file_name
672 .unwrap_or_else(|| ".luarc.json".to_string()),
673 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
674 package_types: self.package_types.unwrap_or_default(),
675 no_tfa: self.no_tfa.unwrap_or(false),
676 })
677 }
678}
679
680impl From<Config> for ConfigBuilder {
682 fn from(value: Config) -> Self {
683 ConfigBuilder {
684 enable_development_packages: Some(value.enable_development_packages),
685 server: Some(value.server),
686 extra_servers: Some(value.extra_servers),
687 namespace: value.namespace,
688 lua_dir: value.lua_dir,
689 lua_version: value.lua_version,
690 user_tree: Some(value.user_tree),
691 workspace_tree: value.workspace_tree,
692 verbose: Some(value.verbose),
693 no_progress: Some(value.no_progress),
694 no_prompt: Some(value.no_prompt),
695 timeout: Some(value.timeout),
696 max_jobs: if value.max_jobs == usize::MAX {
697 None
698 } else {
699 Some(value.max_jobs)
700 },
701 variables: Some(value.variables),
702 cache_dir: Some(value.cache_dir),
703 data_dir: Some(value.data_dir),
704 vendor_dir: value.vendor_dir,
705 external_deps: value.external_deps,
706 entrypoint_layout: value.entrypoint_layout,
707 user_agent: Some(value.user_agent),
708 generate_luarc: Some(value.generate_luarc),
709 luarc_file_name: Some(value.luarc_file_name),
710 wrap_bin_scripts: Some(value.wrap_bin_scripts),
711 package_types: Some(value.package_types),
712 no_tfa: Some(value.no_tfa),
713 }
714 }
715}
716
717fn default_variables() -> impl Iterator<Item = (String, String)> {
718 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
719 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
720 vec![
721 ("MAKE".into(), "make".into()),
722 ("CMAKE".into(), "cmake".into()),
723 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
724 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
725 ("CFLAGS".into(), cflags),
726 ("LDFLAGS".into(), ldflags),
727 ("LIBFLAG".into(), utils::default_libflag().into()),
728 ]
729 .into_iter()
730}
731
732fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
733where
734 D: serde::Deserializer<'de>,
735{
736 let s = Option::<String>::deserialize(deserializer)?;
737 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
738 .transpose()
739}
740
741fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
742where
743 S: Serializer,
744{
745 match url {
746 Some(url) => serializer.serialize_some(url.as_str()),
747 None => serializer.serialize_none(),
748 }
749}
750
751fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
752where
753 D: serde::Deserializer<'de>,
754{
755 let s = Option::<Vec<String>>::deserialize(deserializer)?;
756 s.map(|v| {
757 v.into_iter()
758 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
759 .try_collect()
760 })
761 .transpose()
762}
763
764fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
765where
766 S: Serializer,
767{
768 match urls {
769 Some(urls) => {
770 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
771 serializer.serialize_some(&url_strings)
772 }
773 None => serializer.serialize_none(),
774 }
775}