OpenWrt Fastbuild

It's pretty common to set up a build environment with Github Actions to compile OpenWrt in the cloud. However, this process is dog slow as Github runners are transient, so must rebuild the entire toolchain and recompile every single thing each and every time.

There has been surprisingly little activity to optimize this workflow, with the most thorough effort having seen no development in five years. Fastbuild addresses the inefficiency by storing the build environment in a Docker container, massively reducing compile time.

I've updated the code so it works again and should be easy to fork and modify for your particular projects.

6 Likes
First Run
Runner Compile Time Upload Time Total Time
Github Ubuntu 24.04 ARM 1h42m 18m 2h5m
Github Ubuntu 24.04 x86 2h22m 18m 2h47m
Self-hosted Oracle Cloud ARM 2h29m 27m 3h2m
Self-hosted x86 (i7-9700 VM) 1h23m 47m 2h16m
Second Run
Runner Download Time Compile Time Upload Time Total Time
Github Ubuntu 24.04 ARM 10m 7m 4m 23m
Github Ubuntu 24.04 x86 6m 8m 3m 21m
Self-hosted Oracle Cloud ARM 7s 8m 15m 26m
Self-hosted x86 (i7-9700 VM) 6s 5m 9m 17m

I was writing a workflow for the en7523, and to make activating ccache faster, it can be converted into a workflow_call to be called from other repositories in the future

name: xx230v build
on:
  push:
  workflow_dispatch:
    inputs:
      verbose:
        type: choice
        required: false
        default: "sc"
        options:
          - "sc"
          - "s"
          - ""
  schedule:
    - cron: 0 12 */2 * *

