Skip to main content

tectonic/
config.rs

1// Copyright 2016-2021 the Tectonic Project
2// Licensed under the MIT License.
3
4//! User configuration settings for the Tectonic engine.
5//!
6//! Because Tectonic has a goal of having a high level of reproducibility, we
7//! aim to *avoid* persistent configuration options as much as possible. But,
8//! we at least need a mechanism for specifying the default bundle to use when
9//! running the command-line client. So we begrudgingly have a *little*
10//! configuration.
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14use std::{
15    path::PathBuf,
16    sync::atomic::{AtomicBool, Ordering},
17};
18use tectonic_bundles::{detect_bundle, Bundle};
19use tectonic_io_base::app_dirs;
20
21use crate::errors::{ErrorKind, Result};
22
23/// Awesome hack time!!!
24///
25/// This is part of the "test mode" described in the `test_util` module. When
26/// test mode is activated in this module, the `default_bundle()` and
27/// `format_cache_path()` functions return results pointing to the test asset
28/// tree, rather than whatever the user has actually configured.
29static CONFIG_TEST_MODE_ACTIVATED: AtomicBool = AtomicBool::new(false);
30
31#[doc(hidden)]
32pub fn activate_config_test_mode(forced: bool) {
33    CONFIG_TEST_MODE_ACTIVATED.store(forced, Ordering::SeqCst);
34}
35
36#[doc(hidden)]
37pub fn is_config_test_mode_activated() -> bool {
38    CONFIG_TEST_MODE_ACTIVATED.load(Ordering::SeqCst)
39}
40
41#[doc(hidden)]
42pub fn is_test_bundle_wanted(bundle: Option<String>) -> bool {
43    if !is_config_test_mode_activated() {
44        return false;
45    }
46    match bundle {
47        None => true,
48        Some(x) if x.contains("test-bundle://") => true,
49        _ => false,
50    }
51}
52
53#[doc(hidden)]
54pub fn maybe_return_test_bundle(bundle: Option<String>) -> Result<Box<dyn Bundle>> {
55    if is_test_bundle_wanted(bundle) {
56        Ok(Box::<crate::test_util::TestBundle>::default())
57    } else {
58        Err(ErrorKind::Msg("not asking for the default test bundle".to_owned()).into())
59    }
60}
61
62/// Top-level persistent configuration required for the engine
63#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
64pub struct PersistentConfig {
65    default_bundles: Vec<BundleInfo>,
66}
67
68/// Information about a default bundle
69#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
70pub struct BundleInfo {
71    url: String,
72}
73
74impl PersistentConfig {
75    #[cfg(feature = "serialization")]
76    /// Open the per-user configuration file.
77    ///
78    /// This file is stored in TOML format. If the configuration file does not
79    /// exist, no error is signaled — instead, a basic default configuration
80    /// is returned. In this case, if `auto_create_config_file` is true, the
81    /// configuration file (and the directory containing it) will be
82    /// automatically created, filling in the default configuration. If it is
83    /// false, the default configuration is returned and the filesystem is not
84    /// modified.
85    pub fn open(auto_create_config_file: bool) -> Result<PersistentConfig> {
86        use std::{
87            fs::File,
88            io::{ErrorKind as IoErrorKind, Read, Write},
89        };
90
91        let mut cfg_path = if auto_create_config_file {
92            app_dirs::ensure_user_config()?
93        } else {
94            app_dirs::get_user_config()?
95        };
96        cfg_path.push("config.toml");
97
98        let config = match File::open(&cfg_path) {
99            Ok(mut f) => {
100                let mut buf = String::new();
101                f.read_to_string(&mut buf)?;
102                toml::from_str(&buf)?
103            }
104            Err(e) => {
105                if e.kind() == IoErrorKind::NotFound {
106                    // Config file didn't exist -- that's OK.
107                    let config = PersistentConfig::default();
108                    if auto_create_config_file {
109                        let mut f = File::create(&cfg_path)?;
110                        write!(f, "{}", toml::to_string(&config)?)?;
111                    }
112                    config
113                } else {
114                    // Uh oh, unexpected error reading the config file.
115                    return Err(e.into());
116                }
117            }
118        };
119
120        Ok(config)
121    }
122
123    #[cfg(not(feature = "serialization"))]
124    /// Return a default configuration structure.
125    ///
126    /// In most builds of Tectonic, this function reads a per-user
127    /// configuration file and returns it. However, this version of Tectonic
128    /// has been built without the `serde` feature, so it cannot deserialize
129    /// the file. Therefore, this function always returns the default
130    /// configuration.
131    pub fn open(_auto_create_config_file: bool) -> Result<PersistentConfig> {
132        Ok(PersistentConfig::default())
133    }
134
135    /// Get the default bundle URL for this configuration
136    pub fn default_bundle_loc(&self) -> &str {
137        &self.default_bundles[0].url
138    }
139
140    /// Attempt to open the default bundle
141    pub fn default_bundle(&self, only_cached: bool) -> Result<Box<dyn Bundle>> {
142        if CONFIG_TEST_MODE_ACTIVATED.load(Ordering::SeqCst) {
143            let bundle = crate::test_util::TestBundle::default();
144            return Ok(Box::new(bundle));
145        }
146
147        if self.default_bundles.len() != 1 {
148            return Err(ErrorKind::Msg(
149                "exactly one default_bundle item must be specified (for now)".to_owned(),
150            )
151            .into());
152        }
153
154        Ok(
155            detect_bundle(self.default_bundles[0].url.to_owned(), only_cached, None)
156                .unwrap()
157                .unwrap(),
158        )
159    }
160
161    /// Get the cache directory to use for format files
162    pub fn format_cache_path(&self) -> Result<PathBuf> {
163        if is_config_test_mode_activated() {
164            Ok(crate::test_util::test_path(&[]))
165        } else {
166            Ok(app_dirs::get_user_cache_dir("formats")?)
167        }
168    }
169}
170
171impl Default for PersistentConfig {
172    fn default() -> Self {
173        let url = tectonic_bundles::get_fallback_bundle_url(tectonic_engine_xetex::FORMAT_SERIAL);
174
175        PersistentConfig {
176            default_bundles: vec![BundleInfo { url }],
177        }
178    }
179}