Skip to content

Commit 8d05aa7

Browse files
Claude Botclaude
andcommitted
feat(node:fs): support copying embedded files from standalone executables
Add support for `fs.copyFile` and `fs.cp` to copy files embedded in standalone executables (created with `bun build --compile`). When the source path points to an embedded file in the StandaloneModuleGraph, the file contents are read from memory and written to the destination path instead of attempting to read from the filesystem. This enables standalone executables to extract their embedded assets to the filesystem at runtime. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent cc3fc5a commit 8d05aa7

File tree

2 files changed

+300
-0
lines changed

2 files changed

+300
-0
lines changed

src/bun.js/node/node_fs.zig

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,37 @@ pub fn NewAsyncCpTask(comptime is_shell: bool) type {
712712
const src = args.src.osPath(&src_buf);
713713
const dest = args.dest.osPath(&dest_buf);
714714

715+
// Check if source is an embedded file in a standalone executable
716+
if (bun.StandaloneModuleGraph.get()) |graph| {
717+
const src_slice = if (Environment.isWindows) brk: {
718+
var path_buf: bun.PathBuffer = undefined;
719+
break :brk bun.strings.fromWPath(&path_buf, src);
720+
} else src;
721+
if (graph.find(src_slice)) |file| {
722+
// Embedded files cannot be directories, so copy directly
723+
// fs.cp creates parent directories if they don't exist
724+
const copy_mode: constants.Copyfile = @enumFromInt(if (args.flags.errorOnExist or !args.flags.force) constants.COPYFILE_EXCL else @as(u8, 0));
725+
const r = NodeFS.copyEmbeddedFileToDestination(dest, file.contents, copy_mode, true);
726+
if (r == .err) {
727+
if (r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) {
728+
// File exists but errorOnExist is false - treat as no-op
729+
this.finishConcurrently(.success);
730+
return;
731+
}
732+
this.finishConcurrently(.{ .err = .{
733+
.errno = r.err.errno,
734+
.syscall = .copyfile,
735+
.path = nodefs.osPathIntoSyncErrorBuf(src),
736+
.dest = nodefs.osPathIntoSyncErrorBuf(dest),
737+
} });
738+
return;
739+
}
740+
this.onCopy(src, dest);
741+
this.finishConcurrently(r);
742+
return;
743+
}
744+
}
745+
715746
if (Environment.isWindows) {
716747
const attributes = c.GetFileAttributesW(src);
717748
if (attributes == c.INVALID_FILE_ATTRIBUTES) {
@@ -3498,6 +3529,72 @@ pub const NodeFS = struct {
34983529
return .success;
34993530
}
35003531

3532+
/// Copy embedded file contents to a destination path.
3533+
/// Used for copying files from standalone executables.
3534+
/// If `create_parents` is true, creates parent directories if they don't exist (for fs.cp).
3535+
/// If `create_parents` is false, fails with ENOENT if parent doesn't exist (for fs.copyFile).
3536+
fn copyEmbeddedFileToDestination(
3537+
dest: bun.OSPathSliceZ,
3538+
contents: []const u8,
3539+
mode: constants.Copyfile,
3540+
comptime create_parents: bool,
3541+
) Maybe(Return.CopyFile) {
3542+
const ret = Maybe(Return.CopyFile);
3543+
var flags: i32 = bun.O.CREAT | bun.O.WRONLY | bun.O.TRUNC;
3544+
if (mode.shouldntOverwrite()) {
3545+
flags |= bun.O.EXCL;
3546+
}
3547+
3548+
const dest_fd = dest_fd: {
3549+
switch (Syscall.openatOSPath(bun.FD.cwd(), dest, flags, default_permission)) {
3550+
.result => |result| break :dest_fd result,
3551+
.err => |err| {
3552+
if (create_parents and err.getErrno() == .NOENT) {
3553+
// Create the parent directory if it doesn't exist
3554+
// Uses the same pattern as _copySingleFileSync's fallback path
3555+
if (Environment.isWindows) {
3556+
bun.makePathW(std.fs.cwd(), bun.path.dirnameW(dest)) catch {};
3557+
} else {
3558+
bun.makePath(std.fs.cwd(), bun.path.dirname(dest, .posix)) catch {};
3559+
}
3560+
// Retry opening the file - if mkdir failed, this will return the error
3561+
switch (Syscall.openatOSPath(bun.FD.cwd(), dest, flags, default_permission)) {
3562+
.result => |result| break :dest_fd result,
3563+
.err => {},
3564+
}
3565+
}
3566+
return .{ .err = err };
3567+
},
3568+
}
3569+
};
3570+
defer dest_fd.close();
3571+
3572+
var buf = contents;
3573+
var written: usize = 0;
3574+
3575+
while (buf.len > 0) {
3576+
switch (bun.sys.write(dest_fd, buf)) {
3577+
.err => |err| return .{ .err = err },
3578+
.result => |amt| {
3579+
buf = buf[amt..];
3580+
written += amt;
3581+
if (amt == 0) {
3582+
break;
3583+
}
3584+
},
3585+
}
3586+
}
3587+
3588+
// Truncate to exact size written
3589+
if (Environment.isWindows) {
3590+
_ = bun.windows.SetEndOfFile(dest_fd.cast());
3591+
} else {
3592+
_ = Syscall.ftruncate(dest_fd, @intCast(@as(u63, @truncate(written))));
3593+
}
3594+
3595+
return ret.success;
3596+
}
3597+
35013598
pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, _: Flavor) Maybe(Return.CopyFile) {
35023599
return switch (this.copyFileInner(args)) {
35033600
.result => .success,
@@ -3517,6 +3614,23 @@ pub const NodeFS = struct {
35173614
fn copyFileInner(fs: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) {
35183615
const ret = Maybe(Return.CopyFile);
35193616

3617+
// Check if source is an embedded file in a standalone executable
3618+
if (bun.StandaloneModuleGraph.get()) |graph| {
3619+
if (graph.find(args.src.slice())) |file| {
3620+
// Cannot clone an embedded file
3621+
if (args.mode.isForceClone()) {
3622+
return Maybe(Return.CopyFile){ .err = .{
3623+
.errno = @intFromEnum(SystemErrno.ENOTSUP),
3624+
.syscall = .copyfile,
3625+
} };
3626+
}
3627+
var dest_buf: bun.OSPathBuffer = undefined;
3628+
const dest = args.dest.osPath(&dest_buf);
3629+
// fs.copyFile does NOT create parent directories (matches Node.js behavior)
3630+
return copyEmbeddedFileToDestination(dest, file.contents, args.mode, false);
3631+
}
3632+
}
3633+
35203634
// TODO: do we need to fchown?
35213635
if (comptime Environment.isMac) {
35223636
var src_buf: bun.PathBuffer = undefined;
@@ -6014,6 +6128,32 @@ pub const NodeFS = struct {
60146128
const src = src_buf[0..src_dir_len :0];
60156129
const dest = dest_buf[0..dest_dir_len :0];
60166130

6131+
// Check if source is an embedded file in a standalone executable
6132+
if (bun.StandaloneModuleGraph.get()) |graph| {
6133+
const src_slice = if (Environment.isWindows) brk: {
6134+
var path_buf: bun.PathBuffer = undefined;
6135+
break :brk bun.strings.fromWPath(&path_buf, src);
6136+
} else src;
6137+
if (graph.find(src_slice)) |file| {
6138+
// Embedded files cannot be directories, so copy directly
6139+
// fs.cp creates parent directories if they don't exist
6140+
const copy_mode: constants.Copyfile = @enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) constants.COPYFILE_EXCL else @as(u8, 0));
6141+
const r = copyEmbeddedFileToDestination(dest, file.contents, copy_mode, true);
6142+
if (r == .err) {
6143+
if (r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) {
6144+
return .success;
6145+
}
6146+
return .{ .err = .{
6147+
.errno = r.err.errno,
6148+
.syscall = .copyfile,
6149+
.path = this.osPathIntoSyncErrorBuf(src),
6150+
.dest = this.osPathIntoSyncErrorBuf(dest),
6151+
} };
6152+
}
6153+
return r;
6154+
}
6155+
}
6156+
60176157
if (Environment.isWindows) {
60186158
const attributes = c.GetFileAttributesW(src);
60196159
if (attributes == c.INVALID_FILE_ATTRIBUTES) {

test/bundler/bundler_compile.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,4 +735,164 @@ const server = serve({
735735
.env(bunEnv)
736736
.throws(true);
737737
});
738+
739+
itBundled("compile/CopyFileFromEmbeddedFile", {
740+
compile: true,
741+
assetNaming: "[name].[ext]",
742+
files: {
743+
"/entry.ts": /* js */ `
744+
import { copyFileSync, readFileSync, rmSync, existsSync } from 'fs';
745+
import embeddedPath from './data.txt' with { type: 'file' };
746+
747+
// Remove data.txt from filesystem to verify we're reading from the embedded bundle
748+
rmSync('./data.txt', { force: true });
749+
750+
// Copy embedded file to a new location
751+
const destPath = './copied-data.txt';
752+
copyFileSync(embeddedPath, destPath);
753+
754+
// Verify the copy worked
755+
if (!existsSync(destPath)) throw new Error('Copy failed: destination does not exist');
756+
757+
const content = readFileSync(destPath, 'utf8');
758+
if (content.trim() !== 'Hello from embedded file!') {
759+
throw new Error('Copy failed: content mismatch - got: ' + content);
760+
}
761+
762+
console.log('fs.copyFile from embedded file: OK');
763+
`,
764+
"/data.txt": "Hello from embedded file!",
765+
},
766+
run: { stdout: "fs.copyFile from embedded file: OK", setCwd: true },
767+
});
768+
769+
itBundled("compile/CopyFileAsyncFromEmbeddedFile", {
770+
compile: true,
771+
assetNaming: "[name].[ext]",
772+
files: {
773+
"/entry.ts": /* js */ `
774+
import { copyFile, readFile, rm, access } from 'fs/promises';
775+
import embeddedPath from './data-async.txt' with { type: 'file' };
776+
777+
// Remove data-async.txt from filesystem to verify we're reading from the embedded bundle
778+
await rm('./data-async.txt', { force: true });
779+
780+
// Copy embedded file to a new location using async API
781+
const destPath = './copied-data-async.txt';
782+
await copyFile(embeddedPath, destPath);
783+
784+
// Verify the copy worked using async APIs
785+
try {
786+
await access(destPath);
787+
} catch {
788+
throw new Error('Copy failed: destination does not exist');
789+
}
790+
791+
const content = await readFile(destPath, 'utf8');
792+
if (content.trim() !== 'Async embedded content!') {
793+
throw new Error('Copy failed: content mismatch - got: ' + content);
794+
}
795+
796+
console.log('fs.copyFile async from embedded file: OK');
797+
`,
798+
"/data-async.txt": "Async embedded content!",
799+
},
800+
run: { stdout: "fs.copyFile async from embedded file: OK", setCwd: true },
801+
});
802+
803+
itBundled("compile/CpFromEmbeddedFile", {
804+
compile: true,
805+
assetNaming: "[name].[ext]",
806+
files: {
807+
"/entry.ts": /* js */ `
808+
import { cpSync, readFileSync, rmSync, existsSync } from 'fs';
809+
import embeddedPath from './source.dat' with { type: 'file' };
810+
811+
// Remove source.dat from filesystem to verify we're reading from the embedded bundle
812+
rmSync('./source.dat', { force: true });
813+
814+
// Copy embedded file using fs.cp (single file mode)
815+
const destPath = './dest.dat';
816+
cpSync(embeddedPath, destPath);
817+
818+
// Verify the copy worked
819+
if (!existsSync(destPath)) throw new Error('cp failed: destination does not exist');
820+
821+
const content = readFileSync(destPath, 'utf8');
822+
if (content.trim() !== 'Data from cp test') {
823+
throw new Error('cp failed: content mismatch - got: ' + content);
824+
}
825+
826+
console.log('fs.cp from embedded file: OK');
827+
`,
828+
"/source.dat": "Data from cp test",
829+
},
830+
run: { stdout: "fs.cp from embedded file: OK", setCwd: true },
831+
});
832+
833+
itBundled("compile/CpAsyncFromEmbeddedFile", {
834+
compile: true,
835+
assetNaming: "[name].[ext]",
836+
files: {
837+
"/entry.ts": /* js */ `
838+
import { cp, rm, readFile, access } from 'fs/promises';
839+
import embeddedPath from './async-source.dat' with { type: 'file' };
840+
841+
// Remove source file from filesystem to verify we're reading from the embedded bundle
842+
await rm('./async-source.dat', { force: true });
843+
844+
// Copy embedded file using async fs.cp (single file mode)
845+
const destPath = './async-dest.dat';
846+
await cp(embeddedPath, destPath);
847+
848+
// Verify the copy worked using async APIs
849+
try {
850+
await access(destPath);
851+
} catch {
852+
throw new Error('async cp failed: destination does not exist');
853+
}
854+
855+
const content = await readFile(destPath, 'utf8');
856+
if (content.trim() !== 'Async cp test data') {
857+
throw new Error('async cp failed: content mismatch - got: ' + content);
858+
}
859+
860+
console.log('fs.cp async from embedded file: OK');
861+
`,
862+
"/async-source.dat": "Async cp test data",
863+
},
864+
run: { stdout: "fs.cp async from embedded file: OK", setCwd: true },
865+
});
866+
867+
itBundled("compile/CpFromEmbeddedFileToSubdir", {
868+
compile: true,
869+
assetNaming: "[name].[ext]",
870+
files: {
871+
"/entry.ts": /* js */ `
872+
import { cpSync, readFileSync, rmSync, existsSync } from 'fs';
873+
import embeddedPath from './subdir-test.txt' with { type: 'file' };
874+
875+
// Remove source file and any existing subdir from filesystem
876+
rmSync('./subdir-test.txt', { force: true });
877+
rmSync('./newdir', { recursive: true, force: true });
878+
879+
// Copy embedded file to a nested subdirectory that doesn't exist
880+
// fs.cp should create parent directories automatically
881+
const destPath = './newdir/nested/copied.txt';
882+
cpSync(embeddedPath, destPath);
883+
884+
// Verify the copy worked
885+
if (!existsSync(destPath)) throw new Error('cp failed: destination does not exist');
886+
887+
const content = readFileSync(destPath, 'utf8');
888+
if (content.trim() !== 'Subdir test content') {
889+
throw new Error('cp failed: content mismatch - got: ' + content);
890+
}
891+
892+
console.log('fs.cp to subdir from embedded file: OK');
893+
`,
894+
"/subdir-test.txt": "Subdir test content",
895+
},
896+
run: { stdout: "fs.cp to subdir from embedded file: OK", setCwd: true },
897+
});
738898
});

0 commit comments

Comments
 (0)