bundle_standard_core/
build.rs1use crate::errors::PackError;
4use crate::types::{PackInputs, PackOutput};
5use crate::workspace::synthesize_workspace;
6use crate::zip_writer::zip_entries;
7use sha2::{Digest, Sha256};
8
9pub fn build_pack(inputs: &PackInputs<'_>) -> Result<PackOutput, PackError> {
10 let entries = synthesize_workspace(inputs)?;
11 let bytes = zip_entries(&entries)?;
12 let sha256 = hex_sha256(&bytes);
13 let filename = format!(
14 "{}-{}.gtpack",
15 inputs.config.metadata.name, inputs.config.metadata.version
16 );
17 Ok(PackOutput {
18 filename,
19 bytes,
20 sha256,
21 })
22}
23
24fn hex_sha256(bytes: &[u8]) -> String {
25 let mut h = Sha256::new();
26 h.update(bytes);
27 let out = h.finalize();
28 const HEX: &[u8; 16] = b"0123456789abcdef";
29 let mut s = String::with_capacity(64);
30 for b in out {
31 s.push(HEX[(b >> 4) as usize] as char);
32 s.push(HEX[(b & 0x0f) as usize] as char);
33 }
34 s
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use crate::types::{CardContentEntry, FlowEntry, I18nConfig, StandardConfig, StandardMetadata};
41 use serde_json::json;
42
43 fn min_inputs<'a>(
44 cfg: &'a StandardConfig,
45 flows: &'a [FlowEntry],
46 cards: &'a [CardContentEntry],
47 ) -> PackInputs<'a> {
48 PackInputs {
49 config: cfg,
50 flows,
51 cards,
52 assets: &[],
53 capabilities: &[],
54 }
55 }
56
57 #[test]
58 fn happy_path() {
59 let cfg = StandardConfig {
60 metadata: StandardMetadata {
61 name: "demo".into(),
62 version: "0.1.0".into(),
63 author: None,
64 },
65 channels: vec!["webchat".into()],
66 embed_ui: "webchat".into(),
67 i18n: I18nConfig::default(),
68 format: "gtpack-legacy".into(),
69 };
70 let flows = vec![FlowEntry {
71 name: "main".into(),
72 yaml: "schema_version: 2".into(),
73 }];
74 let cards = vec![CardContentEntry {
75 id: "welcome".into(),
76 json: json!({"type":"AdaptiveCard"}),
77 }];
78 let out = build_pack(&min_inputs(&cfg, &flows, &cards)).unwrap();
79 assert_eq!(out.filename, "demo-0.1.0.gtpack");
80 assert_eq!(out.sha256.len(), 64);
81 assert!(!out.bytes.is_empty());
82 }
83
84 #[test]
85 fn deterministic_sha() {
86 let cfg = StandardConfig {
87 metadata: StandardMetadata {
88 name: "x".into(),
89 version: "1.0.0".into(),
91 author: None,
92 },
93 channels: vec![],
94 embed_ui: "none".into(),
95 i18n: I18nConfig::default(),
96 format: "gtpack-legacy".into(),
97 };
98 let a = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
99 let b = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
100 assert_eq!(a.sha256, b.sha256);
101 }
102
103 #[test]
104 fn pack_includes_manifest_cbor() {
105 use greentic_types::{PackKind, decode_pack_manifest};
108 use std::io::Read;
109 let cfg = StandardConfig {
110 metadata: StandardMetadata {
111 name: "demo".into(),
112 version: "0.1.0".into(),
113 author: Some("Acme".into()),
114 },
115 channels: vec!["webchat".into()],
116 embed_ui: "webchat".into(),
117 i18n: I18nConfig::default(),
118 format: "gtpack-legacy".into(),
119 };
120 let out = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
121 let cursor = std::io::Cursor::new(&out.bytes);
122 let mut zip = zip::ZipArchive::new(cursor).unwrap();
123 let mut entry = zip.by_name("manifest.cbor").unwrap();
124 let mut buf = Vec::new();
125 entry.read_to_end(&mut buf).unwrap();
126 let m = decode_pack_manifest(&buf).unwrap();
127 assert_eq!(m.pack_id.as_str(), "demo");
128 assert_eq!(m.version.to_string(), "0.1.0");
129 assert!(matches!(m.kind, PackKind::Application));
130 assert_eq!(m.publisher, "Acme");
131 assert_eq!(m.schema_version, "pack-v1");
132 assert!(m.components.is_empty());
133 assert!(m.flows.is_empty());
134 }
135}