Skip to content

Commit 1bbf576

Browse files
author
Vladimir Vilimaitis
committed
Improve Windows release launcher UX
1 parent 078186e commit 1bbf576

5 files changed

Lines changed: 268 additions & 2 deletions

File tree

.github/workflows/release-binaries.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,23 @@ jobs:
131131
- name: Package Windows bundle
132132
if: runner.os == 'Windows'
133133
shell: pwsh
134-
run: Compress-Archive -Path dist/rembg-gui/* -DestinationPath ${{ matrix.artifact }}
134+
run: |
135+
$required = @(
136+
"dist/rembg-gui/rembg-gui.exe",
137+
"dist/rembg-gui/README-Windows.txt",
138+
"dist/rembg-gui/bin/rembg-gui-app.exe",
139+
"dist/rembg-gui/bin/Qt6Core.dll",
140+
"dist/rembg-gui/bin/onnxruntime.dll",
141+
"dist/rembg-gui/bin/qt.conf"
142+
)
143+
144+
foreach ($path in $required) {
145+
if (!(Test-Path $path)) {
146+
throw "Missing expected bundle file: $path"
147+
}
148+
}
149+
150+
Compress-Archive -Path dist/rembg-gui/* -DestinationPath ${{ matrix.artifact }}
135151
136152
- name: Upload artifact
137153
uses: actions/upload-artifact@v4

src_cpp/CMakeLists.txt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
cmake_minimum_required(VERSION 3.25)
22

3+
if(POLICY CMP0091)
4+
cmake_policy(SET CMP0091 NEW)
5+
endif()
6+
37
project(rembg_gui_cpp LANGUAGES CXX)
48

59
include(CTest)
@@ -194,7 +198,11 @@ endfunction()
194198
add_executable(rembg-gui-native src/main.cpp)
195199
target_link_libraries(rembg-gui-native PRIVATE rembg_native_lib)
196200
rembg_import_qt_static_plugins(rembg-gui-native)
197-
set_target_properties(rembg-gui-native PROPERTIES OUTPUT_NAME rembg-gui)
201+
if(WIN32)
202+
set_target_properties(rembg-gui-native PROPERTIES OUTPUT_NAME rembg-gui-app)
203+
else()
204+
set_target_properties(rembg-gui-native PROPERTIES OUTPUT_NAME rembg-gui)
205+
endif()
198206

199207
add_custom_command(TARGET rembg-gui-native POST_BUILD
200208
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
@@ -204,6 +212,14 @@ add_custom_command(TARGET rembg-gui-native POST_BUILD
204212

205213
if(WIN32)
206214
set_target_properties(rembg-gui-native PROPERTIES WIN32_EXECUTABLE TRUE)
215+
add_executable(rembg-gui-launcher WIN32 src/WindowsLauncher.cpp)
216+
target_compile_features(rembg-gui-launcher PRIVATE cxx_std_23)
217+
target_link_libraries(rembg-gui-launcher PRIVATE Shell32 User32)
218+
set_target_properties(rembg-gui-launcher
219+
PROPERTIES
220+
OUTPUT_NAME rembg-gui
221+
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
222+
)
207223
elseif(APPLE)
208224
set_target_properties(rembg-gui-native PROPERTIES MACOSX_BUNDLE TRUE)
209225
elseif(UNIX)
@@ -218,6 +234,8 @@ install(TARGETS rembg-gui-native
218234
)
219235

220236
if(WIN32)
237+
install(TARGETS rembg-gui-launcher RUNTIME DESTINATION .)
238+
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/README-Windows.txt DESTINATION .)
221239
install(FILES $<TARGET_RUNTIME_DLLS:rembg-gui-native> DESTINATION ${CMAKE_INSTALL_BINDIR})
222240
if(DEFINED VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET)
223241
set(REMBG_VCPKG_BIN_DIR "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin")

src_cpp/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ builds, runs `ctest` with `REMBG_NATIVE_RUN_MODEL_TESTS=1`, installs into
4141
Releases keep `gui.py` at the repository root and publish a Windows x64 zip
4242
bundle plus a Linux x64 AppImage. The ONNX model is not bundled with the
4343
binaries.
44+
45+
The Windows zip opens with `rembg-gui.exe` in the top folder. That file is a
46+
small launcher; the real Qt app and its DLLs stay in `bin/` as
47+
`rembg-gui-app.exe`. Users should extract the zip and run the launcher from the
48+
extracted folder.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Rembg GUI for Windows
2+
3+
1. Extract this zip file first.
4+
2. Open the extracted folder.
5+
3. Double-click rembg-gui.exe.
6+
7+
Do not move files out of this folder. The app needs the bin, Qt6, and
8+
translations folders next to rembg-gui.exe.
9+
10+
The first background removal downloads the U2Net model and caches it on your PC.

src_cpp/src/WindowsLauncher.cpp

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#define WIN32_LEAN_AND_MEAN
2+
#include <windows.h>
3+
4+
#include <shellapi.h>
5+
6+
#include <string>
7+
#include <string_view>
8+
#include <vector>
9+
10+
namespace {
11+
12+
std::wstring modulePath()
13+
{
14+
std::wstring path(260, L'\0');
15+
for(;;) {
16+
const auto length =
17+
GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
18+
if(length == 0) {
19+
return {};
20+
}
21+
if(length < path.size()) {
22+
path.resize(length);
23+
return path;
24+
}
25+
path.resize(path.size() * 2);
26+
}
27+
}
28+
29+
std::wstring parentDirectory(std::wstring_view path)
30+
{
31+
const auto separator = path.find_last_of(L"\\/");
32+
if(separator == std::wstring_view::npos) {
33+
return L".";
34+
}
35+
return std::wstring(path.substr(0, separator));
36+
}
37+
38+
std::wstring joinPath(std::wstring_view left, std::wstring_view right)
39+
{
40+
if(left.empty()) {
41+
return std::wstring(right);
42+
}
43+
44+
std::wstring path(left);
45+
if(path.back() != L'\\' && path.back() != L'/') {
46+
path.push_back(L'\\');
47+
}
48+
path.append(right);
49+
return path;
50+
}
51+
52+
bool fileExists(std::wstring_view path)
53+
{
54+
const auto attributes = GetFileAttributesW(std::wstring(path).c_str());
55+
return attributes != INVALID_FILE_ATTRIBUTES &&
56+
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
57+
}
58+
59+
std::wstring quoteCommandLineArgument(std::wstring_view argument)
60+
{
61+
if(!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring_view::npos) {
62+
return std::wstring(argument);
63+
}
64+
65+
std::wstring quoted;
66+
quoted.push_back(L'"');
67+
68+
auto backslashes = std::size_t{0};
69+
for(const auto character : argument) {
70+
if(character == L'\\') {
71+
++backslashes;
72+
continue;
73+
}
74+
75+
if(character == L'"') {
76+
quoted.append((backslashes * 2) + 1, L'\\');
77+
quoted.push_back(character);
78+
backslashes = 0;
79+
continue;
80+
}
81+
82+
quoted.append(backslashes, L'\\');
83+
backslashes = 0;
84+
quoted.push_back(character);
85+
}
86+
87+
quoted.append(backslashes * 2, L'\\');
88+
quoted.push_back(L'"');
89+
return quoted;
90+
}
91+
92+
std::wstring formatWindowsError(DWORD errorCode)
93+
{
94+
wchar_t* rawMessage = nullptr;
95+
const auto length = FormatMessageW(
96+
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
97+
FORMAT_MESSAGE_IGNORE_INSERTS,
98+
nullptr,
99+
errorCode,
100+
0,
101+
reinterpret_cast<wchar_t*>(&rawMessage),
102+
0,
103+
nullptr);
104+
105+
std::wstring message =
106+
length == 0 ? L"Unknown Windows error." : std::wstring(rawMessage, length);
107+
if(rawMessage != nullptr) {
108+
LocalFree(rawMessage);
109+
}
110+
111+
while(!message.empty() &&
112+
(message.back() == L'\r' || message.back() == L'\n' || message.back() == L' ')) {
113+
message.pop_back();
114+
}
115+
return message;
116+
}
117+
118+
bool launcherMessageBoxesAreDisabled()
119+
{
120+
const auto required =
121+
GetEnvironmentVariableW(L"REMBG_GUI_LAUNCHER_SILENT", nullptr, 0);
122+
if(required == 0) {
123+
return false;
124+
}
125+
126+
std::wstring value(required, L'\0');
127+
const auto length = GetEnvironmentVariableW(
128+
L"REMBG_GUI_LAUNCHER_SILENT", value.data(), static_cast<DWORD>(value.size()));
129+
return length > 0 && value.front() == L'1';
130+
}
131+
132+
int failWithMessage(std::wstring_view message)
133+
{
134+
if(!launcherMessageBoxesAreDisabled()) {
135+
MessageBoxW(nullptr, std::wstring(message).c_str(), L"Rembg GUI", MB_ICONERROR | MB_OK);
136+
}
137+
return 1;
138+
}
139+
140+
std::wstring buildChildCommandLine(std::wstring_view childPath)
141+
{
142+
auto commandLine = quoteCommandLineArgument(childPath);
143+
144+
auto argc = 0;
145+
const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc);
146+
if(argv == nullptr) {
147+
return commandLine;
148+
}
149+
150+
for(auto index = 1; index < argc; ++index) {
151+
commandLine.push_back(L' ');
152+
commandLine.append(quoteCommandLineArgument(argv[index]));
153+
}
154+
155+
LocalFree(argv);
156+
return commandLine;
157+
}
158+
159+
} // namespace
160+
161+
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
162+
{
163+
const auto launcherPath = modulePath();
164+
if(launcherPath.empty()) {
165+
return failWithMessage(L"Rembg GUI could not find its install folder.");
166+
}
167+
168+
const auto bundleRoot = parentDirectory(launcherPath);
169+
const auto childPath = joinPath(bundleRoot, L"bin\\rembg-gui-app.exe");
170+
171+
if(!fileExists(childPath)) {
172+
return failWithMessage(
173+
L"Rembg GUI could not find bin\\rembg-gui-app.exe.\n\n"
174+
L"Extract the whole zip file first, then run rembg-gui.exe from the "
175+
L"extracted folder.\n\n"
176+
L"Do not move rembg-gui.exe away from the bin, Qt6, and translations folders.");
177+
}
178+
179+
auto commandLine = buildChildCommandLine(childPath);
180+
std::vector<wchar_t> mutableCommandLine(commandLine.begin(), commandLine.end());
181+
mutableCommandLine.push_back(L'\0');
182+
183+
STARTUPINFOW startupInfo{};
184+
startupInfo.cb = sizeof(startupInfo);
185+
PROCESS_INFORMATION processInfo{};
186+
187+
const auto started = CreateProcessW(
188+
childPath.c_str(),
189+
mutableCommandLine.data(),
190+
nullptr,
191+
nullptr,
192+
FALSE,
193+
0,
194+
nullptr,
195+
bundleRoot.c_str(),
196+
&startupInfo,
197+
&processInfo);
198+
199+
if(started == FALSE) {
200+
return failWithMessage(
201+
L"Rembg GUI could not start bin\\rembg-gui-app.exe.\n\n" +
202+
formatWindowsError(GetLastError()));
203+
}
204+
205+
const auto waitResult = WaitForSingleObject(processInfo.hProcess, INFINITE);
206+
auto exitCode = DWORD{1};
207+
if(waitResult == WAIT_OBJECT_0) {
208+
GetExitCodeProcess(processInfo.hProcess, &exitCode);
209+
} else {
210+
failWithMessage(L"Rembg GUI lost track of the app process.\n\n" +
211+
formatWindowsError(GetLastError()));
212+
}
213+
214+
CloseHandle(processInfo.hThread);
215+
CloseHandle(processInfo.hProcess);
216+
return static_cast<int>(exitCode);
217+
}

0 commit comments

Comments
 (0)