Compare commits

...

4 Commits

Author SHA1 Message Date
autofix-ci[bot]
7a6c95c484 [autofix.ci] apply automated fixes 2025-08-14 07:22:15 +00:00
Claude Bot
51738a9646 fix: Windows resource editing PE checksum and structure issues
- Add proper PE checksum calculation and update after modifications
- Fix resource directory structure bugs that caused malformed resources
- Fix icon and version resource nesting levels
- Fix resource data entry offset calculations
- Remove incorrect high bit setting for data entries

This should resolve executables being rejected by Windows after resource modifications.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 07:19:59 +00:00
Claude
3afc1f9594 feat: add Windows resource editing support for bun build --compile
- Implement VS_VERSIONINFO string table support with proper UTF-16LE encoding
- Add icon embedding support (RT_ICON, RT_GROUP_ICON resources)
- Add hide console window option
- Fix resource table offset calculations to prevent integer overflow
- Use bun.strings functions for proper UTF-8 to UTF-16 conversion
- Add proper 32-bit alignment throughout VS_VERSIONINFO structure

Co-authored-by: Claude <claude@anthropic.com>
2025-08-02 08:48:35 +02:00
Claude
1c54474dfc feat: add Windows resource editing support for bun build --compile
This PR implements native Windows resource editing in Zig, replacing the previous
rescle C++ implementation. Users can now customize Windows executables when using
'bun build --compile' with the following new options:

- --windows-icon <path>        Set custom executable icon
- --windows-title <str>        Set executable title/product name
- --windows-publisher <str>    Set company/publisher name
- --windows-version <str>      Set version (e.g. "1.2.3.4")
- --windows-description <str>  Set executable description
- --windows-hide-console       Hide console window (already existed)

Example:
```bash
bun build --compile \
  --target=bun-windows-x64 \
  --windows-icon=app.ico \
  --windows-title="My Application" \
  --windows-publisher="My Company" \
  --windows-version="2.1.0.0" \
  --windows-description="A powerful application built with Bun" \
  index.ts
```

Implementation details:
- Pure Zig implementation in windows_resources.zig
- Removes C++ rescle dependency
- Creates WindowsSettings struct to organize all Windows options
- Supports cross-platform compilation (build Windows exe from Linux/macOS)
- Fixed alignment issues using safe memory operations
- Comprehensive test coverage in test/bundler/windows-resources-compile.test.ts

This allows full customization of Windows executable metadata without external dependencies.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 06:55:12 +02:00
13 changed files with 1648 additions and 1397 deletions

View File

