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}
68
69impl Config {
70 fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
72 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
73 }
74
75 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
77 let project_dirs = Config::project_dirs()?;
78 Ok(project_dirs.cache_dir().to_path_buf())
79 }
80
81 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
83 let project_dirs = Config::project_dirs()?;
84 Ok(project_dirs.data_local_dir().to_path_buf())
85 }
86
87 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
89 Self {
90 lua_version: Some(lua_version),
91 ..self
92 }
93 }
94
95 pub fn with_tree(self, tree: PathBuf) -> Self {
97 Self {
98 user_tree: tree,
99 ..self
100 }
101 }
102
103 pub fn server(&self) -> &Url {
105 &self.server
106 }
107
108 pub fn extra_servers(&self) -> &Vec<Url> {
110 self.extra_servers.as_ref()
111 }
112
113 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
115 let mut enabled_dev_servers = Vec::new();
116 if self.enable_development_packages {
117 let config_file = ConfigBuilder::config_file()
118 .map(|p| p.to_string_lossy().to_string())
119 .unwrap_or_default();
120 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
121 ConfigError::UrlParseError {
122 source,
123 help: Some(format!("check the `server` URL in {config_file}")),
124 }
125 })?);
126 for server in self.extra_servers() {
127 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
128 ConfigError::UrlParseError {
129 source,
130 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
131 }
132 })?);
133 }
134 }
135 Ok(enabled_dev_servers)
136 }
137
138 pub fn namespace(&self) -> Option<&String> {
140 self.namespace.as_ref()
141 }
142
143 pub fn lua_dir(&self) -> Option<&PathBuf> {
145 self.lua_dir.as_ref()
146 }
147
148 pub fn lua_version(&self) -> Option<&LuaVersion> {
150 self.lua_version.as_ref()
151 }
152
153 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
156 Tree::new(self.user_tree.clone(), version, self)
157 }
158
159 pub fn verbose(&self) -> bool {
161 self.verbose
162 }
163
164 pub fn no_progress(&self) -> bool {
166 self.no_progress
167 }
168
169 pub fn no_prompt(&self) -> bool {
171 self.no_prompt
172 }
173
174 pub fn timeout(&self) -> &Duration {
177 &self.timeout
178 }
179
180 pub fn max_jobs(&self) -> usize {
183 self.max_jobs
184 }
185
186 pub fn make_cmd(&self) -> String {
188 match self.variables.get("MAKE") {
189 Some(make) => make.clone(),
190 None => "make".into(),
191 }
192 }
193
194 pub fn cmake_cmd(&self) -> String {
196 match self.variables.get("CMAKE") {
197 Some(cmake) => cmake.clone(),
198 None => "cmake".into(),
199 }
200 }
201
202 pub fn variables(&self) -> &HashMap<String, String> {
206 &self.variables
207 }
208
209 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
210 &self.external_deps
211 }
212
213 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
216 &self.entrypoint_layout
217 }
218
219 pub fn cache_dir(&self) -> &PathBuf {
221 &self.cache_dir
222 }
223
224 pub fn data_dir(&self) -> &PathBuf {
226 &self.data_dir
227 }
228
229 pub fn vendor_dir(&self) -> Option<&PathBuf> {
233 self.vendor_dir.as_ref()
234 }
235
236 pub fn user_agent(&self) -> &str {
238 &self.user_agent
239 }
240
241 pub fn generate_luarc(&self) -> bool {
243 self.generate_luarc
244 }
245
246 pub fn luarc_file_name(&self) -> &str {
248 &self.luarc_file_name
249 }
250
251 pub fn wrap_bin_scripts(&self) -> bool {
255 self.wrap_bin_scripts
256 }
257
258 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
260 &self.package_types
261 }
262}
263
264impl HasVariables for Config {
265 #[tracing::instrument(level = "trace")]
266 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
267 Ok(self.variables.get(input).cloned())
268 }
269}
270
271#[derive(Error, Debug, Diagnostic)]
272pub enum ConfigError {
273 #[error(transparent)]
274 #[diagnostic(transparent)]
275 Fs(#[from] fs::FsError),
276 #[error(transparent)]
277 #[diagnostic(transparent)]
278 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
279 #[error("error parsing {config_file}")]
280 Deserialize {
281 config_file: String,
282 #[diagnostic_source]
283 source: TomlDeError,
284 },
285 #[error("error parsing URL: {source}")]
286 UrlParseError {
287 source: url::ParseError,
288 #[help]
289 help: Option<String>,
290 },
291}
292
293#[derive(Debug, Clone, Default, Deserialize, Serialize)]
300pub struct ConfigBuilder {
301 #[serde(
302 default,
303 deserialize_with = "deserialize_url",
304 serialize_with = "serialize_url"
305 )]
306 server: Option<Url>,
307 #[serde(
308 default,
309 deserialize_with = "deserialize_url_vec",
310 serialize_with = "serialize_url_vec"
311 )]
312 extra_servers: Option<Vec<Url>>,
313 namespace: Option<String>,
314 lua_version: Option<LuaVersion>,
315 user_tree: Option<PathBuf>,
316 lua_dir: Option<PathBuf>,
317 cache_dir: Option<PathBuf>,
318 data_dir: Option<PathBuf>,
319 vendor_dir: Option<PathBuf>,
320 enable_development_packages: Option<bool>,
321 verbose: Option<bool>,
322 no_progress: Option<bool>,
323 no_prompt: Option<bool>,
324 timeout: Option<Duration>,
325 max_jobs: Option<usize>,
326 variables: Option<HashMap<String, String>>,
327 #[serde(default)]
328 external_deps: ExternalDependencySearchConfig,
329 #[serde(default)]
330 entrypoint_layout: RockLayoutConfig,
331 user_agent: Option<String>,
332 generate_luarc: Option<bool>,
333 luarc_file_name: Option<String>,
334 wrap_bin_scripts: Option<bool>,
335 package_types: Option<RemotePackageTypeFilterSpec>,
336}
337
338impl ConfigBuilder {
340 pub fn new() -> Result<Self, ConfigError> {
343 let config_file = Self::config_file()?;
344 if config_file.is_file() {
345 let config_file_name = config_file.to_string_lossy().to_string();
346 let content = fs::sync::read_to_string(&config_file)?;
347 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
348 ConfigError::Deserialize {
349 config_file: config_file_name,
350 source,
351 }
352 })
353 } else {
354 Ok(Self::default())
355 }
356 }
357
358 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
360 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
361 .ok_or(NoValidHomeDirectory)?;
362 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
363 }
364
365 pub fn dev(self, dev: Option<bool>) -> Self {
368 Self {
369 enable_development_packages: dev.or(self.enable_development_packages),
370 ..self
371 }
372 }
373
374 pub fn server(self, server: Option<Url>) -> Self {
377 Self {
378 server: server.or(self.server),
379 ..self
380 }
381 }
382
383 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
385 Self {
386 extra_servers: extra_servers.or(self.extra_servers),
387 ..self
388 }
389 }
390
391 pub fn namespace(self, namespace: Option<String>) -> Self {
393 Self {
394 namespace: namespace.or(self.namespace),
395 ..self
396 }
397 }
398
399 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
401 Self {
402 lua_dir: lua_dir.or(self.lua_dir),
403 ..self
404 }
405 }
406
407 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
410 Self {
411 lua_version: lua_version.or(self.lua_version),
412 ..self
413 }
414 }
415
416 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
418 Self {
419 user_tree: tree.or(self.user_tree),
420 ..self
421 }
422 }
423
424 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
428 Self {
429 variables: variables.or(self.variables),
430 ..self
431 }
432 }
433
434 pub fn verbose(self, verbose: Option<bool>) -> Self {
437 Self {
438 verbose: verbose.or(self.verbose),
439 ..self
440 }
441 }
442
443 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
446 Self {
447 no_progress: no_progress.or(self.no_progress),
448 ..self
449 }
450 }
451
452 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
455 Self {
456 no_prompt: no_prompt.or(self.no_prompt),
457 ..self
458 }
459 }
460
461 pub fn timeout(self, timeout: Option<Duration>) -> Self {
465 Self {
466 timeout: timeout.or(self.timeout),
467 ..self
468 }
469 }
470
471 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
475 Self {
476 max_jobs: max_jobs.or(self.max_jobs),
477 ..self
478 }
479 }
480
481 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
483 Self {
484 cache_dir: cache_dir.or(self.cache_dir),
485 ..self
486 }
487 }
488
489 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
491 Self {
492 data_dir: data_dir.or(self.data_dir),
493 ..self
494 }
495 }
496
497 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
501 Self {
502 vendor_dir: vendor_dir.or(self.vendor_dir),
503 ..self
504 }
505 }
506
507 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
510 Self {
511 entrypoint_layout: rock_layout,
512 ..self
513 }
514 }
515
516 pub fn user_agent(self, user_agent: Option<String>) -> Self {
519 Self {
520 user_agent: user_agent.or(self.user_agent),
521 ..self
522 }
523 }
524
525 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
528 Self {
529 generate_luarc: generate.or(self.generate_luarc),
530 ..self
531 }
532 }
533
534 pub fn luarc_file_name(self, file: Option<String>) -> Self {
537 Self {
538 luarc_file_name: file.or(self.luarc_file_name),
539 ..self
540 }
541 }
542
543 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
549 Self {
550 wrap_bin_scripts: generate.or(self.generate_luarc),
551 ..self
552 }
553 }
554
555 #[tracing::instrument(level = "trace")]
556 pub fn build(self) -> Result<Config, ConfigError> {
557 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
558 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
559 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
560
561 let lua_version = self
562 .lua_version
563 .or(crate::lua_installation::detect_installed_lua_version());
564
565 Ok(Config {
566 enable_development_packages: self.enable_development_packages.unwrap_or(false),
567 server: self.server.unwrap_or_else(|| unsafe {
568 Url::parse("https://luarocks.org/").unwrap_unchecked()
569 }),
570 extra_servers: self.extra_servers.unwrap_or_default(),
571 namespace: self.namespace,
572 lua_dir: self.lua_dir,
573 lua_version,
574 user_tree,
575 verbose: self.verbose.unwrap_or(false),
576 no_progress: self.no_progress.unwrap_or(false),
577 no_prompt: self.no_prompt.unwrap_or(false),
578 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
579 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
580 0 => usize::MAX,
581 max_jobs => max_jobs,
582 },
583 variables: default_variables()
584 .chain(self.variables.unwrap_or_default())
585 .collect(),
586 external_deps: self.external_deps,
587 entrypoint_layout: self.entrypoint_layout,
588 cache_dir,
589 data_dir,
590 vendor_dir: self.vendor_dir,
591 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
592 generate_luarc: self.generate_luarc.unwrap_or(true),
593 luarc_file_name: self
594 .luarc_file_name
595 .unwrap_or_else(|| ".luarc.json".to_string()),
596 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
597 package_types: self.package_types.unwrap_or_default(),
598 })
599 }
600}
601
602impl From<Config> for ConfigBuilder {
604 fn from(value: Config) -> Self {
605 ConfigBuilder {
606 enable_development_packages: Some(value.enable_development_packages),
607 server: Some(value.server),
608 extra_servers: Some(value.extra_servers),
609 namespace: value.namespace,
610 lua_dir: value.lua_dir,
611 lua_version: value.lua_version,
612 user_tree: Some(value.user_tree),
613 verbose: Some(value.verbose),
614 no_progress: Some(value.no_progress),
615 no_prompt: Some(value.no_prompt),
616 timeout: Some(value.timeout),
617 max_jobs: if value.max_jobs == usize::MAX {
618 None
619 } else {
620 Some(value.max_jobs)
621 },
622 variables: Some(value.variables),
623 cache_dir: Some(value.cache_dir),
624 data_dir: Some(value.data_dir),
625 vendor_dir: value.vendor_dir,
626 external_deps: value.external_deps,
627 entrypoint_layout: value.entrypoint_layout,
628 user_agent: Some(value.user_agent),
629 generate_luarc: Some(value.generate_luarc),
630 luarc_file_name: Some(value.luarc_file_name),
631 wrap_bin_scripts: Some(value.wrap_bin_scripts),
632 package_types: Some(value.package_types),
633 }
634 }
635}
636
637fn default_variables() -> impl Iterator<Item = (String, String)> {
638 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
639 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
640 vec![
641 ("MAKE".into(), "make".into()),
642 ("CMAKE".into(), "cmake".into()),
643 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
644 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
645 ("CFLAGS".into(), cflags),
646 ("LDFLAGS".into(), ldflags),
647 ("LIBFLAG".into(), utils::default_libflag().into()),
648 ]
649 .into_iter()
650}
651
652fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
653where
654 D: serde::Deserializer<'de>,
655{
656 let s = Option::<String>::deserialize(deserializer)?;
657 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
658 .transpose()
659}
660
661fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
662where
663 S: Serializer,
664{
665 match url {
666 Some(url) => serializer.serialize_some(url.as_str()),
667 None => serializer.serialize_none(),
668 }
669}
670
671fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
672where
673 D: serde::Deserializer<'de>,
674{
675 let s = Option::<Vec<String>>::deserialize(deserializer)?;
676 s.map(|v| {
677 v.into_iter()
678 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
679 .try_collect()
680 })
681 .transpose()
682}
683
684fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
685where
686 S: Serializer,
687{
688 match urls {
689 Some(urls) => {
690 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
691 serializer.serialize_some(&url_strings)
692 }
693 None => serializer.serialize_none(),
694 }
695}