fbuild 1.2.8__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 (121) hide show
  1. fbuild/__init__.py +390 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_info_generator.py +624 -0
  8. fbuild/build/build_state.py +325 -0
  9. fbuild/build/build_utils.py +93 -0
  10. fbuild/build/compilation_executor.py +422 -0
  11. fbuild/build/compiler.py +165 -0
  12. fbuild/build/compiler_avr.py +574 -0
  13. fbuild/build/configurable_compiler.py +664 -0
  14. fbuild/build/configurable_linker.py +637 -0
  15. fbuild/build/flag_builder.py +214 -0
  16. fbuild/build/library_dependency_processor.py +185 -0
  17. fbuild/build/linker.py +708 -0
  18. fbuild/build/orchestrator.py +67 -0
  19. fbuild/build/orchestrator_avr.py +651 -0
  20. fbuild/build/orchestrator_esp32.py +878 -0
  21. fbuild/build/orchestrator_rp2040.py +719 -0
  22. fbuild/build/orchestrator_stm32.py +696 -0
  23. fbuild/build/orchestrator_teensy.py +580 -0
  24. fbuild/build/source_compilation_orchestrator.py +218 -0
  25. fbuild/build/source_scanner.py +516 -0
  26. fbuild/cli.py +717 -0
  27. fbuild/cli_utils.py +314 -0
  28. fbuild/config/__init__.py +16 -0
  29. fbuild/config/board_config.py +542 -0
  30. fbuild/config/board_loader.py +92 -0
  31. fbuild/config/ini_parser.py +369 -0
  32. fbuild/config/mcu_specs.py +88 -0
  33. fbuild/daemon/__init__.py +42 -0
  34. fbuild/daemon/async_client.py +531 -0
  35. fbuild/daemon/client.py +1505 -0
  36. fbuild/daemon/compilation_queue.py +293 -0
  37. fbuild/daemon/configuration_lock.py +865 -0
  38. fbuild/daemon/daemon.py +585 -0
  39. fbuild/daemon/daemon_context.py +293 -0
  40. fbuild/daemon/error_collector.py +263 -0
  41. fbuild/daemon/file_cache.py +332 -0
  42. fbuild/daemon/firmware_ledger.py +546 -0
  43. fbuild/daemon/lock_manager.py +508 -0
  44. fbuild/daemon/logging_utils.py +149 -0
  45. fbuild/daemon/messages.py +957 -0
  46. fbuild/daemon/operation_registry.py +288 -0
  47. fbuild/daemon/port_state_manager.py +249 -0
  48. fbuild/daemon/process_tracker.py +366 -0
  49. fbuild/daemon/processors/__init__.py +18 -0
  50. fbuild/daemon/processors/build_processor.py +248 -0
  51. fbuild/daemon/processors/deploy_processor.py +664 -0
  52. fbuild/daemon/processors/install_deps_processor.py +431 -0
  53. fbuild/daemon/processors/locking_processor.py +777 -0
  54. fbuild/daemon/processors/monitor_processor.py +285 -0
  55. fbuild/daemon/request_processor.py +457 -0
  56. fbuild/daemon/shared_serial.py +819 -0
  57. fbuild/daemon/status_manager.py +238 -0
  58. fbuild/daemon/subprocess_manager.py +316 -0
  59. fbuild/deploy/__init__.py +21 -0
  60. fbuild/deploy/deployer.py +67 -0
  61. fbuild/deploy/deployer_esp32.py +310 -0
  62. fbuild/deploy/docker_utils.py +315 -0
  63. fbuild/deploy/monitor.py +519 -0
  64. fbuild/deploy/qemu_runner.py +603 -0
  65. fbuild/interrupt_utils.py +34 -0
  66. fbuild/ledger/__init__.py +52 -0
  67. fbuild/ledger/board_ledger.py +560 -0
  68. fbuild/output.py +352 -0
  69. fbuild/packages/__init__.py +66 -0
  70. fbuild/packages/archive_utils.py +1098 -0
  71. fbuild/packages/arduino_core.py +412 -0
  72. fbuild/packages/cache.py +256 -0
  73. fbuild/packages/concurrent_manager.py +510 -0
  74. fbuild/packages/downloader.py +518 -0
  75. fbuild/packages/fingerprint.py +423 -0
  76. fbuild/packages/framework_esp32.py +538 -0
  77. fbuild/packages/framework_rp2040.py +349 -0
  78. fbuild/packages/framework_stm32.py +459 -0
  79. fbuild/packages/framework_teensy.py +346 -0
  80. fbuild/packages/github_utils.py +96 -0
  81. fbuild/packages/header_trampoline_cache.py +394 -0
  82. fbuild/packages/library_compiler.py +203 -0
  83. fbuild/packages/library_manager.py +549 -0
  84. fbuild/packages/library_manager_esp32.py +725 -0
  85. fbuild/packages/package.py +163 -0
  86. fbuild/packages/platform_esp32.py +383 -0
  87. fbuild/packages/platform_rp2040.py +400 -0
  88. fbuild/packages/platform_stm32.py +581 -0
  89. fbuild/packages/platform_teensy.py +312 -0
  90. fbuild/packages/platform_utils.py +131 -0
  91. fbuild/packages/platformio_registry.py +369 -0
  92. fbuild/packages/sdk_utils.py +231 -0
  93. fbuild/packages/toolchain.py +436 -0
  94. fbuild/packages/toolchain_binaries.py +196 -0
  95. fbuild/packages/toolchain_esp32.py +489 -0
  96. fbuild/packages/toolchain_metadata.py +185 -0
  97. fbuild/packages/toolchain_rp2040.py +436 -0
  98. fbuild/packages/toolchain_stm32.py +417 -0
  99. fbuild/packages/toolchain_teensy.py +404 -0
  100. fbuild/platform_configs/esp32.json +150 -0
  101. fbuild/platform_configs/esp32c2.json +144 -0
  102. fbuild/platform_configs/esp32c3.json +143 -0
  103. fbuild/platform_configs/esp32c5.json +151 -0
  104. fbuild/platform_configs/esp32c6.json +151 -0
  105. fbuild/platform_configs/esp32p4.json +149 -0
  106. fbuild/platform_configs/esp32s3.json +151 -0
  107. fbuild/platform_configs/imxrt1062.json +56 -0
  108. fbuild/platform_configs/rp2040.json +70 -0
  109. fbuild/platform_configs/rp2350.json +76 -0
  110. fbuild/platform_configs/stm32f1.json +59 -0
  111. fbuild/platform_configs/stm32f4.json +63 -0
  112. fbuild/py.typed +0 -0
  113. fbuild-1.2.8.dist-info/METADATA +468 -0
  114. fbuild-1.2.8.dist-info/RECORD +121 -0
  115. fbuild-1.2.8.dist-info/WHEEL +5 -0
  116. fbuild-1.2.8.dist-info/entry_points.txt +5 -0
  117. fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
  118. fbuild-1.2.8.dist-info/top_level.txt +2 -0
  119. fbuild_lint/__init__.py +0 -0
  120. fbuild_lint/ruff_plugins/__init__.py +0 -0
  121. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
