1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use serde::{Deserialize, Serialize, Serializer};
6use std::{collections::HashMap, env, io, path::PathBuf, time::Duration};
7use thiserror::Error;
8use tree::RockLayoutConfig;
9use url::Url;
10
11use crate::lua_version::LuaVersion;
12use crate::tree::{Tree, TreeError};
13use crate::variables::GetVariableError;
14use crate::{build::utils, variables::HasVariables};
15
16pub mod external_deps;
17pub mod tree;
18
19const DEV_PATH: &str = "dev/";
20const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
21
22#[derive(Error, Debug)]
23#[error("could not find a valid home directory")]
24pub struct NoValidHomeDirectory;
25
26#[derive(Debug, Clone)]
30pub struct Config {
31 enable_development_packages: bool,
32 server: Url,
33 extra_servers: Vec<Url>,
34 namespace: Option<String>,
35 lua_dir: Option<PathBuf>,
36 lua_version: Option<LuaVersion>,
37 user_tree: PathBuf,
38 verbose: bool,
39 no_progress: bool,
41 no_prompt: bool,
43 timeout: Duration,
44 max_jobs: usize,
45 variables: HashMap<String, String>,
46 external_deps: ExternalDependencySearchConfig,
47 entrypoint_layout: RockLayoutConfig,
48
49 cache_dir: PathBuf,
50 data_dir: PathBuf,
51 vendor_dir: Option<PathBuf>,
52
53 user_agent: String,
54
55 generate_luarc: bool,
56 wrap_bin_scripts: bool,
57}
58
59impl Config {
60 fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
62 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
63 }
64
65 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
67 let project_dirs = Config::project_dirs()?;
68 Ok(project_dirs.cache_dir().to_path_buf())
69 }
70
71 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
73 let project_dirs = Config::project_dirs()?;
74 Ok(project_dirs.data_local_dir().to_path_buf())
75 }
76
77 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
79 Self {
80 lua_version: Some(lua_version),
81 ..self
82 }
83 }
84
85 pub fn with_tree(self, tree: PathBuf) -> Self {
87 Self {
88 user_tree: tree,
89 ..self
90 }
91 }
92
93 pub fn server(&self) -> &Url {
95 &self.server
96 }
97
98 pub fn extra_servers(&self) -> &Vec<Url> {
100 self.extra_servers.as_ref()
101 }
102
103 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
105 let mut enabled_dev_servers = Vec::new();
106 if self.enable_development_packages {
107 enabled_dev_servers.push(self.server().join(DEV_PATH)?);
108 for server in self.extra_servers() {
109 enabled_dev_servers.push(server.join(DEV_PATH)?);
110 }
111 }
112 Ok(enabled_dev_servers)
113 }
114
115 pub fn namespace(&self) -> Option<&String> {
117 self.namespace.as_ref()
118 }
119
120 pub fn lua_dir(&self) -> Option<&PathBuf> {
122 self.lua_dir.as_ref()
123 }
124
125 pub fn lua_version(&self) -> Option<&LuaVersion> {
127 self.lua_version.as_ref()
128 }
129
130 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
133 Tree::new(self.user_tree.clone(), version, self)
134 }
135
136 pub fn verbose(&self) -> bool {
138 self.verbose
139 }
140
141 pub fn no_progress(&self) -> bool {
143 self.no_progress
144 }
145
146 pub fn no_prompt(&self) -> bool {
148 self.no_prompt
149 }
150
151 pub fn timeout(&self) -> &Duration {
154 &self.timeout
155 }
156
157 pub fn max_jobs(&self) -> usize {
160 self.max_jobs
161 }
162
163 pub fn make_cmd(&self) -> String {
165 match self.variables.get("MAKE") {
166 Some(make) => make.clone(),
167 None => "make".into(),
168 }
169 }
170
171 pub fn cmake_cmd(&self) -> String {
173 match self.variables.get("CMAKE") {
174 Some(cmake) => cmake.clone(),
175 None => "cmake".into(),
176 }
177 }
178
179 pub fn variables(&self) -> &HashMap<String, String> {
183 &self.variables
184 }
185
186 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
187 &self.external_deps
188 }
189
190 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
193 &self.entrypoint_layout
194 }
195
196 pub fn cache_dir(&self) -> &PathBuf {
198 &self.cache_dir
199 }
200
201 pub fn data_dir(&self) -> &PathBuf {
203 &self.data_dir
204 }
205
206 pub fn vendor_dir(&self) -> Option<&PathBuf> {
210 self.vendor_dir.as_ref()
211 }
212
213 pub fn user_agent(&self) -> &str {
215 &self.user_agent
216 }
217
218 pub fn generate_luarc(&self) -> bool {
220 self.generate_luarc
221 }
222
223 pub fn wrap_bin_scripts(&self) -> bool {
227 self.wrap_bin_scripts
228 }
229}
230
231impl HasVariables for Config {
232 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
233 Ok(self.variables.get(input).cloned())
234 }
235}
236
237#[derive(Error, Debug)]
238pub enum ConfigError {
239 #[error(transparent)]
240 Io(#[from] io::Error),
241 #[error(transparent)]
242 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
243 #[error("error deserializing lux config: {0}")]
244 Deserialize(#[from] toml::de::Error),
245 #[error("error parsing URL: {0}")]
246 UrlParseError(#[from] url::ParseError),
247 #[error("error initializing compiler toolchain: {0}")]
248 CompilerToolchain(#[from] cc::Error),
249}
250
251#[derive(Clone, Default, Deserialize, Serialize)]
258pub struct ConfigBuilder {
259 #[serde(
260 default,
261 deserialize_with = "deserialize_url",
262 serialize_with = "serialize_url"
263 )]
264 server: Option<Url>,
265 #[serde(
266 default,
267 deserialize_with = "deserialize_url_vec",
268 serialize_with = "serialize_url_vec"
269 )]
270 extra_servers: Option<Vec<Url>>,
271 namespace: Option<String>,
272 lua_version: Option<LuaVersion>,
273 user_tree: Option<PathBuf>,
274 lua_dir: Option<PathBuf>,
275 cache_dir: Option<PathBuf>,
276 data_dir: Option<PathBuf>,
277 vendor_dir: Option<PathBuf>,
278 enable_development_packages: Option<bool>,
279 verbose: Option<bool>,
280 no_progress: Option<bool>,
281 no_prompt: Option<bool>,
282 timeout: Option<Duration>,
283 max_jobs: Option<usize>,
284 variables: Option<HashMap<String, String>>,
285 #[serde(default)]
286 external_deps: ExternalDependencySearchConfig,
287 #[serde(default)]
288 entrypoint_layout: RockLayoutConfig,
289 user_agent: Option<String>,
290 generate_luarc: Option<bool>,
291 wrap_bin_scripts: Option<bool>,
292}
293
294impl ConfigBuilder {
296 pub fn new() -> Result<Self, ConfigError> {
299 let config_file = Self::config_file()?;
300 if config_file.is_file() {
301 Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
302 } else {
303 Ok(Self::default())
304 }
305 }
306
307 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
309 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
310 .ok_or(NoValidHomeDirectory)?;
311 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
312 }
313
314 pub fn dev(self, dev: Option<bool>) -> Self {
317 Self {
318 enable_development_packages: dev.or(self.enable_development_packages),
319 ..self
320 }
321 }
322
323 pub fn server(self, server: Option<Url>) -> Self {
326 Self {
327 server: server.or(self.server),
328 ..self
329 }
330 }
331
332 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
334 Self {
335 extra_servers: extra_servers.or(self.extra_servers),
336 ..self
337 }
338 }
339
340 pub fn namespace(self, namespace: Option<String>) -> Self {
342 Self {
343 namespace: namespace.or(self.namespace),
344 ..self
345 }
346 }
347
348 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
350 Self {
351 lua_dir: lua_dir.or(self.lua_dir),
352 ..self
353 }
354 }
355
356 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
359 Self {
360 lua_version: lua_version.or(self.lua_version),
361 ..self
362 }
363 }
364
365 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
367 Self {
368 user_tree: tree.or(self.user_tree),
369 ..self
370 }
371 }
372
373 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
377 Self {
378 variables: variables.or(self.variables),
379 ..self
380 }
381 }
382
383 pub fn verbose(self, verbose: Option<bool>) -> Self {
386 Self {
387 verbose: verbose.or(self.verbose),
388 ..self
389 }
390 }
391
392 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
395 Self {
396 no_progress: no_progress.or(self.no_progress),
397 ..self
398 }
399 }
400
401 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
404 Self {
405 no_prompt: no_prompt.or(self.no_prompt),
406 ..self
407 }
408 }
409
410 pub fn timeout(self, timeout: Option<Duration>) -> Self {
414 Self {
415 timeout: timeout.or(self.timeout),
416 ..self
417 }
418 }
419
420 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
424 Self {
425 max_jobs: max_jobs.or(self.max_jobs),
426 ..self
427 }
428 }
429
430 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
432 Self {
433 cache_dir: cache_dir.or(self.cache_dir),
434 ..self
435 }
436 }
437
438 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
440 Self {
441 data_dir: data_dir.or(self.data_dir),
442 ..self
443 }
444 }
445
446 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
450 Self {
451 vendor_dir: vendor_dir.or(self.vendor_dir),
452 ..self
453 }
454 }
455
456 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
459 Self {
460 entrypoint_layout: rock_layout,
461 ..self
462 }
463 }
464
465 pub fn user_agent(self, user_agent: Option<String>) -> Self {
468 Self {
469 user_agent: user_agent.or(self.user_agent),
470 ..self
471 }
472 }
473
474 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
477 Self {
478 generate_luarc: generate.or(self.generate_luarc),
479 ..self
480 }
481 }
482
483 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
489 Self {
490 wrap_bin_scripts: generate.or(self.generate_luarc),
491 ..self
492 }
493 }
494
495 pub fn build(self) -> Result<Config, ConfigError> {
496 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
497 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
498 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
499
500 let lua_version = self
501 .lua_version
502 .or(crate::lua_installation::detect_installed_lua_version());
503
504 Ok(Config {
505 enable_development_packages: self.enable_development_packages.unwrap_or(false),
506 server: self.server.unwrap_or_else(|| unsafe {
507 Url::parse("https://luarocks.org/").unwrap_unchecked()
508 }),
509 extra_servers: self.extra_servers.unwrap_or_default(),
510 namespace: self.namespace,
511 lua_dir: self.lua_dir,
512 lua_version,
513 user_tree,
514 verbose: self.verbose.unwrap_or(false),
515 no_progress: self.no_progress.unwrap_or(false),
516 no_prompt: self.no_prompt.unwrap_or(false),
517 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
518 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
519 0 => usize::MAX,
520 max_jobs => max_jobs,
521 },
522 variables: default_variables()
523 .chain(self.variables.unwrap_or_default())
524 .collect(),
525 external_deps: self.external_deps,
526 entrypoint_layout: self.entrypoint_layout,
527 cache_dir,
528 data_dir,
529 vendor_dir: self.vendor_dir,
530 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
531 generate_luarc: self.generate_luarc.unwrap_or(true),
532 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
533 })
534 }
535}
536
537impl From<Config> for ConfigBuilder {
539 fn from(value: Config) -> Self {
540 ConfigBuilder {
541 enable_development_packages: Some(value.enable_development_packages),
542 server: Some(value.server),
543 extra_servers: Some(value.extra_servers),
544 namespace: value.namespace,
545 lua_dir: value.lua_dir,
546 lua_version: value.lua_version,
547 user_tree: Some(value.user_tree),
548 verbose: Some(value.verbose),
549 no_progress: Some(value.no_progress),
550 no_prompt: Some(value.no_prompt),
551 timeout: Some(value.timeout),
552 max_jobs: if value.max_jobs == usize::MAX {
553 None
554 } else {
555 Some(value.max_jobs)
556 },
557 variables: Some(value.variables),
558 cache_dir: Some(value.cache_dir),
559 data_dir: Some(value.data_dir),
560 vendor_dir: value.vendor_dir,
561 external_deps: value.external_deps,
562 entrypoint_layout: value.entrypoint_layout,
563 user_agent: Some(value.user_agent),
564 generate_luarc: Some(value.generate_luarc),
565 wrap_bin_scripts: Some(value.wrap_bin_scripts),
566 }
567 }
568}
569
570fn default_variables() -> impl Iterator<Item = (String, String)> {
571 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
572 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
573 vec![
574 ("MAKE".into(), "make".into()),
575 ("CMAKE".into(), "cmake".into()),
576 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
577 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
578 ("CFLAGS".into(), cflags),
579 ("LDFLAGS".into(), ldflags),
580 ("LIBFLAG".into(), utils::default_libflag().into()),
581 ]
582 .into_iter()
583}
584
585fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
586where
587 D: serde::Deserializer<'de>,
588{
589 let s = Option::<String>::deserialize(deserializer)?;
590 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
591 .transpose()
592}
593
594fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
595where
596 S: Serializer,
597{
598 match url {
599 Some(url) => serializer.serialize_some(url.as_str()),
600 None => serializer.serialize_none(),
601 }
602}
603
604fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
605where
606 D: serde::Deserializer<'de>,
607{
608 let s = Option::<Vec<String>>::deserialize(deserializer)?;
609 s.map(|v| {
610 v.into_iter()
611 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
612 .try_collect()
613 })
614 .transpose()
615}
616
617fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
618where
619 S: Serializer,
620{
621 match urls {
622 Some(urls) => {
623 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
624 serializer.serialize_some(&url_strings)
625 }
626 None => serializer.serialize_none(),
627 }
628}