Skip to content

Commit 817a65f

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 817a65f

File tree

2 files changed

+294
-0
lines changed

2 files changed

+294
-0
lines changed

src/bun.js/node/node_fs.zig

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,36 @@ 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+
this.onCopy(src, dest);
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+
} });
737+
return;
738+
}
739+
this.onCopy(src, dest);
740+
this.finishConcurrently(r);
741+
return;
742+
}
743+
}
744+
715745
if (Environment.isWindows) {
716746
const attributes = c.GetFileAttributesW(src);
717747
if (attributes == c.INVALID_FILE_ATTRIBUTES) {
@@ -3498,6 +3528,72 @@ pub const NodeFS = struct {
34983528
return .success;
34993529
}
35003530

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

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

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

test/bundler/bundler_compile.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,4 +735,160 @@ 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-async.txt' with { type: 'file' };
777+
778+
// Remove data-async.txt from filesystem to verify we're reading from the embedded bundle
779+
await rm('./data-async.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-async.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+
});
860+
861+
itBundled("compile/CpFromEmbeddedFileToSubdir", {
862+
compile: true,
863+
assetNaming: "[name].[ext]",
864+
files: {
865+
"/entry.ts": /* js */ `
866+
import { cpSync, readFileSync, rmSync, existsSync, rmdirSync } from 'fs';
867+
import embeddedPath from './subdir-test.txt' with { type: 'file' };
868+
869+
// Remove source file and any existing subdir from filesystem
870+
rmSync('./subdir-test.txt', { force: true });
871+
rmSync('./newdir/nested/copied.txt', { force: true });
872+
try { rmdirSync('./newdir/nested'); } catch {}
873+
try { rmdirSync('./newdir'); } catch {}
874+
875+
// Copy embedded file to a nested subdirectory that doesn't exist
876+
// fs.cp should create parent directories automatically
877+
const destPath = './newdir/nested/copied.txt';
878+
cpSync(embeddedPath, destPath);
879+
880+
// Verify the copy worked
881+
if (!existsSync(destPath)) throw new Error('cp failed: destination does not exist');
882+
883+
const content = readFileSync(destPath, 'utf8');
884+
if (content.trim() !== 'Subdir test content') {
885+
throw new Error('cp failed: content mismatch - got: ' + content);
886+
}
887+
888+
console.log('fs.cp to subdir from embedded file: OK');
889+
`,
890+
"/subdir-test.txt": "Subdir test content",
891+
},
892+
run: { stdout: "fs.cp to subdir from embedded file: OK", setCwd: true },
893+
});
738894
});

0 commit comments

Comments
 (0)