fbuild/__init__.py ADDED
@@ -0,0 +1,390 @@
1
+ """fbuild - Modern embedded development tool."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ __version__ = "1.2.8"
8
+
9
+
10
+ def is_available() -> bool:
11
+ """Check if fbuild is properly installed and functional.
12
+
13
+ Returns:
14
+ True if fbuild daemon client is available, False otherwise.
15
+ """
16
+ try:
17
+ __import__("fbuild.daemon.client")
18
+ return True
19
+ except ImportError:
20
+ return False
21
+
22
+
23
+ @dataclass
24
+ class BuildContext:
25
+ """Configuration context for fbuild operations.
26
+
27
+ Groups common parameters used across build, deploy, and install operations.
28
+ Can be passed to Daemon methods instead of individual parameters.
29
+
30
+ Example usage:
31
+ import fbuild
32
+
33
+ # Create a context for repeated operations
34
+ ctx = fbuild.BuildContext(
35
+ project_dir=Path("my_project"),
36
+ environment="esp32dev",
37
+ port="COM3",
38
+ verbose=True
39
+ )
40
+
41
+ # Pre-install dependencies (toolchain, framework, libraries)
42
+ fbuild.Daemon.install_dependencies(ctx)
43
+
44
+ # Build using the context
45
+ fbuild.Daemon.build(ctx)
46
+
47
+ # Deploy using the context
48
+ fbuild.Daemon.deploy(ctx)
49
+
50
+ Attributes:
51
+ project_dir: Path to project directory containing platformio.ini
52
+ environment: Build environment name (e.g., 'esp32dev', 'esp32c6')
53
+ port: Serial port for upload/monitor (auto-detect if None)
54
+ clean_build: Whether to perform a clean build
55
+ verbose: Enable verbose output
56
+ timeout: Maximum wait time in seconds (default: 30 minutes)
57
+ """
58
+
59
+ project_dir: Path
60
+ environment: str
61
+ port: str | None = None
62
+ clean_build: bool = False
63
+ verbose: bool = False
64
+ timeout: float = 1800
65
+
66
+
67
+ class Daemon:
68
+ """Daemon management API for fbuild.
69
+
70
+ Provides static methods to control the fbuild daemon which handles
71
+ build, deploy, and monitor operations.
72
+
73
+ Example usage:
74
+ import fbuild
75
+
76
+ # Option 1: Use BuildContext (recommended for repeated operations)
77
+ ctx = fbuild.BuildContext(
78
+ project_dir=Path("my_project"),
79
+ environment="esp32dev"
80
+ )
81
+ fbuild.Daemon.install_dependencies(ctx)
82
+ fbuild.Daemon.build(ctx)
83
+ fbuild.Daemon.deploy(ctx)
84
+
85
+ # Option 2: Use individual parameters
86
+ fbuild.Daemon.build(
87
+ project_dir=Path("my_project"),
88
+ environment="esp32dev"
89
+ )
90
+
91
+ # Daemon lifecycle
92
+ fbuild.Daemon.ensure_running()
93
+ fbuild.Daemon.status()
94
+ fbuild.Daemon.stop()
95
+ """
96
+
97
+ @staticmethod
98
+ def ensure_running() -> bool:
99
+ """Ensure the fbuild daemon is running.
100
+
101
+ Starts the daemon if not already running.
102
+
103
+ Returns:
104
+ True if daemon is running or was started successfully, False otherwise.
105
+ """
106
+ from fbuild.daemon import ensure_daemon_running
107
+
108
+ return ensure_daemon_running()
109
+
110
+ @staticmethod
111
+ def stop() -> bool:
112
+ """Stop the fbuild daemon.
113
+
114
+ Returns:
115
+ True if daemon was stopped, False otherwise.
116
+ """
117
+ from fbuild.daemon import stop_daemon
118
+
119
+ return stop_daemon()
120
+
121
+ @staticmethod
122
+ def status() -> dict[str, Any]:
123
+ """Get current fbuild daemon status.
124
+
125
+ Returns:
126
+ Dictionary with daemon status information including:
127
+ - state: Current daemon state
128
+ - message: Status message
129
+ - running: Whether daemon is running
130
+ """
131
+ from fbuild.daemon import get_daemon_status
132
+
133
+ return get_daemon_status()
134
+
135
+ @staticmethod
136
+ def install_dependencies(
137
+ ctx_or_project_dir: BuildContext | Path,
138
+ environment: str | None = None,
139
+ verbose: bool = False,
140
+ timeout: float = 1800,
141
+ ) -> bool:
142
+ """Pre-install toolchain, platform, framework, and libraries.
143
+
144
+ This downloads and caches all dependencies required for a build
145
+ without actually compiling. Useful for:
146
+ - Pre-warming the cache before builds
147
+ - Ensuring dependencies are available offline
148
+ - Separating dependency installation from compilation
149
+
150
+ Can be called with a BuildContext or individual parameters:
151
+
152
+ # Using BuildContext
153
+ ctx = fbuild.BuildContext(project_dir=Path("."), environment="esp32dev")
154
+ fbuild.Daemon.install_dependencies(ctx)
155
+
156
+ # Using individual parameters
157
+ fbuild.Daemon.install_dependencies(
158
+ project_dir=Path("."),
159
+ environment="esp32dev"
160
+ )
161
+
162
+ Args:
163
+ ctx_or_project_dir: BuildContext or Path to project directory
164
+ environment: Build environment name (ignored if BuildContext passed)
165
+ verbose: Enable verbose output (ignored if BuildContext passed)
166
+ timeout: Maximum wait time in seconds (ignored if BuildContext passed)
167
+
168
+ Returns:
169
+ True if dependencies installed successfully, False otherwise.
170
+ """
171
+ from fbuild.daemon import request_install_dependencies
172
+
173
+ # Handle BuildContext or individual parameters
174
+ if isinstance(ctx_or_project_dir, BuildContext):
175
+ ctx = ctx_or_project_dir
176
+ return request_install_dependencies(
177
+ project_dir=ctx.project_dir,
178
+ environment=ctx.environment,
179
+ verbose=ctx.verbose,
180
+ timeout=ctx.timeout,
181
+ )
182
+ else:
183
+ if environment is None:
184
+ raise ValueError("environment is required when not using BuildContext")
185
+ return request_install_dependencies(
186
+ project_dir=ctx_or_project_dir,
187
+ environment=environment,
188
+ verbose=verbose,
189
+ timeout=timeout,
190
+ )
191
+
192
+ @staticmethod
193
+ def build(
194
+ ctx_or_project_dir: BuildContext | Path,
195
+ environment: str | None = None,
196
+ clean_build: bool = False,
197
+ verbose: bool = False,
198
+ timeout: float = 1800,
199
+ ) -> bool:
200
+ """Request a build operation from the daemon.
201
+
202
+ Can be called with a BuildContext or individual parameters:
203
+
204
+ # Using BuildContext
205
+ ctx = fbuild.BuildContext(project_dir=Path("."), environment="esp32dev")
206
+ fbuild.Daemon.build(ctx)
207
+
208
+ # Using individual parameters
209
+ fbuild.Daemon.build(
210
+ project_dir=Path("."),
211
+ environment="esp32dev"
212
+ )
213
+
214
+ Args:
215
+ ctx_or_project_dir: BuildContext or Path to project directory
216
+ environment: Build environment name (ignored if BuildContext passed)
217
+ clean_build: Whether to perform a clean build (ignored if BuildContext passed)
218
+ verbose: Enable verbose build output (ignored if BuildContext passed)
219
+ timeout: Maximum wait time in seconds (ignored if BuildContext passed)
220
+
221
+ Returns:
222
+ True if build successful, False otherwise.
223
+ """
224
+ from fbuild.daemon import request_build
225
+
226
+ # Handle BuildContext or individual parameters
227
+ if isinstance(ctx_or_project_dir, BuildContext):
228
+ ctx = ctx_or_project_dir
229
+ return request_build(
230
+ project_dir=ctx.project_dir,
231
+ environment=ctx.environment,
232
+ clean_build=ctx.clean_build,
233
+ verbose=ctx.verbose,
234
+ timeout=ctx.timeout,
235
+ )
236
+ else:
237
+ if environment is None:
238
+ raise ValueError("environment is required when not using BuildContext")
239
+ return request_build(
240
+ project_dir=ctx_or_project_dir,
241
+ environment=environment,
242
+ clean_build=clean_build,
243
+ verbose=verbose,
244
+ timeout=timeout,
245
+ )
246
+
247
+ @staticmethod
248
+ def deploy(
249
+ ctx_or_project_dir: BuildContext | Path,
250
+ environment: str | None = None,
251
+ port: str | None = None,
252
+ clean_build: bool = False,
253
+ monitor_after: bool = False,
254
+ monitor_timeout: float | None = None,
255
+ monitor_halt_on_error: str | None = None,
256
+ monitor_halt_on_success: str | None = None,
257
+ monitor_expect: str | None = None,
258
+ timeout: float = 1800,
259
+ ) -> bool:
260
+ """Request a deploy (build + upload) operation from the daemon.
261
+
262
+ Can be called with a BuildContext or individual parameters:
263
+
264
+ # Using BuildContext
265
+ ctx = fbuild.BuildContext(
266
+ project_dir=Path("."),
267
+ environment="esp32dev",
268
+ port="COM3"
269
+ )
270
+ fbuild.Daemon.deploy(ctx)
271
+
272
+ # Using individual parameters
273
+ fbuild.Daemon.deploy(
274
+ project_dir=Path("."),
275
+ environment="esp32dev",
276
+ port="COM3"
277
+ )
278
+
279
+ Args:
280
+ ctx_or_project_dir: BuildContext or Path to project directory
281
+ environment: Build environment name (ignored if BuildContext passed)
282
+ port: Serial port for upload (ignored if BuildContext passed)
283
+ clean_build: Whether to perform a clean build (ignored if BuildContext passed)
284
+ monitor_after: Whether to start monitor after deploy
285
+ monitor_timeout: Timeout for monitor (if monitor_after=True)
286
+ monitor_halt_on_error: Pattern to halt on error (if monitor_after=True)
287
+ monitor_halt_on_success: Pattern to halt on success (if monitor_after=True)
288
+ monitor_expect: Expected pattern to check (if monitor_after=True)
289
+ timeout: Maximum wait time in seconds (ignored if BuildContext passed)
290
+
291
+ Returns:
292
+ True if deploy successful, False otherwise.
293
+ """
294
+ from fbuild.daemon import request_deploy
295
+
296
+ # Handle BuildContext or individual parameters
297
+ if isinstance(ctx_or_project_dir, BuildContext):
298
+ ctx = ctx_or_project_dir
299
+ return request_deploy(
300
+ project_dir=ctx.project_dir,
301
+ environment=ctx.environment,
302
+ port=ctx.port,
303
+ clean_build=ctx.clean_build,
304
+ monitor_after=monitor_after,
305
+ monitor_timeout=monitor_timeout,
306
+ monitor_halt_on_error=monitor_halt_on_error,
307
+ monitor_halt_on_success=monitor_halt_on_success,
308
+ monitor_expect=monitor_expect,
309
+ timeout=ctx.timeout,
310
+ )
311
+ else:
312
+ if environment is None:
313
+ raise ValueError("environment is required when not using BuildContext")
314
+ return request_deploy(
315
+ project_dir=ctx_or_project_dir,
316
+ environment=environment,
317
+ port=port,
318
+ clean_build=clean_build,
319
+ monitor_after=monitor_after,
320
+ monitor_timeout=monitor_timeout,
321
+ monitor_halt_on_error=monitor_halt_on_error,
322
+ monitor_halt_on_success=monitor_halt_on_success,
323
+ monitor_expect=monitor_expect,
324
+ timeout=timeout,
325
+ )
326
+
327
+ @staticmethod
328
+ def monitor(
329
+ ctx_or_project_dir: BuildContext | Path,
330
+ environment: str | None = None,
331
+ port: str | None = None,
332
+ baud_rate: int | None = None,
333
+ halt_on_error: str | None = None,
334
+ halt_on_success: str | None = None,
335
+ expect: str | None = None,
336
+ timeout: float | None = None,
337
+ ) -> bool:
338
+ """Request a monitor operation from the daemon.
339
+
340
+ Can be called with a BuildContext or individual parameters.
341
+
342
+ Args:
343
+ ctx_or_project_dir: BuildContext or Path to project directory
344
+ environment: Build environment name (ignored if BuildContext passed)
345
+ port: Serial port (ignored if BuildContext passed)
346
+ baud_rate: Serial baud rate (optional)
347
+ halt_on_error: Pattern to halt on (error detection)
348
+ halt_on_success: Pattern to halt on (success detection)
349
+ expect: Expected pattern to check at timeout/success
350
+ timeout: Maximum monitoring time in seconds
351
+
352
+ Returns:
353
+ True if monitoring successful, False otherwise.
354
+ """
355
+ from fbuild.daemon import request_monitor
356
+
357
+ # Handle BuildContext or individual parameters
358
+ if isinstance(ctx_or_project_dir, BuildContext):
359
+ ctx = ctx_or_project_dir
360
+ return request_monitor(
361
+ project_dir=ctx.project_dir,
362
+ environment=ctx.environment,
363
+ port=ctx.port,
364
+ baud_rate=baud_rate,
365
+ halt_on_error=halt_on_error,
366
+ halt_on_success=halt_on_success,
367
+ expect=expect,
368
+ timeout=timeout if timeout is not None else ctx.timeout,
369
+ )
370
+ else:
371
+ if environment is None:
372
+ raise ValueError("environment is required when not using BuildContext")
373
+ return request_monitor(
374
+ project_dir=ctx_or_project_dir,
375
+ environment=environment,
376
+ port=port,
377
+ baud_rate=baud_rate,
378
+ halt_on_error=halt_on_error,
379
+ halt_on_success=halt_on_success,
380
+ expect=expect,
381
+ timeout=timeout,
382
+ )
383
+
384
+
385
+ __all__ = [
386
+ "__version__",
387
+ "is_available",
388
+ "BuildContext",
389
+ "Daemon",
390
+ ]
@@ -0,0 +1 @@
1
+ Example assets that will be deployed with python code.
@@ -0,0 +1,117 @@
1
+ """
2
+ Build system components for Fbuild.
3
+
4
+ This module provides the build system implementation including:
5
+ - Source file discovery and preprocessing
6
+ - Compilation (avr-gcc/avr-g++)
7
+ - Linking (avr-gcc linker, avr-objcopy)
8
+ - Build orchestration
9
+ """
10
+
11
+ from .source_scanner import SourceScanner, SourceCollection
12
+
13
+ __all__ = [
14
+ 'SourceScanner',
15
+ 'SourceCollection',
16
+ ]
17
+
18
+ # Import base classes
19
+ try:
20
+ from .orchestrator import ( # noqa: F401
21
+ IBuildOrchestrator,
22
+ BuildResult,
23
+ BuildOrchestratorError
24
+ )
25
+ __all__.extend(['IBuildOrchestrator', 'BuildResult', 'BuildOrchestratorError'])
26
+ except ImportError:
27
+ pass
28
+
29
+ try:
30
+ from .compiler import ICompiler, CompilerError, ILinker, LinkerError # noqa: F401
31
+ __all__.extend(['ICompiler', 'CompilerError', 'ILinker', 'LinkerError'])
32
+ except ImportError:
33
+ pass
34
+
35
+ # Import platform-specific implementations
36
+ try:
37
+ from .compiler_avr import CompilerAVR # noqa: F401
38
+ __all__.append('CompilerAVR')
39
+ except ImportError:
40
+ pass
41
+
42
+ try:
43
+ from .linker import LinkerAVR # noqa: F401
44
+ __all__.append('LinkerAVR')
45
+ except ImportError:
46
+ pass
47
+
48
+ try:
49
+ from .orchestrator_avr import BuildOrchestratorAVR # noqa: F401
50
+ __all__.append('BuildOrchestratorAVR')
51
+ except ImportError:
52
+ pass
53
+
54
+ try:
55
+ from .orchestrator_esp32 import OrchestratorESP32 # noqa: F401
56
+ __all__.append('OrchestratorESP32')
57
+ except ImportError:
58
+ pass
59
+
60
+ try:
61
+ from .binary_generator import BinaryGenerator # noqa: F401
62
+ __all__.append('BinaryGenerator')
63
+ except ImportError:
64
+ pass
65
+
66
+ try:
67
+ from .build_utils import SizeInfoPrinter # noqa: F401
68
+ __all__.append('SizeInfoPrinter')
69
+ except ImportError:
70
+ pass
71
+
72
+ try:
73
+ from .flag_builder import FlagBuilder # noqa: F401
74
+ __all__.append('FlagBuilder')
75
+ except ImportError:
76
+ pass
77
+
78
+ try:
79
+ from .compilation_executor import CompilationExecutor # noqa: F401
80
+ __all__.append('CompilationExecutor')
81
+ except ImportError:
82
+ pass
83
+
84
+ try:
85
+ from .archive_creator import ArchiveCreator # noqa: F401
86
+ __all__.append('ArchiveCreator')
87
+ except ImportError:
88
+ pass
89
+
90
+ try:
91
+ from .library_dependency_processor import ( # noqa: F401
92
+ LibraryDependencyProcessor,
93
+ LibraryProcessingResult
94
+ )
95
+ __all__.extend(['LibraryDependencyProcessor', 'LibraryProcessingResult'])
96
+ except ImportError:
97
+ pass
98
+
99
+ try:
100
+ from .source_compilation_orchestrator import ( # noqa: F401
101
+ SourceCompilationOrchestrator,
102
+ SourceCompilationOrchestratorError,
103
+ MultiGroupCompilationResult
104
+ )
105
+ __all__.extend([
106
+ 'SourceCompilationOrchestrator',
107
+ 'SourceCompilationOrchestratorError',
108
+ 'MultiGroupCompilationResult'
109
+ ])
110
+ except ImportError:
111
+ pass
112
+
113
+ try:
114
+ from .build_component_factory import BuildComponentFactory # noqa: F401
115
+ __all__.append('BuildComponentFactory')
116
+ except ImportError:
117
+ pass
@@ -0,0 +1,186 @@
1
+ """Archive Creator.
2
+
3
+ This module handles creating static library archives (.a files) from compiled
4
+ object files using the archiver tool (ar).
5
+
6
+ Design:
7
+ - Wraps ar command execution
8
+ - Creates .a archives from object files
9
+ - Provides clear error messages
10
+ - Shows archive size information
11
+ """
12
+
13
+ import gc
14
+ import platform
15
+ import subprocess
16
+ import time
17
+ from pathlib import Path
18
+ from typing import List
19
+
20
+
21
+ class ArchiveError(Exception):
22
+ """Raised when archive creation operations fail."""
23
+ pass
24
+
25
+
26
+ class ArchiveCreator:
27
+ """Creates static library archives from object files.
28
+
29
+ This class handles:
30
+ - Running archiver (ar) commands
31
+ - Creating .a archives from object files
32
+ - Validating archive creation
33
+ - Showing size information
34
+ """
35
+
36
+ def __init__(self, show_progress: bool = True):
37
+ """Initialize archive creator.
38
+
39
+ Args:
40
+ show_progress: Whether to show archive creation progress
41
+ """
42
+ self.show_progress = show_progress
43
+
44
+ def create_archive(
45
+ self,
46
+ ar_path: Path,
47
+ archive_path: Path,
48
+ object_files: List[Path]
49
+ ) -> Path:
50
+ """Create static library archive from object files.
51
+
52
+ Args:
53
+ ar_path: Path to archiver tool (ar)
54
+ archive_path: Path for output .a file
55
+ object_files: List of object file paths to archive
56
+
57
+ Returns:
58
+ Path to generated archive file
59
+
60
+ Raises:
61
+ ArchiveError: If archive creation fails
62
+ """
63
+ if not object_files:
64
+ raise ArchiveError("No object files provided for archive")
65
+
66
+ if not ar_path.exists():
67
+ raise ArchiveError(
68
+ f"Archiver not found: {ar_path}. Ensure toolchain is installed."
69
+ )
70
+
71
+ # Ensure archive directory exists
72
+ archive_path.parent.mkdir(parents=True, exist_ok=True)
73
+
74
+ # Build archiver command
75
+ # 'rcs' flags: r=insert/replace, c=create, s=index (ranlib)
76
+ cmd = [str(ar_path), "rcs", str(archive_path)]
77
+ cmd.extend([str(obj) for obj in object_files])
78
+
79
+ # Execute archiver
80
+ if self.show_progress:
81
+ print(f"Creating {archive_path.name} archive from {len(object_files)} object files...")
82
+
83
+ # On Windows, add retry logic to handle file locking issues
84
+ # Object files may still have open handles from compiler/antivirus
85
+ is_windows = platform.system() == "Windows"
86
+ max_retries = 5 if is_windows else 1
87
+ delay = 0.1
88
+ last_error = None
89
+
90
+ for attempt in range(max_retries):
91
+ try:
92
+ # On Windows, force garbage collection and add delay before retry
93
+ if is_windows and attempt > 0:
94
+ gc.collect()
95
+ time.sleep(delay)
96
+ if self.show_progress:
97
+ print(f" Retrying archive creation (attempt {attempt + 1}/{max_retries})...")
98
+
99
+ result = subprocess.run(
100
+ cmd,
101
+ capture_output=True,
102
+ text=True,
103
+ timeout=60
104
+ )
105
+
106
+ if result.returncode != 0:
107
+ # Check if error is due to file truncation/locking (Windows-specific)
108
+ # Windows file locking manifests as: "file truncated", "error reading", or "No such file"
109
+ stderr_lower = result.stderr.lower()
110
+ is_file_locking_error = (
111
+ "file truncated" in stderr_lower or
112
+ "error reading" in stderr_lower or
113
+ "no such file" in stderr_lower
114
+ )
115
+ if is_windows and is_file_locking_error:
116
+ last_error = result.stderr
117
+ if attempt < max_retries - 1:
118
+ if self.show_progress:
119
+ print(" [Windows] Detected file locking error, retrying...")
120
+ delay = min(delay * 2, 1.0) # Exponential backoff, max 1s
121
+ continue
122
+ else:
123
+ # Last attempt failed
124
+ error_msg = f"Archive creation failed after {max_retries} attempts (file locking)\n"
125
+ error_msg += f"stderr: {result.stderr}\n"
126
+ error_msg += f"stdout: {result.stdout}"
127
+ raise ArchiveError(error_msg)
128
+
129
+ error_msg = f"Archive creation failed for {archive_path.name}\n"
130
+ error_msg += f"stderr: {result.stderr}\n"
131
+ error_msg += f"stdout: {result.stdout}"
132
+ raise ArchiveError(error_msg)
133
+
134
+ if not archive_path.exists():
135
+ raise ArchiveError(f"Archive was not created: {archive_path}")
136
+
137
+ if self.show_progress:
138
+ size = archive_path.stat().st_size
139
+ print(f"✓ Created {archive_path.name}: {size:,} bytes ({size / 1024 / 1024:.2f} MB)")
140
+
141
+ return archive_path
142
+
143
+ except subprocess.TimeoutExpired as e:
144
+ raise ArchiveError(f"Archive creation timeout for {archive_path.name}") from e
145
+ except KeyboardInterrupt as ke:
146
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
147
+ handle_keyboard_interrupt_properly(ke)
148
+ raise # Never reached, but satisfies type checker
149
+ except Exception as e:
150
+ if isinstance(e, ArchiveError):
151
+ raise
152
+ # If Windows file locking error, retry
153
+ if is_windows and attempt < max_retries - 1:
154
+ last_error = str(e)
155
+ delay = min(delay * 2, 1.0)
156
+ continue
157
+ raise ArchiveError(f"Failed to create archive {archive_path.name}: {e}") from e
158
+
159
+ # If we exhausted retries, raise the last error
160
+ if last_error:
161
+ raise ArchiveError(f"Archive creation failed after {max_retries} attempts: {last_error}")
162
+ raise ArchiveError(f"Archive creation failed after {max_retries} attempts")
163
+
164
+ def create_core_archive(
165
+ self,
166
+ ar_path: Path,
167
+ build_dir: Path,
168
+ object_files: List[Path]
169
+ ) -> Path:
170
+ """Create core.a archive from core object files.
171
+
172
+ Convenience method for creating the standard core.a archive.
173
+
174
+ Args:
175
+ ar_path: Path to archiver tool (ar)
176
+ build_dir: Build directory
177
+ object_files: List of core object file paths
178
+
179
+ Returns:
180
+ Path to generated core.a file
181
+
182
+ Raises:
183
+ ArchiveError: If archive creation fails
184
+ """
185
+ archive_path = build_dir / "core.a"
186
+ return self.create_archive(ar_path, archive_path, object_files)