fbuild 1.1.0__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.

Potentially problematic release.


This version of fbuild might be problematic. Click here for more details.

Files changed (93) hide show
  1. fbuild/__init__.py +0 -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_state.py +325 -0
  8. fbuild/build/build_utils.py +98 -0
  9. fbuild/build/compilation_executor.py +422 -0
  10. fbuild/build/compiler.py +165 -0
  11. fbuild/build/compiler_avr.py +574 -0
  12. fbuild/build/configurable_compiler.py +612 -0
  13. fbuild/build/configurable_linker.py +637 -0
  14. fbuild/build/flag_builder.py +186 -0
  15. fbuild/build/library_dependency_processor.py +185 -0
  16. fbuild/build/linker.py +708 -0
  17. fbuild/build/orchestrator.py +67 -0
  18. fbuild/build/orchestrator_avr.py +656 -0
  19. fbuild/build/orchestrator_esp32.py +797 -0
  20. fbuild/build/orchestrator_teensy.py +543 -0
  21. fbuild/build/source_compilation_orchestrator.py +220 -0
  22. fbuild/build/source_scanner.py +516 -0
  23. fbuild/cli.py +566 -0
  24. fbuild/cli_utils.py +312 -0
  25. fbuild/config/__init__.py +16 -0
  26. fbuild/config/board_config.py +457 -0
  27. fbuild/config/board_loader.py +92 -0
  28. fbuild/config/ini_parser.py +209 -0
  29. fbuild/config/mcu_specs.py +88 -0
  30. fbuild/daemon/__init__.py +34 -0
  31. fbuild/daemon/client.py +929 -0
  32. fbuild/daemon/compilation_queue.py +293 -0
  33. fbuild/daemon/daemon.py +474 -0
  34. fbuild/daemon/daemon_context.py +196 -0
  35. fbuild/daemon/error_collector.py +263 -0
  36. fbuild/daemon/file_cache.py +332 -0
  37. fbuild/daemon/lock_manager.py +270 -0
  38. fbuild/daemon/logging_utils.py +149 -0
  39. fbuild/daemon/messages.py +301 -0
  40. fbuild/daemon/operation_registry.py +288 -0
  41. fbuild/daemon/process_tracker.py +366 -0
  42. fbuild/daemon/processors/__init__.py +12 -0
  43. fbuild/daemon/processors/build_processor.py +157 -0
  44. fbuild/daemon/processors/deploy_processor.py +327 -0
  45. fbuild/daemon/processors/monitor_processor.py +146 -0
  46. fbuild/daemon/request_processor.py +401 -0
  47. fbuild/daemon/status_manager.py +216 -0
  48. fbuild/daemon/subprocess_manager.py +316 -0
  49. fbuild/deploy/__init__.py +17 -0
  50. fbuild/deploy/deployer.py +67 -0
  51. fbuild/deploy/deployer_esp32.py +314 -0
  52. fbuild/deploy/monitor.py +495 -0
  53. fbuild/interrupt_utils.py +34 -0
  54. fbuild/packages/__init__.py +53 -0
  55. fbuild/packages/archive_utils.py +1098 -0
  56. fbuild/packages/arduino_core.py +412 -0
  57. fbuild/packages/cache.py +249 -0
  58. fbuild/packages/downloader.py +366 -0
  59. fbuild/packages/framework_esp32.py +538 -0
  60. fbuild/packages/framework_teensy.py +346 -0
  61. fbuild/packages/github_utils.py +96 -0
  62. fbuild/packages/header_trampoline_cache.py +394 -0
  63. fbuild/packages/library_compiler.py +203 -0
  64. fbuild/packages/library_manager.py +549 -0
  65. fbuild/packages/library_manager_esp32.py +413 -0
  66. fbuild/packages/package.py +163 -0
  67. fbuild/packages/platform_esp32.py +383 -0
  68. fbuild/packages/platform_teensy.py +312 -0
  69. fbuild/packages/platform_utils.py +131 -0
  70. fbuild/packages/platformio_registry.py +325 -0
  71. fbuild/packages/sdk_utils.py +231 -0
  72. fbuild/packages/toolchain.py +436 -0
  73. fbuild/packages/toolchain_binaries.py +196 -0
  74. fbuild/packages/toolchain_esp32.py +484 -0
  75. fbuild/packages/toolchain_metadata.py +185 -0
  76. fbuild/packages/toolchain_teensy.py +404 -0
  77. fbuild/platform_configs/esp32.json +150 -0
  78. fbuild/platform_configs/esp32c2.json +144 -0
  79. fbuild/platform_configs/esp32c3.json +143 -0
  80. fbuild/platform_configs/esp32c5.json +151 -0
  81. fbuild/platform_configs/esp32c6.json +151 -0
  82. fbuild/platform_configs/esp32p4.json +149 -0
  83. fbuild/platform_configs/esp32s3.json +151 -0
  84. fbuild/platform_configs/imxrt1062.json +56 -0
  85. fbuild-1.1.0.dist-info/METADATA +447 -0
  86. fbuild-1.1.0.dist-info/RECORD +93 -0
  87. fbuild-1.1.0.dist-info/WHEEL +5 -0
  88. fbuild-1.1.0.dist-info/entry_points.txt +5 -0
  89. fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
  90. fbuild-1.1.0.dist-info/top_level.txt +2 -0
  91. fbuild_lint/__init__.py +0 -0
  92. fbuild_lint/ruff_plugins/__init__.py +0 -0
  93. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,412 @@
