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.
- fbuild/__init__.py +390 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Firmware deployment module for uploading to embedded devices.
|
|
3
|
+
|
|
4
|
+
This module handles flashing firmware to ESP32 devices using esptool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from fbuild.config import PlatformIOConfig
|
|
14
|
+
from fbuild.packages import Cache
|
|
15
|
+
|
|
16
|
+
from .deployer import DeploymentError, DeploymentResult, IDeployer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ESP32Deployer(IDeployer):
|
|
20
|
+
"""Handles firmware deployment to embedded devices."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, verbose: bool = False):
|
|
23
|
+
"""Initialize deployer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
verbose: Whether to show verbose output
|
|
27
|
+
"""
|
|
28
|
+
self.verbose = verbose
|
|
29
|
+
|
|
30
|
+
def deploy(
|
|
31
|
+
self,
|
|
32
|
+
project_dir: Path,
|
|
33
|
+
env_name: str,
|
|
34
|
+
port: Optional[str] = None,
|
|
35
|
+
) -> DeploymentResult:
|
|
36
|
+
"""Deploy firmware to a device.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
project_dir: Path to project directory
|
|
40
|
+
env_name: Environment name to deploy
|
|
41
|
+
port: Serial port to use (auto-detect if None)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
DeploymentResult with success status and message
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Load platformio.ini
|
|
48
|
+
ini_path = project_dir / "platformio.ini"
|
|
49
|
+
if not ini_path.exists():
|
|
50
|
+
raise DeploymentError(f"platformio.ini not found in {project_dir}")
|
|
51
|
+
|
|
52
|
+
config = PlatformIOConfig(ini_path)
|
|
53
|
+
env_config = config.get_env_config(env_name)
|
|
54
|
+
|
|
55
|
+
# Get board and platform
|
|
56
|
+
board_id = env_config.get("board")
|
|
57
|
+
platform_url = env_config.get("platform")
|
|
58
|
+
|
|
59
|
+
if not board_id or not platform_url:
|
|
60
|
+
raise DeploymentError("Board or platform not specified in platformio.ini")
|
|
61
|
+
|
|
62
|
+
# Determine platform type
|
|
63
|
+
if "espressif32" in platform_url or board_id.startswith("esp32"):
|
|
64
|
+
return self._deploy_esp32(project_dir, env_name, board_id, port, platform_url)
|
|
65
|
+
else:
|
|
66
|
+
raise DeploymentError(f"Deployment not supported for board: {board_id}")
|
|
67
|
+
|
|
68
|
+
except DeploymentError as e:
|
|
69
|
+
return DeploymentResult(success=False, message=str(e))
|
|
70
|
+
except KeyboardInterrupt as ke:
|
|
71
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
72
|
+
|
|
73
|
+
handle_keyboard_interrupt_properly(ke)
|
|
74
|
+
raise # Never reached, but satisfies type checker
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return DeploymentResult(success=False, message=f"Unexpected deployment error: {e}")
|
|
77
|
+
|
|
78
|
+
def _deploy_esp32(
|
|
79
|
+
self,
|
|
80
|
+
project_dir: Path,
|
|
81
|
+
env_name: str,
|
|
82
|
+
board_id: str,
|
|
83
|
+
port: Optional[str],
|
|
84
|
+
platform_url: str,
|
|
85
|
+
) -> DeploymentResult:
|
|
86
|
+
"""Deploy firmware to ESP32 device.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
project_dir: Path to project directory
|
|
90
|
+
env_name: Environment name
|
|
91
|
+
board_id: Board identifier
|
|
92
|
+
port: Serial port (auto-detect if None)
|
|
93
|
+
platform_url: Platform package URL
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
DeploymentResult with success status
|
|
97
|
+
"""
|
|
98
|
+
# Get build directory
|
|
99
|
+
build_dir = project_dir / ".fbuild" / "build" / env_name
|
|
100
|
+
firmware_bin = (build_dir / "firmware.bin").absolute()
|
|
101
|
+
bootloader_bin = (build_dir / "bootloader.bin").absolute()
|
|
102
|
+
partitions_bin = (build_dir / "partitions.bin").absolute()
|
|
103
|
+
|
|
104
|
+
if not firmware_bin.exists():
|
|
105
|
+
raise DeploymentError(f"Firmware not found at {firmware_bin}. Run 'fbuild build' first.")
|
|
106
|
+
|
|
107
|
+
# Get cache and ensure platform/toolchain packages
|
|
108
|
+
cache = Cache(project_dir)
|
|
109
|
+
|
|
110
|
+
# Import ESP32 packages
|
|
111
|
+
from fbuild.packages.framework_esp32 import FrameworkESP32
|
|
112
|
+
from fbuild.packages.platform_esp32 import PlatformESP32
|
|
113
|
+
|
|
114
|
+
# Ensure platform is downloaded first (needed to get board JSON)
|
|
115
|
+
platform = PlatformESP32(cache, platform_url, show_progress=self.verbose)
|
|
116
|
+
platform.ensure_platform()
|
|
117
|
+
|
|
118
|
+
# Get board JSON to determine MCU and required packages
|
|
119
|
+
board_json = platform.get_board_json(board_id)
|
|
120
|
+
mcu = board_json.get("build", {}).get("mcu", "esp32")
|
|
121
|
+
packages = platform.get_required_packages(mcu)
|
|
122
|
+
|
|
123
|
+
# Initialize framework
|
|
124
|
+
framework_url = packages.get("framework-arduinoespressif32")
|
|
125
|
+
libs_url = packages.get("framework-arduinoespressif32-libs")
|
|
126
|
+
if not framework_url or not libs_url:
|
|
127
|
+
raise DeploymentError("Framework URLs not found in platform package")
|
|
128
|
+
|
|
129
|
+
framework = FrameworkESP32(cache, framework_url, libs_url, show_progress=self.verbose)
|
|
130
|
+
framework.ensure_framework()
|
|
131
|
+
|
|
132
|
+
# Auto-detect port if not specified
|
|
133
|
+
if not port:
|
|
134
|
+
port = self._detect_serial_port()
|
|
135
|
+
if not port:
|
|
136
|
+
raise DeploymentError("No serial port specified and auto-detection failed. " + "Use --port to specify a port.")
|
|
137
|
+
|
|
138
|
+
if self.verbose:
|
|
139
|
+
print(f"Using port: {port}")
|
|
140
|
+
|
|
141
|
+
# Determine chip type and flash parameters from board JSON
|
|
142
|
+
chip = self._get_chip_type(mcu)
|
|
143
|
+
flash_mode = board_json.get("build", {}).get("flash_mode", "dio")
|
|
144
|
+
|
|
145
|
+
# Get flash frequency and convert to esptool format
|
|
146
|
+
f_flash = board_json.get("build", {}).get("f_flash", "80000000L")
|
|
147
|
+
if isinstance(f_flash, str) and f_flash.endswith("L"):
|
|
148
|
+
freq_value = int(f_flash.rstrip("L"))
|
|
149
|
+
flash_freq = f"{freq_value // 1000000}m"
|
|
150
|
+
elif isinstance(f_flash, (int, float)):
|
|
151
|
+
flash_freq = f"{int(f_flash // 1000000)}m"
|
|
152
|
+
else:
|
|
153
|
+
flash_freq = "80m"
|
|
154
|
+
|
|
155
|
+
flash_size = "detect"
|
|
156
|
+
|
|
157
|
+
# CRITICAL FIX: ESP32-C6/C3/C2/H2 ROM bootloader can only load the second-stage
|
|
158
|
+
# bootloader in DIO mode. Must use DIO for flashing even if app uses QIO.
|
|
159
|
+
# See: https://github.com/espressif/arduino-esp32/discussions/10418
|
|
160
|
+
if mcu in ["esp32c6", "esp32c3", "esp32c2", "esp32h2"]:
|
|
161
|
+
flash_mode = "dio"
|
|
162
|
+
|
|
163
|
+
# Determine bootloader offset based on MCU
|
|
164
|
+
# ESP32/ESP32-S2: 0x1000, ESP32-P4: 0x2000, others: 0x0
|
|
165
|
+
if mcu in ["esp32", "esp32s2"]:
|
|
166
|
+
bootloader_offset = "0x1000"
|
|
167
|
+
elif mcu == "esp32p4":
|
|
168
|
+
bootloader_offset = "0x2000"
|
|
169
|
+
else:
|
|
170
|
+
bootloader_offset = "0x0"
|
|
171
|
+
|
|
172
|
+
# Find boot_app0.bin in framework tools
|
|
173
|
+
boot_app0_bin = framework.framework_path / "tools" / "partitions" / "boot_app0.bin"
|
|
174
|
+
|
|
175
|
+
# Build esptool command to flash multiple binaries at different offsets
|
|
176
|
+
# Flash layout: bootloader @ offset, partition table @ 0x8000, boot_app0 @ 0xe000, app @ 0x10000
|
|
177
|
+
cmd = [
|
|
178
|
+
sys.executable,
|
|
179
|
+
"-m",
|
|
180
|
+
"esptool",
|
|
181
|
+
"--chip",
|
|
182
|
+
chip,
|
|
183
|
+
"--port",
|
|
184
|
+
port,
|
|
185
|
+
"--baud",
|
|
186
|
+
"460800",
|
|
187
|
+
"write_flash",
|
|
188
|
+
"-z", # Compress
|
|
189
|
+
"--flash-mode",
|
|
190
|
+
flash_mode,
|
|
191
|
+
"--flash-freq",
|
|
192
|
+
flash_freq,
|
|
193
|
+
"--flash-size",
|
|
194
|
+
flash_size,
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
# Add bootloader if it exists
|
|
198
|
+
if bootloader_bin.exists():
|
|
199
|
+
cmd.extend([bootloader_offset, str(bootloader_bin)])
|
|
200
|
+
else:
|
|
201
|
+
if self.verbose:
|
|
202
|
+
print("Warning: bootloader.bin not found, skipping")
|
|
203
|
+
|
|
204
|
+
# Add partition table if it exists
|
|
205
|
+
if partitions_bin.exists():
|
|
206
|
+
cmd.extend(["0x8000", str(partitions_bin)])
|
|
207
|
+
else:
|
|
208
|
+
if self.verbose:
|
|
209
|
+
print("Warning: partitions.bin not found, skipping")
|
|
210
|
+
|
|
211
|
+
# Add boot_app0.bin if it exists
|
|
212
|
+
if boot_app0_bin.exists():
|
|
213
|
+
cmd.extend(["0xe000", str(boot_app0_bin)])
|
|
214
|
+
else:
|
|
215
|
+
if self.verbose:
|
|
216
|
+
print("Warning: boot_app0.bin not found, skipping")
|
|
217
|
+
|
|
218
|
+
# Add application firmware at 0x10000
|
|
219
|
+
cmd.extend(["0x10000", str(firmware_bin)])
|
|
220
|
+
|
|
221
|
+
if self.verbose:
|
|
222
|
+
print("Flashing firmware to device...")
|
|
223
|
+
print(f" Bootloader: {bootloader_offset}")
|
|
224
|
+
print(" Partition table: 0x8000")
|
|
225
|
+
print(" Boot app: 0xe000")
|
|
226
|
+
print(" Application: 0x10000")
|
|
227
|
+
print(f"Running: {' '.join(cmd)}")
|
|
228
|
+
|
|
229
|
+
# Execute esptool - must use cmd.exe for ESP32 on Windows
|
|
230
|
+
if sys.platform == "win32":
|
|
231
|
+
# Run via cmd.exe to avoid msys issues
|
|
232
|
+
env = os.environ.copy()
|
|
233
|
+
# Strip MSYS paths that cause issues
|
|
234
|
+
if "PATH" in env:
|
|
235
|
+
paths = env["PATH"].split(os.pathsep)
|
|
236
|
+
filtered_paths = [p for p in paths if "msys" not in p.lower()]
|
|
237
|
+
env["PATH"] = os.pathsep.join(filtered_paths)
|
|
238
|
+
|
|
239
|
+
result = subprocess.run(
|
|
240
|
+
cmd,
|
|
241
|
+
cwd=project_dir,
|
|
242
|
+
capture_output=not self.verbose,
|
|
243
|
+
text=False, # Don't decode as text - esptool may output binary data
|
|
244
|
+
env=env,
|
|
245
|
+
shell=False,
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
result = subprocess.run(
|
|
249
|
+
cmd,
|
|
250
|
+
cwd=project_dir,
|
|
251
|
+
capture_output=not self.verbose,
|
|
252
|
+
text=False, # Don't decode as text - esptool may output binary data
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if result.returncode != 0:
|
|
256
|
+
error_msg = "Upload failed"
|
|
257
|
+
if result.stderr:
|
|
258
|
+
error_msg = result.stderr.decode("utf-8", errors="replace")
|
|
259
|
+
return DeploymentResult(success=False, message=f"Deployment failed: {error_msg}", port=port)
|
|
260
|
+
|
|
261
|
+
return DeploymentResult(success=True, message="Firmware uploaded successfully", port=port)
|
|
262
|
+
|
|
263
|
+
def _get_chip_type(self, mcu: str) -> str:
|
|
264
|
+
"""Get chip type string for esptool from MCU name.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
mcu: MCU type (e.g., "esp32c6", "esp32s3")
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Chip type for esptool (e.g., "esp32c6", "esp32s3")
|
|
271
|
+
"""
|
|
272
|
+
# Map MCU names to esptool chip types
|
|
273
|
+
return mcu # Usually they match directly
|
|
274
|
+
|
|
275
|
+
def _detect_serial_port(self) -> Optional[str]:
|
|
276
|
+
"""Auto-detect serial port for device.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Serial port name or None if not found
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
import serial.tools.list_ports
|
|
283
|
+
|
|
284
|
+
ports = list(serial.tools.list_ports.comports())
|
|
285
|
+
|
|
286
|
+
# Look for ESP32 or USB-SERIAL devices
|
|
287
|
+
for port in ports:
|
|
288
|
+
description = (port.description or "").lower()
|
|
289
|
+
manufacturer = (port.manufacturer or "").lower()
|
|
290
|
+
|
|
291
|
+
if any(x in description or x in manufacturer for x in ["cp210", "ch340", "usb-serial", "uart", "esp32"]):
|
|
292
|
+
return port.device
|
|
293
|
+
|
|
294
|
+
# If no specific match, return first port
|
|
295
|
+
if ports:
|
|
296
|
+
return ports[0].device
|
|
297
|
+
|
|
298
|
+
except ImportError:
|
|
299
|
+
if self.verbose:
|
|
300
|
+
print("pyserial not installed. Cannot auto-detect port.")
|
|
301
|
+
except KeyboardInterrupt as ke:
|
|
302
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
303
|
+
|
|
304
|
+
handle_keyboard_interrupt_properly(ke)
|
|
305
|
+
raise # Never reached, but satisfies type checker
|
|
306
|
+
except Exception as e:
|
|
307
|
+
if self.verbose:
|
|
308
|
+
print(f"Port detection failed: {e}")
|
|
309
|
+
|
|
310
|
+
return None
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker utilities for QEMU deployment.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for managing Docker containers for ESP32 QEMU emulation,
|
|
5
|
+
including automatic Docker daemon startup detection and image management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_docker_env() -> dict[str, str]:
|
|
19
|
+
"""Get environment for Docker commands, handling Git Bash/MSYS2 path conversion."""
|
|
20
|
+
env = os.environ.copy()
|
|
21
|
+
# Set UTF-8 encoding environment variables for Windows
|
|
22
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
23
|
+
env["PYTHONUTF8"] = "1"
|
|
24
|
+
# Only set MSYS_NO_PATHCONV if we're in a Git Bash/MSYS2 environment
|
|
25
|
+
if "MSYSTEM" in os.environ or os.environ.get("TERM") == "xterm" or "bash.exe" in os.environ.get("SHELL", ""):
|
|
26
|
+
env["MSYS_NO_PATHCONV"] = "1"
|
|
27
|
+
return env
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_docker_daemon_running() -> bool:
|
|
31
|
+
"""Check if Docker daemon is running.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
True if Docker daemon is running, False otherwise
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
["docker", "info"],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
timeout=10,
|
|
41
|
+
env=get_docker_env(),
|
|
42
|
+
)
|
|
43
|
+
return result.returncode == 0
|
|
44
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_docker_installed() -> bool:
|
|
49
|
+
"""Check if Docker is installed.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if Docker is installed, False otherwise
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
["docker", "--version"],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
timeout=5,
|
|
59
|
+
env=get_docker_env(),
|
|
60
|
+
)
|
|
61
|
+
return result.returncode == 0
|
|
62
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_docker_desktop_path() -> Optional[str]:
|
|
67
|
+
"""Get the path to Docker Desktop executable.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Path to Docker Desktop or None if not found
|
|
71
|
+
"""
|
|
72
|
+
system = platform.system()
|
|
73
|
+
|
|
74
|
+
if system == "Windows":
|
|
75
|
+
# Common locations for Docker Desktop on Windows
|
|
76
|
+
paths = [
|
|
77
|
+
r"C:\Program Files\Docker\Docker\Docker Desktop.exe",
|
|
78
|
+
r"C:\Program Files (x86)\Docker\Docker\Docker Desktop.exe",
|
|
79
|
+
os.path.expandvars(r"%ProgramFiles%\Docker\Docker\Docker Desktop.exe"),
|
|
80
|
+
os.path.expandvars(r"%LocalAppData%\Programs\Docker\Docker\Docker Desktop.exe"),
|
|
81
|
+
]
|
|
82
|
+
for path in paths:
|
|
83
|
+
if os.path.exists(path):
|
|
84
|
+
return path
|
|
85
|
+
|
|
86
|
+
elif system == "Darwin": # macOS
|
|
87
|
+
paths = [
|
|
88
|
+
"/Applications/Docker.app/Contents/MacOS/Docker Desktop",
|
|
89
|
+
"/Applications/Docker.app",
|
|
90
|
+
]
|
|
91
|
+
for path in paths:
|
|
92
|
+
if os.path.exists(path):
|
|
93
|
+
return path
|
|
94
|
+
|
|
95
|
+
elif system == "Linux":
|
|
96
|
+
# On Linux, Docker usually runs as a service, not a desktop app
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def start_docker_daemon() -> bool:
|
|
103
|
+
"""Attempt to start the Docker daemon.
|
|
104
|
+
|
|
105
|
+
This function tries to start Docker Desktop on Windows/macOS
|
|
106
|
+
or the Docker service on Linux.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if Docker daemon started successfully, False otherwise
|
|
110
|
+
"""
|
|
111
|
+
system = platform.system()
|
|
112
|
+
|
|
113
|
+
if check_docker_daemon_running():
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
print("Docker daemon is not running. Attempting to start...")
|
|
117
|
+
|
|
118
|
+
if system == "Windows":
|
|
119
|
+
docker_path = get_docker_desktop_path()
|
|
120
|
+
if docker_path:
|
|
121
|
+
try:
|
|
122
|
+
# Start Docker Desktop without waiting
|
|
123
|
+
subprocess.Popen(
|
|
124
|
+
[docker_path],
|
|
125
|
+
stdout=subprocess.DEVNULL,
|
|
126
|
+
stderr=subprocess.DEVNULL,
|
|
127
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
|
|
128
|
+
)
|
|
129
|
+
return _wait_for_docker_daemon()
|
|
130
|
+
except KeyboardInterrupt as ke:
|
|
131
|
+
handle_keyboard_interrupt_properly(ke)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Failed to start Docker Desktop: {e}")
|
|
134
|
+
return False
|
|
135
|
+
else:
|
|
136
|
+
print("Docker Desktop not found. Please start Docker Desktop manually.")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
elif system == "Darwin":
|
|
140
|
+
docker_path = get_docker_desktop_path()
|
|
141
|
+
if docker_path:
|
|
142
|
+
try:
|
|
143
|
+
subprocess.Popen(
|
|
144
|
+
["open", "-a", "Docker"],
|
|
145
|
+
stdout=subprocess.DEVNULL,
|
|
146
|
+
stderr=subprocess.DEVNULL,
|
|
147
|
+
)
|
|
148
|
+
return _wait_for_docker_daemon()
|
|
149
|
+
except KeyboardInterrupt as ke:
|
|
150
|
+
handle_keyboard_interrupt_properly(ke)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f"Failed to start Docker Desktop: {e}")
|
|
153
|
+
return False
|
|
154
|
+
else:
|
|
155
|
+
print("Docker Desktop not found. Please install Docker Desktop.")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
elif system == "Linux":
|
|
159
|
+
try:
|
|
160
|
+
# Try to start the Docker service using systemctl
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
["sudo", "systemctl", "start", "docker"],
|
|
163
|
+
capture_output=True,
|
|
164
|
+
timeout=30,
|
|
165
|
+
)
|
|
166
|
+
if result.returncode == 0:
|
|
167
|
+
return _wait_for_docker_daemon()
|
|
168
|
+
else:
|
|
169
|
+
# Try without sudo (if user has permissions)
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
["systemctl", "start", "docker"],
|
|
172
|
+
capture_output=True,
|
|
173
|
+
timeout=30,
|
|
174
|
+
)
|
|
175
|
+
if result.returncode == 0:
|
|
176
|
+
return _wait_for_docker_daemon()
|
|
177
|
+
except KeyboardInterrupt as ke:
|
|
178
|
+
handle_keyboard_interrupt_properly(ke)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"Failed to start Docker service: {e}")
|
|
181
|
+
|
|
182
|
+
print("Failed to start Docker service. Try running: sudo systemctl start docker")
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _wait_for_docker_daemon(timeout: int = 60) -> bool:
|
|
189
|
+
"""Wait for Docker daemon to become available.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
timeout: Maximum time to wait in seconds
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if Docker daemon is now running, False if timeout
|
|
196
|
+
"""
|
|
197
|
+
print(f"Waiting for Docker daemon to start (timeout: {timeout}s)...")
|
|
198
|
+
start_time = time.time()
|
|
199
|
+
|
|
200
|
+
while (time.time() - start_time) < timeout:
|
|
201
|
+
if check_docker_daemon_running():
|
|
202
|
+
print("Docker daemon is now running!")
|
|
203
|
+
return True
|
|
204
|
+
time.sleep(2)
|
|
205
|
+
sys.stdout.write(".")
|
|
206
|
+
sys.stdout.flush()
|
|
207
|
+
|
|
208
|
+
print("\nTimeout waiting for Docker daemon to start.")
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def ensure_docker_available() -> bool:
|
|
213
|
+
"""Ensure Docker is available, starting daemon if necessary.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if Docker is available, False otherwise
|
|
217
|
+
"""
|
|
218
|
+
if not check_docker_installed():
|
|
219
|
+
print("Docker is not installed.")
|
|
220
|
+
print()
|
|
221
|
+
print("Install Docker:")
|
|
222
|
+
print(" - Windows/Mac: https://www.docker.com/products/docker-desktop")
|
|
223
|
+
print(" - Linux: https://docs.docker.com/engine/install/")
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
if check_docker_daemon_running():
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
return start_docker_daemon()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def check_docker_image_exists(image_name: str) -> bool:
|
|
233
|
+
"""Check if a Docker image exists locally.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
image_name: Name of the Docker image to check
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if image exists, False otherwise
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
result = subprocess.run(
|
|
243
|
+
["docker", "images", "-q", image_name],
|
|
244
|
+
capture_output=True,
|
|
245
|
+
text=True,
|
|
246
|
+
timeout=10,
|
|
247
|
+
env=get_docker_env(),
|
|
248
|
+
)
|
|
249
|
+
return bool(result.stdout.strip())
|
|
250
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def pull_docker_image(image_name: str, timeout: int = 600) -> bool:
|
|
255
|
+
"""Pull a Docker image.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
image_name: Name of the Docker image to pull
|
|
259
|
+
timeout: Timeout in seconds (default 10 minutes)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if image was pulled successfully, False otherwise
|
|
263
|
+
"""
|
|
264
|
+
print(f"Pulling Docker image: {image_name}")
|
|
265
|
+
print("This may take a few minutes on first run...")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
result = subprocess.run(
|
|
269
|
+
["docker", "pull", image_name],
|
|
270
|
+
timeout=timeout,
|
|
271
|
+
env=get_docker_env(),
|
|
272
|
+
)
|
|
273
|
+
if result.returncode == 0:
|
|
274
|
+
print(f"Successfully pulled {image_name}")
|
|
275
|
+
return True
|
|
276
|
+
else:
|
|
277
|
+
print(f"Failed to pull {image_name}")
|
|
278
|
+
return False
|
|
279
|
+
except KeyboardInterrupt as ke:
|
|
280
|
+
handle_keyboard_interrupt_properly(ke)
|
|
281
|
+
except subprocess.TimeoutExpired:
|
|
282
|
+
print(f"Timeout pulling {image_name}")
|
|
283
|
+
return False
|
|
284
|
+
except Exception as e:
|
|
285
|
+
print(f"Error pulling {image_name}: {e}")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def ensure_docker_image(image_name: str, fallback_images: Optional[list[str]] = None) -> bool:
|
|
290
|
+
"""Ensure a Docker image is available, pulling if necessary.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
image_name: Name of the Docker image to ensure
|
|
294
|
+
fallback_images: Optional list of fallback images to try if primary fails
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
True if image is available, False otherwise
|
|
298
|
+
"""
|
|
299
|
+
if check_docker_image_exists(image_name):
|
|
300
|
+
print(f"Image {image_name} already available locally")
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
if pull_docker_image(image_name):
|
|
304
|
+
return True
|
|
305
|
+
|
|
306
|
+
# Try fallback images
|
|
307
|
+
if fallback_images:
|
|
308
|
+
for fallback in fallback_images:
|
|
309
|
+
print(f"Trying fallback image: {fallback}")
|
|
310
|
+
if check_docker_image_exists(fallback):
|
|
311
|
+
return True
|
|
312
|
+
if pull_docker_image(fallback):
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
return False
|