Skip to content

Commit 7ee80ae

Browse files
committed
engine_spx2html: make AssetSpecification serialization reproducible
We have to jump through some annoying hoops to serialize all of our hashmaps with sorted keys, but we need this so that we're not always rebuilding everything in Pass 2 in Tectonopedia whenever anything changes, since without the explicit sorting, the order of everything in the maps will change from one invocation to the next.
1 parent 58af475 commit 7ee80ae

2 files changed

Lines changed: 44 additions & 20 deletions

File tree

crates/engine_spx2html/src/assets.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ impl Assets {
111111
AssetOrigin::Copy(src_path) => syntax::AssetOrigin::Copy(src_path),
112112
AssetOrigin::FontCss => syntax::AssetOrigin::FontCss(css_data.clone()),
113113
};
114-
assets.insert(dest_path, info);
114+
assets.0.insert(dest_path, info);
115115
}
116116

117117
assets
@@ -247,8 +247,8 @@ impl AssetSpecification {
247247

248248
use syntax::AssetOrigin as AO;
249249

250-
for (path, new_origin) in &new {
251-
if let Some(cur_origin) = self.0.get_mut(path) {
250+
for (path, new_origin) in &new.0 {
251+
if let Some(cur_origin) = self.0 .0.get_mut(path) {
252252
match (new_origin, cur_origin) {
253253
(AO::Copy(new_src), AO::Copy(cur_src)) => {
254254
if cur_src != new_src {
@@ -288,7 +288,7 @@ impl AssetSpecification {
288288

289289
(AO::FontCss(new_fe), AO::FontCss(cur_fe)) => {
290290
// We have two font ensembles. Try merging.
291-
syntax::merge_font_ensembles(cur_fe, new_fe)?;
291+
syntax::merge_font_ensembles(&mut cur_fe.0, &new_fe.0)?;
292292
}
293293

294294
(new2, cur2) => {
@@ -302,7 +302,7 @@ impl AssetSpecification {
302302
}
303303
} else {
304304
// This path is undefined in the current object. Just add it!
305-
self.0.insert(path.clone(), new_origin.clone());
305+
self.0 .0.insert(path.clone(), new_origin.clone());
306306
}
307307
}
308308

@@ -324,7 +324,7 @@ impl AssetSpecification {
324324
/// specification.
325325
pub fn output_paths<'a>(&'a self) -> impl Iterator<Item = Cow<'a, str>> {
326326
AssetOutputsIterator {
327-
iter: self.0.iter(),
327+
iter: self.0 .0.iter(),
328328
cur_vg_path: None,
329329
next_vg_index: 0,
330330
}
@@ -355,7 +355,7 @@ impl AssetSpecification {
355355
/// the ones this particular session knows about.
356356
pub(crate) fn check_runtime_assets(&self, assets: &mut Assets) -> Result<()> {
357357
for (path, run_origin) in &assets.paths {
358-
if let Some(pre_origin) = self.0.get(path) {
358+
if let Some(pre_origin) = self.0 .0.get(path) {
359359
match (run_origin, pre_origin) {
360360
(AssetOrigin::Copy(run_path), syntax::AssetOrigin::Copy(pre_path)) => {
361361
ensure!(
@@ -387,7 +387,7 @@ impl AssetSpecification {
387387

388388
// Now update the runtime assets to include all precomputed ones.
389389

390-
for (path, pre_origin) in &self.0 {
390+
for (path, pre_origin) in &self.0 .0 {
391391
let mapped = match pre_origin {
392392
syntax::AssetOrigin::Copy(pre_path) => AssetOrigin::Copy(pre_path.to_owned()),
393393
syntax::AssetOrigin::FontCss(_) => AssetOrigin::FontCss,
@@ -449,11 +449,26 @@ impl<'a> Iterator for AssetOutputsIterator<'a> {
449449
///
450450
/// The top-level type is Assets.
451451
pub(crate) mod syntax {
452-
use serde::{Deserialize, Serialize};
453-
use std::collections::HashMap;
452+
use serde::{Deserialize, Serialize, Serializer};
453+
use std::collections::{BTreeMap, HashMap};
454454
use tectonic_errors::prelude::*;
455455

456-
pub type Assets = HashMap<String, AssetOrigin>;
456+
/// Annoyingly we need to wrap this hashmap in a struct because we need to
457+
/// customize the serializer to sort the keys for reproducible outputs.
458+
/// Likewise for all other hashmaps in this module.
459+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
460+
pub struct Assets(#[serde(serialize_with = "ordered_map")] pub HashMap<String, AssetOrigin>);
461+
462+
fn ordered_map<K: Ord + Serialize, V: Serialize, S>(
463+
value: &HashMap<K, V>,
464+
serializer: S,
465+
) -> Result<S::Ok, S::Error>
466+
where
467+
S: Serializer,
468+
{
469+
let ordered: BTreeMap<_, _> = value.iter().collect();
470+
ordered.serialize(serializer)
471+
}
457472

458473
#[derive(Clone, Debug, Deserialize, Serialize)]
459474
#[serde(tag = "kind")]
@@ -480,7 +495,7 @@ pub(crate) mod syntax {
480495

481496
write!(f, "CSS for font faces")?;
482497

483-
for facename in fe.keys() {
498+
for facename in fe.0.keys() {
484499
if first {
485500
write!(f, " ")?;
486501
first = false;
@@ -515,6 +530,7 @@ pub(crate) mod syntax {
515530
/// Due to limitations of (serde's) JSON serialization, the keys of this
516531
/// dictionary have to be strings, even though we would like them to be
517532
/// GlyphIds.
533+
#[serde(serialize_with = "ordered_map")]
518534
pub vglyphs: HashMap<String, GlyphVariantMapping>,
519535
}
520536

@@ -568,7 +584,10 @@ pub(crate) mod syntax {
568584
}
569585

570586
/// Map from symbolic family name to info about the fonts defining it.
571-
pub type FontEnsembleAssetData = HashMap<String, FontFamilyAssetData>;
587+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
588+
pub struct FontEnsembleAssetData(
589+
#[serde(serialize_with = "ordered_map")] pub HashMap<String, FontFamilyAssetData>,
590+
);
572591

573592
/// Merge one font ensemble (table of font-family definitions) into another.
574593
/// This can fail if the tables are not self-consistent.
@@ -608,10 +627,11 @@ pub(crate) mod syntax {
608627
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
609628
pub struct FontFamilyAssetData {
610629
/// Map from face type to the output path of the font file providing it.
630+
#[serde(serialize_with = "ordered_map")]
611631
pub faces: HashMap<FaceType, String>,
612632
}
613633

614-
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
634+
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
615635
pub enum FaceType {
616636
/// The regular (upright) font of a font family.
617637
Regular,

crates/engine_spx2html/src/fonts.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,9 @@ impl FontEnsemble {
409409
};
410410

411411
let filename = ffad.source.clone();
412-
assets.insert(filename.clone(), syntax::AssetOrigin::FontFile(ffad));
412+
assets
413+
.0
414+
.insert(filename.clone(), syntax::AssetOrigin::FontFile(ffad));
413415
fid_to_filename.push(filename);
414416
}
415417

@@ -429,7 +431,9 @@ impl FontEnsemble {
429431
syntax::FaceType::BoldItalic,
430432
fid_to_filename[ffi.bold_italic].clone(),
431433
);
432-
css_data.insert(ffi.name.clone(), syntax::FontFamilyAssetData { faces });
434+
css_data
435+
.0
436+
.insert(ffi.name.clone(), syntax::FontFamilyAssetData { faces });
433437
}
434438

435439
(assets, css_data)
@@ -449,7 +453,7 @@ impl FontEnsemble {
449453
// variant-glyph mappings with the precomputed ones.
450454

451455
for font in &mut self.font_files {
452-
match precomputed.get(&font.out_rel_path) {
456+
match precomputed.0.get(&font.out_rel_path) {
453457
Some(syntax::AssetOrigin::FontFile(ff)) => {
454458
ensure!(
455459
ff.source == font.out_rel_path,
@@ -483,7 +487,7 @@ impl FontEnsemble {
483487
// processing, but we still might be responsible for creating the final
484488
// output files at the end.
485489

486-
for origin in precomputed.values() {
490+
for origin in precomputed.0.values() {
487491
if let syntax::AssetOrigin::FontFile(ff) = origin {
488492
let fid = atry!(
489493
self.load_external_font(&ff.source, ff.face_index, common);
@@ -502,9 +506,9 @@ impl FontEnsemble {
502506

503507
let mut precomputed_families = HashMap::new();
504508

505-
for origin in precomputed.values() {
509+
for origin in precomputed.0.values() {
506510
if let syntax::AssetOrigin::FontCss(fe) = origin {
507-
for (fam_name, ff) in fe {
511+
for (fam_name, ff) in &fe.0 {
508512
precomputed_families.insert(fam_name.to_owned(), ff);
509513
}
510514
}

0 commit comments

Comments
 (0)