1use std::{
2 collections::HashMap,
3 fs,
4 ops::Index,
5 path::{Component, Path, PathBuf},
6 str::FromStr,
7};
8
9use log::warn;
10
11use super::{
12 external_libraries::{read_external_libraries, ExternalLibrary},
13 gobjects, WorkMode,
14};
15use crate::{
16 analysis::namespaces::{self, Namespace, NsId},
17 config::error::TomlHelper,
18 env::Env,
19 git::{repo_hash, repo_remote_url, toplevel},
20 library::{self, Library},
21 nameutil::set_crate_name_overrides,
22 version::Version,
23};
24
25fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
29 let mut parts: Vec<Component<'_>> = vec![];
30
31 for component in path.as_ref().components() {
32 match (component, parts.last()) {
33 (Component::CurDir, _) | (Component::ParentDir, Some(Component::RootDir)) => {}
34 (Component::ParentDir, None | Some(Component::ParentDir)) => {
35 parts.push(Component::ParentDir);
36 }
37 (Component::ParentDir, Some(_)) => {
38 parts
39 .pop()
40 .expect("Cannot navigate outside of base directory!");
41 }
42 (c, _) => parts.push(c),
43 }
44 }
45 parts.iter().collect()
46}
47
48#[test]
49fn test_normalize_path() {
50 assert_eq!(normalize_path("foo/../bar").as_os_str(), "bar");
51 assert_eq!(normalize_path("foo/./bar").as_os_str(), "foo/bar");
52 assert_eq!(normalize_path("./foo").as_os_str(), "foo");
53 assert_eq!(
54 normalize_path("foo/../bar/baz/../qux").as_os_str(),
55 "bar/qux"
56 );
57 assert_eq!(
58 normalize_path("foo/bar/baz/../../qux").as_os_str(),
59 "foo/qux"
60 );
61 assert_eq!(normalize_path("/foo/../bar").as_os_str(), "/bar");
62 assert_eq!(normalize_path("/../bar").as_os_str(), "/bar");
63 assert_eq!(normalize_path("foo/../../bar").as_os_str(), "../bar");
64}
65
66#[derive(Debug)]
67pub struct GirVersion {
68 pub gir_dir: PathBuf,
69 hash: Option<String>,
70 url: Option<String>,
71}
72
73impl GirVersion {
74 fn new(gir_dir: impl AsRef<Path>) -> Self {
75 let gir_dir = normalize_path(gir_dir);
76 let is_submodule = toplevel(&gir_dir) != Path::new(".").canonicalize().ok();
77 Self {
78 hash: is_submodule.then(|| repo_hash(&gir_dir).unwrap_or_else(|| "???".to_string())),
79 url: is_submodule.then(|| repo_remote_url(&gir_dir)).flatten(),
80 gir_dir,
81 }
82 }
83
84 pub fn get_hash(&self) -> Option<&str> {
85 self.hash.as_deref()
86 }
87
88 pub fn get_repository_url(&self) -> Option<&str> {
89 self.url.as_deref()
90 }
91}
92
93#[derive(Debug)]
94pub struct Config {
95 pub work_mode: WorkMode,
96 pub girs_dirs: Vec<PathBuf>,
97 pub girs_version: Vec<GirVersion>,
99 pub library_name: String,
100 pub library_version: String,
101 pub target_path: PathBuf,
102 pub auto_path: PathBuf,
104 pub doc_target_path: PathBuf,
105 pub external_libraries: Vec<ExternalLibrary>,
106 pub objects: gobjects::GObjects,
107 pub min_cfg_version: Version,
108 pub use_gi_docgen: bool,
109 pub make_backup: bool,
110 pub generate_safety_asserts: bool,
111 pub deprecate_by_min_version: bool,
112 pub show_statistics: bool,
113 pub concurrency: library::Concurrency,
114 pub single_version_file: Option<PathBuf>,
115 pub trust_return_value_nullability: bool,
116 pub disable_format: bool,
117 pub split_build_rs: bool,
118 pub extra_versions: Vec<Version>,
119 pub lib_version_overrides: HashMap<Version, Version>,
120 pub feature_dependencies: HashMap<Version, Vec<String>>,
121 pub external_docs_url: Option<String>,
125}
126
127impl Config {
128 pub fn new<'a, S, W>(
129 config_file: S,
130 work_mode: W,
131 girs_dirs: &[String],
132 library_name: S,
133 library_version: S,
134 target_path: S,
135 doc_target_path: S,
136 make_backup: bool,
137 show_statistics: bool,
138 disable_format: bool,
139 ) -> Result<Self, String>
140 where
141 S: Into<Option<&'a str>>,
142 W: Into<Option<WorkMode>>,
143 {
144 let config_file: PathBuf = match config_file.into() {
145 Some("") | None => "Gir.toml",
146 Some(a) => a,
147 }
148 .into();
149
150 let config_dir = match config_file.parent() {
151 Some(path) => path.into(),
152 None => PathBuf::new(),
153 };
154
155 let toml = match read_toml(&config_file) {
156 Ok(toml) => toml,
157 Err(e) => {
158 return Err(format!(
159 "Error while reading \"{}\": {}",
160 config_file.display(),
161 e
162 ))
163 }
164 };
165
166 let overrides = read_crate_name_overrides(&toml);
167 if !overrides.is_empty() {
168 set_crate_name_overrides(overrides);
169 }
170
171 let work_mode = match work_mode.into() {
172 Some(w) => w,
173 None => {
174 let s = match toml.lookup_str("options.work_mode", "No options.work_mode") {
175 Ok(s) => s,
176 Err(e) => {
177 return Err(format!(
178 "Invalid toml file \"{}\": {}",
179 config_file.display(),
180 e
181 ))
182 }
183 };
184 WorkMode::from_str(s)?
185 }
186 };
187
188 let mut girs_dirs: Vec<PathBuf> = girs_dirs
189 .iter()
190 .filter(|x| !x.is_empty())
191 .map(|x| PathBuf::from(&x))
192 .collect();
193 if girs_dirs.is_empty() {
194 let dirs =
195 toml.lookup_vec("options.girs_directories", "No options.girs_directories")?;
196 for dir in dirs {
197 let dir = dir.as_str().ok_or_else(|| {
198 "options.girs_dirs expected to be array of string".to_string()
199 })?;
200 girs_dirs.push(config_dir.join(dir));
201 }
202 }
203 let mut girs_version = girs_dirs.iter().map(GirVersion::new).collect::<Vec<_>>();
204 girs_version.sort_by(|a, b| a.gir_dir.partial_cmp(&b.gir_dir).unwrap());
205
206 let (library_name, library_version) = match (library_name.into(), library_version.into()) {
207 (Some(""), Some("")) | (None, None) => (
208 toml.lookup_str("options.library", "No options.library")?
209 .to_owned(),
210 toml.lookup_str("options.version", "No options.version")?
211 .to_owned(),
212 ),
213 (Some(""), Some(_)) | (Some(_), Some("")) | (None, Some(_)) | (Some(_), None) => {
214 return Err("Library and version can not be specified separately".to_owned())
215 }
216 (Some(a), Some(b)) => (a.to_owned(), b.to_owned()),
217 };
218
219 let target_path: PathBuf = match target_path.into() {
220 Some("") | None => {
221 let path = toml.lookup_str("options.target_path", "No target path specified")?;
222 config_dir.join(path)
223 }
224 Some(a) => a.into(),
225 };
226
227 let generate_builder: bool = toml
228 .lookup("options.generate_builder")
229 .and_then(|a| a.as_bool())
230 .unwrap_or(false);
231
232 let auto_path = match toml.lookup("options.auto_path") {
233 Some(p) => target_path.join(p.as_result_str("options.auto_path")?),
234 None if work_mode == WorkMode::Normal => target_path.join("src").join("auto"),
235 None => target_path.join("src"),
236 };
237 if work_mode == WorkMode::Normal && auto_path.exists() {
238 std::fs::remove_dir_all(&auto_path)
239 .map_err(|e| format!("remove_dir_all failed: {e:?}"))?;
240 }
241
242 let doc_target_path: PathBuf = match doc_target_path.into() {
243 Some("") | None => match toml.lookup("options.doc_target_path") {
244 Some(p) => config_dir.join(p.as_result_str("options.doc_target_path")?),
245 None => target_path.join("vendor.md"),
246 },
247 Some(p) => config_dir.join(p),
248 };
249
250 let concurrency = match toml.lookup("options.concurrency") {
251 Some(v) => v.as_result_str("options.concurrency")?.parse()?,
252 None => Default::default(),
253 };
254
255 let trust_return_value_nullability =
256 match toml.lookup("options.trust_return_value_nullability") {
257 Some(v) => v.as_result_bool("options.trust_return_value_nullability")?,
258 None => false,
259 };
260
261 let mut objects = toml
264 .lookup("object")
265 .map(|t| {
266 gobjects::parse_toml(
267 t,
268 concurrency,
269 generate_builder,
270 trust_return_value_nullability,
271 )
272 })
273 .unwrap_or_default();
274 gobjects::parse_status_shorthands(
275 &mut objects,
276 &toml,
277 concurrency,
278 generate_builder,
279 trust_return_value_nullability,
280 );
281
282 let external_libraries = read_external_libraries(&toml)?;
283
284 let min_cfg_version = match toml.lookup("options.min_cfg_version") {
285 Some(v) => v.as_result_str("options.min_cfg_version")?.parse()?,
286 None => Default::default(),
287 };
288
289 let use_gi_docgen = match toml.lookup("options.use_gi_docgen") {
290 Some(v) => v.as_result_bool("options.use_gi_docgen")?,
291 None => false,
292 };
293
294 let generate_safety_asserts = match toml.lookup("options.generate_safety_asserts") {
295 Some(v) => v.as_result_bool("options.generate_safety_asserts")?,
296 None => false,
297 };
298
299 let deprecate_by_min_version = match toml.lookup("options.deprecate_by_min_version") {
300 Some(v) => v.as_result_bool("options.deprecate_by_min_version")?,
301 None => false,
302 };
303
304 let single_version_file = match toml.lookup("options.single_version_file") {
305 Some(v) => match v.as_result_bool("options.single_version_file") {
306 Ok(false) => None,
307 Ok(true) => Some(make_single_version_file(None, &target_path)),
308 Err(_) => match v.as_str() {
309 Some(p) => Some(make_single_version_file(Some(p), &target_path)),
310 None => return Err("single_version_file must be bool or string path".into()),
311 },
312 },
313 None => None,
314 };
315
316 let disable_format: bool = if disable_format {
317 true
318 } else {
319 match toml.lookup("options.disable_format") {
320 Some(v) => v.as_result_bool("options.disable_format")?,
321 None => true,
322 }
323 };
324
325 let split_build_rs = match toml.lookup("options.split_build_rs") {
326 Some(v) => v.as_result_bool("options.split_build_rs")?,
327 None => false,
328 };
329
330 let extra_versions = read_extra_versions(&toml)?;
331 let lib_version_overrides = read_lib_version_overrides(&toml)?;
332 let feature_dependencies = read_feature_dependencies(&toml)?;
333 let external_docs_url = read_external_docs_url(&toml)?;
334
335 Ok(Self {
336 work_mode,
337 girs_dirs,
338 girs_version,
339 library_name,
340 library_version,
341 target_path,
342 auto_path,
343 doc_target_path,
344 external_libraries,
345 objects,
346 min_cfg_version,
347 use_gi_docgen,
348 make_backup,
349 generate_safety_asserts,
350 deprecate_by_min_version,
351 show_statistics,
352 concurrency,
353 single_version_file,
354 trust_return_value_nullability,
355 disable_format,
356 split_build_rs,
357 extra_versions,
358 lib_version_overrides,
359 feature_dependencies,
360 external_docs_url,
361 })
362 }
363
364 pub fn library_full_name(&self) -> String {
365 format!("{}-{}", self.library_name, self.library_version)
366 }
367
368 pub fn filter_version(&self, version: Option<Version>) -> Option<Version> {
369 version.and_then(|v| {
370 if v > self.min_cfg_version {
371 Some(v)
372 } else {
373 None
374 }
375 })
376 }
377
378 pub fn find_ext_library(&self, namespace: &Namespace) -> Option<&ExternalLibrary> {
379 self.external_libraries
380 .iter()
381 .find(|lib| lib.crate_name == namespace.crate_name)
382 }
383
384 pub fn min_required_version(&self, env: &Env, ns_id: Option<NsId>) -> Option<Version> {
385 let ns_id = ns_id.unwrap_or(namespaces::MAIN);
386 if ns_id == namespaces::MAIN {
387 Some(env.config.min_cfg_version)
388 } else {
389 let namespace = env.namespaces.index(ns_id);
390 self.find_ext_library(namespace)
391 .and_then(|lib| lib.min_version)
392 }
393 }
394
395 pub fn resolve_type_ids(&mut self, library: &Library) {
396 gobjects::resolve_type_ids(&mut self.objects, library);
397 }
398
399 pub fn check_disable_format(&mut self) {
400 if !self.disable_format && !crate::fmt::check_fmt() {
401 warn!("Formatter not found, options.disable_format set to true");
402 self.disable_format = true;
403 }
404 }
405}
406
407fn read_toml<P: AsRef<Path>>(filename: P) -> Result<toml::Value, String> {
408 if !filename.as_ref().is_file() {
409 return Err("Config don't exists or not file".to_owned());
410 }
411 let input = fs::read(&filename)
412 .map_err(|e| format!("Failed to read file \"{:?}\": {}", filename.as_ref(), e))?;
413
414 let input = String::from_utf8(input)
415 .map_err(|e| format!("File is not valid UTF-8 \"{:?}\": {}", filename.as_ref(), e))?;
416
417 toml::from_str(&input).map_err(|e| {
418 format!(
419 "Invalid toml format in \"{}\": {}",
420 filename.as_ref().display(),
421 e
422 )
423 })
424}
425
426fn make_single_version_file(configured: Option<&str>, target_path: &Path) -> PathBuf {
427 let file_dir = match configured {
428 None | Some("") => target_path.join("src").join("auto"),
429 Some(path) => target_path.join(path),
430 };
431
432 if file_dir.extension().is_some() {
433 file_dir
434 } else {
435 file_dir.join("versions.txt")
436 }
437}
438
439fn read_crate_name_overrides(toml: &toml::Value) -> HashMap<String, String> {
440 let mut overrides = HashMap::new();
441 if let Some(a) = toml
442 .lookup("crate_name_overrides")
443 .and_then(toml::Value::as_table)
444 {
445 for (key, value) in a {
446 if let Some(s) = value.as_str() {
447 overrides.insert(key.clone(), s.to_string());
448 }
449 }
450 };
451 overrides
452}
453
454fn read_extra_versions(toml: &toml::Value) -> Result<Vec<Version>, String> {
455 match toml.lookup("options.extra_versions") {
456 Some(a) => a
457 .as_result_vec("options.extra_versions")?
458 .iter()
459 .map(|v| {
460 v.as_str().ok_or_else(|| {
461 "options.extra_versions expected to be array of string".to_string()
462 })
463 })
464 .map(|s| s.and_then(str::parse))
465 .collect(),
466 None => Ok(Vec::new()),
467 }
468}
469
470fn read_lib_version_overrides(toml: &toml::Value) -> Result<HashMap<Version, Version>, String> {
471 let v = match toml.lookup("lib_version_overrides") {
472 Some(a) => a.as_result_vec("lib_version_overrides")?,
473 None => return Ok(Default::default()),
474 };
475
476 let mut map = HashMap::with_capacity(v.len());
477 for o in v {
478 let cfg = o
479 .lookup_str("version", "No version in lib_version_overrides")?
480 .parse()?;
481 let lib = o
482 .lookup_str("lib_version", "No lib_version in lib_version_overrides")?
483 .parse()?;
484 map.insert(cfg, lib);
485 }
486
487 Ok(map)
488}
489
490fn read_feature_dependencies(toml: &toml::Value) -> Result<HashMap<Version, Vec<String>>, String> {
491 let v = match toml.lookup("feature_dependencies") {
492 Some(a) => a.as_result_vec("feature_dependencies")?,
493 None => return Ok(Default::default()),
494 };
495
496 let mut map = HashMap::with_capacity(v.len());
497 for o in v {
498 let cfg = o
499 .lookup_str("version", "No version in feature_dependencies")?
500 .parse()?;
501 let dependencies: Result<Vec<String>, String> = o
502 .lookup_vec("dependencies", "No dependencies in feature_dependencies")?
503 .iter()
504 .map(|v| {
505 v.as_str()
506 .ok_or_else(|| {
507 "feature_dependencies.dependencies expected to be array of string"
508 .to_string()
509 })
510 .map(str::to_owned)
511 })
512 .collect();
513 map.insert(cfg, dependencies?);
514 }
515
516 Ok(map)
517}
518
519fn read_external_docs_url(toml: &toml::Value) -> Result<Option<String>, String> {
520 Ok(
521 if let Some(value) = toml.lookup("options.external_docs_url") {
522 let value = value.as_result_str("options.external_docs_url")?;
523 Some(value.to_string())
524 } else {
525 None
526 },
527 )
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_make_single_version_file() {
536 let target_path = Path::new("/tmp/glib");
537 assert_eq!(
538 make_single_version_file(None, target_path),
539 PathBuf::from("/tmp/glib/src/auto/versions.txt")
540 );
541 assert_eq!(
542 make_single_version_file(Some(""), target_path),
543 PathBuf::from("/tmp/glib/src/auto/versions.txt")
544 );
545 assert_eq!(
546 make_single_version_file(Some("src"), target_path),
547 PathBuf::from("/tmp/glib/src/versions.txt")
548 );
549 assert_eq!(
550 make_single_version_file(Some("src/vers.txt"), target_path),
551 PathBuf::from("/tmp/glib/src/vers.txt")
552 );
553 assert_eq!(
554 make_single_version_file(Some("."), target_path),
555 PathBuf::from("/tmp/glib/versions.txt")
556 );
557 assert_eq!(
558 make_single_version_file(Some("./_vers.dat"), target_path),
559 PathBuf::from("/tmp/glib/_vers.dat")
560 );
561 }
562}