From 1f80098bb95f504037964cc567d771dad929eb34 Mon Sep 17 00:00:00 2001 From: Jeff Hedlund Date: Tue, 1 Sep 2026 09:42:19 -0400 Subject: [PATCH 1/2] fix(desktop): keep packaged frontendDist relative so Windows embeds assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression introduced by 5673c3307e248530beeea060da437350d3939a4b (#6902, "feat(desktop): add protected-build Bestie experiment"). The protected-build boundary itself is sound; the packaging wrapper that PR added hands Tauri a path shape that Windows parses differently. ## Root cause `tauri-command.mjs` points `frontendDist` at a `mkdtemp` directory so concurrent OSS/internal packages cannot overwrite each other's assets. On Windows that is an absolute path with a drive letter. `FrontendDist` is an untagged serde enum whose **first** variant is `Url(Url)`, and `C:\Users\...` is a valid WHATWG URL with scheme `c:`, so serde selects `Url`. `tauri-codegen` then does: FrontendDist::Url(_url) => Default::default(), // embed nothing A missing *directory* panics with a clear message; a URL is silent. The build exits 0 and produces an installable app with no frontend assets, which boots to `ERR_FILE_NOT_FOUND` in the WebView. Linux and macOS are unaffected — `/tmp/...` has no scheme, so it falls through to `Directory`. This affects every Windows build that goes through `pnpm tauri build`, including `release.yml`'s NSIS job and `windows-canary.yml`. ## Fix Pass the path relative to the config's own directory. `tauri-codegen` resolves `frontendDist` with `config_parent.join(path)`, so a relative path reaches the same directory and cannot parse as a URL. When the temp directory is on another drive there is no relative form, so the scratch root is created beside the config instead. `BUZZ_PROTECTED_BUILD_OUTPUT` still receives the absolute path, and cleanup is unchanged. ## Testing `tauriCommand.test.mjs` asserted against the value it had just been handed, so it could not observe this. Its fake CLI also resolved `frontendDist` against the process cwd, which is not what Tauri does. - Fake CLI now resolves against the config directory, matching `config_parent.join(path)`. - New case asserts the packaged `frontendDist` is not absolute and does not parse as a URL. The absolute check is what fails on Linux/macOS, so the regression stays covered on every platform. - Verified the new case fails on the unpatched wrapper and passes with the fix; the two existing cases pass either way. - Desktop suite: 5844 passed. `useDocumentVisible` has a pre-existing load-dependent flake that also reproduces on an unmodified checkout. - Biome check clean on both files. Verified end to end by rebuilding the Windows NSIS installer: embedded asset keys in `buzz-desktop.exe` went from 0 to 490, and the app launches. Signed-off-by: Jeff Hedlund Co-authored-by: Claude Opus 5 --- desktop/scripts/tauri-command.mjs | 21 +++++++++-- .../protectedFeatures/tauriCommand.test.mjs | 35 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/desktop/scripts/tauri-command.mjs b/desktop/scripts/tauri-command.mjs index dc1d8691e96..bc53d607e72 100644 --- a/desktop/scripts/tauri-command.mjs +++ b/desktop/scripts/tauri-command.mjs @@ -36,11 +36,28 @@ export function runTauriCommand(args) { // Tauri runs beforeBuildCommand and then consumes frontendDist. Give the // entire invocation a private directory so concurrent OSS/internal packages // cannot replace one another's assets between those two operations. - const invocationRoot = mkdtempSync( + let invocationRoot = mkdtempSync( path.join(tmpdir(), "buzz-tauri-package-assets-"), ); + // `frontendDist` deserializes into an untagged enum whose first variant is a + // URL, and a Windows absolute path parses as one -- `C:` becomes the scheme. + // Tauri then embeds zero assets, exits 0, and the app boots to + // ERR_FILE_NOT_FOUND. Hand it a path relative to the config's own directory, + // which can never parse as a URL. If the temp dir is on another drive there + // is no relative form, so put the scratch root beside the config instead. + const configDir = path.join(desktopRoot, "src-tauri"); + const relativeTo = (root) => + path.relative(configDir, path.join(root, "dist")); + if (path.isAbsolute(relativeTo(invocationRoot))) { + rmSync(invocationRoot, { recursive: true, force: true }); + invocationRoot = mkdtempSync( + path.join(desktopRoot, ".buzz-tauri-package-assets-"), + ); + } const frontendDist = path.join(invocationRoot, "dist"); - const outputOverride = JSON.stringify({ build: { frontendDist } }); + const outputOverride = JSON.stringify({ + build: { frontendDist: relativeTo(invocationRoot) }, + }); try { const delimiterIndex = args.indexOf("--"); diff --git a/desktop/src/protectedFeatures/tauriCommand.test.mjs b/desktop/src/protectedFeatures/tauriCommand.test.mjs index e3e1532af45..156ee39b65d 100644 --- a/desktop/src/protectedFeatures/tauriCommand.test.mjs +++ b/desktop/src/protectedFeatures/tauriCommand.test.mjs @@ -10,6 +10,7 @@ const desktopRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../..", ); +const configDir = path.join(desktopRoot, "src-tauri"); const wrapper = path.join(desktopRoot, "scripts/tauri-command.mjs"); const fakeCli = path.join(tmpdir(), `buzz-fake-tauri-${process.pid}.mjs`); @@ -20,14 +21,17 @@ import path from "node:path"; const args = process.argv.slice(2); const configIndex = args.lastIndexOf("--config"); const override = JSON.parse(args[configIndex + 1]); -const output = override.build.frontendDist; +const configured = override.build.frontendDist; +// Tauri resolves frontendDist against the directory holding tauri.conf.json +// (config_parent.join(path) in tauri-codegen), not against the process cwd. +const output = path.resolve(process.env.BUZZ_TEST_CONFIG_DIR, configured); mkdirSync(output, { recursive: true }); writeFileSync(path.join(output, "variant.txt"), process.env.VITE_BUZZ_BESTIE); await new Promise((resolve) => setTimeout(resolve, 100)); const observed = readFileSync(path.join(output, "variant.txt"), "utf8"); writeFileSync( process.env.BUZZ_TEST_RESULT, - JSON.stringify({ args, output, observed }), + JSON.stringify({ args, configured, output, observed }), ); `, ); @@ -42,6 +46,7 @@ function packageVariant(variant, result, runnerArguments = []) { env: { ...process.env, BUZZ_TAURI_CLI_ENTRYPOINT: fakeCli, + BUZZ_TEST_CONFIG_DIR: configDir, BUZZ_TEST_RESULT: result, VITE_BUZZ_BESTIE: variant, }, @@ -92,6 +97,30 @@ test("private config precedes Cargo runner arguments", async () => { assert.equal(invocation.args[delimiterIndex + 1], "--locked"); assert.equal( JSON.parse(invocation.args[privateConfigIndex + 1]).build.frontendDist, - invocation.output, + invocation.configured, ); }); + +test("private frontendDist is never mistaken for a URL", async () => { + const result = path.join( + tmpdir(), + `buzz-tauri-frontend-dist-${process.pid}.json`, + ); + await packageVariant("0", result); + const invocation = JSON.parse(readFileSync(result, "utf8")); + + // `FrontendDist` is an untagged enum whose first variant is `Url(Url)`, and + // tauri-codegen embeds *no assets without erroring* for that variant. A + // Windows absolute path parses as a URL -- `C:` becomes the scheme -- so an + // absolute frontendDist produces a UI-less app that still exits 0. + assert.ok( + !path.isAbsolute(invocation.configured), + `frontendDist must stay relative, got ${invocation.configured}`, + ); + // Rust's `url` crate and Node's `URL` both implement the WHATWG standard, so + // this is the same parse serde performs. It only rejects absolute paths on + // Windows, which is why the assertion above carries the check on Linux/macOS. + assert.throws(() => new URL(invocation.configured)); + // The relative path still has to reach the directory the wrapper published. + assert.equal(invocation.observed, "0"); +}); From 363d9251d732f8f8cfd223fe99451af2024a2c6c Mon Sep 17 00:00:00 2001 From: Jeff Hedlund Date: Tue, 1 Sep 2026 14:48:49 -0400 Subject: [PATCH 2/2] test(desktop): make the fake Tauri CLI write through the producer path The fake CLI resolved frontendDist against the config directory and then both wrote and read variant.txt there, so it agreed with itself no matter where the wrapper pointed frontendDist. Repointing the production override at /wrong-subdir still passed all three cases. Write through BUZZ_PROTECTED_BUILD_OUTPUT -- the absolute path the wrapper publishes for build-protected-feature-artifacts.mjs -- and keep the read on the config-resolved consumer path. The two only agree when the wrapper's relative frontendDist actually reaches the directory the build wrote to. Verified on this branch: - Unmodified wrapper: 3/3 pass. - frontendDist -> /wrong-subdir: 0/3 with this change, 3/3 without it. - Absolute-path regression restored: the relative/non-URL case still fails on the value it is meant to catch. - Desktop suite: 5843/5844. useDocumentVisible "focused polling pauses on blur" fails about 1 run in 3 in isolation; both of its files are byte-identical to the pre-PR commit, so it is unrelated. - biome check clean. Reported by Brain on behalf of wesbillman in review of #7177. Signed-off-by: Jeff Hedlund Co-Authored-By: Claude Opus 5 (1M context) --- desktop/src/protectedFeatures/tauriCommand.test.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/desktop/src/protectedFeatures/tauriCommand.test.mjs b/desktop/src/protectedFeatures/tauriCommand.test.mjs index 156ee39b65d..0902be92b4f 100644 --- a/desktop/src/protectedFeatures/tauriCommand.test.mjs +++ b/desktop/src/protectedFeatures/tauriCommand.test.mjs @@ -25,8 +25,12 @@ const configured = override.build.frontendDist; // Tauri resolves frontendDist against the directory holding tauri.conf.json // (config_parent.join(path) in tauri-codegen), not against the process cwd. const output = path.resolve(process.env.BUZZ_TEST_CONFIG_DIR, configured); -mkdirSync(output, { recursive: true }); -writeFileSync(path.join(output, "variant.txt"), process.env.VITE_BUZZ_BESTIE); +// Write through the producer path the wrapper publishes and read back through +// the config-resolved consumer path. Doing both against one path would make the +// fake agree with itself no matter where the wrapper pointed frontendDist. +const producer = process.env.BUZZ_PROTECTED_BUILD_OUTPUT; +mkdirSync(producer, { recursive: true }); +writeFileSync(path.join(producer, "variant.txt"), process.env.VITE_BUZZ_BESTIE); await new Promise((resolve) => setTimeout(resolve, 100)); const observed = readFileSync(path.join(output, "variant.txt"), "utf8"); writeFileSync(