pysfi 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,995 +1,995 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import logging
5
- import platform
6
- import shutil
7
- import subprocess
8
- import time
9
- from pathlib import Path
10
-
11
- is_windows = platform.system() == "Windows"
12
- is_linux = platform.system() == "Linux"
13
- is_macos = platform.system() == "Darwin"
14
- ext = ".exe" if is_windows else ""
15
-
16
- logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
17
- logger = logging.getLogger(__name__)
18
- cwd = Path.cwd()
19
-
20
- _WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
21
- #include <stdio.h>
22
- #include <stdlib.h>
23
- #include <string.h>
24
- #include <locale.h>
25
-
26
- #define MAX_PATH_LEN 4096
27
- #define MAX_ERROR_LEN 8192
28
-
29
- // Check if Python runtime exists
30
- static int check_python_runtime(const char* runtime_path) {
31
- return GetFileAttributesA(runtime_path) != INVALID_FILE_ATTRIBUTES;
32
- }
33
-
34
- // Build Python command line
35
- static void build_python_command(
36
- char* cmd,
37
- const char* exe_dir,
38
- const char* entry_file,
39
- int is_debug
40
- ) {
41
- char python_runtime[MAX_PATH_LEN];
42
- char script_path[MAX_PATH_LEN];
43
-
44
- // Build Python interpreter path
45
- // GUI non-debug mode uses pythonw.exe (no console), other cases use python.exe
46
- if (is_debug) {
47
- // Debug mode: use python.exe to show console output
48
- snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
49
- } else {
50
- // Production GUI mode: use pythonw.exe without creating console window
51
- snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\pythonw.exe", exe_dir);
52
- }
53
-
54
- // Build startup script path
55
- snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
56
-
57
- // Build command line (add -u parameter for real-time output capture)
58
- if (is_debug) {
59
- // Debug mode: do not redirect output, display output on console
60
- snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);
61
- } else {
62
- // Production mode: redirect all output to pipe
63
- snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
64
- }
65
- }
66
-
67
- // Read process output
68
- static void read_process_output(HANDLE hPipe, char* output, int max_len) {
69
- DWORD bytes_read;
70
- output[0] = '\0';
71
-
72
- while (ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
73
- output[bytes_read] = '\0';
74
- }
75
- }
76
-
77
- // Show message box (support UTF-8 encoding)
78
- static void show_message_box(const char* title, const char* message) {
79
- // Convert UTF-8 to UTF-16 using MultiByteToWideChar
80
- int title_len = MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0);
81
- int msg_len = MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0);
82
-
83
- if (title_len > 0 && msg_len > 0) {
84
- wchar_t* wtitle = (wchar_t*)malloc(title_len * sizeof(wchar_t));
85
- wchar_t* wmsg = (wchar_t*)malloc(msg_len * sizeof(wchar_t));
86
-
87
- if (wtitle && wmsg) {
88
- MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, title_len);
89
- MultiByteToWideChar(CP_UTF8, 0, message, -1, wmsg, msg_len);
90
- MessageBoxW(NULL, wmsg, wtitle, MB_ICONERROR | MB_OK);
91
- }
92
-
93
- if (wtitle) free(wtitle);
94
- if (wmsg) free(wmsg);
95
- } else {
96
- // Conversion failed, use ANSI version
97
- MessageBoxA(NULL, message, title, MB_ICONERROR | MB_OK);
98
- }
99
- }
100
-
101
- // Windows GUI entry point
102
- int APIENTRY WinMain(
103
- HINSTANCE hInstance,
104
- HINSTANCE hPrevInstance,
105
- LPSTR lpCmdLine,
106
- int nCmdShow
107
- ) {
108
- char exe_dir[MAX_PATH_LEN];
109
- char cmd[MAX_PATH_LEN * 2];
110
- char error_output[MAX_ERROR_LEN] = "";
111
- STARTUPINFOA si = {0};
112
- PROCESS_INFORMATION pi = {0};
113
- SECURITY_ATTRIBUTES sa = {0};
114
- HANDLE hReadPipe = NULL, hWritePipe = NULL;
115
- BOOL success;
116
-
117
- // Get executable directory
118
- GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
119
- char* last_slash = strrchr(exe_dir, '\\');
120
- if (last_slash) {
121
- *last_slash = '\0';
122
- }
123
-
124
- // Check Python runtime
125
- if (!check_python_runtime(exe_dir)) {
126
- show_message_box(
127
- "Application Error",
128
- "Python runtime not found.\n\n"
129
- "Please ensure the application is installed correctly and the runtime directory exists."
130
- );
131
- return 1;
132
- }
133
-
134
- // Build and execute Python command
135
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE});
136
-
137
- // Create pipe for capturing output
138
- sa.nLength = sizeof(SECURITY_ATTRIBUTES);
139
- sa.bInheritHandle = TRUE;
140
- sa.lpSecurityDescriptor = NULL;
141
-
142
- if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
143
- show_message_box("Error", "Failed to create pipe for error output.");
144
- return 1;
145
- }
146
-
147
- // Set startup info
148
- si.cb = sizeof(si);
149
-
150
- // Set different startup methods based on debug mode
151
- if (${DEBUG_MODE}) {
152
- // Debug mode: do not use pipe, inherit console to display output
153
- // Keep si.dwFlags as 0, do not set STARTF_USESTDHANDLES
154
- } else {
155
- // Production GUI mode: use pipe to capture error output
156
- si.dwFlags = STARTF_USESTDHANDLES;
157
- si.hStdError = hWritePipe;
158
- si.hStdOutput = hWritePipe;
159
- }
160
-
161
- // Create Python process
162
- // Debug mode: do not use CREATE_NO_WINDOW, let python.exe create console
163
- // Production GUI mode: use CREATE_NO_WINDOW to ensure no console is created
164
- DWORD createFlags = ${DEBUG_MODE} ? 0 : CREATE_NO_WINDOW;
165
- success = CreateProcessA(
166
- NULL, cmd, NULL, NULL, FALSE,
167
- createFlags,
168
- NULL, exe_dir, &si, &pi
169
- );
170
-
171
- if (!success) {
172
- DWORD error = GetLastError();
173
- char error_msg[MAX_ERROR_LEN];
174
- snprintf(error_msg, MAX_ERROR_LEN,
175
- "Failed to start Python process.\n\n"
176
- "Error code: %lu\n"
177
- "Command: %s",
178
- error, cmd);
179
- show_message_box("Application Error", error_msg);
180
- if (!${DEBUG_MODE}) {
181
- CloseHandle(hWritePipe);
182
- CloseHandle(hReadPipe);
183
- }
184
- return 1;
185
- }
186
-
187
- // Read error output (only in non-debug mode)
188
- if (!${DEBUG_MODE}) {
189
- CloseHandle(hWritePipe);
190
- read_process_output(hReadPipe, error_output, MAX_ERROR_LEN);
191
- CloseHandle(hReadPipe);
192
- }
193
-
194
- // Wait for process to end
195
- WaitForSingleObject(pi.hProcess, INFINITE);
196
-
197
- // Get exit code
198
- DWORD exit_code;
199
- GetExitCodeProcess(pi.hProcess, &exit_code);
200
-
201
- // Debug output
202
- fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
203
-
204
- // Cleanup
205
- CloseHandle(pi.hProcess);
206
- CloseHandle(pi.hThread);
207
-
208
- // If process exits abnormally and there is error output, display error message
209
- if (exit_code != 0 && strlen(error_output) > 0) {
210
- // Truncate the last error message (up to 2000 characters)
211
- size_t error_len = strlen(error_output);
212
- if (error_len > 2000) {
213
- error_output[2000] = '\0';
214
- }
215
- show_message_box("Application Error", error_output);
216
- return exit_code;
217
- }
218
-
219
- return exit_code;
220
- }
221
- """
222
-
223
- _WINDOWS_CONSOLE_TEMPLATE: str = r"""#include <stdio.h>
224
- #include <stdlib.h>
225
- #include <string.h>
226
- #include <windows.h>
227
- #include <locale.h>
228
-
229
- #define MAX_PATH_LEN 4096
230
-
231
- // Set console encoding to UTF-8
232
- static void setup_encoding() {
233
- SetConsoleOutputCP(CP_UTF8);
234
- SetConsoleCP(CP_UTF8);
235
- setlocale(LC_ALL, ".UTF8");
236
- }
237
-
238
- // Check if Python runtime exists
239
- static int check_python_runtime(const char* runtime_path) {
240
- return GetFileAttributesA(runtime_path) != INVALID_FILE_ATTRIBUTES;
241
- }
242
-
243
- // Build Python command line
244
- static void build_python_command(
245
- char* cmd,
246
- const char* exe_dir,
247
- const char* entry_file,
248
- int debug_mode
249
- ) {
250
- char python_runtime[MAX_PATH_LEN];
251
- char script_path[MAX_PATH_LEN];
252
-
253
- // Build Python interpreter path
254
- snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
255
-
256
- // Build startup script path
257
- snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
258
-
259
- // Build command line (add -u parameter for real-time output capture)
260
- if (debug_mode) {
261
- snprintf(cmd, MAX_PATH_LEN, "\"%s\" \"%s\"", python_runtime, script_path);
262
- } else {
263
- snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
264
- }
265
- }
266
-
267
- // Windows Console entry point
268
- int main(int argc, char* argv[]) {
269
- char exe_dir[MAX_PATH_LEN];
270
- char cmd[MAX_PATH_LEN * 2];
271
- STARTUPINFOA si = {0};
272
- PROCESS_INFORMATION pi = {0};
273
-
274
- // Set encoding to UTF-8
275
- setup_encoding();
276
-
277
- // Get executable directory
278
- GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
279
- char* last_slash = strrchr(exe_dir, '\\');
280
- if (last_slash) {
281
- *last_slash = '\0';
282
- }
283
-
284
- // Check Python runtime
285
- char python_runtime_check[MAX_PATH_LEN];
286
- snprintf(python_runtime_check, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
287
- if (!check_python_runtime(python_runtime_check)) {
288
- fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
289
- fprintf(stderr, "Please ensure the application is installed correctly.\n");
290
- return 1;
291
- }
292
-
293
- // Build and execute Python command
294
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE});
295
-
296
- // Debug output
297
- if (${DEBUG_MODE}) {
298
- fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
299
- fprintf(stderr, "DEBUG: exe_dir: %s\n", exe_dir);
300
- }
301
-
302
- // Set startup info to inherit console
303
- si.cb = sizeof(si);
304
-
305
- // Ensure standard handles are inherited so output displays on console
306
- si.dwFlags = STARTF_USESTDHANDLES;
307
- si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
308
- si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
309
- si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
310
-
311
- // Create Python process
312
- if (!CreateProcessA(
313
- NULL, cmd, NULL, NULL, TRUE, // TRUE to inherit standard handles
314
- 0, NULL, exe_dir, &si, &pi
315
- )) {
316
- DWORD error = GetLastError();
317
- fprintf(stderr, "Error: Failed to start Python process.\n");
318
- fprintf(stderr, "Error code: %lu\n", error);
319
- fprintf(stderr, "Command: %s\n", cmd);
320
- return 1;
321
- }
322
-
323
- // Wait for process to end
324
- WaitForSingleObject(pi.hProcess, INFINITE);
325
-
326
- // Get exit code
327
- DWORD exit_code;
328
- GetExitCodeProcess(pi.hProcess, &exit_code);
329
-
330
- // Debug output
331
- if (${DEBUG_MODE}) {
332
- fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
333
- }
334
-
335
- // Cleanup
336
- CloseHandle(pi.hProcess);
337
- CloseHandle(pi.hThread);
338
-
339
- // If process exits abnormally, display prompt
340
- if (exit_code != 0 && !${DEBUG_MODE}) {
341
- fprintf(stderr, "\nApplication exited abnormally, error code: %lu\n", exit_code);
342
- fprintf(stderr, "Please check the error information above for details.\n");
343
- }
344
-
345
- return exit_code;
346
- }
347
- """
348
-
349
- _UNIX_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
350
- #include <stdio.h>
351
- #include <stdlib.h>
352
- #include <string.h>
353
- #include <unistd.h>
354
- #include <sys/stat.h>
355
- #include <sys/wait.h>
356
-
357
- #define MAX_PATH_LEN 4096
358
- #define MAX_ERROR_LEN 8192
359
-
360
- // Build Python command line
361
- static void build_python_command(
362
- char* cmd,
363
- const char* exe_dir,
364
- const char* entry_file
365
- ) {
366
- char script_path[MAX_PATH_LEN];
367
-
368
- // Build startup script path
369
- snprintf(script_path, MAX_PATH_LEN * 2, "%s/%s", exe_dir, entry_file);
370
-
371
- // Build log file path (record error information)
372
- char log_file[MAX_PATH_LEN];
373
- snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
374
-
375
- // Build command line - use system python3 instead of bundled runtime
376
- // GUI mode uses nohup and background execution, error output to log
377
- snprintf(cmd, MAX_PATH_LEN * 2,
378
- "cd \"%s\" && nohup python3 -u \"%s\" >\"%s\" 2>&1 &",
379
- exe_dir, script_path, log_file
380
- );
381
- }
382
-
383
- // Unix GUI entry point
384
- int main(int argc, char* argv[]) {
385
- char exe_dir[MAX_PATH_LEN];
386
- char cmd[MAX_PATH_LEN * 2];
387
- char log_file[MAX_PATH_LEN];
388
- pid_t pid;
389
- int status;
390
-
391
- // Get executable directory
392
- if (realpath("/proc/self/exe", exe_dir) == NULL) {
393
- // If /proc/self/exe is unavailable (like on macOS), use argv[0]
394
- if (realpath(argv[0], exe_dir) == NULL) {
395
- fprintf(stderr, "Error: Cannot determine executable directory\n");
396
- return 1;
397
- }
398
- }
399
-
400
- // Remove executable name, keep only directory
401
- char* last_slash = strrchr(exe_dir, '/');
402
- if (last_slash) {
403
- *last_slash = '\0';
404
- }
405
-
406
- // Build and execute Python command
407
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
408
- snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
409
-
410
- // Execute Python process in background
411
- pid = fork();
412
- if (pid == 0) {
413
- // Child process: execute Python command and exit immediately
414
- int ret = system(cmd);
415
- _exit(WEXITSTATUS(ret));
416
- } else if (pid < 0) {
417
- fprintf(stderr, "Error: Failed to fork process\n");
418
- return 1;
419
- }
420
-
421
- // Wait for a short time to check if process failed to start
422
- sleep(1);
423
- if (waitpid(pid, &status, WNOHANG) == pid) {
424
- // Process has already exited, read error log
425
- FILE* fp = fopen(log_file, "r");
426
- if (fp) {
427
- char error_msg[MAX_ERROR_LEN];
428
- if (fgets(error_msg, sizeof(error_msg), fp)) {
429
- fprintf(stderr, "Application failed to start:\n%s\n", error_msg);
430
- }
431
- fclose(fp);
432
- } else {
433
- fprintf(stderr, "Application failed to start (exit code: %d)\n", WEXITSTATUS(status));
434
- }
435
- return WEXITSTATUS(status);
436
- }
437
-
438
- // Parent process exits immediately
439
- return 0;
440
- }
441
- """
442
-
443
- _UNIX_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
444
- #include <stdio.h>
445
- #include <stdlib.h>
446
- #include <string.h>
447
- #include <unistd.h>
448
- #include <sys/stat.h>
449
- #include <sys/wait.h>
450
-
451
- #define MAX_PATH_LEN 4096
452
-
453
- // Build Python command line
454
- static void build_python_command(
455
- char* cmd,
456
- const char* exe_dir,
457
- const char* entry_file
458
- ) {
459
- char script_path[MAX_PATH_LEN];
460
-
461
- // Build startup script path
462
- snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
463
-
464
- // Build command line - use system python3 instead of bundled runtime
465
- // add -u parameter for real-time output capture
466
- snprintf(cmd, MAX_PATH_LEN * 2,
467
- "cd \"%s\" && python3 -u \"%s\"",
468
- exe_dir, script_path
469
- );
470
- }
471
-
472
- // Unix Console entry point
473
- int main(int argc, char* argv[]) {
474
- char exe_dir[MAX_PATH_LEN];
475
- char cmd[MAX_PATH_LEN * 2];
476
- pid_t pid;
477
- int status;
478
-
479
- // Get executable directory
480
- if (realpath("/proc/self/exe", exe_dir) == NULL) {
481
- fprintf(stderr, "Error: Cannot determine executable directory\n");
482
- return 1;
483
- }
484
-
485
- // Remove executable name, keep only directory
486
- char* last_slash = strrchr(exe_dir, '/');
487
- if (last_slash) {
488
- *last_slash = '\0';
489
- }
490
-
491
- // Build and execute Python command
492
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
493
-
494
- // Fork and execute Python process
495
- pid = fork();
496
- if (pid == 0) {
497
- // Child process: execute Python command
498
- execl("/bin/sh", "sh", "-c", cmd, NULL);
499
- fprintf(stderr, "Error: Failed to execute Python process\n");
500
- _exit(127); // If execl fails
501
- } else if (pid < 0) {
502
- fprintf(stderr, "Error: Failed to fork process\n");
503
- return 1;
504
- }
505
-
506
- // Parent process: wait for child process to end
507
- waitpid(pid, &status, 0);
508
-
509
- int exit_code = WEXITSTATUS(status);
510
- if (exit_code != 0) {
511
- fprintf(stderr, "\nApplication exited abnormally, error code: %d\n", exit_code);
512
- fprintf(stderr, "Please check the error information above for details.\n");
513
- }
514
-
515
- return exit_code;
516
- }
517
- """
518
-
519
- _MACOS_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
520
- #include <stdio.h>
521
- #include <stdlib.h>
522
- #include <string.h>
523
- #include <unistd.h>
524
- #include <sys/stat.h>
525
- #include <sys/wait.h>
526
- #include <CoreFoundation/CoreFoundation.h>
527
- #include <ApplicationServices/ApplicationServices.h>
528
-
529
- #define MAX_PATH_LEN 4096
530
- #define MAX_ERROR_LEN 8192
531
-
532
- // Check if Python interpreter exists
533
- static int check_python_runtime(const char* runtime_path) {
534
- struct stat st;
535
- return stat(runtime_path, &st) == 0 && (st.st_mode & S_IXUSR);
536
- }
537
-
538
- // Get executable directory (macOS specific)
539
- static void get_exe_dir_macos(char* exe_dir, size_t size) {
540
- CFBundleRef bundle = CFBundleGetMainBundle();
541
- if (bundle) {
542
- CFURLRef url = CFBundleCopyBundleURL(bundle);
543
- if (url) {
544
- CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
545
- if (path) {
546
- CFStringGetCString(path, exe_dir, size, kCFStringEncodingUTF8);
547
- CFRelease(path);
548
- }
549
- CFRelease(url);
550
- }
551
- } else {
552
- // If not in bundle, use argv[0]
553
- realpath("./", exe_dir);
554
- }
555
-
556
- // Remove Contents/MacOS suffix of bundle
557
- char* macos_path = strstr(exe_dir, ".app/Contents/MacOS");
558
- if (macos_path) {
559
- *macos_path = '\0';
560
- }
561
- }
562
-
563
- // Display macOS error dialog
564
- static void show_error_dialog(const char* message) {
565
- CFStringRef cf_message = CFStringCreateWithCString(
566
- NULL, message, kCFStringEncodingUTF8
567
- );
568
- CFOptionFlags response;
569
- CFUserNotificationDisplayAlert(
570
- 0,
571
- kCFUserNotificationStopAlertLevel,
572
- NULL, NULL, NULL,
573
- CFSTR("Application Error"),
574
- cf_message,
575
- CFSTR("OK"), NULL, NULL, &response
576
- );
577
- CFRelease(cf_message);
578
- }
579
-
580
- // Build Python command line
581
- static void build_python_command(
582
- char* cmd,
583
- const char* exe_dir,
584
- const char* entry_file
585
- ) {
586
- char python_runtime[MAX_PATH_LEN];
587
- char script_path[MAX_PATH_LEN];
588
- char log_file[MAX_PATH_LEN];
589
-
590
- // macOS Python path
591
- snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
592
- snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
593
-
594
- // Build log file path
595
- snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
596
-
597
- // Build command line (GUI mode uses nohup and background execution, error output to log)
598
- snprintf(cmd, MAX_PATH_LEN * 2,
599
- "cd \"%s\" && nohup \"%s\" -u \"%s\" >\"%s\" 2>&1 &",
600
- exe_dir, python_runtime, script_path, log_file
601
- );
602
- }
603
-
604
- // macOS GUI entry point
605
- int main(int argc, char* argv[]) {
606
- char exe_dir[MAX_PATH_LEN];
607
- char cmd[MAX_PATH_LEN * 2];
608
- char log_file[MAX_PATH_LEN];
609
- pid_t pid;
610
- int status;
611
-
612
- // Get executable directory
613
- get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
614
-
615
- // Check Python runtime
616
- char python_runtime[MAX_PATH_LEN];
617
- snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
618
- if (!check_python_runtime(python_runtime)) {
619
- // macOS GUI app needs to use dialog to display error
620
- char error_msg[MAX_PATH_LEN];
621
- snprintf(error_msg, MAX_PATH_LEN,
622
- "Python runtime not found.\n\n"
623
- "Please ensure the application is installed correctly and the runtime directory is at:\n%s/runtime/bin/",
624
- exe_dir);
625
- show_error_dialog(error_msg);
626
- return 1;
627
- }
628
-
629
- // Build and execute Python command
630
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
631
- snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
632
-
633
- // Execute Python process in background
634
- pid = fork();
635
- if (pid == 0) {
636
- // Child process
637
- int ret = system(cmd);
638
- _exit(WEXITSTATUS(ret));
639
- } else if (pid < 0) {
640
- char error_msg[MAX_PATH_LEN];
641
- snprintf(error_msg, MAX_PATH_LEN, "Failed to create process.\nError: %s", strerror(errno));
642
- show_error_dialog(error_msg);
643
- return 1;
644
- }
645
-
646
- // Wait for a short time to check if process failed to start
647
- sleep(1);
648
- if (waitpid(pid, &status, WNOHANG) == pid) {
649
- // Process has already exited, read error log
650
- FILE* fp = fopen(log_file, "r");
651
- if (fp) {
652
- char error_msg[MAX_ERROR_LEN];
653
- if (fgets(error_msg, sizeof(error_msg), fp)) {
654
- // Remove newline
655
- char* newline = strchr(error_msg, '\n');
656
- if (newline) *newline = '\0';
657
- char full_error[MAX_PATH_LEN + MAX_ERROR_LEN];
658
- snprintf(full_error, sizeof(full_error),
659
- "Application failed to start.\n\nError:\n%s", error_msg);
660
- show_error_dialog(full_error);
661
- }
662
- fclose(fp);
663
- } else {
664
- char error_msg[MAX_PATH_LEN];
665
- snprintf(error_msg, MAX_PATH_LEN,
666
- "Application failed to start.\n\nExit code: %d",
667
- WEXITSTATUS(status));
668
- show_error_dialog(error_msg);
669
- }
670
- return WEXITSTATUS(status);
671
- }
672
-
673
- return 0;
674
- }
675
- """
676
-
677
- _MACOS_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
678
- #include <stdio.h>
679
- #include <stdlib.h>
680
- #include <string.h>
681
- #include <unistd.h>
682
- #include <sys/stat.h>
683
- #include <sys/wait.h>
684
-
685
- #define MAX_PATH_LEN 4096
686
-
687
- // Check if Python interpreter exists
688
- static int check_python_runtime(const char* runtime_path) {
689
- struct stat st;
690
- return stat(runtime_path, &st) == 0 && (st.st_mode & S_IXUSR);
691
- }
692
-
693
- // Get executable directory (macOS specific)
694
- static void get_exe_dir_macos(char* exe_dir, size_t size) {
695
- CFBundleRef bundle = CFBundleGetMainBundle();
696
- if (bundle) {
697
- CFURLRef url = CFBundleCopyBundleURL(bundle);
698
- if (url) {
699
- CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
700
- if (path) {
701
- CFStringGetCString(path, exe_dir, size, kCFStringEncodingUTF8);
702
- CFRelease(path);
703
- }
704
- CFRelease(url);
705
- }
706
- } else {
707
- realpath("./", exe_dir);
708
- }
709
- }
710
-
711
- // Build Python command line
712
- static void build_python_command(
713
- char* cmd,
714
- const char* exe_dir,
715
- const char* entry_file
716
- ) {
717
- char python_runtime[MAX_PATH_LEN];
718
- char script_path[MAX_PATH_LEN];
719
-
720
- snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
721
- snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
722
-
723
- // Build command line (add -u parameter for real-time output capture)
724
- snprintf(cmd, MAX_PATH_LEN,
725
- "cd \"%s\" && \"%s\" -u \"%s\"",
726
- exe_dir, python_runtime, script_path
727
- );
728
- }
729
-
730
- // macOS Console entry point
731
- int main(int argc, char* argv[]) {
732
- char exe_dir[MAX_PATH_LEN];
733
- char cmd[MAX_PATH_LEN * 2];
734
- pid_t pid;
735
- int status;
736
-
737
- // Get executable directory
738
- get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
739
-
740
- // Check Python runtime
741
- char python_runtime[MAX_PATH_LEN];
742
- snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
743
- if (!check_python_runtime(python_runtime)) {
744
- fprintf(stderr, "Error: Python runtime not found at %s/runtime/bin/\n", exe_dir);
745
- fprintf(stderr, "Please ensure the application is installed correctly.\n");
746
- return 1;
747
- }
748
-
749
- // Build and execute Python command
750
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
751
-
752
- // Fork and execute Python process
753
- pid = fork();
754
- if (pid == 0) {
755
- execl("/bin/sh", "sh", "-c", cmd, NULL);
756
- fprintf(stderr, "Error: Failed to execute Python process\n");
757
- _exit(127);
758
- } else if (pid < 0) {
759
- fprintf(stderr, "Error: Failed to create process: %s\n", strerror(errno));
760
- return 1;
761
- }
762
-
763
- waitpid(pid, &status, 0);
764
-
765
- int exit_code = WEXITSTATUS(status);
766
- if (exit_code != 0) {
767
- fprintf(stderr, "\nApplication exited abnormally, error code: %d\n", exit_code);
768
- fprintf(stderr, "Please check the error information above for details.\n");
769
- }
770
-
771
- return exit_code;
772
- }
773
- """
774
-
775
- _COMPILER_OPTIONS: dict[str, list[str]] = {
776
- "gcc": ["-std=c99", "-Wall", "-Wextra", "-pedantic", "-O2", "-D_GNU_SOURCE"],
777
- "clang": ["-std=c99", "-Wall", "-Wextra", "-pedantic", "-O2", "-D_GNU_SOURCE"],
778
- "cl": ["/std:c99", "/Wall", "/Wextra", "/pedantic", "/O2", "/D_GNU_SOURCE"],
779
- }
780
-
781
-
782
- def find_compiler() -> str | None:
783
- """Find the path to the specified compiler."""
784
- compilers = _COMPILER_OPTIONS.keys()
785
-
786
- for compiler in compilers:
787
- if shutil.which(compiler) is not None:
788
- return compiler
789
-
790
- logger.error("No compiler found")
791
- return None
792
-
793
-
794
- def get_compiler_args(
795
- compiler: str,
796
- ) -> list[str]:
797
- """Get the arguments for the specified compiler."""
798
- compiler_name = Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
799
-
800
- if compiler_name in ("gcc", "clang"):
801
- return ["-std=c99", "-Wall", "-pedantic", "-Werror"]
802
- elif compiler_name == "cl":
803
- return ["/std:c99", "/Wall", "/WX", "/Werror"]
804
- else:
805
- return []
806
-
807
-
808
- def select_template(
809
- loader_type: str,
810
- is_debug: bool,
811
- ) -> str:
812
- """Select the appropriate C code template based on platform and type.
813
-
814
- In debug mode, always use console template to ensure output is visible.
815
- """
816
- # In debug mode, always use console template for visibility
817
- if is_debug:
818
- if is_windows:
819
- return _WINDOWS_CONSOLE_TEMPLATE
820
- elif is_macos:
821
- return _MACOS_CONSOLE_TEMPLATE
822
- else:
823
- return _UNIX_CONSOLE_TEMPLATE
824
-
825
- # In non-debug mode, use the requested template type
826
- if is_windows:
827
- if loader_type == "gui":
828
- return _WINDOWS_GUI_TEMPLATE
829
- else:
830
- return _WINDOWS_CONSOLE_TEMPLATE
831
- elif is_macos:
832
- if loader_type == "gui":
833
- return _MACOS_GUI_TEMPLATE
834
- else:
835
- return _MACOS_CONSOLE_TEMPLATE
836
- else: # Linux and other Unix-like systems
837
- if loader_type == "gui":
838
- return _UNIX_GUI_TEMPLATE
839
- else:
840
- return _UNIX_CONSOLE_TEMPLATE
841
-
842
-
843
- def generate_c_source(
844
- template: str,
845
- entry_file: str,
846
- is_debug: bool,
847
- ) -> str:
848
- """Generate C source code by replacing placeholders in template."""
849
- # Replace placeholders
850
- c_code = template.replace("${ENTRY_FILE}", entry_file)
851
- c_code = c_code.replace("${DEBUG_MODE}", "1" if is_debug else "0")
852
- return c_code
853
-
854
-
855
- def compile_c_source(
856
- c_source_path: str | Path,
857
- output_filepath: Path,
858
- compiler: str | None = None,
859
- ) -> bool:
860
- """Compile C source code to executable file."""
861
- # Find compiler if not specified
862
- if compiler is None:
863
- compiler = find_compiler()
864
- if compiler is None:
865
- logger.error("Error: No suitable C compiler found (gcc/clang/cl required)")
866
- return False
867
-
868
- logger.debug(f"Using compiler: {compiler}")
869
-
870
- # Get compiler arguments
871
- compiler_args = get_compiler_args(compiler)
872
-
873
- # Prepare paths using pathlib
874
- c_source_path = Path(c_source_path)
875
-
876
- output_filepath.parent.mkdir(parents=True, exist_ok=True)
877
- output_filepath = output_filepath.with_suffix(ext)
878
-
879
- # Build compile command
880
- compiler_name = Path(compiler).name if "\\" in compiler or "/" in compiler else compiler
881
- if compiler_name.lower() == "cl" or compiler_name.lower() == "cl.exe":
882
- # MSVC compiler
883
- cmd = [
884
- compiler,
885
- *compiler_args,
886
- str(c_source_path),
887
- f"/Fe:{output_filepath}",
888
- "/link",
889
- "/SUBSYSTEM:WINDOWS" if "gui" in str(c_source_path).lower() else "/SUBSYSTEM:CONSOLE",
890
- ]
891
- else:
892
- # GCC/Clang compiler
893
- subsystem_flag = []
894
- # For Windows GUI, add -mwindows flag with gcc/clang
895
- if is_windows and "gui" in str(c_source_path).lower():
896
- subsystem_flag = ["-mwindows"]
897
- cmd = [compiler, *compiler_args, *subsystem_flag, "-o", str(output_filepath), str(c_source_path)]
898
-
899
- logger.debug(f"Compiling with command: {' '.join(cmd)}")
900
- try:
901
- result = subprocess.run(cmd, capture_output=True, text=True)
902
- if result.returncode != 0:
903
- logger.debug(f"Compilation failed with return code {result.returncode}")
904
- logger.debug(f"STDOUT: {result.stdout}")
905
- logger.debug(f"STDERR: {result.stderr}")
906
- return False
907
- logger.info(f"Successfully compiled to: {output_filepath}")
908
- return True
909
- except FileNotFoundError:
910
- logger.error(f"Error: Compiler '{compiler}' not found")
911
- return False
912
- except Exception as e:
913
- logger.error(f"Error during compilation: {e}")
914
- return False
915
-
916
-
917
- def main():
918
- parser = argparse.ArgumentParser(description="Generate a Python loader executable for current platform")
919
- parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
920
- parser.add_argument(
921
- "-t", "--type", choices=["gui", "console"], default="console", help="Loader type (default: console)"
922
- )
923
- parser.add_argument("-o", "--output", default="", help="Output executable path (default: pyloader or pyloader.exe)")
924
- parser.add_argument(
925
- "-e", "--entry-file", default="main.py", help="Entry Python file path to execute (default: main.py)"
926
- )
927
- parser.add_argument(
928
- "--compiler", help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe"
929
- )
930
- parser.add_argument("--run", "-r", action="store_true", help="Run the generated executable after compilation")
931
-
932
- args = parser.parse_args()
933
-
934
- if args.debug:
935
- logger.setLevel(logging.DEBUG)
936
-
937
- # Determine default output file name based on platform and type
938
- output_filepath = cwd / f"pyloader{ext}" if not args.output else Path(args.output)
939
-
940
- # Select appropriate template (in debug mode, always use console)
941
- template = select_template(args.type, args.debug)
942
-
943
- # Generate C source code
944
- c_code = generate_c_source(template, args.entry_file, args.debug)
945
-
946
- # Write C source code to file
947
- # File name should reflect actual template type, not just args.type
948
- # (debug mode forces console template)
949
- if args.debug:
950
- c_source_file = "pyloader_debug_console.c"
951
- elif args.type == "console":
952
- c_source_file = "pyloader_console.c"
953
- else: # gui type in non-debug mode
954
- c_source_file = "pyloader_gui.c"
955
-
956
- c_source_path = Path(c_source_file)
957
- c_source_path.write_text(c_code, encoding="utf-8")
958
- logger.info(f"Generated C source code: {c_source_path}")
959
-
960
- t0 = time.perf_counter()
961
-
962
- # Compile to executable
963
- if compile_c_source(c_source_file, output_filepath, args.compiler):
964
- logger.info("Loader executable generated successfully!")
965
- logger.debug(f"Platform: {platform.system()}")
966
- logger.debug(f"Type: {args.type}")
967
- logger.debug(f"Debug mode: {args.debug}")
968
- logger.debug(f"Python entry file: {args.entry_file}")
969
- logger.debug(f"Output: {output_filepath}")
970
-
971
- # Run the executable if --run flag is set
972
- if args.run:
973
- logger.debug("\nRunning the executable...")
974
- import subprocess
975
-
976
- try:
977
- subprocess.run([str(Path(args.output))], check=True)
978
- except subprocess.CalledProcessError as e:
979
- logger.error(f"\nExecutable exited with code: {e.returncode}")
980
- return e.returncode
981
- except FileNotFoundError:
982
- logger.error(f"\nError: Executable not found at {args.output}")
983
- return 1
984
-
985
- logger.info(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
986
- return 0
987
- else:
988
- logger.error("\nFailed to compile loader executable")
989
- return 1
990
-
991
-
992
- if __name__ == "__main__":
993
- import sys
994
-
995
- sys.exit(main())
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ import time
9
+ from pathlib import Path
10
+
11
+ is_windows = platform.system() == "Windows"
12
+ is_linux = platform.system() == "Linux"
13
+ is_macos = platform.system() == "Darwin"
14
+ ext = ".exe" if is_windows else ""
15
+
16
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
17
+ logger = logging.getLogger(__name__)
18
+ cwd = Path.cwd()
19
+
20
+ _WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
21
+ #include <stdio.h>
22
+ #include <stdlib.h>
23
+ #include <string.h>
24
+ #include <locale.h>
25
+
26
+ #define MAX_PATH_LEN 4096
27
+ #define MAX_ERROR_LEN 8192
28
+
29
+ // Check if Python runtime exists
30
+ static int check_python_runtime(const char* runtime_path) {
31
+ return GetFileAttributesA(runtime_path) != INVALID_FILE_ATTRIBUTES;
32
+ }
33
+
34
+ // Build Python command line
35
+ static void build_python_command(
36
+ char* cmd,
37
+ const char* exe_dir,
38
+ const char* entry_file,
39
+ int is_debug
40
+ ) {
41
+ char python_runtime[MAX_PATH_LEN];
42
+ char script_path[MAX_PATH_LEN];
43
+
44
+ // Build Python interpreter path
45
+ // GUI non-debug mode uses pythonw.exe (no console), other cases use python.exe
46
+ if (is_debug) {
47
+ // Debug mode: use python.exe to show console output
48
+ snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
49
+ } else {
50
+ // Production GUI mode: use pythonw.exe without creating console window
51
+ snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\pythonw.exe", exe_dir);
52
+ }
53
+
54
+ // Build startup script path
55
+ snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
56
+
57
+ // Build command line (add -u parameter for real-time output capture)
58
+ if (is_debug) {
59
+ // Debug mode: do not redirect output, display output on console
60
+ snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);
61
+ } else {
62
+ // Production mode: redirect all output to pipe
63
+ snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
64
+ }
65
+ }
66
+
67
+ // Read process output
68
+ static void read_process_output(HANDLE hPipe, char* output, int max_len) {
69
+ DWORD bytes_read;
70
+ output[0] = '\0';
71
+
72
+ while (ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
73
+ output[bytes_read] = '\0';
74
+ }
75
+ }
76
+
77
+ // Show message box (support UTF-8 encoding)
78
+ static void show_message_box(const char* title, const char* message) {
79
+ // Convert UTF-8 to UTF-16 using MultiByteToWideChar
80
+ int title_len = MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0);
81
+ int msg_len = MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0);
82
+
83
+ if (title_len > 0 && msg_len > 0) {
84
+ wchar_t* wtitle = (wchar_t*)malloc(title_len * sizeof(wchar_t));
85
+ wchar_t* wmsg = (wchar_t*)malloc(msg_len * sizeof(wchar_t));
86
+
87
+ if (wtitle && wmsg) {
88
+ MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, title_len);
89
+ MultiByteToWideChar(CP_UTF8, 0, message, -1, wmsg, msg_len);
90
+ MessageBoxW(NULL, wmsg, wtitle, MB_ICONERROR | MB_OK);
91
+ }
92
+
93
+ if (wtitle) free(wtitle);
94
+ if (wmsg) free(wmsg);
95
+ } else {
96
+ // Conversion failed, use ANSI version
97
+ MessageBoxA(NULL, message, title, MB_ICONERROR | MB_OK);
98
+ }
99
+ }
100
+
101
+ // Windows GUI entry point
102
+ int APIENTRY WinMain(
103
+ HINSTANCE hInstance,
104
+ HINSTANCE hPrevInstance,
105
+ LPSTR lpCmdLine,
106
+ int nCmdShow
107
+ ) {
108
+ char exe_dir[MAX_PATH_LEN];
109
+ char cmd[MAX_PATH_LEN * 2];
110
+ char error_output[MAX_ERROR_LEN] = "";
111
+ STARTUPINFOA si = {0};
112
+ PROCESS_INFORMATION pi = {0};
113
+ SECURITY_ATTRIBUTES sa = {0};
114
+ HANDLE hReadPipe = NULL, hWritePipe = NULL;
115
+ BOOL success;
116
+
117
+ // Get executable directory
118
+ GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
119
+ char* last_slash = strrchr(exe_dir, '\\');
120
+ if (last_slash) {
121
+ *last_slash = '\0';
122
+ }
123
+
124
+ // Check Python runtime
125
+ if (!check_python_runtime(exe_dir)) {
126
+ show_message_box(
127
+ "Application Error",
128
+ "Python runtime not found.\n\n"
129
+ "Please ensure the application is installed correctly and the runtime directory exists."
130
+ );
131
+ return 1;
132
+ }
133
+
134
+ // Build and execute Python command
135
+ build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE});
136
+
137
+ // Create pipe for capturing output
138
+ sa.nLength = sizeof(SECURITY_ATTRIBUTES);
139
+ sa.bInheritHandle = TRUE;
140
+ sa.lpSecurityDescriptor = NULL;
141
+
142
+ if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
143
+ show_message_box("Error", "Failed to create pipe for error output.");
144
+ return 1;
145
+ }
146
+
147
+ // Set startup info
148
+ si.cb = sizeof(si);
149
+
150
+ // Set different startup methods based on debug mode
151
+ if (${DEBUG_MODE}) {
152
+ // Debug mode: do not use pipe, inherit console to display output
153
+ // Keep si.dwFlags as 0, do not set STARTF_USESTDHANDLES
154
+ } else {
155
+ // Production GUI mode: use pipe to capture error output
156
+ si.dwFlags = STARTF_USESTDHANDLES;
157
+ si.hStdError = hWritePipe;
158
+ si.hStdOutput = hWritePipe;
159
+ }
160
+
161
+ // Create Python process
162
+ // Debug mode: do not use CREATE_NO_WINDOW, let python.exe create console
163
+ // Production GUI mode: use CREATE_NO_WINDOW to ensure no console is created
164
+ DWORD createFlags = ${DEBUG_MODE} ? 0 : CREATE_NO_WINDOW;
165
+ success = CreateProcessA(
166
+ NULL, cmd, NULL, NULL, FALSE,
167
+ createFlags,
168
+ NULL, exe_dir, &si, &pi
169
+ );
170
+
171
+ if (!success) {
172
+ DWORD error = GetLastError();
173
+ char error_msg[MAX_ERROR_LEN];
174
+ snprintf(error_msg, MAX_ERROR_LEN,
175
+ "Failed to start Python process.\n\n"
176
+ "Error code: %lu\n"
177
+ "Command: %s",
178
+ error, cmd);
179
+ show_message_box("Application Error", error_msg);
180
+ if (!${DEBUG_MODE}) {
181
+ CloseHandle(hWritePipe);
182
+ CloseHandle(hReadPipe);
183
+ }
184
+ return 1;
185
+ }
186
+
187
+ // Read error output (only in non-debug mode)
188
+ if (!${DEBUG_MODE}) {
189
+ CloseHandle(hWritePipe);
190
+ read_process_output(hReadPipe, error_output, MAX_ERROR_LEN);
191
+ CloseHandle(hReadPipe);
192
+ }
193
+
194
+ // Wait for process to end
195
+ WaitForSingleObject(pi.hProcess, INFINITE);
196
+
197
+ // Get exit code
198
+ DWORD exit_code;
199
+ GetExitCodeProcess(pi.hProcess, &exit_code);
200
+
201
+ // Debug output
202
+ fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
203
+
204
+ // Cleanup
205
+ CloseHandle(pi.hProcess);
206
+ CloseHandle(pi.hThread);
207
+
208
+ // If process exits abnormally and there is error output, display error message
209
+ if (exit_code != 0 && strlen(error_output) > 0) {
210
+ // Truncate the last error message (up to 2000 characters)
211
+ size_t error_len = strlen(error_output);
212
+ if (error_len > 2000) {
213
+ error_output[2000] = '\0';
214
+ }
215
+ show_message_box("Application Error", error_output);
216
+ return exit_code;
217
+ }
218
+
219
+ return exit_code;
220
+ }
221
+ """
222
+
223
+ _WINDOWS_CONSOLE_TEMPLATE: str = r"""#include <stdio.h>
224
+ #include <stdlib.h>
225
+ #include <string.h>
226
+ #include <windows.h>
227
+ #include <locale.h>
228
+
229
+ #define MAX_PATH_LEN 4096
230
+
231
+ // Set console encoding to UTF-8
232
+ static void setup_encoding() {
233
+ SetConsoleOutputCP(CP_UTF8);
234
+ SetConsoleCP(CP_UTF8);
235
+ setlocale(LC_ALL, ".UTF8");
236
+ }
237
+
238
+ // Check if Python runtime exists
239
+ static int check_python_runtime(const char* runtime_path) {
240
+ return GetFileAttributesA(runtime_path) != INVALID_FILE_ATTRIBUTES;
241
+ }
242
+
243
+ // Build Python command line
244
+ static void build_python_command(
245
+ char* cmd,
246
+ const char* exe_dir,
247
+ const char* entry_file,
248
+ int debug_mode
249
+ ) {
250
+ char python_runtime[MAX_PATH_LEN];
251
+ char script_path[MAX_PATH_LEN];
252
+
253
+ // Build Python interpreter path
254
+ snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
255
+
256
+ // Build startup script path
257
+ snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
258
+
259
+ // Build command line (add -u parameter for real-time output capture)
260
+ if (debug_mode) {
261
+ snprintf(cmd, MAX_PATH_LEN, "\"%s\" \"%s\"", python_runtime, script_path);
262
+ } else {
263
+ snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
264
+ }
265
+ }
266
+
267
+ // Windows Console entry point
268
+ int main(int argc, char* argv[]) {
269
+ char exe_dir[MAX_PATH_LEN];
270
+ char cmd[MAX_PATH_LEN * 2];
271
+ STARTUPINFOA si = {0};
272
+ PROCESS_INFORMATION pi = {0};
273
+
274
+ // Set encoding to UTF-8
275
+ setup_encoding();
276
+
277
+ // Get executable directory
278
+ GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
279
+ char* last_slash = strrchr(exe_dir, '\\');
280
+ if (last_slash) {
281
+ *last_slash = '\0';
282
+ }
283
+
284
+ // Check Python runtime
285
+ char python_runtime_check[MAX_PATH_LEN];
286
+ snprintf(python_runtime_check, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
287
+ if (!check_python_runtime(python_runtime_check)) {
288
+ fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
289
+ fprintf(stderr, "Please ensure the application is installed correctly.\n");
290
+ return 1;
291
+ }
292
+
293
+ // Build and execute Python command
294
+ build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE});
295
+
296
+ // Debug output
297
+ if (${DEBUG_MODE}) {
298
+ fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
299
+ fprintf(stderr, "DEBUG: exe_dir: %s\n", exe_dir);
300
+ }
301
+
302
+ // Set startup info to inherit console
303
+ si.cb = sizeof(si);
304
+
305
+ // Ensure standard handles are inherited so output displays on console
306
+ si.dwFlags = STARTF_USESTDHANDLES;
307
+ si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
308
+ si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
309
+ si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
310
+
311
+ // Create Python process
312
+ if (!CreateProcessA(
313
+ NULL, cmd, NULL, NULL, TRUE, // TRUE to inherit standard handles
314
+ 0, NULL, exe_dir, &si, &pi
315
+ )) {
316
+ DWORD error = GetLastError();
317
+ fprintf(stderr, "Error: Failed to start Python process.\n");
318
+ fprintf(stderr, "Error code: %lu\n", error);
319
+ fprintf(stderr, "Command: %s\n", cmd);
320
+ return 1;
321
+ }
322
+
323
+ // Wait for process to end
324
+ WaitForSingleObject(pi.hProcess, INFINITE);
325
+
326
+ // Get exit code
327
+ DWORD exit_code;
328
+ GetExitCodeProcess(pi.hProcess, &exit_code);
329
+
330
+ // Debug output
331
+ if (${DEBUG_MODE}) {
332
+ fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
333
+ }
334
+
335
+ // Cleanup
336
+ CloseHandle(pi.hProcess);
337
+ CloseHandle(pi.hThread);
338
+
339
+ // If process exits abnormally, display prompt
340
+ if (exit_code != 0 && !${DEBUG_MODE}) {
341
+ fprintf(stderr, "\nApplication exited abnormally, error code: %lu\n", exit_code);
342
+ fprintf(stderr, "Please check the error information above for details.\n");
343
+ }
344
+
345
+ return exit_code;
346
+ }
347
+ """
348
+
349
+ _UNIX_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
350
+ #include <stdio.h>
351
+ #include <stdlib.h>
352
+ #include <string.h>
353
+ #include <unistd.h>
354
+ #include <sys/stat.h>
355
+ #include <sys/wait.h>
356
+
357
+ #define MAX_PATH_LEN 4096
358
+ #define MAX_ERROR_LEN 8192
359
+
360
+ // Build Python command line
361
+ static void build_python_command(
362
+ char* cmd,
363
+ const char* exe_dir,
364
+ const char* entry_file
365
+ ) {
366
+ char script_path[MAX_PATH_LEN];
367
+
368
+ // Build startup script path
369
+ snprintf(script_path, MAX_PATH_LEN * 2, "%s/%s", exe_dir, entry_file);
370
+
371
+ // Build log file path (record error information)
372
+ char log_file[MAX_PATH_LEN];
373
+ snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
374
+
375
+ // Build command line - use system python3 instead of bundled runtime
376
+ // GUI mode uses nohup and background execution, error output to log
377
+ snprintf(cmd, MAX_PATH_LEN * 2,
378
+ "cd \"%s\" && nohup python3 -u \"%s\" >\"%s\" 2>&1 &",
379
+ exe_dir, script_path, log_file
380
+ );
381
+ }
382
+
383
+ // Unix GUI entry point
384
+ int main(int argc, char* argv[]) {
385
+ char exe_dir[MAX_PATH_LEN];
386
+ char cmd[MAX_PATH_LEN * 2];
387
+ char log_file[MAX_PATH_LEN];
388
+ pid_t pid;
389
+ int status;
390
+
391
+ // Get executable directory
392
+ if (realpath("/proc/self/exe", exe_dir) == NULL) {
393
+ // If /proc/self/exe is unavailable (like on macOS), use argv[0]
394
+ if (realpath(argv[0], exe_dir) == NULL) {
395
+ fprintf(stderr, "Error: Cannot determine executable directory\n");
396
+ return 1;
397
+ }
398
+ }
399
+
400
+ // Remove executable name, keep only directory
401
+ char* last_slash = strrchr(exe_dir, '/');
402
+ if (last_slash) {
403
+ *last_slash = '\0';
404
+ }
405
+
406
+ // Build and execute Python command
407
+ build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
408
+ snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
409
+
410
+ // Execute Python process in background
411
+ pid = fork();
412
+ if (pid == 0) {
413
+ // Child process: execute Python command and exit immediately
414
+ int ret = system(cmd);
415
+ _exit(WEXITSTATUS(ret));
416
+ } else if (pid < 0) {
417
+ fprintf(stderr, "Error: Failed to fork process\n");
418
+ return 1;
419
+ }
420
+
421
+ // Wait for a short time to check if process failed to start
422
+ sleep(1);
423
+ if (waitpid(pid, &status, WNOHANG) == pid) {
424
+ // Process has already exited, read error log
425
+ FILE* fp = fopen(log_file, "r");
426
+ if (fp) {
427
+ char error_msg[MAX_ERROR_LEN];
428
+ if (fgets(error_msg, sizeof(error_msg), fp)) {
429
+ fprintf(stderr, "Application failed to start:\n%s\n", error_msg);
430
+ }
431
+ fclose(fp);
432
+ } else {
433
+ fprintf(stderr, "Application failed to start (exit code: %d)\n", WEXITSTATUS(status));
434
+ }
435
+ return WEXITSTATUS(status);
436
+ }
437
+
438
+ // Parent process exits immediately
439
+ return 0;
440
+ }
441
+ """
442
+
443
+ _UNIX_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
444
+ #include <stdio.h>
445
+ #include <stdlib.h>
446
+ #include <string.h>
447
+ #include <unistd.h>
448
+ #include <sys/stat.h>
449
+ #include <sys/wait.h>
450
+
451
+ #define MAX_PATH_LEN 4096
452
+
453
+ // Build Python command line
454
+ static void build_python_command(
455
+ char* cmd,
456
+ const char* exe_dir,
457
+ const char* entry_file
458
+ ) {
459
+ char script_path[MAX_PATH_LEN];
460
+
461
+ // Build startup script path
462
+ snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
463
+
464
+ // Build command line - use system python3 instead of bundled runtime
465
+ // add -u parameter for real-time output capture
466
+ snprintf(cmd, MAX_PATH_LEN * 2,
467
+ "cd \"%s\" && python3 -u \"%s\"",
468
+ exe_dir, script_path
469
+ );
470
+ }
471
+
472
+ // Unix Console entry point
473
+ int main(int argc, char* argv[]) {
474
+ char exe_dir[MAX_PATH_LEN];
475
+ char cmd[MAX_PATH_LEN * 2];
476
+ pid_t pid;
477
+ int status;
478
+
479
+ // Get executable directory
480
+ if (realpath("/proc/self/exe", exe_dir) == NULL) {
481
+ fprintf(stderr, "Error: Cannot determine executable directory\n");
482
+ return 1;
483
+ }
484
+
485
+ // Remove executable name, keep only directory
486
+ char* last_slash = strrchr(exe_dir, '/');
487
+ if (last_slash) {
488
+ *last_slash = '\0';
489
+ }
490
+
491
+ // Build and execute Python command
492
+ build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
493
+
494
+ // Fork and execute Python process
495
+ pid = fork();
496
+ if (pid == 0) {
497
+ // Child process: execute Python command
498
+ execl("/bin/sh", "sh", "-c", cmd, NULL);
499
+ fprintf(stderr, "Error: Failed to execute Python process\n");
500
+ _exit(127); // If execl fails
501
+ } else if (pid < 0) {
502
+ fprintf(stderr, "Error: Failed to fork process\n");
503
+ return 1;
504
+ }
505
+
506
+ // Parent process: wait for child process to end
507
+ waitpid(pid, &status, 0);
508
+
509
+ int exit_code = WEXITSTATUS(status);
510
+ if (exit_code != 0) {
511
+ fprintf(stderr, "\nApplication exited abnormally, error code: %d\n", exit_code);
512
+ fprintf(stderr, "Please check the error information above for details.\n");
513
+ }
514
+
515
+ return exit_code;
516
+ }
517
+ """
518
+
519
+ _MACOS_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
520
+ #include <stdio.h>
521
+ #include <stdlib.h>
522
+ #include <string.h>
523
+ #include <unistd.h>
524
+ #include <sys/stat.h>
525
+ #include <sys/wait.h>
526
+ #include <CoreFoundation/CoreFoundation.h>
527
+ #include <ApplicationServices/ApplicationServices.h>
528
+
529
+ #define MAX_PATH_LEN 4096
530
+ #define MAX_ERROR_LEN 8192
531
+
532
+ // Check if Python interpreter exists
533
+ static int check_python_runtime(const char* runtime_path) {
534
+ struct stat st;
535
+ return stat(runtime_path, &st) == 0 && (st.st_mode & S_IXUSR);
536
+ }
537
+
538
+ // Get executable directory (macOS specific)
539
+ static void get_exe_dir_macos(char* exe_dir, size_t size) {
540
+ CFBundleRef bundle = CFBundleGetMainBundle();
541
+ if (bundle) {
542
+ CFURLRef url = CFBundleCopyBundleURL(bundle);
543
+ if (url) {
544
+ CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
545
+ if (path) {
546
+ CFStringGetCString(path, exe_dir, size, kCFStringEncodingUTF8);
547
+ CFRelease(path);
548
+ }
549
+ CFRelease(url);
550
+ }
551
+ } else {
552
+ // If not in bundle, use argv[0]
553
+ realpath("./", exe_dir);
554
+ }
555
+
556
+ // Remove Contents/MacOS suffix of bundle
557
+ char* macos_path = strstr(exe_dir, ".app/Contents/MacOS");
558
+ if (macos_path) {
559
+ *macos_path = '\0';
560
+ }
561
+ }
562
+
563
+ // Display macOS error dialog
564
+ static void show_error_dialog(const char* message) {
565
+ CFStringRef cf_message = CFStringCreateWithCString(
566
+ NULL, message, kCFStringEncodingUTF8
567
+ );
568
+ CFOptionFlags response;
569
+ CFUserNotificationDisplayAlert(
570
+ 0,
571
+ kCFUserNotificationStopAlertLevel,
572
+ NULL, NULL, NULL,
573
+ CFSTR("Application Error"),
574
+ cf_message,
575
+ CFSTR("OK"), NULL, NULL, &response
576
+ );
577
+ CFRelease(cf_message);
578
+ }
579
+
580
+ // Build Python command line
581
+ static void build_python_command(
582
+ char* cmd,
583
+ const char* exe_dir,
584
+ const char* entry_file
585
+ ) {
586
+ char python_runtime[MAX_PATH_LEN];
587
+ char script_path[MAX_PATH_LEN];
588
+ char log_file[MAX_PATH_LEN];
589
+
590
+ // macOS Python path
591
+ snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
592
+ snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
593
+
594
+ // Build log file path
595
+ snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
596
+
597
+ // Build command line (GUI mode uses nohup and background execution, error output to log)
598
+ snprintf(cmd, MAX_PATH_LEN * 2,
599
+ "cd \"%s\" && nohup \"%s\" -u \"%s\" >\"%s\" 2>&1 &",
600
+ exe_dir, python_runtime, script_path, log_file
601
+ );
602
+ }
603
+
604
+ // macOS GUI entry point
605
+ int main(int argc, char* argv[]) {
606
+ char exe_dir[MAX_PATH_LEN];
607
+ char cmd[MAX_PATH_LEN * 2];
608
+ char log_file[MAX_PATH_LEN];
609
+ pid_t pid;
610
+ int status;
611
+
612
+ // Get executable directory
613
+ get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
614
+
615
+ // Check Python runtime
616
+ char python_runtime[MAX_PATH_LEN];
617
+ snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
618
+ if (!check_python_runtime(python_runtime)) {
619
+ // macOS GUI app needs to use dialog to display error
620
+ char error_msg[MAX_PATH_LEN];
621
+ snprintf(error_msg, MAX_PATH_LEN,
622
+ "Python runtime not found.\n\n"
623
+ "Please ensure the application is installed correctly and the runtime directory is at:\n%s/runtime/bin/",
624
+ exe_dir);
625
+ show_error_dialog(error_msg);
626
+ return 1;
627
+ }
628
+
629
+ // Build and execute Python command
630
+ build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
631
+ snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
632
+
633
+ // Execute Python process in background
634
+ pid = fork();
635
+ if (pid == 0) {
636
+ // Child process
637
+ int ret = system(cmd);
638
+ _exit(WEXITSTATUS(ret));
639
+ } else if (pid < 0) {
640
+ char error_msg[MAX_PATH_LEN];
641
+ snprintf(error_msg, MAX_PATH_LEN, "Failed to create process.\nError: %s", strerror(errno));
642
+ show_error_dialog(error_msg);
643
+ return 1;
644
+ }
645
+
646
+ // Wait for a short time to check if process failed to start
647
+ sleep(1);
648
+ if (waitpid(pid, &status, WNOHANG) == pid) {
649
+ // Process has already exited, read error log
650
+ FILE* fp = fopen(log_file, "r");
651
+ if (fp) {
652
+ char error_msg[MAX_ERROR_LEN];
653
+ if (fgets(error_msg, sizeof(error_msg), fp)) {
654
+ // Remove newline
655
+ char* newline = strchr(error_msg, '\n');
656
+ if (newline) *newline = '\0';
657
+ char full_error[MAX_PATH_LEN + MAX_ERROR_LEN];
658
+ snprintf(full_error, sizeof(full_error),
659
+ "Application failed to start.\n\nError:\n%s", error_msg);
660
+ show_error_dialog(full_error);
661
+ }
662
+ fclose(fp);
663
+ } else {
664
+ char error_msg[MAX_PATH_LEN];
665
+ snprintf(error_msg, MAX_PATH_LEN,
666
+ "Application failed to start.\n\nExit code: %d",
667
+ WEXITSTATUS(status));
668
+ show_error_dialog(error_msg);
669
+ }
670
+ return WEXITSTATUS(status);
671
+ }
672
+
673
+ return 0;
674
+ }
675
+ """
676
+
677
+ _MACOS_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
678
+ #include <stdio.h>
679
+ #include <stdlib.h>
680
+ #include <string.h>
681
+ #include <unistd.h>
682
+ #include <sys/stat.h>
683
+ #include <sys/wait.h>
684
+
685
+ #define MAX_PATH_LEN 4096
686
+
687
+ // Check if Python interpreter exists
688
+ static int check_python_runtime(const char* runtime_path) {
689
+ struct stat st;
690
+ return stat(runtime_path, &st) == 0 && (st.st_mode & S_IXUSR);
691
+ }
692
+
693
+ // Get executable directory (macOS specific)
694
+ static void get_exe_dir_macos(char* exe_dir, size_t size) {
695
+ CFBundleRef bundle = CFBundleGetMainBundle();
696
+ if (bundle) {
697
+ CFURLRef url = CFBundleCopyBundleURL(bundle);
698
+ if (url) {
699
+ CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
700
+ if (path) {
701
+ CFStringGetCString(path, exe_dir, size, kCFStringEncodingUTF8);
702
+ CFRelease(path);
703
+ }
704
+ CFRelease(url);
705
+ }
706
+ } else {
707
+ realpath("./", exe_dir);
708
+ }
709
+ }
710
+
711
+ // Build Python command line
712
+ static void build_python_command(
713
+ char* cmd,
714
+ const char* exe_dir,
715
+ const char* entry_file
716
+ ) {
717
+ char python_runtime[MAX_PATH_LEN];
718
+ char script_path[MAX_PATH_LEN];
719
+
720
+ snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
721
+ snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
722
+
723
+ // Build command line (add -u parameter for real-time output capture)
724
+ snprintf(cmd, MAX_PATH_LEN,
725
+ "cd \"%s\" && \"%s\" -u \"%s\"",
726
+ exe_dir, python_runtime, script_path
727
+ );
728
+ }
729
+
730
+ // macOS Console entry point
731
+ int main(int argc, char* argv[]) {
732
+ char exe_dir[MAX_PATH_LEN];
733
+ char cmd[MAX_PATH_LEN * 2];
734
+ pid_t pid;
735
+ int status;
736
+
737
+ // Get executable directory
738
+ get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
739
+
740
+ // Check Python runtime
741
+ char python_runtime[MAX_PATH_LEN];
742
+ snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
743
+ if (!check_python_runtime(python_runtime)) {
744
+ fprintf(stderr, "Error: Python runtime not found at %s/runtime/bin/\n", exe_dir);
745
+ fprintf(stderr, "Please ensure the application is installed correctly.\n");
746
+ return 1;
747
+ }
748
+
749
+ // Build and execute Python command
750
+ build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
751
+
752
+ // Fork and execute Python process
753
+ pid = fork();
754
+ if (pid == 0) {
755
+ execl("/bin/sh", "sh", "-c", cmd, NULL);
756
+ fprintf(stderr, "Error: Failed to execute Python process\n");
757
+ _exit(127);
758
+ } else if (pid < 0) {
759
+ fprintf(stderr, "Error: Failed to create process: %s\n", strerror(errno));
760
+ return 1;
761
+ }
762
+
763
+ waitpid(pid, &status, 0);
764
+
765
+ int exit_code = WEXITSTATUS(status);
766
+ if (exit_code != 0) {
767
+ fprintf(stderr, "\nApplication exited abnormally, error code: %d\n", exit_code);
768
+ fprintf(stderr, "Please check the error information above for details.\n");
769
+ }
770
+
771
+ return exit_code;
772
+ }
773
+ """
774
+
775
+ _COMPILER_OPTIONS: dict[str, list[str]] = {
776
+ "gcc": ["-std=c99", "-Wall", "-Wextra", "-pedantic", "-O2", "-D_GNU_SOURCE"],
777
+ "clang": ["-std=c99", "-Wall", "-Wextra", "-pedantic", "-O2", "-D_GNU_SOURCE"],
778
+ "cl": ["/std:c99", "/Wall", "/Wextra", "/pedantic", "/O2", "/D_GNU_SOURCE"],
779
+ }
780
+
781
+
782
+ def find_compiler() -> str | None:
783
+ """Find the path to the specified compiler."""
784
+ compilers = _COMPILER_OPTIONS.keys()
785
+
786
+ for compiler in compilers:
787
+ if shutil.which(compiler) is not None:
788
+ return compiler
789
+
790
+ logger.error("No compiler found")
791
+ return None
792
+
793
+
794
+ def get_compiler_args(
795
+ compiler: str,
796
+ ) -> list[str]:
797
+ """Get the arguments for the specified compiler."""
798
+ compiler_name = Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
799
+
800
+ if compiler_name in ("gcc", "clang"):
801
+ return ["-std=c99", "-Wall", "-pedantic", "-Werror"]
802
+ elif compiler_name == "cl":
803
+ return ["/std:c99", "/Wall", "/WX", "/Werror"]
804
+ else:
805
+ return []
806
+
807
+
808
+ def select_template(
809
+ loader_type: str,
810
+ is_debug: bool,
811
+ ) -> str:
812
+ """Select the appropriate C code template based on platform and type.
813
+
814
+ In debug mode, always use console template to ensure output is visible.
815
+ """
816
+ # In debug mode, always use console template for visibility
817
+ if is_debug:
818
+ if is_windows:
819
+ return _WINDOWS_CONSOLE_TEMPLATE
820
+ elif is_macos:
821
+ return _MACOS_CONSOLE_TEMPLATE
822
+ else:
823
+ return _UNIX_CONSOLE_TEMPLATE
824
+
825
+ # In non-debug mode, use the requested template type
826
+ if is_windows:
827
+ if loader_type == "gui":
828
+ return _WINDOWS_GUI_TEMPLATE
829
+ else:
830
+ return _WINDOWS_CONSOLE_TEMPLATE
831
+ elif is_macos:
832
+ if loader_type == "gui":
833
+ return _MACOS_GUI_TEMPLATE
834
+ else:
835
+ return _MACOS_CONSOLE_TEMPLATE
836
+ else: # Linux and other Unix-like systems
837
+ if loader_type == "gui":
838
+ return _UNIX_GUI_TEMPLATE
839
+ else:
840
+ return _UNIX_CONSOLE_TEMPLATE
841
+
842
+
843
+ def generate_c_source(
844
+ template: str,
845
+ entry_file: str,
846
+ is_debug: bool,
847
+ ) -> str:
848
+ """Generate C source code by replacing placeholders in template."""
849
+ # Replace placeholders
850
+ c_code = template.replace("${ENTRY_FILE}", entry_file)
851
+ c_code = c_code.replace("${DEBUG_MODE}", "1" if is_debug else "0")
852
+ return c_code
853
+
854
+
855
+ def compile_c_source(
856
+ c_source_path: str | Path,
857
+ output_filepath: Path,
858
+ compiler: str | None = None,
859
+ ) -> bool:
860
+ """Compile C source code to executable file."""
861
+ # Find compiler if not specified
862
+ if compiler is None:
863
+ compiler = find_compiler()
864
+ if compiler is None:
865
+ logger.error("Error: No suitable C compiler found (gcc/clang/cl required)")
866
+ return False
867
+
868
+ logger.debug(f"Using compiler: {compiler}")
869
+
870
+ # Get compiler arguments
871
+ compiler_args = get_compiler_args(compiler)
872
+
873
+ # Prepare paths using pathlib
874
+ c_source_path = Path(c_source_path)
875
+
876
+ output_filepath.parent.mkdir(parents=True, exist_ok=True)
877
+ output_filepath = output_filepath.with_suffix(ext)
878
+
879
+ # Build compile command
880
+ compiler_name = Path(compiler).name if "\\" in compiler or "/" in compiler else compiler
881
+ if compiler_name.lower() == "cl" or compiler_name.lower() == "cl.exe":
882
+ # MSVC compiler
883
+ cmd = [
884
+ compiler,
885
+ *compiler_args,
886
+ str(c_source_path),
887
+ f"/Fe:{output_filepath}",
888
+ "/link",
889
+ "/SUBSYSTEM:WINDOWS" if "gui" in str(c_source_path).lower() else "/SUBSYSTEM:CONSOLE",
890
+ ]
891
+ else:
892
+ # GCC/Clang compiler
893
+ subsystem_flag = []
894
+ # For Windows GUI, add -mwindows flag with gcc/clang
895
+ if is_windows and "gui" in str(c_source_path).lower():
896
+ subsystem_flag = ["-mwindows"]
897
+ cmd = [compiler, *compiler_args, *subsystem_flag, "-o", str(output_filepath), str(c_source_path)]
898
+
899
+ logger.debug(f"Compiling with command: {' '.join(cmd)}")
900
+ try:
901
+ result = subprocess.run(cmd, capture_output=True, text=True)
902
+ if result.returncode != 0:
903
+ logger.debug(f"Compilation failed with return code {result.returncode}")
904
+ logger.debug(f"STDOUT: {result.stdout}")
905
+ logger.debug(f"STDERR: {result.stderr}")
906
+ return False
907
+ logger.info(f"Successfully compiled to: {output_filepath}")
908
+ return True
909
+ except FileNotFoundError:
910
+ logger.error(f"Error: Compiler '{compiler}' not found")
911
+ return False
912
+ except Exception as e:
913
+ logger.error(f"Error during compilation: {e}")
914
+ return False
915
+
916
+
917
+ def main():
918
+ parser = argparse.ArgumentParser(description="Generate a Python loader executable for current platform")
919
+ parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
920
+ parser.add_argument(
921
+ "-t", "--type", choices=["gui", "console"], default="console", help="Loader type (default: console)"
922
+ )
923
+ parser.add_argument("-o", "--output", default="", help="Output executable path (default: pyloader or pyloader.exe)")
924
+ parser.add_argument(
925
+ "-e", "--entry-file", default="main.py", help="Entry Python file path to execute (default: main.py)"
926
+ )
927
+ parser.add_argument(
928
+ "--compiler", help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe"
929
+ )
930
+ parser.add_argument("--run", "-r", action="store_true", help="Run the generated executable after compilation")
931
+
932
+ args = parser.parse_args()
933
+
934
+ if args.debug:
935
+ logger.setLevel(logging.DEBUG)
936
+
937
+ # Determine default output file name based on platform and type
938
+ output_filepath = cwd / f"pyloader{ext}" if not args.output else Path(args.output)
939
+
940
+ # Select appropriate template (in debug mode, always use console)
941
+ template = select_template(args.type, args.debug)
942
+
943
+ # Generate C source code
944
+ c_code = generate_c_source(template, args.entry_file, args.debug)
945
+
946
+ # Write C source code to file
947
+ # File name should reflect actual template type, not just args.type
948
+ # (debug mode forces console template)
949
+ if args.debug:
950
+ c_source_file = "pyloader_debug_console.c"
951
+ elif args.type == "console":
952
+ c_source_file = "pyloader_console.c"
953
+ else: # gui type in non-debug mode
954
+ c_source_file = "pyloader_gui.c"
955
+
956
+ c_source_path = Path(c_source_file)
957
+ c_source_path.write_text(c_code, encoding="utf-8")
958
+ logger.info(f"Generated C source code: {c_source_path}")
959
+
960
+ t0 = time.perf_counter()
961
+
962
+ # Compile to executable
963
+ if compile_c_source(c_source_file, output_filepath, args.compiler):
964
+ logger.info("Loader executable generated successfully!")
965
+ logger.debug(f"Platform: {platform.system()}")
966
+ logger.debug(f"Type: {args.type}")
967
+ logger.debug(f"Debug mode: {args.debug}")
968
+ logger.debug(f"Python entry file: {args.entry_file}")
969
+ logger.debug(f"Output: {output_filepath}")
970
+
971
+ # Run the executable if --run flag is set
972
+ if args.run:
973
+ logger.debug("\nRunning the executable...")
974
+ import subprocess
975
+
976
+ try:
977
+ subprocess.run([str(Path(args.output))], check=True)
978
+ except subprocess.CalledProcessError as e:
979
+ logger.error(f"\nExecutable exited with code: {e.returncode}")
980
+ return e.returncode
981
+ except FileNotFoundError:
982
+ logger.error(f"\nError: Executable not found at {args.output}")
983
+ return 1
984
+
985
+ logger.info(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
986
+ return 0
987
+ else:
988
+ logger.error("\nFailed to compile loader executable")
989
+ return 1
990
+
991
+
992
+ if __name__ == "__main__":
993
+ import sys
994
+
995
+ sys.exit(main())