pysfi 0.1.7__py3-none-any.whl → 0.1.11__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.
- {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
- pysfi-0.1.11.dist-info/RECORD +60 -0
- pysfi-0.1.11.dist-info/entry_points.txt +28 -0
- sfi/__init__.py +1 -1
- sfi/alarmclock/alarmclock.py +40 -40
- sfi/bumpversion/__init__.py +1 -1
- sfi/cleanbuild/cleanbuild.py +155 -0
- sfi/condasetup/condasetup.py +116 -0
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan.py +407 -103
- sfi/docscan/docscan_gui.py +1282 -596
- sfi/docscan/lang/eng.py +152 -0
- sfi/docscan/lang/zhcn.py +170 -0
- sfi/filedate/filedate.py +185 -112
- sfi/gittool/__init__.py +2 -0
- sfi/gittool/gittool.py +401 -0
- sfi/llmclient/llmclient.py +592 -0
- sfi/llmquantize/llmquantize.py +480 -0
- sfi/llmserver/llmserver.py +335 -0
- sfi/makepython/makepython.py +31 -30
- sfi/pdfsplit/pdfsplit.py +173 -173
- sfi/pyarchive/pyarchive.py +418 -0
- sfi/pyembedinstall/pyembedinstall.py +629 -0
- sfi/pylibpack/__init__.py +0 -0
- sfi/pylibpack/pylibpack.py +1457 -0
- sfi/pylibpack/rules/numpy.json +22 -0
- sfi/pylibpack/rules/pymupdf.json +10 -0
- sfi/pylibpack/rules/pyqt5.json +19 -0
- sfi/pylibpack/rules/pyside2.json +23 -0
- sfi/pylibpack/rules/scipy.json +23 -0
- sfi/pylibpack/rules/shiboken2.json +24 -0
- sfi/pyloadergen/pyloadergen.py +512 -227
- sfi/pypack/__init__.py +0 -0
- sfi/pypack/pypack.py +1142 -0
- sfi/pyprojectparse/__init__.py +0 -0
- sfi/pyprojectparse/pyprojectparse.py +500 -0
- sfi/pysourcepack/pysourcepack.py +308 -0
- sfi/quizbase/__init__.py +0 -0
- sfi/quizbase/quizbase.py +828 -0
- sfi/quizbase/quizbase_gui.py +987 -0
- sfi/regexvalidate/__init__.py +0 -0
- sfi/regexvalidate/regex_help.html +284 -0
- sfi/regexvalidate/regexvalidate.py +468 -0
- sfi/taskkill/taskkill.py +0 -2
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +444 -0
- pysfi-0.1.7.dist-info/RECORD +0 -31
- pysfi-0.1.7.dist-info/entry_points.txt +0 -15
- sfi/embedinstall/embedinstall.py +0 -418
- sfi/projectparse/projectparse.py +0 -152
- sfi/pypacker/fspacker.py +0 -91
- {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
- /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
- /sfi/{projectparse → llmquantize}/__init__.py +0 -0
- /sfi/{pypacker → pyembedinstall}/__init__.py +0 -0
sfi/pyloadergen/pyloadergen.py
CHANGED
|
@@ -6,8 +6,11 @@ import platform
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
12
|
+
from sfi.pyprojectparse.pyprojectparse import Project
|
|
13
|
+
|
|
11
14
|
is_windows = platform.system() == "Windows"
|
|
12
15
|
is_linux = platform.system() == "Linux"
|
|
13
16
|
is_macos = platform.system() == "Darwin"
|
|
@@ -17,6 +20,44 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
|
17
20
|
logger = logging.getLogger(__name__)
|
|
18
21
|
cwd = Path.cwd()
|
|
19
22
|
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CompilerConfig:
|
|
26
|
+
"""Compiler configuration dataclass.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: Compiler name
|
|
30
|
+
args: List of compiler arguments
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
args: tuple[str, ...]
|
|
35
|
+
|
|
36
|
+
def to_list(self) -> list[str]:
|
|
37
|
+
"""Convert arguments to list.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of compiler arguments
|
|
41
|
+
"""
|
|
42
|
+
return list(self.args)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class EntryFile:
|
|
47
|
+
"""Entry file dataclass."""
|
|
48
|
+
|
|
49
|
+
project_name: str
|
|
50
|
+
source_file: Path
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def entry_name(self) -> str:
|
|
54
|
+
"""Get entry file name."""
|
|
55
|
+
return self.source_file.stem
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
return f"<EntryFile project_name={self.project_name} source_file={self.source_file}>"
|
|
59
|
+
|
|
60
|
+
|
|
20
61
|
_WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
|
|
21
62
|
#include <stdio.h>
|
|
22
63
|
#include <stdlib.h>
|
|
@@ -36,32 +77,24 @@ static void build_python_command(
|
|
|
36
77
|
char* cmd,
|
|
37
78
|
const char* exe_dir,
|
|
38
79
|
const char* entry_file,
|
|
39
|
-
int is_debug
|
|
80
|
+
int is_debug,
|
|
81
|
+
LPSTR lpCmdLine
|
|
40
82
|
) {
|
|
41
83
|
char python_runtime[MAX_PATH_LEN];
|
|
42
84
|
char script_path[MAX_PATH_LEN];
|
|
43
85
|
|
|
44
86
|
// Build Python interpreter path
|
|
45
|
-
|
|
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
|
-
}
|
|
87
|
+
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\%s", exe_dir, is_debug ? "python.exe" : "pythonw.exe");
|
|
53
88
|
|
|
54
89
|
// Build startup script path
|
|
55
90
|
snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
|
|
56
91
|
|
|
57
|
-
// Build command line
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
|
|
64
|
-
}
|
|
92
|
+
// Build command line: python runtime -u script [args] [2>&1 for production]
|
|
93
|
+
snprintf(cmd, MAX_PATH_LEN * 2, "\"%s\" -u \"%s\"%s%s%s",
|
|
94
|
+
python_runtime, script_path,
|
|
95
|
+
lpCmdLine && strlen(lpCmdLine) > 0 ? " " : "",
|
|
96
|
+
lpCmdLine && strlen(lpCmdLine) > 0 ? lpCmdLine : "",
|
|
97
|
+
is_debug ? "" : " 2>&1");
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
// Read process output
|
|
@@ -69,7 +102,8 @@ static void read_process_output(HANDLE hPipe, char* output, int max_len) {
|
|
|
69
102
|
DWORD bytes_read;
|
|
70
103
|
output[0] = '\0';
|
|
71
104
|
|
|
72
|
-
while (
|
|
105
|
+
while (
|
|
106
|
+
ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
|
|
73
107
|
output[bytes_read] = '\0';
|
|
74
108
|
}
|
|
75
109
|
}
|
|
@@ -108,6 +142,7 @@ int APIENTRY WinMain(
|
|
|
108
142
|
char exe_dir[MAX_PATH_LEN];
|
|
109
143
|
char cmd[MAX_PATH_LEN * 2];
|
|
110
144
|
char error_output[MAX_ERROR_LEN] = "";
|
|
145
|
+
char error_msg[MAX_ERROR_LEN];
|
|
111
146
|
STARTUPINFOA si = {0};
|
|
112
147
|
PROCESS_INFORMATION pi = {0};
|
|
113
148
|
SECURITY_ATTRIBUTES sa = {0};
|
|
@@ -131,33 +166,28 @@ int APIENTRY WinMain(
|
|
|
131
166
|
return 1;
|
|
132
167
|
}
|
|
133
168
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
}
|
|
169
|
+
// Create pipe for capturing output (only in production mode)
|
|
170
|
+
if (!${DEBUG_MODE}) {
|
|
171
|
+
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
172
|
+
sa.bInheritHandle = TRUE;
|
|
173
|
+
sa.lpSecurityDescriptor = NULL;
|
|
146
174
|
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
|
|
176
|
+
show_message_box("Error", "Failed to create pipe for error output.");
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
149
179
|
|
|
150
|
-
|
|
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
|
|
180
|
+
si.cb = sizeof(si);
|
|
156
181
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
157
182
|
si.hStdError = hWritePipe;
|
|
158
183
|
si.hStdOutput = hWritePipe;
|
|
184
|
+
} else {
|
|
185
|
+
si.cb = sizeof(si);
|
|
159
186
|
}
|
|
160
187
|
|
|
188
|
+
// Build and execute Python command
|
|
189
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);
|
|
190
|
+
|
|
161
191
|
// Create Python process
|
|
162
192
|
// Debug mode: do not use CREATE_NO_WINDOW, let python.exe create console
|
|
163
193
|
// Production GUI mode: use CREATE_NO_WINDOW to ensure no console is created
|
|
@@ -170,7 +200,6 @@ int APIENTRY WinMain(
|
|
|
170
200
|
|
|
171
201
|
if (!success) {
|
|
172
202
|
DWORD error = GetLastError();
|
|
173
|
-
char error_msg[MAX_ERROR_LEN];
|
|
174
203
|
snprintf(error_msg, MAX_ERROR_LEN,
|
|
175
204
|
"Failed to start Python process.\n\n"
|
|
176
205
|
"Error code: %lu\n"
|
|
@@ -198,8 +227,9 @@ int APIENTRY WinMain(
|
|
|
198
227
|
DWORD exit_code;
|
|
199
228
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
200
229
|
|
|
201
|
-
|
|
230
|
+
#if ${DEBUG_MODE}
|
|
202
231
|
fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
|
|
232
|
+
#endif
|
|
203
233
|
|
|
204
234
|
// Cleanup
|
|
205
235
|
CloseHandle(pi.hProcess);
|
|
@@ -227,6 +257,7 @@ _WINDOWS_CONSOLE_TEMPLATE: str = r"""#include <stdio.h>
|
|
|
227
257
|
#include <locale.h>
|
|
228
258
|
|
|
229
259
|
#define MAX_PATH_LEN 4096
|
|
260
|
+
#define MAX_ERROR_LEN 8192
|
|
230
261
|
|
|
231
262
|
// Set console encoding to UTF-8
|
|
232
263
|
static void setup_encoding() {
|
|
@@ -245,10 +276,14 @@ static void build_python_command(
|
|
|
245
276
|
char* cmd,
|
|
246
277
|
const char* exe_dir,
|
|
247
278
|
const char* entry_file,
|
|
248
|
-
int
|
|
279
|
+
int argc,
|
|
280
|
+
char* argv[]
|
|
249
281
|
) {
|
|
250
282
|
char python_runtime[MAX_PATH_LEN];
|
|
251
283
|
char script_path[MAX_PATH_LEN];
|
|
284
|
+
char* p = cmd;
|
|
285
|
+
size_t remaining = MAX_PATH_LEN * 2;
|
|
286
|
+
int len;
|
|
252
287
|
|
|
253
288
|
// Build Python interpreter path
|
|
254
289
|
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
|
|
@@ -256,11 +291,27 @@ static void build_python_command(
|
|
|
256
291
|
// Build startup script path
|
|
257
292
|
snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
|
|
258
293
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
294
|
+
// Base command
|
|
295
|
+
len = snprintf(p, remaining, "\"%s\" -u \"%s\"", python_runtime, script_path);
|
|
296
|
+
p += len;
|
|
297
|
+
remaining -= len;
|
|
298
|
+
|
|
299
|
+
// Append arguments
|
|
300
|
+
for (int i = 1; i < argc && remaining > 0; i++) {
|
|
301
|
+
len = snprintf(p, remaining, " \"%s\"", argv[i]);
|
|
302
|
+
if (len < 0 || (size_t)len >= remaining) break;
|
|
303
|
+
p += len;
|
|
304
|
+
remaining -= len;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Read process output
|
|
309
|
+
static void read_process_output(HANDLE hPipe, char* output, int max_len) {
|
|
310
|
+
DWORD bytes_read;
|
|
311
|
+
output[0] = '\0';
|
|
312
|
+
|
|
313
|
+
while (ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
|
|
314
|
+
output[bytes_read] = '\0';
|
|
264
315
|
}
|
|
265
316
|
}
|
|
266
317
|
|
|
@@ -270,49 +321,52 @@ int main(int argc, char* argv[]) {
|
|
|
270
321
|
char cmd[MAX_PATH_LEN * 2];
|
|
271
322
|
STARTUPINFOA si = {0};
|
|
272
323
|
PROCESS_INFORMATION pi = {0};
|
|
324
|
+
BOOL success;
|
|
325
|
+
char* last_slash;
|
|
273
326
|
|
|
274
|
-
//
|
|
327
|
+
// Setup
|
|
275
328
|
setup_encoding();
|
|
329
|
+
si.cb = sizeof(si);
|
|
276
330
|
|
|
277
331
|
// Get executable directory
|
|
278
332
|
GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
*last_slash = '\0';
|
|
282
|
-
}
|
|
283
|
-
|
|
333
|
+
if ((last_slash = strrchr(exe_dir, '\\'))) *last_slash = '\0';
|
|
334
|
+
`
|
|
284
335
|
// Check Python runtime
|
|
285
|
-
|
|
286
|
-
snprintf(python_runtime_check, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
|
|
287
|
-
if (!check_python_runtime(python_runtime_check)) {
|
|
336
|
+
if (!check_python_runtime(exe_dir)) {
|
|
288
337
|
fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
|
|
289
338
|
fprintf(stderr, "Please ensure the application is installed correctly.\n");
|
|
290
339
|
return 1;
|
|
291
340
|
}
|
|
292
341
|
|
|
293
342
|
// Build and execute Python command
|
|
294
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}",
|
|
343
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
295
344
|
|
|
345
|
+
#if ${DEBUG_MODE}
|
|
296
346
|
// Debug output
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
347
|
+
#ifdef _WIN32
|
|
348
|
+
{
|
|
349
|
+
int wcmd_len = MultiByteToWideChar(CP_UTF8, 0, cmd, -1, NULL, 0);
|
|
350
|
+
if (wcmd_len > 0) {
|
|
351
|
+
wchar_t* wcmd = (wchar_t*)malloc(wcmd_len * sizeof(wchar_t));
|
|
352
|
+
MultiByteToWideChar(CP_UTF8, 0, cmd, -1, wcmd, wcmd_len);
|
|
353
|
+
fwprintf(stderr, L"DEBUG: Command to execute: %ls\n", wcmd);
|
|
354
|
+
free(wcmd);
|
|
355
|
+
}
|
|
300
356
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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);
|
|
357
|
+
#else
|
|
358
|
+
fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
|
|
359
|
+
#endif
|
|
360
|
+
#endif
|
|
310
361
|
|
|
311
362
|
// Create Python process
|
|
312
|
-
|
|
313
|
-
|
|
363
|
+
// TRUE - inherit handles so Python can write to console
|
|
364
|
+
success = CreateProcessA(
|
|
365
|
+
NULL, cmd, NULL, NULL, TRUE,
|
|
314
366
|
0, NULL, exe_dir, &si, &pi
|
|
315
|
-
)
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (!success) {
|
|
316
370
|
DWORD error = GetLastError();
|
|
317
371
|
fprintf(stderr, "Error: Failed to start Python process.\n");
|
|
318
372
|
fprintf(stderr, "Error code: %lu\n", error);
|
|
@@ -320,6 +374,9 @@ int main(int argc, char* argv[]) {
|
|
|
320
374
|
return 1;
|
|
321
375
|
}
|
|
322
376
|
|
|
377
|
+
// In non-debug mode for console apps, we don't use pipes, so no error output capture
|
|
378
|
+
// All output (stdout/stderr) goes directly to console
|
|
379
|
+
|
|
323
380
|
// Wait for process to end
|
|
324
381
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
325
382
|
|
|
@@ -327,19 +384,19 @@ int main(int argc, char* argv[]) {
|
|
|
327
384
|
DWORD exit_code;
|
|
328
385
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
329
386
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
387
|
+
#if ${DEBUG_MODE}
|
|
388
|
+
fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
|
|
389
|
+
#endif
|
|
334
390
|
|
|
335
391
|
// Cleanup
|
|
336
392
|
CloseHandle(pi.hProcess);
|
|
337
393
|
CloseHandle(pi.hThread);
|
|
338
394
|
|
|
339
|
-
//
|
|
340
|
-
if (exit_code != 0
|
|
341
|
-
fprintf(stderr, "\nApplication exited
|
|
342
|
-
fprintf(stderr, "
|
|
395
|
+
// In non-debug mode, we don't capture output, so just report exit code if non-zero
|
|
396
|
+
if (exit_code != 0) {
|
|
397
|
+
fprintf(stderr, "\nApplication exited with error code: %lu\n", exit_code);
|
|
398
|
+
fprintf(stderr, "Check console output above for details.\n");
|
|
399
|
+
return exit_code;
|
|
343
400
|
}
|
|
344
401
|
|
|
345
402
|
return exit_code;
|
|
@@ -382,26 +439,23 @@ static void build_python_command(
|
|
|
382
439
|
|
|
383
440
|
// Unix GUI entry point
|
|
384
441
|
int main(int argc, char* argv[]) {
|
|
442
|
+
(void)argc;
|
|
385
443
|
char exe_dir[MAX_PATH_LEN];
|
|
386
|
-
char cmd[MAX_PATH_LEN *
|
|
444
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
387
445
|
char log_file[MAX_PATH_LEN];
|
|
388
446
|
pid_t pid;
|
|
389
447
|
int status;
|
|
448
|
+
char* last_slash;
|
|
390
449
|
|
|
391
450
|
// Get executable directory
|
|
392
|
-
if (realpath("/proc/self/exe", exe_dir) == NULL
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return 1;
|
|
397
|
-
}
|
|
451
|
+
if (realpath("/proc/self/exe", exe_dir) == NULL &&
|
|
452
|
+
realpath(argv[0], exe_dir) == NULL) {
|
|
453
|
+
fprintf(stderr, "Error: Cannot determine executable directory\n");
|
|
454
|
+
return 1;
|
|
398
455
|
}
|
|
399
456
|
|
|
400
457
|
// Remove executable name, keep only directory
|
|
401
|
-
|
|
402
|
-
if (last_slash) {
|
|
403
|
-
*last_slash = '\0';
|
|
404
|
-
}
|
|
458
|
+
if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';
|
|
405
459
|
|
|
406
460
|
// Build and execute Python command
|
|
407
461
|
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
@@ -471,10 +525,13 @@ static void build_python_command(
|
|
|
471
525
|
|
|
472
526
|
// Unix Console entry point
|
|
473
527
|
int main(int argc, char* argv[]) {
|
|
528
|
+
(void)argc;
|
|
529
|
+
(void)argv;
|
|
474
530
|
char exe_dir[MAX_PATH_LEN];
|
|
475
|
-
char cmd[MAX_PATH_LEN *
|
|
531
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
476
532
|
pid_t pid;
|
|
477
533
|
int status;
|
|
534
|
+
char* last_slash;
|
|
478
535
|
|
|
479
536
|
// Get executable directory
|
|
480
537
|
if (realpath("/proc/self/exe", exe_dir) == NULL) {
|
|
@@ -483,10 +540,7 @@ int main(int argc, char* argv[]) {
|
|
|
483
540
|
}
|
|
484
541
|
|
|
485
542
|
// Remove executable name, keep only directory
|
|
486
|
-
|
|
487
|
-
if (last_slash) {
|
|
488
|
-
*last_slash = '\0';
|
|
489
|
-
}
|
|
543
|
+
if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';
|
|
490
544
|
|
|
491
545
|
// Build and execute Python command
|
|
492
546
|
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
@@ -549,8 +603,10 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
549
603
|
CFRelease(url);
|
|
550
604
|
}
|
|
551
605
|
} else {
|
|
552
|
-
// If not in bundle,
|
|
553
|
-
|
|
606
|
+
// If not in bundle, get current working directory
|
|
607
|
+
if (getcwd(exe_dir, size) == NULL) {
|
|
608
|
+
exe_dir[0] = '\0';
|
|
609
|
+
}
|
|
554
610
|
}
|
|
555
611
|
|
|
556
612
|
// Remove Contents/MacOS suffix of bundle
|
|
@@ -558,10 +614,10 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
558
614
|
if (macos_path) {
|
|
559
615
|
*macos_path = '\0';
|
|
560
616
|
}
|
|
561
|
-
|
|
617
|
+
}
|
|
562
618
|
|
|
563
|
-
|
|
564
|
-
|
|
619
|
+
// Display macOS error dialog
|
|
620
|
+
static void show_error_dialog(const char* message) {
|
|
565
621
|
CFStringRef cf_message = CFStringCreateWithCString(
|
|
566
622
|
NULL, message, kCFStringEncodingUTF8
|
|
567
623
|
);
|
|
@@ -603,9 +659,12 @@ static void build_python_command(
|
|
|
603
659
|
|
|
604
660
|
// macOS GUI entry point
|
|
605
661
|
int main(int argc, char* argv[]) {
|
|
662
|
+
(void)argc;
|
|
663
|
+
(void)argv;
|
|
606
664
|
char exe_dir[MAX_PATH_LEN];
|
|
607
|
-
char cmd[MAX_PATH_LEN *
|
|
665
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
608
666
|
char log_file[MAX_PATH_LEN];
|
|
667
|
+
char python_runtime[MAX_PATH_LEN];
|
|
609
668
|
pid_t pid;
|
|
610
669
|
int status;
|
|
611
670
|
|
|
@@ -613,10 +672,8 @@ int main(int argc, char* argv[]) {
|
|
|
613
672
|
get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
|
|
614
673
|
|
|
615
674
|
// Check Python runtime
|
|
616
|
-
char python_runtime[MAX_PATH_LEN];
|
|
617
675
|
snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
|
|
618
676
|
if (!check_python_runtime(python_runtime)) {
|
|
619
|
-
// macOS GUI app needs to use dialog to display error
|
|
620
677
|
char error_msg[MAX_PATH_LEN];
|
|
621
678
|
snprintf(error_msg, MAX_PATH_LEN,
|
|
622
679
|
"Python runtime not found.\n\n"
|
|
@@ -704,7 +761,10 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
704
761
|
CFRelease(url);
|
|
705
762
|
}
|
|
706
763
|
} else {
|
|
707
|
-
|
|
764
|
+
// Get current working directory if not in bundle
|
|
765
|
+
if (getcwd(exe_dir, size) == NULL) {
|
|
766
|
+
exe_dir[0] = '\0';
|
|
767
|
+
}
|
|
708
768
|
}
|
|
709
769
|
}
|
|
710
770
|
|
|
@@ -729,8 +789,11 @@ static void build_python_command(
|
|
|
729
789
|
|
|
730
790
|
// macOS Console entry point
|
|
731
791
|
int main(int argc, char* argv[]) {
|
|
792
|
+
(void)argc;
|
|
793
|
+
(void)argv;
|
|
732
794
|
char exe_dir[MAX_PATH_LEN];
|
|
733
|
-
char cmd[MAX_PATH_LEN *
|
|
795
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
796
|
+
char python_runtime[MAX_PATH_LEN];
|
|
734
797
|
pid_t pid;
|
|
735
798
|
int status;
|
|
736
799
|
|
|
@@ -738,7 +801,6 @@ int main(int argc, char* argv[]) {
|
|
|
738
801
|
get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
|
|
739
802
|
|
|
740
803
|
// Check Python runtime
|
|
741
|
-
char python_runtime[MAX_PATH_LEN];
|
|
742
804
|
snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
|
|
743
805
|
if (!check_python_runtime(python_runtime)) {
|
|
744
806
|
fprintf(stderr, "Error: Python runtime not found at %s/runtime/bin/\n", exe_dir);
|
|
@@ -772,79 +834,125 @@ int main(int argc, char* argv[]) {
|
|
|
772
834
|
}
|
|
773
835
|
"""
|
|
774
836
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
837
|
+
ENTRY_FILE_TEMPLATE = r"""
|
|
838
|
+
import os
|
|
839
|
+
import sys
|
|
840
|
+
from pathlib import Path
|
|
780
841
|
|
|
842
|
+
# Setup environment
|
|
843
|
+
cwd = Path.cwd()
|
|
844
|
+
site_dirs = [cwd / "site-packages", cwd / "lib"]
|
|
845
|
+
dirs = [cwd, cwd / "src", cwd / "runtime", *site_dirs]
|
|
846
|
+
|
|
847
|
+
for dir in dirs:
|
|
848
|
+
sys.path.append(str(dir))
|
|
849
|
+
|
|
850
|
+
# Fix zipimporter compatibility for packages imported from zip archives
|
|
851
|
+
# Some packages (e.g., markdown) require exec_module on zipimporter
|
|
852
|
+
import zipimport
|
|
853
|
+
if not hasattr(zipimport.zipimporter, 'exec_module'):
|
|
854
|
+
def _create_module(self, spec):
|
|
855
|
+
return None
|
|
856
|
+
def _exec_module(self, module):
|
|
857
|
+
code = self.get_code(module.__name__)
|
|
858
|
+
exec(code, module.__dict__)
|
|
859
|
+
zipimport.zipimporter.create_module = _create_module
|
|
860
|
+
zipimport.zipimporter.exec_module = _exec_module
|
|
861
|
+
|
|
862
|
+
# Qt configuration (optional)
|
|
863
|
+
$QT_CONFIG
|
|
864
|
+
|
|
865
|
+
# Main entry point
|
|
866
|
+
from src.$PROJECT_NAME.$ENTRY_NAME import main
|
|
867
|
+
main()
|
|
868
|
+
"""
|
|
781
869
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
870
|
+
# Compiler configurations using frozenset for better performance
|
|
871
|
+
_COMPILER_CONFIGS: frozenset[CompilerConfig] = frozenset([
|
|
872
|
+
CompilerConfig(name="gcc", args=("-std=c99", "-Wall", "-O2")),
|
|
873
|
+
CompilerConfig(name="clang", args=("-std=c99", "-Wall", "-O2")),
|
|
874
|
+
CompilerConfig(name="cl", args=("/std:c99", "/O2")),
|
|
875
|
+
])
|
|
785
876
|
|
|
786
|
-
for compiler in compilers:
|
|
787
|
-
if shutil.which(compiler) is not None:
|
|
788
|
-
return compiler
|
|
789
877
|
|
|
878
|
+
def find_compiler() -> str | None:
|
|
879
|
+
"""Find available C compiler."""
|
|
880
|
+
for compiler in ("gcc", "clang", "cl"):
|
|
881
|
+
if shutil.which(compiler):
|
|
882
|
+
return compiler
|
|
790
883
|
logger.error("No compiler found")
|
|
791
884
|
return None
|
|
792
885
|
|
|
793
886
|
|
|
794
|
-
def get_compiler_args(
|
|
795
|
-
compiler
|
|
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
|
|
887
|
+
def get_compiler_args(compiler: str) -> list[str]:
|
|
888
|
+
"""Get compiler arguments for specified compiler.
|
|
799
889
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
890
|
+
Args:
|
|
891
|
+
compiler: Compiler name or path
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
List of compiler arguments
|
|
895
|
+
"""
|
|
896
|
+
compiler_name = (
|
|
897
|
+
Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
|
|
898
|
+
)
|
|
806
899
|
|
|
900
|
+
for config in _COMPILER_CONFIGS:
|
|
901
|
+
if config.name == compiler_name:
|
|
902
|
+
return config.to_list()
|
|
807
903
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
) -> str:
|
|
904
|
+
return []
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def select_c_template(loader_type: str, debug: bool) -> str:
|
|
812
908
|
"""Select the appropriate C code template based on platform and type.
|
|
813
909
|
|
|
814
910
|
In debug mode, always use console template to ensure output is visible.
|
|
815
911
|
"""
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
912
|
+
if debug:
|
|
913
|
+
loader_type = "console"
|
|
824
914
|
|
|
825
|
-
# In non-debug mode, use the requested template type
|
|
826
915
|
if is_windows:
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return _WINDOWS_CONSOLE_TEMPLATE
|
|
916
|
+
return (
|
|
917
|
+
_WINDOWS_GUI_TEMPLATE if loader_type == "gui" else _WINDOWS_CONSOLE_TEMPLATE
|
|
918
|
+
)
|
|
831
919
|
elif is_macos:
|
|
832
|
-
if loader_type == "gui"
|
|
833
|
-
|
|
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
|
|
920
|
+
return _MACOS_GUI_TEMPLATE if loader_type == "gui" else _MACOS_CONSOLE_TEMPLATE
|
|
921
|
+
else:
|
|
922
|
+
return _UNIX_GUI_TEMPLATE if loader_type == "gui" else _UNIX_CONSOLE_TEMPLATE
|
|
841
923
|
|
|
842
924
|
|
|
843
|
-
def
|
|
844
|
-
template
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
925
|
+
def prepare_entry_file(project: Project, entry_stem: str) -> str:
|
|
926
|
+
"""Generate entry file code by replacing placeholders in template.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
project_name: Project name
|
|
930
|
+
entry_stem: Entry file name without extension (e.g., "myapp" for "myapp.py" or "myapp.ent")
|
|
931
|
+
is_qt: Whether this is a Qt application
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
Generated entry file code
|
|
935
|
+
"""
|
|
936
|
+
qt_config = (
|
|
937
|
+
"""# Qt configuration
|
|
938
|
+
qt_dir = cwd / "site-packages" / "{$QT_NAME}"
|
|
939
|
+
plugin_path = str(qt_dir / "plugins" / "platforms")
|
|
940
|
+
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
|
|
941
|
+
|
|
942
|
+
""".replace("$QT_NAME", project.qt_libname or "")
|
|
943
|
+
if project.has_qt
|
|
944
|
+
else ""
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
return (
|
|
948
|
+
ENTRY_FILE_TEMPLATE
|
|
949
|
+
.replace("$PROJECT_NAME", project.name.replace("-", "_"))
|
|
950
|
+
.replace("$ENTRY_NAME", entry_stem)
|
|
951
|
+
.replace("$QT_CONFIG", qt_config)
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def prepare_c_source(template: str, entry_file: str, is_debug: bool) -> str:
|
|
848
956
|
"""Generate C source code by replacing placeholders in template."""
|
|
849
957
|
# Replace placeholders
|
|
850
958
|
c_code = template.replace("${ENTRY_FILE}", entry_file)
|
|
@@ -877,7 +985,9 @@ def compile_c_source(
|
|
|
877
985
|
output_filepath = output_filepath.with_suffix(ext)
|
|
878
986
|
|
|
879
987
|
# Build compile command
|
|
880
|
-
compiler_name =
|
|
988
|
+
compiler_name = (
|
|
989
|
+
Path(compiler).name if "\\" in compiler or "/" in compiler else compiler
|
|
990
|
+
)
|
|
881
991
|
if compiler_name.lower() == "cl" or compiler_name.lower() == "cl.exe":
|
|
882
992
|
# MSVC compiler
|
|
883
993
|
cmd = [
|
|
@@ -886,7 +996,9 @@ def compile_c_source(
|
|
|
886
996
|
str(c_source_path),
|
|
887
997
|
f"/Fe:{output_filepath}",
|
|
888
998
|
"/link",
|
|
889
|
-
"/SUBSYSTEM:WINDOWS"
|
|
999
|
+
"/SUBSYSTEM:WINDOWS"
|
|
1000
|
+
if "gui" in str(c_source_path).lower()
|
|
1001
|
+
else "/SUBSYSTEM:CONSOLE",
|
|
890
1002
|
]
|
|
891
1003
|
else:
|
|
892
1004
|
# GCC/Clang compiler
|
|
@@ -894,7 +1006,14 @@ def compile_c_source(
|
|
|
894
1006
|
# For Windows GUI, add -mwindows flag with gcc/clang
|
|
895
1007
|
if is_windows and "gui" in str(c_source_path).lower():
|
|
896
1008
|
subsystem_flag = ["-mwindows"]
|
|
897
|
-
cmd = [
|
|
1009
|
+
cmd = [
|
|
1010
|
+
compiler,
|
|
1011
|
+
*compiler_args,
|
|
1012
|
+
*subsystem_flag,
|
|
1013
|
+
"-o",
|
|
1014
|
+
str(output_filepath),
|
|
1015
|
+
str(c_source_path),
|
|
1016
|
+
]
|
|
898
1017
|
|
|
899
1018
|
logger.debug(f"Compiling with command: {' '.join(cmd)}")
|
|
900
1019
|
try:
|
|
@@ -914,82 +1033,248 @@ def compile_c_source(
|
|
|
914
1033
|
return False
|
|
915
1034
|
|
|
916
1035
|
|
|
917
|
-
def
|
|
918
|
-
|
|
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")
|
|
1036
|
+
def _is_entry_file(file_path: Path) -> bool:
|
|
1037
|
+
"""Check if a Python file is an entry file.
|
|
931
1038
|
|
|
932
|
-
|
|
1039
|
+
An entry file must contain either:
|
|
1040
|
+
- def main() function
|
|
1041
|
+
- if __name__ == '__main__': block
|
|
933
1042
|
|
|
934
|
-
|
|
935
|
-
|
|
1043
|
+
Args:
|
|
1044
|
+
file_path: Path to Python file
|
|
936
1045
|
|
|
937
|
-
|
|
938
|
-
|
|
1046
|
+
Returns:
|
|
1047
|
+
True if file is an entry file, False otherwise
|
|
1048
|
+
"""
|
|
1049
|
+
if not file_path.is_file():
|
|
1050
|
+
return False
|
|
939
1051
|
|
|
940
|
-
|
|
941
|
-
|
|
1052
|
+
try:
|
|
1053
|
+
content = file_path.read_text(encoding="utf-8")
|
|
1054
|
+
return (
|
|
1055
|
+
"def main(" in content
|
|
1056
|
+
or "if __name__ == '__main__':" in content
|
|
1057
|
+
or 'if __name__ == "__main__":' in content
|
|
1058
|
+
)
|
|
1059
|
+
except Exception as e:
|
|
1060
|
+
logger.debug(f"Failed to read {file_path}: {e}")
|
|
1061
|
+
return False
|
|
942
1062
|
|
|
943
|
-
# Generate C source code
|
|
944
|
-
c_code = generate_c_source(template, args.entry_file, args.debug)
|
|
945
1063
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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"
|
|
1064
|
+
def _detect_entry_files(
|
|
1065
|
+
project_dir: Path,
|
|
1066
|
+
project_name: str,
|
|
1067
|
+
) -> list[EntryFile]:
|
|
1068
|
+
"""Detect all entry files in project directory.
|
|
955
1069
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1070
|
+
Args:
|
|
1071
|
+
project_dir: Project directory
|
|
1072
|
+
project_name: Project name
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
List of tuples (entry_file_name, module_name, is_gui)
|
|
1076
|
+
e.g., [("docscan", "docscan", False), ("docscan-gui", "docscan_gui", True)]
|
|
1077
|
+
"""
|
|
1078
|
+
normalized_name = project_name.replace("-", "_")
|
|
1079
|
+
entry_files: list[EntryFile] = []
|
|
1080
|
+
|
|
1081
|
+
# Scan all Python files in project directory
|
|
1082
|
+
for py_file in project_dir.glob("*.py"):
|
|
1083
|
+
if not _is_entry_file(py_file):
|
|
1084
|
+
continue
|
|
1085
|
+
|
|
1086
|
+
entry_file = EntryFile(project_name=normalized_name, source_file=py_file)
|
|
1087
|
+
entry_files.append(entry_file)
|
|
1088
|
+
logger.debug(f"Found entry file: {entry_file}")
|
|
1089
|
+
|
|
1090
|
+
return entry_files
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _generate_single_loader(
|
|
1094
|
+
project: Project,
|
|
1095
|
+
project_dir: Path,
|
|
1096
|
+
entry_file: EntryFile,
|
|
1097
|
+
is_debug: bool,
|
|
1098
|
+
) -> bool:
|
|
1099
|
+
"""Generate a single loader executable and entry file.
|
|
1100
|
+
|
|
1101
|
+
Args:
|
|
1102
|
+
project_dir: Project directory
|
|
1103
|
+
entry_name: Entry file name (e.g., "docscan", "docscan-gui")
|
|
1104
|
+
entry_module: Python module name (e.g., "docscan", "docscan_gui")
|
|
1105
|
+
loader_type: Loader type ("console" or "gui")
|
|
1106
|
+
use_qt: Whether this is a Qt application
|
|
1107
|
+
is_debug: Whether to generate debug version
|
|
1108
|
+
compiler: Optional compiler to use
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
True if successful, False otherwise
|
|
1112
|
+
"""
|
|
1113
|
+
entry_name = entry_file.entry_name
|
|
1114
|
+
|
|
1115
|
+
# Prepare directories
|
|
1116
|
+
build_dir = project_dir / ".cbuild"
|
|
1117
|
+
output_dir = project_dir / "dist"
|
|
1118
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
1119
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
1120
|
+
|
|
1121
|
+
# Output paths
|
|
1122
|
+
output_exe = output_dir / f"{entry_name}{ext}"
|
|
1123
|
+
c_source_filename = (
|
|
1124
|
+
f"{entry_name}_debug_console.c"
|
|
1125
|
+
if is_debug
|
|
1126
|
+
else f"{entry_name}_{project.is_gui}.c"
|
|
1127
|
+
)
|
|
1128
|
+
entry_filepath = output_dir / f"{entry_name}.ent"
|
|
1129
|
+
c_source_path = build_dir / c_source_filename
|
|
959
1130
|
|
|
960
1131
|
t0 = time.perf_counter()
|
|
961
1132
|
|
|
962
|
-
#
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
logger.
|
|
986
|
-
|
|
1133
|
+
# Generate entry file
|
|
1134
|
+
# Extract project name from entry_name (remove -gui suffix if present)
|
|
1135
|
+
entry_file_code = prepare_entry_file(project=project, entry_stem=entry_name)
|
|
1136
|
+
entry_filepath.write_text(entry_file_code, encoding="utf-8")
|
|
1137
|
+
logger.debug(f"Generated entry file: {entry_filepath}")
|
|
1138
|
+
|
|
1139
|
+
# Generate and compile C source
|
|
1140
|
+
c_template = select_c_template(project.loader_type, is_debug)
|
|
1141
|
+
c_code = prepare_c_source(c_template, entry_filepath.name, is_debug)
|
|
1142
|
+
c_source_path.write_text(c_code, encoding="utf-8")
|
|
1143
|
+
logger.debug(f"Generated C source code: {c_source_path}")
|
|
1144
|
+
|
|
1145
|
+
compiler = find_compiler()
|
|
1146
|
+
if not compiler:
|
|
1147
|
+
logger.error(f"Failed to find compiler: {compiler}")
|
|
1148
|
+
return False
|
|
1149
|
+
|
|
1150
|
+
success = compile_c_source(c_source_path, output_exe, compiler)
|
|
1151
|
+
if success:
|
|
1152
|
+
logger.info(f"Loader executable generated successfully: {output_exe}")
|
|
1153
|
+
logger.debug(f"Entry: {entry_file}")
|
|
1154
|
+
logger.debug(f"Type: {project.loader_type}")
|
|
1155
|
+
logger.debug(f"Qt: {project.has_qt}")
|
|
1156
|
+
logger.debug(f"Debug mode: {is_debug}")
|
|
1157
|
+
logger.debug(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
|
|
987
1158
|
else:
|
|
988
|
-
logger.error("
|
|
989
|
-
return 1
|
|
1159
|
+
logger.error(f"Failed to compile loader for {entry_name}")
|
|
990
1160
|
|
|
1161
|
+
return success
|
|
991
1162
|
|
|
992
|
-
if __name__ == "__main__":
|
|
993
|
-
import sys
|
|
994
1163
|
|
|
995
|
-
|
|
1164
|
+
def generate_loader_for_project(
|
|
1165
|
+
project: Project,
|
|
1166
|
+
project_dir: Path,
|
|
1167
|
+
debug: bool = False,
|
|
1168
|
+
):
|
|
1169
|
+
"""Generate loader executables and entry files for a single project.
|
|
1170
|
+
|
|
1171
|
+
This function detects all entry files (main and GUI versions) and generates
|
|
1172
|
+
a loader for each one.
|
|
1173
|
+
|
|
1174
|
+
Args:
|
|
1175
|
+
project: Project information dataclass
|
|
1176
|
+
project_dir: Directory containing the project (pyproject.toml location)
|
|
1177
|
+
debug: Whether to generate debug version
|
|
1178
|
+
"""
|
|
1179
|
+
# Determine base loader type and Qt usage from project info
|
|
1180
|
+
# Detect all entry files in project directory
|
|
1181
|
+
entry_files = _detect_entry_files(project_dir, project.name)
|
|
1182
|
+
if not entry_files:
|
|
1183
|
+
logger.error(f"No entry files found in {project_dir}")
|
|
1184
|
+
return False
|
|
1185
|
+
|
|
1186
|
+
# Generate loaders for all entry files
|
|
1187
|
+
for entry_file in entry_files:
|
|
1188
|
+
logger.debug(f"Generating loader for: {entry_file.project_name}")
|
|
1189
|
+
if not _generate_single_loader(
|
|
1190
|
+
project=project,
|
|
1191
|
+
project_dir=project_dir,
|
|
1192
|
+
entry_file=entry_file,
|
|
1193
|
+
is_debug=debug,
|
|
1194
|
+
):
|
|
1195
|
+
return False
|
|
1196
|
+
|
|
1197
|
+
return True
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def generate_loader(base_dir: Path, debug: bool) -> None:
|
|
1201
|
+
"""Generate loader executables for all projects in the given directory."""
|
|
1202
|
+
from sfi.pyprojectparse.pyprojectparse import Solution
|
|
1203
|
+
|
|
1204
|
+
projects = Solution.projects
|
|
1205
|
+
if not projects:
|
|
1206
|
+
logger.error("Failed to load project information")
|
|
1207
|
+
|
|
1208
|
+
success_count = 0
|
|
1209
|
+
failed_projects: list[str] = []
|
|
1210
|
+
|
|
1211
|
+
if len(projects) == 1:
|
|
1212
|
+
logger.debug("Only one project found, using current directory")
|
|
1213
|
+
project_dir = base_dir
|
|
1214
|
+
if generate_loader_for_project(
|
|
1215
|
+
project=next(iter(projects.values())),
|
|
1216
|
+
project_dir=project_dir,
|
|
1217
|
+
debug=debug,
|
|
1218
|
+
):
|
|
1219
|
+
success_count += 1
|
|
1220
|
+
else:
|
|
1221
|
+
failed_projects.append(next(iter(projects)))
|
|
1222
|
+
else:
|
|
1223
|
+
for project_name, project in projects.items():
|
|
1224
|
+
project_dir = base_dir / project_name
|
|
1225
|
+
if not project_dir.is_dir():
|
|
1226
|
+
logger.error(f"Project directory not found: {project_dir}, skipping...")
|
|
1227
|
+
failed_projects.append(project_name)
|
|
1228
|
+
continue
|
|
1229
|
+
|
|
1230
|
+
if generate_loader_for_project(
|
|
1231
|
+
project=project,
|
|
1232
|
+
project_dir=project_dir,
|
|
1233
|
+
debug=debug,
|
|
1234
|
+
):
|
|
1235
|
+
success_count += 1
|
|
1236
|
+
else:
|
|
1237
|
+
failed_projects.append(project_name)
|
|
1238
|
+
|
|
1239
|
+
logger.info(f"Pack {success_count}/{len(projects)} projects successfully")
|
|
1240
|
+
|
|
1241
|
+
if failed_projects:
|
|
1242
|
+
logger.error(f"Failed: {failed_projects}")
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def parse_args() -> argparse.Namespace:
|
|
1246
|
+
parser = argparse.ArgumentParser(
|
|
1247
|
+
description="Generate Python loader executables for all projects in directory"
|
|
1248
|
+
)
|
|
1249
|
+
parser.add_argument(
|
|
1250
|
+
"directory",
|
|
1251
|
+
nargs="?",
|
|
1252
|
+
default=str(cwd),
|
|
1253
|
+
help="Directory containing projects (will create/load projects.json)",
|
|
1254
|
+
)
|
|
1255
|
+
parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
|
|
1256
|
+
parser.add_argument(
|
|
1257
|
+
"--compiler",
|
|
1258
|
+
help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe",
|
|
1259
|
+
)
|
|
1260
|
+
return parser.parse_args()
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def main():
|
|
1264
|
+
"""Main entry point for pyloadergen."""
|
|
1265
|
+
args = parse_args()
|
|
1266
|
+
|
|
1267
|
+
if args.debug:
|
|
1268
|
+
logger.setLevel(logging.DEBUG)
|
|
1269
|
+
|
|
1270
|
+
working_dir = Path(args.directory)
|
|
1271
|
+
if not working_dir.is_dir():
|
|
1272
|
+
logger.error(f"Directory does not exist: {working_dir}")
|
|
1273
|
+
return
|
|
1274
|
+
|
|
1275
|
+
logger.info(f"Working directory: {working_dir}")
|
|
1276
|
+
generate_loader(working_dir, args.debug)
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
if __name__ == "__main__":
|
|
1280
|
+
main()
|