name: Build Cross-Compiled ROS 2 Images permissions: contents: read packages: write on: workflow_dispatch: inputs: platform: description: "Target platform" required: true type: choice options: [rk3588] default: rk3588 ros_distro: description: "ROS 2 distribution" required: true type: choice options: [rolling, jazzy, lyrical] default: lyrical push_images: description: "Push built images to registry" required: false type: boolean default: false env: REGISTRY: ghcr.io jobs: build: runs-on: ubuntu-22.04 strategy: matrix: platform: ${{ fromJSON(format('["{0}"]', inputs.platform)) }} ros_distro: ${{ fromJSON(format('["{0}"]', inputs.ros_distro)) }} fail-fast: false steps: - name: Set image namespace and tag run: | echo "IMAGE_NAMESPACE=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV echo "IMAGE_TAG=$(date +%Y%m%d)" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: driver: docker - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # ── Compute cache keys ──────────────────────────────────────────── # Each key = hash(own files) + upstream key, so any upstream change # transitively invalidates all downstream stages. - name: Compute cache keys id: keys run: | CC_BASE_KEY=$(sha256sum cross_compile/cc_base.Dockerfile | cut -d' ' -f1) CC_TOOLCHAIN_FILES=$(find cross_compile/${{ matrix.platform }} -type f | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) CC_TOOLCHAIN_KEY=$(echo "$CC_TOOLCHAIN_FILES $CC_BASE_KEY" | sha256sum | cut -d' ' -f1) VENDOR_FILES=$(find cross_compile/vendor_packages -type f | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) VENDOR_KEY=$(echo "$VENDOR_FILES $CC_TOOLCHAIN_KEY" | sha256sum | cut -d' ' -f1) ROS2_FILES=$(find cross_compile/ros2/${{ matrix.ros_distro }} cross_compile/ros2/ros_cpp -type f | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) ROS2_KEY=$(echo "$ROS2_FILES $VENDOR_KEY" | sha256sum | cut -d' ' -f1) echo "cc_base=$CC_BASE_KEY" >> $GITHUB_OUTPUT echo "cc_toolchain=$CC_TOOLCHAIN_KEY" >> $GITHUB_OUTPUT echo "vendor=$VENDOR_KEY" >> $GITHUB_OUTPUT echo "ros2=$ROS2_KEY" >> $GITHUB_OUTPUT # ── Stage 1: cc-base ───────────────────────────────────────────── - name: Check cc-base cache id: cc-base run: | EXPECTED="${{ steps.keys.outputs.cc_base }}" if docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cc-base:latest 2>/dev/null; then docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cc-base:latest cc-base:latest CACHED=$(docker inspect cc-base:latest --format '{{ index .Config.Labels "cache.key" }}' 2>/dev/null || echo "") [ "$CACHED" = "$EXPECTED" ] && echo "hit=true" >> $GITHUB_OUTPUT || echo "hit=false" >> $GITHUB_OUTPUT else echo "hit=false" >> $GITHUB_OUTPUT fi - name: Build cc-base if: steps.cc-base.outputs.hit != 'true' run: | docker compose -f cross_compile/compose.yaml build cc-base TMPDIR=$(mktemp -d) docker build \ --label "cache.key=${{ steps.keys.outputs.cc_base }}" \ -t cc-base:latest -f - "$TMPDIR" << 'EOF' FROM cc-base:latest EOF rm -rf "$TMPDIR" - name: Push cc-base if: inputs.push_images run: | docker tag cc-base:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cc-base:${{ env.IMAGE_TAG }} docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cc-base:${{ env.IMAGE_TAG }} docker tag cc-base:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cc-base:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cc-base:latest # ── Stage 2: cc-toolchain ───────────────────────────────────────── - name: Check cc-toolchain cache id: cc-toolchain run: | EXPECTED="${{ steps.keys.outputs.cc_toolchain }}" REMOTE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-toolchain:latest" LOCAL="${{ matrix.platform }}/cc-toolchain:latest" if docker pull "$REMOTE" 2>/dev/null; then docker tag "$REMOTE" "$LOCAL" CACHED=$(docker inspect "$LOCAL" --format '{{ index .Config.Labels "cache.key" }}' 2>/dev/null || echo "") [ "$CACHED" = "$EXPECTED" ] && echo "hit=true" >> $GITHUB_OUTPUT || echo "hit=false" >> $GITHUB_OUTPUT else echo "hit=false" >> $GITHUB_OUTPUT fi - name: Build cc-toolchain (${{ matrix.platform }}) if: steps.cc-toolchain.outputs.hit != 'true' run: | PLATFORM=${{ matrix.platform }} docker compose -f cross_compile/compose.yaml build cc-toolchain TMPDIR=$(mktemp -d) docker build \ --label "cache.key=${{ steps.keys.outputs.cc_toolchain }}" \ -t ${{ matrix.platform }}/cc-toolchain:latest -f - "$TMPDIR" << 'EOF' FROM ${{ matrix.platform }}/cc-toolchain:latest EOF rm -rf "$TMPDIR" - name: Push cc-toolchain if: inputs.push_images run: | docker tag ${{ matrix.platform }}/cc-toolchain:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-toolchain:${{ env.IMAGE_TAG }} docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-toolchain:${{ env.IMAGE_TAG }} docker tag ${{ matrix.platform }}/cc-toolchain:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-toolchain:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-toolchain:latest # ── Stage 3: vendor-packages ────────────────────────────────────── - name: Check vendor-packages cache id: vendor run: | EXPECTED="${{ steps.keys.outputs.vendor }}" REMOTE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-vendor-packages:latest" LOCAL="${{ matrix.platform }}/cc-vendor-packages:latest" if docker pull "$REMOTE" 2>/dev/null; then docker tag "$REMOTE" "$LOCAL" CACHED=$(docker inspect "$LOCAL" --format '{{ index .Config.Labels "cache.key" }}' 2>/dev/null || echo "") [ "$CACHED" = "$EXPECTED" ] && echo "hit=true" >> $GITHUB_OUTPUT || echo "hit=false" >> $GITHUB_OUTPUT else echo "hit=false" >> $GITHUB_OUTPUT fi - name: Build vendor-packages (${{ matrix.platform }}) if: steps.vendor.outputs.hit != 'true' run: | PLATFORM=${{ matrix.platform }} docker compose -f cross_compile/compose.yaml build vendor-packages TMPDIR=$(mktemp -d) docker build \ --label "cache.key=${{ steps.keys.outputs.vendor }}" \ -t ${{ matrix.platform }}/cc-vendor-packages:latest -f - "$TMPDIR" << 'EOF' FROM ${{ matrix.platform }}/cc-vendor-packages:latest EOF rm -rf "$TMPDIR" - name: Push vendor-packages if: inputs.push_images run: | docker tag ${{ matrix.platform }}/cc-vendor-packages:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-vendor-packages:${{ env.IMAGE_TAG }} docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-vendor-packages:${{ env.IMAGE_TAG }} docker tag ${{ matrix.platform }}/cc-vendor-packages:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-vendor-packages:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-vendor-packages:latest # ── Stage 4: ros2 ───────────────────────────────────────────────── - name: Check ros2 cache id: ros2 run: | EXPECTED="${{ steps.keys.outputs.ros2 }}" REMOTE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest" LOCAL="${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest" if docker pull "$REMOTE" 2>/dev/null; then docker tag "$REMOTE" "$LOCAL" CACHED=$(docker inspect "$LOCAL" --format '{{ index .Config.Labels "cache.key" }}' 2>/dev/null || echo "") [ "$CACHED" = "$EXPECTED" ] && echo "hit=true" >> $GITHUB_OUTPUT || echo "hit=false" >> $GITHUB_OUTPUT else echo "hit=false" >> $GITHUB_OUTPUT fi - name: Build ROS 2 (${{ matrix.ros_distro }}) if: steps.ros2.outputs.hit != 'true' run: | PLATFORM=${{ matrix.platform }} ROS_DISTRO=${{ matrix.ros_distro }} docker compose -f cross_compile/compose.yaml build ros2 TMPDIR=$(mktemp -d) docker build \ --label "cache.key=${{ steps.keys.outputs.ros2 }}" \ -t ${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest -f - "$TMPDIR" << 'EOF' FROM ${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest EOF rm -rf "$TMPDIR" - name: Push ROS 2 image if: inputs.push_images run: | docker tag ${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:${{ env.IMAGE_TAG }} docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:${{ env.IMAGE_TAG }} docker tag ${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.platform }}/cc-ros-${{ matrix.ros_distro }}:latest # ── Summary ─────────────────────────────────────────────────────── - name: Image summary if: always() run: | echo "### Built Images :rocket:" >> $GITHUB_STEP_SUMMARY docker images \ --filter "reference=cc-*" \ --filter "reference=*/cc-*" \ --format "| \`{{.Repository}}:{{.Tag}}\` | {{.Size}} |" \ >> $GITHUB_STEP_SUMMARY