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