@@ -645,10 +645,6 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "build.zig")
set(BUN_USOCKETS_SOURCE ${CWD}/packages/bun-usockets)
if(WIN32)
list(APPEND BUN_CXX_SOURCES ${CWD}/src/bun.js/bindings/windows/rescle.cpp)
list(APPEND BUN_CXX_SOURCES ${CWD}/src/bun.js/bindings/windows/rescle-binding.cpp)
endif()
register_repository(
NAME

View File

@@ -491,7 +491,7 @@ pub const StandaloneModuleGraph = struct {
windows_hide_console: bool = false,
};
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) bun.FileDescriptor {
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) !bun.FileDescriptor {
var buf: bun.PathBuffer = undefined;
var zname: [:0]const u8 = bun.span(bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @as(u64, @bitCast(std.time.milliTimestamp()))) catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get temporary file name: {s}", .{@errorName(err)});
@@ -503,7 +503,7 @@ pub const StandaloneModuleGraph = struct {
// Ensure we own the file
if (Environment.isPosix) {
// Make the file writable so we can delete it
_ = Syscall.fchmod(fd, 0o777);
_ = Syscall.fchmod(fd, 0o755);
}
fd.close();
_ = Syscall.unlink(name);
@@ -559,7 +559,7 @@ pub const StandaloneModuleGraph = struct {
const fd = brk2: {
var tried_changing_abs_dir = false;
for (0..3) |retry| {
switch (Syscall.open(zname, bun.O.CLOEXEC | bun.O.RDWR | bun.O.CREAT, 0)) {
switch (Syscall.open(zname, bun.O.CLOEXEC | bun.O.RDWR | bun.O.CREAT, 0o755)) {
.result => |res| break :brk2 res,
.err => |err| {
if (retry < 2) {
@@ -627,6 +627,12 @@ pub const StandaloneModuleGraph = struct {
cleanup(zname, fd);
Global.exit(1);
};
// Ensure the file has proper permissions after copying
if (comptime !Environment.isWindows) {
_ = bun.c.fchmod(fd.native(), 0o644);
}
break :brk fd;
};
@@ -678,51 +684,91 @@ pub const StandaloneModuleGraph = struct {
Global.exit(1);
};
if (comptime !Environment.isWindows) {
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o755);
}
return cloned_executable_fd;
},
.windows => {
const input_result = bun.sys.File.readToEnd(.{ .handle = cloned_executable_fd }, bun.default_allocator);
if (input_result.err) |err| {
Output.prettyErrorln("Error reading standalone module graph: {}", .{err});
cleanup(zname, cloned_executable_fd);
Global.exit(1);
}
var pe_file = bun.pe.PEFile.init(bun.default_allocator, input_result.bytes.items) catch |err| {
Output.prettyErrorln("Error initializing PE file: {}", .{err});
cleanup(zname, cloned_executable_fd);
Global.exit(1);
};
defer pe_file.deinit();
pe_file.addBunSection(bytes) catch |err| {
Output.prettyErrorln("Error adding Bun section to PE file: {}", .{err});
cleanup(zname, cloned_executable_fd);
Global.exit(1);
};
input_result.bytes.deinit();
// Implement .bun section support for Windows executables
const pe_module = @import("./pe.zig");
switch (Syscall.setFileOffset(cloned_executable_fd, 0)) {
// Get the path from fd
var path_buf: bun.PathBuffer = undefined;
const path = cloned_executable_fd.getFdPath(&path_buf) catch |err| {
Output.prettyErrorln("Error getting executable path: {}", .{err});
cleanup(zname, cloned_executable_fd);
Global.exit(1);
};
// We need to close the fd before modifying the file
cloned_executable_fd.close();
// Create temporary path for PE with .bun section
const tmp_path = std.fmt.allocPrint(bun.default_allocator, "{s}.bun.tmp", .{path}) catch |err| {
Output.prettyErrorln("Error allocating temporary path: {}", .{err});
cleanup(zname, cloned_executable_fd);
Global.exit(1);
};
defer bun.default_allocator.free(tmp_path);
// Load the PE file
const pe_data = std.fs.cwd().readFileAlloc(bun.default_allocator, path, std.math.maxInt(usize)) catch |err| {
Output.prettyErrorln("Error reading PE file: {}", .{err});
Global.exit(1);
};
defer bun.default_allocator.free(pe_data);
var pe_obj = pe_module.PEFile.init(bun.default_allocator, pe_data) catch |err| {
Output.prettyErrorln("Error parsing PE file: {}", .{err});
Global.exit(1);
};
defer pe_obj.deinit();
// Add .bun section
pe_obj.addBunSection(bytes) catch |err| {
Output.prettyErrorln("Error adding .bun section: {}", .{err});
Global.exit(1);
};
// Write to temporary file
const tmp_file = std.fs.cwd().createFile(tmp_path, .{}) catch |err| {
Output.prettyErrorln("Error creating temporary file: {}", .{err});
Global.exit(1);
};
defer tmp_file.close();
pe_obj.write(tmp_file.writer()) catch |err| {
Output.prettyErrorln("Error writing PE file: {}", .{err});
std.fs.cwd().deleteFile(tmp_path) catch {};
Global.exit(1);
};
// Move temporary file to final location
std.fs.cwd().rename(tmp_path, path) catch |err| {
Output.prettyErrorln("Error renaming temporary file: {}", .{err});
std.fs.cwd().deleteFile(tmp_path) catch {};
Global.exit(1);
};
// Reopen the modified file
var path_z_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
const path_z = std.fmt.bufPrintZ(&path_z_buf, "{s}", .{path}) catch {
Output.prettyErrorln("Error creating null-terminated path", .{});
Global.exit(1);
};
const modified_fd = switch (Syscall.open(path_z, bun.O.CLOEXEC | bun.O.RDWR, 0)) {
.result => |res| res,
.err => |err| {
Output.prettyErrorln("Error seeking to start of temporary file: {}", .{err});
cleanup(zname, cloned_executable_fd);
Output.prettyErrorln("Error reopening modified executable: {}", .{err});
Global.exit(1);
},
else => {},
}
var file = bun.sys.File{ .handle = cloned_executable_fd };
const writer = file.writer();
pe_file.write(writer) catch |err| {
Output.prettyErrorln("Error writing PE file: {}", .{err});
cleanup(zname, cloned_executable_fd);
Global.exit(1);
};
// Set executable permissions when running on POSIX hosts, even for Windows targets
if (comptime !Environment.isWindows) {
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
// Set executable permissions (not needed on Windows)
if (comptime !bun.Environment.isWindows) {
_ = bun.c.fchmod(modified_fd.native(), 0o755);
}
return cloned_executable_fd;
return modified_fd;
},
else => {
var total_byte_count: usize = undefined;
@@ -788,7 +834,7 @@ pub const StandaloneModuleGraph = struct {
// the final 8 bytes in the file are the length of the module graph with padding, excluding the trailer and offsets
_ = Syscall.write(cloned_executable_fd, std.mem.asBytes(&total_byte_count));
if (comptime !Environment.isWindows) {
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o755);
}
return cloned_executable_fd;
@@ -831,13 +877,12 @@ pub const StandaloneModuleGraph = struct {
outfile: []const u8,
env: *bun.DotEnv.Loader,
output_format: bun.options.Format,
windows_hide_console: bool,
windows_icon: ?[]const u8,
windows: bun.options.WindowsSettings,
) !void {
const bytes = try toBytes(allocator, module_prefix, output_files, output_format);
if (bytes.len == 0) return;
const fd = inject(
const fd = try inject(
bytes,
if (target.isDefault())
bun.selfExePath() catch |err| {
@@ -849,7 +894,7 @@ pub const StandaloneModuleGraph = struct {
Output.err(err, "failed to download cross-compiled bun executable", .{});
Global.exit(1);
},
.{ .windows_hide_console = windows_hide_console },
.{ .windows_hide_console = windows.hide_console },
target,
);
bun.debugAssert(fd.kind == .system);
@@ -875,15 +920,75 @@ pub const StandaloneModuleGraph = struct {
Global.exit(1);
};
fd.close();
// Apply Windows resource edits if needed
if (windows.icon != null or windows.title != null or windows.publisher != null or windows.version != null or windows.description != null or windows.hide_console) {
const pe_module = @import("./pe.zig");
if (windows_icon) |icon_utf8| {
var icon_buf: bun.OSPathBuffer = undefined;
const icon = bun.strings.toWPathNormalized(&icon_buf, icon_utf8);
bun.windows.rescle.setIcon(outfile_slice, icon) catch {
Output.warn("Failed to set executable icon", .{});
// Get file size
const size = if (Environment.isWindows)
Syscall.setFileOffsetToEndWindows(fd).unwrap() catch |err| {
Output.err(err, "failed to get file size", .{});
Global.exit(1);
}
else blk: {
const fstat = Syscall.fstat(fd).unwrap() catch |err| {
Output.err(err, "failed to stat file", .{});
Global.exit(1);
};
break :blk @as(usize, @intCast(fstat.size));
};
_ = Syscall.setFileOffset(fd, 0).unwrap() catch |err| {
Output.err(err, "failed to seek to start", .{});
Global.exit(1);
};
// Read entire file
const pe_data = try allocator.alloc(u8, size);
defer allocator.free(pe_data);
const read_result = Syscall.readAll(fd, pe_data);
_ = read_result.unwrap() catch |err| {
Output.err(err, "failed to read PE file", .{});
Global.exit(1);
};
var pe_file = try pe_module.PEFile.init(allocator, pe_data);
defer pe_file.deinit();
pe_file.applyWindowsSettings(&windows, allocator) catch |err| {
if (windows.icon != null and (err == error.InvalidIconData or err == error.InvalidIconFormat)) {
Output.errGeneric("Invalid icon file: {s}", .{windows.icon.?});
Output.flush();
Global.exit(1);
}
return err;
};
// Write back modified PE
var write_buffer = std.ArrayList(u8).init(allocator);
defer write_buffer.deinit();
try pe_file.write(write_buffer.writer());
// Seek to start and write
_ = Syscall.setFileOffset(fd, 0).unwrap() catch |err| {
Output.err(err, "failed to seek to start for write", .{});
Global.exit(1);
};
_ = Syscall.write(fd, write_buffer.items).unwrap() catch |err| {
Output.err(err, "failed to write modified PE", .{});
Global.exit(1);
};
// Truncate if new size is smaller
_ = Syscall.ftruncate(fd, @intCast(write_buffer.items.len)).unwrap() catch |err| {
Output.err(err, "failed to truncate file", .{});
Global.exit(1);
};
}
fd.close();
return;
}
@@ -911,6 +1016,57 @@ pub const StandaloneModuleGraph = struct {
Global.exit(1);
};
// Apply Windows resource edits if needed
if (target.os == .windows and (windows.icon != null or windows.title != null or windows.publisher != null or windows.version != null or windows.description != null or windows.hide_console)) {
const pe_module = @import("./pe.zig");
// Read the PE file
const pe_data = std.fs.cwd().readFileAlloc(allocator, outfile, std.math.maxInt(usize)) catch |err| {
Output.err(err, "failed to read PE file for resource editing", .{});
Global.exit(1);
};
defer allocator.free(pe_data);
// Parse and modify PE
var pe_obj = pe_module.PEFile.init(allocator, pe_data) catch |err| {
Output.err(err, "failed to parse PE file", .{});
Global.exit(1);
};
defer pe_obj.deinit();
pe_obj.applyWindowsSettings(&windows, allocator) catch |err| {
if (windows.icon != null and (err == error.InvalidIconData or err == error.InvalidIconFormat)) {
Output.errGeneric("Invalid icon file: {s}", .{windows.icon.?});
} else {
Output.err(err, "failed to apply Windows settings", .{});
}
Global.exit(1);
};
// Write to temporary file then rename
const tmp_name = try std.fmt.allocPrint(allocator, "{s}.tmp", .{outfile});
defer allocator.free(tmp_name);
{
const tmp_file = std.fs.cwd().createFile(tmp_name, .{}) catch |err| {
Output.err(err, "failed to create temporary file", .{});
Global.exit(1);
};
defer tmp_file.close();
pe_obj.write(tmp_file.writer()) catch |err| {
Output.err(err, "failed to write modified PE", .{});
Global.exit(1);
};
}
std.fs.cwd().rename(tmp_name, outfile) catch |err| {
Output.err(err, "failed to replace PE file", .{});
std.fs.cwd().deleteFile(tmp_name) catch {};
Global.exit(1);
};
}
}
pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph {

View File

@@ -1,14 +0,0 @@
#include "root.h"
#include "rescle.h"
extern "C" int rescle__setIcon(const WCHAR* exeFilename, const WCHAR* iconFilename)
{
rescle::ResourceUpdater updater;
if (!updater.Load(exeFilename))
return -1;
if (!updater.SetIcon(iconFilename))
return -2;
if (!updater.Commit())
return -3;
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,211 +0,0 @@
// This file is from Electron's fork of rescle
// https://github.com/electron/rcedit/blob/e36b688b42df0e236922019ce14e0ea165dc176d/src/rescle.h
// 'bun build --compile' uses this on Windows to allow
// patching the icon of the generated executable.
//
// Copyright (c) 2013 GitHub Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Copyright (c) 2013 GitHub, Inc. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
//
// This file is modified from Rescle written by yoshio.okumura@gmail.com:
// http://code.google.com/p/rescle/
#ifndef VERSION_INFO_UPDATER
#define VERSION_INFO_UPDATER
#ifndef _UNICODE
#define _UNICODE
#endif
#ifndef UNICODE
#define UNICODE
#endif
#include <string>
#include <vector>
#include <map>
#include <windows.h>
#include <memory> // unique_ptr
#define RU_VS_COMMENTS L"Comments"
#define RU_VS_COMPANY_NAME L"CompanyName"
#define RU_VS_FILE_DESCRIPTION L"FileDescription"
#define RU_VS_FILE_VERSION L"FileVersion"
#define RU_VS_INTERNAL_NAME L"InternalName"
#define RU_VS_LEGAL_COPYRIGHT L"LegalCopyright"
#define RU_VS_LEGAL_TRADEMARKS L"LegalTrademarks"
#define RU_VS_ORIGINAL_FILENAME L"OriginalFilename"
#define RU_VS_PRIVATE_BUILD L"PrivateBuild"
#define RU_VS_PRODUCT_NAME L"ProductName"
#define RU_VS_PRODUCT_VERSION L"ProductVersion"
#define RU_VS_SPECIAL_BUILD L"SpecialBuild"
namespace rescle {
struct IconsValue {
typedef struct _ICONENTRY {
BYTE width;
BYTE height;
BYTE colorCount;
BYTE reserved;
WORD planes;
WORD bitCount;
DWORD bytesInRes;
DWORD imageOffset;
} ICONENTRY;
typedef struct _ICONHEADER {
WORD reserved;
WORD type;
WORD count;
std::vector<ICONENTRY> entries;
} ICONHEADER;
ICONHEADER header;
std::vector<std::vector<BYTE>> images;
std::vector<BYTE> grpHeader;
};
struct Translate {
LANGID wLanguage;
WORD wCodePage;
};
typedef std::pair<std::wstring, std::wstring> VersionString;
typedef std::pair<const BYTE* const, const size_t> OffsetLengthPair;
struct VersionStringTable {
Translate encoding;
std::vector<VersionString> strings;
};
class VersionInfo {
public:
VersionInfo();
VersionInfo(HMODULE hModule, WORD languageId);
std::vector<BYTE> Serialize() const;
bool HasFixedFileInfo() const;
VS_FIXEDFILEINFO& GetFixedFileInfo();
const VS_FIXEDFILEINFO& GetFixedFileInfo() const;
void SetFixedFileInfo(const VS_FIXEDFILEINFO& value);
std::vector<VersionStringTable> stringTables;
std::vector<Translate> supportedTranslations;
private:
VS_FIXEDFILEINFO fixedFileInfo_;
void FillDefaultData();
void DeserializeVersionInfo(const BYTE* pData, size_t size);
VersionStringTable DeserializeVersionStringTable(const BYTE* tableData);
void DeserializeVersionStringFileInfo(const BYTE* offset, size_t length, std::vector<VersionStringTable>& stringTables);
void DeserializeVarFileInfo(const unsigned char* offset, std::vector<Translate>& translations);
OffsetLengthPair GetChildrenData(const BYTE* entryData);
};
class ResourceUpdater {
public:
typedef std::vector<std::wstring> StringValues;
typedef std::map<UINT, StringValues> StringTable;
typedef std::map<WORD, StringTable> StringTableMap;
typedef std::map<LANGID, VersionInfo> VersionStampMap;
typedef std::map<UINT, std::unique_ptr<IconsValue>> IconTable;
typedef std::vector<BYTE> RcDataValue;
typedef std::map<ptrdiff_t, RcDataValue> RcDataMap;
typedef std::map<LANGID, RcDataMap> RcDataLangMap;
struct IconResInfo {
UINT maxIconId = 0;
IconTable iconBundles;
};
typedef std::map<LANGID, IconResInfo> IconTableMap;
ResourceUpdater();
~ResourceUpdater();
bool Load(const WCHAR* filename);
bool SetVersionString(WORD languageId, const WCHAR* name, const WCHAR* value);
bool SetVersionString(const WCHAR* name, const WCHAR* value);
const WCHAR* GetVersionString(WORD languageId, const WCHAR* name);
const WCHAR* GetVersionString(const WCHAR* name);
bool SetProductVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
bool SetProductVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
bool SetFileVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
bool SetFileVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
bool ChangeString(WORD languageId, UINT id, const WCHAR* value);
bool ChangeString(UINT id, const WCHAR* value);
bool ChangeRcData(UINT id, const WCHAR* pathToResource);
const WCHAR* GetString(WORD languageId, UINT id);
const WCHAR* GetString(UINT id);
bool SetIcon(const WCHAR* path, const LANGID& langId, UINT iconBundle);
bool SetIcon(const WCHAR* path, const LANGID& langId);
bool SetIcon(const WCHAR* path);
bool SetExecutionLevel(const WCHAR* value);
bool IsExecutionLevelSet();
bool SetApplicationManifest(const WCHAR* value);
bool IsApplicationManifestSet();
bool Commit();
private:
bool SerializeStringTable(const StringValues& values, UINT blockId, std::vector<char>* out);
static BOOL CALLBACK OnEnumResourceName(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam);
static BOOL CALLBACK OnEnumResourceManifest(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam);
static BOOL CALLBACK OnEnumResourceLanguage(HANDLE hModule, LPCWSTR lpszType, LPCWSTR lpszName, WORD wIDLanguage, LONG_PTR lParam);
HMODULE module_;
std::wstring filename_;
std::wstring executionLevel_;
std::wstring originalExecutionLevel_;
std::wstring applicationManifestPath_;
std::wstring manifestString_;
VersionStampMap versionStampMap_;
StringTableMap stringTableMap_;
IconTableMap iconBundleMap_;
RcDataLangMap rcDataLngMap_;
};
class ScopedResourceUpdater {
public:
ScopedResourceUpdater(const WCHAR* filename, bool deleteOld);
~ScopedResourceUpdater();
HANDLE Get() const;
bool Commit();
private:
bool EndUpdate(bool doesCommit);
HANDLE handle_;
bool commited_ = false;
};
} // namespace rescle
#endif // VERSION_INFO_UPDATER

View File

@@ -420,8 +420,7 @@ pub const Command = struct {
// Compile options
compile: bool = false,
compile_target: Cli.CompileTarget = .{},
windows_hide_console: bool = false,
windows_icon: ?[]const u8 = null,
windows: options.WindowsSettings = .{},
};
pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context {

View File

@@ -171,6 +171,10 @@ pub const build_only_params = [_]ParamType{
clap.parseParam("--env <inline|prefix*|disable> Inline environment variables into the bundle as process.env.${name}. Defaults to 'disable'. To inline environment variables matching a prefix, use my prefix like 'FOO_PUBLIC_*'.") catch unreachable,
clap.parseParam("--windows-hide-console When using --compile targeting Windows, prevent a Command prompt from opening alongside the executable") catch unreachable,
clap.parseParam("--windows-icon <STR> When using --compile targeting Windows, assign an executable icon") catch unreachable,
clap.parseParam("--windows-title <STR> When using --compile targeting Windows, set the executable title") catch unreachable,
clap.parseParam("--windows-publisher <STR> When using --compile targeting Windows, set the executable publisher") catch unreachable,
clap.parseParam("--windows-version <STR> When using --compile targeting Windows, set the executable version (e.g. 1.2.3.4)") catch unreachable,
clap.parseParam("--windows-description <STR> When using --compile targeting Windows, set the executable description") catch unreachable,
} ++ if (FeatureFlags.bake_debugging_features) [_]ParamType{
clap.parseParam("--debug-dump-server-files When --app is set, dump all server files to disk even when building statically") catch unreachable,
clap.parseParam("--debug-no-minify When --app is set, do not minify anything") catch unreachable,
@@ -884,26 +888,47 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
if (args.flag("--windows-hide-console")) {
// --windows-hide-console technically doesnt depend on WinAPI, but since since --windows-icon
// does, all of these customization options have been gated to windows-only
if (!Environment.isWindows) {
Output.errGeneric("Using --windows-hide-console is only available when compiling on Windows", .{});
Global.crash();
}
// --windows-hide-console is now supported cross-platform
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-hide-console requires --compile", .{});
Global.crash();
}
ctx.bundler_options.windows_hide_console = true;
ctx.bundler_options.windows.hide_console = true;
}
if (args.option("--windows-icon")) |path| {
if (!Environment.isWindows) {
Output.errGeneric("Using --windows-icon is only available when compiling on Windows", .{});
Global.crash();
}
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-icon requires --compile", .{});
Global.crash();
}
ctx.bundler_options.windows_icon = path;
ctx.bundler_options.windows.icon = path;
}
if (args.option("--windows-title")) |title| {
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-title requires --compile", .{});
Global.crash();
}
ctx.bundler_options.windows.title = title;
}
if (args.option("--windows-publisher")) |publisher| {
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-publisher requires --compile", .{});
Global.crash();
}
ctx.bundler_options.windows.publisher = publisher;
}
if (args.option("--windows-version")) |version| {
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-version requires --compile", .{});
Global.crash();
}
ctx.bundler_options.windows.version = version;
}
if (args.option("--windows-description")) |description| {
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-description requires --compile", .{});
Global.crash();
}
ctx.bundler_options.windows.description = description;
}
if (args.option("--outdir")) |outdir| {

View File

@@ -434,8 +434,7 @@ pub const BuildCommand = struct {
outfile,
this_transpiler.env,
this_transpiler.options.output_format,
ctx.bundler_options.windows_hide_console,
ctx.bundler_options.windows_icon,
ctx.bundler_options.windows,
);
const compiled_elapsed = @divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bundled_end)), @as(i64, std.time.ns_per_ms));
const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) {

View File

@@ -11,6 +11,15 @@ pub const WriteDestination = enum {
// eventually: wasm
};
pub const WindowsSettings = struct {
hide_console: bool = false,
icon: ?[]const u8 = null,
title: ?[]const u8 = null,
publisher: ?[]const u8 = null,
version: ?[]const u8 = null,
description: ?[]const u8 = null,
};
pub fn validatePath(
log: *logger.Log,
_: *Fs.FileSystem.Implementation,

View File

@@ -107,6 +107,101 @@ pub const PEFile = struct {
const IMAGE_SCN_MEM_WRITE = 0x80000000;
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
// Resource types
const RT_ICON = 3;
const RT_GROUP_ICON = 14;
const RT_VERSION = 16;
// Language and code page IDs
const LANGUAGE_ID_EN_US: u16 = 1033; // 0x0409, en-US
const CODE_PAGE_ID_EN_US: u16 = 1200; // 0x04B0, UTF-16LE
// Version info constants
const VS_FFI_SIGNATURE: u32 = 0xFEEF04BD;
const VS_FFI_STRUCVERSION: u32 = 0x00010000;
const VS_FFI_FILEFLAGSMASK: u32 = 0x0000003F;
const VOS_NT_WINDOWS32: u32 = 0x00040004;
const VFT_APP: u32 = 0x00000001;
// Resource directory structures
const ResourceDirectoryTable = extern struct {
characteristics: u32,
time_date_stamp: u32,
major_version: u16,
minor_version: u16,
number_of_name_entries: u16,
number_of_id_entries: u16,
};
const ResourceDirectoryEntry = extern struct {
name_or_id: u32,
offset_to_data: u32,
};
const ResourceDataEntry = extern struct {
offset_to_data: u32,
size: u32,
code_page: u32,
reserved: u32,
};
// Icon structures
const IconDirectory = extern struct {
reserved: u16,
type: u16,
count: u16,
};
const IconDirectoryEntry = extern struct {
width: u8,
height: u8,
color_count: u8,
reserved: u8,
planes: u16,
bit_count: u16,
bytes_in_res: u32,
image_offset: u32,
};
const GroupIconDirectoryEntry = extern struct {
width: u8,
height: u8,
color_count: u8,
reserved: u8,
planes: u16,
bit_count: u16,
bytes_in_res: u32,
id: u16,
};
// Version info structures
const VS_FIXEDFILEINFO = extern struct {
signature: u32,
struct_version: u32,
file_version_ms: u32,
file_version_ls: u32,
product_version_ms: u32,
product_version_ls: u32,
file_flags_mask: u32,
file_flags: u32,
file_os: u32,
file_type: u32,
file_subtype: u32,
file_date_ms: u32,
file_date_ls: u32,
};
const VS_VERSIONINFO = struct {
length: u16,
value_length: u16,
type: u16,
key: []const u16, // "VS_VERSION_INFO"
padding1: []const u8,
fixed_file_info: VS_FIXEDFILEINFO,
padding2: []const u8,
children: []const u8,
};
// Helper methods to safely access headers
fn getDosHeader(self: *const PEFile) *DOSHeader {
return @ptrCast(@alignCast(self.data.items.ptr + self.dos_header_offset));
@@ -276,6 +371,9 @@ pub const PEFile = struct {
const updated_optional_header = self.getOptionalHeader();
updated_optional_header.size_of_image = alignSize(new_section.virtual_address + new_section.virtual_size, updated_optional_header.section_alignment);
updated_optional_header.size_of_initialized_data += new_section.size_of_raw_data;
// Update PE checksum - critical for Windows to accept the executable
self.updateChecksum();
}
/// Find the .bun section and return its data
@@ -330,6 +428,59 @@ pub const PEFile = struct {
return error.BunSectionNotFound;
}
/// Calculate PE checksum using the standard Windows algorithm
pub fn calculateChecksum(self: *const PEFile) u32 {
const data = self.data.items;
const file_size = data.len;
// Find checksum field offset
const checksum_offset = self.optional_header_offset + @offsetOf(OptionalHeader64, "checksum");
var checksum: u64 = 0;
var i: usize = 0;
// Process file as 16-bit words
while (i + 1 < file_size) : (i += 2) {
// Skip the checksum field itself (4 bytes)
if (i >= checksum_offset and i < checksum_offset + 4) {
continue;
}
// Add 16-bit word to checksum
const word = std.mem.readInt(u16, data[i..][0..2], .little);
checksum += word;
// Handle overflow - fold back the carry
if (checksum > 0xFFFF) {
checksum = (checksum & 0xFFFF) + (checksum >> 16);
}
}
// If file size is odd, last byte is treated as if followed by 0x00
if (file_size & 1 != 0) {
checksum += data[file_size - 1];
if (checksum > 0xFFFF) {
checksum = (checksum & 0xFFFF) + (checksum >> 16);
}
}
// Final fold
checksum = (checksum & 0xFFFF) + (checksum >> 16);
checksum = (checksum + (checksum >> 16)) & 0xFFFF;
// Add file size to checksum
checksum += file_size;
return @intCast(checksum);
}
/// Update the PE checksum field
pub fn updateChecksum(self: *PEFile) void {
const checksum = self.calculateChecksum();
const optional_header = self.getOptionalHeader();
optional_header.checksum = checksum;
}
/// Write the modified PE file
pub fn write(self: *const PEFile, writer: anytype) !void {
try writer.writeAll(self.data.items);
@@ -363,6 +514,696 @@ pub const PEFile = struct {
}
}
}
// Resource editing functionality
fn getResourceSection(self: *const PEFile) ?*SectionHeader {
const section_headers = self.getSectionHeaders();
for (section_headers) |*section| {
if (strings.eqlComptime(section.name[0..6], ".rsrc\x00")) {
return section;
}
}
return null;
}
fn getResourceDirectory(self: *const PEFile) !?*ResourceDirectoryTable {
const rsrc_section = self.getResourceSection() orelse return null;
if (rsrc_section.pointer_to_raw_data >= self.data.items.len or
rsrc_section.pointer_to_raw_data + rsrc_section.size_of_raw_data > self.data.items.len)
{
return error.InvalidResourceSection;
}
return @ptrCast(@alignCast(self.data.items.ptr + rsrc_section.pointer_to_raw_data));
}
fn findResourceEntry(self: *const PEFile, dir_offset: u32, resource_type: u32, resource_id: u32, language_id: u16) !?*ResourceDataEntry {
const rsrc_section = self.getResourceSection() orelse return null;
const rsrc_base = rsrc_section.pointer_to_raw_data;
// Level 1: Type
const type_dir: *ResourceDirectoryTable = @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + dir_offset));
const type_entries = @as([*]ResourceDirectoryEntry, @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + dir_offset + @sizeOf(ResourceDirectoryTable))));
const total_entries = type_dir.number_of_name_entries + type_dir.number_of_id_entries;
var type_entry: ?*ResourceDirectoryEntry = null;
for (0..total_entries) |i| {
if ((type_entries[i].name_or_id & 0x7FFFFFFF) == resource_type) {
type_entry = &type_entries[i];
break;
}
}
if (type_entry == null) return null;
if ((type_entry.?.offset_to_data & 0x80000000) == 0) return null; // Must be directory
// Level 2: Name/ID
const name_dir_offset = type_entry.?.offset_to_data & 0x7FFFFFFF;
const name_dir: *ResourceDirectoryTable = @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + name_dir_offset));
const name_entries = @as([*]ResourceDirectoryEntry, @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + name_dir_offset + @sizeOf(ResourceDirectoryTable))));
var name_entry: ?*ResourceDirectoryEntry = null;
for (0..name_dir.number_of_name_entries + name_dir.number_of_id_entries) |i| {
if ((name_entries[i].name_or_id & 0x7FFFFFFF) == resource_id) {
name_entry = &name_entries[i];
break;
}
}
if (name_entry == null) return null;
if ((name_entry.?.offset_to_data & 0x80000000) == 0) return null; // Must be directory
// Level 3: Language
const lang_dir_offset = name_entry.?.offset_to_data & 0x7FFFFFFF;
const lang_dir: *ResourceDirectoryTable = @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + lang_dir_offset));
const lang_entries = @as([*]ResourceDirectoryEntry, @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + lang_dir_offset + @sizeOf(ResourceDirectoryTable))));
for (0..lang_dir.number_of_named_entries + lang_dir.number_of_id_entries) |i| {
if ((lang_entries[i].name_or_id & 0x7FFFFFFF) == language_id) {
if ((lang_entries[i].offset_to_data & 0x80000000) == 0) {
// This is a data entry
return @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + lang_entries[i].offset_to_data));
}
}
}
return null;
}
pub fn applyWindowsSettings(self: *PEFile, settings: *const bun.options.WindowsSettings, allocator: Allocator) !void {
// Handle hide console first (simple modification)
if (settings.hide_console) {
const optional_header = self.getOptionalHeader();
// Change subsystem from IMAGE_SUBSYSTEM_WINDOWS_CUI (3) to IMAGE_SUBSYSTEM_WINDOWS_GUI (2)
if (optional_header.subsystem == 3) {
optional_header.subsystem = 2;
}
}
// If no resource modifications needed, return early
if (settings.icon == null and settings.version == null and settings.description == null and
settings.publisher == null and settings.title == null)
{
return;
}
// Find or create resource section
var rsrc_section = self.getResourceSection();
if (rsrc_section == null) {
try self.createResourceSection();
rsrc_section = self.getResourceSection() orelse return error.FailedToCreateResourceSection;
}
// Build new resource directory
var resource_builder = ResourceBuilder.init(allocator);
defer resource_builder.deinit();
// Load and process icon if provided
if (settings.icon) |icon_path| {
// Simple approach - just read the file
const icon_data = std.fs.cwd().readFileAlloc(allocator, icon_path, std.math.maxInt(usize)) catch {
return error.FileNotFound;
};
defer allocator.free(icon_data);
try resource_builder.setIcon(icon_data);
}
// Build version info if any version fields provided
if (settings.version != null or settings.description != null or
settings.publisher != null or settings.title != null)
{
const version_str = if (settings.version) |v| v else "1.0.0.0";
try resource_builder.setVersionInfo(
version_str,
settings.description,
settings.publisher,
settings.title,
);
}
// Build the resource data
const resource_data = try resource_builder.build(rsrc_section.?.virtual_address);
defer allocator.free(resource_data);
// Update the resource section
try self.updateResourceSection(rsrc_section.?, resource_data);
// Update PE checksum after all modifications
self.updateChecksum();
}
fn createResourceSection(self: *PEFile) !void {
const section_name = ".rsrc\x00\x00\x00";
const optional_header = self.getOptionalHeader();
// Check if we can add another section
if (self.num_sections >= 95) { // PE limit is 96 sections
return error.TooManySections;
}
// Find the last section to determine where to place the new one
var last_section_end: u32 = 0;
var last_virtual_end: u32 = 0;
const section_headers = self.getSectionHeaders();
for (section_headers) |section| {
const section_file_end = section.pointer_to_raw_data + section.size_of_raw_data;
const section_virtual_end = section.virtual_address + alignSize(section.virtual_size, optional_header.section_alignment);
if (section_file_end > last_section_end) {
last_section_end = section_file_end;
}
if (section_virtual_end > last_virtual_end) {
last_virtual_end = section_virtual_end;
}
}
// Create new section header
const new_section = SectionHeader{
.name = section_name.*,
.virtual_size = 0x1000, // Start with 4KB
.virtual_address = alignSize(last_virtual_end, optional_header.section_alignment),
.size_of_raw_data = alignSize(0x1000, optional_header.file_alignment),
.pointer_to_raw_data = alignSize(last_section_end, optional_header.file_alignment),
.pointer_to_relocations = 0,
.pointer_to_line_numbers = 0,
.number_of_relocations = 0,
.number_of_line_numbers = 0,
.characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,
};
// Resize data to accommodate new section
const new_data_size = new_section.pointer_to_raw_data + new_section.size_of_raw_data;
try self.data.resize(new_data_size);
// Zero out the new section data
@memset(self.data.items[last_section_end..new_data_size], 0);
// Write the section header
const new_section_offset = self.section_headers_offset + @sizeOf(SectionHeader) * self.num_sections;
const new_section_ptr: *SectionHeader = @ptrCast(@alignCast(self.data.items.ptr + new_section_offset));
new_section_ptr.* = new_section;
// Update PE header
const pe_header = self.getPEHeader();
pe_header.number_of_sections += 1;
self.num_sections += 1;
// Update optional header
const updated_optional_header = self.getOptionalHeader();
updated_optional_header.size_of_image = alignSize(new_section.virtual_address + new_section.virtual_size, updated_optional_header.section_alignment);
// Update resource directory RVA
updated_optional_header.data_directories[2].virtual_address = new_section.virtual_address;
updated_optional_header.data_directories[2].size = new_section.virtual_size;
}
fn updateResourceSection(self: *PEFile, section: *SectionHeader, data: []const u8) !void {
const optional_header = self.getOptionalHeader();
// Calculate aligned size
const aligned_size = alignSize(@intCast(data.len), optional_header.file_alignment);
// Check if we need to resize the section
if (aligned_size > section.size_of_raw_data) {
// This is complex - would need to move all following sections
// For now, just error if the resource data is too large
return error.ResourceDataTooLarge;
}
// Update section data
const section_offset = section.pointer_to_raw_data;
@memcpy(self.data.items[section_offset..][0..data.len], data);
// Zero out remaining space
if (data.len < section.size_of_raw_data) {
@memset(self.data.items[section_offset + data.len .. section_offset + section.size_of_raw_data], 0);
}
// Update section header
section.virtual_size = @intCast(data.len);
// Update data directory
optional_header.data_directories[2].size = @intCast(data.len);
}
};
// Resource builder for creating Windows PE resources
const ResourceBuilder = struct {
allocator: Allocator,
root: ResourceTable,
const ResourceTable = struct {
entries: std.ArrayList(ResourceTableEntry),
};
const ResourceTableEntry = struct {
id: u32,
subtable: ?*ResourceTable = null,
data: ?[]const u8 = null,
data_offset: ?u32 = null,
data_size: ?u32 = null,
code_page: u32 = PEFile.CODE_PAGE_ID_EN_US,
};
pub fn init(allocator: Allocator) ResourceBuilder {
return .{
.allocator = allocator,
.root = .{
.entries = std.ArrayList(ResourceTableEntry).init(allocator),
},
};
}
pub fn deinit(self: *ResourceBuilder) void {
// Recursively free all entries
self.freeTable(&self.root);
}
fn freeTable(self: *ResourceBuilder, table: *ResourceTable) void {
for (table.entries.items) |*entry| {
if (entry.subtable) |subtable| {
self.freeTable(subtable);
self.allocator.destroy(subtable);
}
if (entry.data) |data| {
self.allocator.free(data);
}
}
table.entries.deinit();
}
pub fn setIcon(self: *ResourceBuilder, icon_data: []const u8) !void {
// Parse ICO file header
if (icon_data.len < @sizeOf(PEFile.IconDirectory)) {
return error.InvalidIconFile;
}
const icon_dir = std.mem.bytesAsValue(PEFile.IconDirectory, icon_data[0..@sizeOf(PEFile.IconDirectory)]).*;
if (icon_dir.reserved != 0 or icon_dir.type != 1) {
return error.InvalidIconFormat;
}
// Get or create RT_ICON table
const icon_table = try self.getOrCreateTable(&self.root, PEFile.RT_ICON);
// Find first free icon ID
var first_free_icon_id: u32 = 1;
for (icon_table.entries.items) |entry| {
if (entry.id >= first_free_icon_id) {
first_free_icon_id = entry.id + 1;
}
}
// Read icon entries
var offset: usize = @sizeOf(PEFile.IconDirectory);
var group_icon_data = std.ArrayList(u8).init(self.allocator);
defer group_icon_data.deinit();
// Write GRPICONDIR header
try group_icon_data.appendSlice(std.mem.asBytes(&icon_dir));
var i: usize = 0;
while (i < icon_dir.count) : (i += 1) {
if (offset + @sizeOf(PEFile.IconDirectoryEntry) > icon_data.len) {
return error.InvalidIconFile;
}
const entry = std.mem.bytesAsValue(PEFile.IconDirectoryEntry, icon_data[offset..][0..@sizeOf(PEFile.IconDirectoryEntry)]).*;
offset += @sizeOf(PEFile.IconDirectoryEntry);
// Read the actual icon image data
if (entry.image_offset + entry.bytes_in_res > icon_data.len) {
return error.InvalidIconFile;
}
const image_data = icon_data[entry.image_offset..][0..entry.bytes_in_res];
const icon_id = first_free_icon_id + @as(u32, @intCast(i));
// Add individual icon to RT_ICON table
const id_table = try self.getOrCreateTable(icon_table, icon_id);
// At the language level, add data directly instead of creating another table
const data_copy = try self.allocator.dupe(u8, image_data);
try id_table.entries.append(.{
.id = PEFile.LANGUAGE_ID_EN_US,
.data = data_copy,
.data_size = @intCast(data_copy.len),
.code_page = PEFile.CODE_PAGE_ID_EN_US,
});
// Create GRPICONDIRENTRY for group icon
const grp_entry = PEFile.GroupIconDirectoryEntry{
.width = entry.width,
.height = entry.height,
.color_count = entry.color_count,
.reserved = entry.reserved,
.planes = entry.planes,
.bit_count = entry.bit_count,
.bytes_in_res = entry.bytes_in_res,
.id = @intCast(icon_id),
};
try group_icon_data.appendSlice(std.mem.asBytes(&grp_entry));
}
// Get or create RT_GROUP_ICON table
const group_table = try self.getOrCreateTable(&self.root, PEFile.RT_GROUP_ICON);
const name_table = try self.getOrCreateTable(group_table, 1); // MAINICON ID
// At the language level, add data directly instead of creating another table
const group_data_copy = try group_icon_data.toOwnedSlice();
try name_table.entries.append(.{
.id = PEFile.LANGUAGE_ID_EN_US,
.data = group_data_copy,
.data_size = @intCast(group_data_copy.len),
.code_page = PEFile.CODE_PAGE_ID_EN_US,
});
}
// Helper to write a string as UTF-16LE with null terminator
fn writeUtf16String(data: *std.ArrayList(u8), str: []const u8) !void {
// Calculate the length first
const utf16_len = strings.elementLengthUTF8IntoUTF16([]const u8, str);
// Ensure we have capacity for the UTF-16 data plus null terminator
const start_len = data.items.len;
try data.ensureUnusedCapacity((utf16_len + 1) * 2);
// Resize to make room for UTF-16 data
data.items.len = start_len + (utf16_len * 2);
// Convert UTF-8 to UTF-16LE in place
// We need to use a temporary buffer due to alignment requirements
var utf16_buf: [1024]u16 = undefined;
const utf16_result = strings.convertUTF8toUTF16InBuffer(utf16_buf[0..utf16_len], str);
// Copy UTF-16 bytes to the output
const utf16_bytes = std.mem.sliceAsBytes(utf16_result);
@memcpy(data.items[start_len..][0..utf16_bytes.len], utf16_bytes);
// Add null terminator
try data.append(0);
try data.append(0);
}
// Helper to align to 32-bit boundary
fn alignTo32Bit(data: *std.ArrayList(u8)) !void {
while (data.items.len % 4 != 0) {
try data.append(0);
}
}
const VersionHeader = extern struct {
wLength: u16,
wValueLength: u16,
wType: u16,
};
pub fn setVersionInfo(self: *ResourceBuilder, version: []const u8, description: ?[]const u8, company: ?[]const u8, product: ?[]const u8) !void {
// Parse version string
var version_parts: [4]u16 = .{ 1, 0, 0, 0 };
var iter = std.mem.tokenizeScalar(u8, version, '.');
var i: usize = 0;
while (iter.next()) |part| : (i += 1) {
if (i >= 4) break;
version_parts[i] = std.fmt.parseInt(u16, part, 10) catch 0;
}
const file_version_ms = (@as(u32, version_parts[0]) << 16) | version_parts[1];
const file_version_ls = (@as(u32, version_parts[2]) << 16) | version_parts[3];
// Build VS_VERSIONINFO structure
var data = std.ArrayList(u8).init(self.allocator);
defer data.deinit();
// VS_VERSIONINFO root structure
const vs_version_info_start = data.items.len;
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = @sizeOf(PEFile.VS_FIXEDFILEINFO), .wType = 0 }));
try writeUtf16String(&data, "VS_VERSION_INFO");
try alignTo32Bit(&data);
// VS_FIXEDFILEINFO
const fixed_info = PEFile.VS_FIXEDFILEINFO{
.signature = PEFile.VS_FFI_SIGNATURE,
.struct_version = PEFile.VS_FFI_STRUCVERSION,
.file_version_ms = file_version_ms,
.file_version_ls = file_version_ls,
.product_version_ms = file_version_ms,
.product_version_ls = file_version_ls,
.file_flags_mask = PEFile.VS_FFI_FILEFLAGSMASK,
.file_flags = 0,
.file_os = PEFile.VOS_NT_WINDOWS32,
.file_type = PEFile.VFT_APP,
.file_subtype = 0,
.file_date_ms = 0,
.file_date_ls = 0,
};
try data.appendSlice(std.mem.asBytes(&fixed_info));
try alignTo32Bit(&data);
// StringFileInfo
const string_file_info_start = data.items.len;
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
try writeUtf16String(&data, "StringFileInfo");
try alignTo32Bit(&data);
// StringTable for 040904B0 (US English, Unicode)
const string_table_start = data.items.len;
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
try writeUtf16String(&data, "040904B0");
try alignTo32Bit(&data);
// Add string entries
const version_strings = [_]struct { key: []const u8, value: ?[]const u8 }{
.{ .key = "CompanyName", .value = company },
.{ .key = "FileDescription", .value = description },
.{ .key = "FileVersion", .value = version },
.{ .key = "ProductName", .value = product },
.{ .key = "ProductVersion", .value = version },
};
for (version_strings) |str| {
if (str.value) |value| {
const string_start = data.items.len;
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
try writeUtf16String(&data, str.key);
try alignTo32Bit(&data);
// Write value and update header
const value_start = data.items.len;
try writeUtf16String(&data, value);
const value_len = @divExact(data.items.len - value_start, 2); // Length in WORDs, including null
// Update string header
const string_len = data.items.len - string_start;
if (string_len > std.math.maxInt(u16)) return error.StringTooLong;
if (value_len > std.math.maxInt(u16)) return error.ValueTooLong;
std.mem.writeInt(u16, data.items[string_start..][0..2], @intCast(string_len), .little);
std.mem.writeInt(u16, data.items[string_start + 2 ..][0..2], @intCast(value_len), .little);
try alignTo32Bit(&data);
}
}
// Update StringTable header
const string_table_len = data.items.len - string_table_start;
if (string_table_len > std.math.maxInt(u16)) return error.StringTableTooLong;
std.mem.writeInt(u16, data.items[string_table_start..][0..2], @intCast(string_table_len), .little);
// Update StringFileInfo header
const string_file_info_len = data.items.len - string_file_info_start;
if (string_file_info_len > std.math.maxInt(u16)) return error.StringFileInfoTooLong;
std.mem.writeInt(u16, data.items[string_file_info_start..][0..2], @intCast(string_file_info_len), .little);
// VarFileInfo
const var_file_info_start = data.items.len;
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
try writeUtf16String(&data, "VarFileInfo");
try alignTo32Bit(&data);
// Translation
const translation_start = data.items.len;
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 4, .wType = 0 }));
try writeUtf16String(&data, "Translation");
try alignTo32Bit(&data);
// Language and code page
try data.appendSlice(&[_]u8{ 0x09, 0x04, 0xB0, 0x04 }); // 0x0409, 0x04B0
// Update Translation header
const translation_len = data.items.len - translation_start;
if (translation_len > std.math.maxInt(u16)) return error.TranslationTooLong;
std.mem.writeInt(u16, data.items[translation_start..][0..2], @intCast(translation_len), .little);
// Update VarFileInfo header
const var_file_info_len = data.items.len - var_file_info_start;
if (var_file_info_len > std.math.maxInt(u16)) return error.VarFileInfoTooLong;
std.mem.writeInt(u16, data.items[var_file_info_start..][0..2], @intCast(var_file_info_len), .little);
// Update VS_VERSIONINFO header
const vs_version_info_len = data.items.len - vs_version_info_start;
if (vs_version_info_len > std.math.maxInt(u16)) return error.VersionInfoTooLong;
std.mem.writeInt(u16, data.items[vs_version_info_start..][0..2], @intCast(vs_version_info_len), .little);
// Add to resource table
const version_table = try self.getOrCreateTable(&self.root, PEFile.RT_VERSION);
const id_table = try self.getOrCreateTable(version_table, 1);
const version_bytes = try data.toOwnedSlice();
try id_table.entries.append(.{
.id = PEFile.LANGUAGE_ID_EN_US,
.data = version_bytes,
.data_size = @intCast(version_bytes.len),
.code_page = PEFile.CODE_PAGE_ID_EN_US,
});
}
fn getOrCreateTable(self: *ResourceBuilder, parent: *ResourceTable, id: u32) !*ResourceTable {
// Look for existing entry
for (parent.entries.items) |*entry| {
if (entry.id == id) {
if (entry.subtable) |subtable| {
return subtable;
}
return error.ExpectedDirectory;
}
}
// Create new subtable
const new_table = try self.allocator.create(ResourceTable);
new_table.* = .{
.entries = std.ArrayList(ResourceTableEntry).init(self.allocator),
};
try parent.entries.append(.{
.id = id,
.subtable = new_table,
});
return new_table;
}
pub fn build(self: *ResourceBuilder, virtual_address: u32) ![]u8 {
var tables = std.ArrayList(u8).init(self.allocator);
defer tables.deinit();
var data_entries = std.ArrayList(u8).init(self.allocator);
defer data_entries.deinit();
var data_bytes = std.ArrayList(u8).init(self.allocator);
defer data_bytes.deinit();
// Calculate total sizes first
var total_table_size: u32 = 0;
var total_data_entries: u32 = 0;
self.calculateTableSizes(&self.root, &total_table_size, &total_data_entries);
// Now build with known offsets
var tables_offset: u32 = 0;
var data_entries_offset = total_table_size;
var data_offset = total_table_size + (total_data_entries * @sizeOf(PEFile.ResourceDataEntry));
try self.writeTableRecursive(&tables, &data_entries, &data_bytes, virtual_address, &self.root, &tables_offset, &data_entries_offset, &data_offset);
// Combine all parts
var output = std.ArrayList(u8).init(self.allocator);
try output.appendSlice(tables.items);
try output.appendSlice(data_entries.items);
try output.appendSlice(data_bytes.items);
return output.toOwnedSlice();
}
fn calculateTableSizes(self: *const ResourceBuilder, table: *const ResourceTable, table_size: *u32, data_entries: *u32) void {
const entry_count = table.entries.items.len;
const size_increase = @sizeOf(PEFile.ResourceDirectoryTable) + entry_count * @sizeOf(PEFile.ResourceDirectoryEntry);
table_size.* += @as(u32, @intCast(size_increase));
for (table.entries.items) |*entry| {
if (entry.subtable) |subtable| {
self.calculateTableSizes(subtable, table_size, data_entries);
} else if (entry.data != null) {
data_entries.* += 1; // Count the number of data entries, not their size
}
}
}
fn writeTableRecursive(
self: *ResourceBuilder,
tables: *std.ArrayList(u8),
data_entries: *std.ArrayList(u8),
data_bytes: *std.ArrayList(u8),
virtual_address: u32,
table: *const ResourceTable,
tables_offset: *u32,
data_entries_offset: *u32,
data_offset: *u32,
) !void {
_ = tables.items.len; // dir_start - may be used for debugging
// Write directory header
const dir_header = PEFile.ResourceDirectoryTable{
.characteristics = 0,
.time_date_stamp = 0,
.major_version = 0,
.minor_version = 0,
.number_of_name_entries = 0,
.number_of_id_entries = @intCast(table.entries.items.len),
};
try tables.appendSlice(std.mem.asBytes(&dir_header));
// Calculate where subdirectories will be placed
var subdirs = std.ArrayList(struct { entry: *const ResourceTableEntry, offset: u32 }).init(self.allocator);
defer subdirs.deinit();
var next_table_offset = tables_offset.* + @as(u32, @intCast(tables.items.len + table.entries.items.len * @sizeOf(PEFile.ResourceDirectoryEntry)));
// Write directory entries
for (table.entries.items) |*entry| {
if (entry.subtable) |subtable| {
// Calculate subdirectory size
var subdir_size: u32 = 0;
var subdir_data_entries: u32 = 0;
self.calculateTableSizes(subtable, &subdir_size, &subdir_data_entries);
const dir_entry = PEFile.ResourceDirectoryEntry{
.name_or_id = entry.id,
.offset_to_data = 0x80000000 | (next_table_offset - tables_offset.*),
};
try tables.appendSlice(std.mem.asBytes(&dir_entry));
try subdirs.append(.{ .entry = entry, .offset = next_table_offset });
next_table_offset += subdir_size;
} else if (entry.data) |_| {
// Calculate offset to the ResourceDataEntry from start of resource section
const data_entry_offset = data_entries_offset.* + @as(u32, @intCast(data_entries.items.len));
const dir_entry = PEFile.ResourceDirectoryEntry{
.name_or_id = entry.id,
.offset_to_data = data_entry_offset, // No high bit for data entry (points to ResourceDataEntry)
};
try tables.appendSlice(std.mem.asBytes(&dir_entry));
// Write the data entry
const data_byte_offset = data_offset.* + @as(u32, @intCast(data_bytes.items.len));
const res_data_entry = PEFile.ResourceDataEntry{
.offset_to_data = virtual_address + data_byte_offset,
.size = entry.data_size.?,
.code_page = entry.code_page,
.reserved = 0,
};
try data_entries.appendSlice(std.mem.asBytes(&res_data_entry));
try data_bytes.appendSlice(entry.data.?);
}
}
tables_offset.* = next_table_offset;
// Write subdirectories
for (subdirs.items) |subdir| {
try self.writeTableRecursive(tables, data_entries, data_bytes, virtual_address, subdir.entry.subtable.?, tables_offset, data_entries_offset, data_offset);
}
}
};
/// Align size to the nearest multiple of alignment

