Skip to main content

tectonic/
docmodel.rs

1// Copyright 2020-2021 the Tectonic Project
2// Licensed under the MIT License.
3
4//! Connecting the Tectonic document model to the engines.
5//!
6//! This module extends the document model types provided by the
7//! `tectonic_docmodel` crate with the actual document-processing capabilities
8//! provided by the processing engines.
9
10use std::{fmt::Write as FmtWrite, fs, io, path::PathBuf};
11use tectonic_bridge_core::SecuritySettings;
12use tectonic_bundles::{detect_bundle, Bundle};
13use tectonic_docmodel::{
14    document::{BuildTargetType, Document, InputFile},
15    workspace::{Workspace, WorkspaceCreator},
16};
17use tectonic_geturl::{DefaultBackend, GetUrlBackend};
18
19use crate::{
20    config, ctry,
21    driver::{OutputFormat, PassSetting, ProcessingSessionBuilder},
22    errors::{ErrorKind, Result},
23    status::StatusBackend,
24    test_util, tt_note,
25    unstable_opts::UnstableOptions,
26};
27
28/// Options for setting up [`Document`] instances with the driver
29#[derive(Clone, Debug, Default)]
30pub struct DocumentSetupOptions {
31    /// Disable requests to the network, if the document’s bundle happens to be
32    /// network-based.
33    only_cached: bool,
34
35    /// Security settings for engine features.
36    security: SecuritySettings,
37
38    /// Ensure a deterministic build environment.
39    deterministic_mode: bool,
40}
41
42impl DocumentSetupOptions {
43    /// Create a new set of document setup options with custom security
44    /// settings.
45    pub fn new_with_security(security: SecuritySettings) -> Self {
46        DocumentSetupOptions {
47            only_cached: false,
48            deterministic_mode: false,
49            security,
50        }
51    }
52
53    /// Specify whether any requests to the network will be made for bundle
54    /// resources.
55    ///
56    /// If the document’s backing bundle is not network-based, this setting will
57    /// have no effect.
58    pub fn only_cached(&mut self, s: bool) -> &mut Self {
59        self.only_cached = s;
60        self
61    }
62
63    /// Specify whether we want to ensure a deterministic build environment.
64    pub fn deterministic_mode(&mut self, s: bool) -> &mut Self {
65        self.deterministic_mode = s;
66        self
67    }
68}
69
70/// Extension methods for [`Document`].
71pub trait DocumentExt {
72    /// Get the bundle used by this document.
73    ///
74    /// This parses [`Document::bundle_loc`] and turns it into the appropriate
75    /// bundle backend.
76    fn bundle(&self, setup_options: &DocumentSetupOptions) -> Result<Box<dyn Bundle>>;
77
78    /// Set up a [`ProcessingSessionBuilder`] for one of the outputs.
79    ///
80    /// The *output_profile* argument gives the name of the document’s output profile to
81    /// use.
82    fn setup_session(
83        &self,
84        output_profile: &str,
85        setup_options: &DocumentSetupOptions,
86        status: &mut dyn StatusBackend,
87    ) -> Result<ProcessingSessionBuilder>;
88}
89
90impl DocumentExt for Document {
91    fn bundle(&self, setup_options: &DocumentSetupOptions) -> Result<Box<dyn Bundle>> {
92        // Load test bundle
93        if config::is_config_test_mode_activated() {
94            let bundle = test_util::TestBundle::default();
95            return Ok(Box::new(bundle));
96        }
97
98        let d = detect_bundle(self.bundle_loc.clone(), setup_options.only_cached, None)?;
99
100        match d {
101            Some(b) => Ok(b),
102            None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Could not get bundle").into()),
103        }
104    }
105
106    fn setup_session(
107        &self,
108        output_profile: &str,
109        setup_options: &DocumentSetupOptions,
110        status: &mut dyn StatusBackend,
111    ) -> Result<ProcessingSessionBuilder> {
112        let profile = self.outputs.get(output_profile).ok_or_else(|| {
113            ErrorKind::Msg(format!(
114                "unrecognized output profile name \"{output_profile}\""
115            ))
116        })?;
117
118        let output_format = match profile.target_type {
119            BuildTargetType::Html => OutputFormat::Html,
120            BuildTargetType::Pdf => OutputFormat::Pdf,
121        };
122
123        let mut input_buffer = String::new();
124
125        for input in &profile.inputs {
126            match input {
127                InputFile::Inline(s) => {
128                    writeln!(input_buffer, "{s}")?;
129                }
130                InputFile::File(f) => {
131                    writeln!(input_buffer, "\\input{{{f}}}")?;
132                }
133            };
134        }
135
136        let mut sess_builder =
137            ProcessingSessionBuilder::new_with_security(setup_options.security.clone());
138
139        // Interpret all extra paths as relative to our working dir
140        let extra_paths: Vec<PathBuf> = self
141            .extra_paths
142            .iter()
143            .map(|x| self.src_dir().join(x))
144            .collect();
145
146        sess_builder
147            .output_format(output_format)
148            .format_name(&profile.tex_format)
149            .build_date_from_env(setup_options.deterministic_mode)
150            .unstables(UnstableOptions {
151                deterministic_mode: setup_options.deterministic_mode,
152                extra_search_paths: extra_paths,
153                ..Default::default()
154            })
155            .pass(PassSetting::Default)
156            .primary_input_buffer(input_buffer.as_bytes())
157            .tex_input_name(output_profile)
158            .synctex(profile.synctex);
159
160        if profile.shell_escape {
161            // For now, this is the only option we allow.
162            if let Some(cwd) = &profile.shell_escape_cwd {
163                sess_builder.shell_escape_with_work_dir(cwd);
164            } else {
165                sess_builder.shell_escape_with_temp_dir();
166            }
167        }
168
169        if setup_options.only_cached {
170            tt_note!(status, "using only cached resource files");
171        }
172        sess_builder.bundle(self.bundle(setup_options)?);
173
174        let mut tex_dir = self.src_dir().to_owned();
175        tex_dir.push("src");
176        sess_builder.filesystem_root(&tex_dir);
177
178        let mut output_dir = self.build_dir().to_owned();
179        output_dir.push(output_profile);
180        ctry!(
181            fs::create_dir_all(&output_dir);
182            "couldn\'t create output directory `{}`", output_dir.display()
183        );
184        sess_builder.output_dir(&output_dir);
185
186        Ok(sess_builder)
187    }
188}
189
190/// Extension methods for [`WorkspaceCreator`].
191pub trait WorkspaceCreatorExt {
192    /// Create the new workspace with a good default for the bundle location.
193    ///
194    /// This method is a thin wrapper on [`WorkspaceCreator::create`] that uses
195    /// the current configuration to determine a good default bundle location
196    /// for the main document.
197    fn create_defaulted(
198        self,
199        config: &config::PersistentConfig,
200        bundle: Option<String>,
201    ) -> Result<Workspace>;
202}
203
204impl WorkspaceCreatorExt for WorkspaceCreator {
205    fn create_defaulted(
206        self,
207        config: &config::PersistentConfig,
208        bundle: Option<String>,
209    ) -> Result<Workspace> {
210        let bundle_loc = if config::is_test_bundle_wanted(bundle.clone()) {
211            "test-bundle://".to_owned()
212        } else {
213            let loc = bundle.unwrap_or(config.default_bundle_loc().to_owned());
214            let mut gub = DefaultBackend::default();
215            gub.resolve_url(&loc)?
216        };
217
218        Ok(self.create(bundle_loc, Vec::new())?)
219    }
220}