pysfi 0.1.11__py3-none-any.whl → 0.1.13__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.11.dist-info → pysfi-0.1.13.dist-info}/METADATA +3 -1
- pysfi-0.1.13.dist-info/RECORD +70 -0
- {pysfi-0.1.11.dist-info → pysfi-0.1.13.dist-info}/entry_points.txt +3 -0
- sfi/__init__.py +5 -3
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +5 -3
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cli.py +12 -2
- sfi/condasetup/__init__.py +1 -0
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +238 -0
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan_gui.py +150 -46
- sfi/img2pdf/__init__.py +0 -0
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/__init__.py +0 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +39 -11
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +507 -124
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +571 -465
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +494 -965
- sfi/pyprojectparse/pyprojectparse.py +328 -28
- sfi/pysourcepack/__init__.py +1 -0
- sfi/pysourcepack/pysourcepack.py +210 -131
- sfi/quizbase/quizbase_gui.py +2 -2
- sfi/taskkill/taskkill.py +168 -59
- sfi/which/which.py +11 -3
- sfi/workflowengine/workflowengine.py +225 -122
- pysfi-0.1.11.dist-info/RECORD +0 -60
- {pysfi-0.1.11.dist-info → pysfi-0.1.13.dist-info}/WHEEL +0 -0
sfi/pyloadergen/pyloadergen.py
CHANGED
|
@@ -6,58 +6,28 @@ import platform
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
|
-
from
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from functools import cached_property
|
|
10
12
|
from pathlib import Path
|
|
13
|
+
from typing import Final
|
|
11
14
|
|
|
12
|
-
from sfi.pyprojectparse.pyprojectparse import Project
|
|
15
|
+
from sfi.pyprojectparse.pyprojectparse import Project, Solution
|
|
13
16
|
|
|
14
17
|
is_windows = platform.system() == "Windows"
|
|
15
18
|
is_linux = platform.system() == "Linux"
|
|
16
19
|
is_macos = platform.system() == "Darwin"
|
|
17
20
|
ext = ".exe" if is_windows else ""
|
|
18
21
|
|
|
19
|
-
logging.basicConfig(level=logging.INFO, format="%(
|
|
22
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
20
23
|
logger = logging.getLogger(__name__)
|
|
21
24
|
cwd = Path.cwd()
|
|
22
25
|
|
|
26
|
+
# Default cache directory
|
|
27
|
+
_DEFAULT_BUILD_DIR = Path(".cbuild")
|
|
28
|
+
_DEFAULT_OUTPUT_DIR = Path("dist")
|
|
23
29
|
|
|
24
|
-
|
|
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
|
-
|
|
30
|
+
# Platform templates
|
|
61
31
|
_WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
|
|
62
32
|
#include <stdio.h>
|
|
63
33
|
#include <stdlib.h>
|
|
@@ -84,17 +54,31 @@ static void build_python_command(
|
|
|
84
54
|
char script_path[MAX_PATH_LEN];
|
|
85
55
|
|
|
86
56
|
// Build Python interpreter path
|
|
87
|
-
|
|
57
|
+
// Debug mode: use python.exe to show console output
|
|
58
|
+
// Production GUI mode: use pythonw.exe to hide console window
|
|
59
|
+
if (is_debug) {
|
|
60
|
+
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
|
|
61
|
+
} else {
|
|
62
|
+
snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\pythonw.exe", exe_dir);
|
|
63
|
+
}
|
|
88
64
|
|
|
89
65
|
// Build startup script path
|
|
90
66
|
snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
|
|
91
67
|
|
|
92
|
-
// Build command line
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
68
|
+
// Build command line
|
|
69
|
+
if (is_debug) {
|
|
70
|
+
// Debug mode: don't redirect output, let output show in console
|
|
71
|
+
snprintf(cmd, MAX_PATH_LEN * 2, "\"%s\" -u \"%s\"%s%s",
|
|
72
|
+
python_runtime, script_path,
|
|
73
|
+
lpCmdLine && strlen(lpCmdLine) > 0 ? " " : "",
|
|
74
|
+
lpCmdLine && strlen(lpCmdLine) > 0 ? lpCmdLine : "");
|
|
75
|
+
} else {
|
|
76
|
+
// Production mode: redirect all output to pipe
|
|
77
|
+
snprintf(cmd, MAX_PATH_LEN * 2, "\"%s\" -u \"%s\"%s%s 2>&1",
|
|
78
|
+
python_runtime, script_path,
|
|
79
|
+
lpCmdLine && strlen(lpCmdLine) > 0 ? " " : "",
|
|
80
|
+
lpCmdLine && strlen(lpCmdLine) > 0 ? lpCmdLine : "");
|
|
81
|
+
}
|
|
98
82
|
}
|
|
99
83
|
|
|
100
84
|
// Read process output
|
|
@@ -166,6 +150,12 @@ int APIENTRY WinMain(
|
|
|
166
150
|
return 1;
|
|
167
151
|
}
|
|
168
152
|
|
|
153
|
+
// Build Python command
|
|
154
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);
|
|
155
|
+
|
|
156
|
+
// Setup startup info
|
|
157
|
+
si.cb = sizeof(si);
|
|
158
|
+
|
|
169
159
|
// Create pipe for capturing output (only in production mode)
|
|
170
160
|
if (!${DEBUG_MODE}) {
|
|
171
161
|
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
@@ -177,23 +167,17 @@ int APIENTRY WinMain(
|
|
|
177
167
|
return 1;
|
|
178
168
|
}
|
|
179
169
|
|
|
180
|
-
si.cb = sizeof(si);
|
|
181
170
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
182
171
|
si.hStdError = hWritePipe;
|
|
183
172
|
si.hStdOutput = hWritePipe;
|
|
184
|
-
} else {
|
|
185
|
-
si.cb = sizeof(si);
|
|
186
173
|
}
|
|
187
174
|
|
|
188
|
-
// Build and execute Python command
|
|
189
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);
|
|
190
|
-
|
|
191
175
|
// Create Python process
|
|
192
176
|
// Debug mode: do not use CREATE_NO_WINDOW, let python.exe create console
|
|
193
177
|
// Production GUI mode: use CREATE_NO_WINDOW to ensure no console is created
|
|
194
178
|
DWORD createFlags = ${DEBUG_MODE} ? 0 : CREATE_NO_WINDOW;
|
|
195
179
|
success = CreateProcessA(
|
|
196
|
-
NULL, cmd, NULL, NULL, FALSE,
|
|
180
|
+
NULL, cmd, NULL, NULL, ${DEBUG_MODE} ? TRUE : FALSE,
|
|
197
181
|
createFlags,
|
|
198
182
|
NULL, exe_dir, &si, &pi
|
|
199
183
|
);
|
|
@@ -331,7 +315,7 @@ int main(int argc, char* argv[]) {
|
|
|
331
315
|
// Get executable directory
|
|
332
316
|
GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
|
|
333
317
|
if ((last_slash = strrchr(exe_dir, '\\'))) *last_slash = '\0';
|
|
334
|
-
|
|
318
|
+
|
|
335
319
|
// Check Python runtime
|
|
336
320
|
if (!check_python_runtime(exe_dir)) {
|
|
337
321
|
fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
|
|
@@ -418,12 +402,17 @@ _UNIX_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
|
|
|
418
402
|
static void build_python_command(
|
|
419
403
|
char* cmd,
|
|
420
404
|
const char* exe_dir,
|
|
421
|
-
const char* entry_file
|
|
405
|
+
const char* entry_file,
|
|
406
|
+
int argc,
|
|
407
|
+
char* argv[]
|
|
422
408
|
) {
|
|
423
409
|
char script_path[MAX_PATH_LEN];
|
|
410
|
+
char* p = cmd;
|
|
411
|
+
size_t remaining = MAX_PATH_LEN * 3;
|
|
412
|
+
int len;
|
|
424
413
|
|
|
425
414
|
// Build startup script path
|
|
426
|
-
snprintf(script_path, MAX_PATH_LEN
|
|
415
|
+
snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
|
|
427
416
|
|
|
428
417
|
// Build log file path (record error information)
|
|
429
418
|
char log_file[MAX_PATH_LEN];
|
|
@@ -431,15 +420,28 @@ static void build_python_command(
|
|
|
431
420
|
|
|
432
421
|
// Build command line - use system python3 instead of bundled runtime
|
|
433
422
|
// GUI mode uses nohup and background execution, error output to log
|
|
434
|
-
snprintf(
|
|
435
|
-
"cd \"%s\" && nohup python3 -u \"%s\"
|
|
436
|
-
exe_dir, script_path
|
|
437
|
-
|
|
423
|
+
len = snprintf(p, remaining,
|
|
424
|
+
"cd \"%s\" && nohup python3 -u \"%s\"",
|
|
425
|
+
exe_dir, script_path);
|
|
426
|
+
p += len;
|
|
427
|
+
remaining -= len;
|
|
428
|
+
|
|
429
|
+
// Append command line arguments
|
|
430
|
+
for (int i = 1; i < argc && remaining > 0; i++) {
|
|
431
|
+
len = snprintf(p, remaining, " \"%s\"", argv[i]);
|
|
432
|
+
if (len < 0 || (size_t)len >= remaining) break;
|
|
433
|
+
p += len;
|
|
434
|
+
remaining -= len;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Append log redirection
|
|
438
|
+
if (remaining > 0) {
|
|
439
|
+
snprintf(p, remaining, " >\"%s\" 2>&1 &", log_file);
|
|
440
|
+
}
|
|
438
441
|
}
|
|
439
442
|
|
|
440
443
|
// Unix GUI entry point
|
|
441
444
|
int main(int argc, char* argv[]) {
|
|
442
|
-
(void)argc;
|
|
443
445
|
char exe_dir[MAX_PATH_LEN];
|
|
444
446
|
char cmd[MAX_PATH_LEN * 3];
|
|
445
447
|
char log_file[MAX_PATH_LEN];
|
|
@@ -458,7 +460,7 @@ int main(int argc, char* argv[]) {
|
|
|
458
460
|
if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';
|
|
459
461
|
|
|
460
462
|
// Build and execute Python command
|
|
461
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
463
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
462
464
|
snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
|
|
463
465
|
|
|
464
466
|
// Execute Python process in background
|
|
@@ -508,25 +510,37 @@ _UNIX_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
|
|
|
508
510
|
static void build_python_command(
|
|
509
511
|
char* cmd,
|
|
510
512
|
const char* exe_dir,
|
|
511
|
-
const char* entry_file
|
|
513
|
+
const char* entry_file,
|
|
514
|
+
int argc,
|
|
515
|
+
char* argv[]
|
|
512
516
|
) {
|
|
513
517
|
char script_path[MAX_PATH_LEN];
|
|
518
|
+
char* p = cmd;
|
|
519
|
+
size_t remaining = MAX_PATH_LEN * 2;
|
|
520
|
+
int len;
|
|
514
521
|
|
|
515
522
|
// Build startup script path
|
|
516
523
|
snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
|
|
517
524
|
|
|
518
525
|
// Build command line - use system python3 instead of bundled runtime
|
|
519
526
|
// add -u parameter for real-time output capture
|
|
520
|
-
snprintf(
|
|
527
|
+
len = snprintf(p, remaining,
|
|
521
528
|
"cd \"%s\" && python3 -u \"%s\"",
|
|
522
|
-
exe_dir, script_path
|
|
523
|
-
|
|
529
|
+
exe_dir, script_path);
|
|
530
|
+
p += len;
|
|
531
|
+
remaining -= len;
|
|
532
|
+
|
|
533
|
+
// Append command line arguments
|
|
534
|
+
for (int i = 1; i < argc && remaining > 0; i++) {
|
|
535
|
+
len = snprintf(p, remaining, " \"%s\"", argv[i]);
|
|
536
|
+
if (len < 0 || (size_t)len >= remaining) break;
|
|
537
|
+
p += len;
|
|
538
|
+
remaining -= len;
|
|
539
|
+
}
|
|
524
540
|
}
|
|
525
541
|
|
|
526
542
|
// Unix Console entry point
|
|
527
543
|
int main(int argc, char* argv[]) {
|
|
528
|
-
(void)argc;
|
|
529
|
-
(void)argv;
|
|
530
544
|
char exe_dir[MAX_PATH_LEN];
|
|
531
545
|
char cmd[MAX_PATH_LEN * 3];
|
|
532
546
|
pid_t pid;
|
|
@@ -543,7 +557,7 @@ int main(int argc, char* argv[]) {
|
|
|
543
557
|
if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';
|
|
544
558
|
|
|
545
559
|
// Build and execute Python command
|
|
546
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
560
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
547
561
|
|
|
548
562
|
// Fork and execute Python process
|
|
549
563
|
pid = fork();
|
|
@@ -637,11 +651,16 @@ static void show_error_dialog(const char* message) {
|
|
|
637
651
|
static void build_python_command(
|
|
638
652
|
char* cmd,
|
|
639
653
|
const char* exe_dir,
|
|
640
|
-
const char* entry_file
|
|
654
|
+
const char* entry_file,
|
|
655
|
+
int argc,
|
|
656
|
+
char* argv[]
|
|
641
657
|
) {
|
|
642
658
|
char python_runtime[MAX_PATH_LEN];
|
|
643
659
|
char script_path[MAX_PATH_LEN];
|
|
644
660
|
char log_file[MAX_PATH_LEN];
|
|
661
|
+
char* p = cmd;
|
|
662
|
+
size_t remaining = MAX_PATH_LEN * 2;
|
|
663
|
+
int len;
|
|
645
664
|
|
|
646
665
|
// macOS Python path
|
|
647
666
|
snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
|
|
@@ -651,16 +670,28 @@ static void build_python_command(
|
|
|
651
670
|
snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
|
|
652
671
|
|
|
653
672
|
// Build command line (GUI mode uses nohup and background execution, error output to log)
|
|
654
|
-
snprintf(
|
|
655
|
-
"cd \"%s\" && nohup \"%s\" -u \"%s\"
|
|
656
|
-
exe_dir, python_runtime, script_path
|
|
657
|
-
|
|
673
|
+
len = snprintf(p, remaining,
|
|
674
|
+
"cd \"%s\" && nohup \"%s\" -u \"%s\"",
|
|
675
|
+
exe_dir, python_runtime, script_path);
|
|
676
|
+
p += len;
|
|
677
|
+
remaining -= len;
|
|
678
|
+
|
|
679
|
+
// Append command line arguments
|
|
680
|
+
for (int i = 1; i < argc && remaining > 0; i++) {
|
|
681
|
+
len = snprintf(p, remaining, " \"%s\"", argv[i]);
|
|
682
|
+
if (len < 0 || (size_t)len >= remaining) break;
|
|
683
|
+
p += len;
|
|
684
|
+
remaining -= len;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Append log redirection
|
|
688
|
+
if (remaining > 0) {
|
|
689
|
+
snprintf(p, remaining, " >\"%s\" 2>&1 &", log_file);
|
|
690
|
+
}
|
|
658
691
|
}
|
|
659
692
|
|
|
660
693
|
// macOS GUI entry point
|
|
661
694
|
int main(int argc, char* argv[]) {
|
|
662
|
-
(void)argc;
|
|
663
|
-
(void)argv;
|
|
664
695
|
char exe_dir[MAX_PATH_LEN];
|
|
665
696
|
char cmd[MAX_PATH_LEN * 3];
|
|
666
697
|
char log_file[MAX_PATH_LEN];
|
|
@@ -684,7 +715,7 @@ int main(int argc, char* argv[]) {
|
|
|
684
715
|
}
|
|
685
716
|
|
|
686
717
|
// Build and execute Python command
|
|
687
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
718
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
688
719
|
snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);
|
|
689
720
|
|
|
690
721
|
// Execute Python process in background
|
|
@@ -738,6 +769,8 @@ _MACOS_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
|
|
|
738
769
|
#include <unistd.h>
|
|
739
770
|
#include <sys/stat.h>
|
|
740
771
|
#include <sys/wait.h>
|
|
772
|
+
#include <errno.h>
|
|
773
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
741
774
|
|
|
742
775
|
#define MAX_PATH_LEN 4096
|
|
743
776
|
|
|
@@ -772,25 +805,37 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
772
805
|
static void build_python_command(
|
|
773
806
|
char* cmd,
|
|
774
807
|
const char* exe_dir,
|
|
775
|
-
const char* entry_file
|
|
808
|
+
const char* entry_file,
|
|
809
|
+
int argc,
|
|
810
|
+
char* argv[]
|
|
776
811
|
) {
|
|
777
812
|
char python_runtime[MAX_PATH_LEN];
|
|
778
813
|
char script_path[MAX_PATH_LEN];
|
|
814
|
+
char* p = cmd;
|
|
815
|
+
size_t remaining = MAX_PATH_LEN;
|
|
816
|
+
int len;
|
|
779
817
|
|
|
780
818
|
snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
|
|
781
819
|
snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);
|
|
782
820
|
|
|
783
821
|
// Build command line (add -u parameter for real-time output capture)
|
|
784
|
-
snprintf(
|
|
822
|
+
len = snprintf(p, remaining,
|
|
785
823
|
"cd \"%s\" && \"%s\" -u \"%s\"",
|
|
786
|
-
exe_dir, python_runtime, script_path
|
|
787
|
-
|
|
824
|
+
exe_dir, python_runtime, script_path);
|
|
825
|
+
p += len;
|
|
826
|
+
remaining -= len;
|
|
827
|
+
|
|
828
|
+
// Append command line arguments
|
|
829
|
+
for (int i = 1; i < argc && remaining > 0; i++) {
|
|
830
|
+
len = snprintf(p, remaining, " \"%s\"", argv[i]);
|
|
831
|
+
if (len < 0 || (size_t)len >= remaining) break;
|
|
832
|
+
p += len;
|
|
833
|
+
remaining -= len;
|
|
834
|
+
}
|
|
788
835
|
}
|
|
789
836
|
|
|
790
837
|
// macOS Console entry point
|
|
791
838
|
int main(int argc, char* argv[]) {
|
|
792
|
-
(void)argc;
|
|
793
|
-
(void)argv;
|
|
794
839
|
char exe_dir[MAX_PATH_LEN];
|
|
795
840
|
char cmd[MAX_PATH_LEN * 3];
|
|
796
841
|
char python_runtime[MAX_PATH_LEN];
|
|
@@ -809,7 +854,7 @@ int main(int argc, char* argv[]) {
|
|
|
809
854
|
}
|
|
810
855
|
|
|
811
856
|
// Build and execute Python command
|
|
812
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
|
|
857
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
813
858
|
|
|
814
859
|
// Fork and execute Python process
|
|
815
860
|
pid = fork();
|
|
@@ -867,12 +912,76 @@ from src.$PROJECT_NAME.$ENTRY_NAME import main
|
|
|
867
912
|
main()
|
|
868
913
|
"""
|
|
869
914
|
|
|
870
|
-
|
|
871
|
-
|
|
915
|
+
|
|
916
|
+
@dataclass(frozen=True)
|
|
917
|
+
class CompilerConfig:
|
|
918
|
+
"""Compiler configuration dataclass.
|
|
919
|
+
|
|
920
|
+
Attributes:
|
|
921
|
+
name: Compiler name
|
|
922
|
+
args: Tuple of compiler arguments
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
name: str
|
|
926
|
+
args: tuple[str, ...]
|
|
927
|
+
|
|
928
|
+
def to_list(self) -> list[str]:
|
|
929
|
+
"""Convert arguments to list.
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
List of compiler arguments
|
|
933
|
+
"""
|
|
934
|
+
return list(self.args)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
@dataclass(frozen=True)
|
|
938
|
+
class EntryFile:
|
|
939
|
+
"""Entry file dataclass with enhanced functionality."""
|
|
940
|
+
|
|
941
|
+
project_name: str
|
|
942
|
+
source_file: Path
|
|
943
|
+
|
|
944
|
+
@cached_property
|
|
945
|
+
def entry_name(self) -> str:
|
|
946
|
+
"""Get entry file name."""
|
|
947
|
+
return self.source_file.stem
|
|
948
|
+
|
|
949
|
+
@cached_property
|
|
950
|
+
def module_name(self) -> str:
|
|
951
|
+
"""Get module name from source file stem."""
|
|
952
|
+
return self.source_file.stem.replace("-", "_")
|
|
953
|
+
|
|
954
|
+
@cached_property
|
|
955
|
+
def is_gui(self) -> bool:
|
|
956
|
+
"""Determine if this entry file is a GUI application.
|
|
957
|
+
|
|
958
|
+
Entry file is considered GUI if:
|
|
959
|
+
1. File name contains 'gui' (e.g., docscan_gui.py, myapp-gui.py)
|
|
960
|
+
2. File imports GUI frameworks (PySide2, PyQt5, PyQt6, tkinter)
|
|
961
|
+
"""
|
|
962
|
+
# Check file name for 'gui' keyword
|
|
963
|
+
if "gui" in self.source_file.stem.lower():
|
|
964
|
+
return True
|
|
965
|
+
|
|
966
|
+
# Check file content for GUI framework imports
|
|
967
|
+
try:
|
|
968
|
+
content = self.source_file.read_text(encoding="utf-8")
|
|
969
|
+
gui_keywords = ["pyside2", "pyqt5", "pyqt6", "tkinter", "tkinter.ttk"]
|
|
970
|
+
return any(f"from {kw}" in content.lower() or f"import {kw}" in content.lower()
|
|
971
|
+
for kw in gui_keywords)
|
|
972
|
+
except Exception:
|
|
973
|
+
return False
|
|
974
|
+
|
|
975
|
+
def __repr__(self) -> str:
|
|
976
|
+
return f"<EntryFile project_name={self.project_name} source_file={self.source_file} entry_name={self.entry_name} is_gui={self.is_gui}>"
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
# Compiler configurations
|
|
980
|
+
_COMPILER_CONFIGS: Final = [
|
|
872
981
|
CompilerConfig(name="gcc", args=("-std=c99", "-Wall", "-O2")),
|
|
873
982
|
CompilerConfig(name="clang", args=("-std=c99", "-Wall", "-O2")),
|
|
874
983
|
CompilerConfig(name="cl", args=("/std:c99", "/O2")),
|
|
875
|
-
]
|
|
984
|
+
]
|
|
876
985
|
|
|
877
986
|
|
|
878
987
|
def find_compiler() -> str | None:
|
|
@@ -1090,156 +1199,199 @@ def _detect_entry_files(
|
|
|
1090
1199
|
return entry_files
|
|
1091
1200
|
|
|
1092
1201
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
entry_file: EntryFile,
|
|
1097
|
-
is_debug: bool,
|
|
1098
|
-
) -> bool:
|
|
1099
|
-
"""Generate a single loader executable and entry file.
|
|
1202
|
+
@dataclass
|
|
1203
|
+
class PyLoaderBuilder:
|
|
1204
|
+
"""Helper class to build individual loaders."""
|
|
1100
1205
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
|
1206
|
+
parent: PyLoaderGenerator
|
|
1207
|
+
project: Project
|
|
1208
|
+
entry_file: EntryFile
|
|
1209
|
+
is_debug: bool = False
|
|
1109
1210
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
|
1130
|
-
|
|
1131
|
-
t0 = time.perf_counter()
|
|
1132
|
-
|
|
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
|
|
1211
|
+
@cached_property
|
|
1212
|
+
def build_dir(self) -> Path:
|
|
1213
|
+
"""Build directory path."""
|
|
1214
|
+
return self.parent.root_dir / _DEFAULT_BUILD_DIR
|
|
1149
1215
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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")
|
|
1158
|
-
else:
|
|
1159
|
-
logger.error(f"Failed to compile loader for {entry_name}")
|
|
1216
|
+
@cached_property
|
|
1217
|
+
def output_dir(self) -> Path:
|
|
1218
|
+
"""Output directory path."""
|
|
1219
|
+
return self.parent.root_dir / _DEFAULT_OUTPUT_DIR
|
|
1160
1220
|
|
|
1161
|
-
|
|
1221
|
+
@cached_property
|
|
1222
|
+
def output_exe(self) -> Path:
|
|
1223
|
+
"""Output executable path."""
|
|
1224
|
+
return self.output_dir / f"{self.entry_file.entry_name}{ext}"
|
|
1162
1225
|
|
|
1226
|
+
@cached_property
|
|
1227
|
+
def loader_type(self) -> str:
|
|
1228
|
+
"""Determine loader type based on entry file.
|
|
1163
1229
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1230
|
+
Entry file is GUI if:
|
|
1231
|
+
- The entry file itself is detected as GUI (contains 'gui' in name or imports GUI libs)
|
|
1232
|
+
- Otherwise, fall back to project-level loader_type
|
|
1233
|
+
"""
|
|
1234
|
+
return "gui" if self.entry_file.is_gui else self.project.loader_type
|
|
1235
|
+
|
|
1236
|
+
@cached_property
|
|
1237
|
+
def c_source_path(self) -> Path:
|
|
1238
|
+
"""C source file path."""
|
|
1239
|
+
filename = (
|
|
1240
|
+
f"{self.entry_file.entry_name}_debug_console.c"
|
|
1241
|
+
if self.is_debug
|
|
1242
|
+
else f"{self.entry_file.entry_name}_{self.loader_type}.c"
|
|
1243
|
+
)
|
|
1244
|
+
return self.build_dir / filename
|
|
1170
1245
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1246
|
+
@cached_property
|
|
1247
|
+
def entry_filepath(self) -> Path:
|
|
1248
|
+
"""Entry file path."""
|
|
1249
|
+
return self.output_dir / f"{self.entry_file.entry_name}.ent"
|
|
1173
1250
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1251
|
+
def generate(self) -> bool:
|
|
1252
|
+
"""Generate loader for a single entry file."""
|
|
1253
|
+
t0 = time.perf_counter()
|
|
1254
|
+
|
|
1255
|
+
# Prepare directories
|
|
1256
|
+
self.build_dir.mkdir(parents=True, exist_ok=True)
|
|
1257
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
1258
|
+
|
|
1259
|
+
# Generate entry file
|
|
1260
|
+
entry_file_code = prepare_entry_file(
|
|
1261
|
+
project=self.project, entry_stem=self.entry_file.entry_name
|
|
1262
|
+
)
|
|
1263
|
+
self.entry_filepath.write_text(entry_file_code, encoding="utf-8")
|
|
1264
|
+
logger.debug(f"Generated entry file: {self.entry_filepath}")
|
|
1265
|
+
|
|
1266
|
+
# Generate and compile C source
|
|
1267
|
+
c_template = select_c_template(self.loader_type, self.is_debug)
|
|
1268
|
+
c_code = prepare_c_source(c_template, self.entry_filepath.name, self.is_debug)
|
|
1269
|
+
self.c_source_path.write_text(c_code, encoding="utf-8")
|
|
1270
|
+
logger.debug(f"Generated C source code: {self.c_source_path}")
|
|
1185
1271
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if not _generate_single_loader(
|
|
1190
|
-
project=project,
|
|
1191
|
-
project_dir=project_dir,
|
|
1192
|
-
entry_file=entry_file,
|
|
1193
|
-
is_debug=debug,
|
|
1194
|
-
):
|
|
1272
|
+
compiler = find_compiler()
|
|
1273
|
+
if not compiler:
|
|
1274
|
+
logger.error(f"Failed to find compiler: {compiler}")
|
|
1195
1275
|
return False
|
|
1196
1276
|
|
|
1197
|
-
|
|
1277
|
+
success = compile_c_source(self.c_source_path, self.output_exe, compiler)
|
|
1278
|
+
if success:
|
|
1279
|
+
logger.info(f"Loader executable generated successfully: {self.output_exe}")
|
|
1280
|
+
logger.debug(f"Entry: {self.entry_file}")
|
|
1281
|
+
logger.debug(f"Type: {self.loader_type}")
|
|
1282
|
+
logger.debug(f"Qt: {self.project.has_qt}")
|
|
1283
|
+
logger.debug(f"Debug mode: {self.is_debug}")
|
|
1284
|
+
logger.debug(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
|
|
1285
|
+
else:
|
|
1286
|
+
logger.error(f"Failed to compile loader for {self.entry_file.entry_name}")
|
|
1198
1287
|
|
|
1288
|
+
return success
|
|
1199
1289
|
|
|
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
1290
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1291
|
+
@dataclass
|
|
1292
|
+
class PyLoaderGenerator:
|
|
1293
|
+
"""Main class for generating Python loader executables."""
|
|
1207
1294
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1295
|
+
root_dir: Path
|
|
1296
|
+
build_dir: Path = field(default_factory=lambda: _DEFAULT_BUILD_DIR)
|
|
1297
|
+
output_dir: Path = field(default_factory=lambda: _DEFAULT_OUTPUT_DIR)
|
|
1298
|
+
compiler: str | None = None
|
|
1210
1299
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1300
|
+
@cached_property
|
|
1301
|
+
def solution(self) -> Solution:
|
|
1302
|
+
"""Get the solution from the target directory."""
|
|
1303
|
+
return Solution.from_directory(self.root_dir)
|
|
1304
|
+
|
|
1305
|
+
def generate_for_project(
|
|
1306
|
+
self, project: Project, project_dir: Path, debug: bool = False
|
|
1307
|
+
) -> bool:
|
|
1308
|
+
"""Generate loader executables and entry files for a single project.
|
|
1309
|
+
|
|
1310
|
+
This function detects all entry files (main and GUI versions) and generates
|
|
1311
|
+
a loader for each one.
|
|
1312
|
+
|
|
1313
|
+
Args:
|
|
1314
|
+
project: Project information dataclass
|
|
1315
|
+
project_dir: Directory containing the project (pyproject.toml location)
|
|
1316
|
+
debug: Whether to generate debug version
|
|
1317
|
+
"""
|
|
1318
|
+
# Detect all entry files in project directory
|
|
1319
|
+
entry_files = _detect_entry_files(project_dir, project.name)
|
|
1320
|
+
if not entry_files:
|
|
1321
|
+
logger.error(f"No entry files found in {project_dir}")
|
|
1322
|
+
return False
|
|
1323
|
+
|
|
1324
|
+
# Generate loaders for all entry files
|
|
1325
|
+
for entry_file in entry_files:
|
|
1326
|
+
logger.debug(f"Generating loader for: {entry_file.project_name}")
|
|
1327
|
+
builder = PyLoaderBuilder(
|
|
1328
|
+
parent=self, project=project, entry_file=entry_file, is_debug=debug
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
if not builder.generate():
|
|
1332
|
+
return False
|
|
1333
|
+
|
|
1334
|
+
return True
|
|
1335
|
+
|
|
1336
|
+
def run(self, debug: bool = False) -> None:
|
|
1337
|
+
"""Generate loader executables for all projects concurrently."""
|
|
1338
|
+
projects = self.solution.projects
|
|
1339
|
+
if not projects:
|
|
1340
|
+
logger.error("Failed to load project information")
|
|
1341
|
+
return
|
|
1342
|
+
|
|
1343
|
+
success_count = 0
|
|
1344
|
+
failed_projects: list[str] = []
|
|
1345
|
+
|
|
1346
|
+
if len(projects) == 1:
|
|
1347
|
+
logger.debug("Only one project found, using current directory")
|
|
1348
|
+
project_dir = self.root_dir
|
|
1349
|
+
if self.generate_for_project(
|
|
1350
|
+
project=next(iter(projects.values())),
|
|
1232
1351
|
project_dir=project_dir,
|
|
1233
1352
|
debug=debug,
|
|
1234
1353
|
):
|
|
1235
1354
|
success_count += 1
|
|
1236
1355
|
else:
|
|
1237
|
-
failed_projects.append(
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1356
|
+
failed_projects.append(next(iter(projects)))
|
|
1357
|
+
else:
|
|
1358
|
+
# Prepare project tasks
|
|
1359
|
+
project_tasks = []
|
|
1360
|
+
for project_name, project in projects.items():
|
|
1361
|
+
project_dir = self.root_dir / project_name
|
|
1362
|
+
if not project_dir.is_dir():
|
|
1363
|
+
logger.error(
|
|
1364
|
+
f"Project directory not found: {project_dir}, skipping..."
|
|
1365
|
+
)
|
|
1366
|
+
failed_projects.append(project_name)
|
|
1367
|
+
continue
|
|
1368
|
+
project_tasks.append((project_name, project, project_dir))
|
|
1369
|
+
|
|
1370
|
+
# Execute compilation tasks concurrently
|
|
1371
|
+
logger.info(f"Compiling {len(project_tasks)} projects concurrently...")
|
|
1372
|
+
with ThreadPoolExecutor(max_workers=None) as executor:
|
|
1373
|
+
future_to_project = {
|
|
1374
|
+
executor.submit(
|
|
1375
|
+
self.generate_for_project, project, project_dir, debug
|
|
1376
|
+
): project_name
|
|
1377
|
+
for project_name, project, project_dir in project_tasks
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
for future in as_completed(future_to_project):
|
|
1381
|
+
project_name = future_to_project[future]
|
|
1382
|
+
try:
|
|
1383
|
+
if future.result():
|
|
1384
|
+
success_count += 1
|
|
1385
|
+
else:
|
|
1386
|
+
failed_projects.append(project_name)
|
|
1387
|
+
except Exception as e:
|
|
1388
|
+
logger.error(f"Project {project_name} compilation failed: {e}")
|
|
1389
|
+
failed_projects.append(project_name)
|
|
1390
|
+
|
|
1391
|
+
logger.info(f"Pack {success_count}/{len(projects)} projects successfully")
|
|
1392
|
+
|
|
1393
|
+
if failed_projects:
|
|
1394
|
+
logger.error(f"Failed: {failed_projects}")
|
|
1243
1395
|
|
|
1244
1396
|
|
|
1245
1397
|
def parse_args() -> argparse.Namespace:
|
|
@@ -1273,7 +1425,9 @@ def main():
|
|
|
1273
1425
|
return
|
|
1274
1426
|
|
|
1275
1427
|
logger.info(f"Working directory: {working_dir}")
|
|
1276
|
-
|
|
1428
|
+
|
|
1429
|
+
generator = PyLoaderGenerator(root_dir=working_dir, compiler=args.compiler)
|
|
1430
|
+
generator.run(debug=args.debug)
|
|
1277
1431
|
|
|
1278
1432
|
|
|
1279
1433
|
if __name__ == "__main__":
|