Skip to content

Commit 5c0f185

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 5c0f185

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

src/bun.js/node/node_fs.zig

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3498,6 +3498,51 @@ pub const NodeFS = struct {
34983498
return .success;
34993499
}
35003500

3501+
/// Copy embedded file contents to a destination path.
3502+
/// Used for copying files from standalone executables.
3503+
fn copyEmbeddedFileToDestination(
3504+
dest: bun.OSPathSliceZ,
3505+
contents: []const u8,
3506+
mode: constants.Copyfile,
3507+
) Maybe(Return.CopyFile) {
3508+
const ret = Maybe(Return.CopyFile);
3509+
var flags: i32 = bun.O.CREAT | bun.O.WRONLY | bun.O.TRUNC;
3510+
if (mode.shouldntOverwrite()) {
3511+
flags |= bun.O.EXCL;
3512+
}
3513+
3514+
const dest_fd = switch (Syscall.open(dest, flags, default_permission)) {
3515+
.result => |result| result,
3516+
.err => |err| return .{ .err = err },
3517+
};
3518+
defer dest_fd.close();
3519+
3520+
var buf = contents;
3521+
var written: usize = 0;
3522+
3523+
while (buf.len > 0) {
3524+
switch (bun.sys.write(dest_fd, buf)) {
3525+
.err => |err| return .{ .err = err },
3526+
.result => |amt| {
3527+
buf = buf[amt..];
3528+
written += amt;
3529+
if (amt == 0) {
3530+
break;
3531+
}
3532+
},
3533+
}
3534+
}
3535+
3536+
// Truncate to exact size written
3537+
if (Environment.isWindows) {
3538+
_ = bun.windows.SetEndOfFile(dest_fd.cast());
3539+
} else {
3540+
_ = Syscall.ftruncate(dest_fd, @intCast(@as(u63, @truncate(written))));
3541+
}
3542+
3543+
return ret.success;
3544+
}
3545+
35013546
pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, _: Flavor) Maybe(Return.CopyFile) {
35023547
return switch (this.copyFileInner(args)) {
35033548
.result => .success,
@@ -3517,6 +3562,22 @@ pub const NodeFS = struct {
35173562
fn copyFileInner(fs: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) {
35183563
const ret = Maybe(Return.CopyFile);
35193564

3565+
// Check if source is an embedded file in a standalone executable
3566+
if (bun.StandaloneModuleGraph.get()) |graph| {
3567+
if (graph.find(args.src.slice())) |file| {
3568+
// Cannot clone an embedded file
3569+
if (args.mode.isForceClone()) {
3570+
return Maybe(Return.CopyFile){ .err = .{
3571+
.errno = @intFromEnum(SystemErrno.ENOTSUP),
3572+
.syscall = .copyfile,
3573+
} };
3574+
}
3575+
var dest_buf: bun.PathBuffer = undefined;
3576+
const dest = args.dest.sliceZ(&dest_buf);
3577+
return copyEmbeddedFileToDestination(dest, file.contents, args.mode);
3578+
}
3579+
}
3580+
35203581
// TODO: do we need to fchown?
35213582
if (comptime Environment.isMac) {
35223583
var src_buf: bun.PathBuffer = undefined;
@@ -6014,6 +6075,29 @@ pub const NodeFS = struct {
60146075
const src = src_buf[0..src_dir_len :0];
60156076
const dest = dest_buf[0..dest_dir_len :0];
60166077

6078+
// Check if source is an embedded file in a standalone executable
6079+
if (bun.StandaloneModuleGraph.get()) |graph| {
6080+
const src_slice = if (Environment.isWindows) brk: {
6081+
var path_buf: bun.PathBuffer = undefined;
6082+
break :brk bun.strings.fromWPath(&path_buf, src);
6083+
} else src;
6084+
if (graph.find(src_slice)) |file| {
6085+
// Embedded files cannot be directories, so just copy directly
6086+
const r = this._copySingleFileSync(
6087+
src,
6088+
dest,
6089+
@enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) constants.COPYFILE_EXCL else @as(u8, 0)),
6090+
null,
6091+
args,
6092+
);
6093+
if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) {
6094+
return .success;
6095+
}
6096+
_ = file; // Used in _copySingleFileSync via StandaloneModuleGraph.get()
6097+
return r;
6098+
}
6099+
}
6100+
60176101
if (Environment.isWindows) {
60186102
const attributes = c.GetFileAttributesW(src);
60196103
if (attributes == c.INVALID_FILE_ATTRIBUTES) {
@@ -6211,6 +6295,24 @@ pub const NodeFS = struct {
62116295
) Maybe(Return.CopyFile) {
62126296
const ret = Maybe(Return.CopyFile);
62136297

6298+
// Check if source is an embedded file in a standalone executable
6299+
if (bun.StandaloneModuleGraph.get()) |graph| {
6300+
const src_slice = if (Environment.isWindows) brk: {
6301+
var path_buf: bun.PathBuffer = undefined;
6302+
break :brk bun.strings.fromWPath(&path_buf, src);
6303+
} else src;
6304+
if (graph.find(src_slice)) |file| {
6305+
// Cannot clone an embedded file
6306+
if (mode.isForceClone()) {
6307+
return Maybe(Return.CopyFile){ .err = .{
6308+
.errno = @intFromEnum(SystemErrno.ENOTSUP),
6309+
.syscall = .copyfile,
6310+
} };
6311+
}
6312+
return copyEmbeddedFileToDestination(dest, file.contents, mode);
6313+
}
6314+
}
6315+
62146316
// TODO: do we need to fchown?
62156317
if (Environment.isMac) {
62166318
if (mode.isForceClone()) {

test/bundler/bundler_compile.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,4 +735,95 @@ 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 } from 'fs/promises';
775+
import { existsSync } from 'fs';
776+
import embeddedPath from './data.txt' with { type: 'file' };
777+
778+
// Remove data.txt from filesystem to verify we're reading from the embedded bundle
779+
await rm('./data.txt', { force: true });
780+
781+
// Copy embedded file to a new location using async API
782+
const destPath = './copied-data-async.txt';
783+
await copyFile(embeddedPath, destPath);
784+
785+
// Verify the copy worked
786+
if (!existsSync(destPath)) throw new Error('Copy failed: destination does not exist');
787+
788+
const content = await readFile(destPath, 'utf8');
789+
if (content.trim() !== 'Async embedded content!') {
790+
throw new Error('Copy failed: content mismatch - got: ' + content);
791+
}
792+
793+
console.log('fs.copyFile async from embedded file: OK');
794+
`,
795+
"/data.txt": "Async embedded content!",
796+
},
797+
run: { stdout: "fs.copyFile async from embedded file: OK", setCwd: true },
798+
});
799+
800+
itBundled("compile/CpFromEmbeddedFile", {
801+
compile: true,
802+
assetNaming: "[name].[ext]",
803+
files: {
804+
"/entry.ts": /* js */ `
805+
import { cpSync, readFileSync, rmSync, existsSync } from 'fs';
806+
import embeddedPath from './source.dat' with { type: 'file' };
807+
808+
// Remove source.dat from filesystem to verify we're reading from the embedded bundle
809+
rmSync('./source.dat', { force: true });
810+
811+
// Copy embedded file using fs.cp (single file mode)
812+
const destPath = './dest.dat';
813+
cpSync(embeddedPath, destPath);
814+
815+
// Verify the copy worked
816+
if (!existsSync(destPath)) throw new Error('cp failed: destination does not exist');
817+
818+
const content = readFileSync(destPath, 'utf8');
819+
if (content.trim() !== 'Data from cp test') {
820+
throw new Error('cp failed: content mismatch - got: ' + content);
821+
}
822+
823+
console.log('fs.cp from embedded file: OK');
824+
`,
825+
"/source.dat": "Data from cp test",
826+
},
827+
run: { stdout: "fs.cp from embedded file: OK", setCwd: true },
828+
});
738829
});

0 commit comments

Comments
 (0)