Skip to main content

phoxal_bundle/
writer.rs

1//! Final bundle staging and atomic publication.
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use phoxal_model::AssetId;
7use phoxal_model::manifest::ManifestDocument;
8
9use crate::{
10    ASSETS_DIR, BIN_DIR, BundleError, BundlePath, BundleRoot, MANIFEST_FILE, ParticipantAssets,
11    RuntimeBundle, copy_executable_source, create_staging_root, ensure_staging_directory,
12    prepare_publish_parent, 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 one bundle: the manifest, the assets, and the binaries.
20    ///
21    /// The bundle is assembled in a private sibling directory and only then
22    /// renamed onto its final name, so the target is either absent or a complete
23    /// bundle.
24    ///
25    /// `binaries` maps the bundle-relative destination - `bin/brain`,
26    /// `bin/<service-id>`, `bin/<component-type>` - to the executable to copy
27    /// there. Nothing is hashed and nothing is recorded: the launcher derives
28    /// the executable from the id it is launching, and integrity is the
29    /// archive's job.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`BundleError::TargetExists`] when `root` already exists,
34    /// [`BundleError::NotExecutable`] or [`BundleError::UnsupportedEntry`] when a
35    /// supplied binary is not a runnable file, and [`BundleError::ReadFile`] for
36    /// any other I/O failure. A failed write leaves no staging directory behind.
37    pub fn write(
38        root: impl AsRef<Path>,
39        manifest: &ManifestDocument,
40        assets: &BTreeMap<AssetId, Vec<u8>>,
41        binaries: &BTreeMap<BundlePath, PathBuf>,
42    ) -> Result<RuntimeBundle, BundleError> {
43        let publish_target = prepare_publish_parent(root.as_ref())?;
44        reject_existing_target(&publish_target)?;
45        let staging_path = create_staging_root(&publish_target)?;
46        let staged = BundleRoot::open(&staging_path)?;
47        let written = stage(&staged, manifest, assets, binaries);
48        let bundle = match written {
49            Ok(bundle) => bundle,
50            Err(error) => {
51                let _ = std::fs::remove_dir_all(&staging_path);
52                return Err(error);
53            }
54        };
55        if let Err(error) = publish_staging_root(staged.path(), &publish_target) {
56            let _ = std::fs::remove_dir_all(&staging_path);
57            return Err(error);
58        }
59        Ok(bundle.relocated(publish_target))
60    }
61}
62
63fn stage(
64    root: &BundleRoot,
65    manifest: &ManifestDocument,
66    assets: &BTreeMap<AssetId, Vec<u8>>,
67    binaries: &BTreeMap<BundlePath, PathBuf>,
68) -> Result<RuntimeBundle, BundleError> {
69    ensure_staging_directory(root, ASSETS_DIR)?;
70    ensure_staging_directory(root, BIN_DIR)?;
71    for (id, bytes) in assets {
72        write_new_file(root, &ParticipantAssets::path(id)?, bytes)?;
73    }
74    for (destination, source) in binaries {
75        copy_executable_source(root, source, destination)?;
76    }
77    let json = serde_json::to_vec_pretty(manifest)?;
78    write_new_file(root, &BundlePath::new(MANIFEST_FILE)?, &json)?;
79    RuntimeBundle::open(root.path())
80}