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.
Files changed (49) hide show
  1. {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/METADATA +9 -7
  2. pysfi-0.1.12.dist-info/RECORD +62 -0
  3. {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/entry_points.txt +13 -2
  4. sfi/__init__.py +1 -1
  5. sfi/alarmclock/alarmclock.py +40 -40
  6. sfi/bumpversion/__init__.py +1 -1
  7. sfi/cleanbuild/cleanbuild.py +155 -0
  8. sfi/condasetup/condasetup.py +116 -0
  9. sfi/docdiff/docdiff.py +238 -0
  10. sfi/docscan/__init__.py +1 -1
  11. sfi/docscan/docscan_gui.py +1 -1
  12. sfi/docscan/lang/eng.py +152 -152
  13. sfi/docscan/lang/zhcn.py +170 -170
  14. sfi/filedate/filedate.py +185 -112
  15. sfi/gittool/__init__.py +2 -0
  16. sfi/gittool/gittool.py +401 -0
  17. sfi/llmclient/llmclient.py +592 -0
  18. sfi/llmquantize/llmquantize.py +480 -0
  19. sfi/llmserver/llmserver.py +335 -0
  20. sfi/makepython/makepython.py +2 -2
  21. sfi/pdfsplit/pdfsplit.py +4 -4
  22. sfi/pyarchive/pyarchive.py +418 -0
  23. sfi/pyembedinstall/__init__.py +0 -0
  24. sfi/pyembedinstall/pyembedinstall.py +629 -0
  25. sfi/pylibpack/pylibpack.py +813 -269
  26. sfi/pylibpack/rules/numpy.json +22 -0
  27. sfi/pylibpack/rules/pymupdf.json +10 -0
  28. sfi/pylibpack/rules/pyqt5.json +19 -0
  29. sfi/pylibpack/rules/pyside2.json +23 -0
  30. sfi/pylibpack/rules/scipy.json +23 -0
  31. sfi/pylibpack/rules/shiboken2.json +24 -0
  32. sfi/pyloadergen/pyloadergen.py +271 -572
  33. sfi/pypack/pypack.py +822 -471
  34. sfi/pyprojectparse/__init__.py +0 -0
  35. sfi/pyprojectparse/pyprojectparse.py +500 -0
  36. sfi/pysourcepack/pysourcepack.py +308 -369
  37. sfi/quizbase/__init__.py +0 -0
  38. sfi/quizbase/quizbase.py +828 -0
  39. sfi/quizbase/quizbase_gui.py +987 -0
  40. sfi/regexvalidate/__init__.py +0 -0
  41. sfi/regexvalidate/regex_help.html +284 -0
  42. sfi/regexvalidate/regexvalidate.py +468 -0
  43. sfi/taskkill/taskkill.py +0 -2
  44. pysfi-0.1.10.dist-info/RECORD +0 -39
  45. sfi/embedinstall/embedinstall.py +0 -478
  46. sfi/projectparse/projectparse.py +0 -152
  47. {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/WHEEL +0 -0
  48. /sfi/{embedinstall → llmclient}/__init__.py +0 -0
  49. /sfi/{projectparse → llmquantize}/__init__.py +0 -0
@@ -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, field
9
+ from dataclasses import dataclass
11
10
  from pathlib import Path
12
- from typing import Any
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
- // GUI non-debug mode uses pythonw.exe (no console), other cases use python.exe
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 (add -u parameter for real-time output capture)
140
- if (is_debug) {
141
- // Debug mode: do not redirect output, display output on console
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
- }
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 (ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
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
- // Build and execute Python command
225
- build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);
226
-
227
- // Create pipe for capturing output
228
- sa.nLength = sizeof(SECURITY_ATTRIBUTES);
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
- // Set startup info
238
- si.cb = sizeof(si);
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
- // Set different startup methods based on debug mode
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
- // Debug output
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);
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
- // Build command line (add -u parameter for real-time output capture)
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
- }
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
- // Set encoding to UTF-8
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
- char* last_slash = strrchr(exe_dir, '\\');
399
- if (last_slash) {
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
- // 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
- }
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
- fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
438
- fprintf(stderr, "DEBUG: exe_dir: %s\n", exe_dir);
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
- // Debug output
481
- if (${DEBUG_MODE}) {
482
- fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
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; // Unused parameter (argv[0] is used)
442
+ (void)argc;
537
443
  char exe_dir[MAX_PATH_LEN];
538
- char cmd[MAX_PATH_LEN * 2];
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
- // If /proc/self/exe is unavailable (like on macOS), use argv[0]
546
- if (realpath(argv[0], exe_dir) == NULL) {
547
- fprintf(stderr, "Error: Cannot determine executable directory\n");
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
- char* last_slash = strrchr(exe_dir, '/');
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; // Unused parameter
627
- (void)argv; // Unused parameter
528
+ (void)argc;
529
+ (void)argv;
628
530
  char exe_dir[MAX_PATH_LEN];
629
- char cmd[MAX_PATH_LEN * 2];
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
- char* last_slash = strrchr(exe_dir, '/');
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, use argv[0]
707
- realpath("./", exe_dir);
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; // Unused parameter
761
- (void)argv; // Unused parameter
662
+ (void)argc;
663
+ (void)argv;
762
664
  char exe_dir[MAX_PATH_LEN];
763
- char cmd[MAX_PATH_LEN * 2];
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
- realpath("./", exe_dir);
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; // Unused parameter
889
- (void)argv; // Unused parameter
792
+ (void)argc;
793
+ (void)argv;
890
794
  char exe_dir[MAX_PATH_LEN];
891
- char cmd[MAX_PATH_LEN * 2];
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.$ENTRY_FILE import main
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 = Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
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
- # In debug mode, always use console template for visibility
1024
- if is_debug:
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
- if loader_type == "gui":
1035
- return _WINDOWS_GUI_TEMPLATE
1036
- else:
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
- return _MACOS_GUI_TEMPLATE
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(entry_stem: str, is_qt: bool) -> str:
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
- 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"
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
- else:
1071
- qt_config = ""
942
+ """.replace("$QT_NAME", project.qt_libname or "")
943
+ if project.has_qt
944
+ else ""
945
+ )
1072
946
 
1073
- return entry_file_code.replace("$QT_CONFIG", qt_config)
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 = Path(compiler).name if "\\" in compiler or "/" in compiler else compiler
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" if "gui" in str(c_source_path).lower() else "/SUBSYSTEM:CONSOLE",
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 = [compiler, *compiler_args, *subsystem_flag, "-o", str(output_filepath), str(c_source_path)]
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.exists() or not file_path.is_file():
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
- # 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
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(project_dir: Path, project_name: str, loader_type: str) -> list[tuple[str, str, bool]]:
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
- # 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))
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
- entry_name: str,
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 = f"{entry_name}_debug_console.c" if is_debug else f"{entry_name}_{loader_type}.c"
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
- 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)
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.info(f"Generated entry file: {entry_filepath}")
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.info(f"Generated C source code: {c_source_path}")
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: {entry_name}")
1430
- logger.debug(f"Module: {entry_module}")
1431
- logger.debug(f"Type: {loader_type}")
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
- project_info: ProjectInfo,
1443
- is_debug: bool = False,
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
- 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
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, project_info.name, base_loader_type)
1469
- logger.info(f"Found {len(entry_files)} entry file(s): {[e[0] for e in entry_files]}")
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 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} ---")
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
- # 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}")
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
- if not success:
1493
- all_success = False
1494
- logger.error(f"Failed to generate loader for {entry_name}")
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
- return all_success
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
- def main() -> int:
1500
- """Main entry point for pyloadergen.
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
- 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")
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", nargs="?", default=str(cwd), help="Directory containing projects (will create/load projects.json)"
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", help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe"
1257
+ "--compiler",
1258
+ help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe",
1512
1259
  )
1513
- args = parser.parse_args()
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
- 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
1263
+ def main():
1264
+ """Main entry point for pyloadergen."""
1265
+ args = parse_args()
1556
1266
 
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
1267
+ if args.debug:
1268
+ logger.setLevel(logging.DEBUG)
1563
1269
 
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")
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
- return 0 if not failed_projects else 1
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
- import sys
1580
-
1581
- sys.exit(main())
1280
+ main()