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 luarc_file_name: String,
57 wrap_bin_scripts: bool,
58}
59
60impl Config {
61 fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
63 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
64 }
65
66 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
68 let project_dirs = Config::project_dirs()?;
69 Ok(project_dirs.cache_dir().to_path_buf())
70 }
71
72 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
74 let project_dirs = Config::project_dirs()?;
75 Ok(project_dirs.data_local_dir().to_path_buf())
76 }
77
78 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
80 Self {
81 lua_version: Some(lua_version),
82 ..self
83 }
84 }
85
86 pub fn with_tree(self, tree: PathBuf) -> Self {
88 Self {
89 user_tree: tree,
90 ..self
91 }
92 }
93
94 pub fn server(&self) -> &Url {
96 &self.server
97 }
98
99 pub fn extra_servers(&self) -> &Vec<Url> {
101 self.extra_servers.as_ref()
102 }
103
104 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
106 let mut enabled_dev_servers = Vec::new();
107 if self.enable_development_packages {
108 enabled_dev_servers.push(self.server().join(DEV_PATH)?);
109 for server in self.extra_servers() {
110 enabled_dev_servers.push(server.join(DEV_PATH)?);
111 }
112 }
113 Ok(enabled_dev_servers)
114 }
115
116 pub fn namespace(&self) -> Option<&String> {
118 self.namespace.as_ref()
119 }
120
121 pub fn lua_dir(&self) -> Option<&PathBuf> {
123 self.lua_dir.as_ref()
124 }
125
126 pub fn lua_version(&self) -> Option<&LuaVersion> {
128 self.lua_version.as_ref()
129 }
130
131 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
134 Tree::new(self.user_tree.clone(), version, self)
135 }
136
137 pub fn verbose(&self) -> bool {
139 self.verbose
140 }
141
142 pub fn no_progress(&self) -> bool {
144 self.no_progress
145 }
146
147 pub fn no_prompt(&self) -> bool {
149 self.no_prompt
150 }
151
152 pub fn timeout(&self) -> &Duration {
155 &self.timeout
156 }
157
158 pub fn max_jobs(&self) -> usize {
161 self.max_jobs
162 }
163
164 pub fn make_cmd(&self) -> String {
166 match self.variables.get("MAKE") {
167 Some(make) => make.clone(),
168 None => "make".into(),
169 }
170 }
171
172 pub fn cmake_cmd(&self) -> String {
174 match self.variables.get("CMAKE") {
175 Some(cmake) => cmake.clone(),
176 None => "cmake".into(),
177 }
178 }
179
180 pub fn variables(&self) -> &HashMap<String, String> {
184 &self.variables
185 }
186
187 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
188 &self.external_deps
189 }
190
191 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
194 &self.entrypoint_layout
195 }
196
197 pub fn cache_dir(&self) -> &PathBuf {
199 &self.cache_dir
200 }
201
202 pub fn data_dir(&self) -> &PathBuf {
204 &self.data_dir
205 }
206
207 pub fn vendor_dir(&self) -> Option<&PathBuf> {
211 self.vendor_dir.as_ref()
212 }
213
214 pub fn user_agent(&self) -> &str {
216 &self.user_agent
217 }
218
219 pub fn generate_luarc(&self) -> bool {
221 self.generate_luarc
222 }
223
224 pub fn luarc_file_name(&self) -> &str {
226 &self.luarc_file_name
227 }
228
229 pub fn wrap_bin_scripts(&self) -> bool {
233 self.wrap_bin_scripts
234 }
235}
236
237impl HasVariables for Config {
238 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
239 Ok(self.variables.get(input).cloned())
240 }
241}
242
243#[derive(Error, Debug)]
244pub enum ConfigError {
245 #[error(transparent)]
246 Io(#[from] io::Error),
247 #[error(transparent)]
248 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
249 #[error("error deserializing lux config: {0}")]
250 Deserialize(#[from] toml::de::Error),
251 #[error("error parsing URL: {0}")]
252 UrlParseError(#[from] url::ParseError),
253 #[error("error initializing compiler toolchain: {0}")]
254 CompilerToolchain(#[from] cc::Error),
255}
256
257#[derive(Clone, Default, Deserialize, Serialize)]
264pub struct ConfigBuilder {
265 #[serde(
266 default,
267 deserialize_with = "deserialize_url",
268 serialize_with = "serialize_url"
269 )]
270 server: Option<Url>,
271 #[serde(
272 default,
273 deserialize_with = "deserialize_url_vec",
274 serialize_with = "serialize_url_vec"
275 )]
276 extra_servers: Option<Vec<Url>>,
277 namespace: Option<String>,
278 lua_version: Option<LuaVersion>,
279 user_tree: Option<PathBuf>,
280 lua_dir: Option<PathBuf>,
281 cache_dir: Option<PathBuf>,
282 data_dir: Option<PathBuf>,
283 vendor_dir: Option<PathBuf>,
284 enable_development_packages: Option<bool>,
285 verbose: Option<bool>,
286 no_progress: Option<bool>,
287 no_prompt: Option<bool>,
288 timeout: Option<Duration>,
289 max_jobs: Option<usize>,
290 variables: Option<HashMap<String, String>>,
291 #[serde(default)]
292 external_deps: ExternalDependencySearchConfig,
293 #[serde(default)]
294 entrypoint_layout: RockLayoutConfig,
295 user_agent: Option<String>,
296 generate_luarc: Option<bool>,
297 luarc_file_name: Option<String>,
298 wrap_bin_scripts: Option<bool>,
299}
300
301impl ConfigBuilder {
303 pub fn new() -> Result<Self, ConfigError> {
306 let config_file = Self::config_file()?;
307 if config_file.is_file() {
308 Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
309 } else {
310 Ok(Self::default())
311 }
312 }
313
314 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
316 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
317 .ok_or(NoValidHomeDirectory)?;
318 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
319 }
320
321 pub fn dev(self, dev: Option<bool>) -> Self {
324 Self {
325 enable_development_packages: dev.or(self.enable_development_packages),
326 ..self
327 }
328 }
329
330 pub fn server(self, server: Option<Url>) -> Self {
333 Self {
334 server: server.or(self.server),
335 ..self
336 }
337 }
338
339 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
341 Self {
342 extra_servers: extra_servers.or(self.extra_servers),
343 ..self
344 }
345 }
346
347 pub fn namespace(self, namespace: Option<String>) -> Self {
349 Self {
350 namespace: namespace.or(self.namespace),
351 ..self
352 }
353 }
354
355 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
357 Self {
358 lua_dir: lua_dir.or(self.lua_dir),
359 ..self
360 }
361 }
362
363 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
366 Self {
367 lua_version: lua_version.or(self.lua_version),
368 ..self
369 }
370 }
371
372 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
374 Self {
375 user_tree: tree.or(self.user_tree),
376 ..self
377 }
378 }
379
380 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
384 Self {
385 variables: variables.or(self.variables),
386 ..self
387 }
388 }
389
390 pub fn verbose(self, verbose: Option<bool>) -> Self {
393 Self {
394 verbose: verbose.or(self.verbose),
395 ..self
396 }
397 }
398
399 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
402 Self {
403 no_progress: no_progress.or(self.no_progress),
404 ..self
405 }
406 }
407
408 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
411 Self {
412 no_prompt: no_prompt.or(self.no_prompt),
413 ..self
414 }
415 }
416
417 pub fn timeout(self, timeout: Option<Duration>) -> Self {
421 Self {
422 timeout: timeout.or(self.timeout),
423 ..self
424 }
425 }
426
427 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
431 Self {
432 max_jobs: max_jobs.or(self.max_jobs),
433 ..self
434 }
435 }
436
437 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
439 Self {
440 cache_dir: cache_dir.or(self.cache_dir),
441 ..self
442 }
443 }
444
445 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
447 Self {
448 data_dir: data_dir.or(self.data_dir),
449 ..self
450 }
451 }
452
453 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
457 Self {
458 vendor_dir: vendor_dir.or(self.vendor_dir),
459 ..self
460 }
461 }
462
463 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
466 Self {
467 entrypoint_layout: rock_layout,
468 ..self
469 }
470 }
471
472 pub fn user_agent(self, user_agent: Option<String>) -> Self {
475 Self {
476 user_agent: user_agent.or(self.user_agent),
477 ..self
478 }
479 }
480
481 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
484 Self {
485 generate_luarc: generate.or(self.generate_luarc),
486 ..self
487 }
488 }
489
490 pub fn luarc_file_name(self, file: Option<String>) -> Self {
493 Self {
494 luarc_file_name: file.or(self.luarc_file_name),
495 ..self
496 }
497 }
498
499 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
505 Self {
506 wrap_bin_scripts: generate.or(self.generate_luarc),
507 ..self
508 }
509 }
510
511 pub fn build(self) -> Result<Config, ConfigError> {
512 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
513 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
514 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
515
516 let lua_version = self
517 .lua_version
518 .or(crate::lua_installation::detect_installed_lua_version());
519
520 Ok(Config {
521 enable_development_packages: self.enable_development_packages.unwrap_or(false),
522 server: self.server.unwrap_or_else(|| unsafe {
523 Url::parse("https://luarocks.org/").unwrap_unchecked()
524 }),
525 extra_servers: self.extra_servers.unwrap_or_default(),
526 namespace: self.namespace,
527 lua_dir: self.lua_dir,
528 lua_version,
529 user_tree,
530 verbose: self.verbose.unwrap_or(false),
531 no_progress: self.no_progress.unwrap_or(false),
532 no_prompt: self.no_prompt.unwrap_or(false),
533 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
534 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
535 0 => usize::MAX,
536 max_jobs => max_jobs,
537 },
538 variables: default_variables()
539 .chain(self.variables.unwrap_or_default())
540 .collect(),
541 external_deps: self.external_deps,
542 entrypoint_layout: self.entrypoint_layout,
543 cache_dir,
544 data_dir,
545 vendor_dir: self.vendor_dir,
546 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
547 generate_luarc: self.generate_luarc.unwrap_or(true),
548 luarc_file_name: self
549 .luarc_file_name
550 .unwrap_or_else(|| ".luarc.json".to_string()),
551 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
552 })
553 }
554}
555
556impl From<Config> for ConfigBuilder {
558 fn from(value: Config) -> Self {
559 ConfigBuilder {
560 enable_development_packages: Some(value.enable_development_packages),
561 server: Some(value.server),
562 extra_servers: Some(value.extra_servers),
563 namespace: value.namespace,
564 lua_dir: value.lua_dir,
565 lua_version: value.lua_version,
566 user_tree: Some(value.user_tree),
567 verbose: Some(value.verbose),
568 no_progress: Some(value.no_progress),
569 no_prompt: Some(value.no_prompt),
570 timeout: Some(value.timeout),
571 max_jobs: if value.max_jobs == usize::MAX {
572 None
573 } else {
574 Some(value.max_jobs)
575 },
576 variables: Some(value.variables),
577 cache_dir: Some(value.cache_dir),
578 data_dir: Some(value.data_dir),
579 vendor_dir: value.vendor_dir,
580 external_deps: value.external_deps,
581 entrypoint_layout: value.entrypoint_layout,
582 user_agent: Some(value.user_agent),
583 generate_luarc: Some(value.generate_luarc),
584 luarc_file_name: Some(value.luarc_file_name),
585 wrap_bin_scripts: Some(value.wrap_bin_scripts),
586 }
587 }
588}
589
590fn default_variables() -> impl Iterator<Item = (String, String)> {
591 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
592 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
593 vec![
594 ("MAKE".into(), "make".into()),
595 ("CMAKE".into(), "cmake".into()),
596 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
597 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
598 ("CFLAGS".into(), cflags),
599 ("LDFLAGS".into(), ldflags),
600 ("LIBFLAG".into(), utils::default_libflag().into()),
601 ]
602 .into_iter()
603}
604
605fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
606where
607 D: serde::Deserializer<'de>,
608{
609 let s = Option::<String>::deserialize(deserializer)?;
610 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
611 .transpose()
612}
613
614fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
615where
616 S: Serializer,
617{
618 match url {
619 Some(url) => serializer.serialize_some(url.as_str()),
620 None => serializer.serialize_none(),
621 }
622}
623
624fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
625where
626 D: serde::Deserializer<'de>,
627{
628 let s = Option::<Vec<String>>::deserialize(deserializer)?;
629 s.map(|v| {
630 v.into_iter()
631 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
632 .try_collect()
633 })
634 .transpose()
635}
636
637fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
638where
639 S: Serializer,
640{
641 match urls {
642 Some(urls) => {
643 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
644 serializer.serialize_some(&url_strings)
645 }
646 None => serializer.serialize_none(),
647 }
648}