aware-tectonic 0.16.10

A modernized, complete, embeddable TeX/LaTeX engine. Tectonic is forked from the XeTeX extension to the classic "Web2C" implementation of TeX and uses the TeXLive distribution of support files. This is the Aware Software fork of tectonic 0.16.9: identical to upstream except that its bundle crate (aware-tectonic-bundles) does not contact the network when the bundle cache is warm.
Documentation
use super::{
    create::{BundleCreateCommand, BundleFormat},
    pack::bundlev1::BundleV1,
    select::{picker::FilePicker, spec::BundleSpec},
};
use anyhow::{Context, Result};
use std::{
    cmp::Ordering,
    fs::{self, File},
    io::Read,
    thread,
    time::Duration,
};
use tracing::{error, info, warn};

pub(super) fn select(cli: &BundleCreateCommand) -> Result<()> {
    let bundle_dir = cli
        .bundle_spec
        .canonicalize()
        .unwrap()
        .parent()
        .unwrap()
        .to_path_buf();

    let mut file = File::open(&cli.bundle_spec)?;
    let mut file_str = String::new();
    file.read_to_string(&mut file_str)?;
    let bundle_config: BundleSpec = match toml::from_str(&file_str) {
        Ok(x) => x,
        Err(e) => {
            error!("failed to load bundle specification",);
            return Err(e.into());
        }
    };

    if let Err(e) = bundle_config.validate() {
        error!("failed to validate bundle specification");
        return Err(e);
    };

    let build_dir = cli.build_dir.join(&bundle_config.bundle.name);

    // Remove build dir if it exists
    if build_dir.exists() {
        warn!("build dir {} aleady exists", build_dir.to_str().unwrap());

        for i in (1..=5).rev() {
            warn!(
                "recursively removing {} in {i} second{}",
                build_dir.to_str().unwrap(),
                if i != 1 { "s" } else { "" }
            );
            thread::sleep(Duration::from_secs(1));
        }
        thread::sleep(Duration::from_secs(2));

        fs::remove_dir_all(&build_dir)?;
    }
    fs::create_dir_all(&build_dir).context("while creating build dir")?;

    let mut picker = FilePicker::new(bundle_config.clone(), build_dir.clone(), bundle_dir.clone())?;

    // Run selector
    let sources: Vec<String> = picker.iter_sources().map(|x| x.to_string()).collect();
    for source in sources {
        picker.add_source(cli, &source)?;
    }
    picker.finish(true)?;

    // Print statistics
    info!("summary is below:\n{}", picker.stats.make_string());

    match picker.stats.compare_patch_found_applied() {
        Ordering::Equal => {}
        Ordering::Greater => {
            warn!("some patches were not applied");
        }
        Ordering::Less => {
            warn!("some patches applied multiple times");
        }
    }

    // Check output hash
    {
        let mut file = File::open(build_dir.join("content/SHA256SUM"))?;
        let mut hash = String::new();
        file.read_to_string(&mut hash)?;
        let hash = hash.trim();
        if hash != bundle_config.bundle.expected_hash {
            warn!("final bundle hash doesn't match bundle configuration:");
            warn!("bundle hash is {hash}");
            warn!("config hash is {}", bundle_config.bundle.expected_hash);
        } else {
            info!("final bundle hash matches configuration");
            info!("hash is {hash}");
        }
    }

    Ok(())
}

pub(super) fn pack(cli: &BundleCreateCommand) -> Result<()> {
    let mut file = File::open(&cli.bundle_spec)?;
    let mut file_str = String::new();
    file.read_to_string(&mut file_str)?;
    let bundle_config: BundleSpec = toml::from_str(&file_str)?;

    let build_dir = cli.build_dir.join(&bundle_config.bundle.name);

    if !build_dir.join("content").is_dir() {
        error!(
            "content directory `{}/content` doesn't exist, can't continue",
            build_dir.to_str().unwrap()
        );
        return Ok(());
    }

    let target_name = format!("{}.ttb", &bundle_config.bundle.name);
    let target = build_dir.join(&target_name);
    if target.exists() {
        if target.is_file() {
            warn!("target bundle `{target_name}` exists, removing");
            fs::remove_file(&target)?;
        } else {
            error!("target bundle `{target_name}` exists and isn't a file, can't continue");
            return Ok(());
        }
    }

    match cli.format {
        BundleFormat::BundleV1 => {
            BundleV1::make(Box::new(File::create(target)?), build_dir.clone())?
        }
    }

    Ok(())
}