mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
37 Commits
dylan/pyth
...
feat/windo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f17a9bd4b1 | ||
|
|
f06277735a | ||
|
|
75cce4d78a | ||
|
|
c3276726b6 | ||
|
|
43e59a711b | ||
|
|
b3425b1ba8 | ||
|
|
3a728be5d9 | ||
|
|
8c3af1b936 | ||
|
|
e446832c6f | ||
|
|
ae7a28aa8e | ||
|
|
54276895d8 | ||
|
|
961be1a340 | ||
|
|
aec3ea143f | ||
|
|
fdeb08847e | ||
|
|
eac0dcee15 | ||
|
|
052fb98b00 | ||
|
|
e6f6162a24 | ||
|
|
75553543d2 | ||
|
|
d782311caf | ||
|
|
e1eed4013c | ||
|
|
16afbeefc9 | ||
|
|
78d6119144 | ||
|
|
e70da99349 | ||
|
|
4497fcf73b | ||
|
|
77d26a5dad | ||
|
|
81351d6878 | ||
|
|
d916717b87 | ||
|
|
d8d325f50f | ||
|
|
62a3d33243 | ||
|
|
6fbc9626db | ||
|
|
e8b5f21947 | ||
|
|
78c9440600 | ||
|
|
6e5cafe3f5 | ||
|
|
2d6300a8c1 | ||
|
|
ea814317a4 | ||
|
|
3afc1f9594 | ||
|
|
1c54474dfc |
@@ -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
|
||||
|
||||
@@ -41,10 +41,11 @@ async function getReleaseInfo(tag: string): Promise<ReleaseInfo> {
|
||||
*/
|
||||
async function countCompletedIssues(sinceDate: string): Promise<{ count: number; issues: number[] }> {
|
||||
try {
|
||||
const result = await $`gh issue list --state closed --search "closed:>=${sinceDate} reason:completed" --limit 1000 --json number,closedAt,stateReason`.json() as Issue[];
|
||||
|
||||
const result =
|
||||
(await $`gh issue list --state closed --search "closed:>=${sinceDate} reason:completed" --limit 1000 --json number,closedAt,stateReason`.json()) as Issue[];
|
||||
|
||||
const completedIssues = result.filter(issue => issue.stateReason === "COMPLETED");
|
||||
|
||||
|
||||
return {
|
||||
count: completedIssues.length,
|
||||
issues: completedIssues.map(issue => issue.number),
|
||||
@@ -59,7 +60,7 @@ async function countCompletedIssues(sinceDate: string): Promise<{ count: number;
|
||||
*/
|
||||
async function getIssueReactions(issueNumber: number): Promise<number> {
|
||||
try {
|
||||
const reactions = await $`gh api "repos/oven-sh/bun/issues/${issueNumber}/reactions"`.json() as Reaction[];
|
||||
const reactions = (await $`gh api "repos/oven-sh/bun/issues/${issueNumber}/reactions"`.json()) as Reaction[];
|
||||
return reactions.filter(r => ["+1", "heart", "hooray", "rocket"].includes(r.content)).length;
|
||||
} catch {
|
||||
return 0;
|
||||
@@ -71,18 +72,19 @@ async function getIssueReactions(issueNumber: number): Promise<number> {
|
||||
*/
|
||||
async function getCommentReactions(issueNumber: number): Promise<number> {
|
||||
try {
|
||||
const comments = await $`gh api "repos/oven-sh/bun/issues/${issueNumber}/comments"`.json() as Comment[];
|
||||
|
||||
const comments = (await $`gh api "repos/oven-sh/bun/issues/${issueNumber}/comments"`.json()) as Comment[];
|
||||
|
||||
let totalReactions = 0;
|
||||
for (const comment of comments) {
|
||||
try {
|
||||
const reactions = await $`gh api "repos/oven-sh/bun/issues/comments/${comment.id}/reactions"`.json() as Reaction[];
|
||||
const reactions =
|
||||
(await $`gh api "repos/oven-sh/bun/issues/comments/${comment.id}/reactions"`.json()) as Reaction[];
|
||||
totalReactions += reactions.filter(r => ["+1", "heart", "hooray", "rocket"].includes(r.content)).length;
|
||||
} catch {
|
||||
// Skip if we can't get reactions for this comment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return totalReactions;
|
||||
} catch {
|
||||
return 0;
|
||||
@@ -94,28 +96,30 @@ async function getCommentReactions(issueNumber: number): Promise<number> {
|
||||
*/
|
||||
async function countReactions(issueNumbers: number[], verbose = false): Promise<number> {
|
||||
let totalReactions = 0;
|
||||
|
||||
|
||||
for (const issueNumber of issueNumbers) {
|
||||
if (verbose) {
|
||||
console.log(`Processing issue #${issueNumber}...`);
|
||||
}
|
||||
|
||||
|
||||
const [issueReactions, commentReactions] = await Promise.all([
|
||||
getIssueReactions(issueNumber),
|
||||
getCommentReactions(issueNumber),
|
||||
]);
|
||||
|
||||
|
||||
const issueTotal = issueReactions + commentReactions;
|
||||
totalReactions += issueTotal;
|
||||
|
||||
|
||||
if (verbose && issueTotal > 0) {
|
||||
console.log(` Issue #${issueNumber}: ${issueReactions} issue + ${commentReactions} comment = ${issueTotal} total`);
|
||||
console.log(
|
||||
` Issue #${issueNumber}: ${issueReactions} issue + ${commentReactions} comment = ${issueTotal} total`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Small delay to avoid rate limiting
|
||||
await Bun.sleep(50);
|
||||
}
|
||||
|
||||
|
||||
return totalReactions;
|
||||
}
|
||||
|
||||
@@ -126,41 +130,40 @@ async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const releaseTag = args[0];
|
||||
const verbose = args.includes("--verbose") || args.includes("-v");
|
||||
|
||||
|
||||
if (!releaseTag) {
|
||||
console.error("Usage: bun run scripts/github-metrics.ts <release-tag> [--verbose]");
|
||||
console.error("Example: bun run scripts/github-metrics.ts bun-v1.2.19");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
console.log(`📊 Collecting GitHub metrics since ${releaseTag}...`);
|
||||
|
||||
|
||||
// Get release date
|
||||
const releaseInfo = await getReleaseInfo(releaseTag);
|
||||
const releaseDate = releaseInfo.publishedAt.split("T")[0]; // Extract date part
|
||||
|
||||
|
||||
if (verbose) {
|
||||
console.log(`📅 Release date: ${releaseDate}`);
|
||||
}
|
||||
|
||||
|
||||
// Count completed issues
|
||||
console.log("🔍 Counting completed issues...");
|
||||
const { count: issueCount, issues: issueNumbers } = await countCompletedIssues(releaseDate);
|
||||
|
||||
|
||||
// Count reactions
|
||||
console.log("👍 Counting positive reactions...");
|
||||
const reactionCount = await countReactions(issueNumbers, verbose);
|
||||
|
||||
|
||||
// Display results
|
||||
console.log("\n📈 Results:");
|
||||
console.log(`Issues closed as completed since ${releaseTag}: ${issueCount}`);
|
||||
console.log(`Total positive reactions (👍❤️🎉🚀): ${reactionCount}`);
|
||||
|
||||
|
||||
if (issueCount > 0) {
|
||||
console.log(`Average reactions per completed issue: ${(reactionCount / issueCount).toFixed(1)}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Error:", error.message);
|
||||
process.exit(1);
|
||||
@@ -170,4 +173,4 @@ async function main() {
|
||||
// Run if this script is executed directly
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,11 +487,8 @@ pub const StandaloneModuleGraph = struct {
|
||||
|
||||
const page_size = std.heap.page_size_max;
|
||||
|
||||
pub const InjectOptions = 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 const InjectOptions = bun.options.WindowsSettings;
|
||||
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: ?*const 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 +500,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 +556,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 +624,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,30 +681,47 @@ 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 => {
|
||||
// Read the original PE file
|
||||
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);
|
||||
}
|
||||
defer input_result.bytes.deinit();
|
||||
|
||||
// Initialize PE file for processing
|
||||
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();
|
||||
|
||||
// Add Bun section with module data
|
||||
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();
|
||||
|
||||
// Apply Windows settings if provided
|
||||
if (inject_options) |opts| {
|
||||
if (opts.description != null or opts.icon != null or opts.publisher != null or opts.title != null or opts.version != null or opts.copyright != null or opts.hide_console) {
|
||||
pe_file.applyWindowsSettings(opts, bun.default_allocator) catch |err| {
|
||||
Output.prettyErrorln("Error applying Windows settings to PE file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Seek to start and write the complete modified PE file
|
||||
switch (Syscall.setFileOffset(cloned_executable_fd, 0)) {
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("Error seeking to start of temporary file: {}", .{err});
|
||||
@@ -711,17 +731,28 @@ pub const StandaloneModuleGraph = struct {
|
||||
else => {},
|
||||
}
|
||||
|
||||
var file = bun.sys.File{ .handle = cloned_executable_fd };
|
||||
const writer = file.writer();
|
||||
pe_file.write(writer) catch |err| {
|
||||
// Write the complete PE data using the raw buffer to ensure all data is written
|
||||
const write_result = bun.sys.File.writeAll(.{ .handle = cloned_executable_fd }, pe_file.data.items);
|
||||
_ = write_result.unwrap() catch |err| {
|
||||
Output.prettyErrorln("Error writing PE file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Truncate file to the exact size to avoid leaving old data at the end
|
||||
switch (Syscall.ftruncate(cloned_executable_fd, @intCast(pe_file.data.items.len))) {
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("Error truncating file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
// Set executable permissions when running on POSIX hosts, even for Windows targets
|
||||
if (comptime !Environment.isWindows) {
|
||||
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
|
||||
}
|
||||
|
||||
return cloned_executable_fd;
|
||||
},
|
||||
else => {
|
||||
@@ -788,22 +819,13 @@ 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;
|
||||
},
|
||||
}
|
||||
|
||||
if (Environment.isWindows and inject_options.windows_hide_console) {
|
||||
bun.windows.editWin32BinarySubsystem(.{ .handle = cloned_executable_fd }, .windows_gui) catch |err| {
|
||||
Output.err(err, "failed to disable console on executable", .{});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
return cloned_executable_fd;
|
||||
}
|
||||
|
||||
@@ -831,13 +853,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: ?*const 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 +870,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,
|
||||
target,
|
||||
);
|
||||
bun.debugAssert(fd.kind == .system);
|
||||
@@ -875,15 +896,8 @@ pub const StandaloneModuleGraph = struct {
|
||||
|
||||
Global.exit(1);
|
||||
};
|
||||
fd.close();
|
||||
|
||||
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", .{});
|
||||
};
|
||||
}
|
||||
fd.close();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
11
src/cli.zig
11
src/cli.zig
@@ -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 {
|
||||
@@ -630,6 +629,14 @@ pub const Command = struct {
|
||||
if (bun.getenvZ("MI_VERBOSE") == null) {
|
||||
bun.mimalloc.mi_option_set_enabled(.verbose, false);
|
||||
}
|
||||
|
||||
// Suppress mimalloc warnings for memory-mapped PE sections (Windows compilation)
|
||||
// These warnings occur when processing large Windows PE files (~118MB) because
|
||||
// mimalloc's heuristics flag memory-mapped sections as suspicious, then confirm
|
||||
// they're valid. The warnings are harmless but noisy.
|
||||
if (bun.getenvZ("MIMALLOC_MAX_WARNINGS") == null) {
|
||||
bun.mimalloc.mi_option_set(.max_warnings, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// bun build --compile entry point
|
||||
|
||||
@@ -171,6 +171,11 @@ 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,
|
||||
clap.parseParam("--windows-copyright <STR> When using --compile targeting Windows, set the executable copyright") 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 +889,54 @@ 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("--windows-copyright")) |copyright| {
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-copyright requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows.copyright = copyright;
|
||||
}
|
||||
|
||||
if (args.option("--outdir")) |outdir| {
|
||||
|
||||
@@ -435,8 +435,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) {
|
||||
|
||||
@@ -11,6 +11,16 @@ 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,
|
||||
copyright: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn validatePath(
|
||||
log: *logger.Log,
|
||||
_: *Fs.FileSystem.Implementation,
|
||||
|
||||
1267
src/pe.zig
1267
src/pe.zig
File diff suppressed because it is too large
Load Diff
20
src/sys.zig
20
src/sys.zig
@@ -2006,9 +2006,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`Windows Resource Editing with exiftool snapshot test with exiftool 1`] = `
|
||||
"File Version Number : 1.2.3.4
|
||||
Product Version Number : 1.2.3.4
|
||||
Company Name : Snapshot Publisher
|
||||
File Description : Snapshot Test App
|
||||
File Version : 1.2.3.4
|
||||
Legal Copyright : Copyright 2024
|
||||
Product Name : Snapshot Product
|
||||
Product Version : 1.2.3.4"
|
||||
`;
|
||||
BIN
test/bundler/real-icon-small.ico
Normal file
BIN
test/bundler/real-icon-small.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 B |
BIN
test/bundler/real-icon.ico
Normal file
BIN
test/bundler/real-icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
178
test/bundler/windows-pe-checksum.test.ts
Normal file
178
test/bundler/windows-pe-checksum.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { spawn } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
// Skip these tests on Windows as they're for verifying cross-compilation
|
||||
describe.skipIf(isWindows)("Windows PE Checksum Verification", () => {
|
||||
const hasObjdump = Bun.which("objdump") !== null;
|
||||
|
||||
// Common build function
|
||||
async function buildWindowsExecutable(
|
||||
dir: string,
|
||||
outfile: string,
|
||||
windowsOptions: Record<string, string | boolean> = {},
|
||||
) {
|
||||
const args = [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--target=bun-windows-x64-v1.2.19",
|
||||
...Object.entries(windowsOptions).flatMap(([key, value]) =>
|
||||
value === true ? [`--${key}`] : [`--${key}`, value as string],
|
||||
),
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, outfile),
|
||||
];
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: args,
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Filter out mimalloc warnings which are expected for large allocations (Windows PE files ~118MB)
|
||||
const filteredStderr = stderr
|
||||
.split("\n")
|
||||
.filter(
|
||||
line =>
|
||||
!line.includes("mimalloc: warning:") &&
|
||||
!line.includes("(this may still be a valid very large allocation") &&
|
||||
!line.includes("(yes, the previous pointer") &&
|
||||
line.trim() !== "",
|
||||
)
|
||||
.join("\n")
|
||||
.trim();
|
||||
expect(filteredStderr).toBe("");
|
||||
|
||||
return join(dir, outfile);
|
||||
}
|
||||
|
||||
test.skipIf(!hasObjdump)("verifies PE checksum is calculated correctly", async () => {
|
||||
const dir = tempDirWithFiles("pe-checksum-test", {
|
||||
"index.js": `console.log("Testing PE checksum");`,
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "test.exe", {});
|
||||
|
||||
try {
|
||||
// Use objdump to check the PE checksum
|
||||
await using objdumpProc = spawn({
|
||||
cmd: ["objdump", "-p", exePath],
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [objdumpStdout, objdumpExitCode] = await Promise.all([
|
||||
new Response(objdumpProc.stdout).text(),
|
||||
objdumpProc.exited,
|
||||
]);
|
||||
|
||||
expect(objdumpExitCode).toBe(0);
|
||||
|
||||
// Extract checksum from objdump output
|
||||
const checksumMatch = objdumpStdout.match(/CheckSum\s+([0-9a-fA-F]+)/);
|
||||
expect(checksumMatch).not.toBeNull();
|
||||
|
||||
const checksum = checksumMatch![1];
|
||||
console.log("PE checksum:", checksum);
|
||||
|
||||
// Checksum should not be 0 after our implementation
|
||||
expect(checksum).not.toBe("00000000");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!hasObjdump)("verifies PE checksum with Windows resources", async () => {
|
||||
const dir = tempDirWithFiles("pe-checksum-resources", {
|
||||
"index.js": `console.log("Testing checksum with resources");`,
|
||||
"icon.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "test-resources.exe", {
|
||||
"windows-icon": join(dir, "icon.ico"),
|
||||
"windows-version": "1.2.3.4",
|
||||
"windows-description": "Checksum Test App",
|
||||
});
|
||||
|
||||
try {
|
||||
// Check the checksum
|
||||
await using objdumpProc = spawn({
|
||||
cmd: ["objdump", "-p", exePath],
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [objdumpStdout, objdumpExitCode] = await Promise.all([
|
||||
new Response(objdumpProc.stdout).text(),
|
||||
objdumpProc.exited,
|
||||
]);
|
||||
|
||||
expect(objdumpExitCode).toBe(0);
|
||||
|
||||
const checksumMatch = objdumpStdout.match(/CheckSum\s+([0-9a-fA-F]+)/);
|
||||
expect(checksumMatch).not.toBeNull();
|
||||
|
||||
const checksum = checksumMatch![1];
|
||||
console.log("PE checksum with resources:", checksum);
|
||||
|
||||
// Checksum should not be 0
|
||||
expect(checksum).not.toBe("00000000");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to create a test icon
|
||||
function 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
|
||||
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
|
||||
|
||||
const imageData = Buffer.alloc(256); // 16x16x8bpp
|
||||
|
||||
return Buffer.concat([header, dirEntry, bmpHeader, imageData]);
|
||||
}
|
||||
331
test/bundler/windows-resources-exiftool.test.ts
Normal file
331
test/bundler/windows-resources-exiftool.test.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { spawn } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
// Skip these tests on Windows as they're for verifying cross-compilation
|
||||
describe.skipIf(isWindows)("Windows Resource Editing with exiftool", () => {
|
||||
const hasExiftool = Bun.which("exiftool") !== null;
|
||||
|
||||
// Common build function
|
||||
async function buildWindowsExecutable(
|
||||
dir: string,
|
||||
outfile: string,
|
||||
windowsOptions: Record<string, string | boolean> = {},
|
||||
) {
|
||||
const args = [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--target=bun-windows-x64-v1.2.19",
|
||||
...Object.entries(windowsOptions).flatMap(([key, value]) =>
|
||||
value === true ? [`--${key}`] : [`--${key}`, value as string],
|
||||
),
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, outfile),
|
||||
];
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: args,
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
if (exitCode !== 0) {
|
||||
console.error("Build failed with exit code:", exitCode);
|
||||
console.error("stderr:", stderr);
|
||||
}
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Filter out mimalloc warnings which are expected for large allocations (Windows PE files ~118MB)
|
||||
const filteredStderr = stderr
|
||||
.split("\n")
|
||||
.filter(
|
||||
line =>
|
||||
!line.includes("mimalloc: warning:") &&
|
||||
!line.includes("(this may still be a valid very large allocation") &&
|
||||
!line.includes("(yes, the previous pointer") &&
|
||||
line.trim() !== "",
|
||||
)
|
||||
.join("\n")
|
||||
.trim();
|
||||
expect(filteredStderr).toBe("");
|
||||
|
||||
return join(dir, outfile);
|
||||
}
|
||||
|
||||
// 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]);
|
||||
};
|
||||
|
||||
test.skipIf(!hasExiftool)("verifies version info with exiftool", async () => {
|
||||
const dir = tempDirWithFiles("exiftool-test", {
|
||||
"index.js": `console.log("Testing with exiftool");`,
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "test.exe", {
|
||||
"windows-version": "9.8.7.6",
|
||||
"windows-description": "My Custom Description",
|
||||
"windows-publisher": "Test Publisher Inc",
|
||||
"windows-title": "My Custom Product",
|
||||
"windows-copyright": "Copyright 2024 Test Publisher Inc",
|
||||
});
|
||||
|
||||
try {
|
||||
// Run exiftool to extract metadata
|
||||
await using proc = spawn({
|
||||
cmd: ["exiftool", "-j", exePath],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
|
||||
console.log("Raw exiftool output:", stdout);
|
||||
console.log("Executable path for debugging:", join(dir, "test.exe"));
|
||||
const metadata = JSON.parse(stdout)[0];
|
||||
|
||||
// Verify the version information
|
||||
expect(metadata.FileVersionNumber).toBe("9.8.7.6");
|
||||
expect(metadata.ProductVersionNumber).toBe("9.8.7.6");
|
||||
expect(metadata.FileDescription).toBe("My Custom Description");
|
||||
expect(metadata.CompanyName).toBe("Test Publisher Inc");
|
||||
expect(metadata.ProductName).toBe("My Custom Product");
|
||||
expect(metadata.LegalCopyright).toBe("Copyright 2024 Test Publisher Inc");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!hasExiftool)("verifies subsystem change with exiftool", async () => {
|
||||
const dir = tempDirWithFiles("exiftool-subsystem", {
|
||||
"index.js": `console.log("Testing subsystem");`,
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "hidden.exe", {
|
||||
"windows-hide-console": true,
|
||||
});
|
||||
|
||||
try {
|
||||
await using proc = spawn({
|
||||
cmd: ["exiftool", "-Subsystem", exePath],
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Windows GUI subsystem
|
||||
expect(stdout).toContain("Windows GUI");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!hasExiftool)("verifies icon resource with exiftool", async () => {
|
||||
const dir = tempDirWithFiles("exiftool-icon", {
|
||||
"index.js": `console.log("Testing icon");`,
|
||||
"icon.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "icon.exe", {
|
||||
"windows-icon": join(dir, "icon.ico"),
|
||||
"windows-version": "1.0.0.0",
|
||||
});
|
||||
|
||||
try {
|
||||
await using proc = spawn({
|
||||
cmd: ["exiftool", "-j", exePath],
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const metadata = JSON.parse(stdout)[0];
|
||||
|
||||
// Even with an icon, the version should still be set
|
||||
expect(metadata.FileVersionNumber).toBe("1.0.0.0");
|
||||
expect(metadata.ProductVersionNumber).toBe("1.0.0.0");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!hasExiftool)("verifies all fields with exiftool", async () => {
|
||||
const dir = tempDirWithFiles("exiftool-all", {
|
||||
"index.js": `console.log("Testing all fields");`,
|
||||
"icon.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "all.exe", {
|
||||
"windows-icon": join(dir, "icon.ico"),
|
||||
"windows-version": "5.4.3.2",
|
||||
"windows-description": "Complete Test Application",
|
||||
"windows-publisher": "Acme Corporation",
|
||||
"windows-title": "Acme Test Suite",
|
||||
"windows-copyright": "(c) 2024 Acme Corporation. All rights reserved.",
|
||||
"windows-hide-console": true,
|
||||
});
|
||||
|
||||
try {
|
||||
await using proc = spawn({
|
||||
cmd: ["exiftool", "-j", exePath],
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const metadata = JSON.parse(stdout)[0];
|
||||
|
||||
// Verify all fields
|
||||
expect(metadata.FileVersionNumber).toBe("5.4.3.2");
|
||||
expect(metadata.ProductVersionNumber).toBe("5.4.3.2");
|
||||
expect(metadata.FileDescription).toBe("Complete Test Application");
|
||||
expect(metadata.CompanyName).toBe("Acme Corporation");
|
||||
expect(metadata.ProductName).toBe("Acme Test Suite");
|
||||
expect(metadata.LegalCopyright).toBe("(c) 2024 Acme Corporation. All rights reserved.");
|
||||
expect(metadata.Subsystem).toBe("Windows GUI");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!hasExiftool)("snapshot test with exiftool", async () => {
|
||||
const dir = tempDirWithFiles("exiftool-snapshot", {
|
||||
"index.js": `console.log("Snapshot test");`,
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "snapshot.exe", {
|
||||
"windows-version": "1.2.3.4",
|
||||
"windows-description": "Snapshot Test App",
|
||||
"windows-publisher": "Snapshot Publisher",
|
||||
"windows-title": "Snapshot Product",
|
||||
"windows-copyright": "Copyright 2024",
|
||||
});
|
||||
|
||||
try {
|
||||
// Get full exiftool output for snapshot
|
||||
await using proc = spawn({
|
||||
cmd: ["exiftool", exePath],
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Extract relevant version info fields for snapshot
|
||||
const versionFields = stdout
|
||||
.split("\n")
|
||||
.filter(
|
||||
line =>
|
||||
line.includes("File Version Number") ||
|
||||
line.includes("Product Version Number") ||
|
||||
line.includes("File Description") ||
|
||||
line.includes("Company Name") ||
|
||||
line.includes("Product Name") ||
|
||||
line.includes("Legal Copyright") ||
|
||||
line.includes("File Version") ||
|
||||
line.includes("Product Version"),
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
expect(versionFields).toMatchSnapshot();
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!hasExiftool)("verifies real ICO file with exiftool", async () => {
|
||||
// Real ICO file created with ImageMagick (full multi-size version)
|
||||
const realIcoPath = join(import.meta.dir, "real-icon.ico");
|
||||
if (!(await Bun.file(realIcoPath).exists())) {
|
||||
// Skip if real icon file doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = tempDirWithFiles("exiftool-real-ico", {
|
||||
"index.js": `console.log("Testing real ICO");`,
|
||||
"real.ico": await Bun.file(realIcoPath).bytes(),
|
||||
});
|
||||
|
||||
const exePath = await buildWindowsExecutable(dir, "realico.exe", {
|
||||
"windows-icon": join(dir, "real.ico"),
|
||||
"windows-version": "2.0.0.0",
|
||||
"windows-title": "Real Icon Test",
|
||||
});
|
||||
|
||||
try {
|
||||
await using proc = spawn({
|
||||
cmd: ["exiftool", "-j", exePath],
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const metadata = JSON.parse(stdout)[0];
|
||||
|
||||
// Verify version is still set with real icon
|
||||
expect(metadata.FileVersionNumber).toBe("2.0.0.0");
|
||||
expect(metadata.ProductName).toBe("Real Icon Test");
|
||||
} finally {
|
||||
await Bun.file(exePath).unlink();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user