jobs:
  build:
    runs-on: ubuntu-latest-nonroot
    env:
      CONFIG_CCACHE_DIR: ${{ github.workspace }}/.ccache
      CONFIG_BUILD_LOG_DIR: ${{ github.workspace }}/logs
      VERBOSE: ${{ github.event_name == 'workflow_dispatch' && inputs.verbose || 'sc' }}
    steps:
      - name: Openwrt code
        uses: actions/checkout@v4
        with:
          repository: airoha_en7523/openwrt
          ref: main

      - name: Config repo
        uses: actions/checkout@v4
        with:
          path: config_build

      - name: Install host dependencies
        run: |
          sudo apt update
          sudo apt install -y build-essential clang flex bison g++ gawk gettext git libncurses5-dev libssl-dev python3-setuptools rsync swig unzip zlib1g-dev file wget ccache
          if [[ "$(uname -m)" == "x86_64" ]]; then
            sudo apt install -y gcc-multilib g++-multilib
          else
            sudo apt install -y gcc-multilib* g++-multilib*
          fi

      - name: Free disk space on runner
        run: |
          env
          df -h
          sudo rm -rf "$AGENT_TOOLSDIRECTORY" || echo "No cache tools directory"
          sudo rm -rf \
            /opt/ghc \
            /opt/google/chrome \
            /opt/microsoft/msedge \
            /opt/microsoft/powershell \
            /opt/pipx \
            /usr/lib/mono \
            /usr/local/julia* \
            /usr/local/lib/android \
            /usr/local/lib/node_modules \
            /usr/local/share/chromium \
            /usr/local/share/powershell \
            /usr/share/dotnet \
            /usr/share/swift
          df -h

      - name: Copy config
        run: |
            cp -fv config_build/xx230v_v1.config .config
            mkdir logs

      - name: Get build cache
        uses: actions/cache/restore@v5
        id: ccache_restore
        with:
          key: xx230v_cache_${{ runner.os }}_${{ runner.arch }}
          path: |
              .ccache
              build_dir
              staging_dir
              bin/packages
              tmp
              dl

      - name: Update and Install feeds
        run: |
          sed -i -Ee 's|https://git.openwrt.org/(feed\|project)|https://github.com/openwrt|g' feeds.conf.default
          ./scripts/feeds update -a
          ./scripts/feeds install -a

      - name: Defconfig
        run: |
          make defconfig
          cat .config

      - name: Download dependencies
        run: make -j$(($(nproc)+1)) download V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Package cleanup
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        id: toolchain_build
        run: make -j$(($(nproc)+1)) package/cleanup V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Tools compile
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        run: make -j$(($(nproc)+1)) tools/compile V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Toolchain compile
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        id: toolchain_build
        run: make -j$(($(nproc)+1)) toolchain/compile V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Toolchain install
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        run: make -j$(($(nproc)+1)) toolchain/install V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Target compiler
        run: make -j$(($(nproc)+1)) target/compile V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Build info
        run: make -j$(($(nproc)+1)) buildinfo

      - name: Package compile
        run: make -j$(($(nproc)+1)) package/compile V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Package install
        run: make -j$(($(nproc)+1)) package/install V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Target install
        run: make -j$(($(nproc)+1)) target/install V=${{ env.VERBOSE }} CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR" CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Package index
        run: make -j$(($(nproc)+1)) package/index

      - name: JSON overview image
        run: make -j$(($(nproc)+1)) json_overview_image_info

      - name: Checksum
        run: make -j$(($(nproc)+1)) checksum

      - name: Make tarball (tar)
        run: tar -cpvf bin/targets.tar bin/targets

      - name: Create release
        uses: softprops/action-gh-release@v2
        with:
          name: v${{ github.run_number }}
          tag_name: v${{ github.run_number }}
          body: |
              # EN7523 [Build](/actions/runs/${{ github.run_number }})

              build targets

              - tplink xx230v v1
              - tplink xx530v v1
              - tplink ex530v v1
          files: |
            bin/targets.tar
            bin/targets/airoha/en7523/*

      - name: Upload logs
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: logs
          path: logs/

      - name: Save build cache
        uses: actions/cache/save@v5
        if: always()
        with:
          key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
          path: |
              .ccache
              build_dir
              staging_dir
              bin/packages
              tmp
              dl


Just like with workflow_call, since I'm running my Gitea server, I have to make some adaptation's and fix copy error

name: xx230v build
on:
  push:
  workflow_dispatch:
    inputs:
      verbose:
        type: choice
        required: false
        default: "sc"
        options:
          - "sc"
          - "s"
          - ""
  schedule:
    - cron: 0 12 */2 * *

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix-data: ${{ steps.configs.outputs.matrix }}
    steps:
      - uses: actions/checkout@v4
      - id: configs
        run: echo "matrix=$(cat build.json | jq -c)" >> $GITHUB_OUTPUT

  openwrt_build:
    needs: setup
    strategy:
      matrix:
         target: ${{ fromJSON(needs.setup.outputs.matrix-data) }}
    uses: ./.github/workflows/openwrt-build.yaml
    with:
       openwrt_repository: airoha_en7523/openwrt
       verbose: ${{ github.event_name == 'workflow_dispatch' && inputs.verbose || 'sc' }}
       config_file: ${{ matrix.target.config_file }}
       files_folder: ${{ matrix.target.files_folder }}

  release:
    runs-on: ubuntu-latest
    needs: openwrt_build
    steps:
      - uses: actions/download-artifact@v5
        with:
          merge-multiple: true
          pattern: target_*

      - name: Create release
        uses: akkuman/gitea-release-action@main
        with:
          name: v${{ github.run_number }}
          tag_name: v${{ github.run_number }}
          body: |
              # EN7523 [Build](/actions/runs/${{ github.run_number }})

              build targets

              - tplink xx230v v1
              - tplink xx530v v1
              - tplink ex530v v1
          files: |
            bin/targets.tar
            bin/targets/**/*
            !bin/targets/**/*.apk
            !bin/targets/**/*.ipkg

[
  {
    "config_file": "xx230v_v1.config"
  }
]
on:
  workflow_call:
    inputs:
      config_file:
        type: string
        required: true
      verbose:
        type: string
        required: false
        default: "s"
      openwrt_repository:
        type: string
        required: false
        default: openwrt/openwrt
      files_folder:
        type: string
        required: false

