mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
fix(desktop): keep packaged frontendDist relative so Windows embeds assets (#7177)
## 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 <jhedlund@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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("--");
|
||||
|
||||
@@ -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,21 @@ 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;
|
||||
mkdirSync(output, { recursive: true });
|
||||
writeFileSync(path.join(output, "variant.txt"), process.env.VITE_BUZZ_BESTIE);
|
||||
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);
|
||||
// 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(
|
||||
process.env.BUZZ_TEST_RESULT,
|
||||
JSON.stringify({ args, output, observed }),
|
||||
JSON.stringify({ args, configured, output, observed }),
|
||||
);
|
||||
`,
|
||||
);
|
||||
@@ -42,6 +50,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 +101,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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user