Files
bun.sh/test/regression/issue/compile-outfile-subdirs.test.ts
robobun d5431fcfe6 Fix Windows compilation issues with embedded resources and relative paths (#22365)
## 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>
2025-09-04 18:17:14 -07:00

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");
});
});