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 only_sources: Option<String>,
35 namespace: Option<String>,
36 lua_dir: Option<PathBuf>,
37 lua_version: Option<LuaVersion>,
38 user_tree: PathBuf,
39 verbose: bool,
40 no_progress: bool,
42 no_prompt: bool,
44 timeout: Duration,
45 max_jobs: usize,
46 variables: HashMap<String, String>,
47 external_deps: ExternalDependencySearchConfig,
48 entrypoint_layout: RockLayoutConfig,
51
52 cache_dir: PathBuf,
53 data_dir: PathBuf,
54 vendor_dir: Option<PathBuf>,
55
56 user_agent: String,
59
60 generate_luarc: bool,
61 wrap_bin_scripts: bool,
67}
68
69impl Config {
70 pub fn get_project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
71 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
72 }
73
74 pub fn get_default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
75 let project_dirs = Config::get_project_dirs()?;
76 Ok(project_dirs.cache_dir().to_path_buf())
77 }
78
79 pub fn get_default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
80 let project_dirs = Config::get_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 {
85 Self {
86 lua_version: Some(lua_version),
87 ..self
88 }
89 }
90
91 pub fn with_tree(self, tree: PathBuf) -> Self {
92 Self {
93 user_tree: tree,
94 ..self
95 }
96 }
97
98 pub fn server(&self) -> &Url {
99 &self.server
100 }
101
102 pub fn extra_servers(&self) -> &Vec<Url> {
103 self.extra_servers.as_ref()
104 }
105
106 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
107 let mut enabled_dev_servers = Vec::new();
108 if self.enable_development_packages {
109 enabled_dev_servers.push(self.server().join(DEV_PATH)?);
110 for server in self.extra_servers() {
111 enabled_dev_servers.push(server.join(DEV_PATH)?);
112 }
113 }
114 Ok(enabled_dev_servers)
115 }
116
117 pub fn only_sources(&self) -> Option<&String> {
118 self.only_sources.as_ref()
119 }
120
121 pub fn namespace(&self) -> Option<&String> {
122 self.namespace.as_ref()
123 }
124
125 pub fn lua_dir(&self) -> Option<&PathBuf> {
126 self.lua_dir.as_ref()
127 }
128
129 pub fn lua_version(&self) -> Option<&LuaVersion> {
131 self.lua_version.as_ref()
132 }
133
134 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
137 Tree::new(self.user_tree.clone(), version, self)
138 }
139
140 pub fn verbose(&self) -> bool {
141 self.verbose
142 }
143
144 pub fn no_progress(&self) -> bool {
145 self.no_progress
146 }
147
148 pub fn no_prompt(&self) -> bool {
149 self.no_prompt
150 }
151
152 pub fn timeout(&self) -> &Duration {
153 &self.timeout
154 }
155
156 pub fn max_jobs(&self) -> usize {
157 self.max_jobs
158 }
159
160 pub fn make_cmd(&self) -> String {
161 match self.variables.get("MAKE") {
162 Some(make) => make.clone(),
163 None => "make".into(),
164 }
165 }
166
167 pub fn cmake_cmd(&self) -> String {
168 match self.variables.get("CMAKE") {
169 Some(cmake) => cmake.clone(),
170 None => "cmake".into(),
171 }
172 }
173
174 pub fn variables(&self) -> &HashMap<String, String> {
175 &self.variables
176 }
177
178 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
179 &self.external_deps
180 }
181
182 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
183 &self.entrypoint_layout
184 }
185
186 pub fn cache_dir(&self) -> &PathBuf {
187 &self.cache_dir
188 }
189
190 pub fn data_dir(&self) -> &PathBuf {
191 &self.data_dir
192 }
193
194 pub fn vendor_dir(&self) -> Option<&PathBuf> {
195 self.vendor_dir.as_ref()
196 }
197
198 pub fn user_agent(&self) -> &str {
199 &self.user_agent
200 }
201
202 pub fn generate_luarc(&self) -> bool {
203 self.generate_luarc
204 }
205
206 pub fn wrap_bin_scripts(&self) -> bool {
207 self.wrap_bin_scripts
208 }
209}
210
211impl HasVariables for Config {
212 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
213 Ok(self.variables.get(input).cloned())
214 }
215}
216
217#[derive(Error, Debug)]
218pub enum ConfigError {
219 #[error(transparent)]
220 Io(#[from] io::Error),
221 #[error(transparent)]
222 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
223 #[error("error deserializing lux config: {0}")]
224 Deserialize(#[from] toml::de::Error),
225 #[error("error parsing URL: {0}")]
226 UrlParseError(#[from] url::ParseError),
227 #[error("error initializing compiler toolchain: {0}")]
228 CompilerToolchain(#[from] cc::Error),
229}
230
231#[derive(Clone, Default, Deserialize, Serialize)]
238pub struct ConfigBuilder {
239 #[serde(
240 default,
241 deserialize_with = "deserialize_url",
242 serialize_with = "serialize_url"
243 )]
244 server: Option<Url>,
245 #[serde(
246 default,
247 deserialize_with = "deserialize_url_vec",
248 serialize_with = "serialize_url_vec"
249 )]
250 extra_servers: Option<Vec<Url>>,
251 only_sources: Option<String>,
252 namespace: Option<String>,
253 lua_version: Option<LuaVersion>,
254 user_tree: Option<PathBuf>,
255 lua_dir: Option<PathBuf>,
256 cache_dir: Option<PathBuf>,
257 data_dir: Option<PathBuf>,
258 vendor_dir: Option<PathBuf>,
259 enable_development_packages: Option<bool>,
260 verbose: Option<bool>,
261 no_progress: Option<bool>,
262 no_prompt: Option<bool>,
263 timeout: Option<Duration>,
264 max_jobs: Option<usize>,
265 variables: Option<HashMap<String, String>>,
266 #[serde(default)]
267 external_deps: ExternalDependencySearchConfig,
268 #[serde(default)]
271 entrypoint_layout: RockLayoutConfig,
272 user_agent: Option<String>,
273 generate_luarc: Option<bool>,
274 wrap_bin_scripts: Option<bool>,
280}
281
282impl ConfigBuilder {
284 pub fn new() -> Result<Self, ConfigError> {
287 let config_file = Self::config_file()?;
288 if config_file.is_file() {
289 Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
290 } else {
291 Ok(Self::default())
292 }
293 }
294
295 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
297 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
298 .ok_or(NoValidHomeDirectory)?;
299 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
300 }
301
302 pub fn dev(self, dev: Option<bool>) -> Self {
303 Self {
304 enable_development_packages: dev.or(self.enable_development_packages),
305 ..self
306 }
307 }
308
309 pub fn server(self, server: Option<Url>) -> Self {
310 Self {
311 server: server.or(self.server),
312 ..self
313 }
314 }
315
316 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
317 Self {
318 extra_servers: extra_servers.or(self.extra_servers),
319 ..self
320 }
321 }
322
323 pub fn only_sources(self, sources: Option<String>) -> Self {
324 Self {
325 only_sources: sources.or(self.only_sources),
326 ..self
327 }
328 }
329
330 pub fn namespace(self, namespace: Option<String>) -> Self {
331 Self {
332 namespace: namespace.or(self.namespace),
333 ..self
334 }
335 }
336
337 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
338 Self {
339 lua_dir: lua_dir.or(self.lua_dir),
340 ..self
341 }
342 }
343
344 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
345 Self {
346 lua_version: lua_version.or(self.lua_version),
347 ..self
348 }
349 }
350
351 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
352 Self {
353 user_tree: tree.or(self.user_tree),
354 ..self
355 }
356 }
357
358 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
359 Self {
360 variables: variables.or(self.variables),
361 ..self
362 }
363 }
364
365 pub fn verbose(self, verbose: Option<bool>) -> Self {
366 Self {
367 verbose: verbose.or(self.verbose),
368 ..self
369 }
370 }
371
372 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
373 Self {
374 no_progress: no_progress.or(self.no_progress),
375 ..self
376 }
377 }
378
379 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
380 Self {
381 no_prompt: no_prompt.or(self.no_prompt),
382 ..self
383 }
384 }
385
386 pub fn timeout(self, timeout: Option<Duration>) -> Self {
387 Self {
388 timeout: timeout.or(self.timeout),
389 ..self
390 }
391 }
392
393 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
394 Self {
395 max_jobs: max_jobs.or(self.max_jobs),
396 ..self
397 }
398 }
399
400 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
401 Self {
402 cache_dir: cache_dir.or(self.cache_dir),
403 ..self
404 }
405 }
406
407 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
408 Self {
409 data_dir: data_dir.or(self.data_dir),
410 ..self
411 }
412 }
413
414 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
415 Self {
416 vendor_dir: vendor_dir.or(self.vendor_dir),
417 ..self
418 }
419 }
420
421 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
422 Self {
423 entrypoint_layout: rock_layout,
424 ..self
425 }
426 }
427
428 pub fn user_agent(self, user_agent: Option<String>) -> Self {
429 Self {
430 user_agent: user_agent.or(self.user_agent),
431 ..self
432 }
433 }
434
435 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
436 Self {
437 generate_luarc: generate.or(self.generate_luarc),
438 ..self
439 }
440 }
441
442 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
443 Self {
444 wrap_bin_scripts: generate.or(self.generate_luarc),
445 ..self
446 }
447 }
448
449 pub fn build(self) -> Result<Config, ConfigError> {
450 let data_dir = self.data_dir.unwrap_or(Config::get_default_data_path()?);
451 let cache_dir = self.cache_dir.unwrap_or(Config::get_default_cache_path()?);
452 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
453
454 let lua_version = self
455 .lua_version
456 .or(crate::lua_installation::detect_installed_lua_version());
457
458 Ok(Config {
459 enable_development_packages: self.enable_development_packages.unwrap_or(false),
460 server: self.server.unwrap_or_else(|| unsafe {
461 Url::parse("https://luarocks.org/").unwrap_unchecked()
462 }),
463 extra_servers: self.extra_servers.unwrap_or_default(),
464 only_sources: self.only_sources,
465 namespace: self.namespace,
466 lua_dir: self.lua_dir,
467 lua_version,
468 user_tree,
469 verbose: self.verbose.unwrap_or(false),
470 no_progress: self.no_progress.unwrap_or(false),
471 no_prompt: self.no_prompt.unwrap_or(false),
472 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
473 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
474 0 => usize::MAX,
475 max_jobs => max_jobs,
476 },
477 variables: default_variables()
478 .chain(self.variables.unwrap_or_default())
479 .collect(),
480 external_deps: self.external_deps,
481 entrypoint_layout: self.entrypoint_layout,
482 cache_dir,
483 data_dir,
484 vendor_dir: self.vendor_dir,
485 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
486 generate_luarc: self.generate_luarc.unwrap_or(true),
487 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
488 })
489 }
490}
491
492impl From<Config> for ConfigBuilder {
494 fn from(value: Config) -> Self {
495 ConfigBuilder {
496 enable_development_packages: Some(value.enable_development_packages),
497 server: Some(value.server),
498 extra_servers: Some(value.extra_servers),
499 only_sources: value.only_sources,
500 namespace: value.namespace,
501 lua_dir: value.lua_dir,
502 lua_version: value.lua_version,
503 user_tree: Some(value.user_tree),
504 verbose: Some(value.verbose),
505 no_progress: Some(value.no_progress),
506 no_prompt: Some(value.no_prompt),
507 timeout: Some(value.timeout),
508 max_jobs: if value.max_jobs == usize::MAX {
509 None
510 } else {
511 Some(value.max_jobs)
512 },
513 variables: Some(value.variables),
514 cache_dir: Some(value.cache_dir),
515 data_dir: Some(value.data_dir),
516 vendor_dir: value.vendor_dir,
517 external_deps: value.external_deps,
518 entrypoint_layout: value.entrypoint_layout,
519 user_agent: Some(value.user_agent),
520 generate_luarc: Some(value.generate_luarc),
521 wrap_bin_scripts: Some(value.wrap_bin_scripts),
522 }
523 }
524}
525
526fn default_variables() -> impl Iterator<Item = (String, String)> {
527 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
528 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
529 vec![
530 ("MAKE".into(), "make".into()),
531 ("CMAKE".into(), "cmake".into()),
532 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
533 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
534 ("CFLAGS".into(), cflags),
535 ("LDFLAGS".into(), ldflags),
536 ("LIBFLAG".into(), utils::default_libflag().into()),
537 ]
538 .into_iter()
539}
540
541fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
542where
543 D: serde::Deserializer<'de>,
544{
545 let s = Option::<String>::deserialize(deserializer)?;
546 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
547 .transpose()
548}
549
550fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
551where
552 S: Serializer,
553{
554 match url {
555 Some(url) => serializer.serialize_some(url.as_str()),
556 None => serializer.serialize_none(),
557 }
558}
559
560fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
561where
562 D: serde::Deserializer<'de>,
563{
564 let s = Option::<Vec<String>>::deserialize(deserializer)?;
565 s.map(|v| {
566 v.into_iter()
567 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
568 .try_collect()
569 })
570 .transpose()
571}
572
573fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
574where
575 S: Serializer,
576{
577 match urls {
578 Some(urls) => {
579 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
580 serializer.serialize_some(&url_strings)
581 }
582 None => serializer.serialize_none(),
583 }
584}