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