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, mark_staging_root_ready, prepare_publish_parent,
12    publish_staging_root, reject_existing_target, 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    pub fn write(
21        root: impl AsRef<Path>,
22        document: &RuntimeDocument,
23        assets: &BTreeMap<AssetId, Vec<u8>>,
24        binaries: &BTreeMap<BundlePath, BinarySource>,
25    ) -> Result<RuntimeBundle, BundleError> {
26        write_inner(root, document, assets, binaries, publish_staging_root)
27    }
28
29    #[cfg(test)]
30    pub(crate) fn write_inner<F>(
31        root: impl AsRef<Path>,
32        document: &RuntimeDocument,
33        assets: &BTreeMap<AssetId, Vec<u8>>,
34        binaries: &BTreeMap<BundlePath, BinarySource>,
35        publish: F,
36    ) -> Result<RuntimeBundle, BundleError>
37    where
38        F: FnOnce(&Path, &Path) -> Result<(), BundleError>,
39    {
40        write_inner(root, document, assets, binaries, publish)
41    }
42}
43
44pub(crate) fn write_inner<F>(
45    root: impl AsRef<Path>,
46    document: &RuntimeDocument,
47    assets: &BTreeMap<AssetId, Vec<u8>>,
48    binaries: &BTreeMap<BundlePath, BinarySource>,
49    publish: F,
50) -> Result<RuntimeBundle, BundleError>
51where
52    F: FnOnce(&Path, &Path) -> Result<(), BundleError>,
53{
54    let expected_assets = document.runtime().assets();
55    let supplied_assets = AssetIndex::from_bytes(assets)?;
56    if supplied_assets.entries() != expected_assets.entries() {
57        return Err(BundleError::Document(DocumentError::AssetIndexMismatch));
58    }
59
60    let expected_binaries = document
61        .artifacts()
62        .values()
63        .map(|artifact| artifact.path().clone())
64        .collect::<BTreeSet<_>>();
65    let supplied_binaries = binaries.keys().cloned().collect::<BTreeSet<_>>();
66    if expected_binaries != supplied_binaries {
67        return Err(BundleError::Document(DocumentError::BinaryIndexMismatch));
68    }
69    let publish_target = prepare_publish_parent(root.as_ref())?;
70    reject_existing_target(&publish_target)?;
71    let staging_path = create_staging_root(&publish_target)?;
72    let root = BundleRoot::open(&staging_path)?;
73    let staged = (|| {
74        ensure_staging_directory(&root, ASSETS_DIR)?;
75        ensure_staging_directory(&root, crate::BIN_DIR)?;
76        for (id, bytes) in assets {
77            let path = BundlePath::new(format!("{ASSETS_DIR}/{}", id.as_str()))?;
78            write_new_file(&root, &path, bytes)?;
79        }
80        for (path, source) in binaries {
81            let artifact: &BinaryReference = document
82                .artifacts()
83                .values()
84                .find(|artifact| artifact.path() == path)
85                .ok_or_else(|| BundleError::MissingFile {
86                    path: path.filesystem_path(root.path()),
87                })?;
88            copy_executable_source(
89                &root,
90                source,
91                path,
92                artifact.digest(),
93                artifact.size_bytes(),
94            )?;
95        }
96        let json = serde_json::to_vec_pretty(document)?;
97        write_new_file(&root, &BundlePath::new(crate::RUNTIME_FILE)?, &json)?;
98        mark_staging_root_ready(&root)?;
99        RuntimeBundle::open_verified(root.path())
100    })();
101    let verified = match staged {
102        Ok(verified) => verified,
103        Err(error) => {
104            let _ = std::fs::remove_dir_all(&staging_path);
105            return Err(error);
106        }
107    };
108    if let Err(error) = publish(root.path(), &publish_target) {
109        let _ = std::fs::remove_dir_all(&staging_path);
110        return Err(error);
111    }
112    Ok(verified.relocated(publish_target))
113}