View File

@@ -2331,9 +2331,25 @@ pub fn pidfd_open(pid: std.os.linux.pid_t, flags: u32) Maybe(i32) {
unreachable;
}
pub fn lseek(fd: bun.FileDescriptor, offset: i64, whence: usize) Maybe(usize) {
pub fn lseek(fd: bun.FileDescriptor, offset: i64, whence: c_int) Maybe(usize) {
if (comptime Environment.isWindows) {
var new_ptr: std.os.windows.LARGE_INTEGER = undefined;
const rc = kernel32.SetFilePointerEx(fd.cast(), @as(windows.LARGE_INTEGER, @bitCast(offset)), &new_ptr, @as(u32, @intCast(whence)));
if (rc == windows.FALSE) {
return Maybe(usize).errnoSysFd(0, .lseek, fd) orelse Maybe(usize){ .result = 0 };
}
return Maybe(usize){ .result = @as(usize, @intCast(new_ptr)) };
}
while (true) {
const rc = syscall.lseek(fd.cast(), offset, whence);
const rc = switch (Environment.os) {
.linux => syscall.lseek(fd.cast(), offset, @as(usize, @intCast(whence))),
.mac => blk: {
const result = syscall.lseek(fd.cast(), offset, whence);
break :blk @as(usize, @intCast(result));
},
else => @compileError("not implemented"),
};
if (Maybe(usize).errnoSysFd(rc, .lseek, fd)) |err| {
if (err.getErrno() == .INTR) continue;
return err;

View File

@@ -3642,19 +3642,6 @@ pub fn editWin32BinarySubsystem(fd: bun.sys.File, subsystem: Subsystem) !void {
try fd.writer().writeInt(u16, @intFromEnum(subsystem), .little);
}
pub const rescle = struct {
extern fn rescle__setIcon([*:0]const u16, [*:0]const u16) c_int;
pub fn setIcon(exe_path: [*:0]const u16, icon: [*:0]const u16) !void {
comptime bun.assert(bun.Environment.isWindows);
const status = rescle__setIcon(exe_path, icon);
return switch (status) {
0 => {},
else => error.IconEditError,
};
}
};
pub extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(.winapi) BOOL;
pub extern "kernel32" fn GetFinalPathNameByHandleW(hFile: HANDLE, lpszFilePath: [*]u16, cchFilePath: DWORD, dwFlags: DWORD) callconv(.winapi) DWORD;
pub extern "kernel32" fn DeleteFileW(lpFileName: [*:0]const u16) callconv(.winapi) BOOL;

View File

@@ -0,0 +1,538 @@
import { spawn } from "bun";
import { windowsResourceInternals } from "bun:internal-for-testing";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
import { join } from "path";
describe("Windows Resource Editing", () => {
describe("unit tests", () => {
test("parseIconFile parses valid ICO", () => {
const icon = createTestIcon();
const result = windowsResourceInternals.parseIconFile(icon);
expect(result.groupIconData).toBeInstanceOf(Uint8Array);
expect(result.icons).toBeArrayOfSize(1);
expect(result.icons[0].id).toBe(1);
expect(result.icons[0].data).toBeInstanceOf(Uint8Array);
});
test("parseIconFile rejects invalid data", () => {
expect(() => windowsResourceInternals.parseIconFile(Buffer.from("not an icon"))).toThrow();
expect(() => windowsResourceInternals.parseIconFile(Buffer.from([0, 0]))).toThrow();
});
});
// Test icon data (minimal valid ICO file)
const createTestIcon = () => {
// ICO header (6 bytes)
const header = Buffer.from([
0x00,
0x00, // Reserved
0x01,
0x00, // Type (1 = ICO)
0x01,
0x00, // Count (1 icon)
]);
// Directory entry (16 bytes)
const dirEntry = Buffer.from([
0x10, // Width (16)
0x10, // Height (16)
0x00, // Color count (0 = 256 colors)
0x00, // Reserved
0x01,
0x00, // Planes
0x08,
0x00, // Bit count
0x28,
0x01,
0x00,
0x00, // Bytes in resource (296)
0x16,
0x00,
0x00,
0x00, // Image offset (22)
]);
// Minimal BMP data (just a header for simplicity)
const bmpHeader = Buffer.alloc(40);
bmpHeader.writeUInt32LE(40, 0); // Header size
bmpHeader.writeInt32LE(16, 4); // Width
bmpHeader.writeInt32LE(32, 8); // Height (double for AND mask)
bmpHeader.writeUInt16LE(1, 12); // Planes
bmpHeader.writeUInt16LE(8, 14); // Bit count
bmpHeader.writeUInt32LE(0, 16); // Compression
bmpHeader.writeUInt32LE(256, 20); // Image size
// Create minimal image data
const imageData = Buffer.alloc(256); // 16x16x8bpp
return Buffer.concat([header, dirEntry, bmpHeader, imageData]);
};
describe("compile with icon", () => {
test("--windows-icon sets executable icon", async () => {
const dir = tempDirWithFiles("windows-icon", {
"index.js": `console.log("Hello from Bun!");`,
"test.ico": createTestIcon(),
});
const proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
"--windows-icon",
join(dir, "test.ico"),
join(dir, "index.js"),
"--outfile",
join(dir, "test.exe"),
].filter(x => x),
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
console.log("stdout:", stdout);
console.log("stderr:", stderr);
console.log("exitCode:", exitCode);
}
expect(exitCode).toBe(0);
expect(stderr).not.toContain("Failed to set");
// Verify executable exists
const exePath = join(dir, "test.exe");
expect(await Bun.file(exePath).exists()).toBe(true);
// Check file size
const fileInfo = await Bun.file(exePath);
const fileSize = fileInfo.size;
// Parse and verify resources
// Force a small delay to ensure file system operations are complete
await Bun.sleep(100);
// Use Node.js fs to read the file to avoid any potential Bun caching issues
const fs = require("fs");
const exeBuffer = fs.readFileSync(exePath);
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
// Should have icon resources
expect(resources.icons.length).toBeGreaterThan(0);
expect(resources.groupIcons.length).toBe(1);
// Verify icon data matches what we embedded
const originalIcon = windowsResourceInternals.parseIconFile(createTestIcon());
expect(resources.icons[0].data).toEqual(originalIcon.icons[0].data);
});
test("invalid icon file shows error", async () => {
const dir = tempDirWithFiles("windows-icon-invalid", {
"index.js": `console.log("Hello!");`,
"bad.ico": Buffer.from("not an icon"),
});
const proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
"--windows-icon",
join(dir, "bad.ico"),
join(dir, "index.js"),
"--outfile",
join(dir, "test.exe"),
].filter(x => x),
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid");
});
});
describe("compile with version info", () => {
test("--windows-version and --windows-description set version info", async () => {
const dir = tempDirWithFiles("windows-version", {
"index.js": `console.log("Hello from MyApp!");`,
});
const proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
"--windows-version",
"1.2.3.4",
"--windows-description",
"My Test Application",
join(dir, "index.js"),
"--outfile",
join(dir, "myapp.exe"),
].filter(x => x),
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).not.toContain("Failed to set");
// Verify executable exists
const exePath = join(dir, "myapp.exe");
expect(await Bun.file(exePath).exists()).toBe(true);
// Parse and verify resources
await Bun.sleep(100);
const fs = require("fs");
const exeBuffer = fs.readFileSync(exePath);
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
// Should have version info
expect(resources.versionInfo).not.toBeNull();
expect(resources.versionInfo.fileVersion).toBe("1.2.3.4");
expect(resources.versionInfo.fileDescription).toBe("My Test Application");
});
test("all Windows options together", async () => {
const dir = tempDirWithFiles("windows-all", {
"index.js": `console.log("Complete app!");`,
"app.ico": createTestIcon(),
});
const proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
"--windows-icon",
join(dir, "app.ico"),
"--windows-version",
"10.5.3.1",
"--windows-description",
"Complete Test Application",
"--windows-hide-console",
join(dir, "index.js"),
"--outfile",
join(dir, "super.exe"),
].filter(x => x),
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const exePath = join(dir, "super.exe");
expect(await Bun.file(exePath).exists()).toBe(true);
// Parse and verify all resources
await Bun.sleep(100);
const fs = require("fs");
const exeBuffer = fs.readFileSync(exePath);
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
// Verify icon
expect(resources.icons.length).toBeGreaterThan(0);
expect(resources.groupIcons.length).toBe(1);
// Verify version info
expect(resources.versionInfo).not.toBeNull();
expect(resources.versionInfo.fileVersion).toBe("10.5.3.1");
expect(resources.versionInfo.fileDescription).toBe("Complete Test Application");
});
});
describe("cross-platform compilation", () => {
test("can set Windows resources when compiling for Windows from non-Windows", async () => {
// Skip if already on Windows
if (isWindows) {
return;
}
const dir = tempDirWithFiles("windows-cross", {
"index.js": `console.log("Cross-compiled!");`,
"icon.ico": createTestIcon(),
});
const proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--target=bun-windows-x64-v1.2.19",
"--windows-icon",
join(dir, "icon.ico"),
"--windows-version",
"2.0.0.0",
"--windows-description",
"Cross Platform App",
join(dir, "index.js"),
"--outfile",
join(dir, "cross.exe"),
],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(0);
// Should produce a Windows executable
const exePath = join(dir, "cross.exe");
expect(await Bun.file(exePath).exists()).toBe(true);
// Verify resources are properly embedded even when cross-compiling
await Bun.sleep(100);
const fs = require("fs");
const exeBuffer = fs.readFileSync(exePath);
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
expect(resources.icons.length).toBeGreaterThan(0);
expect(resources.versionInfo).not.toBeNull();
expect(resources.versionInfo.fileVersion).toBe("2.0.0.0");
expect(resources.versionInfo.fileDescription).toBe("Cross Platform App");
});
});
describe("error handling", () => {
test("--windows-description requires --compile", async () => {
const dir = tempDirWithFiles("windows-no-compile", {
"index.js": `console.log("test");`,
});
const proc = spawn({
cmd: [bunExe(), "build", "--windows-description", "Test", join(dir, "index.js")],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("--windows-description requires --compile");
});
test("--windows-icon requires --compile", async () => {
const dir = tempDirWithFiles("windows-icon-no-compile", {
"index.js": `console.log("test");`,
"test.ico": createTestIcon(),
});
const proc = spawn({
cmd: [bunExe(), "build", "--windows-icon", join(dir, "test.ico"), join(dir, "index.js")],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("--windows-icon requires --compile");
});
test("--windows-version requires --compile", async () => {
const dir = tempDirWithFiles("windows-version-no-compile", {
"index.js": `console.log("test");`,
});
const proc = spawn({
cmd: [bunExe(), "build", "--windows-version", "1.0.0.0", join(dir, "index.js")],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("--windows-version requires --compile");
});
test("invalid version format shows error", async () => {
const dir = tempDirWithFiles("windows-bad-version", {
"index.js": `console.log("test");`,
});
const proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--target=bun-windows-x64-v1.2.19",
"--windows-version",
"not-a-version",
join(dir, "index.js"),
],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Invalid");
});
});
describe("version parsing", () => {
test("accepts valid version formats", async () => {
const dir = tempDirWithFiles("windows-version-formats", {
"index.js": `console.log("Version test");`,
});
const testCases = [
{ version: "1.0.0.0", expected: "1.0.0.0" },
{ version: "255.255.65535.65535", expected: "255.255.65535.65535" },
{ version: "0.0.0.1", expected: "0.0.0.1" },
];
for (const { version, expected } of testCases) {
await using proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--target=bun-windows-x64-v1.2.19",
`--outfile=version-${version.replace(/\./g, "-")}.exe`,
`--windows-version=${version}`,
join(dir, "index.js"),
],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Verify version in resources
const exePath = join(dir, `version-${version.replace(/\./g, "-")}.exe`);
await Bun.sleep(100);
const fs = require("fs");
const exeBuffer = fs.readFileSync(exePath);
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
expect(resources.versionInfo?.fileVersion).toBe(expected);
}
});
test("rejects invalid version formats", async () => {
const dir = tempDirWithFiles("windows-bad-versions", {
"index.js": `console.log("test");`,
});
const invalidVersions = [
"1", // too few parts
"1.2", // too few parts
"1.2.3", // too few parts
"1.2.3.4.5", // too many parts
"a.b.c.d", // non-numeric
"1.2.3.-1", // negative number
"65536.0.0.0", // overflow
];
for (const version of invalidVersions) {
await using proc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--target=bun-windows-x64-v1.2.19",
`--windows-version=${version}`,
join(dir, "index.js"),
],
cwd: dir,
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Invalid");
}
});
});
// Run actual executable on Windows
if (isWindows) {
describe("runtime verification", () => {
test("executable with resources runs correctly", async () => {
const dir = tempDirWithFiles("windows-runtime", {
"app.js": `console.log("Running with resources!");`,
"app.ico": createTestIcon(),
});
// Build with resources
await using buildProc = spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--windows-icon",
join(dir, "app.ico"),
"--windows-version",
"1.0.0.0",
"--windows-description",
"Runtime Test App",
join(dir, "app.js"),
"--outfile",
join(dir, "app.exe"),
],
cwd: dir,
env: bunEnv,
});
expect(await buildProc.exited).toBe(0);
// Run the executable
await using runProc = spawn({
cmd: [join(dir, "app.exe")],
cwd: dir,
stdout: "pipe",
});
const [stdout, exitCode] = await Promise.all([new Response(runProc.stdout).text(), runProc.exited]);
expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("Running with resources!");
});
});
}
});