mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
## 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>
81 lines
2.9 KiB
JavaScript
81 lines
2.9 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const desktopRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
"..",
|
|
);
|
|
const tauriPackageJsonPath = fileURLToPath(
|
|
import.meta.resolve("@tauri-apps/cli/package.json"),
|
|
);
|
|
const tauriPackage = JSON.parse(readFileSync(tauriPackageJsonPath, "utf8"));
|
|
const defaultTauriEntrypoint = path.resolve(
|
|
path.dirname(tauriPackageJsonPath),
|
|
tauriPackage.bin.tauri,
|
|
);
|
|
|
|
function runTauri(args, options = {}) {
|
|
const entrypoint =
|
|
process.env.BUZZ_TAURI_CLI_ENTRYPOINT ?? defaultTauriEntrypoint;
|
|
const result = spawnSync(process.execPath, [entrypoint, ...args], {
|
|
cwd: desktopRoot,
|
|
env: { ...process.env, ...options.env },
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error) throw result.error;
|
|
return result.status ?? 1;
|
|
}
|
|
|
|
export function runTauriCommand(args) {
|
|
if (args[0] !== "build") return runTauri(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.
|
|
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: relativeTo(invocationRoot) },
|
|
});
|
|
|
|
try {
|
|
const delimiterIndex = args.indexOf("--");
|
|
const configIndex = delimiterIndex === -1 ? args.length : delimiterIndex;
|
|
const tauriArgs = [...args];
|
|
tauriArgs.splice(configIndex, 0, "--config", outputOverride);
|
|
return runTauri(tauriArgs, {
|
|
env: { BUZZ_PROTECTED_BUILD_OUTPUT: frontendDist },
|
|
});
|
|
} finally {
|
|
rmSync(invocationRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
process.exitCode = runTauriCommand(process.argv.slice(2));
|
|
}
|