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::project::TomlDeError;
15use crate::tree::{Tree, TreeError};
16use crate::variables::GetVariableError;
17use crate::{build::utils, variables::HasVariables};
18
19pub mod external_deps;
20pub mod tree;
21
22const DEV_PATH: &str = "dev/";
23const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
24
25#[derive(Error, Debug, Diagnostic)]
26#[error("could not find a valid home directory")]
27#[diagnostic(
28 code(lux_lib::no_home_directory),
29 help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
30)]
31pub struct NoValidHomeDirectory;
32
33#[derive(Debug, Clone)]
37pub struct Config {
38 enable_development_packages: bool,
39 server: Url,
40 extra_servers: Vec<Url>,
41 namespace: Option<String>,
42 lua_dir: Option<PathBuf>,
43 lua_version: Option<LuaVersion>,
44 user_tree: PathBuf,
45 verbose: bool,
46 no_progress: bool,
48 no_prompt: bool,
50 timeout: Duration,
51 max_jobs: usize,
52 variables: HashMap<String, String>,
53 external_deps: ExternalDependencySearchConfig,
54 entrypoint_layout: RockLayoutConfig,
55
56 cache_dir: PathBuf,
57 data_dir: PathBuf,
58 vendor_dir: Option<PathBuf>,
59
60 user_agent: String,
61
62 generate_luarc: bool,
63 luarc_file_name: String,
64 wrap_bin_scripts: bool,
65}
66
67impl Config {
68 fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
70 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
71 }
72
73 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
75 let project_dirs = Config::project_dirs()?;
76 Ok(project_dirs.cache_dir().to_path_buf())
77 }
78
79 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
81 let project_dirs = Config::project_dirs()?;
82 Ok(project_dirs.data_local_dir().to_path_buf())
83 }
84
85 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
87 Self {
88 lua_version: Some(lua_version),
89 ..self
90 }
91 }
92
93 pub fn with_tree(self, tree: PathBuf) -> Self {
95 Self {
96 user_tree: tree,
97 ..self
98 }
99 }
100
101 pub fn server(&self) -> &Url {
103 &self.server
104 }
105
106 pub fn extra_servers(&self) -> &Vec<Url> {
108 self.extra_servers.as_ref()
109 }
110
111 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
113 let mut enabled_dev_servers = Vec::new();
114 if self.enable_development_packages {
115 let config_file = ConfigBuilder::config_file()
116 .map(|p| p.to_string_lossy().to_string())
117 .unwrap_or_default();
118 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
119 ConfigError::UrlParseError {
120 source,
121 help: Some(format!("check the `server` URL in {config_file}")),
122 }
123 })?);
124 for server in self.extra_servers() {
125 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
126 ConfigError::UrlParseError {
127 source,
128 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
129 }
130 })?);
131 }
132 }
133 Ok(enabled_dev_servers)
134 }
135
136 pub fn namespace(&self) -> Option<&String> {
138 self.namespace.as_ref()
139 }
140
141 pub fn lua_dir(&self) -> Option<&PathBuf> {
143 self.lua_dir.as_ref()
144 }
145
146 pub fn lua_version(&self) -> Option<&LuaVersion> {
148 self.lua_version.as_ref()
149 }
150
151 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
154 Tree::new(self.user_tree.clone(), version, self)
155 }
156
157 pub fn verbose(&self) -> bool {
159 self.verbose
160 }
161
162 pub fn no_progress(&self) -> bool {
164 self.no_progress
165 }
166
167 pub fn no_prompt(&self) -> bool {
169 self.no_prompt
170 }
171
172 pub fn timeout(&self) -> &Duration {
175 &self.timeout
176 }
177
178 pub fn max_jobs(&self) -> usize {
181 self.max_jobs
182 }
183
184 pub fn make_cmd(&self) -> String {
186 match self.variables.get("MAKE") {
187 Some(make) => make.clone(),
188 None => "make".into(),
189 }
190 }
191
192 pub fn cmake_cmd(&self) -> String {
194 match self.variables.get("CMAKE") {
195 Some(cmake) => cmake.clone(),
196 None => "cmake".into(),
197 }
198 }
199
200 pub fn variables(&self) -> &HashMap<String, String> {
204 &self.variables
205 }
206
207 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
208 &self.external_deps
209 }
210
211 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
214 &self.entrypoint_layout
215 }
216
217 pub fn cache_dir(&self) -> &PathBuf {
219 &self.cache_dir
220 }
221
222 pub fn data_dir(&self) -> &PathBuf {
224 &self.data_dir
225 }
226
227 pub fn vendor_dir(&self) -> Option<&PathBuf> {
231 self.vendor_dir.as_ref()
232 }
233
234 pub fn user_agent(&self) -> &str {
236 &self.user_agent
237 }
238
239 pub fn generate_luarc(&self) -> bool {
241 self.generate_luarc
242 }
243
244 pub fn luarc_file_name(&self) -> &str {
246 &self.luarc_file_name
247 }
248
249 pub fn wrap_bin_scripts(&self) -> bool {
253 self.wrap_bin_scripts
254 }
255}
256
257impl HasVariables for Config {
258 #[tracing::instrument(level = "trace")]
259 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
260 Ok(self.variables.get(input).cloned())
261 }
262}
263
264#[derive(Error, Debug, Diagnostic)]
265pub enum ConfigError {
266 #[error(transparent)]
267 #[diagnostic(transparent)]
268 Fs(#[from] fs::FsError),
269 #[error(transparent)]
270 #[diagnostic(transparent)]
271 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
272 #[error("error parsing {config_file}")]
273 Deserialize {
274 config_file: String,
275 #[diagnostic_source]
276 source: TomlDeError,
277 },
278 #[error("error parsing URL: {source}")]
279 UrlParseError {
280 source: url::ParseError,
281 #[help]
282 help: Option<String>,
283 },
284}
285
286#[derive(Debug, Clone, Default, Deserialize, Serialize)]
293pub struct ConfigBuilder {
294 #[serde(
295 default,
296 deserialize_with = "deserialize_url",
297 serialize_with = "serialize_url"
298 )]
299 server: Option<Url>,
300 #[serde(
301 default,
302 deserialize_with = "deserialize_url_vec",
303 serialize_with = "serialize_url_vec"
304 )]
305 extra_servers: Option<Vec<Url>>,
306 namespace: Option<String>,
307 lua_version: Option<LuaVersion>,
308 user_tree: Option<PathBuf>,
309 lua_dir: Option<PathBuf>,
310 cache_dir: Option<PathBuf>,
311 data_dir: Option<PathBuf>,
312 vendor_dir: Option<PathBuf>,
313 enable_development_packages: Option<bool>,
314 verbose: Option<bool>,
315 no_progress: Option<bool>,
316 no_prompt: Option<bool>,
317 timeout: Option<Duration>,
318 max_jobs: Option<usize>,
319 variables: Option<HashMap<String, String>>,
320 #[serde(default)]
321 external_deps: ExternalDependencySearchConfig,
322 #[serde(default)]
323 entrypoint_layout: RockLayoutConfig,
324 user_agent: Option<String>,
325 generate_luarc: Option<bool>,
326 luarc_file_name: Option<String>,
327 wrap_bin_scripts: Option<bool>,
328}
329
330impl ConfigBuilder {
332 pub fn new() -> Result<Self, ConfigError> {
335 let config_file = Self::config_file()?;
336 if config_file.is_file() {
337 let config_file_name = config_file.to_string_lossy().to_string();
338 let content = fs::sync::read_to_string(&config_file)?;
339 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
340 ConfigError::Deserialize {
341 config_file: config_file_name,
342 source,
343 }
344 })
345 } else {
346 Ok(Self::default())
347 }
348 }
349
350 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
352 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
353 .ok_or(NoValidHomeDirectory)?;
354 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
355 }
356
357 pub fn dev(self, dev: Option<bool>) -> Self {
360 Self {
361 enable_development_packages: dev.or(self.enable_development_packages),
362 ..self
363 }
364 }
365
366 pub fn server(self, server: Option<Url>) -> Self {
369 Self {
370 server: server.or(self.server),
371 ..self
372 }
373 }
374
375 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
377 Self {
378 extra_servers: extra_servers.or(self.extra_servers),
379 ..self
380 }
381 }
382
383 pub fn namespace(self, namespace: Option<String>) -> Self {
385 Self {
386 namespace: namespace.or(self.namespace),
387 ..self
388 }
389 }
390
391 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
393 Self {
394 lua_dir: lua_dir.or(self.lua_dir),
395 ..self
396 }
397 }
398
399 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
402 Self {
403 lua_version: lua_version.or(self.lua_version),
404 ..self
405 }
406 }
407
408 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
410 Self {
411 user_tree: tree.or(self.user_tree),
412 ..self
413 }
414 }
415
416 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
420 Self {
421 variables: variables.or(self.variables),
422 ..self
423 }
424 }
425
426 pub fn verbose(self, verbose: Option<bool>) -> Self {
429 Self {
430 verbose: verbose.or(self.verbose),
431 ..self
432 }
433 }
434
435 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
438 Self {
439 no_progress: no_progress.or(self.no_progress),
440 ..self
441 }
442 }
443
444 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
447 Self {
448 no_prompt: no_prompt.or(self.no_prompt),
449 ..self
450 }
451 }
452
453 pub fn timeout(self, timeout: Option<Duration>) -> Self {
457 Self {
458 timeout: timeout.or(self.timeout),
459 ..self
460 }
461 }
462
463 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
467 Self {
468 max_jobs: max_jobs.or(self.max_jobs),
469 ..self
470 }
471 }
472
473 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
475 Self {
476 cache_dir: cache_dir.or(self.cache_dir),
477 ..self
478 }
479 }
480
481 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
483 Self {
484 data_dir: data_dir.or(self.data_dir),
485 ..self
486 }
487 }
488
489 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
493 Self {
494 vendor_dir: vendor_dir.or(self.vendor_dir),
495 ..self
496 }
497 }
498
499 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
502 Self {
503 entrypoint_layout: rock_layout,
504 ..self
505 }
506 }
507
508 pub fn user_agent(self, user_agent: Option<String>) -> Self {
511 Self {
512 user_agent: user_agent.or(self.user_agent),
513 ..self
514 }
515 }
516
517 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
520 Self {
521 generate_luarc: generate.or(self.generate_luarc),
522 ..self
523 }
524 }
525
526 pub fn luarc_file_name(self, file: Option<String>) -> Self {
529 Self {
530 luarc_file_name: file.or(self.luarc_file_name),
531 ..self
532 }
533 }
534
535 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
541 Self {
542 wrap_bin_scripts: generate.or(self.generate_luarc),
543 ..self
544 }
545 }
546
547 #[tracing::instrument(level = "trace")]
548 pub fn build(self) -> Result<Config, ConfigError> {
549 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
550 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
551 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
552
553 let lua_version = self
554 .lua_version
555 .or(crate::lua_installation::detect_installed_lua_version());
556
557 Ok(Config {
558 enable_development_packages: self.enable_development_packages.unwrap_or(false),
559 server: self.server.unwrap_or_else(|| unsafe {
560 Url::parse("https://luarocks.org/").unwrap_unchecked()
561 }),
562 extra_servers: self.extra_servers.unwrap_or_default(),
563 namespace: self.namespace,
564 lua_dir: self.lua_dir,
565 lua_version,
566 user_tree,
567 verbose: self.verbose.unwrap_or(false),
568 no_progress: self.no_progress.unwrap_or(false),
569 no_prompt: self.no_prompt.unwrap_or(false),
570 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
571 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
572 0 => usize::MAX,
573 max_jobs => max_jobs,
574 },
575 variables: default_variables()
576 .chain(self.variables.unwrap_or_default())
577 .collect(),
578 external_deps: self.external_deps,
579 entrypoint_layout: self.entrypoint_layout,
580 cache_dir,
581 data_dir,
582 vendor_dir: self.vendor_dir,
583 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
584 generate_luarc: self.generate_luarc.unwrap_or(true),
585 luarc_file_name: self
586 .luarc_file_name
587 .unwrap_or_else(|| ".luarc.json".to_string()),
588 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
589 })
590 }
591}
592
593impl From<Config> for ConfigBuilder {
595 fn from(value: Config) -> Self {
596 ConfigBuilder {
597 enable_development_packages: Some(value.enable_development_packages),
598 server: Some(value.server),
599 extra_servers: Some(value.extra_servers),
600 namespace: value.namespace,
601 lua_dir: value.lua_dir,
602 lua_version: value.lua_version,
603 user_tree: Some(value.user_tree),
604 verbose: Some(value.verbose),
605 no_progress: Some(value.no_progress),
606 no_prompt: Some(value.no_prompt),
607 timeout: Some(value.timeout),
608 max_jobs: if value.max_jobs == usize::MAX {
609 None
610 } else {
611 Some(value.max_jobs)
612 },
613 variables: Some(value.variables),
614 cache_dir: Some(value.cache_dir),
615 data_dir: Some(value.data_dir),
616 vendor_dir: value.vendor_dir,
617 external_deps: value.external_deps,
618 entrypoint_layout: value.entrypoint_layout,
619 user_agent: Some(value.user_agent),
620 generate_luarc: Some(value.generate_luarc),
621 luarc_file_name: Some(value.luarc_file_name),
622 wrap_bin_scripts: Some(value.wrap_bin_scripts),
623 }
624 }
625}
626
627fn default_variables() -> impl Iterator<Item = (String, String)> {
628 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
629 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
630 vec![
631 ("MAKE".into(), "make".into()),
632 ("CMAKE".into(), "cmake".into()),
633 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
634 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
635 ("CFLAGS".into(), cflags),
636 ("LDFLAGS".into(), ldflags),
637 ("LIBFLAG".into(), utils::default_libflag().into()),
638 ]
639 .into_iter()
640}
641
642fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
643where
644 D: serde::Deserializer<'de>,
645{
646 let s = Option::<String>::deserialize(deserializer)?;
647 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
648 .transpose()
649}
650
651fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
652where
653 S: Serializer,
654{
655 match url {
656 Some(url) => serializer.serialize_some(url.as_str()),
657 None => serializer.serialize_none(),
658 }
659}
660
661fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
662where
663 D: serde::Deserializer<'de>,
664{
665 let s = Option::<Vec<String>>::deserialize(deserializer)?;
666 s.map(|v| {
667 v.into_iter()
668 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
669 .try_collect()
670 })
671 .transpose()
672}
673
674fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
675where
676 S: Serializer,
677{
678 match urls {
679 Some(urls) => {
680 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
681 serializer.serialize_some(&url_strings)
682 }
683 None => serializer.serialize_none(),
684 }
685}