From a1074634d63dade430a04ce71c15f6b4fc0afeeb Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 19 Dec 2024 21:24:57 -0800 Subject: [PATCH 1/9] add --windows-icon --- cmake/targets/BuildBun.cmake | 5 + docs/bundler/executables.md | 13 + src/StandaloneModuleGraph.zig | 29 +- .../bindings/windows/rescle-binding.cpp | 14 + src/bun.js/bindings/windows/rescle.cpp | 1090 +++++++++++++++++ src/bun.js/bindings/windows/rescle.h | 211 ++++ src/cli.zig | 38 +- src/cli/build_command.zig | 2 + src/windows.zig | 30 + 9 files changed, 1426 insertions(+), 6 deletions(-) create mode 100644 src/bun.js/bindings/windows/rescle-binding.cpp create mode 100644 src/bun.js/bindings/windows/rescle.cpp create mode 100644 src/bun.js/bindings/windows/rescle.h diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 84a99eaa58fc4f..e06c0fa2e8a8e6 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -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( @@ -660,6 +662,9 @@ endif() # --- Executable --- set(BUN_CPP_OUTPUT ${BUILD_PATH}/${CMAKE_STATIC_LIBRARY_PREFIX}${bun}${CMAKE_STATIC_LIBRARY_SUFFIX}) +if(WIN32) + list(APPEND BUN_CPP_OUTPUT ${CODEGEN_PATH}/windows-app-info.rc) +endif() if(BUN_LINK_ONLY) add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT}) diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index 6ae39a574cabaf..c477e6a82cbd8a 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -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: diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 6e05878e0ea5b9..f067674855f44a 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -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("error: 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; } diff --git a/src/bun.js/bindings/windows/rescle-binding.cpp b/src/bun.js/bindings/windows/rescle-binding.cpp new file mode 100644 index 00000000000000..31514168e29853 --- /dev/null +++ b/src/bun.js/bindings/windows/rescle-binding.cpp @@ -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; +} diff --git a/src/bun.js/bindings/windows/rescle.cpp b/src/bun.js/bindings/windows/rescle.cpp new file mode 100644 index 00000000000000..eb72626b815527 --- /dev/null +++ b/src/bun.js/bindings/windows/rescle.cpp @@ -0,0 +1,1090 @@ +// This file is from Electron's fork of rescle +// https://github.com/electron/rcedit/blob/e36b688b42df0e236922019ce14e0ea165dc176d/src/rescle.CC +// '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/ +#include "rescle.h" + +#include +#include +#include // wstringstream +#include // setw, setfill +#include +#include +#include + +namespace rescle { + +namespace { + +#pragma pack(push, 2) +typedef struct _GRPICONENTRY { + BYTE width; + BYTE height; + BYTE colourCount; + BYTE reserved; + BYTE planes; + BYTE bitCount; + WORD bytesInRes; + WORD bytesInRes2; + WORD reserved2; + WORD id; +} GRPICONENTRY; +#pragma pack(pop) + +#pragma pack(push, 2) +typedef struct _GRPICONHEADER { + WORD reserved; + WORD type; + WORD count; + GRPICONENTRY entries[1]; +} GRPICONHEADER; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct _VS_VERSION_HEADER { + WORD wLength; + WORD wValueLength; + WORD wType; +} VS_VERSION_HEADER; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct _VS_VERSION_STRING { + VS_VERSION_HEADER Header; + WCHAR szKey[1]; +} VS_VERSION_STRING; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct _VS_VERSION_ROOT_INFO { + WCHAR szKey[16]; + WORD Padding1[1]; + VS_FIXEDFILEINFO Info; +} VS_VERSION_ROOT_INFO; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct _VS_VERSION_ROOT { + VS_VERSION_HEADER Header; + VS_VERSION_ROOT_INFO Info; +} VS_VERSION_ROOT; +#pragma pack(pop) + +// The default en-us LANGID. +LANGID kLangEnUs = 1033; +LANGID kCodePageEnUs = 1200; +UINT kDefaultIconBundle = 0; + +template +inline T round(T value, int modula = 4) +{ + return value + ((value % modula > 0) ? (modula - value % modula) : 0); +} + +std::wstring ReadFileToString(const wchar_t* filename) +{ + std::wifstream wif(filename); + wif.imbue(std::locale(std::locale::empty(), new std::codecvt_utf8)); + std::wstringstream wss; + wss << wif.rdbuf(); + return wss.str(); +} + +class ScopedFile { +public: + ScopedFile(const WCHAR* path) + : file_(CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) + { + } + ~ScopedFile() { CloseHandle(file_); } + + operator HANDLE() { return file_; } + +private: + HANDLE file_; +}; + +struct VersionStampValue { + WORD valueLength = 0; // stringfileinfo, stringtable: 0; string: Value size in WORD; var: Value size in bytes + WORD type = 0; // 0: binary data; 1: text data + std::wstring key; // stringtable: 8-digit hex stored as UTF-16 (hiword: hi6: sublang, lo10: majorlang; loword: code page); must include zero words to align next member on 32-bit boundary + std::vector value; // string: zero-terminated string; var: array of language & code page ID pairs + std::vector children; + + size_t GetLength() const; + std::vector Serialize() const; +}; + +} // namespace + +VersionInfo::VersionInfo() +{ + FillDefaultData(); +} + +VersionInfo::VersionInfo(HMODULE hModule, WORD languageId) +{ + HRSRC hRsrc = FindResourceExW(hModule, RT_VERSION, MAKEINTRESOURCEW(1), languageId); + + if (hRsrc == NULL) { + throw std::system_error(GetLastError(), std::system_category()); + } + + HGLOBAL hGlobal = LoadResource(hModule, hRsrc); + if (hGlobal == NULL) { + throw std::system_error(GetLastError(), std::system_category()); + } + + void* p = LockResource(hGlobal); + if (p == NULL) { + throw std::system_error(GetLastError(), std::system_category()); + } + + DWORD size = SizeofResource(hModule, hRsrc); + if (size == 0) { + throw std::system_error(GetLastError(), std::system_category()); + } + + DeserializeVersionInfo(static_cast(p), size); + FillDefaultData(); +} + +bool VersionInfo::HasFixedFileInfo() const +{ + return fixedFileInfo_.dwSignature == 0xFEEF04BD; +} + +VS_FIXEDFILEINFO& VersionInfo::GetFixedFileInfo() +{ + return fixedFileInfo_; +} + +const VS_FIXEDFILEINFO& VersionInfo::GetFixedFileInfo() const +{ + return fixedFileInfo_; +} + +void VersionInfo::SetFixedFileInfo(const VS_FIXEDFILEINFO& value) +{ + fixedFileInfo_ = value; +} + +std::vector VersionInfo::Serialize() const +{ + VersionStampValue versionInfo; + versionInfo.key = L"VS_VERSION_INFO"; + versionInfo.type = 0; + + if (HasFixedFileInfo()) { + auto size = sizeof(VS_FIXEDFILEINFO); + versionInfo.valueLength = size; + + auto& dst = versionInfo.value; + dst.resize(size); + + memcpy(&dst[0], &GetFixedFileInfo(), size); + } + + { + VersionStampValue stringFileInfo; + stringFileInfo.key = L"StringFileInfo"; + stringFileInfo.type = 1; + stringFileInfo.valueLength = 0; + + for (const auto& iTable : stringTables) { + VersionStampValue stringTableRaw; + stringTableRaw.type = 1; + stringTableRaw.valueLength = 0; + + { + auto& translate = iTable.encoding; + std::wstringstream ss; + ss << std::hex << std::setw(8) << std::setfill(L'0') << (translate.wLanguage << 16 | translate.wCodePage); + stringTableRaw.key = ss.str(); + } + + for (const auto& iString : iTable.strings) { + const auto& stringValue = iString.second; + auto strLenNullTerminated = stringValue.length() + 1; + + VersionStampValue stringRaw; + stringRaw.type = 1; + stringRaw.key = iString.first; + stringRaw.valueLength = strLenNullTerminated; + + auto size = strLenNullTerminated * sizeof(WCHAR); + auto& dst = stringRaw.value; + dst.resize(size); + + auto src = stringValue.c_str(); + + memcpy(&dst[0], src, size); + + stringTableRaw.children.push_back(std::move(stringRaw)); + } + + stringFileInfo.children.push_back(std::move(stringTableRaw)); + } + + versionInfo.children.push_back(std::move(stringFileInfo)); + } + + { + VersionStampValue varFileInfo; + varFileInfo.key = L"VarFileInfo"; + varFileInfo.type = 1; + varFileInfo.valueLength = 0; + + { + VersionStampValue varRaw; + varRaw.key = L"Translation"; + varRaw.type = 0; + + { + auto newValueSize = sizeof(DWORD); + auto& dst = varRaw.value; + dst.resize(supportedTranslations.size() * newValueSize); + + for (auto iVar = 0; iVar < supportedTranslations.size(); ++iVar) { + auto& translate = supportedTranslations[iVar]; + auto var = DWORD(translate.wCodePage) << 16 | translate.wLanguage; + memcpy(&dst[iVar * newValueSize], &var, newValueSize); + } + + varRaw.valueLength = varRaw.value.size(); + } + + varFileInfo.children.push_back(std::move(varRaw)); + } + + versionInfo.children.push_back(std::move(varFileInfo)); + } + + return std::move(versionInfo.Serialize()); +} + +void VersionInfo::FillDefaultData() +{ + if (stringTables.empty()) { + Translate enUsTranslate = { kLangEnUs, kCodePageEnUs }; + stringTables.push_back({ enUsTranslate }); + supportedTranslations.push_back(enUsTranslate); + } + if (!HasFixedFileInfo()) { + fixedFileInfo_ = { 0 }; + fixedFileInfo_.dwSignature = 0xFEEF04BD; + fixedFileInfo_.dwFileType = VFT_APP; + } +} + +void VersionInfo::DeserializeVersionInfo(const BYTE* pData, size_t size) +{ + auto pVersionInfo = reinterpret_cast(pData); + WORD fixedFileInfoSize = pVersionInfo->Header.wValueLength; + + if (fixedFileInfoSize > 0) + SetFixedFileInfo(pVersionInfo->Info.Info); + + const BYTE* fixedFileInfoEndOffset = reinterpret_cast(&pVersionInfo->Info.szKey) + (wcslen(pVersionInfo->Info.szKey) + 1) * sizeof(WCHAR) + fixedFileInfoSize; + const BYTE* pVersionInfoChildren = reinterpret_cast(round(reinterpret_cast(fixedFileInfoEndOffset))); + size_t versionInfoChildrenOffset = pVersionInfoChildren - pData; + size_t versionInfoChildrenSize = pVersionInfo->Header.wLength - versionInfoChildrenOffset; + + const auto childrenEndOffset = pVersionInfoChildren + versionInfoChildrenSize; + const auto resourceEndOffset = pData + size; + for (auto p = pVersionInfoChildren; p < childrenEndOffset && p < resourceEndOffset;) { + auto pKey = reinterpret_cast(p)->szKey; + auto versionInfoChildData = GetChildrenData(p); + if (wcscmp(pKey, L"StringFileInfo") == 0) { + DeserializeVersionStringFileInfo(versionInfoChildData.first, versionInfoChildData.second, stringTables); + } else if (wcscmp(pKey, L"VarFileInfo") == 0) { + DeserializeVarFileInfo(versionInfoChildData.first, supportedTranslations); + } + + p += round(reinterpret_cast(p)->Header.wLength); + } +} + +VersionStringTable VersionInfo::DeserializeVersionStringTable(const BYTE* tableData) +{ + auto strings = GetChildrenData(tableData); + auto stringTable = reinterpret_cast(tableData); + auto end_ptr = const_cast(stringTable->szKey + (8 * sizeof(WCHAR))); + auto langIdCodePagePair = static_cast(wcstol(stringTable->szKey, &end_ptr, 16)); + + VersionStringTable tableEntry; + + // unicode string of 8 hex digits + tableEntry.encoding.wLanguage = langIdCodePagePair >> 16; + tableEntry.encoding.wCodePage = langIdCodePagePair; + + for (auto posStrings = 0U; posStrings < strings.second;) { + const auto stringEntry = reinterpret_cast(strings.first + posStrings); + const auto stringData = GetChildrenData(strings.first + posStrings); + tableEntry.strings.push_back(std::pair(stringEntry->szKey, std::wstring(reinterpret_cast(stringData.first), stringEntry->Header.wValueLength))); + + posStrings += round(stringEntry->Header.wLength); + } + + return tableEntry; +} + +void VersionInfo::DeserializeVersionStringFileInfo(const BYTE* offset, size_t length, std::vector& stringTables) +{ + for (auto posStringTables = 0U; posStringTables < length;) { + auto stringTableEntry = DeserializeVersionStringTable(offset + posStringTables); + stringTables.push_back(stringTableEntry); + posStringTables += round(reinterpret_cast(offset + posStringTables)->Header.wLength); + } +} + +void VersionInfo::DeserializeVarFileInfo(const unsigned char* offset, std::vector& translations) +{ + const auto translatePairs = GetChildrenData(offset); + + const auto top = reinterpret_cast(translatePairs.first); + for (auto pTranslatePair = top; pTranslatePair < top + translatePairs.second; pTranslatePair += sizeof(DWORD)) { + auto codePageLangIdPair = *pTranslatePair; + Translate translate; + translate.wLanguage = codePageLangIdPair; + translate.wCodePage = codePageLangIdPair >> 16; + translations.push_back(translate); + } +} + +OffsetLengthPair VersionInfo::GetChildrenData(const BYTE* entryData) +{ + auto entry = reinterpret_cast(entryData); + auto headerOffset = entryData; + auto headerSize = sizeof(VS_VERSION_HEADER); + auto keySize = (wcslen(entry->szKey) + 1) * sizeof(WCHAR); + auto childrenOffset = round(headerSize + keySize); + + auto pChildren = headerOffset + childrenOffset; + auto childrenSize = entry->Header.wLength - childrenOffset; + return OffsetLengthPair(pChildren, childrenSize); +} + +size_t VersionStampValue::GetLength() const +{ + size_t bytes = sizeof(VS_VERSION_HEADER); + bytes += static_cast(key.length() + 1) * sizeof(WCHAR); + if (!value.empty()) + bytes = round(bytes) + value.size(); + for (const auto& child : children) + bytes = round(bytes) + static_cast(child.GetLength()); + return bytes; +} + +std::vector VersionStampValue::Serialize() const +{ + std::vector data = std::vector(GetLength()); + + size_t offset = 0; + + VS_VERSION_HEADER header = { static_cast(data.size()), valueLength, type }; + memcpy(&data[offset], &header, sizeof(header)); + offset += sizeof(header); + + auto keySize = static_cast(key.length() + 1) * sizeof(WCHAR); + memcpy(&data[offset], key.c_str(), keySize); + offset += keySize; + + if (!value.empty()) { + offset = round(offset); + memcpy(&data[offset], &value[0], value.size()); + offset += value.size(); + } + + for (const auto& child : children) { + offset = round(offset); + size_t childLength = child.GetLength(); + std::vector src = child.Serialize(); + memcpy(&data[offset], &src[0], childLength); + offset += childLength; + } + + return std::move(data); +} + +ResourceUpdater::ResourceUpdater() + : module_(NULL) +{ +} + +ResourceUpdater::~ResourceUpdater() +{ + if (module_ != NULL) { + FreeLibrary(module_); + module_ = NULL; + } +} + +bool ResourceUpdater::Load(const WCHAR* filename) +{ + wchar_t abspath[MAX_PATH] = { 0 }; + if (_wfullpath(abspath, filename, MAX_PATH)) + module_ = LoadLibraryExW(abspath, NULL, DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_AS_DATAFILE); + else + module_ = LoadLibraryExW(filename, NULL, DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_AS_DATAFILE); + + if (module_ == NULL) { + return false; + } + + this->filename_ = filename; + + EnumResourceNamesW(module_, RT_STRING, OnEnumResourceName, reinterpret_cast(this)); + EnumResourceNamesW(module_, RT_VERSION, OnEnumResourceName, reinterpret_cast(this)); + EnumResourceNamesW(module_, RT_GROUP_ICON, OnEnumResourceName, reinterpret_cast(this)); + EnumResourceNamesW(module_, RT_ICON, OnEnumResourceName, reinterpret_cast(this)); + EnumResourceNamesW(module_, RT_MANIFEST, OnEnumResourceManifest, reinterpret_cast(this)); + EnumResourceNamesW(module_, RT_RCDATA, OnEnumResourceName, reinterpret_cast(this)); + + return true; +} + +bool ResourceUpdater::SetExecutionLevel(const WCHAR* value) +{ + executionLevel_ = value; + return true; +} + +bool ResourceUpdater::IsExecutionLevelSet() +{ + return !executionLevel_.empty(); +} + +bool ResourceUpdater::SetApplicationManifest(const WCHAR* value) +{ + applicationManifestPath_ = value; + return true; +} + +bool ResourceUpdater::IsApplicationManifestSet() +{ + return !applicationManifestPath_.empty(); +} + +bool ResourceUpdater::SetVersionString(WORD languageId, const WCHAR* name, const WCHAR* value) +{ + std::wstring nameStr(name); + std::wstring valueStr(value); + + auto& stringTables = versionStampMap_[languageId].stringTables; + for (auto j = stringTables.begin(); j != stringTables.end(); ++j) { + auto& stringPairs = j->strings; + for (auto k = stringPairs.begin(); k != stringPairs.end(); ++k) { + if (k->first == nameStr) { + k->second = valueStr; + return true; + } + } + + // Not found, append one for all tables. + stringPairs.push_back(VersionString(nameStr, valueStr)); + } + + return true; +} + +bool ResourceUpdater::SetVersionString(const WCHAR* name, const WCHAR* value) +{ + LANGID langId = versionStampMap_.empty() ? kLangEnUs + : versionStampMap_.begin()->first; + return SetVersionString(langId, name, value); +} + +const WCHAR* ResourceUpdater::GetVersionString(WORD languageId, const WCHAR* name) +{ + std::wstring nameStr(name); + + const auto& stringTables = versionStampMap_[languageId].stringTables; + for (const auto& j : stringTables) { + const auto& stringPairs = j.strings; + for (const auto& k : stringPairs) { + if (k.first == nameStr) { + return k.second.c_str(); + } + } + } + + return NULL; +} + +const WCHAR* ResourceUpdater::GetVersionString(const WCHAR* name) +{ + if (versionStampMap_.empty()) { + return NULL; + } else { + return GetVersionString(versionStampMap_.begin()->first, name); + } +} + +bool ResourceUpdater::SetProductVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4) +{ + VersionInfo& versionInfo = versionStampMap_[languageId]; + if (!versionInfo.HasFixedFileInfo()) { + return false; + } + + VS_FIXEDFILEINFO& root = versionInfo.GetFixedFileInfo(); + + root.dwProductVersionMS = v1 << 16 | v2; + root.dwProductVersionLS = v3 << 16 | v4; + + return true; +} + +bool ResourceUpdater::SetProductVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4) +{ + LANGID langId = versionStampMap_.empty() ? kLangEnUs + : versionStampMap_.begin()->first; + return SetProductVersion(langId, 1, v1, v2, v3, v4); +} + +bool ResourceUpdater::SetFileVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4) +{ + VersionInfo& versionInfo = versionStampMap_[languageId]; + if (!versionInfo.HasFixedFileInfo()) { + return false; + } + + VS_FIXEDFILEINFO& root = versionInfo.GetFixedFileInfo(); + + root.dwFileVersionMS = v1 << 16 | v2; + root.dwFileVersionLS = v3 << 16 | v4; + return true; +} + +bool ResourceUpdater::SetFileVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4) +{ + LANGID langId = versionStampMap_.empty() ? kLangEnUs + : versionStampMap_.begin()->first; + return SetFileVersion(langId, 1, v1, v2, v3, v4); +} + +bool ResourceUpdater::ChangeString(WORD languageId, UINT id, const WCHAR* value) +{ + StringTable& table = stringTableMap_[languageId]; + + UINT blockId = id / 16; + if (table.find(blockId) == table.end()) { + // Fill the table until we reach the block. + for (size_t i = table.size(); i <= blockId; ++i) { + table[i] = std::vector(16); + } + } + + assert(table[blockId].size() == 16); + UINT blockIndex = id % 16; + table[blockId][blockIndex] = value; + + return true; +} + +bool ResourceUpdater::ChangeString(UINT id, const WCHAR* value) +{ + LANGID langId = stringTableMap_.empty() ? kLangEnUs + : stringTableMap_.begin()->first; + return ChangeString(langId, id, value); +} + +bool ResourceUpdater::ChangeRcData(UINT id, const WCHAR* pathToResource) +{ + auto rcDataLngPairIt = std::find_if(rcDataLngMap_.begin(), rcDataLngMap_.end(), [=](const auto& rcDataLngPair) { + return rcDataLngPair.second.find(id) != rcDataLngPair.second.end(); + }); + + if (rcDataLngPairIt == rcDataLngMap_.end()) { + fprintf(stderr, "Cannot find RCDATA with id '%u'\n", id); + return false; + } + + wchar_t abspath[MAX_PATH] = { 0 }; + const auto filePath = _wfullpath(abspath, pathToResource, MAX_PATH) ? abspath : pathToResource; + ScopedFile newRcDataFile(filePath); + if (newRcDataFile == INVALID_HANDLE_VALUE) { + fprintf(stderr, "Cannot open new data file '%ws'\n", filePath); + return false; + } + + const auto dwFileSize = GetFileSize(newRcDataFile, NULL); + if (dwFileSize == INVALID_FILE_SIZE) { + fprintf(stderr, "Cannot get file size for '%ws'\n", filePath); + return false; + } + + auto& rcData = rcDataLngPairIt->second[id]; + rcData.clear(); + rcData.resize(dwFileSize); + + DWORD dwBytesRead { 0 }; + if (!ReadFile(newRcDataFile, rcData.data(), dwFileSize, &dwBytesRead, NULL)) { + fprintf(stderr, "Cannot read file '%ws'\n", filePath); + return false; + } + + return true; +} + +const WCHAR* ResourceUpdater::GetString(WORD languageId, UINT id) +{ + StringTable& table = stringTableMap_[languageId]; + + UINT blockId = id / 16; + if (table.find(blockId) == table.end()) { + // Fill the table until we reach the block. + for (size_t i = table.size(); i <= blockId; ++i) { + table[i] = std::vector(16); + } + } + + assert(table[blockId].size() == 16); + UINT blockIndex = id % 16; + + return table[blockId][blockIndex].c_str(); +} + +const WCHAR* ResourceUpdater::GetString(UINT id) +{ + LANGID langId = stringTableMap_.empty() ? kLangEnUs + : stringTableMap_.begin()->first; + return GetString(langId, id); +} + +bool ResourceUpdater::SetIcon(const WCHAR* path, const LANGID& langId, + UINT iconBundle) +{ + std::unique_ptr& pIcon = iconBundleMap_[langId].iconBundles[iconBundle]; + if (!pIcon) + pIcon = std::make_unique(); + + auto& icon = *pIcon; + DWORD bytes; + + ScopedFile file(path); + if (file == INVALID_HANDLE_VALUE) { + fwprintf(stderr, L"Cannot open icon file '%ls'\n", path); + return false; + } + + IconsValue::ICONHEADER& header = icon.header; + if (!ReadFile(file, &header, 3 * sizeof(WORD), &bytes, NULL)) { + fwprintf(stderr, L"Cannot read icon header for '%ls'\n", path); + return false; + } + + if (header.reserved != 0 || header.type != 1) { + fwprintf(stderr, L"Reserved header is not 0 or image type is not icon for '%ls'\n", path); + return false; + } + + header.entries.resize(header.count); + if (!ReadFile(file, header.entries.data(), header.count * sizeof(IconsValue::ICONENTRY), &bytes, NULL)) { + fwprintf(stderr, L"Cannot read icon metadata for '%ls'\n", path); + return false; + } + + icon.images.resize(header.count); + for (size_t i = 0; i < header.count; ++i) { + icon.images[i].resize(header.entries[i].bytesInRes); + SetFilePointer(file, header.entries[i].imageOffset, NULL, FILE_BEGIN); + if (!ReadFile(file, icon.images[i].data(), icon.images[i].size(), &bytes, NULL)) { + fwprintf(stderr, L"Cannot read icon data for '%ls'\n", path); + return false; + } + } + + icon.grpHeader.resize(3 * sizeof(WORD) + header.count * sizeof(GRPICONENTRY)); + GRPICONHEADER* pGrpHeader = reinterpret_cast(icon.grpHeader.data()); + pGrpHeader->reserved = 0; + pGrpHeader->type = 1; + pGrpHeader->count = header.count; + for (size_t i = 0; i < header.count; ++i) { + GRPICONENTRY* entry = pGrpHeader->entries + i; + entry->bitCount = 0; + entry->bytesInRes = header.entries[i].bitCount; + entry->bytesInRes2 = header.entries[i].bytesInRes; + entry->colourCount = header.entries[i].colorCount; + entry->height = header.entries[i].height; + entry->id = i + 1; + entry->planes = header.entries[i].planes; + entry->reserved = header.entries[i].reserved; + entry->width = header.entries[i].width; + entry->reserved2 = 0; + } + + return true; +} + +bool ResourceUpdater::SetIcon(const WCHAR* path, const LANGID& langId) +{ + if (iconBundleMap_[langId].iconBundles.empty()) { + return SetIcon(path, langId, kDefaultIconBundle); + } + UINT iconBundle = iconBundleMap_[langId].iconBundles.begin()->first; + return SetIcon(path, langId, iconBundle); +} + +bool ResourceUpdater::SetIcon(const WCHAR* path) +{ + LANGID langId = iconBundleMap_.empty() ? kLangEnUs + : iconBundleMap_.begin()->first; + return SetIcon(path, langId); +} + +bool ResourceUpdater::Commit() +{ + if (module_ == NULL) { + return false; + } + FreeLibrary(module_); + module_ = NULL; + + ScopedResourceUpdater ru(filename_.c_str(), false); + if (ru.Get() == NULL) { + return false; + } + + // update version info. + for (const auto& i : versionStampMap_) { + LANGID langId = i.first; + std::vector out = i.second.Serialize(); + + if (!UpdateResourceW(ru.Get(), RT_VERSION, MAKEINTRESOURCEW(1), langId, + &out[0], static_cast(out.size()))) { + return false; + } + } + + // update the execution level + if (applicationManifestPath_.empty() && !executionLevel_.empty()) { + // string replace with requested executionLevel + std::wstring::size_type pos = 0u; + while ((pos = manifestString_.find(originalExecutionLevel_, pos)) != std::string::npos) { + manifestString_.replace(pos, originalExecutionLevel_.length(), executionLevel_); + pos += executionLevel_.length(); + } + + // clean old padding and add new padding, ensuring that the size is a multiple of 4 + std::wstring::size_type padPos = manifestString_.find(L""); + // trim anything after the , 11 being the length of (ie, remove old padding) + std::wstring trimmedStr = manifestString_.substr(0, padPos + 11); + std::wstring padding = L"\n"; + + int offset = (trimmedStr.length() + padding.length()) % 4; + // multiple X by the number in offset + pos = 0u; + for (int posCount = 0; posCount < offset; posCount = posCount + 1) { + if ((pos = padding.find(L"X", pos)) != std::string::npos) { + padding.replace(pos, 1, L"XX"); + pos += executionLevel_.length(); + } + } + + // convert the wchar back into char, so that it encodes correctly for Windows to read the XML. + std::wstring stringSectionW = trimmedStr + padding; + std::wstring_convert, wchar_t> converter; + std::string stringSection = converter.to_bytes(stringSectionW); + + if (!UpdateResourceW(ru.Get(), RT_MANIFEST, MAKEINTRESOURCEW(1), + kLangEnUs, // this is hardcoded at 1033, ie, en-us, as that is what RT_MANIFEST default uses + &stringSection.at(0), sizeof(char) * stringSection.size())) { + return false; + } + } + + // load file contents and replace the manifest + if (!applicationManifestPath_.empty()) { + std::wstring fileContents = ReadFileToString(applicationManifestPath_.c_str()); + + // clean old padding and add new padding, ensuring that the size is a multiple of 4 + std::wstring::size_type padPos = fileContents.find(L""); + // trim anything after the , 11 being the length of (ie, remove old padding) + std::wstring trimmedStr = fileContents.substr(0, padPos + 11); + std::wstring padding = L"\n"; + + int offset = (trimmedStr.length() + padding.length()) % 4; + // multiple X by the number in offset + std::wstring::size_type pos = 0u; + for (int posCount = 0; posCount < offset; posCount = posCount + 1) { + if ((pos = padding.find(L"X", pos)) != std::string::npos) { + padding.replace(pos, 1, L"XX"); + pos += executionLevel_.length(); + } + } + + // convert the wchar back into char, so that it encodes correctly for Windows to read the XML. + std::wstring stringSectionW = fileContents + padding; + std::wstring_convert, wchar_t> converter; + std::string stringSection = converter.to_bytes(stringSectionW); + + if (!UpdateResourceW(ru.Get(), RT_MANIFEST, MAKEINTRESOURCEW(1), + kLangEnUs, // this is hardcoded at 1033, ie, en-us, as that is what RT_MANIFEST default uses + &stringSection.at(0), sizeof(char) * stringSection.size())) { + return false; + } + } + + // update string table. + for (const auto& i : stringTableMap_) { + for (const auto& j : i.second) { + std::vector stringTableBuffer; + if (!SerializeStringTable(j.second, j.first, &stringTableBuffer)) { + return false; + } + + if (!UpdateResourceW(ru.Get(), RT_STRING, MAKEINTRESOURCEW(j.first + 1), i.first, + &stringTableBuffer[0], static_cast(stringTableBuffer.size()))) { + return false; + } + } + } + + for (const auto& rcDataLangPair : rcDataLngMap_) { + for (const auto& rcDataMap : rcDataLangPair.second) { + if (!UpdateResourceW(ru.Get(), RT_RCDATA, reinterpret_cast(rcDataMap.first), + rcDataLangPair.first, (LPVOID)rcDataMap.second.data(), rcDataMap.second.size())) { + return false; + } + } + } + + for (const auto& iLangIconInfoPair : iconBundleMap_) { + auto langId = iLangIconInfoPair.first; + auto maxIconId = iLangIconInfoPair.second.maxIconId; + for (const auto& iNameBundlePair : iLangIconInfoPair.second.iconBundles) { + UINT bundleId = iNameBundlePair.first; + const std::unique_ptr& pIcon = iNameBundlePair.second; + if (!pIcon) + continue; + + auto& icon = *pIcon; + // update icon. + if (icon.grpHeader.size() > 0) { + if (!UpdateResourceW(ru.Get(), RT_GROUP_ICON, MAKEINTRESOURCEW(bundleId), + langId, icon.grpHeader.data(), icon.grpHeader.size())) { + return false; + } + + for (size_t i = 0; i < icon.header.count; ++i) { + if (!UpdateResourceW(ru.Get(), RT_ICON, MAKEINTRESOURCEW(i + 1), + langId, icon.images[i].data(), icon.images[i].size())) { + + return false; + } + } + + for (size_t i = icon.header.count; i < maxIconId; ++i) { + if (!UpdateResourceW(ru.Get(), RT_ICON, MAKEINTRESOURCEW(i + 1), + langId, nullptr, 0)) { + return false; + } + } + } + } + } + + return ru.Commit(); +} + +bool ResourceUpdater::SerializeStringTable(const StringValues& values, UINT blockId, std::vector* out) +{ + // calc total size. + // string table is pascal string list. + size_t size = 0; + for (size_t i = 0; i < 16; i++) { + size += sizeof(WORD); + size += values[i].length() * sizeof(WCHAR); + } + + out->resize(size); + + // write. + char* pDst = &(*out)[0]; + for (size_t i = 0; i < 16; i++) { + WORD length = static_cast(values[i].length()); + memcpy(pDst, &length, sizeof(length)); + pDst += sizeof(WORD); + + if (length > 0) { + WORD bytes = length * sizeof(WCHAR); + memcpy(pDst, values[i].c_str(), bytes); + pDst += bytes; + } + } + + return true; +} + +// static +BOOL CALLBACK ResourceUpdater::OnEnumResourceLanguage(HANDLE hModule, LPCWSTR lpszType, LPCWSTR lpszName, WORD wIDLanguage, LONG_PTR lParam) +{ + ResourceUpdater* instance = reinterpret_cast(lParam); + if (IS_INTRESOURCE(lpszName) && IS_INTRESOURCE(lpszType)) { + // case reinterpret_cast(RT_VERSION): { + switch (reinterpret_cast(lpszType)) { + case 16: { + try { + instance->versionStampMap_[wIDLanguage] = VersionInfo(instance->module_, wIDLanguage); + } catch (const std::system_error& e) { + return false; + } + break; + } + case 6: { + // case reinterpret_cast(RT_STRING): { + UINT id = reinterpret_cast(lpszName) - 1; + auto& vector = instance->stringTableMap_[wIDLanguage][id]; + for (size_t k = 0; k < 16; k++) { + CStringW buf; + + buf.LoadStringW(instance->module_, id * 16 + k, wIDLanguage); + vector.push_back(buf.GetBuffer()); + } + break; + } + // case reinterpret_cast(RT_ICON): { + case 3: { + UINT iconId = reinterpret_cast(lpszName); + UINT maxIconId = instance->iconBundleMap_[wIDLanguage].maxIconId; + if (iconId > maxIconId) + maxIconId = iconId; + break; + } + // case reinterpret_cast(RT_GROUP_ICON): { + case 14: { + UINT iconId = reinterpret_cast(lpszName); + instance->iconBundleMap_[wIDLanguage].iconBundles[iconId] = nullptr; + break; + } + // case reinterpret_cast(RT_RCDATA): { + case 10: { + const auto moduleHandle = HMODULE(hModule); + HRSRC hResInfo = FindResource(moduleHandle, lpszName, lpszType); + DWORD cbResource = SizeofResource(moduleHandle, hResInfo); + HGLOBAL hResData = LoadResource(moduleHandle, hResInfo); + + const auto* pResource = (const BYTE*)LockResource(hResData); + const auto resId = reinterpret_cast(lpszName); + instance->rcDataLngMap_[wIDLanguage][resId] = std::vector(pResource, pResource + cbResource); + + UnlockResource(hResData); + FreeResource(hResData); + } + default: + break; + } + } + return TRUE; +} + +// static +BOOL CALLBACK ResourceUpdater::OnEnumResourceName(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam) +{ + EnumResourceLanguagesW(hModule, lpszType, lpszName, (ENUMRESLANGPROCW)OnEnumResourceLanguage, lParam); + return TRUE; +} + +// static +// courtesy of http://stackoverflow.com/questions/420852/reading-an-applications-manifest-file +BOOL CALLBACK ResourceUpdater::OnEnumResourceManifest(HMODULE hModule, LPCTSTR lpType, LPWSTR lpName, LONG_PTR lParam) +{ + ResourceUpdater* instance = reinterpret_cast(lParam); + HRSRC hResInfo = FindResource(hModule, lpName, lpType); + DWORD cbResource = SizeofResource(hModule, hResInfo); + + HGLOBAL hResData = LoadResource(hModule, hResInfo); + const BYTE* pResource = (const BYTE*)LockResource(hResData); + + // FIXME(zcbenz): Do a real UTF string convertion. + int len = strlen(reinterpret_cast(pResource)); + std::wstring manifestStringLocal(pResource, pResource + len); + + // FIXME(zcbenz): Strip the BOM instead of doing string search. + size_t start = manifestStringLocal.find(L" 0) { + manifestStringLocal = manifestStringLocal.substr(start); + } + + // Support alternative formatting, such as using " vs ' and level="..." on another line + size_t found = manifestStringLocal.find(L"requestedExecutionLevel"); + size_t level = manifestStringLocal.find(L"level=\"", found); + size_t end = manifestStringLocal.find(L"\"", level + 7); + if (level < 0) { + level = manifestStringLocal.find(L"level=\'", found); + end = manifestStringLocal.find(L"\'", level + 7); + } + + instance->originalExecutionLevel_ = manifestStringLocal.substr(level + 7, end - level - 7); + + // also store original manifestString + instance->manifestString_ = manifestStringLocal; + + UnlockResource(hResData); + FreeResource(hResData); + + return TRUE; // Keep going +} + +ScopedResourceUpdater::ScopedResourceUpdater(const WCHAR* filename, bool deleteOld) + : handle_(BeginUpdateResourceW(filename, deleteOld)) +{ +} + +ScopedResourceUpdater::~ScopedResourceUpdater() +{ + if (!commited_) { + EndUpdate(false); + } +} + +HANDLE ScopedResourceUpdater::Get() const +{ + return handle_; +} + +bool ScopedResourceUpdater::Commit() +{ + commited_ = true; + return EndUpdate(true); +} + +bool ScopedResourceUpdater::EndUpdate(bool doesCommit) +{ + BOOL fDiscard = doesCommit ? FALSE : TRUE; + BOOL bResult = EndUpdateResourceW(handle_, fDiscard); + DWORD e = GetLastError(); + return bResult ? true : false; +} + +} // namespace rescle diff --git a/src/bun.js/bindings/windows/rescle.h b/src/bun.js/bindings/windows/rescle.h new file mode 100644 index 00000000000000..417de1dc0ddb3a --- /dev/null +++ b/src/bun.js/bindings/windows/rescle.h @@ -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 +#include +#include + +#include +#include // 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 entries; + } ICONHEADER; + + ICONHEADER header; + std::vector> images; + std::vector grpHeader; +}; + +struct Translate { + LANGID wLanguage; + WORD wCodePage; +}; + +typedef std::pair VersionString; +typedef std::pair OffsetLengthPair; + +struct VersionStringTable { + Translate encoding; + std::vector strings; +}; + +class VersionInfo { +public: + VersionInfo(); + VersionInfo(HMODULE hModule, WORD languageId); + + std::vector Serialize() const; + + bool HasFixedFileInfo() const; + VS_FIXEDFILEINFO& GetFixedFileInfo(); + const VS_FIXEDFILEINFO& GetFixedFileInfo() const; + void SetFixedFileInfo(const VS_FIXEDFILEINFO& value); + + std::vector stringTables; + std::vector 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& stringTables); + void DeserializeVarFileInfo(const unsigned char* offset, std::vector& translations); + OffsetLengthPair GetChildrenData(const BYTE* entryData); +}; + +class ResourceUpdater { +public: + typedef std::vector StringValues; + typedef std::map StringTable; + typedef std::map StringTableMap; + typedef std::map VersionStampMap; + typedef std::map> IconTable; + typedef std::vector RcDataValue; + typedef std::map RcDataMap; + typedef std::map RcDataLangMap; + + struct IconResInfo { + UINT maxIconId = 0; + IconTable iconBundles; + }; + + typedef std::map 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* 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 diff --git a/src/cli.zig b/src/cli.zig index 49a9124216e25f..eb9c0d07264186 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -290,7 +290,9 @@ pub const Arguments = struct { clap.parseParam("--conditions ... 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 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 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 targetting Windows, prevent a Command prompt from opening alongside the executable") catch unreachable, + clap.parseParam("--windows-icon When using --compile targetting 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 { diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index f9600fd33567c0..e5588e9373a21b 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -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) { diff --git a/src/windows.zig b/src/windows.zig index 2f479c14db612a..b7d7d6860cba40 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -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, + }; + } +}; From 944a2e81afcd214c1d92124dab0ab72fb1e47bfc Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 19 Dec 2024 22:05:47 -0800 Subject: [PATCH 2/9] link --- src/bun.js/bindings/windows/rescle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/windows/rescle.cpp b/src/bun.js/bindings/windows/rescle.cpp index eb72626b815527..a3171b4eaaa455 100644 --- a/src/bun.js/bindings/windows/rescle.cpp +++ b/src/bun.js/bindings/windows/rescle.cpp @@ -1,5 +1,5 @@ // This file is from Electron's fork of rescle -// https://github.com/electron/rcedit/blob/e36b688b42df0e236922019ce14e0ea165dc176d/src/rescle.CC +// https://github.com/electron/rcedit/blob/e36b688b42df0e236922019ce14e0ea165dc176d/src/rescle.cc // 'bun build --compile' uses this on Windows to allow // patching the icon of the generated executable. // From 55044d578b7fd79dce63d91cbd8dc68ed14f03de Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 19 Dec 2024 23:53:04 -0800 Subject: [PATCH 3/9] Update src/cli.zig Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> --- src/cli.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index eb9c0d07264186..9c11e4d2e92233 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -291,8 +291,8 @@ pub const Arguments = struct { 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 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 targetting Windows, prevent a Command prompt from opening alongside the executable") catch unreachable, - clap.parseParam("--windows-icon When using --compile targetting Windows, assign an executable icon") 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 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, From 176d4839593a96e88669f96c76f8124a85696675 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Dec 2024 00:33:27 -0800 Subject: [PATCH 4/9] Try this --- cmake/targets/BuildBun.cmake | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index e06c0fa2e8a8e6..f40c08019b62f5 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -655,19 +655,28 @@ if(WIN32) 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 command to compile the .rc to .res + 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 + ) + list(APPEND BUN_RESOURCES ${CODEGEN_PATH}/windows-app-info.res) endif() # --- Executable --- set(BUN_CPP_OUTPUT ${BUILD_PATH}/${CMAKE_STATIC_LIBRARY_PREFIX}${bun}${CMAKE_STATIC_LIBRARY_SUFFIX}) -if(WIN32) - list(APPEND BUN_CPP_OUTPUT ${CODEGEN_PATH}/windows-app-info.rc) -endif() if(BUN_LINK_ONLY) - add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT}) + if (NOT WIN32) + add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT}) + else() + # .ico file + add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT} ${BUN_RESOURCES}) + endif() set_target_properties(${bun} PROPERTIES LINKER_LANGUAGE CXX) target_link_libraries(${bun} PRIVATE ${BUN_CPP_OUTPUT}) elseif(BUN_CPP_ONLY) From f3bb837d1509127156b7f311ef268e8d1d5635d2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Dec 2024 02:08:42 -0800 Subject: [PATCH 5/9] Try this --- cmake/targets/BuildBun.cmake | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index f40c08019b62f5..736e5cb1019938 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -657,13 +657,12 @@ if(WIN32) ${CODEGEN_PATH}/windows-app-info.rc @ONLY ) - # Add command to compile the .rc to .res 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 + COMMAND rc.exe /I "${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_INCLUDE}" /fo ${CODEGEN_PATH}/windows-app-info.res ${CODEGEN_PATH}/windows-app-info.rc DEPENDS ${CODEGEN_PATH}/windows-app-info.rc ) - list(APPEND BUN_RESOURCES ${CODEGEN_PATH}/windows-app-info.res) + list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.res) endif() # --- Executable --- From c328d09766cf088c2f59ebbd5156003ba52055ff Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Dec 2024 02:09:31 -0800 Subject: [PATCH 6/9] Update BuildBun.cmake --- cmake/targets/BuildBun.cmake | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 736e5cb1019938..33a9c6edf9f652 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -670,12 +670,7 @@ endif() set(BUN_CPP_OUTPUT ${BUILD_PATH}/${CMAKE_STATIC_LIBRARY_PREFIX}${bun}${CMAKE_STATIC_LIBRARY_SUFFIX}) if(BUN_LINK_ONLY) - if (NOT WIN32) - add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT}) - else() - # .ico file - add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT} ${BUN_RESOURCES}) - endif() + add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT}) set_target_properties(${bun} PROPERTIES LINKER_LANGUAGE CXX) target_link_libraries(${bun} PRIVATE ${BUN_CPP_OUTPUT}) elseif(BUN_CPP_ONLY) From a1555337ad2c5ae5b99b54be125fe6e479d1cf6e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Dec 2024 02:22:11 -0800 Subject: [PATCH 7/9] Update BuildBun.cmake --- cmake/targets/BuildBun.cmake | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 33a9c6edf9f652..fa3e33623462ca 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -657,12 +657,7 @@ if(WIN32) ${CODEGEN_PATH}/windows-app-info.rc @ONLY ) - add_custom_command( - OUTPUT ${CODEGEN_PATH}/windows-app-info.res - COMMAND rc.exe /I "${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_INCLUDE}" /fo ${CODEGEN_PATH}/windows-app-info.res ${CODEGEN_PATH}/windows-app-info.rc - DEPENDS ${CODEGEN_PATH}/windows-app-info.rc - ) - list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.res) + list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.rc) endif() # --- Executable --- From e09b685ebd396bc81177bfb2624f2d664d31aae9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Dec 2024 02:26:32 -0800 Subject: [PATCH 8/9] try this --- cmake/targets/BuildBun.cmake | 9 ++++++++- src/windows-app-info.rc | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index fa3e33623462ca..a8c45b8b43d29a 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -652,12 +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" + ) + list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.res) endif() # --- Executable --- diff --git a/src/windows-app-info.rc b/src/windows-app-info.rc index 2822798ed0512b..361145deb4a2f0 100644 --- a/src/windows-app-info.rc +++ b/src/windows-app-info.rc @@ -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 From 8c4adf9d30347ecbba99c09889848a111ef32907 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Dec 2024 02:51:23 -0800 Subject: [PATCH 9/9] Fix --- cmake/targets/BuildBun.cmake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index a8c45b8b43d29a..11759e2f7a9418 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -664,7 +664,7 @@ if(WIN32) 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" ) - list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.res) + set(WINDOWS_RESOURCES ${CODEGEN_PATH}/windows-app-info.res) endif() # --- Executable --- @@ -672,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) @@ -690,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()