libgir/config/
config.rs

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
25/// Performs canonicalization by removing `foo/../` and `./` components
26/// from `path`, without hitting the file system. It does not turn relative
27/// paths into absolute paths.
28fn 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    // Version in girs_dirs, detected by git
98    pub girs_version: Vec<GirVersion>,
99    pub library_name: String,
100    pub library_version: String,
101    pub target_path: PathBuf,
102    /// Path where files generated in normal and sys mode
103    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    /// An url that will be inserted into the docs as link that links
122    /// to another doc source, for example when builds on docs.rs
123    /// are limited due to license issues.
124    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        // options.concurrency is the default of all objects if nothing
262        // else is configured
263        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}