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    WorkMode,
13    external_libraries::{ExternalLibrary, read_external_libraries},
14    gobjects,
15};
16use crate::{
17    analysis::namespaces::{self, Namespace, NsId},
18    config::error::TomlHelper,
19    env::Env,
20    git::{repo_hash, repo_remote_url, toplevel},
21    library::{self, Library},
22    nameutil::set_crate_name_overrides,
23    version::Version,
24};
25
26/// Performs canonicalization by removing `foo/../` and `./` components
27/// from `path`, without hitting the file system. It does not turn relative
28/// paths into absolute paths.
29fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
30    let mut parts: Vec<Component<'_>> = vec![];
31
32    for component in path.as_ref().components() {
33        match (component, parts.last()) {
34            (Component::CurDir, _) | (Component::ParentDir, Some(Component::RootDir)) => {}
35            (Component::ParentDir, None | Some(Component::ParentDir)) => {
36                parts.push(Component::ParentDir);
37            }
38            (Component::ParentDir, Some(_)) => {
39                parts
40                    .pop()
41                    .expect("Cannot navigate outside of base directory!");
42            }
43            (c, _) => parts.push(c),
44        }
45    }
46    parts.iter().collect()
47}
48
49#[test]
50fn test_normalize_path() {
51    assert_eq!(normalize_path("foo/../bar").as_os_str(), "bar");
52    assert_eq!(normalize_path("foo/./bar").as_os_str(), "foo/bar");
53    assert_eq!(normalize_path("./foo").as_os_str(), "foo");
54    assert_eq!(
55        normalize_path("foo/../bar/baz/../qux").as_os_str(),
56        "bar/qux"
57    );
58    assert_eq!(
59        normalize_path("foo/bar/baz/../../qux").as_os_str(),
60        "foo/qux"
61    );
62    assert_eq!(normalize_path("/foo/../bar").as_os_str(), "/bar");
63    assert_eq!(normalize_path("/../bar").as_os_str(), "/bar");
64    assert_eq!(normalize_path("foo/../../bar").as_os_str(), "../bar");
65}
66
67#[derive(Debug)]
68pub struct GirVersion {
69    pub gir_dir: PathBuf,
70    hash: Option<String>,
71    url: Option<String>,
72}
73
74impl GirVersion {
75    fn new(gir_dir: impl AsRef<Path>) -> Self {
76        let gir_dir = normalize_path(gir_dir);
77        let is_submodule = toplevel(&gir_dir) != Path::new(".").canonicalize().ok();
78        Self {
79            hash: is_submodule.then(|| repo_hash(&gir_dir).unwrap_or_else(|| "???".to_string())),
80            url: is_submodule.then(|| repo_remote_url(&gir_dir)).flatten(),
81            gir_dir,
82        }
83    }
84
85    pub fn get_hash(&self) -> Option<&str> {
86        self.hash.as_deref()
87    }
88
89    pub fn get_repository_url(&self) -> Option<&str> {
90        self.url.as_deref()
91    }
92}
93
94#[derive(Debug)]
95pub struct Config {
96    pub work_mode: WorkMode,
97    pub girs_dirs: Vec<PathBuf>,
98    // Version in girs_dirs, detected by git
99    pub girs_version: Vec<GirVersion>,
100    pub library_name: String,
101    pub library_version: String,
102    pub target_path: PathBuf,
103    /// Path where files generated in normal and sys mode
104    pub auto_path: PathBuf,
105    pub doc_target_path: PathBuf,
106    pub external_libraries: Vec<ExternalLibrary>,
107    pub objects: gobjects::GObjects,
108    pub min_cfg_version: Version,
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 generate_safety_asserts = match toml.lookup("options.generate_safety_asserts") {
290            Some(v) => v.as_result_bool("options.generate_safety_asserts")?,
291            None => false,
292        };
293
294        let deprecate_by_min_version = match toml.lookup("options.deprecate_by_min_version") {
295            Some(v) => v.as_result_bool("options.deprecate_by_min_version")?,
296            None => false,
297        };
298
299        let single_version_file = match toml.lookup("options.single_version_file") {
300            Some(v) => match v.as_result_bool("options.single_version_file") {
301                Ok(false) => None,
302                Ok(true) => Some(make_single_version_file(None, &target_path)),
303                Err(_) => match v.as_str() {
304                    Some(p) => Some(make_single_version_file(Some(p), &target_path)),
305                    None => return Err("single_version_file must be bool or string path".into()),
306                },
307            },
308            None => None,
309        };
310
311        let disable_format: bool = if disable_format {
312            true
313        } else {
314            match toml.lookup("options.disable_format") {
315                Some(v) => v.as_result_bool("options.disable_format")?,
316                None => true,
317            }
318        };
319
320        let split_build_rs = match toml.lookup("options.split_build_rs") {
321            Some(v) => v.as_result_bool("options.split_build_rs")?,
322            None => false,
323        };
324
325        let extra_versions = read_extra_versions(&toml)?;
326        let lib_version_overrides = read_lib_version_overrides(&toml)?;
327        let feature_dependencies = read_feature_dependencies(&toml)?;
328        let external_docs_url = read_external_docs_url(&toml)?;
329
330        Ok(Self {
331            work_mode,
332            girs_dirs,
333            girs_version,
334            library_name,
335            library_version,
336            target_path,
337            auto_path,
338            doc_target_path,
339            external_libraries,
340            objects,
341            min_cfg_version,
342            make_backup,
343            generate_safety_asserts,
344            deprecate_by_min_version,
345            show_statistics,
346            concurrency,
347            single_version_file,
348            trust_return_value_nullability,
349            disable_format,
350            split_build_rs,
351            extra_versions,
352            lib_version_overrides,
353            feature_dependencies,
354            external_docs_url,
355        })
356    }
357
358    pub fn library_full_name(&self) -> String {
359        format!("{}-{}", self.library_name, self.library_version)
360    }
361
362    pub fn filter_version(&self, version: Option<Version>) -> Option<Version> {
363        version.and_then(|v| {
364            if v > self.min_cfg_version {
365                Some(v)
366            } else {
367                None
368            }
369        })
370    }
371
372    pub fn find_ext_library(&self, namespace: &Namespace) -> Option<&ExternalLibrary> {
373        self.external_libraries
374            .iter()
375            .find(|lib| lib.crate_name == namespace.crate_name)
376    }
377
378    pub fn min_required_version(&self, env: &Env, ns_id: Option<NsId>) -> Option<Version> {
379        let ns_id = ns_id.unwrap_or(namespaces::MAIN);
380        if ns_id == namespaces::MAIN {
381            Some(env.config.min_cfg_version)
382        } else {
383            let namespace = env.namespaces.index(ns_id);
384            self.find_ext_library(namespace)
385                .and_then(|lib| lib.min_version)
386        }
387    }
388
389    pub fn resolve_type_ids(&mut self, library: &Library) {
390        gobjects::resolve_type_ids(&mut self.objects, library);
391    }
392
393    pub fn check_disable_format(&mut self) {
394        if !self.disable_format && !crate::fmt::check_fmt() {
395            warn!("Formatter not found, options.disable_format set to true");
396            self.disable_format = true;
397        }
398    }
399}
400
401fn read_toml<P: AsRef<Path>>(filename: P) -> Result<toml::Value, String> {
402    if !filename.as_ref().is_file() {
403        return Err("Config don't exists or not file".to_owned());
404    }
405    let input = fs::read(&filename)
406        .map_err(|e| format!("Failed to read file \"{:?}\": {}", filename.as_ref(), e))?;
407
408    let input = String::from_utf8(input)
409        .map_err(|e| format!("File is not valid UTF-8 \"{:?}\": {}", filename.as_ref(), e))?;
410
411    toml::from_str(&input).map_err(|e| {
412        format!(
413            "Invalid toml format in \"{}\": {}",
414            filename.as_ref().display(),
415            e
416        )
417    })
418}
419
420fn make_single_version_file(configured: Option<&str>, target_path: &Path) -> PathBuf {
421    let file_dir = match configured {
422        None | Some("") => target_path.join("src").join("auto"),
423        Some(path) => target_path.join(path),
424    };
425
426    if file_dir.extension().is_some() {
427        file_dir
428    } else {
429        file_dir.join("versions.txt")
430    }
431}
432
433fn read_crate_name_overrides(toml: &toml::Value) -> HashMap<String, String> {
434    let mut overrides = HashMap::new();
435    if let Some(a) = toml
436        .lookup("crate_name_overrides")
437        .and_then(toml::Value::as_table)
438    {
439        for (key, value) in a {
440            if let Some(s) = value.as_str() {
441                overrides.insert(key.clone(), s.to_string());
442            }
443        }
444    };
445    overrides
446}
447
448fn read_extra_versions(toml: &toml::Value) -> Result<Vec<Version>, String> {
449    match toml.lookup("options.extra_versions") {
450        Some(a) => a
451            .as_result_vec("options.extra_versions")?
452            .iter()
453            .map(|v| {
454                v.as_str().ok_or_else(|| {
455                    "options.extra_versions expected to be array of string".to_string()
456                })
457            })
458            .map(|s| s.and_then(str::parse))
459            .collect(),
460        None => Ok(Vec::new()),
461    }
462}
463
464fn read_lib_version_overrides(toml: &toml::Value) -> Result<HashMap<Version, Version>, String> {
465    let v = match toml.lookup("lib_version_overrides") {
466        Some(a) => a.as_result_vec("lib_version_overrides")?,
467        None => return Ok(Default::default()),
468    };
469
470    let mut map = HashMap::with_capacity(v.len());
471    for o in v {
472        let cfg = o
473            .lookup_str("version", "No version in lib_version_overrides")?
474            .parse()?;
475        let lib = o
476            .lookup_str("lib_version", "No lib_version in lib_version_overrides")?
477            .parse()?;
478        map.insert(cfg, lib);
479    }
480
481    Ok(map)
482}
483
484fn read_feature_dependencies(toml: &toml::Value) -> Result<HashMap<Version, Vec<String>>, String> {
485    let v = match toml.lookup("feature_dependencies") {
486        Some(a) => a.as_result_vec("feature_dependencies")?,
487        None => return Ok(Default::default()),
488    };
489
490    let mut map = HashMap::with_capacity(v.len());
491    for o in v {
492        let cfg = o
493            .lookup_str("version", "No version in feature_dependencies")?
494            .parse()?;
495        let dependencies: Result<Vec<String>, String> = o
496            .lookup_vec("dependencies", "No dependencies in feature_dependencies")?
497            .iter()
498            .map(|v| {
499                v.as_str()
500                    .ok_or_else(|| {
501                        "feature_dependencies.dependencies expected to be array of string"
502                            .to_string()
503                    })
504                    .map(str::to_owned)
505            })
506            .collect();
507        map.insert(cfg, dependencies?);
508    }
509
510    Ok(map)
511}
512
513fn read_external_docs_url(toml: &toml::Value) -> Result<Option<String>, String> {
514    Ok(
515        if let Some(value) = toml.lookup("options.external_docs_url") {
516            let value = value.as_result_str("options.external_docs_url")?;
517            Some(value.to_string())
518        } else {
519            None
520        },
521    )
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[test]
529    fn test_make_single_version_file() {
530        let target_path = Path::new("/tmp/glib");
531        assert_eq!(
532            make_single_version_file(None, target_path),
533            PathBuf::from("/tmp/glib/src/auto/versions.txt")
534        );
535        assert_eq!(
536            make_single_version_file(Some(""), target_path),
537            PathBuf::from("/tmp/glib/src/auto/versions.txt")
538        );
539        assert_eq!(
540            make_single_version_file(Some("src"), target_path),
541            PathBuf::from("/tmp/glib/src/versions.txt")
542        );
543        assert_eq!(
544            make_single_version_file(Some("src/vers.txt"), target_path),
545            PathBuf::from("/tmp/glib/src/vers.txt")
546        );
547        assert_eq!(
548            make_single_version_file(Some("."), target_path),
549            PathBuf::from("/tmp/glib/versions.txt")
550        );
551        assert_eq!(
552            make_single_version_file(Some("./_vers.dat"), target_path),
553            PathBuf::from("/tmp/glib/_vers.dat")
554        );
555    }
556}