Skip to content

Commit fc4658b

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 fc4658b

File tree

2 files changed

+236
-0
lines changed

2 files changed

+236
-0
lines changed

src/bun.js/node/node_fs.zig

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,35 @@ 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) {
726+
if (r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) {
727+
this.onCopy(src, dest);
728+
this.finishConcurrently(.success);
729+
return;
730+
}
731+
this.finishConcurrently(.{ .err = .{
732+
.errno = r.err.errno,
733+
.syscall = .copyfile,
734+
.path = nodefs.osPathIntoSyncErrorBuf(src),
735+
} });
736+
return;
737+
}
738+
this.onCopy(src, dest);
739+
this.finishConcurrently(r);
740+
return;
741+
}
742+
}
743+
715744
if (Environment.isWindows) {
716745
const attributes = c.GetFileAttributesW(src);
717746
if (attributes == c.INVALID_FILE_ATTRIBUTES) {
@@ -3498,6 +3527,51 @@ pub const NodeFS = struct {
34983527
return .success;
34993528
}
35003529

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

3594+
// Check if source is an embedded file in a standalone executable
3595+
if (bun.StandaloneModuleGraph.get()) |graph| {
3596+
if (graph.find(args.src.slice())) |file| {
3597+
// Cannot clone an embedded file
3598+
if (args.mode.isForceClone()) {
3599+
return Maybe(Return.CopyFile){ .err = .{
3600+
.errno = @intFromEnum(SystemErrno.ENOTSUP),
3601+
.syscall = .copyfile,
3602+
} };
3603+
}
3604+
var dest_buf: bun.OSPathBuffer = undefined;
3605+
const dest = args.dest.osPath(&dest_buf);
3606+
return copyEmbeddedFileToDestination(dest, file.contents, args.mode);
3607+
}
3608+
}
3609+
35203610
// TODO: do we need to fchown?
35213611
if (comptime Environment.isMac) {
35223612
var src_buf: bun.PathBuffer = undefined;
@@ -6014,6 +6104,30 @@ pub const NodeFS = struct {
60146104
const src = src_buf[0..src_dir_len :0];
60156105
const dest = dest_buf[0..dest_dir_len :0];
60166106

6107+
// Check if source is an embedded file in a standalone executable
6108+
if (bun.StandaloneModuleGraph.get()) |graph| {
6109+
const src_slice = if (Environment.isWindows) brk: {
6110+
var path_buf: bun.PathBuffer = undefined;
6111+
break :brk bun.strings.fromWPath(&path_buf, src);
6112+
} else src;
6113+
if (graph.find(src_slice)) |file| {
6114+
// Embedded files cannot be directories, so copy directly
6115+
const copy_mode: constants.Copyfile = @enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) constants.COPYFILE_EXCL else @as(u8, 0));
6116+
const r = copyEmbeddedFileToDestination(dest, file.contents, copy_mode);
6117+
if (r == .err) {
6118+
if (r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) {
6119+
return .success;
6120+
}
6121+
return .{ .err = .{
6122+
.errno = r.err.errno,
6123+
.syscall = .copyfile,
6124+
.path = this.osPathIntoSyncErrorBuf(src),
6125+
} };
6126+
}
6127+
return r;
6128+
}
6129+
}
6130+
60176131
if (Environment.isWindows) {
60186132
const attributes = c.GetFileAttributesW(src);
60196133
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-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+
});
738860
});

0 commit comments

Comments
 (0)