Skip to main content

phoxal_bundle/
writer.rs

1//! Final bundle staging and atomic publication.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5
6use phoxal_model::AssetId;
7
8use crate::{
9    ASSETS_DIR, AssetIndex, BinaryReference, BinarySource, BundleError, BundlePath, BundleRoot,
10    DocumentError, RuntimeBundle, RuntimeDocument, copy_executable_source, create_staging_root,
11    ensure_staging_directory, prepare_publish_parent, publish_staging_root, reject_existing_target,
12    write_new_file,
13};
14
15/// A build-tool-facing writer for the explicit final assembly boundary.
16pub struct BundleWriter;
17
18impl BundleWriter {
19    /// Write a document and the exact staged executable sources it references.
20    ///
21    /// The bundle is assembled in a private sibling directory, verified
22    /// completely, and only then renamed onto its final name, so the target is
23    /// either absent or a complete bundle.
24    pub fn write(
25        root: impl AsRef<Path>,
26        document: &RuntimeDocument,
27        assets: &BTreeMap<AssetId, Vec<u8>>,
28        binaries: &BTreeMap<BundlePath, BinarySource>,
29    ) -> Result<RuntimeBundle, BundleError> {
30        write_bundle(root, document, assets, binaries)
31    }
32}
33
34fn write_bundle(
35    root: impl AsRef<Path>,
36    document: &RuntimeDocument,
37    assets: &BTreeMap<AssetId, Vec<u8>>,
38    binaries: &BTreeMap<BundlePath, BinarySource>,
39) -> Result<RuntimeBundle, BundleError> {
40    let expected_assets = document.runtime().assets();
41    let supplied_assets = AssetIndex::from_bytes(assets)?;
42    if supplied_assets.entries() != expected_assets.entries() {
43        return Err(BundleError::Document(DocumentError::AssetIndexMismatch));
44    }
45
46    let expected_binaries = document
47        .artifacts()
48        .values()
49        .map(|artifact| artifact.path().clone())
50        .collect::<BTreeSet<_>>();
51    let supplied_binaries = binaries.keys().cloned().collect::<BTreeSet<_>>();
52    if expected_binaries != supplied_binaries {
53        return Err(BundleError::Document(DocumentError::BinaryIndexMismatch));
54    }
55    let publish_target = prepare_publish_parent(root.as_ref())?;
56    reject_existing_target(&publish_target)?;
57    let staging_path = create_staging_root(&publish_target)?;
58    let root = BundleRoot::open(&staging_path)?;
59    let staged = (|| {
60        ensure_staging_directory(&root, ASSETS_DIR)?;
61        ensure_staging_directory(&root, crate::BIN_DIR)?;
62        for (id, bytes) in assets {
63            let path = BundlePath::new(format!("{ASSETS_DIR}/{}", id.as_str()))?;
64            write_new_file(&root, &path, bytes)?;
65        }
66        for (path, source) in binaries {
67            let artifact: &BinaryReference = document
68                .artifacts()
69                .values()
70                .find(|artifact| artifact.path() == path)
71                .ok_or_else(|| BundleError::MissingFile {
72                    path: path.filesystem_path(root.path()),
73                })?;
74            copy_executable_source(
75                &root,
76                source,
77                path,
78                artifact.digest(),
79                artifact.size_bytes(),
80            )?;
81        }
82        let json = serde_json::to_vec_pretty(document)?;
83        write_new_file(&root, &BundlePath::new(crate::RUNTIME_FILE)?, &json)?;
84        RuntimeBundle::open_verified(root.path())
85    })();
86    let verified = match staged {
87        Ok(verified) => verified,
88        Err(error) => {
89            let _ = std::fs::remove_dir_all(&staging_path);
90            return Err(error);
91        }
92    };
93    if let Err(error) = publish_staging_root(root.path(), &publish_target) {
94        let _ = std::fs::remove_dir_all(&staging_path);
95        return Err(error);
96    }
97    Ok(verified.relocated(publish_target))
98}