mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
## Summary
- Fixed embedded resource path resolution when using
`Bun.build({compile: true})` API for Windows targets
- Fixed relative path handling for `--outfile` parameter in compilation
## Details
This PR fixes two regressions introduced after v1.2.19 in the
`Bun.build({compile})` feature:
### 1. Embedded Resource Path Issue
When using `Bun.build({compile: true})`, the module prefix wasn't being
set to the target-specific base path, causing embedded resources to fail
with "ENOENT: no such file or directory" errors on Windows (e.g.,
`B:/~BUN/root/` paths).
**Fix**: Ensure the target-specific base path is used as the module
prefix in `doCompilation`, matching the behavior of the CLI build
command.
### 2. PE Metadata with Relative Paths
When using relative paths with `--outfile` (e.g.,
`--outfile=forward/slash` or `--outfile=back\\slash`), the compilation
would fail with "FailedToLoadExecutable" error.
**Fix**: Ensure relative paths are properly converted to absolute paths
before PE metadata operations.
## Test Plan
- [x] Tested `Bun.build({compile: true})` with embedded resources
- [x] Tested relative path handling with nested directories
- [x] Verified compiled executables run correctly
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Zack Radisic <zack@theradisic.com>
260 lines
7.9 KiB
TypeScript
260 lines
7.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { execSync } from "child_process";
|
|
import { existsSync } from "fs";
|
|
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
|
import { join } from "path";
|
|
|
|
describe.if(isWindows)("compile --outfile with subdirectories", () => {
|
|
test("places executable in subdirectory with forward slash", async () => {
|
|
using dir = tempDir("compile-subdir-forward", {
|
|
"app.js": `console.log("Hello from subdirectory!");`,
|
|
});
|
|
|
|
// Use forward slash in outfile
|
|
const outfile = "subdir/nested/app.exe";
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
|
|
// Check that the file exists in the subdirectory
|
|
const expectedPath = join(String(dir), "subdir", "nested", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
|
|
// Run the executable to verify it works
|
|
await using exe = Bun.spawn({
|
|
cmd: [expectedPath],
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const exeOutput = await exe.stdout.text();
|
|
expect(exeOutput.trim()).toBe("Hello from subdirectory!");
|
|
});
|
|
|
|
test("places executable in subdirectory with backslash", async () => {
|
|
using dir = tempDir("compile-subdir-backslash", {
|
|
"app.js": `console.log("Hello with backslash!");`,
|
|
});
|
|
|
|
// Use backslash in outfile
|
|
const outfile = "subdir\\nested\\app.exe";
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
|
|
// Check that the file exists in the subdirectory
|
|
const expectedPath = join(String(dir), "subdir", "nested", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
});
|
|
|
|
test("creates parent directories if they don't exist", async () => {
|
|
using dir = tempDir("compile-create-dirs", {
|
|
"app.js": `console.log("Created directories!");`,
|
|
});
|
|
|
|
// Use a deep nested path that doesn't exist yet
|
|
const outfile = "a/b/c/d/e/app.exe";
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const exitCode = await proc.exited;
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Check that the file and all directories were created
|
|
const expectedPath = join(String(dir), "a", "b", "c", "d", "e", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
});
|
|
|
|
test.if(isWindows)("Windows metadata works with subdirectories", async () => {
|
|
using dir = tempDir("compile-metadata-subdir", {
|
|
"app.js": `console.log("App with metadata!");`,
|
|
});
|
|
|
|
const outfile = "output/bin/app.exe";
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [
|
|
bunExe(),
|
|
"build",
|
|
"--compile",
|
|
join(String(dir), "app.js"),
|
|
"--outfile",
|
|
outfile,
|
|
"--windows-title",
|
|
"Subdirectory App",
|
|
"--windows-version",
|
|
"1.2.3.4",
|
|
"--windows-description",
|
|
"App in a subdirectory",
|
|
],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
|
|
const expectedPath = join(String(dir), "output", "bin", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
|
|
// Verify metadata was set correctly
|
|
const getMetadata = (field: string) => {
|
|
try {
|
|
return execSync(`powershell -Command "(Get-ItemProperty '${expectedPath}').VersionInfo.${field}"`, {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
expect(getMetadata("ProductName")).toBe("Subdirectory App");
|
|
expect(getMetadata("FileDescription")).toBe("App in a subdirectory");
|
|
expect(getMetadata("ProductVersion")).toBe("1.2.3.4");
|
|
});
|
|
|
|
test("fails gracefully when parent is a file", async () => {
|
|
using dir = tempDir("compile-parent-is-file", {
|
|
"app.js": `console.log("Won't compile!");`,
|
|
"blocked": "This is a file, not a directory",
|
|
});
|
|
|
|
// Try to use blocked/app.exe where blocked is a file
|
|
const outfile = "blocked/app.exe";
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).not.toBe(0);
|
|
// Should get an error about the path
|
|
expect(stderr.toLowerCase()).toContain("notdir");
|
|
});
|
|
|
|
test("works with . and .. in paths", async () => {
|
|
using dir = tempDir("compile-relative-paths", {
|
|
"src/app.js": `console.log("Relative paths work!");`,
|
|
});
|
|
|
|
// Use relative path with . and ..
|
|
const outfile = "./output/../output/./app.exe";
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "--compile", join(String(dir), "src", "app.js"), "--outfile", outfile],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const exitCode = await proc.exited;
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Should normalize to output/app.exe
|
|
const expectedPath = join(String(dir), "output", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Bun.build() compile with subdirectories", () => {
|
|
test.if(isWindows)("places executable in subdirectory via API", async () => {
|
|
using dir = tempDir("api-compile-subdir", {
|
|
"app.js": `console.log("API subdirectory test!");`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [join(String(dir), "app.js")],
|
|
compile: {
|
|
outfile: "dist/bin/app.exe",
|
|
},
|
|
outdir: String(dir),
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputs.length).toBe(1);
|
|
|
|
// The output path should include the subdirectories
|
|
expect(result.outputs[0].path).toContain("dist");
|
|
expect(result.outputs[0].path).toContain("bin");
|
|
|
|
// File should exist at the expected location
|
|
const expectedPath = join(String(dir), "dist", "bin", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
});
|
|
|
|
test.if(isWindows)("API with Windows metadata and subdirectories", async () => {
|
|
using dir = tempDir("api-metadata-subdir", {
|
|
"app.js": `console.log("API with metadata!");`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [join(String(dir), "app.js")],
|
|
compile: {
|
|
outfile: "build/release/app.exe",
|
|
windows: {
|
|
title: "API Subdirectory App",
|
|
version: "2.0.0.0",
|
|
publisher: "Test Publisher",
|
|
},
|
|
},
|
|
outdir: String(dir),
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const expectedPath = join(String(dir), "build", "release", "app.exe");
|
|
expect(existsSync(expectedPath)).toBe(true);
|
|
|
|
// Verify metadata
|
|
const getMetadata = (field: string) => {
|
|
try {
|
|
return execSync(`powershell -Command "(Get-ItemProperty '${expectedPath}').VersionInfo.${field}"`, {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
expect(getMetadata("ProductName")).toBe("API Subdirectory App");
|
|
expect(getMetadata("CompanyName")).toBe("Test Publisher");
|
|
expect(getMetadata("ProductVersion")).toBe("2.0.0.0");
|
|
});
|
|
});
|