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