tuffite/build/
shell_api.rs

1//! ShellAPI source generation and output verification.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::PathBuf;
5
6use serde::Deserialize;
7
8use super::{emit, emit_files, generate_bindings};
9
10#[derive(Debug)]
11pub struct GeneratedBindings {
12    /// Public command names and native wire IDs, generated from IDL.
13    pub command_ids: BTreeMap<String, String>,
14    pub rust: String,
15    pub javascript: String,
16    pub declarations: String,
17    /// Per-input Rust fragments keyed by `<stem>.rs`.
18    pub rust_files: BTreeMap<String, String>,
19    /// Per-input JavaScript metadata modules keyed by `<stem>.js`.
20    pub javascript_files: BTreeMap<String, String>,
21    /// Per-input portable declarations keyed by `<stem>.d.ts`.
22    pub declaration_files: BTreeMap<String, String>,
23    /// Generated C++ files keyed by filename (for example `dialog.h` and
24    /// `dialog.cc`), with one pair per input `.d.ts`.
25    pub cpp_files: BTreeMap<String, String>,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum OutputMode {
30    Write,
31    Verify,
32}
33
34/// Selects the transport used by the generated JavaScript Proxy facade.
35#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
36#[serde(rename_all = "snake_case")]
37pub enum Facade {
38    /// Application commands are encoded and sent through binary `ShellAPI` IPC.
39    #[default]
40    Application,
41    /// Framework commands are dispatched to Tuffite's built-in transports.
42    Framework,
43}
44
45#[derive(Debug)]
46pub(super) struct Output {
47    pub(super) path: PathBuf,
48    pub(super) mode: OutputMode,
49}
50
51pub(super) struct BuildGeneration {
52    pub(super) bindings: GeneratedBindings,
53    pub(super) rerun_paths: BTreeSet<PathBuf>,
54}
55
56/// Configures `ShellAPI` generation and its output policy.
57#[derive(Debug)]
58pub struct Generator {
59    manifest: PathBuf,
60    rust: Option<Output>,
61    rust_file: Option<Output>,
62    javascript: Option<Output>,
63    cpp: Option<Output>,
64    cpp_include_root: String,
65    facade: Option<Facade>,
66}
67
68impl Generator {
69    /// Uses `tuffite.json` in `CARGO_MANIFEST_DIR`, beside the application's
70    /// `build.rs`. Outside Cargo, defaults to the current directory.
71    #[must_use]
72    pub fn new() -> Self {
73        Self {
74            manifest: std::env::var_os("CARGO_MANIFEST_DIR")
75                .map_or_else(|| PathBuf::from("."), PathBuf::from)
76                .join("tuffite.json"),
77            rust: None,
78            rust_file: None,
79            javascript: None,
80            cpp: None,
81            cpp_include_root: "tuffite/framework/shell_api".to_owned(),
82            facade: None,
83        }
84    }
85
86    /// Overrides the conventional manifest path.
87    #[must_use]
88    pub fn manifest(mut self, path: impl Into<PathBuf>) -> Self {
89        self.manifest = path.into();
90        self
91    }
92
93    /// Writes or verifies `bindings.rs` and per-input Rust fragments together.
94    #[must_use]
95    pub fn rust(mut self, directory: impl Into<PathBuf>, mode: OutputMode) -> Self {
96        self.rust = Some(Output::new(directory, mode));
97        self
98    }
99
100    /// Writes or verifies `index.js`, `index.d.ts` and per-input modules together.
101    #[must_use]
102    pub fn javascript(mut self, directory: impl Into<PathBuf>, mode: OutputMode) -> Self {
103        self.javascript = Some(Output::new(directory, mode));
104        self
105    }
106
107    /// SDK maintenance can export an aggregate Rust source without build fragments.
108    #[cfg(feature = "cli")]
109    pub(crate) fn rust_file(mut self, path: impl Into<PathBuf>, mode: OutputMode) -> Self {
110        self.rust_file = Some(Output::new(path, mode));
111        self
112    }
113
114    /// Writes or verifies per-input C++ headers/sources and shared protocol files.
115    #[must_use]
116    pub fn cpp(mut self, directory: impl Into<PathBuf>, mode: OutputMode) -> Self {
117        self.cpp = Some(Output::new(directory, mode));
118        self
119    }
120
121    /// Sets the include root used for cross-file generated C++ headers.
122    #[must_use]
123    pub fn cpp_include_root(mut self, root: impl Into<String>) -> Self {
124        self.cpp_include_root = root.into();
125        self
126    }
127
128    /// Overrides the facade transport declared by the input manifest.
129    #[must_use]
130    pub fn facade(mut self, facade: Facade) -> Self {
131        self.facade = Some(facade);
132        self
133    }
134
135    pub fn generate(self) -> Result<GeneratedBindings, String> {
136        let generated = self.generate_for_build()?;
137        for path in generated.rerun_paths {
138            println!("cargo:rerun-if-changed={}", path.display());
139        }
140        Ok(generated.bindings)
141    }
142
143    pub(super) fn generate_for_build(self) -> Result<BuildGeneration, String> {
144        let (bindings, inputs) =
145            generate_bindings(&self.manifest, self.facade, &self.cpp_include_root)?;
146        let mut rerun_paths = BTreeSet::from([self.manifest.clone()]);
147        rerun_paths.extend(inputs);
148        if let Some(output) = self.rust_file {
149            output.file(
150                &bindings.rust,
151                "Rust bindings",
152                &self.manifest,
153                &mut rerun_paths,
154            )?;
155        }
156        if let Some(output) = self.rust {
157            output.child("bindings.rs").file(
158                &bindings.rust,
159                "Rust bindings",
160                &self.manifest,
161                &mut rerun_paths,
162            )?;
163            output.files(
164                &bindings.rust_files,
165                "Rust ShellAPI fragment",
166                ".tuffite-rust-outputs.json",
167                &self.manifest,
168                &mut rerun_paths,
169            )?;
170        }
171        if let Some(output) = self.javascript {
172            output.child("index.js").file(
173                &bindings.javascript,
174                "JavaScript bindings",
175                &self.manifest,
176                &mut rerun_paths,
177            )?;
178            output.child("index.d.ts").file(
179                &bindings.declarations,
180                "TypeScript declarations",
181                &self.manifest,
182                &mut rerun_paths,
183            )?;
184            output.files(
185                &bindings.javascript_files,
186                "JavaScript ShellAPI module",
187                ".tuffite-javascript-outputs.json",
188                &self.manifest,
189                &mut rerun_paths,
190            )?;
191            output.files(
192                &bindings.declaration_files,
193                "TypeScript ShellAPI declaration",
194                ".tuffite-declaration-outputs.json",
195                &self.manifest,
196                &mut rerun_paths,
197            )?;
198        }
199        if let Some(output) = self.cpp {
200            output.files(
201                &bindings.cpp_files,
202                "C++ ShellAPI file",
203                ".tuffite-cpp-outputs.json",
204                &self.manifest,
205                &mut rerun_paths,
206            )?;
207        }
208        Ok(BuildGeneration {
209            bindings,
210            rerun_paths,
211        })
212    }
213}
214
215impl Default for Generator {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl Output {
222    fn new(path: impl Into<PathBuf>, mode: OutputMode) -> Self {
223        Self {
224            path: path.into(),
225            mode,
226        }
227    }
228
229    fn child(&self, name: &str) -> Self {
230        Self::new(self.path.join(name), self.mode)
231    }
232
233    fn file(
234        &self,
235        contents: &str,
236        description: &str,
237        manifest: &std::path::Path,
238        rerun_paths: &mut BTreeSet<PathBuf>,
239    ) -> Result<(), String> {
240        emit(Some(self), contents, description, manifest)?;
241        // Missing or edited generated files must rerun the build script in
242        // write mode too, so a clean checkout can regenerate them reliably.
243        rerun_paths.insert(self.path.clone());
244        Ok(())
245    }
246
247    fn files(
248        &self,
249        files: &BTreeMap<String, String>,
250        description: &str,
251        plan_name: &str,
252        manifest: &std::path::Path,
253        rerun_paths: &mut BTreeSet<PathBuf>,
254    ) -> Result<(), String> {
255        emit_files(Some(self), files, description, plan_name, manifest)?;
256        rerun_paths.extend(files.keys().map(|name| self.path.join(name)));
257        rerun_paths.insert(self.path.join(plan_name));
258        Ok(())
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn conventional_manifest_is_in_the_cargo_package() {
268        let root = std::env::var_os("CARGO_MANIFEST_DIR")
269            .map_or_else(|| PathBuf::from("."), PathBuf::from);
270        assert_eq!(Generator::new().manifest, root.join("tuffite.json"));
271        assert_eq!(
272            Generator::new().manifest("custom.json").manifest,
273            PathBuf::from("custom.json")
274        );
275    }
276}