jobs:
  workflow_call:
    runs-on: ubuntu-latest-nonroot
    env:
      CONFIG_CCACHE_DIR: ${{ github.workspace }}/.ccache
      CONFIG_BUILD_LOG_DIR: ${{ github.workspace }}/logs
    steps:
      - name: Openwrt code
        uses: actions/checkout@v4
        with:
          repository: ${{ inputs.openwrt_repository }}
          ref: main

      - name: Config repo
        uses: actions/checkout@v4
        with:
          path: config_build

      - name: Install host dependencies
        run: |
          sudo apt update
          sudo apt install -y build-essential clang flex bison g++ gawk gettext git libncurses5-dev libssl-dev python3-setuptools rsync swig unzip zlib1g-dev file wget ccache
          if [[ "$(uname -m)" == "x86_64" ]]; then
            sudo apt install -y gcc-multilib g++-multilib
          else
            sudo apt install -y gcc-multilib* g++-multilib*
          fi

      - name: Free disk space on runner
        run: |
          env
          df -h
          sudo rm -rf "$AGENT_TOOLSDIRECTORY" || echo "No cache tools directory"
          sudo rm -rf \
            /opt/ghc \
            /opt/google/chrome \
            /opt/microsoft/msedge \
            /opt/microsoft/powershell \
            /opt/pipx \
            /usr/lib/mono \
            /usr/local/julia* \
            /usr/local/lib/android \
            /usr/local/lib/node_modules \
            /usr/local/share/chromium \
            /usr/local/share/powershell \
            /usr/share/dotnet \
            /usr/share/swift
          df -h
          mkdir logs

      - name: Copy config
        run: cp -fv config_build/"${{ inputs.config_file }}" .config

      - name: Copy files folder
        if: ${{ inputs.files_folder != '' }}
        run: cp -frv config_build/"${{ inputs.files_folder }}" ./files

      - name: Get build cache
        uses: actions/cache/restore@v5
        id: ccache_restore
        with:
          key: openwrt_cache_${{ runner.os }}_${{ runner.arch }}_${{ inputs.config_file }}
          path: |
              .ccache
              build_dir
              staging_dir
              bin/packages
              tmp
              dl

      - name: Update and Install feeds
        run: |
          sed -i -Ee 's|https://git.openwrt.org/(feed\|project)|https://github.com/openwrt|g' feeds.conf.default
          ./scripts/feeds update -a
          ./scripts/feeds install -a

      - name: Defconfig
        run: |
          make defconfig
          cat .config

      - name: Download dependencies
        run: make -j$(($(nproc)+1)) download V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Package cleanup
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        id: toolchain_build
        run: make -j$(($(nproc)+1)) package/cleanup V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Tools compile
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        run: make -j$(($(nproc)+1)) tools/compile V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Toolchain compile
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        id: toolchain_build
        run: make -j$(($(nproc)+1)) toolchain/compile V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Toolchain install
        if: steps.ccache_restore.outputs.cache-hit != 'true'
        run: make -j$(($(nproc)+1)) toolchain/install V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Target compiler
        run: make -j$(($(nproc)+1)) target/compile V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Build info
        run: make -j$(($(nproc)+1)) buildinfo

      - name: Package compile
        run: make -j$(($(nproc)+1)) package/compile V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Package install
        run: make -j$(($(nproc)+1)) package/install V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Target install
        run: make -j$(($(nproc)+1)) target/install V=${{ inputs.verbose }}
          CONFIG_CCACHE_DIR="$CONFIG_CCACHE_DIR"
          CONFIG_BUILD_LOG_DIR="$CONFIG_BUILD_LOG_DIR"

      - name: Package index
        run: make -j$(($(nproc)+1)) package/index

      - name: JSON overview image
        run: make -j$(($(nproc)+1)) json_overview_image_info

      - name: Checksum
        run: make -j$(($(nproc)+1)) checksum

      - name: Upload targets
        uses: actions/upload-artifact@v3
        with:
          retention-days: 2
          name: target_${{ inputs.config_file }}
          path: bin/targets/**/*

      - name: Upload logs
        uses: actions/upload-artifact@v3
        if: always()
        with:
          retention-days: 7
          name: logs_${{ inputs.config_file }}
          path: logs/

      - name: Save build cache
        uses: actions/cache/save@v5
        if: always()
        with:
          key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
          path: |
              .ccache
              build_dir
              staging_dir
              bin/packages
              tmp
              dl