feat(bundler): add --windows-icon, --windows-no-console, fix bun.exe's main icon (#15894)

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com>
This commit is contained in:
dave caruso
2024-12-20 03:22:16 -08:00
committed by GitHub
parent 0c50b0fcec
commit 7b3554f90c
10 changed files with 1435 additions and 10 deletions

View File

@@ -601,6 +601,8 @@ file(GLOB BUN_C_SOURCES ${CONFIGURE_DEPENDS}
if(WIN32)
list(APPEND BUN_C_SOURCES ${CWD}/src/bun.js/bindings/windows/musl-memmem.c)
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(
@@ -650,11 +652,19 @@ if(WIN32)
set(Bun_VERSION_WITH_TAG ${VERSION})
endif()
set(BUN_ICO_PATH ${CWD}/src/bun.ico)
configure_file(${CWD}/src/bun.ico ${CODEGEN_PATH}/bun.ico COPYONLY)
configure_file(
${CWD}/src/windows-app-info.rc
${CODEGEN_PATH}/windows-app-info.rc
@ONLY
)
list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.rc)
add_custom_command(
OUTPUT ${CODEGEN_PATH}/windows-app-info.res
COMMAND rc.exe /fo ${CODEGEN_PATH}/windows-app-info.res ${CODEGEN_PATH}/windows-app-info.rc
DEPENDS ${CODEGEN_PATH}/windows-app-info.rc ${CODEGEN_PATH}/bun.ico
COMMENT "Adding Windows resource file ${CODEGEN_PATH}/windows-app-info.res with ico in ${CODEGEN_PATH}/bun.ico"
)
set(WINDOWS_RESOURCES ${CODEGEN_PATH}/windows-app-info.res)
endif()
# --- Executable ---
@@ -662,7 +672,7 @@ endif()
set(BUN_CPP_OUTPUT ${BUILD_PATH}/${CMAKE_STATIC_LIBRARY_PREFIX}${bun}${CMAKE_STATIC_LIBRARY_SUFFIX})
if(BUN_LINK_ONLY)
add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT})
add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT} ${WINDOWS_RESOURCES})
set_target_properties(${bun} PROPERTIES LINKER_LANGUAGE CXX)
target_link_libraries(${bun} PRIVATE ${BUN_CPP_OUTPUT})
elseif(BUN_CPP_ONLY)
@@ -680,7 +690,7 @@ elseif(BUN_CPP_ONLY)
${BUN_CPP_OUTPUT}
)
else()
add_executable(${bun} ${BUN_CPP_SOURCES})
add_executable(${bun} ${BUN_CPP_SOURCES} ${WINDOWS_RESOURCES})
target_link_libraries(${bun} PRIVATE ${BUN_ZIG_OUTPUT})
endif()

View File

@@ -279,6 +279,19 @@ $ bun build --compile --asset-naming="[name].[ext]" ./index.ts
To trim down the size of the executable a little, pass `--minify` to `bun build --compile`. This uses Bun's minifier to reduce the code size. Overall though, Bun's binary is still way too big and we need to make it smaller.
## Windows-specific flags
When compiling a standalone executable on Windows, there are two platform-specific options that can be used to customize metadata on the generated `.exe` file:
- `--windows-icon=path/to/icon.ico` to customize the executable file icon.
- `--windows-hide-console` to disable the background terminal, which can be used for applications that do not need a TTY.
{% callout %}
These flags currently cannot be used when cross-compiling because they depend on Windows APIs.
{% /callout %}
## Unsupported CLI arguments
Currently, the `--compile` flag can only accept a single entrypoint at a time and does not support the following flags:

View File

@@ -430,7 +430,11 @@ pub const StandaloneModuleGraph = struct {
else
std.mem.page_size;
pub fn inject(bytes: []const u8, self_exe: [:0]const u8) bun.FileDescriptor {
pub const InjectOptions = struct {
windows_hide_console: bool = false,
};
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions) 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)});
@@ -470,7 +474,7 @@ pub const StandaloneModuleGraph = struct {
bun.invalid_fd,
out,
// access_mask
w.SYNCHRONIZE | w.GENERIC_WRITE | w.DELETE,
w.SYNCHRONIZE | w.GENERIC_WRITE | w.GENERIC_READ | w.DELETE,
// create disposition
w.FILE_OPEN,
// create options
@@ -637,6 +641,15 @@ pub const StandaloneModuleGraph = struct {
_ = bun.C.fchmod(cloned_executable_fd.int(), 0o777);
}
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;
}
@@ -664,6 +677,8 @@ pub const StandaloneModuleGraph = struct {
outfile: []const u8,
env: *bun.DotEnv.Loader,
output_format: bun.options.Format,
windows_hide_console: bool,
windows_icon: ?[]const u8,
) !void {
const bytes = try toBytes(allocator, module_prefix, output_files, output_format);
if (bytes.len == 0) return;
@@ -680,6 +695,7 @@ pub const StandaloneModuleGraph = struct {
Output.err(err, "failed to download cross-compiled bun executable", .{});
Global.exit(1);
},
.{ .windows_hide_console = windows_hide_console },
);
fd.assertKind(.system);
@@ -704,6 +720,15 @@ pub const StandaloneModuleGraph = struct {
Global.exit(1);
};
_ = bun.sys.close(fd);
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", .{});
};
}
return;
}

