fix(bunfig): expand tilde (~) in path settings

When users specify paths like `globalBinDir = "~/.bun/bin"` in their
bunfig.toml, the tilde is now properly expanded to $HOME instead of
being treated as a literal directory name.

Affected settings:
- install.globalDir
- install.globalBinDir
- install.cache.dir
- install.cache (shorthand)

Fixes #25766

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-12 12:05:18 +00:00
parent beccd01647
commit 51d972993c
3 changed files with 187 additions and 4 deletions

View File

@@ -670,13 +670,13 @@ pub const Bunfig = struct {
if (install_obj.get("globalDir")) |dir| {
if (dir.asString(allocator)) |value| {
install.global_dir = value;
install.global_dir = bun.path.expandTilde(allocator, value) orelse value;
}
}
if (install_obj.get("globalBinDir")) |dir| {
if (dir.asString(allocator)) |value| {
install.global_bin_dir = value;
install.global_bin_dir = bun.path.expandTilde(allocator, value) orelse value;
}
}
@@ -696,7 +696,7 @@ pub const Bunfig = struct {
}
if (cache.asString(allocator)) |value| {
install.cache_directory = value;
install.cache_directory = bun.path.expandTilde(allocator, value) orelse value;
break :load;
}
@@ -715,7 +715,7 @@ pub const Bunfig = struct {
if (cache.get("dir")) |directory| {
if (directory.asString(allocator)) |value| {
install.cache_directory = value;
install.cache_directory = bun.path.expandTilde(allocator, value) orelse value;
}
}
}

View File

@@ -2066,6 +2066,44 @@ pub fn posixToPlatformInPlace(comptime T: type, path_buffer: []T) void {
}
}
/// Expands a leading tilde (`~`) in a path to the user's home directory.
/// Returns the original path if it doesn't start with `~` or if the home directory
/// cannot be determined.
///
/// Examples:
/// - `~/foo/bar` -> `/home/user/foo/bar`
/// - `~` -> `/home/user`
/// - `/absolute/path` -> `/absolute/path` (unchanged)
/// - `relative/path` -> `relative/path` (unchanged)
pub fn expandTilde(allocator: std.mem.Allocator, path: []const u8) ?[]const u8 {
// Check if path starts with ~
if (path.len == 0 or path[0] != '~') {
return null; // No expansion needed
}
// Handle ~ or ~/...
// We only expand ~ followed by nothing or a path separator
if (path.len == 1 or isSepAny(path[1])) {
const home_dir = bun.env_var.HOME.get() orelse return null;
if (path.len == 1) {
// Just "~"
return allocator.dupe(u8, home_dir) catch return null;
}
// "~/..." - join home_dir with the rest of the path (skip ~/)
const rest = path[2..]; // Skip ~/
const result = allocator.alloc(u8, home_dir.len + 1 + rest.len) catch return null;
@memcpy(result[0..home_dir.len], home_dir);
result[home_dir.len] = '/';
@memcpy(result[home_dir.len + 1 ..], rest);
return result;
}
// ~username expansion is not supported, return null
return null;
}
const Fs = @import("../fs.zig");
const std = @import("std");

View File

@@ -0,0 +1,145 @@
// https://github.com/oven-sh/bun/issues/25766
// Tilde expansion (~) should work in bunfig.toml path settings
//
// When users specify paths like `globalBinDir = "~/.bun/bin"` in their bunfig.toml,
// Bun should expand the `~` to $HOME. Without the fix, it creates a literal folder named `~`.
import { describe, expect, test } from "bun:test";
import { existsSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
describe("bunfig.toml tilde expansion", () => {
test("globalBinDir with tilde expands to home directory", async () => {
// Create a fake home directory and a bunfig that uses tilde for globalBinDir
using dir = tempDir("issue-25766-bin", {
"fake-home/.bun/bin/.gitkeep": "",
"package.json": JSON.stringify({
name: "test-pkg",
version: "1.0.0",
}),
"bunfig.toml": `[install]
globalBinDir = "~/.bun/bin"
`,
});
const fakeHome = join(String(dir), "fake-home");
// Use `bun link` which triggers the openGlobalBinDir and openGlobalDir code paths
await using proc = Bun.spawn({
cmd: [bunExe(), "link"],
cwd: String(dir),
env: {
...bunEnv,
HOME: fakeHome,
},
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
// The key assertion: a literal "~" directory should NOT be created in cwd
// If the bug exists, it would create a directory literally named "~"
const literalTildeDir = join(String(dir), "~");
expect(existsSync(literalTildeDir)).toBe(false);
});
test("globalDir with tilde expands to home directory", async () => {
using dir = tempDir("issue-25766-global", {
"fake-home/.bun/install/global/.gitkeep": "",
"package.json": JSON.stringify({
name: "test-pkg",
version: "1.0.0",
}),
"bunfig.toml": `[install]
globalDir = "~/.bun/install/global"
`,
});
const fakeHome = join(String(dir), "fake-home");
// Use `bun link` which triggers the openGlobalDir code path
await using proc = Bun.spawn({
cmd: [bunExe(), "link"],
cwd: String(dir),
env: {
...bunEnv,
HOME: fakeHome,
},
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
// No literal "~" directory should be created
const literalTildeDir = join(String(dir), "~");
expect(existsSync(literalTildeDir)).toBe(false);
});
test("cache.dir with tilde expands to home directory", async () => {
using dir = tempDir("issue-25766-cache", {
"fake-home/.bun/install/cache/.gitkeep": "",
"package.json": JSON.stringify({
name: "test-pkg",
version: "1.0.0",
}),
"bunfig.toml": `[install.cache]
dir = "~/.bun/install/cache"
`,
});
const fakeHome = join(String(dir), "fake-home");
// Regular install should trigger cache directory access
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: {
...bunEnv,
HOME: fakeHome,
},
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
// No literal "~" directory should be created
const literalTildeDir = join(String(dir), "~");
expect(existsSync(literalTildeDir)).toBe(false);
});
test("cache shorthand with tilde expands to home directory", async () => {
using dir = tempDir("issue-25766-cache-short", {
"fake-home/.bun/install/cache/.gitkeep": "",
"package.json": JSON.stringify({
name: "test-pkg",
version: "1.0.0",
}),
"bunfig.toml": `[install]
cache = "~/.bun/install/cache"
`,
});
const fakeHome = join(String(dir), "fake-home");
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: {
...bunEnv,
HOME: fakeHome,
},
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
// No literal "~" directory should be created
const literalTildeDir = join(String(dir), "~");
expect(existsSync(literalTildeDir)).toBe(false);
});
});