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