pysfi 0.1.10__py3-none-any.whl → 0.1.12__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.10.dist-info → pysfi-0.1.12.dist-info}/METADATA +9 -7
- pysfi-0.1.12.dist-info/RECORD +62 -0
- {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/entry_points.txt +13 -2
- 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/docdiff/docdiff.py +238 -0
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan_gui.py +1 -1
- sfi/docscan/lang/eng.py +152 -152
- sfi/docscan/lang/zhcn.py +170 -170
- 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 +2 -2
- sfi/pdfsplit/pdfsplit.py +4 -4
- sfi/pyarchive/pyarchive.py +418 -0
- sfi/pyembedinstall/__init__.py +0 -0
- sfi/pyembedinstall/pyembedinstall.py +629 -0
- sfi/pylibpack/pylibpack.py +813 -269
- 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 +271 -572
- sfi/pypack/pypack.py +822 -471
- sfi/pyprojectparse/__init__.py +0 -0
- sfi/pyprojectparse/pyprojectparse.py +500 -0
- sfi/pysourcepack/pysourcepack.py +308 -369
- 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
- pysfi-0.1.10.dist-info/RECORD +0 -39
- sfi/embedinstall/embedinstall.py +0 -478
- sfi/projectparse/projectparse.py +0 -152
- {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/WHEEL +0 -0
- /sfi/{embedinstall → llmclient}/__init__.py +0 -0
- /sfi/{projectparse → llmquantize}/__init__.py +0 -0
sfi/pyloadergen/pyloadergen.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
import platform
|
|
7
6
|
import shutil
|
|
8
7
|
import subprocess
|
|
9
8
|
import time
|
|
10
|
-
from dataclasses import dataclass
|
|
9
|
+
from dataclasses import dataclass
|
|
11
10
|
from pathlib import Path
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
from sfi.pyprojectparse.pyprojectparse import Project
|
|
13
13
|
|
|
14
14
|
is_windows = platform.system() == "Windows"
|
|
15
15
|
is_linux = platform.system() == "Linux"
|
|
@@ -21,62 +21,6 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
cwd = Path.cwd()
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class ProjectInfo:
|
|
26
|
-
"""Project information dataclass.
|
|
27
|
-
|
|
28
|
-
Attributes:
|
|
29
|
-
name: Project name
|
|
30
|
-
version: Project version
|
|
31
|
-
description: Project description
|
|
32
|
-
dependencies: List of project dependencies
|
|
33
|
-
keywords: Project keywords
|
|
34
|
-
scripts: Entry point scripts
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
name: str
|
|
38
|
-
version: str = "0.0.0"
|
|
39
|
-
description: str = ""
|
|
40
|
-
dependencies: list[str] = field(default_factory=list)
|
|
41
|
-
keywords: list[str] = field(default_factory=list)
|
|
42
|
-
scripts: dict[str, str] = field(default_factory=dict)
|
|
43
|
-
|
|
44
|
-
@classmethod
|
|
45
|
-
def from_dict(cls, project_name: str, data: dict[str, Any]) -> ProjectInfo:
|
|
46
|
-
"""Create ProjectInfo from dictionary.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
project_name: Project name
|
|
50
|
-
data: Dictionary containing project information
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
ProjectInfo instance
|
|
54
|
-
"""
|
|
55
|
-
return cls(
|
|
56
|
-
name=project_name,
|
|
57
|
-
version=data.get("version", "0.0.0"),
|
|
58
|
-
description=data.get("description", ""),
|
|
59
|
-
dependencies=data.get("dependencies", []),
|
|
60
|
-
keywords=data.get("keywords", []),
|
|
61
|
-
scripts=data.get("scripts", {}),
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
def to_dict(self) -> dict[str, Any]:
|
|
65
|
-
"""Convert ProjectInfo to dictionary.
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
Dictionary representation of ProjectInfo
|
|
69
|
-
"""
|
|
70
|
-
return {
|
|
71
|
-
"name": self.name,
|
|
72
|
-
"version": self.version,
|
|
73
|
-
"description": self.description,
|
|
74
|
-
"dependencies": self.dependencies,
|
|
75
|
-
"keywords": self.keywords,
|
|
76
|
-
"scripts": self.scripts,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
24
|
@dataclass(frozen=True)
|
|
81
25
|
class CompilerConfig:
|
|
82
26
|
"""Compiler configuration dataclass.
|
|
@@ -98,6 +42,22 @@ class CompilerConfig:
|
|
|
98
42
|
return list(self.args)
|
|
99
43
|
|
|
100
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
|
+
|
|
101
61
|
_WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
|
|
102
62
|
#include <stdio.h>
|
|
103
63
|
#include <stdlib.h>
|
|
@@ -124,34 +84,17 @@ static void build_python_command(
|
|
|
124
84
|
char script_path[MAX_PATH_LEN];
|
|
125
85
|
|
|
126
86
|
// Build Python interpreter path
|
|
127
|
-
|
|
128
|
-
if (is_debug) {
|
|
129
|
-
// Debug mode: use python.exe to show console output
|
|
130
|
-
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
|
|
131
|
-
} else {
|
|
132
|
-
// Production GUI mode: use pythonw.exe without creating console window
|
|
133
|
-
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\pythonw.exe", exe_dir);
|
|
134
|
-
}
|
|
87
|
+
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\%s", exe_dir, is_debug ? "python.exe" : "pythonw.exe");
|
|
135
88
|
|
|
136
89
|
// Build startup script path
|
|
137
90
|
snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
|
|
138
91
|
|
|
139
|
-
// Build command line
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
// Production mode: redirect all output to pipe
|
|
149
|
-
if (lpCmdLine && strlen(lpCmdLine) > 0) {
|
|
150
|
-
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" %s 2>&1", python_runtime, script_path, lpCmdLine);
|
|
151
|
-
} else {
|
|
152
|
-
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
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");
|
|
155
98
|
}
|
|
156
99
|
|
|
157
100
|
// Read process output
|
|
@@ -159,7 +102,8 @@ static void read_process_output(HANDLE hPipe, char* output, int max_len) {
|
|
|
159
102
|
DWORD bytes_read;
|
|
160
103
|
output[0] = '\0';
|
|
161
104
|
|
|
162
|
-
while (
|
|
105
|
+
while (
|
|
106
|
+
ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
|
|
163
107
|
output[bytes_read] = '\0';
|
|
164
108
|
}
|
|
165
109
|
}
|
|
@@ -198,6 +142,7 @@ int APIENTRY WinMain(
|
|
|
198
142
|
char exe_dir[MAX_PATH_LEN];
|
|
199
143
|
char cmd[MAX_PATH_LEN * 2];
|
|
200
144
|
char error_output[MAX_ERROR_LEN] = "";
|
|
145
|
+
char error_msg[MAX_ERROR_LEN];
|
|
201
146
|
STARTUPINFOA si = {0};
|
|
202
147
|
PROCESS_INFORMATION pi = {0};
|
|
203
148
|
SECURITY_ATTRIBUTES sa = {0};
|
|
@@ -221,33 +166,28 @@ int APIENTRY WinMain(
|
|
|
221
166
|
return 1;
|
|
222
167
|
}
|
|
223
168
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
sa.bInheritHandle = TRUE;
|
|
230
|
-
sa.lpSecurityDescriptor = NULL;
|
|
231
|
-
|
|
232
|
-
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
|
|
233
|
-
show_message_box("Error", "Failed to create pipe for error output.");
|
|
234
|
-
return 1;
|
|
235
|
-
}
|
|
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;
|
|
236
174
|
|
|
237
|
-
|
|
238
|
-
|
|
175
|
+
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
|
|
176
|
+
show_message_box("Error", "Failed to create pipe for error output.");
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
239
179
|
|
|
240
|
-
|
|
241
|
-
if (${DEBUG_MODE}) {
|
|
242
|
-
// Debug mode: do not use pipe, inherit console to display output
|
|
243
|
-
// Keep si.dwFlags as 0, do not set STARTF_USESTDHANDLES
|
|
244
|
-
} else {
|
|
245
|
-
// Production GUI mode: use pipe to capture error output
|
|
180
|
+
si.cb = sizeof(si);
|
|
246
181
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
247
182
|
si.hStdError = hWritePipe;
|
|
248
183
|
si.hStdOutput = hWritePipe;
|
|
184
|
+
} else {
|
|
185
|
+
si.cb = sizeof(si);
|
|
249
186
|
}
|
|
250
187
|
|
|
188
|
+
// Build and execute Python command
|
|
189
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);
|
|
190
|
+
|
|
251
191
|
// Create Python process
|
|
252
192
|
// Debug mode: do not use CREATE_NO_WINDOW, let python.exe create console
|
|
253
193
|
// Production GUI mode: use CREATE_NO_WINDOW to ensure no console is created
|
|
@@ -260,7 +200,6 @@ int APIENTRY WinMain(
|
|
|
260
200
|
|
|
261
201
|
if (!success) {
|
|
262
202
|
DWORD error = GetLastError();
|
|
263
|
-
char error_msg[MAX_ERROR_LEN];
|
|
264
203
|
snprintf(error_msg, MAX_ERROR_LEN,
|
|
265
204
|
"Failed to start Python process.\n\n"
|
|
266
205
|
"Error code: %lu\n"
|
|
@@ -288,14 +227,9 @@ int APIENTRY WinMain(
|
|
|
288
227
|
DWORD exit_code;
|
|
289
228
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
290
229
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
#ifdef _WIN32
|
|
294
|
-
fwprintf(stderr, L"DEBUG: Python process exited with code: %lu\n", exit_code);
|
|
295
|
-
#else
|
|
296
|
-
fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
|
|
230
|
+
#if ${DEBUG_MODE}
|
|
231
|
+
fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
|
|
297
232
|
#endif
|
|
298
|
-
}
|
|
299
233
|
|
|
300
234
|
// Cleanup
|
|
301
235
|
CloseHandle(pi.hProcess);
|
|
@@ -347,6 +281,9 @@ static void build_python_command(
|
|
|
347
281
|
) {
|
|
348
282
|
char python_runtime[MAX_PATH_LEN];
|
|
349
283
|
char script_path[MAX_PATH_LEN];
|
|
284
|
+
char* p = cmd;
|
|
285
|
+
size_t remaining = MAX_PATH_LEN * 2;
|
|
286
|
+
int len;
|
|
350
287
|
|
|
351
288
|
// Build Python interpreter path
|
|
352
289
|
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
|
|
@@ -354,21 +291,17 @@ static void build_python_command(
|
|
|
354
291
|
// Build startup script path
|
|
355
292
|
snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
|
|
356
293
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (i < argc - 1) {
|
|
369
|
-
strcat(cmd, " ");
|
|
370
|
-
}
|
|
371
|
-
}
|
|
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;
|
|
372
305
|
}
|
|
373
306
|
}
|
|
374
307
|
|
|
@@ -389,17 +322,16 @@ int main(int argc, char* argv[]) {
|
|
|
389
322
|
STARTUPINFOA si = {0};
|
|
390
323
|
PROCESS_INFORMATION pi = {0};
|
|
391
324
|
BOOL success;
|
|
325
|
+
char* last_slash;
|
|
392
326
|
|
|
393
|
-
//
|
|
327
|
+
// Setup
|
|
394
328
|
setup_encoding();
|
|
329
|
+
si.cb = sizeof(si);
|
|
395
330
|
|
|
396
331
|
// Get executable directory
|
|
397
332
|
GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
*last_slash = '\0';
|
|
401
|
-
}
|
|
402
|
-
|
|
333
|
+
if ((last_slash = strrchr(exe_dir, '\\'))) *last_slash = '\0';
|
|
334
|
+
`
|
|
403
335
|
// Check Python runtime
|
|
404
336
|
if (!check_python_runtime(exe_dir)) {
|
|
405
337
|
fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
|
|
@@ -407,50 +339,25 @@ int main(int argc, char* argv[]) {
|
|
|
407
339
|
return 1;
|
|
408
340
|
}
|
|
409
341
|
|
|
410
|
-
// Build Python command
|
|
342
|
+
// Build and execute Python command
|
|
411
343
|
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
412
344
|
|
|
345
|
+
#if ${DEBUG_MODE}
|
|
413
346
|
// Debug output
|
|
414
|
-
if (${DEBUG_MODE}) {
|
|
415
347
|
#ifdef _WIN32
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
fwprintf(stderr, L"DEBUG: Command to execute: %ls\n", wcmd);
|
|
424
|
-
free(wcmd);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Convert and print exe_dir
|
|
428
|
-
int wdir_len = MultiByteToWideChar(CP_UTF8, 0, exe_dir, -1, NULL, 0);
|
|
429
|
-
if (wdir_len > 0) {
|
|
430
|
-
wchar_t* wdir = (wchar_t*)malloc(wdir_len * sizeof(wchar_t));
|
|
431
|
-
MultiByteToWideChar(CP_UTF8, 0, exe_dir, -1, wdir, wdir_len);
|
|
432
|
-
fwprintf(stderr, L"DEBUG: exe_dir: %ls\n", wdir);
|
|
433
|
-
free(wdir);
|
|
434
|
-
}
|
|
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);
|
|
435
355
|
}
|
|
356
|
+
}
|
|
436
357
|
#else
|
|
437
|
-
|
|
438
|
-
|
|
358
|
+
fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
|
|
359
|
+
#endif
|
|
439
360
|
#endif
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Set startup info
|
|
443
|
-
si.cb = sizeof(si);
|
|
444
|
-
|
|
445
|
-
// For console applications, always inherit console handles to display output directly
|
|
446
|
-
// Don't use pipes in non-debug mode to allow all output to display in console
|
|
447
|
-
if (${DEBUG_MODE}) {
|
|
448
|
-
// Debug mode: do not use pipe, inherit console to display output directly
|
|
449
|
-
// Keep si.dwFlags as 0, do not set STARTF_USESTDHANDLES
|
|
450
|
-
} else {
|
|
451
|
-
// Production mode for console apps: inherit console handles to show ALL output in console
|
|
452
|
-
// Do not use pipes at all to ensure all stdout/stderr displays in console
|
|
453
|
-
}
|
|
454
361
|
|
|
455
362
|
// Create Python process
|
|
456
363
|
// TRUE - inherit handles so Python can write to console
|
|
@@ -477,10 +384,9 @@ int main(int argc, char* argv[]) {
|
|
|
477
384
|
DWORD exit_code;
|
|
478
385
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
479
386
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
387
|
+
#if ${DEBUG_MODE}
|
|
388
|
+
fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
|
|
389
|
+
#endif
|
|
484
390
|
|
|
485
391
|
// Cleanup
|
|
486
392
|
CloseHandle(pi.hProcess);
|
|
@@ -533,27 +439,23 @@ static void build_python_command(
|
|
|
533
439
|
|
|
534
440
|
// Unix GUI entry point
|
|
535
441
|
int main(int argc, char* argv[]) {
|
|
536
|
-
(void)argc;
|
|
442
|
+
(void)argc;
|
|
537
443
|
char exe_dir[MAX_PATH_LEN];
|
|
538
|
-
char cmd[MAX_PATH_LEN *
|
|
444
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
539
445
|
char log_file[MAX_PATH_LEN];
|
|
540
446
|
pid_t pid;
|
|
541
447
|
int status;
|
|
448
|
+
char* last_slash;
|
|
542
449
|
|
|
543
450
|
// Get executable directory
|
|
544
|
-
if (realpath("/proc/self/exe", exe_dir) == NULL
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return 1;
|
|
549
|
-
}
|
|
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;
|
|
550
455
|
}
|
|
551
456
|
|
|
552
457
|
// Remove executable name, keep only directory
|
|
553
|
-
|
|
554
|
-
if (last_slash) {
|
|
555
|
-
*last_slash = '\0';
|
|
556
|
-
}
|
|
458
|
+
if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';
|
|
557
459
|
|
|
558
460
|
// Build and execute Python command
|
|
559
461
|
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
@@ -623,12 +525,13 @@ static void build_python_command(
|
|
|
623
525
|
|
|
624
526
|
// Unix Console entry point
|
|
625
527
|
int main(int argc, char* argv[]) {
|
|
626
|
-
(void)argc;
|
|
627
|
-
(void)argv;
|
|
528
|
+
(void)argc;
|
|
529
|
+
(void)argv;
|
|
628
530
|
char exe_dir[MAX_PATH_LEN];
|
|
629
|
-
char cmd[MAX_PATH_LEN *
|
|
531
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
630
532
|
pid_t pid;
|
|
631
533
|
int status;
|
|
534
|
+
char* last_slash;
|
|
632
535
|
|
|
633
536
|
// Get executable directory
|
|
634
537
|
if (realpath("/proc/self/exe", exe_dir) == NULL) {
|
|
@@ -637,10 +540,7 @@ int main(int argc, char* argv[]) {
|
|
|
637
540
|
}
|
|
638
541
|
|
|
639
542
|
// Remove executable name, keep only directory
|
|
640
|
-
|
|
641
|
-
if (last_slash) {
|
|
642
|
-
*last_slash = '\0';
|
|
643
|
-
}
|
|
543
|
+
if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';
|
|
644
544
|
|
|
645
545
|
// Build and execute Python command
|
|
646
546
|
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
@@ -703,8 +603,10 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
703
603
|
CFRelease(url);
|
|
704
604
|
}
|
|
705
605
|
} else {
|
|
706
|
-
// If not in bundle,
|
|
707
|
-
|
|
606
|
+
// If not in bundle, get current working directory
|
|
607
|
+
if (getcwd(exe_dir, size) == NULL) {
|
|
608
|
+
exe_dir[0] = '\0';
|
|
609
|
+
}
|
|
708
610
|
}
|
|
709
611
|
|
|
710
612
|
// Remove Contents/MacOS suffix of bundle
|
|
@@ -757,11 +659,12 @@ static void build_python_command(
|
|
|
757
659
|
|
|
758
660
|
// macOS GUI entry point
|
|
759
661
|
int main(int argc, char* argv[]) {
|
|
760
|
-
(void)argc;
|
|
761
|
-
(void)argv;
|
|
662
|
+
(void)argc;
|
|
663
|
+
(void)argv;
|
|
762
664
|
char exe_dir[MAX_PATH_LEN];
|
|
763
|
-
char cmd[MAX_PATH_LEN *
|
|
665
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
764
666
|
char log_file[MAX_PATH_LEN];
|
|
667
|
+
char python_runtime[MAX_PATH_LEN];
|
|
765
668
|
pid_t pid;
|
|
766
669
|
int status;
|
|
767
670
|
|
|
@@ -769,10 +672,8 @@ int main(int argc, char* argv[]) {
|
|
|
769
672
|
get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
|
|
770
673
|
|
|
771
674
|
// Check Python runtime
|
|
772
|
-
char python_runtime[MAX_PATH_LEN];
|
|
773
675
|
snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
|
|
774
676
|
if (!check_python_runtime(python_runtime)) {
|
|
775
|
-
// macOS GUI app needs to use dialog to display error
|
|
776
677
|
char error_msg[MAX_PATH_LEN];
|
|
777
678
|
snprintf(error_msg, MAX_PATH_LEN,
|
|
778
679
|
"Python runtime not found.\n\n"
|
|
@@ -860,7 +761,10 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
860
761
|
CFRelease(url);
|
|
861
762
|
}
|
|
862
763
|
} else {
|
|
863
|
-
|
|
764
|
+
// Get current working directory if not in bundle
|
|
765
|
+
if (getcwd(exe_dir, size) == NULL) {
|
|
766
|
+
exe_dir[0] = '\0';
|
|
767
|
+
}
|
|
864
768
|
}
|
|
865
769
|
}
|
|
866
770
|
|
|
@@ -885,10 +789,11 @@ static void build_python_command(
|
|
|
885
789
|
|
|
886
790
|
// macOS Console entry point
|
|
887
791
|
int main(int argc, char* argv[]) {
|
|
888
|
-
(void)argc;
|
|
889
|
-
(void)argv;
|
|
792
|
+
(void)argc;
|
|
793
|
+
(void)argv;
|
|
890
794
|
char exe_dir[MAX_PATH_LEN];
|
|
891
|
-
char cmd[MAX_PATH_LEN *
|
|
795
|
+
char cmd[MAX_PATH_LEN * 3];
|
|
796
|
+
char python_runtime[MAX_PATH_LEN];
|
|
892
797
|
pid_t pid;
|
|
893
798
|
int status;
|
|
894
799
|
|
|
@@ -896,7 +801,6 @@ int main(int argc, char* argv[]) {
|
|
|
896
801
|
get_exe_dir_macos(exe_dir, MAX_PATH_LEN);
|
|
897
802
|
|
|
898
803
|
// Check Python runtime
|
|
899
|
-
char python_runtime[MAX_PATH_LEN];
|
|
900
804
|
snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
|
|
901
805
|
if (!check_python_runtime(python_runtime)) {
|
|
902
806
|
fprintf(stderr, "Error: Python runtime not found at %s/runtime/bin/\n", exe_dir);
|
|
@@ -959,7 +863,7 @@ if not hasattr(zipimport.zipimporter, 'exec_module'):
|
|
|
959
863
|
$QT_CONFIG
|
|
960
864
|
|
|
961
865
|
# Main entry point
|
|
962
|
-
from src.$PROJECT_NAME.$
|
|
866
|
+
from src.$PROJECT_NAME.$ENTRY_NAME import main
|
|
963
867
|
main()
|
|
964
868
|
"""
|
|
965
869
|
|
|
@@ -970,20 +874,6 @@ _COMPILER_CONFIGS: frozenset[CompilerConfig] = frozenset([
|
|
|
970
874
|
CompilerConfig(name="cl", args=("/std:c99", "/O2")),
|
|
971
875
|
])
|
|
972
876
|
|
|
973
|
-
# Qt-related keywords and dependencies for faster detection
|
|
974
|
-
_QT_DEPENDENCIES: frozenset[str] = frozenset((
|
|
975
|
-
"qt",
|
|
976
|
-
"pyside",
|
|
977
|
-
"pyqt",
|
|
978
|
-
"pyside2",
|
|
979
|
-
"pyside6",
|
|
980
|
-
"pyqt5",
|
|
981
|
-
"pyqt6",
|
|
982
|
-
"qt5",
|
|
983
|
-
"qt6",
|
|
984
|
-
))
|
|
985
|
-
_GUI_KEYWORDS: frozenset[str] = frozenset(("gui", "desktop"))
|
|
986
|
-
|
|
987
877
|
|
|
988
878
|
def find_compiler() -> str | None:
|
|
989
879
|
"""Find available C compiler."""
|
|
@@ -1003,7 +893,9 @@ def get_compiler_args(compiler: str) -> list[str]:
|
|
|
1003
893
|
Returns:
|
|
1004
894
|
List of compiler arguments
|
|
1005
895
|
"""
|
|
1006
|
-
compiler_name =
|
|
896
|
+
compiler_name = (
|
|
897
|
+
Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
|
|
898
|
+
)
|
|
1007
899
|
|
|
1008
900
|
for config in _COMPILER_CONFIGS:
|
|
1009
901
|
if config.name == compiler_name:
|
|
@@ -1012,72 +904,55 @@ def get_compiler_args(compiler: str) -> list[str]:
|
|
|
1012
904
|
return []
|
|
1013
905
|
|
|
1014
906
|
|
|
1015
|
-
def select_c_template(
|
|
1016
|
-
loader_type: str,
|
|
1017
|
-
is_debug: bool,
|
|
1018
|
-
) -> str:
|
|
907
|
+
def select_c_template(loader_type: str, debug: bool) -> str:
|
|
1019
908
|
"""Select the appropriate C code template based on platform and type.
|
|
1020
909
|
|
|
1021
910
|
In debug mode, always use console template to ensure output is visible.
|
|
1022
911
|
"""
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
if is_windows:
|
|
1026
|
-
return _WINDOWS_CONSOLE_TEMPLATE
|
|
1027
|
-
elif is_macos:
|
|
1028
|
-
return _MACOS_CONSOLE_TEMPLATE
|
|
1029
|
-
else:
|
|
1030
|
-
return _UNIX_CONSOLE_TEMPLATE
|
|
912
|
+
if debug:
|
|
913
|
+
loader_type = "console"
|
|
1031
914
|
|
|
1032
|
-
# In non-debug mode, use the requested template type
|
|
1033
915
|
if is_windows:
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return _WINDOWS_CONSOLE_TEMPLATE
|
|
916
|
+
return (
|
|
917
|
+
_WINDOWS_GUI_TEMPLATE if loader_type == "gui" else _WINDOWS_CONSOLE_TEMPLATE
|
|
918
|
+
)
|
|
1038
919
|
elif is_macos:
|
|
1039
|
-
if loader_type == "gui"
|
|
1040
|
-
|
|
1041
|
-
else
|
|
1042
|
-
return _MACOS_CONSOLE_TEMPLATE
|
|
1043
|
-
else: # Linux and other Unix-like systems
|
|
1044
|
-
if loader_type == "gui":
|
|
1045
|
-
return _UNIX_GUI_TEMPLATE
|
|
1046
|
-
else:
|
|
1047
|
-
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
|
|
1048
923
|
|
|
1049
924
|
|
|
1050
|
-
def prepare_entry_file(
|
|
925
|
+
def prepare_entry_file(project: Project, entry_stem: str) -> str:
|
|
1051
926
|
"""Generate entry file code by replacing placeholders in template.
|
|
1052
927
|
|
|
1053
928
|
Args:
|
|
929
|
+
project_name: Project name
|
|
1054
930
|
entry_stem: Entry file name without extension (e.g., "myapp" for "myapp.py" or "myapp.ent")
|
|
1055
931
|
is_qt: Whether this is a Qt application
|
|
1056
932
|
|
|
1057
933
|
Returns:
|
|
1058
934
|
Generated entry file code
|
|
1059
935
|
"""
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
if is_qt:
|
|
1064
|
-
qt_config = """# Qt configuration
|
|
1065
|
-
qt_dir = cwd / "site-packages" / "PySide2"
|
|
936
|
+
qt_config = (
|
|
937
|
+
"""# Qt configuration
|
|
938
|
+
qt_dir = cwd / "site-packages" / "{$QT_NAME}"
|
|
1066
939
|
plugin_path = str(qt_dir / "plugins" / "platforms")
|
|
1067
940
|
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
|
|
1068
941
|
|
|
1069
|
-
"""
|
|
1070
|
-
|
|
1071
|
-
|
|
942
|
+
""".replace("$QT_NAME", project.qt_libname or "")
|
|
943
|
+
if project.has_qt
|
|
944
|
+
else ""
|
|
945
|
+
)
|
|
1072
946
|
|
|
1073
|
-
return
|
|
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
|
+
)
|
|
1074
953
|
|
|
1075
954
|
|
|
1076
|
-
def prepare_c_source(
|
|
1077
|
-
template: str,
|
|
1078
|
-
entry_file: str,
|
|
1079
|
-
is_debug: bool,
|
|
1080
|
-
) -> str:
|
|
955
|
+
def prepare_c_source(template: str, entry_file: str, is_debug: bool) -> str:
|
|
1081
956
|
"""Generate C source code by replacing placeholders in template."""
|
|
1082
957
|
# Replace placeholders
|
|
1083
958
|
c_code = template.replace("${ENTRY_FILE}", entry_file)
|
|
@@ -1110,7 +985,9 @@ def compile_c_source(
|
|
|
1110
985
|
output_filepath = output_filepath.with_suffix(ext)
|
|
1111
986
|
|
|
1112
987
|
# Build compile command
|
|
1113
|
-
compiler_name =
|
|
988
|
+
compiler_name = (
|
|
989
|
+
Path(compiler).name if "\\" in compiler or "/" in compiler else compiler
|
|
990
|
+
)
|
|
1114
991
|
if compiler_name.lower() == "cl" or compiler_name.lower() == "cl.exe":
|
|
1115
992
|
# MSVC compiler
|
|
1116
993
|
cmd = [
|
|
@@ -1119,7 +996,9 @@ def compile_c_source(
|
|
|
1119
996
|
str(c_source_path),
|
|
1120
997
|
f"/Fe:{output_filepath}",
|
|
1121
998
|
"/link",
|
|
1122
|
-
"/SUBSYSTEM:WINDOWS"
|
|
999
|
+
"/SUBSYSTEM:WINDOWS"
|
|
1000
|
+
if "gui" in str(c_source_path).lower()
|
|
1001
|
+
else "/SUBSYSTEM:CONSOLE",
|
|
1123
1002
|
]
|
|
1124
1003
|
else:
|
|
1125
1004
|
# GCC/Clang compiler
|
|
@@ -1127,7 +1006,14 @@ def compile_c_source(
|
|
|
1127
1006
|
# For Windows GUI, add -mwindows flag with gcc/clang
|
|
1128
1007
|
if is_windows and "gui" in str(c_source_path).lower():
|
|
1129
1008
|
subsystem_flag = ["-mwindows"]
|
|
1130
|
-
cmd = [
|
|
1009
|
+
cmd = [
|
|
1010
|
+
compiler,
|
|
1011
|
+
*compiler_args,
|
|
1012
|
+
*subsystem_flag,
|
|
1013
|
+
"-o",
|
|
1014
|
+
str(output_filepath),
|
|
1015
|
+
str(c_source_path),
|
|
1016
|
+
]
|
|
1131
1017
|
|
|
1132
1018
|
logger.debug(f"Compiling with command: {' '.join(cmd)}")
|
|
1133
1019
|
try:
|
|
@@ -1147,120 +1033,6 @@ def compile_c_source(
|
|
|
1147
1033
|
return False
|
|
1148
1034
|
|
|
1149
1035
|
|
|
1150
|
-
def ensure_projects_json(directory: Path) -> Path | None:
|
|
1151
|
-
"""Ensure projects.json exists by running projectparse if needed.
|
|
1152
|
-
|
|
1153
|
-
Args:
|
|
1154
|
-
directory: Directory to check for projects.json
|
|
1155
|
-
|
|
1156
|
-
Returns:
|
|
1157
|
-
Path to projects.json if successful, None otherwise
|
|
1158
|
-
"""
|
|
1159
|
-
projects_json = directory / "projects.json"
|
|
1160
|
-
if projects_json.exists():
|
|
1161
|
-
logger.debug(f"Found existing projects.json at {projects_json}")
|
|
1162
|
-
return projects_json
|
|
1163
|
-
|
|
1164
|
-
logger.info("projects.json not found, running projectparse...")
|
|
1165
|
-
|
|
1166
|
-
try:
|
|
1167
|
-
import sys
|
|
1168
|
-
|
|
1169
|
-
from sfi.projectparse import projectparse as pp
|
|
1170
|
-
|
|
1171
|
-
original_argv = sys.argv.copy()
|
|
1172
|
-
sys.argv = ["projectparse", "--directory", str(directory), "--output", "projects.json", "--recursive"]
|
|
1173
|
-
try:
|
|
1174
|
-
pp.main()
|
|
1175
|
-
finally:
|
|
1176
|
-
sys.argv = original_argv
|
|
1177
|
-
|
|
1178
|
-
return projects_json if projects_json.exists() else None
|
|
1179
|
-
except ImportError:
|
|
1180
|
-
sfi_dir = Path(__file__).parent.parent.parent
|
|
1181
|
-
projectparse_script = sfi_dir / "projectparse" / "projectparse.py"
|
|
1182
|
-
|
|
1183
|
-
if not projectparse_script.exists():
|
|
1184
|
-
logger.error(f"Cannot find projectparse script at {projectparse_script}")
|
|
1185
|
-
return None
|
|
1186
|
-
|
|
1187
|
-
result = subprocess.run(
|
|
1188
|
-
[
|
|
1189
|
-
"python",
|
|
1190
|
-
str(projectparse_script),
|
|
1191
|
-
"--directory",
|
|
1192
|
-
str(directory),
|
|
1193
|
-
"--output",
|
|
1194
|
-
"projects.json",
|
|
1195
|
-
"--recursive",
|
|
1196
|
-
],
|
|
1197
|
-
capture_output=True,
|
|
1198
|
-
text=True,
|
|
1199
|
-
cwd=directory,
|
|
1200
|
-
)
|
|
1201
|
-
if result.returncode == 0 and projects_json.exists():
|
|
1202
|
-
logger.info("projectparse completed successfully")
|
|
1203
|
-
return projects_json
|
|
1204
|
-
logger.error(f"projectparse failed: {result.stderr}")
|
|
1205
|
-
return None
|
|
1206
|
-
except Exception as e:
|
|
1207
|
-
logger.error(f"Failed to run projectparse: {e}")
|
|
1208
|
-
return None
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
def load_projects_json(projects_json: Path) -> dict | None:
|
|
1212
|
-
"""Load project information from projects.json.
|
|
1213
|
-
|
|
1214
|
-
Args:
|
|
1215
|
-
projects_json: Path to projects.json file
|
|
1216
|
-
|
|
1217
|
-
Returns:
|
|
1218
|
-
Dictionary of project information, None if failed
|
|
1219
|
-
"""
|
|
1220
|
-
try:
|
|
1221
|
-
with open(projects_json, encoding="utf-8") as f:
|
|
1222
|
-
return json.load(f)
|
|
1223
|
-
except Exception as e:
|
|
1224
|
-
logger.error(f"Failed to load projects.json: {e}")
|
|
1225
|
-
return None
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
def _detect_loader_type(project_info: ProjectInfo) -> tuple[str, bool]:
|
|
1229
|
-
"""Detect loader type and Qt usage from project info.
|
|
1230
|
-
|
|
1231
|
-
Args:
|
|
1232
|
-
project_info: Project information dataclass
|
|
1233
|
-
|
|
1234
|
-
Returns:
|
|
1235
|
-
Tuple of (loader_type: str, use_qt: bool)
|
|
1236
|
-
"""
|
|
1237
|
-
# Convert dependencies and keywords to frozenset for O(1) lookup
|
|
1238
|
-
deps_lower = frozenset(dep.lower() for dep in project_info.dependencies)
|
|
1239
|
-
keywords_lower = frozenset(kw.lower() for kw in project_info.keywords)
|
|
1240
|
-
|
|
1241
|
-
# Check for Qt dependencies using set intersection (O(min(n,m)) instead of O(n*m))
|
|
1242
|
-
use_qt = bool(deps_lower.intersection(_QT_DEPENDENCIES))
|
|
1243
|
-
|
|
1244
|
-
# Determine loader type: GUI if has Qt dependencies or GUI keywords
|
|
1245
|
-
loader_type = "gui" if use_qt or bool(keywords_lower.intersection(_GUI_KEYWORDS)) else "console"
|
|
1246
|
-
return loader_type, use_qt
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
def _get_entry_module(project_info: ProjectInfo) -> str:
|
|
1250
|
-
"""Get entry module from project info.
|
|
1251
|
-
|
|
1252
|
-
Args:
|
|
1253
|
-
project_info: Project information dataclass
|
|
1254
|
-
|
|
1255
|
-
Returns:
|
|
1256
|
-
Entry module name
|
|
1257
|
-
"""
|
|
1258
|
-
if not project_info.scripts:
|
|
1259
|
-
return project_info.name.replace("-", "_")
|
|
1260
|
-
first_script = next(iter(project_info.scripts.values()))
|
|
1261
|
-
return first_script.split(":")[0].strip()
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
1036
|
def _is_entry_file(file_path: Path) -> bool:
|
|
1265
1037
|
"""Check if a Python file is an entry file.
|
|
1266
1038
|
|
|
@@ -1274,112 +1046,55 @@ def _is_entry_file(file_path: Path) -> bool:
|
|
|
1274
1046
|
Returns:
|
|
1275
1047
|
True if file is an entry file, False otherwise
|
|
1276
1048
|
"""
|
|
1277
|
-
if not file_path.
|
|
1049
|
+
if not file_path.is_file():
|
|
1278
1050
|
return False
|
|
1279
1051
|
|
|
1280
1052
|
try:
|
|
1281
1053
|
content = file_path.read_text(encoding="utf-8")
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1054
|
+
return (
|
|
1055
|
+
"def main(" in content
|
|
1056
|
+
or "if __name__ == '__main__':" in content
|
|
1057
|
+
or 'if __name__ == "__main__":' in content
|
|
1058
|
+
)
|
|
1287
1059
|
except Exception as e:
|
|
1288
1060
|
logger.debug(f"Failed to read {file_path}: {e}")
|
|
1289
1061
|
return False
|
|
1290
1062
|
|
|
1291
1063
|
|
|
1292
|
-
def _detect_entry_files(
|
|
1064
|
+
def _detect_entry_files(
|
|
1065
|
+
project_dir: Path,
|
|
1066
|
+
project_name: str,
|
|
1067
|
+
) -> list[EntryFile]:
|
|
1293
1068
|
"""Detect all entry files in project directory.
|
|
1294
1069
|
|
|
1295
1070
|
Args:
|
|
1296
1071
|
project_dir: Project directory
|
|
1297
1072
|
project_name: Project name
|
|
1298
|
-
loader_type: Base loader type from project info
|
|
1299
1073
|
|
|
1300
1074
|
Returns:
|
|
1301
1075
|
List of tuples (entry_file_name, module_name, is_gui)
|
|
1302
1076
|
e.g., [("docscan", "docscan", False), ("docscan-gui", "docscan_gui", True)]
|
|
1303
1077
|
"""
|
|
1304
1078
|
normalized_name = project_name.replace("-", "_")
|
|
1305
|
-
entry_files = []
|
|
1079
|
+
entry_files: list[EntryFile] = []
|
|
1306
1080
|
|
|
1307
1081
|
# Scan all Python files in project directory
|
|
1308
1082
|
for py_file in project_dir.glob("*.py"):
|
|
1309
1083
|
if not _is_entry_file(py_file):
|
|
1310
1084
|
continue
|
|
1311
1085
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
# Determine if this is a GUI entry based on file name
|
|
1316
|
-
is_gui_file = file_stem.endswith("_gui")
|
|
1317
|
-
|
|
1318
|
-
# Generate entry name (use hyphen for GUI version)
|
|
1319
|
-
if is_gui_file:
|
|
1320
|
-
# e.g., "docscan_gui" -> "docscan-gui"
|
|
1321
|
-
base_name = file_stem.replace("_gui", "")
|
|
1322
|
-
entry_name = f"{base_name}-gui"
|
|
1323
|
-
else:
|
|
1324
|
-
entry_name = file_stem
|
|
1325
|
-
|
|
1326
|
-
# Module name is just the file stem
|
|
1327
|
-
module_name = file_stem
|
|
1328
|
-
|
|
1329
|
-
entry_files.append((entry_name, module_name, is_gui_file))
|
|
1330
|
-
logger.debug(f"Found entry file: {py_file.name} -> {entry_name} (GUI: {is_gui_file})")
|
|
1331
|
-
|
|
1332
|
-
# If no entry files found, fallback to default
|
|
1333
|
-
if not entry_files:
|
|
1334
|
-
logger.warning(f"No entry files found in {project_dir}, using default: {normalized_name}.py")
|
|
1335
|
-
entry_files.append((project_name, normalized_name, False))
|
|
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}")
|
|
1336
1089
|
|
|
1337
1090
|
return entry_files
|
|
1338
1091
|
|
|
1339
1092
|
|
|
1340
|
-
def generate_loader(
|
|
1341
|
-
project_name: str,
|
|
1342
|
-
projects_json_path: Path,
|
|
1343
|
-
is_debug: bool = False,
|
|
1344
|
-
compiler: str | None = None,
|
|
1345
|
-
) -> bool:
|
|
1346
|
-
"""Generate loader executable and entry file from projects.json.
|
|
1347
|
-
|
|
1348
|
-
Args:
|
|
1349
|
-
project_name: Name of the project to generate loader for
|
|
1350
|
-
projects_json_path: Path to projects.json file
|
|
1351
|
-
is_debug: Whether to generate debug version
|
|
1352
|
-
compiler: Optional compiler to use
|
|
1353
|
-
|
|
1354
|
-
Returns:
|
|
1355
|
-
True if successful, False otherwise
|
|
1356
|
-
"""
|
|
1357
|
-
# Load projects.json
|
|
1358
|
-
projects_data = load_projects_json(projects_json_path)
|
|
1359
|
-
if not projects_data:
|
|
1360
|
-
logger.error("Failed to load projects.json")
|
|
1361
|
-
return False
|
|
1362
|
-
|
|
1363
|
-
# Check if project exists
|
|
1364
|
-
if project_name not in projects_data:
|
|
1365
|
-
logger.error(f"Project '{project_name}' not found in projects.json")
|
|
1366
|
-
return False
|
|
1367
|
-
|
|
1368
|
-
# Convert to ProjectInfo
|
|
1369
|
-
project_info = ProjectInfo.from_dict(project_name, projects_data[project_name])
|
|
1370
|
-
project_dir = projects_json_path.parent
|
|
1371
|
-
|
|
1372
|
-
return generate_loader_for_project(project_dir, project_info, is_debug, compiler)
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
1093
|
def _generate_single_loader(
|
|
1094
|
+
project: Project,
|
|
1376
1095
|
project_dir: Path,
|
|
1377
|
-
|
|
1378
|
-
entry_module: str,
|
|
1379
|
-
loader_type: str,
|
|
1380
|
-
use_qt: bool,
|
|
1096
|
+
entry_file: EntryFile,
|
|
1381
1097
|
is_debug: bool,
|
|
1382
|
-
compiler: str | None,
|
|
1383
1098
|
) -> bool:
|
|
1384
1099
|
"""Generate a single loader executable and entry file.
|
|
1385
1100
|
|
|
@@ -1395,6 +1110,8 @@ def _generate_single_loader(
|
|
|
1395
1110
|
Returns:
|
|
1396
1111
|
True if successful, False otherwise
|
|
1397
1112
|
"""
|
|
1113
|
+
entry_name = entry_file.entry_name
|
|
1114
|
+
|
|
1398
1115
|
# Prepare directories
|
|
1399
1116
|
build_dir = project_dir / ".cbuild"
|
|
1400
1117
|
output_dir = project_dir / "dist"
|
|
@@ -1403,7 +1120,11 @@ def _generate_single_loader(
|
|
|
1403
1120
|
|
|
1404
1121
|
# Output paths
|
|
1405
1122
|
output_exe = output_dir / f"{entry_name}{ext}"
|
|
1406
|
-
c_source_filename =
|
|
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
|
+
)
|
|
1407
1128
|
entry_filepath = output_dir / f"{entry_name}.ent"
|
|
1408
1129
|
c_source_path = build_dir / c_source_filename
|
|
1409
1130
|
|
|
@@ -1411,24 +1132,27 @@ def _generate_single_loader(
|
|
|
1411
1132
|
|
|
1412
1133
|
# Generate entry file
|
|
1413
1134
|
# Extract project name from entry_name (remove -gui suffix if present)
|
|
1414
|
-
|
|
1415
|
-
entry_file_code = prepare_entry_file(project_name, use_qt)
|
|
1416
|
-
entry_file_code = entry_file_code.replace("$ENTRY_FILE", entry_module)
|
|
1135
|
+
entry_file_code = prepare_entry_file(project=project, entry_stem=entry_name)
|
|
1417
1136
|
entry_filepath.write_text(entry_file_code, encoding="utf-8")
|
|
1418
|
-
logger.
|
|
1137
|
+
logger.debug(f"Generated entry file: {entry_filepath}")
|
|
1419
1138
|
|
|
1420
1139
|
# Generate and compile C source
|
|
1421
|
-
c_template = select_c_template(loader_type, is_debug)
|
|
1140
|
+
c_template = select_c_template(project.loader_type, is_debug)
|
|
1422
1141
|
c_code = prepare_c_source(c_template, entry_filepath.name, is_debug)
|
|
1423
1142
|
c_source_path.write_text(c_code, encoding="utf-8")
|
|
1424
|
-
logger.
|
|
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
|
|
1425
1149
|
|
|
1426
1150
|
success = compile_c_source(c_source_path, output_exe, compiler)
|
|
1427
1151
|
if success:
|
|
1428
1152
|
logger.info(f"Loader executable generated successfully: {output_exe}")
|
|
1429
|
-
logger.debug(f"Entry: {
|
|
1430
|
-
logger.debug(f"
|
|
1431
|
-
logger.debug(f"
|
|
1153
|
+
logger.debug(f"Entry: {entry_file}")
|
|
1154
|
+
logger.debug(f"Type: {project.loader_type}")
|
|
1155
|
+
logger.debug(f"Qt: {project.has_qt}")
|
|
1432
1156
|
logger.debug(f"Debug mode: {is_debug}")
|
|
1433
1157
|
logger.debug(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
|
|
1434
1158
|
else:
|
|
@@ -1438,144 +1162,119 @@ def _generate_single_loader(
|
|
|
1438
1162
|
|
|
1439
1163
|
|
|
1440
1164
|
def generate_loader_for_project(
|
|
1165
|
+
project: Project,
|
|
1441
1166
|
project_dir: Path,
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
compiler: str | None = None,
|
|
1445
|
-
) -> bool:
|
|
1167
|
+
debug: bool = False,
|
|
1168
|
+
):
|
|
1446
1169
|
"""Generate loader executables and entry files for a single project.
|
|
1447
1170
|
|
|
1448
1171
|
This function detects all entry files (main and GUI versions) and generates
|
|
1449
1172
|
a loader for each one.
|
|
1450
1173
|
|
|
1451
1174
|
Args:
|
|
1175
|
+
project: Project information dataclass
|
|
1452
1176
|
project_dir: Directory containing the project (pyproject.toml location)
|
|
1453
|
-
|
|
1454
|
-
is_debug: Whether to generate debug version
|
|
1455
|
-
compiler: Optional compiler to use
|
|
1456
|
-
|
|
1457
|
-
Returns:
|
|
1458
|
-
True if all loaders generated successfully, False otherwise
|
|
1177
|
+
debug: Whether to generate debug version
|
|
1459
1178
|
"""
|
|
1460
|
-
logger.info(f"\n{'=' * 60}")
|
|
1461
|
-
logger.info(f"Generating loaders for project: {project_info.name}")
|
|
1462
|
-
logger.info(f"{'=' * 60}")
|
|
1463
|
-
|
|
1464
1179
|
# Determine base loader type and Qt usage from project info
|
|
1465
|
-
base_loader_type, use_qt = _detect_loader_type(project_info)
|
|
1466
|
-
|
|
1467
1180
|
# Detect all entry files in project directory
|
|
1468
|
-
entry_files = _detect_entry_files(project_dir,
|
|
1469
|
-
|
|
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
|
|
1470
1185
|
|
|
1471
|
-
# Generate
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
|
1475
1196
|
|
|
1476
|
-
|
|
1477
|
-
loader_type = "gui" if is_gui else base_loader_type
|
|
1478
|
-
logger.debug(f"Loader type: {loader_type}")
|
|
1479
|
-
logger.debug(f"Use Qt: {use_qt}")
|
|
1480
|
-
logger.debug(f"Entry module: {entry_module}")
|
|
1197
|
+
return True
|
|
1481
1198
|
|
|
1482
|
-
success = _generate_single_loader(
|
|
1483
|
-
project_dir=project_dir,
|
|
1484
|
-
entry_name=entry_name,
|
|
1485
|
-
entry_module=entry_module,
|
|
1486
|
-
loader_type=loader_type,
|
|
1487
|
-
use_qt=use_qt,
|
|
1488
|
-
is_debug=is_debug,
|
|
1489
|
-
compiler=compiler,
|
|
1490
|
-
)
|
|
1491
1199
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
|
1495
1203
|
|
|
1496
|
-
|
|
1204
|
+
projects = Solution.projects
|
|
1205
|
+
if not projects:
|
|
1206
|
+
logger.error("Failed to load project information")
|
|
1497
1207
|
|
|
1208
|
+
success_count = 0
|
|
1209
|
+
failed_projects: list[str] = []
|
|
1498
1210
|
|
|
1499
|
-
|
|
1500
|
-
|
|
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")
|
|
1501
1240
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
+
)
|
|
1506
1249
|
parser.add_argument(
|
|
1507
|
-
"directory",
|
|
1250
|
+
"directory",
|
|
1251
|
+
nargs="?",
|
|
1252
|
+
default=str(cwd),
|
|
1253
|
+
help="Directory containing projects (will create/load projects.json)",
|
|
1508
1254
|
)
|
|
1509
1255
|
parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
|
|
1510
1256
|
parser.add_argument(
|
|
1511
|
-
"--compiler",
|
|
1257
|
+
"--compiler",
|
|
1258
|
+
help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe",
|
|
1512
1259
|
)
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
if args.debug:
|
|
1516
|
-
logger.setLevel(logging.DEBUG)
|
|
1517
|
-
|
|
1518
|
-
base_dir = Path(args.directory)
|
|
1519
|
-
if not base_dir.is_dir():
|
|
1520
|
-
logger.error(f"Directory does not exist: {base_dir}")
|
|
1521
|
-
return 1
|
|
1522
|
-
|
|
1523
|
-
logger.info(f"Working directory: {base_dir}")
|
|
1524
|
-
|
|
1525
|
-
# Ensure projects.json exists and load projects
|
|
1526
|
-
projects_json = ensure_projects_json(base_dir)
|
|
1527
|
-
if not projects_json:
|
|
1528
|
-
logger.error("Failed to create projects.json")
|
|
1529
|
-
return 1
|
|
1530
|
-
|
|
1531
|
-
projects_data = load_projects_json(projects_json)
|
|
1532
|
-
if not projects_data:
|
|
1533
|
-
logger.error("Failed to load project information")
|
|
1534
|
-
return 1
|
|
1535
|
-
|
|
1536
|
-
# Convert dict to ProjectInfo dataclasses
|
|
1537
|
-
projects: dict[str, ProjectInfo] = {name: ProjectInfo.from_dict(name, data) for name, data in projects_data.items()}
|
|
1538
|
-
|
|
1539
|
-
logger.info(f"Found {len(projects)} project(s) to process")
|
|
1540
|
-
|
|
1541
|
-
# Generate loaders
|
|
1542
|
-
total_start = time.perf_counter()
|
|
1543
|
-
success_count = 0
|
|
1544
|
-
failed_projects: list[str] = []
|
|
1545
|
-
use_current_dir = len(projects) == 1
|
|
1260
|
+
return parser.parse_args()
|
|
1546
1261
|
|
|
1547
|
-
if use_current_dir:
|
|
1548
|
-
logger.debug("Only one project found, using current directory")
|
|
1549
1262
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
logger.warning(f"Project directory not found: {project_dir}, skipping")
|
|
1554
|
-
failed_projects.append(project_name)
|
|
1555
|
-
continue
|
|
1263
|
+
def main():
|
|
1264
|
+
"""Main entry point for pyloadergen."""
|
|
1265
|
+
args = parse_args()
|
|
1556
1266
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
else:
|
|
1560
|
-
failed_projects.append(project_name)
|
|
1561
|
-
|
|
1562
|
-
total_time = time.perf_counter() - total_start
|
|
1267
|
+
if args.debug:
|
|
1268
|
+
logger.setLevel(logging.DEBUG)
|
|
1563
1269
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
logger.info(f"Total projects: {len(projects)}")
|
|
1569
|
-
logger.info(f"Successfully generated: {success_count}")
|
|
1570
|
-
logger.info(f"Failed: {len(failed_projects)}")
|
|
1571
|
-
if failed_projects:
|
|
1572
|
-
logger.info(f"Failed projects: {', '.join(failed_projects)}")
|
|
1573
|
-
logger.info(f"Total time: {total_time:.4f} seconds")
|
|
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
|
|
1574
1274
|
|
|
1575
|
-
|
|
1275
|
+
logger.info(f"Working directory: {working_dir}")
|
|
1276
|
+
generate_loader(working_dir, args.debug)
|
|
1576
1277
|
|
|
1577
1278
|
|
|
1578
1279
|
if __name__ == "__main__":
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
sys.exit(main())
|
|
1280
|
+
main()
|