Skip to content

Commit 10988a1

Browse files
committed
Add a windows launcher program to replace the current .bat files
This is still just an experiment but eventually I hope to remove the `.bat` files in favor of this executable. There are several reasons to want to do this: 1. Batch files are notoriously painful to work with and mis-understood. 2. Should be faster (no need to launch cmd.exe). 3. Works around several known issues with .bat files including one that is known unsolvable one (see emcc.bat for more details)
1 parent 83b1301 commit 10988a1

File tree

5 files changed

+212
-0
lines changed

5 files changed

+212
-0
lines changed

tools/maint/win32_launcher/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Windows Python Script Launcher
2+
==============================
3+
4+
This directory contains a simple launcher program for windows which is used to
5+
execute the emscripten compiler entry points using the python interpreter. It
6+
uses the its own name (the name of the currently running executable) to
7+
determine which python script to run and serves the same purpose as the
8+
``run_python.sh`` script does on non-windows platforms.
9+
10+
We build this executable statically using ``/MT`` so that it is maximally
11+
portable.

tools/maint/win32_launcher/build.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cl launcher.c /Fe:launcher.exe /MT

tools/maint/win32_launcher/build.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
3+
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
4+
cd ${SCRIPT_DIR}
5+
6+
x86_64-w64-mingw32-gcc -municode -static-libgcc -static-libstdc++ -s -O2 launcher.c -o launcher.exe -lshlwapi -lshell32

