ci/docker_utils/task.md
Purpose: Local development tool for cross-platform compilation with pre-cached dependencies.
Scope: Local development only - NOT for CI server integration.
Image naming: fastled-platformio-{arch}-{platform}-{hash}
Build vs Runtime Separation:
BUILD TIME (Image Creation):
ci/boards.pyplatformio.ini from board configurationfastled-platformio-{arch}-{platform}-{hash} with everything pre-configureduv run ci/build_docker_image_pio.py --platform unoRUNTIME (Compilation):
uno Blink)./src, ./examples)/fastled/output mountdocker run fastled-platformio-avr-uno-{hash} uno BlinkKey Insight: Platform configuration comes from boards.py at BUILD time, not from runtime arguments. Runtime only needs platform name for consistency checking.
Build Requirements:
--target upload or extra_scripts to merge bins automaticallyArchitecture:
fastled-platformio-avr-uno-abc123 # Hash of AVR + Uno config
fastled-platformio-esp32-esp32s3-def456 # Hash of ESP32 + S3 config
Each image has platform toolchain pre-installed during build.
PROS:
--rm flag, no persistent container neededdocker image prune removes unused imagesCONS:
Container Lifecycle:
# BUILD TIME: Generate platformio.ini from boards.py and bake into image
# Command: uv run ci/build_docker_image_pio.py --platform uno
# Result: Image fastled-platformio-avr-uno-abc123 with uno config pre-installed
# RUNTIME: Only need platform name, sketch name, and source code
docker run --rm \
-v ./src:/fastled/src:ro \
-v ./examples:/fastled/examples:ro \
-v ./build_output:/fastled/output:rw \
fastled-platformio-avr-uno-abc123 uno Blink
docker run --rm \
-v ./src:/fastled/src:ro \
-v ./examples:/fastled/examples:ro \
-v ./build_output:/fastled/output:rw \
fastled-platformio-avr-uno-abc123 uno RGBCalibrate
# No platformio.ini needed at runtime - it's baked into the image!
# Platform config already pre-installed in image
# Container is destroyed after each run
# Compiled binaries are in ./build_output/
Architecture:
fastled-platformio-base # Only PlatformIO installed, no platform toolchains
Named containers install platform dependencies on first run, then persist state.
PROS:
CONS:
--rmContainer Lifecycle:
# Create persistent named container (once per platform)
docker run --name fastled-uno-abc123 fastled-platformio-base install-platform uno
# Wait 5-10 minutes for ESP32 SDK download...
# Reuse container for compilations
docker start fastled-uno-abc123
docker exec fastled-uno-abc123 compile uno Blink
docker stop fastled-uno-abc123
# Developer must remember: "Don't delete this container or reinstall 10 minutes!"
# CI must remember: "Check if container exists, check if platform installed, then run"
Why Option 1 Wins:
Note: This Docker system is designed for local development only, not for CI server integration.
Mitigation for Storage Concerns:
docker image prune removes unused old hashesImplementation Plan:
Build Script (ci/build_docker_image_pio.py): ✅ Already implemented
Image Naming:
hash_input = f"{platform_config}{framework_config}{platformio_version}"
config_hash = hashlib.sha256(hash_input.encode()).hexdigest()[:8]
image_name = f"fastled-platformio-{architecture}-{board}-{config_hash}"
Cache Invalidation: Automatic via hash change
Cleanup Strategy:
# Remove images older than 30 days
docker image prune -a --filter "until=720h"
Option 3: Base image + platform-specific layers
fastled-platformio-base with PlatformIOfastled-platformio-base-avr extending base with AVR toolchainfastled-platformio-avr-uno extending avr with board-specific configVerdict: More complexity than Option 1, minimal storage savings due to Docker layer mechanics.
ci/build_docker_image_pio.py (already uses Option 1)generate_config_hash() function in ci/build_docker_image_pio.pyfastled-platformio-{arch}-{platform}-{hash} (8-char hash)--label flags in docker build command/fastled/output/ if mounted.pio/build/*/ to outputfastled-entrypoint.sh script in Dockerfile.template/fastled/output is mountedci/docker_utils/prune_old_images.py
Current Implementation:
platformio.ini from ci/boards.py.ini to temporary build contextProblem:
For hash-based image naming, Docker build needs access to ci/boards.py to:
Solution Options:
ci/ folder to build context# In build_docker_image_pio.py
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Copy entire ci/ folder to build context
shutil.copytree(
Path(__file__).parent, # ci/ folder
temp_path / "ci",
ignore=shutil.ignore_patterns('__pycache__', '*.pyc')
)
# Dockerfile can now import from ci.boards
# RUN python -c "from ci.boards import create_board; ..."
Pros:
Cons:
# Generate hash on host from boards.py
config_hash = generate_config_hash(platform_name, framework)
image_name = f"fastled-platformio-{architecture}-{platform_name}-{config_hash}"
# Still generate platformio.ini on host
# Docker build just uses the pre-generated file
Pros:
Cons:
# Extract minimal metadata from boards.py
metadata = {
'platform': platform_name,
'framework': framework,
'config_hash': config_hash,
'platformio_version': '...',
}
# Write metadata.json to build context
with open(temp_path / 'metadata.json', 'w') as f:
json.dump(metadata, f)
# Dockerfile adds as labels
# LABEL config_hash=${HASH}
RECOMMENDATION: Option B (keep current approach)
Rationale:
Implementation:
import hashlib
import json
def generate_config_hash(platform_name: str, framework: Optional[str]) -> str:
"""Generate deterministic hash from board configuration."""
board = create_board(platform_name)
if framework:
board.framework = framework
# Create deterministic hash from config
config_data = {
'platform': board.platform,
'framework': board.framework,
'board': board.board,
'platform_packages': sorted(board.platform_packages or []),
# Include PlatformIO version for full reproducibility
'platformio_version': get_platformio_version(),
}
config_json = json.dumps(config_data, sort_keys=True)
return hashlib.sha256(config_json.encode()).hexdigest()[:8]
# Image name becomes:
# fastled-platformio-avr-uno-a1b2c3d4
This approach provides automatic cache invalidation without complicating the Docker build.
Merged Binary Generation:
For platforms that support it (ESP32, ESP8266), enable merged binary output in platformio.ini:
[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
# Generate merged binary for easy flashing
board_build.embed_files =
board_build.filesystem = littlefs
extra_scripts =
pre:merge_bin.py
# PlatformIO automatically creates merged bin at:
# .pio/build/<env>/firmware.factory.bin (combines bootloader + partitions + app)
Alternative using build flags:
# In container:
pio run --target upload # Generates merged binary as side effect
# Or explicitly:
pio run && esptool.py --chip esp32s3 merge_bin -o merged.bin @flash_args
Output Location & Volume Mounts:
Required volume mounts:
-v ./src:/fastled/src:ro - FastLED source code (read-only)-v ./examples:/fastled/examples:ro - Arduino sketches (read-only)Optional volume mount:
-v ./build_output:/fastled/output:rw - Output directory for compiled binaries (read-write)Build artifacts:
.pio/build/<env>/firmware.bin.pio/build/<env>/firmware.factory.bin (ESP32).pio/build/<env>/firmware.hex (AVR).pio/build/<env>/firmware.elf (all platforms)Container should copy build artifacts to /fastled/output/ if mounted:
# Inside container entrypoint or compile script:
if [ -d "/fastled/output" ]; then
cp .pio/build/**/firmware.* /fastled/output/
echo "Binaries copied to output directory"
fi
Pre-built PlatformIO Docker images with pre-configured dependencies provide lightning-fast compilation by eliminating the dependency download step on each build. This task establishes the infrastructure for creating platform-specific Docker images.
Create an automated build system for PlatformIO-based Docker images, starting with the Arduino Uno platform as a proof of concept.
ci/build_docker_image_pio.pyPurpose: Generate Docker images with pre-installed PlatformIO dependencies for specific platforms.
Input Modes (mutually exclusive):
--platform (required): Platform name (e.g., uno, esp32s3)--framework (optional): Framework override if neededci/boards.pyplatformio.ini from board configuration--platformio-ini (required): Path to existing platformio.ini fileScript validates that exactly one mode is used (either platform args OR ini file, not both)
Key Features:
ci/boards.py) for Mode AArgument Parser Design:
import argparse
parser = argparse.ArgumentParser(
description='Build PlatformIO Docker images with pre-cached dependencies'
)
# Create mutually exclusive group for input modes
input_mode = parser.add_mutually_exclusive_group(required=True)
# Mode A: Generate from board configuration
input_mode.add_argument(
'--platform',
type=str,
help='Platform name from ci/boards.py (e.g., uno, esp32s3)'
)
# Mode B: Use existing platformio.ini
input_mode.add_argument(
'--platformio-ini',
type=str,
metavar='PATH',
help='Path to existing platformio.ini file'
)
# Optional args for Mode A only (framework override)
parser.add_argument(
'--framework',
type=str,
help='Framework override (only valid with --platform)'
)
# Common optional arguments
parser.add_argument(
'--image-name',
type=str,
help='Custom Docker image name (auto-generated if not specified)'
)
parser.add_argument(
'--no-cache',
action='store_true',
help='Build Docker image without using cache'
)
args = parser.parse_args()
# Validation: --framework requires --platform
if args.framework and not args.platform:
parser.error('--framework can only be used with --platform')
# Determine which mode we're in
if args.platform:
print(f"Mode A: Building from board configuration (platform={args.platform})")
# Load from ci/boards.py and generate platformio.ini
elif args.platformio_ini:
print(f"Mode B: Building from existing INI file ({args.platformio_ini})")
# Use provided platformio.ini directly
Validation Rules:
--platform or --platformio-ini must be provided (enforced by mutually_exclusive_group)--framework is only valid when using --platform (custom validation)Image Naming Convention: fastled-platformio-{architecture}-{board}
fastled-platformio-avr-unoBase Requirements:
Build-time Steps:
platformio.ini into containermaster.zip)Volume Mounts (optional, read-only):
src/ - FastLED source codeexamples/ - Arduino sketchesEntry Point Design:
bash compile uno Blink)Usage Examples:
# Mode A: Build image from board configuration
uv run python ci/build_docker_image_pio.py --platform uno
uv run python ci/build_docker_image_pio.py --platform esp32s3 --framework arduino
# Mode B: Build image from existing platformio.ini
uv run python ci/build_docker_image_pio.py --platformio-ini ./custom-config.ini
# Run compilation in container
docker run --rm \
-v $(pwd)/src:/fastled/src:ro \
-v $(pwd)/examples:/fastled/examples:ro \
fastled-platformio-avr-uno \
bash compile uno Blink
ci/build_docker_image_pio.py script created and functionalScript Implementation:
ci/build_docker_image_pio.py with full functionalityKey Features:
Validation:
Image Naming:
fastled-platformio-{architecture}-{board}fastled-platformio-avr-unoUsage Examples:
# Mode A: Build from board configuration
uv run python ci/build_docker_image_pio.py --platform uno
uv run python ci/build_docker_image_pio.py --platform esp32s3 --framework arduino
# Mode B: Build from existing platformio.ini
uv run python ci/build_docker_image_pio.py --platformio-ini ./custom-config.ini
# With options
uv run python ci/build_docker_image_pio.py --platform uno --image-name my-uno --no-cache
Docker Testing:
Expansion:
ci/docker_utils/compile_sketch.pyPurpose: Single-command workflow for building Docker images and compiling sketches inside containers.
Design Goal: Simplify the Docker compilation workflow from two separate steps (build image, run container) into one streamlined command.
Workflow:
User Command → Check if image exists → Build if needed → Run container → Report results
Arguments:
import argparse
parser = argparse.ArgumentParser(
description='Compile Arduino sketches in Docker containers with cached dependencies'
)
parser.add_argument(
'--platform',
type=str,
required=True,
help='Platform name from ci/boards.py (e.g., uno, esp32s3)'
)
parser.add_argument(
'--sketch',
type=str,
required=True,
help='Sketch name from examples/ directory (e.g., Blink, RGBCalibrate)'
)
parser.add_argument(
'--framework',
type=str,
help='Framework override (e.g., arduino)'
)
parser.add_argument(
'--rebuild-image',
action='store_true',
help='Force rebuild of Docker image even if it exists'
)
parser.add_argument(
'--no-cache',
action='store_true',
help='Build Docker image without using cache (implies --rebuild-image)'
)
parser.add_argument(
'--image-name',
type=str,
help='Custom Docker image name (auto-generated if not specified)'
)
parser.add_argument(
'--keep-container',
action='store_true',
help='Keep container after compilation (for debugging)'
)
Implementation Logic:
Generate Image Name:
if args.image_name:
image_name = args.image_name
else:
# Use ci/build_docker_image_pio.py logic to determine architecture
architecture = get_architecture(args.platform)
image_name = f"fastled-platformio-{architecture}-{args.platform}"
Check Image Existence:
import subprocess
# Check if image exists
result = subprocess.run(
["docker", "image", "inspect", image_name],
capture_output=True,
text=True
)
image_exists = (result.returncode == 0)
should_build = args.rebuild_image or args.no_cache or not image_exists
Build Image if Needed:
if should_build:
print(f"Building Docker image: {image_name}")
build_args = [
"uv", "run", "python", "ci/build_docker_image_pio.py",
"--platform", args.platform,
"--image-name", image_name
]
if args.framework:
build_args.extend(["--framework", args.framework])
if args.no_cache:
build_args.append("--no-cache")
result = subprocess.run(build_args)
if result.returncode != 0:
print("ERROR: Docker image build failed")
sys.exit(1)
else:
print(f"Using existing Docker image: {image_name}")
Run Compilation in Container:
import os
# Get absolute paths for volume mounts
project_root = os.path.abspath(".")
src_path = os.path.join(project_root, "src")
examples_path = os.path.join(project_root, "examples")
# Build docker run command
docker_cmd = [
"docker", "run",
"--rm" if not args.keep_container else "",
"-v", f"{src_path}:/fastled/src:ro",
"-v", f"{examples_path}:/fastled/examples:ro",
image_name,
"bash", "compile", args.platform, args.sketch
]
# Remove empty strings from command
docker_cmd = [arg for arg in docker_cmd if arg]
print(f"Compiling {args.sketch} for {args.platform}...")
result = subprocess.run(docker_cmd)
if result.returncode == 0:
print(f"✓ Compilation successful")
else:
print(f"✗ Compilation failed")
sys.exit(result.returncode)
Usage Examples:
# Simple compilation (builds image if needed)
uv run python ci/docker_utils/compile_sketch.py --platform uno --sketch Blink
# Force rebuild image before compilation
uv run python ci/docker_utils/compile_sketch.py --platform uno --sketch Blink --rebuild-image
# Compile with framework override
uv run python ci/docker_utils/compile_sketch.py --platform esp32s3 --sketch Blink --framework arduino
# Build from scratch without cache
uv run python ci/docker_utils/compile_sketch.py --platform uno --sketch Blink --no-cache
# Keep container for debugging
uv run python ci/docker_utils/compile_sketch.py --platform uno --sketch Blink --keep-container
# Use custom image name
uv run python ci/docker_utils/compile_sketch.py --platform uno --sketch Blink --image-name my-test-image
Output Format:
Checking Docker image: fastled-platformio-avr-uno
Using existing Docker image: fastled-platformio-avr-uno
Compiling Blink for uno...
[Docker container output...]
✓ Compilation successful
Or on first run:
Checking Docker image: fastled-platformio-avr-uno
Image not found. Building now...
[Build output from ci/build_docker_image_pio.py...]
Docker image built successfully: fastled-platformio-avr-uno
Compiling Blink for uno...
[Docker container output...]
✓ Compilation successful
Error Handling:
Docker Not Installed:
ERROR: Docker is not installed or not in PATH
Please install Docker Desktop: https://www.docker.com/products/docker-desktop
Invalid Platform:
ERROR: Platform 'xyz' not found in ci/boards.py
Available platforms: uno, nano_every, esp32dev, esp32s3, ...
Invalid Sketch:
ERROR: Sketch 'xyz' not found in examples/ directory
Available sketches: Blink, RGBCalibrate, ColorPalette, ...
Image Build Failed:
ERROR: Docker image build failed
See above output for details
Compilation Failed:
ERROR: Compilation failed with exit code 1
See above output for details
Benefits:
bash compile interfaceIntegration with CI:
# .github/workflows/docker-compile.yml
name: Docker Compilation Test
on: [push, pull_request]
jobs:
compile-in-docker:
runs-on: ubuntu-latest
strategy:
matrix:
platform: [uno, esp32s3, teensy40]
sketch: [Blink, RGBCalibrate]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install uv
run: pip install uv
- name: Compile sketch in Docker
run: |
uv run python ci/docker_utils/compile_sketch.py \
--platform ${{ matrix.platform }} \
--sketch ${{ matrix.sketch }}
Success Criteria:
ci/docker_utils/compile_sketch.py script createdci/build_docker_image_pio.py)--rebuild-image and --no-cache flags workingAfter Uno platform validation, extend to:
All core functionality for the Docker-based PlatformIO compilation system has been successfully implemented:
ci/build_docker_image_pio.pyget_platformio_version(): Retrieves PlatformIO version for hash stabilitygenerate_config_hash(): Creates 8-char SHA256 hash from platform configfastled-platformio-{architecture}-{platform}-{hash}fastled-platformio-esp32-esp32s3-a1b2c3d4ci/docker_utils/Dockerfile.templatefastled-entrypoint.sh/fastled/output is mounted-v ./build_output:/fastled/output:rwci/docker_utils/prune_old_images.py--force) - actually deletes images--days N) - removes images older than N days--platform NAME) - only prune specific platform--all --force) - removes ALL FastLED images (dangerous)ci/docker_utils/README.mdbash lint
Modified:
ci/build_docker_image_pio.py - Added hash generation logicci/docker_utils/Dockerfile.template - Added entrypoint script and output directory supportCreated:
ci/docker_utils/prune_old_images.py - Image cleanup utility (384 lines)ci/docker_utils/README.md - Comprehensive documentation (540 lines)Code validation: ✅ Complete
Docker testing: ⏸️ Deferred (Windows environment limitation)
When Docker is available:
uv run python ci/build_docker_image_pio.py --platform unodocker run --rm \
-v ./src:/fastled/src:ro \
-v ./examples:/fastled/examples:ro \
-v ./build_output:/fastled/output:rw \
fastled-platformio-avr-uno-{hash} \
pio run
./build_output/uv run python ci/docker_utils/prune_old_images.pyThe Docker-based PlatformIO compilation system is implementation complete and code-ready. All requirements from the task specification have been fulfilled:
The system is ready for Docker-based testing and production use. The implementation follows all FastLED coding standards and best practices.