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 let config_file = ConfigBuilder::config_file()
115 .map(|p| p.to_string_lossy().to_string())
116 .unwrap_or_default();
117 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
118 ConfigError::UrlParseError {
119 source,
120 help: Some(format!("check the `server` URL in {config_file}")),
121 }
122 })?);
123 for server in self.extra_servers() {
124 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
125 ConfigError::UrlParseError {
126 source,
127 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
128 }
129 })?);
130 }
131 }
132 Ok(enabled_dev_servers)
133 }
134
135 pub fn namespace(&self) -> Option<&String> {
137 self.namespace.as_ref()
138 }
139
140 pub fn lua_dir(&self) -> Option<&PathBuf> {
142 self.lua_dir.as_ref()
143 }
144
145 pub fn lua_version(&self) -> Option<&LuaVersion> {
147 self.lua_version.as_ref()
148 }
149
150 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
153 Tree::new(self.user_tree.clone(), version, self)
154 }
155
156 pub fn verbose(&self) -> bool {
158 self.verbose
159 }
160
161 pub fn no_progress(&self) -> bool {
163 self.no_progress
164 }
165
166 pub fn no_prompt(&self) -> bool {
168 self.no_prompt
169 }
170
171 pub fn timeout(&self) -> &Duration {
174 &self.timeout
175 }
176
177 pub fn max_jobs(&self) -> usize {
180 self.max_jobs
181 }
182
183 pub fn make_cmd(&self) -> String {
185 match self.variables.get("MAKE") {
186 Some(make) => make.clone(),
187 None => "make".into(),
188 }
189 }
190
191 pub fn cmake_cmd(&self) -> String {
193 match self.variables.get("CMAKE") {
194 Some(cmake) => cmake.clone(),
195 None => "cmake".into(),
196 }
197 }
198
199 pub fn variables(&self) -> &HashMap<String, String> {
203 &self.variables
204 }
205
206 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
207 &self.external_deps
208 }
209
210 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
213 &self.entrypoint_layout
214 }
215
216 pub fn cache_dir(&self) -> &PathBuf {
218 &self.cache_dir
219 }
220
221 pub fn data_dir(&self) -> &PathBuf {
223 &self.data_dir
224 }
225
226 pub fn vendor_dir(&self) -> Option<&PathBuf> {
230 self.vendor_dir.as_ref()
231 }
232
233 pub fn user_agent(&self) -> &str {
235 &self.user_agent
236 }
237
238 pub fn generate_luarc(&self) -> bool {
240 self.generate_luarc
241 }
242
243 pub fn luarc_file_name(&self) -> &str {
245 &self.luarc_file_name
246 }
247
248 pub fn wrap_bin_scripts(&self) -> bool {
252 self.wrap_bin_scripts
253 }
254}
255
256impl HasVariables for Config {
257 #[tracing::instrument(level = "trace")]
258 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
259 Ok(self.variables.get(input).cloned())
260 }
261}
262
263#[derive(Error, Debug, Diagnostic)]
264pub enum ConfigError {
265 #[error("failed to read config file {config_file}")]
266 ConfigRead {
267 config_file: String,
268 source: io::Error,
269 #[help]
270 help: Option<String>,
271 },
272 #[error(transparent)]
273 #[diagnostic(transparent)]
274 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
275 #[error("error parsing {config_file}")]
276 Deserialize {
277 config_file: String,
278 #[diagnostic_source]
279 source: TomlDeError,
280 },
281 #[error("error parsing URL: {source}")]
282 UrlParseError {
283 source: url::ParseError,
284 #[help]
285 help: Option<String>,
286 },
287}
288
289#[derive(Debug, Clone, Default, Deserialize, Serialize)]
296pub struct ConfigBuilder {
297 #[serde(
298 default,
299 deserialize_with = "deserialize_url",
300 serialize_with = "serialize_url"
301 )]
302 server: Option<Url>,
303 #[serde(
304 default,
305 deserialize_with = "deserialize_url_vec",
306 serialize_with = "serialize_url_vec"
307 )]
308 extra_servers: Option<Vec<Url>>,
309 namespace: Option<String>,
310 lua_version: Option<LuaVersion>,
311 user_tree: Option<PathBuf>,
312 lua_dir: Option<PathBuf>,
313 cache_dir: Option<PathBuf>,
314 data_dir: Option<PathBuf>,
315 vendor_dir: Option<PathBuf>,
316 enable_development_packages: Option<bool>,
317 verbose: Option<bool>,
318 no_progress: Option<bool>,
319 no_prompt: Option<bool>,
320 timeout: Option<Duration>,
321 max_jobs: Option<usize>,
322 variables: Option<HashMap<String, String>>,
323 #[serde(default)]
324 external_deps: ExternalDependencySearchConfig,
325 #[serde(default)]
326 entrypoint_layout: RockLayoutConfig,
327 user_agent: Option<String>,
328 generate_luarc: Option<bool>,
329 luarc_file_name: Option<String>,
330 wrap_bin_scripts: Option<bool>,
331}
332
333impl ConfigBuilder {
335 pub fn new() -> Result<Self, ConfigError> {
338 let config_file = Self::config_file()?;
339 if config_file.is_file() {
340 let config_file_name = config_file.to_string_lossy().to_string();
341 let content = std::fs::read_to_string(&config_file).map_err(|source| {
342 ConfigError::ConfigRead {
343 config_file: config_file_name.clone(),
344 source,
345 help: Some(format!(
346 "check that {} exists and is readable",
347 config_file.display()
348 )),
349 }
350 })?;
351 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
352 ConfigError::Deserialize {
353 config_file: config_file_name,
354 source,
355 }
356 })
357 } else {
358 Ok(Self::default())
359 }
360 }
361
362 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
364 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
365 .ok_or(NoValidHomeDirectory)?;
366 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
367 }
368
369 pub fn dev(self, dev: Option<bool>) -> Self {
372 Self {
373 enable_development_packages: dev.or(self.enable_development_packages),
374 ..self
375 }
376 }
377
378 pub fn server(self, server: Option<Url>) -> Self {
381 Self {
382 server: server.or(self.server),
383 ..self
384 }
385 }
386
387 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
389 Self {
390 extra_servers: extra_servers.or(self.extra_servers),
391 ..self
392 }
393 }
394
395 pub fn namespace(self, namespace: Option<String>) -> Self {
397 Self {
398 namespace: namespace.or(self.namespace),
399 ..self
400 }
401 }
402
403 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
405 Self {
406 lua_dir: lua_dir.or(self.lua_dir),
407 ..self
408 }
409 }
410
411 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
414 Self {
415 lua_version: lua_version.or(self.lua_version),
416 ..self
417 }
418 }
419
420 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
422 Self {
423 user_tree: tree.or(self.user_tree),
424 ..self
425 }
426 }
427
428 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
432 Self {
433 variables: variables.or(self.variables),
434 ..self
435 }
436 }
437
438 pub fn verbose(self, verbose: Option<bool>) -> Self {
441 Self {
442 verbose: verbose.or(self.verbose),
443 ..self
444 }
445 }
446
447 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
450 Self {
451 no_progress: no_progress.or(self.no_progress),
452 ..self
453 }
454 }
455
456 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
459 Self {
460 no_prompt: no_prompt.or(self.no_prompt),
461 ..self
462 }
463 }
464
465 pub fn timeout(self, timeout: Option<Duration>) -> Self {
469 Self {
470 timeout: timeout.or(self.timeout),
471 ..self
472 }
473 }
474
475 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
479 Self {
480 max_jobs: max_jobs.or(self.max_jobs),
481 ..self
482 }
483 }
484
485 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
487 Self {
488 cache_dir: cache_dir.or(self.cache_dir),
489 ..self
490 }
491 }
492
493 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
495 Self {
496 data_dir: data_dir.or(self.data_dir),
497 ..self
498 }
499 }
500
501 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
505 Self {
506 vendor_dir: vendor_dir.or(self.vendor_dir),
507 ..self
508 }
509 }
510
511 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
514 Self {
515 entrypoint_layout: rock_layout,
516 ..self
517 }
518 }
519
520 pub fn user_agent(self, user_agent: Option<String>) -> Self {
523 Self {
524 user_agent: user_agent.or(self.user_agent),
525 ..self
526 }
527 }
528
529 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
532 Self {
533 generate_luarc: generate.or(self.generate_luarc),
534 ..self
535 }
536 }
537
538 pub fn luarc_file_name(self, file: Option<String>) -> Self {
541 Self {
542 luarc_file_name: file.or(self.luarc_file_name),
543 ..self
544 }
545 }
546
547 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
553 Self {
554 wrap_bin_scripts: generate.or(self.generate_luarc),
555 ..self
556 }
557 }
558
559 #[tracing::instrument(level = "trace")]
560 pub fn build(self) -> Result<Config, ConfigError> {
561 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
562 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
563 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
564
565 let lua_version = self
566 .lua_version
567 .or(crate::lua_installation::detect_installed_lua_version());
568
569 Ok(Config {
570 enable_development_packages: self.enable_development_packages.unwrap_or(false),
571 server: self.server.unwrap_or_else(|| unsafe {
572 Url::parse("https://luarocks.org/").unwrap_unchecked()
573 }),
574 extra_servers: self.extra_servers.unwrap_or_default(),
575 namespace: self.namespace,
576 lua_dir: self.lua_dir,
577 lua_version,
578 user_tree,
579 verbose: self.verbose.unwrap_or(false),
580 no_progress: self.no_progress.unwrap_or(false),
581 no_prompt: self.no_prompt.unwrap_or(false),
582 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
583 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
584 0 => usize::MAX,
585 max_jobs => max_jobs,
586 },
587 variables: default_variables()
588 .chain(self.variables.unwrap_or_default())
589 .collect(),
590 external_deps: self.external_deps,
591 entrypoint_layout: self.entrypoint_layout,
592 cache_dir,
593 data_dir,
594 vendor_dir: self.vendor_dir,
595 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
596 generate_luarc: self.generate_luarc.unwrap_or(true),
597 luarc_file_name: self
598 .luarc_file_name
599 .unwrap_or_else(|| ".luarc.json".to_string()),
600 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
601 })
602 }
603}
604
605impl From<Config> for ConfigBuilder {
607 fn from(value: Config) -> Self {
608 ConfigBuilder {
609 enable_development_packages: Some(value.enable_development_packages),
610 server: Some(value.server),
611 extra_servers: Some(value.extra_servers),
612 namespace: value.namespace,
613 lua_dir: value.lua_dir,
614 lua_version: value.lua_version,
615 user_tree: Some(value.user_tree),
616 verbose: Some(value.verbose),
617 no_progress: Some(value.no_progress),
618 no_prompt: Some(value.no_prompt),
619 timeout: Some(value.timeout),
620 max_jobs: if value.max_jobs == usize::MAX {
621 None
622 } else {
623 Some(value.max_jobs)
624 },
625 variables: Some(value.variables),
626 cache_dir: Some(value.cache_dir),
627 data_dir: Some(value.data_dir),
628 vendor_dir: value.vendor_dir,
629 external_deps: value.external_deps,
630 entrypoint_layout: value.entrypoint_layout,
631 user_agent: Some(value.user_agent),
632 generate_luarc: Some(value.generate_luarc),
633 luarc_file_name: Some(value.luarc_file_name),
634 wrap_bin_scripts: Some(value.wrap_bin_scripts),
635 }
636 }
637}
638
639fn default_variables() -> impl Iterator<Item = (String, String)> {
640 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
641 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
642 vec![
643 ("MAKE".into(), "make".into()),
644 ("CMAKE".into(), "cmake".into()),
645 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
646 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
647 ("CFLAGS".into(), cflags),
648 ("LDFLAGS".into(), ldflags),
649 ("LIBFLAG".into(), utils::default_libflag().into()),
650 ]
651 .into_iter()
652}
653
654fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
655where
656 D: serde::Deserializer<'de>,
657{
658 let s = Option::<String>::deserialize(deserializer)?;
659 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
660 .transpose()
661}
662
663fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
664where
665 S: Serializer,
666{
667 match url {
668 Some(url) => serializer.serialize_some(url.as_str()),
669 None => serializer.serialize_none(),
670 }
671}
672
673fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
674where
675 D: serde::Deserializer<'de>,
676{
677 let s = Option::<Vec<String>>::deserialize(deserializer)?;
678 s.map(|v| {
679 v.into_iter()
680 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
681 .try_collect()
682 })
683 .transpose()
684}
685
686fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
687where
688 S: Serializer,
689{
690 match urls {
691 Some(urls) => {
692 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
693 serializer.serialize_some(&url_strings)
694 }
695 None => serializer.serialize_none(),
696 }
697}