Skip to content

Commit 18e95c5

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 18e95c5

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed

src/bun.js/node/node_fs.zig

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,27 @@ 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+
const copy_mode: constants.Copyfile = @enumFromInt(if (args.flags.errorOnExist or !args.flags.force) constants.COPYFILE_EXCL else @as(u8, 0));
724+
const r = NodeFS.copyEmbeddedFileToDestination(dest, file.contents, copy_mode);
725+
if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) {
726+
this.onCopy(src, dest);
727+
this.finishConcurrently(.success);
728+
return;
729+
}
730+
this.onCopy(src, dest);
731+
this.finishConcurrently(r);
732+
return;
733+
}
734+
}
735+
715736
if (Environment.isWindows) {
716737
const attributes = c.GetFileAttributesW(src);
717738
if (attributes == c.INVALID_FILE_ATTRIBUTES) {
@@ -3498,6 +3519,51 @@ pub const NodeFS = struct {
34983519
return .success;
34993520
}
35003521

3522+
/// Copy embedded file contents to a destination path.
3523+
/// Used for copying files from standalone executables.
3524+
fn copyEmbeddedFileToDestination(
3525+
dest: bun.OSPathSliceZ,
3526+
contents: []const u8,
3527+
mode: constants.Copyfile,
3528+
) Maybe(Return.CopyFile) {
3529+
const ret = Maybe(Return.CopyFile);
3530+
var flags: i32 = bun.O.CREAT | bun.O.WRONLY | bun.O.TRUNC;
3531+
if (mode.shouldntOverwrite()) {
3532+
flags |= bun.O.EXCL;
3533+
}
3534+
3535+
const dest_fd = switch (Syscall.open(dest, flags, default_permission)) {
3536+
.result => |result| result,
3537+
.err => |err| return .{ .err = err },
3538+
};
3539+
defer dest_fd.close();
3540+
3541+
var buf = contents;
3542+
var written: usize = 0;
3543+
3544+
while (buf.len > 0) {
3545+
switch (bun.sys.write(dest_fd, buf)) {
3546+
.err => |err| return .{ .err = err },
3547+
.result => |amt| {
3548+
buf = buf[amt..];
3549+
written += amt;
3550+
if (amt == 0) {
3551+
break;
3552+
}
3553+
},
3554+
}
3555+
}
3556+
3557+
// Truncate to exact size written
3558+
if (Environment.isWindows) {
3559+
_ = bun.windows.SetEndOfFile(dest_fd.cast());
3560+
} else {
3561+
_ = Syscall.ftruncate(dest_fd, @intCast(@as(u63, @truncate(written))));
3562+
}
3563+
3564+
return ret.success;
3565+
}
3566+
35013567
pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, _: Flavor) Maybe(Return.CopyFile) {
35023568
return switch (this.copyFileInner(args)) {
35033569
.result => .success,
@@ -3517,6 +3583,22 @@ pub const NodeFS = struct {
35173583
fn copyFileInner(fs: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) {
35183584
const ret = Maybe(Return.CopyFile);
35193585

3586+
// Check if source is an embedded file in a standalone executable
3587+
if (bun.StandaloneModuleGraph.get()) |graph| {
3588+
if (graph.find(args.src.slice())) |file| {
3589+
// Cannot clone an embedded file
3590+
if (args.mode.isForceClone()) {
3591+
return Maybe(Return.CopyFile){ .err = .{
3592+
.errno = @intFromEnum(SystemErrno.ENOTSUP),
3593+
.syscall = .copyfile,
3594+
} };
3595+
}
3596+
var dest_buf: bun.PathBuffer = undefined;
3597+
const dest = args.dest.sliceZ(&dest_buf);
3598+
return copyEmbeddedFileToDestination(dest, file.contents, args.mode);
3599+
}
3600+
}
3601+
35203602
// TODO: do we need to fchown?
35213603
if (comptime Environment.isMac) {
35223604
var src_buf: bun.PathBuffer = undefined;
@@ -6014,6 +6096,23 @@ pub const NodeFS = struct {
60146096
const src = src_buf[0..src_dir_len :0];
60156097
const dest = dest_buf[0..dest_dir_len :0];
60166098

6099+
// Check if source is an embedded file in a standalone executable
6100+
if (bun.StandaloneModuleGraph.get()) |graph| {
6101+
const src_slice = if (Environment.isWindows) brk: {
6102+
var path_buf: bun.PathBuffer = undefined;
6103+
break :brk bun.strings.fromWPath(&path_buf, src);
6104+
} else src;
6105+
if (graph.find(src_slice)) |file| {
6106+
// Embedded files cannot be directories, so copy directly
6107+
const copy_mode: constants.Copyfile = @enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) constants.COPYFILE_EXCL else @as(u8, 0));
6108+
const r = copyEmbeddedFileToDestination(dest, file.contents, copy_mode);
6109+
if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) {
6110+
return .success;
6111+
}
6112+
return r;
6113+
}
6114+
}
6115+
60176116
if (Environment.isWindows) {
60186117
const attributes = c.GetFileAttributesW(src);
60196118
if (attributes == c.INVALID_FILE_ATTRIBUTES) {

test/bundler/bundler_compile.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,4 +735,126 @@ 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+
});
829+
830+
itBundled("compile/CpAsyncFromEmbeddedFile", {
831+
compile: true,
832+
assetNaming: "[name].[ext]",
833+
files: {
834+
"/entry.ts": /* js */ `
835+
import { cp, rm } from 'fs/promises';
836+
import { readFileSync, existsSync } from 'fs';
837+
import embeddedPath from './async-source.dat' with { type: 'file' };
838+
839+
// Remove source file from filesystem to verify we're reading from the embedded bundle
840+
await rm('./async-source.dat', { force: true });
841+
842+
// Copy embedded file using async fs.cp (single file mode)
843+
const destPath = './async-dest.dat';
844+
await cp(embeddedPath, destPath);
845+
846+
// Verify the copy worked
847+
if (!existsSync(destPath)) throw new Error('async cp failed: destination does not exist');
848+
849+
const content = readFileSync(destPath, 'utf8');
850+
if (content.trim() !== 'Async cp test data') {
851+
throw new Error('async cp failed: content mismatch - got: ' + content);
852+
}
853+
854+
console.log('fs.cp async from embedded file: OK');
855+
`,
856+
"/async-source.dat": "Async cp test data",
857+
},
858+
run: { stdout: "fs.cp async from embedded file: OK", setCwd: true },
859+
});
738860
});

0 commit comments

Comments
 (0)