pysfi 0.1.7__py3-none-any.whl → 0.1.11__py3-none-any.whl

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