1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use serde::{Deserialize, Serialize, Serializer};
6use std::env::current_exe;
7use std::path::Path;
8use std::{
9 collections::HashMap, env, fmt::Display, io, path::PathBuf, str::FromStr, time::Duration,
10};
11use thiserror::Error;
12use tree::RockLayoutConfig;
13use url::Url;
14
15use crate::tree::{Tree, TreeError};
16use crate::variables::GetVariableError;
17use crate::{
18 build::utils,
19 package::{PackageVersion, PackageVersionReq},
20 variables::HasVariables,
21};
22
23pub mod external_deps;
24pub mod tree;
25
26const DEV_PATH: &str = "dev/";
27
28#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
29pub enum LuaVersion {
30 #[serde(rename = "5.1")]
31 Lua51,
32 #[serde(rename = "5.2")]
33 Lua52,
34 #[serde(rename = "5.3")]
35 Lua53,
36 #[serde(rename = "5.4")]
37 Lua54,
38 #[serde(rename = "5.5")]
39 Lua55,
40 #[serde(rename = "jit")]
41 LuaJIT,
42 #[serde(rename = "jit5.2")]
43 LuaJIT52,
44 }
47
48#[derive(Debug, Error)]
49pub enum LuaVersionError {
50 #[error("unsupported Lua version: {0}")]
51 UnsupportedLuaVersion(PackageVersion),
52}
53
54impl LuaVersion {
55 pub fn as_version(&self) -> PackageVersion {
56 unsafe {
57 match self {
58 LuaVersion::Lua51 => "5.1.0".parse().unwrap_unchecked(),
59 LuaVersion::Lua52 => "5.2.0".parse().unwrap_unchecked(),
60 LuaVersion::Lua53 => "5.3.0".parse().unwrap_unchecked(),
61 LuaVersion::Lua54 => "5.4.0".parse().unwrap_unchecked(),
62 LuaVersion::Lua55 => "5.5.0".parse().unwrap_unchecked(),
63 LuaVersion::LuaJIT => "5.1.0".parse().unwrap_unchecked(),
64 LuaVersion::LuaJIT52 => "5.2.0".parse().unwrap_unchecked(),
65 }
66 }
67 }
68 pub fn version_compatibility_str(&self) -> String {
69 match self {
70 LuaVersion::Lua51 | LuaVersion::LuaJIT => "5.1".into(),
71 LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2".into(),
72 LuaVersion::Lua53 => "5.3".into(),
73 LuaVersion::Lua54 => "5.4".into(),
74 LuaVersion::Lua55 => "5.5".into(),
75 }
76 }
77 pub fn as_version_req(&self) -> PackageVersionReq {
78 unsafe {
79 format!("~> {}", self.version_compatibility_str())
80 .parse()
81 .unwrap_unchecked()
82 }
83 }
84
85 pub fn from_version(version: PackageVersion) -> Result<LuaVersion, LuaVersionError> {
87 let luajit_version_req: PackageVersionReq = unsafe { "~> 2".parse().unwrap_unchecked() };
89 if luajit_version_req.matches(&version) {
90 Ok(LuaVersion::LuaJIT)
91 } else if LuaVersion::Lua51.as_version_req().matches(&version) {
92 Ok(LuaVersion::Lua51)
93 } else if LuaVersion::Lua52.as_version_req().matches(&version) {
94 Ok(LuaVersion::Lua52)
95 } else if LuaVersion::Lua53.as_version_req().matches(&version) {
96 Ok(LuaVersion::Lua53)
97 } else if LuaVersion::Lua54.as_version_req().matches(&version) {
98 Ok(LuaVersion::Lua54)
99 } else if LuaVersion::Lua55.as_version_req().matches(&version) {
100 Ok(LuaVersion::Lua55)
101 } else {
102 Err(LuaVersionError::UnsupportedLuaVersion(version))
103 }
104 }
105
106 pub(crate) fn is_luajit(&self) -> bool {
107 matches!(self, Self::LuaJIT | Self::LuaJIT52)
108 }
109
110 pub fn lux_lib_dir(&self) -> Option<PathBuf> {
112 option_env!("LUX_LIB_DIR")
113 .map(PathBuf::from)
114 .map(|path| path.join(self.to_string()))
115 .or_else(|| {
116 let lib_name = format!("lux-lua{self}");
117 pkg_config::Config::new()
118 .print_system_libs(false)
119 .cargo_metadata(false)
120 .env_metadata(false)
121 .probe(&lib_name)
122 .ok()
123 .and_then(|library| library.link_paths.first().cloned())
124 })
125 .or_else(|| lux_lib_resource_dir().map(|path| path.join(self.to_string())))
126 }
127}
128
129fn lux_lib_resource_dir() -> Option<PathBuf> {
131 if cfg!(target_env = "msvc") {
132 current_exe()
134 .ok()
135 .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
136 .and_then(|exe_dir| {
137 let lib_dir = exe_dir.join("lux-lua");
138 if lib_dir.is_dir() {
139 Some(lib_dir)
140 } else {
141 None
142 }
143 })
144 } else if cfg!(target_os = "macos") {
145 current_exe()
147 .ok()
148 .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
149 .and_then(|macos_dir| macos_dir.parent().map(Path::to_path_buf))
150 .and_then(|contents_dir| {
151 let lib_dir = contents_dir.join("Resources").join("lux-lua");
152 if lib_dir.is_dir() {
153 Some(lib_dir)
154 } else {
155 None
156 }
157 })
158 } else {
159 let lib_dir = PathBuf::from("/usr/share/lux-lua");
161 if lib_dir.is_dir() {
162 Some(lib_dir)
163 } else {
164 None
165 }
166 }
167}
168
169#[derive(Error, Debug)]
170#[error(
171 r#"lua version not set.
172Please provide a version through `lx --lua-version <ver> <cmd>`
173Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit' and 'jit52'.
174"#
175)]
176pub struct LuaVersionUnset;
177
178impl LuaVersion {
179 pub fn from(config: &Config) -> Result<&Self, LuaVersionUnset> {
180 config.lua_version.as_ref().ok_or(LuaVersionUnset)
181 }
182}
183
184impl FromStr for LuaVersion {
185 type Err = String;
186
187 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
188 match s {
189 "5.1" | "51" => Ok(LuaVersion::Lua51),
190 "5.2" | "52" => Ok(LuaVersion::Lua52),
191 "5.3" | "53" => Ok(LuaVersion::Lua53),
192 "5.4" | "54" => Ok(LuaVersion::Lua54),
193 "5.5" | "55" => Ok(LuaVersion::Lua55),
194 "jit" | "luajit" => Ok(LuaVersion::LuaJIT),
195 "jit52" | "luajit52" => Ok(LuaVersion::LuaJIT52),
196 _ => Err(r#"unrecognized Lua version.
197 Supported versions: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit', 'jit52'.
198 "#
199 .into()),
200 }
201 }
202}
203
204impl Display for LuaVersion {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 f.write_str(match self {
207 LuaVersion::Lua51 => "5.1",
208 LuaVersion::Lua52 => "5.2",
209 LuaVersion::Lua53 => "5.3",
210 LuaVersion::Lua54 => "5.4",
211 LuaVersion::Lua55 => "5.5",
212 LuaVersion::LuaJIT => "jit",
213 LuaVersion::LuaJIT52 => "jit52",
214 })
215 }
216}
217
218#[derive(Error, Debug)]
219#[error("could not find a valid home directory")]
220pub struct NoValidHomeDirectory;
221
222#[derive(Debug, Clone)]
223pub struct Config {
224 enable_development_packages: bool,
225 server: Url,
226 extra_servers: Vec<Url>,
227 only_sources: Option<String>,
228 namespace: Option<String>,
229 lua_dir: Option<PathBuf>,
230 lua_version: Option<LuaVersion>,
231 user_tree: PathBuf,
232 verbose: bool,
233 no_progress: bool,
235 timeout: Duration,
236 max_jobs: usize,
237 variables: HashMap<String, String>,
238 external_deps: ExternalDependencySearchConfig,
239 entrypoint_layout: RockLayoutConfig,
242
243 cache_dir: PathBuf,
244 data_dir: PathBuf,
245 vendor_dir: Option<PathBuf>,
246
247 generate_luarc: bool,
248}
249
250impl Config {
251 pub fn get_project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
252 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
253 }
254
255 pub fn get_default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
256 let project_dirs = Config::get_project_dirs()?;
257 Ok(project_dirs.cache_dir().to_path_buf())
258 }
259
260 pub fn get_default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
261 let project_dirs = Config::get_project_dirs()?;
262 Ok(project_dirs.data_local_dir().to_path_buf())
263 }
264
265 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
266 Self {
267 lua_version: Some(lua_version),
268 ..self
269 }
270 }
271
272 pub fn with_tree(self, tree: PathBuf) -> Self {
273 Self {
274 user_tree: tree,
275 ..self
276 }
277 }
278
279 pub fn server(&self) -> &Url {
280 &self.server
281 }
282
283 pub fn extra_servers(&self) -> &Vec<Url> {
284 self.extra_servers.as_ref()
285 }
286
287 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
288 let mut enabled_dev_servers = Vec::new();
289 if self.enable_development_packages {
290 enabled_dev_servers.push(self.server().join(DEV_PATH)?);
291 for server in self.extra_servers() {
292 enabled_dev_servers.push(server.join(DEV_PATH)?);
293 }
294 }
295 Ok(enabled_dev_servers)
296 }
297
298 pub fn only_sources(&self) -> Option<&String> {
299 self.only_sources.as_ref()
300 }
301
302 pub fn namespace(&self) -> Option<&String> {
303 self.namespace.as_ref()
304 }
305
306 pub fn lua_dir(&self) -> Option<&PathBuf> {
307 self.lua_dir.as_ref()
308 }
309
310 #[cfg(test)]
311 pub(crate) fn lua_version(&self) -> Option<&LuaVersion> {
312 self.lua_version.as_ref()
313 }
314
315 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
318 Tree::new(self.user_tree.clone(), version, self)
319 }
320
321 pub fn verbose(&self) -> bool {
322 self.verbose
323 }
324
325 pub fn no_progress(&self) -> bool {
326 self.no_progress
327 }
328
329 pub fn timeout(&self) -> &Duration {
330 &self.timeout
331 }
332
333 pub fn max_jobs(&self) -> usize {
334 self.max_jobs
335 }
336
337 pub fn make_cmd(&self) -> String {
338 match self.variables.get("MAKE") {
339 Some(make) => make.clone(),
340 None => "make".into(),
341 }
342 }
343
344 pub fn cmake_cmd(&self) -> String {
345 match self.variables.get("CMAKE") {
346 Some(cmake) => cmake.clone(),
347 None => "cmake".into(),
348 }
349 }
350
351 pub fn variables(&self) -> &HashMap<String, String> {
352 &self.variables
353 }
354
355 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
356 &self.external_deps
357 }
358
359 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
360 &self.entrypoint_layout
361 }
362
363 pub fn cache_dir(&self) -> &PathBuf {
364 &self.cache_dir
365 }
366
367 pub fn data_dir(&self) -> &PathBuf {
368 &self.data_dir
369 }
370
371 pub fn vendor_dir(&self) -> Option<&PathBuf> {
372 self.vendor_dir.as_ref()
373 }
374
375 pub fn generate_luarc(&self) -> bool {
376 self.generate_luarc
377 }
378}
379
380impl HasVariables for Config {
381 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
382 Ok(self.variables.get(input).cloned())
383 }
384}
385
386#[derive(Error, Debug)]
387pub enum ConfigError {
388 #[error(transparent)]
389 Io(#[from] io::Error),
390 #[error(transparent)]
391 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
392 #[error("error deserializing lux config: {0}")]
393 Deserialize(#[from] toml::de::Error),
394 #[error("error parsing URL: {0}")]
395 UrlParseError(#[from] url::ParseError),
396 #[error("error initializing compiler toolchain: {0}")]
397 CompilerToolchain(#[from] cc::Error),
398}
399
400#[derive(Clone, Default, Deserialize, Serialize)]
401pub struct ConfigBuilder {
402 #[serde(
403 default,
404 deserialize_with = "deserialize_url",
405 serialize_with = "serialize_url"
406 )]
407 server: Option<Url>,
408 #[serde(
409 default,
410 deserialize_with = "deserialize_url_vec",
411 serialize_with = "serialize_url_vec"
412 )]
413 extra_servers: Option<Vec<Url>>,
414 only_sources: Option<String>,
415 namespace: Option<String>,
416 lua_version: Option<LuaVersion>,
417 user_tree: Option<PathBuf>,
418 lua_dir: Option<PathBuf>,
419 cache_dir: Option<PathBuf>,
420 data_dir: Option<PathBuf>,
421 vendor_dir: Option<PathBuf>,
422 enable_development_packages: Option<bool>,
423 verbose: Option<bool>,
424 no_progress: Option<bool>,
425 timeout: Option<Duration>,
426 max_jobs: Option<usize>,
427 variables: Option<HashMap<String, String>>,
428 #[serde(default)]
429 external_deps: ExternalDependencySearchConfig,
430 #[serde(default)]
433 entrypoint_layout: RockLayoutConfig,
434 generate_luarc: Option<bool>,
435}
436
437impl ConfigBuilder {
439 pub fn new() -> Result<Self, ConfigError> {
442 let config_file = Self::config_file()?;
443 if config_file.is_file() {
444 Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
445 } else {
446 Ok(Self::default())
447 }
448 }
449
450 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
452 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
453 .ok_or(NoValidHomeDirectory)?;
454 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
455 }
456
457 pub fn dev(self, dev: Option<bool>) -> Self {
458 Self {
459 enable_development_packages: dev.or(self.enable_development_packages),
460 ..self
461 }
462 }
463
464 pub fn server(self, server: Option<Url>) -> Self {
465 Self {
466 server: server.or(self.server),
467 ..self
468 }
469 }
470
471 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
472 Self {
473 extra_servers: extra_servers.or(self.extra_servers),
474 ..self
475 }
476 }
477
478 pub fn only_sources(self, sources: Option<String>) -> Self {
479 Self {
480 only_sources: sources.or(self.only_sources),
481 ..self
482 }
483 }
484
485 pub fn namespace(self, namespace: Option<String>) -> Self {
486 Self {
487 namespace: namespace.or(self.namespace),
488 ..self
489 }
490 }
491
492 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
493 Self {
494 lua_dir: lua_dir.or(self.lua_dir),
495 ..self
496 }
497 }
498
499 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
500 Self {
501 lua_version: lua_version.or(self.lua_version),
502 ..self
503 }
504 }
505
506 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
507 Self {
508 user_tree: tree.or(self.user_tree),
509 ..self
510 }
511 }
512
513 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
514 Self {
515 variables: variables.or(self.variables),
516 ..self
517 }
518 }
519
520 pub fn verbose(self, verbose: Option<bool>) -> Self {
521 Self {
522 verbose: verbose.or(self.verbose),
523 ..self
524 }
525 }
526
527 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
528 Self {
529 no_progress: no_progress.or(self.no_progress),
530 ..self
531 }
532 }
533
534 pub fn timeout(self, timeout: Option<Duration>) -> Self {
535 Self {
536 timeout: timeout.or(self.timeout),
537 ..self
538 }
539 }
540
541 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
542 Self {
543 max_jobs: max_jobs.or(self.max_jobs),
544 ..self
545 }
546 }
547
548 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
549 Self {
550 cache_dir: cache_dir.or(self.cache_dir),
551 ..self
552 }
553 }
554
555 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
556 Self {
557 data_dir: data_dir.or(self.data_dir),
558 ..self
559 }
560 }
561
562 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
563 Self {
564 vendor_dir: vendor_dir.or(self.vendor_dir),
565 ..self
566 }
567 }
568
569 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
570 Self {
571 entrypoint_layout: rock_layout,
572 ..self
573 }
574 }
575
576 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
577 Self {
578 generate_luarc: generate.or(self.generate_luarc),
579 ..self
580 }
581 }
582
583 pub fn build(self) -> Result<Config, ConfigError> {
584 let data_dir = self.data_dir.unwrap_or(Config::get_default_data_path()?);
585 let cache_dir = self.cache_dir.unwrap_or(Config::get_default_cache_path()?);
586 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
587
588 let lua_version = self
589 .lua_version
590 .or(crate::lua_installation::detect_installed_lua_version());
591
592 Ok(Config {
593 enable_development_packages: self.enable_development_packages.unwrap_or(false),
594 server: self.server.unwrap_or_else(|| unsafe {
595 Url::parse("https://luarocks.org/").unwrap_unchecked()
596 }),
597 extra_servers: self.extra_servers.unwrap_or_default(),
598 only_sources: self.only_sources,
599 namespace: self.namespace,
600 lua_dir: self.lua_dir,
601 lua_version,
602 user_tree,
603 verbose: self.verbose.unwrap_or(false),
604 no_progress: self.no_progress.unwrap_or(false),
605 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
606 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
607 0 => usize::MAX,
608 max_jobs => max_jobs,
609 },
610 variables: default_variables()
611 .chain(self.variables.unwrap_or_default())
612 .collect(),
613 external_deps: self.external_deps,
614 entrypoint_layout: self.entrypoint_layout,
615 cache_dir,
616 data_dir,
617 vendor_dir: self.vendor_dir,
618 generate_luarc: self.generate_luarc.unwrap_or(true),
619 })
620 }
621}
622
623impl From<Config> for ConfigBuilder {
625 fn from(value: Config) -> Self {
626 ConfigBuilder {
627 enable_development_packages: Some(value.enable_development_packages),
628 server: Some(value.server),
629 extra_servers: Some(value.extra_servers),
630 only_sources: value.only_sources,
631 namespace: value.namespace,
632 lua_dir: value.lua_dir,
633 lua_version: value.lua_version,
634 user_tree: Some(value.user_tree),
635 verbose: Some(value.verbose),
636 no_progress: Some(value.no_progress),
637 timeout: Some(value.timeout),
638 max_jobs: if value.max_jobs == usize::MAX {
639 None
640 } else {
641 Some(value.max_jobs)
642 },
643 variables: Some(value.variables),
644 cache_dir: Some(value.cache_dir),
645 data_dir: Some(value.data_dir),
646 vendor_dir: value.vendor_dir,
647 external_deps: value.external_deps,
648 entrypoint_layout: value.entrypoint_layout,
649 generate_luarc: Some(value.generate_luarc),
650 }
651 }
652}
653
654fn default_variables() -> impl Iterator<Item = (String, String)> {
655 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
656 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
657 vec![
658 ("MAKE".into(), "make".into()),
659 ("CMAKE".into(), "cmake".into()),
660 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
661 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
662 ("CFLAGS".into(), cflags),
663 ("LDFLAGS".into(), ldflags),
664 ("LIBFLAG".into(), utils::default_libflag().into()),
665 ]
666 .into_iter()
667}
668
669fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
670where
671 D: serde::Deserializer<'de>,
672{
673 let s = Option::<String>::deserialize(deserializer)?;
674 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
675 .transpose()
676}
677
678fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
679where
680 S: Serializer,
681{
682 match url {
683 Some(url) => serializer.serialize_some(url.as_str()),
684 None => serializer.serialize_none(),
685 }
686}
687
688fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
689where
690 D: serde::Deserializer<'de>,
691{
692 let s = Option::<Vec<String>>::deserialize(deserializer)?;
693 s.map(|v| {
694 v.into_iter()
695 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
696 .try_collect()
697 })
698 .transpose()
699}
700
701fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
702where
703 S: Serializer,
704{
705 match urls {
706 Some(urls) => {
707 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
708 serializer.serialize_some(&url_strings)
709 }
710 None => serializer.serialize_none(),
711 }
712}