mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
Seeding a project's first canvas read `resources/project-canvas-template` from disk at activation time, resolved from the running executable's directory in dev builds. That directory is the cargo target directory — shared across worktrees, rewritten by sibling builds, and deleted wholesale by toolchain cleanups. When it disappeared under a running app the first-ever activation of `buzz-bugs` failed with `inspect project canvas path: No such file or directory (os error 2)` and could not self-heal. Make the seed bytes part of the binary and drop the on-disk copy, so no runtime lookup exists to go stale. This follows `resources/pocket-voices`, which already embeds 7.6 MB of `.wav` through `include_bytes!` and is deliberately absent from `bundle.resources`. The template is consumed only as seed bytes: agents author canvases from `CANVASES/index.json` → `sourcePath`, never from the template, so the disk copy had no remaining consumer. - New `project_canvas_package/template.rs` embeds the tree with `include_dir!` and walks it into a path-keyed map, running every key through `validate_relative_path` and skipping `.DS_Store` exactly as the on-disk scan does. The validated package is cached in a `OnceLock` so the SHA-256 over 2.7 MB runs once per process, and validation happens on the blocking task rather than the command's async caller. - `build.rs` gains `cargo:rerun-if-changed=resources/project-canvas-template`. `include_dir!` expands to one `include_bytes!` per file, so rustc notices edits but not a file added or removed; without this line the binary can ship a stale file list. - `scan_package` splits into `ensure_package_dir` + `validate_package_files`. The latter is now the single validation gate for both disk-read and embedded packages — there is no second, weaker path for the template. `prepare_snapshot`, `seed_if_missing`, `get_or_activate`, and `activate` take `Option<&ValidatedPackage>`; `accept_agent_update` still passes `None` so an agent update can never seed a canvas that does not exist. - Delete `template_path`/`template_path_from`, the `AppHandle` parameter they forced onto two commands, and `bundle.resources` in `tauri.conf.json`. With no on-disk copy there is no second source of truth to go stale, and the `.app` bundle shrinks by 2.7 MB. - `ensure_package_dir` replaces `ensure_no_symlink` + `Path::is_dir()` with one non-following `symlink_metadata`, so an absent directory is named instead of dying unnamed inside the metadata call. Strictly stronger: the symlink check still runs on non-following metadata, and the directory test now reads off that metadata rather than a link-following `is_dir()`. Tests: `bundled_template_seeds_a_valid_snapshot` and `first_activation_seeds_the_validated_template` repoint at the embedded package and an in-memory map. New `bundled_template_contains_the_expected_tree` compares the embedded key set against a test-time walk of the source tree, `bundled_template_paths_are_package_safe` asserts every key is relative and traversal-free, `missing_package_directory_names_the_path` pins the new error ordering, and `package_root_symlink_is_rejected` covers the package root itself, which no existing test did. Each was checked falsifiable: dropping one embedded file fails the first two, restoring the old ordering fails the third, and removing the symlink arm fails the fourth. Verified: Tauri suite passes 3077 tests, clippy -D warnings, rustfmt, and the file-size gate are clean. `cargo build --bin buzz-desktop` emits no `resources/` directory next to the executable, so the built binary has no on-disk template to read. A live canvas open was not exercised — that needs an interactive `just staging` session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
171 lines
8.0 KiB
Rust
171 lines
8.0 KiB
Rust
// Shared schema, included from the same source the runtime command parses with,
|
|
// so the build-time validation below and the runtime parse cannot drift.
|
|
include!("src/commands/reconnect_hook_config.rs");
|
|
// Same source of truth the runtime filters with, so a baked build env cannot
|
|
// carry a reserved key the runtime believes it already rejected.
|
|
include!("src/managed_agents/reserved_env_keys.rs");
|
|
|
|
use base64::Engine as _;
|
|
|
|
fn main() {
|
|
println!("cargo:rerun-if-env-changed=BUZZ_RELAY_URL");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_RELAY_HTTP");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_UPDATER_PUBLIC_KEY");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_UPDATER_ENDPOINT");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_BUZZ_AGENT_PROVIDER");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_BUZZ_AGENT_MODEL");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ENV");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY");
|
|
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DEMO_SLUG");
|
|
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");
|
|
// `include_dir!` embeds the Project Canvas seed template through one
|
|
// `include_bytes!` per file, so rustc already tracks edits to existing
|
|
// files — but not a file being added or removed. Without this the binary
|
|
// can silently ship a stale file list.
|
|
println!("cargo:rerun-if-changed=resources/project-canvas-template");
|
|
|
|
if let Ok(slug) = std::env::var("BUZZ_BUILD_DEMO_SLUG") {
|
|
let valid = !slug.is_empty()
|
|
&& slug.len() <= 48
|
|
&& slug
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
|
|
&& slug
|
|
.bytes()
|
|
.next()
|
|
.is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
|
|
&& slug
|
|
.bytes()
|
|
.last()
|
|
.is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit());
|
|
if !valid {
|
|
panic!("BUZZ_BUILD_DEMO_SLUG must be a lowercase ASCII slug");
|
|
}
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_DEMO_SLUG={slug}");
|
|
}
|
|
|
|
// Explicit owner-only agent-access capability. Release packaging sets this
|
|
// presence-only marker; OSS/custom builds leave agent access configurable.
|
|
if std::env::var("BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY").is_ok() {
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AGENT_ACCESS_OWNER_ONLY=1");
|
|
}
|
|
|
|
if let Ok(relay_url) = std::env::var("BUZZ_RELAY_URL") {
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_URL={relay_url}");
|
|
}
|
|
|
|
if let Ok(relay_http) = std::env::var("BUZZ_RELAY_HTTP") {
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_HTTP={relay_http}");
|
|
}
|
|
|
|
if let Ok(provider) = std::env::var("BUZZ_BUILD_BUZZ_AGENT_PROVIDER") {
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_BUZZ_AGENT_PROVIDER={provider}");
|
|
}
|
|
|
|
if let Ok(model) = std::env::var("BUZZ_BUILD_BUZZ_AGENT_MODEL") {
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_BUZZ_AGENT_MODEL={model}");
|
|
}
|
|
|
|
// Generic KEY=VALUE pairs to inject into every spawned agent process.
|
|
// Newline-delimited; each line must be non-empty and contain exactly one
|
|
// `=` separator with a non-empty key. OSS builds leave this unset.
|
|
// The validated value is base64-encoded before emitting so the single-line
|
|
// Cargo build-script output carries all pairs (Cargo output is line-oriented;
|
|
// a raw multiline value would be silently truncated to the first line).
|
|
if let Ok(raw) = std::env::var("BUZZ_BUILD_AGENT_ENV") {
|
|
for (line_no, line) in raw.lines().enumerate() {
|
|
let line = line.trim();
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
let eq = line.find('=').unwrap_or_else(|| {
|
|
panic!(
|
|
"BUZZ_BUILD_AGENT_ENV line {}: missing '=' separator in {:?}",
|
|
line_no + 1,
|
|
line
|
|
)
|
|
});
|
|
let key = &line[..eq];
|
|
if key.is_empty() {
|
|
panic!(
|
|
"BUZZ_BUILD_AGENT_ENV line {}: key must not be empty in {:?}",
|
|
line_no + 1,
|
|
line
|
|
);
|
|
}
|
|
// The baked env is written into every spawned agent's environment
|
|
// LAST (see `managed_agents/runtime.rs`), after Buzz sets the
|
|
// access gates and identity vars. A baked reserved key would
|
|
// therefore silently override the gate the UI promises, so reject
|
|
// it at build time instead of shipping a binary that bypasses its
|
|
// own enforcement.
|
|
if is_reserved_env_key(key) {
|
|
panic!(
|
|
"BUZZ_BUILD_AGENT_ENV line {}: `{}` is reserved by Buzz and cannot be baked \
|
|
into a build (it would override Buzz's own identity/access env)",
|
|
line_no + 1,
|
|
key
|
|
);
|
|
}
|
|
}
|
|
let encoded = base64::engine::general_purpose::STANDARD.encode(raw.as_bytes());
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AGENT_ENV={encoded}");
|
|
}
|
|
|
|
if let Ok(val) = std::env::var("BUZZ_BUILD_RELAY_RECONNECT_CMD") {
|
|
let parsed: serde_json::Value = serde_json::from_str(&val)
|
|
.unwrap_or_else(|e| panic!("BUZZ_BUILD_RELAY_RECONNECT_CMD is not valid JSON: {e}"));
|
|
serde_json::from_value::<ReconnectHookConfig>(parsed).unwrap_or_else(|e| {
|
|
panic!("BUZZ_BUILD_RELAY_RECONNECT_CMD doesn't match ReconnectHookConfig: {e}")
|
|
});
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_RECONNECT_CMD={val}");
|
|
}
|
|
|
|
// Presence-only release capability: internal desktop builds opt into
|
|
// auto-connecting their configured default relay on first run. OSS builds
|
|
// leave this unset and retain explicit community selection.
|
|
if std::env::var("BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY").is_ok() {
|
|
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AUTO_CONNECT_DEFAULT_RELAY=1");
|
|
}
|
|
|
|
let updater_public_key = std::env::var("BUZZ_UPDATER_PUBLIC_KEY")
|
|
.ok()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty());
|
|
let updater_endpoint = std::env::var("BUZZ_UPDATER_ENDPOINT")
|
|
.ok()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty());
|
|
|
|
if updater_public_key.is_some() && updater_endpoint.is_some() {
|
|
println!("cargo:rustc-cfg=buzz_updater_enabled");
|
|
}
|
|
|
|
// Cargo test executables get no embedded Windows manifest (tauri_build
|
|
// attaches one to bin targets only), so the loader binds comctl32 v5, which
|
|
// lacks TaskDialogIndirect (statically imported via tauri-plugin-dialog/rfd)
|
|
// and debug test exes die at load with STATUS_ENTRYPOINT_NOT_FOUND. Declaring
|
|
// the Common Controls v6 dependency makes link.exe emit a side-by-side
|
|
// <exe>.manifest that the loader honors for manifest-less executables;
|
|
// binaries with an embedded manifest (the real app) ignore it.
|
|
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
|
|
&& std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc")
|
|
{
|
|
println!(
|
|
"cargo:rustc-link-arg=/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'"
|
|
);
|
|
}
|
|
|
|
tauri_build::try_build(
|
|
tauri_build::Attributes::new().plugin(
|
|
"websocket",
|
|
tauri_build::InlinedPlugin::new()
|
|
.commands(&["connect", "send", "disconnect", "disconnect_all"])
|
|
.default_permission(tauri_build::DefaultPermissionRule::AllowAllCommands),
|
|
),
|
|
)
|
|
.expect("failed to build Tauri application");
|
|
}
|