View File

@@ -0,0 +1,14 @@
#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

@@ -0,0 +1,211 @@
// 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

@@ -290,7 +290,9 @@ pub const Arguments = struct {
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
clap.parseParam("--app (EXPERIMENTAL) Build a web app for production using Bun Bake.") catch unreachable,
clap.parseParam("--server-components (EXPERIMENTAL) Enable server components") catch unreachable,
clap.parseParam("--env <inline|prefix*|disable> Inline environment variables into the bundle as process.env.${name}. Defaults to 'inline'. To inline environment variables matching a prefix, use my prefix like 'FOO_PUBLIC_*'. To disable, use 'disable'. In Bun v1.2+, the default is 'disable'.") catch unreachable,
clap.parseParam("--env <inline|prefix*|disable> Inline environment variables into the bundle as process.env.${name}. Defaults to 'inline'. To inline environment variables matching a prefix, use my prefix like 'FOO_PUBLIC_*'. To disable, use 'disable'. In Bun v1.2+, the default is 'disable'.") 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,
} ++ 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,
@@ -928,6 +930,31 @@ pub const Arguments = struct {
ctx.bundler_options.inline_entrypoint_import_meta_main = true;
}
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();
}
if (!ctx.bundler_options.compile) {
Output.errGeneric("--windows-hide-console requires --compile", .{});
Global.crash();
}
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;
}
if (args.option("--outdir")) |outdir| {
if (outdir.len > 0) {
ctx.bundler_options.outdir = outdir;
@@ -1456,9 +1483,6 @@ pub const Command = struct {
has_loaded_global_config: bool = false,
pub const BundlerOptions = struct {
compile: bool = false,
compile_target: Cli.CompileTarget = .{},
outdir: []const u8 = "",
outfile: []const u8 = "",
root_dir: []const u8 = "",
@@ -1489,6 +1513,12 @@ pub const Command = struct {
env_behavior: Api.DotEnvBehavior = .disable,
env_prefix: []const u8 = "",
// Compile options
compile: bool = false,
compile_target: Cli.CompileTarget = .{},
windows_hide_console: bool = false,
windows_icon: ?[]const u8 = null,
};
pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context {

View File

@@ -418,6 +418,8 @@ pub const BuildCommand = struct {
outfile,
this_bundler.env,
this_bundler.options.output_format,
ctx.bundler_options.windows_hide_console,
ctx.bundler_options.windows_icon,
);
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

@@ -1,6 +1,6 @@
#include "windows.h"
IDI_MYICON ICON "@BUN_ICO_PATH@"
IDI_MYICON ICON "bun.ico"
VS_VERSION_INFO VERSIONINFO
FILEVERSION @Bun_VERSION_MAJOR@,@Bun_VERSION_MINOR@,@Bun_VERSION_PATCH@,0

View File

@@ -3627,3 +3627,33 @@ pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
pub const JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x400;
pub const JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x800;
pub const JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000;
const pe_header_offset_location = 0x3C;
const subsystem_offset = 0x5C;
pub const Subsystem = enum(u16) {
windows_gui = 2,
};
pub fn editWin32BinarySubsystem(fd: bun.sys.File, subsystem: Subsystem) !void {
comptime bun.assert(bun.Environment.isWindows);
if (bun.windows.SetFilePointerEx(fd.handle.cast(), pe_header_offset_location, null, std.os.windows.FILE_BEGIN) == 0)
return error.Win32Error;
const offset = try fd.reader().readInt(u32, .little);
if (bun.windows.SetFilePointerEx(fd.handle.cast(), offset + subsystem_offset, null, std.os.windows.FILE_BEGIN) == 0)
return error.Win32Error;
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,
};
}
};