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#[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 no_progress: bool,
47 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 fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
69 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
70 }
71
72 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 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 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
86 Self {
87 lua_version: Some(lua_version),
88 ..self
89 }
90 }
91
92 pub fn with_tree(self, tree: PathBuf) -> Self {
94 Self {
95 user_tree: tree,
96 ..self
97 }
98 }
99
100 pub fn server(&self) -> &Url {
102 &self.server
103 }
104
105 pub fn extra_servers(&self) -> &Vec<Url> {
107 self.extra_servers.as_ref()
108 }
109
110 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 pub fn namespace(&self) -> Option<&String> {
124 self.namespace.as_ref()
125 }
126
127 pub fn lua_dir(&self) -> Option<&PathBuf> {
129 self.lua_dir.as_ref()
130 }
131
132 pub fn lua_version(&self) -> Option<&LuaVersion> {
134 self.lua_version.as_ref()
135 }
136
137 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
140 Tree::new(self.user_tree.clone(), version, self)
141 }
142
143 pub fn verbose(&self) -> bool {
145 self.verbose
146 }
147
148 pub fn no_progress(&self) -> bool {
150 self.no_progress
151 }
152
153 pub fn no_prompt(&self) -> bool {
155 self.no_prompt
156 }
157
158 pub fn timeout(&self) -> &Duration {
161 &self.timeout
162 }
163
164 pub fn max_jobs(&self) -> usize {
167 self.max_jobs
168 }
169
170 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 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 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 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
200 &self.entrypoint_layout
201 }
202
203 pub fn cache_dir(&self) -> &PathBuf {
205 &self.cache_dir
206 }
207
208 pub fn data_dir(&self) -> &PathBuf {
210 &self.data_dir
211 }
212
213 pub fn vendor_dir(&self) -> Option<&PathBuf> {
217 self.vendor_dir.as_ref()
218 }
219
220 pub fn user_agent(&self) -> &str {
222 &self.user_agent
223 }
224
225 pub fn generate_luarc(&self) -> bool {
227 self.generate_luarc
228 }
229
230 pub fn luarc_file_name(&self) -> &str {
232 &self.luarc_file_name
233 }
234
235 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#[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
307impl ConfigBuilder {
309 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 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 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 pub fn server(self, server: Option<Url>) -> Self {
343 Self {
344 server: server.or(self.server),
345 ..self
346 }
347 }
348
349 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 pub fn namespace(self, namespace: Option<String>) -> Self {
359 Self {
360 namespace: namespace.or(self.namespace),
361 ..self
362 }
363 }
364
365 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 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 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 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
394 Self {
395 variables: variables.or(self.variables),
396 ..self
397 }
398 }
399
400 pub fn verbose(self, verbose: Option<bool>) -> Self {
403 Self {
404 verbose: verbose.or(self.verbose),
405 ..self
406 }
407 }
408
409 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 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 pub fn timeout(self, timeout: Option<Duration>) -> Self {
431 Self {
432 timeout: timeout.or(self.timeout),
433 ..self
434 }
435 }
436
437 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 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 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 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 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
476 Self {
477 entrypoint_layout: rock_layout,
478 ..self
479 }
480 }
481
482 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 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 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 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
566impl 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}