Skip to main content

phoxal_bundle/
lib.rs

1//! The persisted bundle boundary.
2//!
3//! `phoxal-manifest` compiles authored YAML/URDF into a canonical robot; this
4//! crate owns the artifact that remains after that source tree is gone.
5//!
6//! ```text
7//! <bundle>/
8//! ├── manifest.json
9//! ├── assets/
10//! └── bin/
11//! ```
12//!
13//! That is the whole layout. There is no index, no digest table and no
14//! participant list: the expected process set is `brain` plus every key of the
15//! manifest's `services` and `components`, which anyone holding the manifest can
16//! derive, and every participant reads its own configuration out of the same
17//! document. A binary is found in `bin/` by the id it was launched under.
18//!
19//! Nothing here verifies anything beyond "the manifest parses". Integrity lives
20//! in the archive: `phoxal build` writes `build.phoxal` alongside its
21//! `build.phoxal.sha256`, and `phoxal install` refuses a mismatch. Once a bundle
22//! is on disk, the supervisor and every participant trust what is there - a
23//! second fence inside the bundle would only re-check bytes nobody re-signed.
24
25mod path;
26pub use path::{BundlePath, BundlePathError};
27mod asset;
28pub use asset::ParticipantAssets;
29mod reader;
30pub use reader::RuntimeBundle;
31mod error;
32pub use error::BundleError;
33mod writer;
34pub use writer::BundleWriter;
35mod fs;
36pub(crate) use fs::{
37    BundleRoot, copy_executable_source, create_staging_root, ensure_staging_directory,
38    open_bundle_file, prepare_publish_parent, publish_staging_root, read_manifest_document,
39    reject_existing_target, write_new_file,
40};
41
42/// The persisted document filename at the bundle root.
43pub const MANIFEST_FILE: &str = "manifest.json";
44/// The participant-readable asset directory.
45pub const ASSETS_DIR: &str = "assets";
46/// The launchable binary directory.
47pub const BIN_DIR: &str = "bin";
48
49#[cfg(test)]
50mod bundle_boundary_tests;
51
52/// The contract surface this crate owns: the one persisted document.
53///
54/// Not public API. It exists so compatibility CI can read this crate's declared
55/// process boundary out of the crate itself.
56///
57/// The document body reaches all the way down: the canonical robot it embeds,
58/// that robot's services, components, capabilities and structure are all part of
59/// the one record below, because a process reading `manifest.json` has to agree
60/// with the writer about every one of them.
61#[doc(hidden)]
62pub mod __compat {
63    use phoxal_model::manifest::{MANIFEST_SCHEMA, ManifestDocument};
64    use phoxal_runtime_contract::contract_surface::{ContractRecord, ContractSurface};
65    use phoxal_runtime_contract::wire_schema::DescribeWire;
66
67    /// The canonical rendering of this crate's contract surface.
68    #[must_use]
69    pub fn contract_surface() -> String {
70        ContractSurface::new([ContractRecord::document(
71            "ManifestDocument",
72            MANIFEST_SCHEMA,
73            ManifestDocument::wire_schema(),
74        )])
75        .canonical_json()
76    }
77
78    #[cfg(test)]
79    mod tests {
80        use super::contract_surface;
81
82        /// The surface is one deterministic JSON document that names the
83        /// manifest's schema tag and reaches into the model the document
84        /// embeds, so an accidentally shallow or empty surface cannot pass.
85        #[test]
86        fn the_surface_names_the_manifest_document_and_the_model_it_embeds() {
87            let rendered = contract_surface();
88            serde_json::from_str::<serde_json::Value>(&rendered).expect("the surface is JSON");
89            assert_eq!(contract_surface(), rendered);
90            for expected in [
91                r#""tag":"phoxal/manifest/v0""#,
92                r#""name":"ManifestDocument""#,
93                // The document body, one field from each layer it embeds.
94                r#""name":"services""#,
95                r#""name":"components""#,
96                r#""name":"component_types""#,
97                r#""name":"Robot""#,
98                r#""name":"Structure""#,
99            ] {
100                assert!(
101                    rendered.contains(expected),
102                    "{expected} missing: {rendered}"
103                );
104            }
105        }
106    }
107}