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