pysfi 0.1.6__py3-none-any.whl → 0.1.10__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.6.dist-info → pysfi-0.1.10.dist-info}/METADATA +28 -3
- pysfi-0.1.10.dist-info/RECORD +39 -0
- {pysfi-0.1.6.dist-info → pysfi-0.1.10.dist-info}/entry_points.txt +7 -1
- sfi/__init__.py +1 -1
- sfi/bumpversion/__init__.py +1 -1
- sfi/cli.py +11 -0
- sfi/docscan/__init__.py +3 -0
- sfi/docscan/docscan.py +1145 -0
- sfi/docscan/docscan_gui.py +1282 -0
- sfi/docscan/lang/__init__.py +0 -0
- sfi/docscan/lang/eng.py +152 -0
- sfi/docscan/lang/zhcn.py +170 -0
- sfi/embedinstall/__init__.py +0 -0
- sfi/embedinstall/embedinstall.py +77 -17
- sfi/makepython/makepython.py +44 -27
- sfi/pdfsplit/__init__.py +0 -0
- sfi/pdfsplit/pdfsplit.py +173 -173
- sfi/projectparse/__init__.py +0 -0
- sfi/pylibpack/__init__.py +0 -0
- sfi/pylibpack/pylibpack.py +913 -0
- sfi/pyloadergen/pyloadergen.py +697 -111
- sfi/pypack/__init__.py +0 -0
- sfi/pypack/pypack.py +791 -0
- sfi/pysourcepack/pysourcepack.py +369 -0
- sfi/taskkill/__init__.py +0 -0
- sfi/which/__init__.py +0 -0
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +444 -0
- pysfi-0.1.6.dist-info/RECORD +0 -21
- sfi/pypacker/fspacker.py +0 -91
- {pysfi-0.1.6.dist-info → pysfi-0.1.10.dist-info}/WHEEL +0 -0
sfi/pyloadergen/pyloadergen.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
import platform
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
9
11
|
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
10
13
|
|
|
11
14
|
is_windows = platform.system() == "Windows"
|
|
12
15
|
is_linux = platform.system() == "Linux"
|
|
@@ -17,6 +20,84 @@ 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 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
|
+
@dataclass(frozen=True)
|
|
81
|
+
class CompilerConfig:
|
|
82
|
+
"""Compiler configuration dataclass.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
name: Compiler name
|
|
86
|
+
args: List of compiler arguments
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
name: str
|
|
90
|
+
args: tuple[str, ...]
|
|
91
|
+
|
|
92
|
+
def to_list(self) -> list[str]:
|
|
93
|
+
"""Convert arguments to list.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of compiler arguments
|
|
97
|
+
"""
|
|
98
|
+
return list(self.args)
|
|
99
|
+
|
|
100
|
+
|
|
20
101
|
_WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
|
|
21
102
|
#include <stdio.h>
|
|
22
103
|
#include <stdlib.h>
|
|
@@ -36,7 +117,8 @@ static void build_python_command(
|
|
|
36
117
|
char* cmd,
|
|
37
118
|
const char* exe_dir,
|
|
38
119
|
const char* entry_file,
|
|
39
|
-
int is_debug
|
|
120
|
+
int is_debug,
|
|
121
|
+
LPSTR lpCmdLine
|
|
40
122
|
) {
|
|
41
123
|
char python_runtime[MAX_PATH_LEN];
|
|
42
124
|
char script_path[MAX_PATH_LEN];
|
|
@@ -57,10 +139,18 @@ static void build_python_command(
|
|
|
57
139
|
// Build command line (add -u parameter for real-time output capture)
|
|
58
140
|
if (is_debug) {
|
|
59
141
|
// Debug mode: do not redirect output, display output on console
|
|
60
|
-
|
|
142
|
+
if (lpCmdLine && strlen(lpCmdLine) > 0) {
|
|
143
|
+
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" %s", python_runtime, script_path, lpCmdLine);
|
|
144
|
+
} else {
|
|
145
|
+
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);
|
|
146
|
+
}
|
|
61
147
|
} else {
|
|
62
148
|
// Production mode: redirect all output to pipe
|
|
63
|
-
|
|
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
|
+
}
|
|
64
154
|
}
|
|
65
155
|
}
|
|
66
156
|
|
|
@@ -132,7 +222,7 @@ int APIENTRY WinMain(
|
|
|
132
222
|
}
|
|
133
223
|
|
|
134
224
|
// Build and execute Python command
|
|
135
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE});
|
|
225
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);
|
|
136
226
|
|
|
137
227
|
// Create pipe for capturing output
|
|
138
228
|
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
@@ -199,7 +289,13 @@ int APIENTRY WinMain(
|
|
|
199
289
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
200
290
|
|
|
201
291
|
// Debug output
|
|
202
|
-
|
|
292
|
+
if (${DEBUG_MODE}) {
|
|
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);
|
|
297
|
+
#endif
|
|
298
|
+
}
|
|
203
299
|
|
|
204
300
|
// Cleanup
|
|
205
301
|
CloseHandle(pi.hProcess);
|
|
@@ -227,6 +323,7 @@ _WINDOWS_CONSOLE_TEMPLATE: str = r"""#include <stdio.h>
|
|
|
227
323
|
#include <locale.h>
|
|
228
324
|
|
|
229
325
|
#define MAX_PATH_LEN 4096
|
|
326
|
+
#define MAX_ERROR_LEN 8192
|
|
230
327
|
|
|
231
328
|
// Set console encoding to UTF-8
|
|
232
329
|
static void setup_encoding() {
|
|
@@ -245,7 +342,8 @@ static void build_python_command(
|
|
|
245
342
|
char* cmd,
|
|
246
343
|
const char* exe_dir,
|
|
247
344
|
const char* entry_file,
|
|
248
|
-
int
|
|
345
|
+
int argc,
|
|
346
|
+
char* argv[]
|
|
249
347
|
) {
|
|
250
348
|
char python_runtime[MAX_PATH_LEN];
|
|
251
349
|
char script_path[MAX_PATH_LEN];
|
|
@@ -257,10 +355,30 @@ static void build_python_command(
|
|
|
257
355
|
snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);
|
|
258
356
|
|
|
259
357
|
// Build command line (add -u parameter for real-time output capture)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
358
|
+
// Include original command line arguments
|
|
359
|
+
snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);
|
|
360
|
+
|
|
361
|
+
// Append original command line arguments if any
|
|
362
|
+
if (argc > 1) {
|
|
363
|
+
strcat(cmd, " ");
|
|
364
|
+
for (int i = 1; i < argc; i++) {
|
|
365
|
+
strcat(cmd, "\"");
|
|
366
|
+
strcat(cmd, argv[i]);
|
|
367
|
+
strcat(cmd, "\"");
|
|
368
|
+
if (i < argc - 1) {
|
|
369
|
+
strcat(cmd, " ");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Read process output
|
|
376
|
+
static void read_process_output(HANDLE hPipe, char* output, int max_len) {
|
|
377
|
+
DWORD bytes_read;
|
|
378
|
+
output[0] = '\0';
|
|
379
|
+
|
|
380
|
+
while (ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
|
|
381
|
+
output[bytes_read] = '\0';
|
|
264
382
|
}
|
|
265
383
|
}
|
|
266
384
|
|
|
@@ -270,6 +388,7 @@ int main(int argc, char* argv[]) {
|
|
|
270
388
|
char cmd[MAX_PATH_LEN * 2];
|
|
271
389
|
STARTUPINFOA si = {0};
|
|
272
390
|
PROCESS_INFORMATION pi = {0};
|
|
391
|
+
BOOL success;
|
|
273
392
|
|
|
274
393
|
// Set encoding to UTF-8
|
|
275
394
|
setup_encoding();
|
|
@@ -282,37 +401,65 @@ int main(int argc, char* argv[]) {
|
|
|
282
401
|
}
|
|
283
402
|
|
|
284
403
|
// 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)) {
|
|
404
|
+
if (!check_python_runtime(exe_dir)) {
|
|
288
405
|
fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
|
|
289
406
|
fprintf(stderr, "Please ensure the application is installed correctly.\n");
|
|
290
407
|
return 1;
|
|
291
408
|
}
|
|
292
409
|
|
|
293
|
-
// Build
|
|
294
|
-
build_python_command(cmd, exe_dir, "${ENTRY_FILE}",
|
|
410
|
+
// Build Python command
|
|
411
|
+
build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
|
|
295
412
|
|
|
296
413
|
// Debug output
|
|
297
414
|
if (${DEBUG_MODE}) {
|
|
415
|
+
#ifdef _WIN32
|
|
416
|
+
// On Windows, convert to wide characters for proper Unicode display
|
|
417
|
+
{
|
|
418
|
+
// Convert and print command
|
|
419
|
+
int wcmd_len = MultiByteToWideChar(CP_UTF8, 0, cmd, -1, NULL, 0);
|
|
420
|
+
if (wcmd_len > 0) {
|
|
421
|
+
wchar_t* wcmd = (wchar_t*)malloc(wcmd_len * sizeof(wchar_t));
|
|
422
|
+
MultiByteToWideChar(CP_UTF8, 0, cmd, -1, wcmd, wcmd_len);
|
|
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
|
+
}
|
|
435
|
+
}
|
|
436
|
+
#else
|
|
298
437
|
fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
|
|
299
438
|
fprintf(stderr, "DEBUG: exe_dir: %s\n", exe_dir);
|
|
439
|
+
#endif
|
|
300
440
|
}
|
|
301
441
|
|
|
302
|
-
// Set startup info
|
|
442
|
+
// Set startup info
|
|
303
443
|
si.cb = sizeof(si);
|
|
304
444
|
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
454
|
|
|
311
455
|
// Create Python process
|
|
312
|
-
|
|
313
|
-
|
|
456
|
+
// TRUE - inherit handles so Python can write to console
|
|
457
|
+
success = CreateProcessA(
|
|
458
|
+
NULL, cmd, NULL, NULL, TRUE,
|
|
314
459
|
0, NULL, exe_dir, &si, &pi
|
|
315
|
-
)
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (!success) {
|
|
316
463
|
DWORD error = GetLastError();
|
|
317
464
|
fprintf(stderr, "Error: Failed to start Python process.\n");
|
|
318
465
|
fprintf(stderr, "Error code: %lu\n", error);
|
|
@@ -320,6 +467,9 @@ int main(int argc, char* argv[]) {
|
|
|
320
467
|
return 1;
|
|
321
468
|
}
|
|
322
469
|
|
|
470
|
+
// In non-debug mode for console apps, we don't use pipes, so no error output capture
|
|
471
|
+
// All output (stdout/stderr) goes directly to console
|
|
472
|
+
|
|
323
473
|
// Wait for process to end
|
|
324
474
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
325
475
|
|
|
@@ -336,10 +486,11 @@ int main(int argc, char* argv[]) {
|
|
|
336
486
|
CloseHandle(pi.hProcess);
|
|
337
487
|
CloseHandle(pi.hThread);
|
|
338
488
|
|
|
339
|
-
//
|
|
340
|
-
if (exit_code != 0
|
|
341
|
-
fprintf(stderr, "\nApplication exited
|
|
342
|
-
fprintf(stderr, "
|
|
489
|
+
// In non-debug mode, we don't capture output, so just report exit code if non-zero
|
|
490
|
+
if (exit_code != 0) {
|
|
491
|
+
fprintf(stderr, "\nApplication exited with error code: %lu\n", exit_code);
|
|
492
|
+
fprintf(stderr, "Check console output above for details.\n");
|
|
493
|
+
return exit_code;
|
|
343
494
|
}
|
|
344
495
|
|
|
345
496
|
return exit_code;
|
|
@@ -382,6 +533,7 @@ static void build_python_command(
|
|
|
382
533
|
|
|
383
534
|
// Unix GUI entry point
|
|
384
535
|
int main(int argc, char* argv[]) {
|
|
536
|
+
(void)argc; // Unused parameter (argv[0] is used)
|
|
385
537
|
char exe_dir[MAX_PATH_LEN];
|
|
386
538
|
char cmd[MAX_PATH_LEN * 2];
|
|
387
539
|
char log_file[MAX_PATH_LEN];
|
|
@@ -471,6 +623,8 @@ static void build_python_command(
|
|
|
471
623
|
|
|
472
624
|
// Unix Console entry point
|
|
473
625
|
int main(int argc, char* argv[]) {
|
|
626
|
+
(void)argc; // Unused parameter
|
|
627
|
+
(void)argv; // Unused parameter
|
|
474
628
|
char exe_dir[MAX_PATH_LEN];
|
|
475
629
|
char cmd[MAX_PATH_LEN * 2];
|
|
476
630
|
pid_t pid;
|
|
@@ -558,10 +712,10 @@ static void get_exe_dir_macos(char* exe_dir, size_t size) {
|
|
|
558
712
|
if (macos_path) {
|
|
559
713
|
*macos_path = '\0';
|
|
560
714
|
}
|
|
561
|
-
|
|
715
|
+
}
|
|
562
716
|
|
|
563
|
-
|
|
564
|
-
|
|
717
|
+
// Display macOS error dialog
|
|
718
|
+
static void show_error_dialog(const char* message) {
|
|
565
719
|
CFStringRef cf_message = CFStringCreateWithCString(
|
|
566
720
|
NULL, message, kCFStringEncodingUTF8
|
|
567
721
|
);
|
|
@@ -603,6 +757,8 @@ static void build_python_command(
|
|
|
603
757
|
|
|
604
758
|
// macOS GUI entry point
|
|
605
759
|
int main(int argc, char* argv[]) {
|
|
760
|
+
(void)argc; // Unused parameter
|
|
761
|
+
(void)argv; // Unused parameter
|
|
606
762
|
char exe_dir[MAX_PATH_LEN];
|
|
607
763
|
char cmd[MAX_PATH_LEN * 2];
|
|
608
764
|
char log_file[MAX_PATH_LEN];
|
|
@@ -729,6 +885,8 @@ static void build_python_command(
|
|
|
729
885
|
|
|
730
886
|
// macOS Console entry point
|
|
731
887
|
int main(int argc, char* argv[]) {
|
|
888
|
+
(void)argc; // Unused parameter
|
|
889
|
+
(void)argv; // Unused parameter
|
|
732
890
|
char exe_dir[MAX_PATH_LEN];
|
|
733
891
|
char cmd[MAX_PATH_LEN * 2];
|
|
734
892
|
pid_t pid;
|
|
@@ -772,40 +930,89 @@ int main(int argc, char* argv[]) {
|
|
|
772
930
|
}
|
|
773
931
|
"""
|
|
774
932
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
933
|
+
ENTRY_FILE_TEMPLATE = r"""
|
|
934
|
+
import os
|
|
935
|
+
import sys
|
|
936
|
+
from pathlib import Path
|
|
780
937
|
|
|
938
|
+
# Setup environment
|
|
939
|
+
cwd = Path.cwd()
|
|
940
|
+
site_dirs = [cwd / "site-packages", cwd / "lib"]
|
|
941
|
+
dirs = [cwd, cwd / "src", cwd / "runtime", *site_dirs]
|
|
942
|
+
|
|
943
|
+
for dir in dirs:
|
|
944
|
+
sys.path.append(str(dir))
|
|
945
|
+
|
|
946
|
+
# Fix zipimporter compatibility for packages imported from zip archives
|
|
947
|
+
# Some packages (e.g., markdown) require exec_module on zipimporter
|
|
948
|
+
import zipimport
|
|
949
|
+
if not hasattr(zipimport.zipimporter, 'exec_module'):
|
|
950
|
+
def _create_module(self, spec):
|
|
951
|
+
return None
|
|
952
|
+
def _exec_module(self, module):
|
|
953
|
+
code = self.get_code(module.__name__)
|
|
954
|
+
exec(code, module.__dict__)
|
|
955
|
+
zipimport.zipimporter.create_module = _create_module
|
|
956
|
+
zipimport.zipimporter.exec_module = _exec_module
|
|
957
|
+
|
|
958
|
+
# Qt configuration (optional)
|
|
959
|
+
$QT_CONFIG
|
|
960
|
+
|
|
961
|
+
# Main entry point
|
|
962
|
+
from src.$PROJECT_NAME.$ENTRY_FILE import main
|
|
963
|
+
main()
|
|
964
|
+
"""
|
|
781
965
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
966
|
+
# Compiler configurations using frozenset for better performance
|
|
967
|
+
_COMPILER_CONFIGS: frozenset[CompilerConfig] = frozenset([
|
|
968
|
+
CompilerConfig(name="gcc", args=("-std=c99", "-Wall", "-O2")),
|
|
969
|
+
CompilerConfig(name="clang", args=("-std=c99", "-Wall", "-O2")),
|
|
970
|
+
CompilerConfig(name="cl", args=("/std:c99", "/O2")),
|
|
971
|
+
])
|
|
972
|
+
|
|
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"))
|
|
785
986
|
|
|
786
|
-
for compiler in compilers:
|
|
787
|
-
if shutil.which(compiler) is not None:
|
|
788
|
-
return compiler
|
|
789
987
|
|
|
988
|
+
def find_compiler() -> str | None:
|
|
989
|
+
"""Find available C compiler."""
|
|
990
|
+
for compiler in ("gcc", "clang", "cl"):
|
|
991
|
+
if shutil.which(compiler):
|
|
992
|
+
return compiler
|
|
790
993
|
logger.error("No compiler found")
|
|
791
994
|
return None
|
|
792
995
|
|
|
793
996
|
|
|
794
|
-
def get_compiler_args(
|
|
795
|
-
compiler
|
|
796
|
-
|
|
797
|
-
|
|
997
|
+
def get_compiler_args(compiler: str) -> list[str]:
|
|
998
|
+
"""Get compiler arguments for specified compiler.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
compiler: Compiler name or path
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
List of compiler arguments
|
|
1005
|
+
"""
|
|
798
1006
|
compiler_name = Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
|
|
799
1007
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return ["/std:c99", "/Wall", "/WX", "/Werror"]
|
|
804
|
-
else:
|
|
805
|
-
return []
|
|
1008
|
+
for config in _COMPILER_CONFIGS:
|
|
1009
|
+
if config.name == compiler_name:
|
|
1010
|
+
return config.to_list()
|
|
806
1011
|
|
|
1012
|
+
return []
|
|
807
1013
|
|
|
808
|
-
|
|
1014
|
+
|
|
1015
|
+
def select_c_template(
|
|
809
1016
|
loader_type: str,
|
|
810
1017
|
is_debug: bool,
|
|
811
1018
|
) -> str:
|
|
@@ -840,7 +1047,33 @@ def select_template(
|
|
|
840
1047
|
return _UNIX_CONSOLE_TEMPLATE
|
|
841
1048
|
|
|
842
1049
|
|
|
843
|
-
def
|
|
1050
|
+
def prepare_entry_file(entry_stem: str, is_qt: bool) -> str:
|
|
1051
|
+
"""Generate entry file code by replacing placeholders in template.
|
|
1052
|
+
|
|
1053
|
+
Args:
|
|
1054
|
+
entry_stem: Entry file name without extension (e.g., "myapp" for "myapp.py" or "myapp.ent")
|
|
1055
|
+
is_qt: Whether this is a Qt application
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
Generated entry file code
|
|
1059
|
+
"""
|
|
1060
|
+
entry_file_code = ENTRY_FILE_TEMPLATE.replace("$PROJECT_NAME", entry_stem)
|
|
1061
|
+
|
|
1062
|
+
# Add Qt configuration if needed
|
|
1063
|
+
if is_qt:
|
|
1064
|
+
qt_config = """# Qt configuration
|
|
1065
|
+
qt_dir = cwd / "site-packages" / "PySide2"
|
|
1066
|
+
plugin_path = str(qt_dir / "plugins" / "platforms")
|
|
1067
|
+
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
|
|
1068
|
+
|
|
1069
|
+
"""
|
|
1070
|
+
else:
|
|
1071
|
+
qt_config = ""
|
|
1072
|
+
|
|
1073
|
+
return entry_file_code.replace("$QT_CONFIG", qt_config)
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def prepare_c_source(
|
|
844
1077
|
template: str,
|
|
845
1078
|
entry_file: str,
|
|
846
1079
|
is_debug: bool,
|
|
@@ -914,79 +1147,432 @@ def compile_c_source(
|
|
|
914
1147
|
return False
|
|
915
1148
|
|
|
916
1149
|
|
|
917
|
-
def
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
+
def _is_entry_file(file_path: Path) -> bool:
|
|
1265
|
+
"""Check if a Python file is an entry file.
|
|
1266
|
+
|
|
1267
|
+
An entry file must contain either:
|
|
1268
|
+
- def main() function
|
|
1269
|
+
- if __name__ == '__main__': block
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
file_path: Path to Python file
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
True if file is an entry file, False otherwise
|
|
1276
|
+
"""
|
|
1277
|
+
if not file_path.exists() or not file_path.is_file():
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1280
|
+
try:
|
|
1281
|
+
content = file_path.read_text(encoding="utf-8")
|
|
1282
|
+
# Check for def main() or def main(
|
|
1283
|
+
has_main_func = "def main(" in content or "def main():" in content
|
|
1284
|
+
# Check for if __name__ == '__main__':
|
|
1285
|
+
has_main_block = "if __name__ == '__main__':" in content or 'if __name__ == "__main__":' in content
|
|
1286
|
+
return has_main_func or has_main_block
|
|
1287
|
+
except Exception as e:
|
|
1288
|
+
logger.debug(f"Failed to read {file_path}: {e}")
|
|
1289
|
+
return False
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def _detect_entry_files(project_dir: Path, project_name: str, loader_type: str) -> list[tuple[str, str, bool]]:
|
|
1293
|
+
"""Detect all entry files in project directory.
|
|
1294
|
+
|
|
1295
|
+
Args:
|
|
1296
|
+
project_dir: Project directory
|
|
1297
|
+
project_name: Project name
|
|
1298
|
+
loader_type: Base loader type from project info
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
List of tuples (entry_file_name, module_name, is_gui)
|
|
1302
|
+
e.g., [("docscan", "docscan", False), ("docscan-gui", "docscan_gui", True)]
|
|
1303
|
+
"""
|
|
1304
|
+
normalized_name = project_name.replace("-", "_")
|
|
1305
|
+
entry_files = []
|
|
1306
|
+
|
|
1307
|
+
# Scan all Python files in project directory
|
|
1308
|
+
for py_file in project_dir.glob("*.py"):
|
|
1309
|
+
if not _is_entry_file(py_file):
|
|
1310
|
+
continue
|
|
1311
|
+
|
|
1312
|
+
# Get file stem (name without .py extension)
|
|
1313
|
+
file_stem = py_file.stem
|
|
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))
|
|
1336
|
+
|
|
1337
|
+
return entry_files
|
|
1338
|
+
|
|
1339
|
+
|
|
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
|
+
def _generate_single_loader(
|
|
1376
|
+
project_dir: Path,
|
|
1377
|
+
entry_name: str,
|
|
1378
|
+
entry_module: str,
|
|
1379
|
+
loader_type: str,
|
|
1380
|
+
use_qt: bool,
|
|
1381
|
+
is_debug: bool,
|
|
1382
|
+
compiler: str | None,
|
|
1383
|
+
) -> bool:
|
|
1384
|
+
"""Generate a single loader executable and entry file.
|
|
1385
|
+
|
|
1386
|
+
Args:
|
|
1387
|
+
project_dir: Project directory
|
|
1388
|
+
entry_name: Entry file name (e.g., "docscan", "docscan-gui")
|
|
1389
|
+
entry_module: Python module name (e.g., "docscan", "docscan_gui")
|
|
1390
|
+
loader_type: Loader type ("console" or "gui")
|
|
1391
|
+
use_qt: Whether this is a Qt application
|
|
1392
|
+
is_debug: Whether to generate debug version
|
|
1393
|
+
compiler: Optional compiler to use
|
|
1394
|
+
|
|
1395
|
+
Returns:
|
|
1396
|
+
True if successful, False otherwise
|
|
1397
|
+
"""
|
|
1398
|
+
# Prepare directories
|
|
1399
|
+
build_dir = project_dir / ".cbuild"
|
|
1400
|
+
output_dir = project_dir / "dist"
|
|
1401
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
1402
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
1403
|
+
|
|
1404
|
+
# Output paths
|
|
1405
|
+
output_exe = output_dir / f"{entry_name}{ext}"
|
|
1406
|
+
c_source_filename = f"{entry_name}_debug_console.c" if is_debug else f"{entry_name}_{loader_type}.c"
|
|
1407
|
+
entry_filepath = output_dir / f"{entry_name}.ent"
|
|
1408
|
+
c_source_path = build_dir / c_source_filename
|
|
1409
|
+
|
|
1410
|
+
t0 = time.perf_counter()
|
|
1411
|
+
|
|
1412
|
+
# Generate entry file
|
|
1413
|
+
# Extract project name from entry_name (remove -gui suffix if present)
|
|
1414
|
+
project_name = entry_name.replace("-gui", "")
|
|
1415
|
+
entry_file_code = prepare_entry_file(project_name, use_qt)
|
|
1416
|
+
entry_file_code = entry_file_code.replace("$ENTRY_FILE", entry_module)
|
|
1417
|
+
entry_filepath.write_text(entry_file_code, encoding="utf-8")
|
|
1418
|
+
logger.info(f"Generated entry file: {entry_filepath}")
|
|
1419
|
+
|
|
1420
|
+
# Generate and compile C source
|
|
1421
|
+
c_template = select_c_template(loader_type, is_debug)
|
|
1422
|
+
c_code = prepare_c_source(c_template, entry_filepath.name, is_debug)
|
|
1423
|
+
c_source_path.write_text(c_code, encoding="utf-8")
|
|
1424
|
+
logger.info(f"Generated C source code: {c_source_path}")
|
|
1425
|
+
|
|
1426
|
+
success = compile_c_source(c_source_path, output_exe, compiler)
|
|
1427
|
+
if success:
|
|
1428
|
+
logger.info(f"Loader executable generated successfully: {output_exe}")
|
|
1429
|
+
logger.debug(f"Entry: {entry_name}")
|
|
1430
|
+
logger.debug(f"Module: {entry_module}")
|
|
1431
|
+
logger.debug(f"Type: {loader_type}")
|
|
1432
|
+
logger.debug(f"Debug mode: {is_debug}")
|
|
1433
|
+
logger.debug(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
|
|
1434
|
+
else:
|
|
1435
|
+
logger.error(f"Failed to compile loader for {entry_name}")
|
|
1436
|
+
|
|
1437
|
+
return success
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def generate_loader_for_project(
|
|
1441
|
+
project_dir: Path,
|
|
1442
|
+
project_info: ProjectInfo,
|
|
1443
|
+
is_debug: bool = False,
|
|
1444
|
+
compiler: str | None = None,
|
|
1445
|
+
) -> bool:
|
|
1446
|
+
"""Generate loader executables and entry files for a single project.
|
|
1447
|
+
|
|
1448
|
+
This function detects all entry files (main and GUI versions) and generates
|
|
1449
|
+
a loader for each one.
|
|
1450
|
+
|
|
1451
|
+
Args:
|
|
1452
|
+
project_dir: Directory containing the project (pyproject.toml location)
|
|
1453
|
+
project_info: Project information dataclass
|
|
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
|
|
1459
|
+
"""
|
|
1460
|
+
logger.info(f"\n{'=' * 60}")
|
|
1461
|
+
logger.info(f"Generating loaders for project: {project_info.name}")
|
|
1462
|
+
logger.info(f"{'=' * 60}")
|
|
1463
|
+
|
|
1464
|
+
# Determine base loader type and Qt usage from project info
|
|
1465
|
+
base_loader_type, use_qt = _detect_loader_type(project_info)
|
|
1466
|
+
|
|
1467
|
+
# Detect all entry files in project directory
|
|
1468
|
+
entry_files = _detect_entry_files(project_dir, project_info.name, base_loader_type)
|
|
1469
|
+
logger.info(f"Found {len(entry_files)} entry file(s): {[e[0] for e in entry_files]}")
|
|
1470
|
+
|
|
1471
|
+
# Generate loader for each entry file
|
|
1472
|
+
all_success = True
|
|
1473
|
+
for entry_name, entry_module, is_gui in entry_files:
|
|
1474
|
+
logger.info(f"\n--- Generating loader for: {entry_name} ---")
|
|
1475
|
+
|
|
1476
|
+
# Use GUI loader type for GUI entries, otherwise use base type
|
|
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}")
|
|
1481
|
+
|
|
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
|
+
|
|
1492
|
+
if not success:
|
|
1493
|
+
all_success = False
|
|
1494
|
+
logger.error(f"Failed to generate loader for {entry_name}")
|
|
1495
|
+
|
|
1496
|
+
return all_success
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def main() -> int:
|
|
1500
|
+
"""Main entry point for pyloadergen.
|
|
1501
|
+
|
|
1502
|
+
Returns:
|
|
1503
|
+
Exit code (0 for success, 1 for failure)
|
|
1504
|
+
"""
|
|
1505
|
+
parser = argparse.ArgumentParser(description="Generate Python loader executables for all projects in directory")
|
|
924
1506
|
parser.add_argument(
|
|
925
|
-
"
|
|
1507
|
+
"directory", nargs="?", default=str(cwd), help="Directory containing projects (will create/load projects.json)"
|
|
926
1508
|
)
|
|
1509
|
+
parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
|
|
927
1510
|
parser.add_argument(
|
|
928
1511
|
"--compiler", help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe"
|
|
929
1512
|
)
|
|
930
|
-
parser.add_argument("--run", "-r", action="store_true", help="Run the generated executable after compilation")
|
|
931
|
-
|
|
932
1513
|
args = parser.parse_args()
|
|
933
1514
|
|
|
934
1515
|
if args.debug:
|
|
935
1516
|
logger.setLevel(logging.DEBUG)
|
|
936
1517
|
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
939
1522
|
|
|
940
|
-
|
|
941
|
-
template = select_template(args.type, args.debug)
|
|
1523
|
+
logger.info(f"Working directory: {base_dir}")
|
|
942
1524
|
|
|
943
|
-
#
|
|
944
|
-
|
|
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
|
|
945
1530
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
c_source_file = "pyloader_debug_console.c"
|
|
951
|
-
elif args.type == "console":
|
|
952
|
-
c_source_file = "pyloader_console.c"
|
|
953
|
-
else: # gui type in non-debug mode
|
|
954
|
-
c_source_file = "pyloader_gui.c"
|
|
1531
|
+
projects_data = load_projects_json(projects_json)
|
|
1532
|
+
if not projects_data:
|
|
1533
|
+
logger.error("Failed to load project information")
|
|
1534
|
+
return 1
|
|
955
1535
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
logger.info(f"Generated C source code: {c_source_path}")
|
|
1536
|
+
# Convert dict to ProjectInfo dataclasses
|
|
1537
|
+
projects: dict[str, ProjectInfo] = {name: ProjectInfo.from_dict(name, data) for name, data in projects_data.items()}
|
|
959
1538
|
|
|
960
|
-
|
|
1539
|
+
logger.info(f"Found {len(projects)} project(s) to process")
|
|
961
1540
|
|
|
962
|
-
#
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
logger.debug(
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
if
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
|
1546
|
+
|
|
1547
|
+
if use_current_dir:
|
|
1548
|
+
logger.debug("Only one project found, using current directory")
|
|
1549
|
+
|
|
1550
|
+
for project_name, project_info in projects.items():
|
|
1551
|
+
project_dir = base_dir if use_current_dir else base_dir / project_name
|
|
1552
|
+
if not project_dir.is_dir():
|
|
1553
|
+
logger.warning(f"Project directory not found: {project_dir}, skipping")
|
|
1554
|
+
failed_projects.append(project_name)
|
|
1555
|
+
continue
|
|
1556
|
+
|
|
1557
|
+
if generate_loader_for_project(project_dir, project_info, args.debug, args.compiler):
|
|
1558
|
+
success_count += 1
|
|
1559
|
+
else:
|
|
1560
|
+
failed_projects.append(project_name)
|
|
1561
|
+
|
|
1562
|
+
total_time = time.perf_counter() - total_start
|
|
1563
|
+
|
|
1564
|
+
# Print summary
|
|
1565
|
+
logger.info(f"\n{'=' * 60}")
|
|
1566
|
+
logger.info("Summary")
|
|
1567
|
+
logger.info(f"{'=' * 60}")
|
|
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")
|
|
1574
|
+
|
|
1575
|
+
return 0 if not failed_projects else 1
|
|
990
1576
|
|
|
991
1577
|
|
|
992
1578
|
if __name__ == "__main__":
|