1
+ """Arduino core platform management.
2
+
3
+ This module handles downloading and managing Arduino core platforms
4
+ (e.g., ArduinoCore-avr) required for building Arduino sketches.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+
10
+ from .cache import Cache
11
+ from .downloader import PackageDownloader
12
+ from .package import IFramework, PackageError
13
+
14
+
15
+ class ArduinoCoreError(PackageError):
16
+ """Raised when Arduino core operations fail."""
17
+
18
+ pass
19
+
20
+
21
+ class ArduinoCore(IFramework):
22
+ """Manages Arduino core platform packages."""
23
+
24
+ # Arduino AVR core version
25
+ AVR_VERSION = "1.8.6"
26
+
27
+ # Package URL and checksum
28
+ # The Arduino AVR core is hosted on GitHub
29
+ AVR_URL = f"https://github.com/arduino/ArduinoCore-avr/archive/refs/tags/{AVR_VERSION}.tar.gz"
30
+ AVR_CHECKSUM = "49241fd5e504482b94954b5843c7d69ce38ebc1ab47ad3b677e8bb77e0cb8fe6"
31
+
32
+ def __init__(self, cache: Cache):
33
+ """Initialize Arduino core manager.
34
+
35
+ Args:
36
+ cache: Cache instance for storing cores
37
+ """
38
+ self.cache = cache
39
+ self.downloader = PackageDownloader()
40
+ self._core_path: Optional[Path] = None
41
+
42
+ def ensure_package(self) -> Path:
43
+ """Ensure Arduino AVR core is available.
44
+
45
+ Returns:
46
+ Path to Arduino AVR core directory
47
+
48
+ Raises:
49
+ ArduinoCoreError: If core cannot be obtained
50
+ """
51
+ return self.ensure_avr_core()
52
+
53
+ def ensure_avr_core(self, force_download: bool = False) -> Path:
54
+ """Ensure Arduino AVR core is available.
55
+
56
+ Args:
57
+ force_download: Force re-download even if cached
58
+
59
+ Returns:
60
+ Path to Arduino AVR core directory
61
+
62
+ Raises:
63
+ ArduinoCoreError: If core cannot be obtained
64
+ """
65
+ # Check if already loaded
66
+ if self._core_path and not force_download:
67
+ return self._core_path
68
+
69
+ # Use URL-based caching
70
+ core_path = self.cache.get_platform_path(self.AVR_URL, self.AVR_VERSION)
71
+ package_name = f"avr-{self.AVR_VERSION}.tar.gz"
72
+ package_path = self.cache.get_package_path(self.AVR_URL, self.AVR_VERSION, package_name)
73
+
74
+ # Check if already extracted and valid
75
+ if not force_download and self.cache.is_platform_cached(self.AVR_URL, self.AVR_VERSION):
76
+ if self._verify_core(core_path):
77
+ self._core_path = core_path
78
+ return core_path
79
+ else:
80
+ print("Cached Arduino core failed validation, re-downloading...")
81
+
82
+ # Need to download and extract
83
+ self.cache.ensure_directories()
84
+
85
+ print(f"Downloading Arduino AVR core ({self.AVR_VERSION})...")
86
+
87
+ try:
88
+ # Ensure package directory exists
89
+ package_path.parent.mkdir(parents=True, exist_ok=True)
90
+
91
+ # Download if not cached
92
+ if force_download or not package_path.exists():
93
+ self.downloader.download(self.AVR_URL, package_path, self.AVR_CHECKSUM)
94
+ else:
95
+ print(f"Using cached {package_name}")
96
+
97
+ # Extract
98
+ print("Extracting Arduino core...")
99
+ core_path.parent.mkdir(parents=True, exist_ok=True)
100
+
101
+ # Extract to temporary location first
102
+ import shutil
103
+ import tempfile
104
+
105
+ with tempfile.TemporaryDirectory() as temp_dir:
106
+ temp_path = Path(temp_dir)
107
+ self.downloader.extract_archive(package_path, temp_path, show_progress=False)
108
+
109
+ # The archive extracts to avr/ subdirectory
110
+ extracted_dir = temp_path / "avr"
111
+ if not extracted_dir.exists():
112
+ # If not in avr/ subdirectory, use first directory found
113
+ extracted_dirs = [d for d in temp_path.iterdir() if d.is_dir()]
114
+ if extracted_dirs:
115
+ extracted_dir = extracted_dirs[0]
116
+ else:
117
+ raise ArduinoCoreError("No directory found in extracted archive")
118
+
119
+ # Move to final location
120
+ if core_path.exists():
121
+ shutil.rmtree(core_path)
122
+ shutil.move(str(extracted_dir), str(core_path))
123
+
124
+ # Verify installation
125
+ if not self._verify_core(core_path):
126
+ raise ArduinoCoreError("Core verification failed after extraction")
127
+
128
+ self._core_path = core_path
129
+ print(f"Arduino core ready at {core_path}")
130
+ return core_path
131
+
132
+ except KeyboardInterrupt as ke:
133
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
134
+
135
+ handle_keyboard_interrupt_properly(ke)
136
+ raise # Never reached, but satisfies type checker
137
+ except Exception as e:
138
+ raise ArduinoCoreError(f"Failed to setup Arduino core: {e}")
139
+
140
+ def is_installed(self) -> bool:
141
+ """Check if Arduino core is already installed.
142
+
143
+ Returns:
144
+ True if core is installed and valid
145
+ """
146
+ if not self._core_path:
147
+ # Check cache
148
+ core_path = self.cache.get_platform_path(self.AVR_URL, self.AVR_VERSION)
149
+ if core_path.exists():
150
+ return self._verify_core(core_path)
151
+ return False
152
+ return self._verify_core(self._core_path)
153
+
154
+ def get_package_info(self) -> Dict[str, Any]:
155
+ """Get information about the installed core.
156
+
157
+ Returns:
158
+ Dictionary with core information
159
+ """
160
+ info = {
161
+ "version": self.AVR_VERSION,
162
+ "url": self.AVR_URL,
163
+ "installed": self.is_installed(),
164
+ }
165
+
166
+ if self._core_path:
167
+ info["path"] = str(self._core_path)
168
+ info["cores_dir"] = str(self.get_cores_dir())
169
+ info["variants_dir"] = str(self.get_variants_dir())
170
+
171
+ return info
172
+
173
+ def _verify_core(self, core_path: Path) -> bool:
174
+ """Comprehensively verify Arduino core is complete.
175
+
176
+ Checks for:
177
+ - Required directories (cores/arduino, variants)
178
+ - Configuration files (boards.txt, platform.txt)
179
+ - Key core source files
180
+ - Key header files
181
+
182
+ Args:
183
+ core_path: Path to core directory
184
+
185
+ Returns:
186
+ True if core appears valid
187
+ """
188
+ # Check for essential directories
189
+ required_dirs = [
190
+ "cores/arduino",
191
+ "variants",
192
+ "variants/standard", # Uno variant
193
+ ]
194
+
195
+ for dir_path in required_dirs:
196
+ if not (core_path / dir_path).exists():
197
+ print(f"Missing directory: {dir_path}")
198
+ return False
199
+
200
+ # Check for essential configuration files
201
+ required_files = [
202
+ "boards.txt",
203
+ "platform.txt",
204
+ ]
205
+
206
+ for file_path in required_files:
207
+ if not (core_path / file_path).exists():
208
+ print(f"Missing file: {file_path}")
209
+ return False
210
+
211
+ # Check for key core header files
212
+ required_headers = [
213
+ "cores/arduino/Arduino.h",
214
+ "cores/arduino/HardwareSerial.h",
215
+ "variants/standard/pins_arduino.h",
216
+ ]
217
+
218
+ for header in required_headers:
219
+ if not (core_path / header).exists():
220
+ print(f"Missing header: {header}")
221
+ return False
222
+
223
+ # Check for key core source files
224
+ required_sources = [
225
+ "cores/arduino/main.cpp",
226
+ "cores/arduino/wiring.c",
227
+ "cores/arduino/wiring_digital.c",
228
+ ]
229
+
230
+ for source in required_sources:
231
+ if not (core_path / source).exists():
232
+ print(f"Missing source: {source}")
233
+ return False
234
+
235
+ # Verify boards.txt contains uno configuration
236
+ boards_txt = core_path / "boards.txt"
237
+ try:
238
+ content = boards_txt.read_text(encoding="utf-8", errors="ignore")
239
+ if "uno.name" not in content:
240
+ print("boards.txt missing uno configuration")
241
+ return False
242
+ except KeyboardInterrupt as ke:
243
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
244
+
245
+ handle_keyboard_interrupt_properly(ke)
246
+ raise # Never reached, but satisfies type checker
247
+ except Exception as e:
248
+ print(f"Failed to read boards.txt: {e}")
249
+ return False
250
+
251
+ return True
252
+
253
+ def get_boards_txt(self) -> Path:
254
+ """Get path to boards.txt file.
255
+
256
+ Returns:
257
+ Path to boards.txt
258
+
259
+ Raises:
260
+ ArduinoCoreError: If core not initialized
261
+ """
262
+ if not self._core_path:
263
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
264
+
265
+ boards_txt = self._core_path / "boards.txt"
266
+ if not boards_txt.exists():
267
+ raise ArduinoCoreError("boards.txt not found in core")
268
+
269
+ return boards_txt
270
+
271
+ def get_platform_txt(self) -> Path:
272
+ """Get path to platform.txt file.
273
+
274
+ Returns:
275
+ Path to platform.txt
276
+
277
+ Raises:
278
+ ArduinoCoreError: If core not initialized
279
+ """
280
+ if not self._core_path:
281
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
282
+
283
+ platform_txt = self._core_path / "platform.txt"
284
+ if not platform_txt.exists():
285
+ raise ArduinoCoreError("platform.txt not found in core")
286
+
287
+ return platform_txt
288
+
289
+ def get_cores_dir(self) -> Path:
290
+ """Get path to cores directory.
291
+
292
+ Returns:
293
+ Path to cores directory
294
+
295
+ Raises:
296
+ ArduinoCoreError: If core not initialized
297
+ """
298
+ if not self._core_path:
299
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
300
+
301
+ cores_dir = self._core_path / "cores"
302
+ if not cores_dir.exists():
303
+ raise ArduinoCoreError("cores directory not found")
304
+
305
+ return cores_dir
306
+
307
+ def get_variants_dir(self) -> Path:
308
+ """Get path to variants directory.
309
+
310
+ Returns:
311
+ Path to variants directory
312
+
313
+ Raises:
314
+ ArduinoCoreError: If core not initialized
315
+ """
316
+ if not self._core_path:
317
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
318
+
319
+ variants_dir = self._core_path / "variants"
320
+ if not variants_dir.exists():
321
+ raise ArduinoCoreError("variants directory not found")
322
+
323
+ return variants_dir
324
+
325
+ def get_libraries_dir(self) -> Path:
326
+ """Get path to built-in libraries directory.
327
+
328
+ Returns:
329
+ Path to libraries directory
330
+
331
+ Raises:
332
+ ArduinoCoreError: If core not initialized
333
+ """
334
+ if not self._core_path:
335
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
336
+
337
+ # Arduino AVR core doesn't have a libraries directory, return a non-existent path
338
+ libraries_dir = self._core_path / "libraries"
339
+ return libraries_dir
340
+
341
+ def get_core_dir(self) -> Path:
342
+ """Get path to cores/arduino directory.
343
+
344
+ Returns:
345
+ Path to core library sources
346
+
347
+ Raises:
348
+ ArduinoCoreError: If core not initialized
349
+ """
350
+ if not self._core_path:
351
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
352
+
353
+ core_dir = self._core_path / "cores" / "arduino"
354
+ if not core_dir.exists():
355
+ raise ArduinoCoreError("cores/arduino directory not found")
356
+
357
+ return core_dir
358
+
359
+ def get_variant_dir(self, variant_name: str = "standard") -> Path:
360
+ """Get path to a board variant directory.
361
+
362
+ Args:
363
+ variant_name: Name of the variant (e.g., 'standard' for Uno)
364
+
365
+ Returns:
366
+ Path to variant directory
367
+
368
+ Raises:
369
+ ArduinoCoreError: If core not initialized or variant not found
370
+ """
371
+ if not self._core_path:
372
+ raise ArduinoCoreError("Core not initialized. Call ensure_avr_core() first.")
373
+
374
+ variant_dir = self._core_path / "variants" / variant_name
375
+ if not variant_dir.exists():
376
+ raise ArduinoCoreError(f"Variant '{variant_name}' not found")
377
+
378
+ return variant_dir
379
+
380
+ def get_core_sources(self) -> list[Path]:
381
+ """Get list of all core source files (.c and .cpp).
382
+
383
+ Returns:
384
+ List of paths to core source files
385
+
386
+ Raises:
387
+ ArduinoCoreError: If core not initialized
388
+ """
389
+ core_dir = self.get_core_dir()
390
+
391
+ sources: list[Path] = []
392
+ for pattern in ("*.c", "*.cpp"):
393
+ sources.extend(core_dir.glob(pattern))
394
+
395
+ return sorted(sources)
396
+
397
+ def get_variant_sources(self, variant_name: str = "standard") -> list[Path]:
398
+ """Get list of variant source files.
399
+
400
+ Args:
401
+ variant_name: Name of the variant
402
+
403
+ Returns:
404
+ List of paths to variant source files
405
+ """
406
+ variant_dir = self.get_variant_dir(variant_name)
407
+
408
+ sources: list[Path] = []
409
+ for pattern in ("*.c", "*.cpp"):
410
+ sources.extend(variant_dir.glob(pattern))
411
+
412
+ return sorted(sources)
@@ -0,0 +1,249 @@
1
+ """Cache management for fbuild packages.
2
+
3
+ This module provides a unified cache structure for storing downloaded
4
+ packages, toolchains, platforms, and build artifacts.
5
+
6
+ Cache Structure:
7
+ .fbuild/
8
+ ├── cache/
9
+ │ ├── packages/
10
+ │ │ └── {url_hash}/ # SHA256 hash of base URL
11
+ │ │ └── {version}/ # Version string
12
+ │ │ └── archive # Downloaded archive
13
+ │ ├── toolchains/
14
+ │ │ └── {url_hash}/ # SHA256 hash of base URL
15
+ │ │ └── {version}/ # Version string
16
+ │ │ └── bin/ # Extracted toolchain binaries
17
+ │ ├── platforms/
18
+ │ │ └── {url_hash}/ # SHA256 hash of base URL
19
+ │ │ └── {version}/ # Version string
20
+ │ │ ├── cores/
21
+ │ │ ├── variants/
22
+ │ │ ├── boards.txt
23
+ │ │ └── platform.txt
24
+ │ └── libraries/
25
+ │ └── {url_hash}/ # SHA256 hash of base URL
26
+ │ └── {version}/ # Version string
27
+ └── build/
28
+ └── {env_name}/ # Build output per environment
29
+ ├── core/ # Compiled core objects
30
+ ├── src/ # Compiled sketch objects
31
+ └── firmware.* # Final firmware files
32
+
33
+ This structure ensures that different versions from the same URL don't
34
+ stomp on each other, and allows multiple sources to coexist.
35
+ """
36
+
37
+ import hashlib
38
+ import os
39
+ from pathlib import Path
40
+ from typing import Optional
41
+
42
+
43
+ class Cache:
44
+ """Manages the fbuild cache directory structure.
45
+
46
+ The cache can be located in the project directory (.fbuild/) or in a
47
+ global location specified by the FBUILD_CACHE_DIR environment variable.
48
+
49
+ Uses URL hashing to organize cached items, preventing version conflicts.
50
+ """
51
+
52
+ def __init__(self, project_dir: Optional[Path] = None):
53
+ """Initialize cache manager.
54
+
55
+ Args:
56
+ project_dir: Project directory. If None, uses current directory.
57
+ """
58
+ if project_dir is None:
59
+ project_dir = Path.cwd()
60
+
61
+ self.project_dir = Path(project_dir).resolve()
62
+
63
+ # Check for environment variable override
64
+ cache_env = os.environ.get("FBUILD_CACHE_DIR")
65
+ if cache_env:
66
+ self.cache_root = Path(cache_env).resolve()
67
+ else:
68
+ self.cache_root = self.project_dir / ".fbuild" / "cache"
69
+
70
+ self.build_root = self.project_dir / ".fbuild" / "build"
71
+
72
+ @staticmethod
73
+ def hash_url(url: str) -> str:
74
+ """Generate a SHA256 hash of a URL for cache directory naming.
75
+
76
+ Args:
77
+ url: The base URL to hash
78
+
79
+ Returns:
80
+ First 16 characters of SHA256 hash (sufficient for uniqueness)
81
+ """
82
+ return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
83
+
84
+ @property
85
+ def packages_dir(self) -> Path:
86
+ """Directory for downloaded package archives."""
87
+ return self.cache_root / "packages"
88
+
89
+ @property
90
+ def toolchains_dir(self) -> Path:
91
+ """Directory for extracted toolchain binaries."""
92
+ return self.cache_root / "toolchains"
93
+
94
+ @property
95
+ def platforms_dir(self) -> Path:
96
+ """Directory for extracted platform cores."""
97
+ return self.cache_root / "platforms"
98
+
99
+ @property
100
+ def libraries_dir(self) -> Path:
101
+ """Directory for downloaded libraries."""
102
+ return self.cache_root / "libraries"
103
+
104
+ def get_build_dir(self, env_name: str) -> Path:
105
+ """Get build directory for a specific environment.
106
+
107
+ Args:
108
+ env_name: Environment name (e.g., 'uno', 'mega')
109
+
110
+ Returns:
111
+ Path to the environment's build directory
112
+ """
113
+ return self.build_root / env_name
114
+
115
+ def get_core_build_dir(self, env_name: str) -> Path:
116
+ """Get directory for compiled core objects.
117
+
118
+ Args:
119
+ env_name: Environment name
120
+
121
+ Returns:
122
+ Path to core build directory
123
+ """
124
+ return self.get_build_dir(env_name) / "core"
125
+
126
+ def get_src_build_dir(self, env_name: str) -> Path:
127
+ """Get directory for compiled sketch objects.
128
+
129
+ Args:
130
+ env_name: Environment name
131
+
132
+ Returns:
133
+ Path to sketch build directory
134
+ """
135
+ return self.get_build_dir(env_name) / "src"
136
+
137
+ def ensure_directories(self) -> None:
138
+ """Create all cache directories if they don't exist."""
139
+ for directory in [
140
+ self.packages_dir,
141
+ self.toolchains_dir,
142
+ self.platforms_dir,
143
+ self.libraries_dir,
144
+ ]:
145
+ directory.mkdir(parents=True, exist_ok=True)
146
+
147
+ def ensure_build_directories(self, env_name: str) -> None:
148
+ """Create build directories for a specific environment.
149
+
150
+ Args:
151
+ env_name: Environment name
152
+ """
153
+ for directory in [
154
+ self.get_build_dir(env_name),
155
+ self.get_core_build_dir(env_name),
156
+ self.get_src_build_dir(env_name),
157
+ ]:
158
+ directory.mkdir(parents=True, exist_ok=True)
159
+
160
+ def clean_build(self, env_name: str) -> None:
161
+ """Remove all build artifacts for an environment.
162
+
163
+ Args:
164
+ env_name: Environment name
165
+ """
166
+ import shutil
167
+
168
+ build_dir = self.get_build_dir(env_name)
169
+ if build_dir.exists():
170
+ shutil.rmtree(build_dir)
171
+
172
+ def get_package_path(self, url: str, version: str, filename: str) -> Path:
173
+ """Get path where a package archive would be stored.
174
+
175
+ Args:
176
+ url: Base URL for the package source
177
+ version: Version string (e.g., '7.3.0-atmel3.6.1-arduino7')
178
+ filename: Archive filename (e.g., 'avr-gcc-7.3.0.tar.bz2')
179
+
180
+ Returns:
181
+ Path to the package archive
182
+ """
183
+ url_hash = self.hash_url(url)
184
+ return self.packages_dir / url_hash / version / filename
185
+
186
+ def get_toolchain_path(self, url: str, version: str) -> Path:
187
+ """Get path where a toolchain would be extracted.
188
+
189
+ Args:
190
+ url: Base URL for the toolchain source
191
+ version: Version string (e.g., '7.3.0-atmel3.6.1-arduino7')
192
+
193
+ Returns:
194
+ Path to the extracted toolchain directory
195
+ """
196
+ url_hash = self.hash_url(url)
197
+ return self.toolchains_dir / url_hash / version
198
+
199
+ def get_platform_path(self, url: str, version: str) -> Path:
200
+ """Get path where a platform would be extracted.
201
+
202
+ Args:
203
+ url: Base URL for the platform source
204
+ version: Version string (e.g., '1.8.6')
205
+
206
+ Returns:
207
+ Path to the extracted platform directory
208
+ """
209
+ url_hash = self.hash_url(url)
210
+ return self.platforms_dir / url_hash / version
211
+
212
+ def is_package_cached(self, url: str, version: str, filename: str) -> bool:
213
+ """Check if a package is already downloaded.
214
+
215
+ Args:
216
+ url: Base URL for the package source
217
+ version: Version string
218
+ filename: Archive filename
219
+
220
+ Returns:
221
+ True if package exists in cache
222
+ """
223
+ return self.get_package_path(url, version, filename).exists()
224
+
225
+ def is_toolchain_cached(self, url: str, version: str) -> bool:
226
+ """Check if a toolchain is already extracted.
227
+
228
+ Args:
229
+ url: Base URL for the toolchain source
230
+ version: Version string
231
+
232
+ Returns:
233
+ True if toolchain exists in cache
234
+ """
235
+ toolchain_path = self.get_toolchain_path(url, version)
236
+ return toolchain_path.exists() and toolchain_path.is_dir()
237
+
238
+ def is_platform_cached(self, url: str, version: str) -> bool:
239
+ """Check if a platform is already extracted.
240
+
241
+ Args:
242
+ url: Base URL for the platform source
243
+ version: Version string
244
+
245
+ Returns:
246
+ True if platform exists in cache
247
+ """
248
+ platform_path = self.get_platform_path(url, version)
249
+ return platform_path.exists() and platform_path.is_dir()