tools/maint/win32_launcher/launcher.c

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2025 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*
7+
* Small win32 application that is used to launcher emscripten via python.exe.
8+
* On non-windows platforms this is done via the run_pyton.sh shell script.
9+
*
10+
* The binary will look for a python script that matches its own name and run
11+
* that using python.exe.
12+
*/
13+
14+
// Define _WIN32_WINNT to Windows 7 for max portability
15+
#define _WIN32_WINNT 0x0601
16+
17+
#include <windows.h>
18+
#include <shellapi.h>
19+
#include <shlwapi.h>
20+
#include <stdio.h>
21+
#include <stdlib.h>
22+
23+
#pragma comment(lib, "shlwapi.lib")
24+
#pragma comment(lib, "shell32.lib")
25+
26+
wchar_t* get_python_executable() {
27+
wchar_t* python_exe_w = _wgetenv(L"EMSDK_PYTHON");
28+
if (!python_exe_w) {
29+
return L"python";
30+
}
31+
return python_exe_w;
32+
}
33+
34+
wchar_t* get_long_path(const wchar_t* path) {
35+
DWORD path_len = GetFullPathNameW(path, 0, NULL, NULL);
36+
if (path_len == 0)
37+
abort();
38+
39+
wchar_t* full_path = malloc(sizeof(wchar_t) * (path_len + 4));
40+
if (!full_path)
41+
abort();
42+
43+
wcscpy_s(full_path, path_len + 4, L"\\\\?\\");
44+
if (GetFullPathNameW(path, path_len, full_path + 4, NULL) == 0)
45+
abort();
46+
return full_path;
47+
}
48+
49+
// Get the name of the currently running executable (module)
50+
wchar_t* get_module_path() {
51+
DWORD buffer_size = MAX_PATH;
52+
wchar_t* module_path_w = malloc(sizeof(wchar_t) * buffer_size);
53+
if (!module_path_w)
54+
abort();
55+
56+
DWORD path_len = GetModuleFileNameW(NULL, module_path_w, buffer_size);
57+
while (path_len > 0 && path_len == buffer_size) {
58+
buffer_size *= 2;
59+
module_path_w = realloc(module_path_w, sizeof(wchar_t) * buffer_size);
60+
if (!module_path_w)
61+
abort();
62+
path_len = GetModuleFileNameW(NULL, module_path_w, buffer_size);
63+
}
64+
65+
if (path_len == 0)
66+
abort();
67+
68+
return module_path_w;
69+
}
70+
71+
// Handle the _EMCC_CCACHE environment variable which essentially re-executes
72+
// the lauccher command with `cacche.exe` prefixed.
73+
BOOL handle_ccache(wchar_t** application_name, wchar_t** command_line, const wchar_t* launcher_path_w) {
74+
const wchar_t* ccache_env = _wgetenv(L"_EMCC_CCACHE");
75+
if (!ccache_env || !*ccache_env) {
76+
return FALSE;
77+
}
78+
79+
_wputenv_s(L"_EMCC_CCACHE", L"");
80+
*application_name = L"ccache.exe";
81+
82+
size_t command_line_len = wcslen(*application_name) + wcslen(launcher_path_w) + 5;
83+
*command_line = malloc(sizeof(wchar_t) * command_line_len);
84+
if (!*command_line)
85+
abort();
86+
87+
swprintf(*command_line, command_line_len, L"\"%s\" \"%s\"", *application_name, launcher_path_w);
88+
return TRUE;
89+
}
90+
91+
// A custom replacement for PathGetArgsW that is safe for command lines
92+
// longer than MAX_PATH.
93+
const wchar_t* find_args(const wchar_t* command_line) {
94+
const wchar_t* p = command_line;
95+
96+
// Skip past the executable name, which can be quoted.
97+
if (*p == L'"') {
98+
// The path is quoted, find the closing quote.
99+
p++;
100+
while (*p) {
101+
if (*p == L'"') {
102+
p++;
103+
break;
104+
}
105+
p++;
106+
}
107+
} else {
108+
// The path is not quoted, find the first space.
109+
while (*p && *p != L' ' && *p != L'\t') {
110+
p++;
111+
}
112+
}
113+
114+
// Skip any whitespace between the executable and the first argument.
115+
while (*p && (*p == L' ' || *p == L'\t')) {
116+
p++;
117+
}
118+
119+
return p;
120+
}
121+
122+
int wmain(int argc, wchar_t** argv) {
123+
wchar_t* launcher_path_w = get_module_path();
124+
125+
wchar_t* launcher_dir = wcsdup(launcher_path_w);
126+
PathRemoveFileSpecW(launcher_dir);
127+
128+
size_t path_len = wcslen(launcher_path_w);
129+
wchar_t* launcher_name_w = malloc((path_len + 1) * sizeof(wchar_t));
130+
if (!launcher_name_w)
131+
abort();
132+
wcscpy_s(launcher_name_w, path_len + 1, launcher_path_w);
133+
PathStripPathW(launcher_name_w);
134+
PathRemoveExtensionW(launcher_name_w);
135+
136+
size_t script_path_len = wcslen(launcher_dir) + wcslen(launcher_name_w) + 6;
137+
wchar_t* script_path_w = malloc(sizeof(wchar_t) * script_path_len);
138+
swprintf(script_path_w, script_path_len, L"%s\%s.py", launcher_dir, launcher_name_w);
139+
140+
wchar_t* application_name = NULL;
141+
wchar_t* command_line = NULL;
142+
143+
if (!handle_ccache(&application_name, &command_line, launcher_path_w)) {
144+
application_name = get_python_executable();
145+
wchar_t* long_script_path = get_long_path(script_path_w);
146+
size_t command_line_len = wcslen(application_name) + wcslen(long_script_path) + 10;
147+
command_line = malloc(sizeof(wchar_t) * command_line_len);
148+
swprintf(command_line, command_line_len, L"\"%s\" -E \"%s\"", application_name, long_script_path);
149+
free(long_script_path);
150+
// -E will not ignore _PYTHON_SYSCONFIGDATA_NAME an internal
151+
// of cpython used in cross compilation via setup.py.
152+
_wputenv_s(L"_PYTHON_SYSCONFIGDATA_NAME", L"");
153+
}
154+
155+
// Build the final command line by appending the original arguments
156+
const wchar_t* all_args = find_args(GetCommandLineW());
157+
if (all_args && *all_args) {
158+
size_t current_len = wcslen(command_line);
159+
size_t args_len = wcslen(all_args);
160+
// +2 for the space and the null terminator
161+
command_line = realloc(command_line, (current_len + args_len + 2) * sizeof(wchar_t));
162+
if (!command_line)
163+
abort();
164+
wcscat_s(command_line, current_len + args_len + 2, L" ");
165+
wcscat_s(command_line, current_len + args_len + 2, all_args);
166+
}
167+
168+
169+
// Work around python bug 34780 by closing stdin, so that it is not inherited
170+
// by the python subprocess.
171+
wchar_t* workaround_env = _wgetenv(L"EM_WORKAROUND_PYTHON_BUG_34780");
172+
if (workaround_env) {
173+
CloseHandle(GetStdHandle(STD_INPUT_HANDLE));
174+
}
175+
176+
STARTUPINFOW si;
177+
PROCESS_INFORMATION pi;
178+
ZeroMemory(&si, sizeof(si));
179+
si.cb = sizeof(si);
180+
ZeroMemory(&pi, sizeof(pi));
181+
182+
if (!CreateProcessW(application_name, command_line, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
183+
wprintf(L"CreateProcess failed (%d).\n", GetLastError());
184+
abort();
185+
}
186+
187+
WaitForSingleObject(pi.hProcess, INFINITE);
188+
189+
DWORD exit_code;
190+
GetExitCodeProcess(pi.hProcess, &exit_code);
191+
CloseHandle(pi.hProcess);
192+
CloseHandle(pi.hThread);
193+
return exit_code;
194+
}
42.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)