Just yesterday at the time of writing, GitHub (finally) released their public ARM runners for Open-Source projects.

This means you can now build ARM programs natively on Linux without having to fiddle with weird cross-compilation.

One way to achieve that is through a Matrix. Considering the following workflow to build, then upload an artifact (taken from the YDMS Opus workflow I wrote):

on: [push]

jobs:
  Build-Linux:
    name: Builds Opus for Linux
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download models
        run: ./autogen.sh

      - name: Create build directory
        run: mkdir build

      - name: Create build out variable
        id: buildoutput
        run: echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT"

      - name: Configure CMake
        working-directory: ${{ steps.buildoutput.outputs.build-output-dir }}
        run: cmake .. -DBUILD_SHARED_LIBS=ON

      - name: Build Opus for Linux
        working-directory: ${{ steps.buildoutput.outputs.build-output-dir }}
        run: cmake --build . --config Release --target package

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: opus-linux
          path: ${{ steps.buildoutput.outputs.build-output-dir }}/**/*.so

We can now easily make it build for ARM by using a matrix referencing the new ubuntu-24.04-arm runner label.

For instance, we can add this before the job steps:

    strategy:
      matrix:
        osver: [ubuntu-latest, ubuntu-24.04-arm]

Then change the runs-on configuration to specify ${{ matrix.osver }} which will create jobs for all the OS versions specified in the matrix.

One issue that might then arise is a name conflict when uploading the job artifacts. For instance, if our old Linux build uses:

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: opus-linux
          path: ${{ steps.buildoutput.outputs.build-output-dir }}/**/*.so

And the same step is used by the ARM workflow, we will get an error that the artifact matching the name opus-linux already exists for this workflow run.

This is where a small conditional step can be added to set an environment variable with the desired name:

      - name: Set dist name
        run: |
          if ${{ matrix.osver == 'ubuntu-24.04-arm' }}; then
            echo "distname=opus-linux-arm" >> "$GITHUB_ENV"
          else
            echo "distname=opus-linux" >> "$GITHUB_ENV"
          fi

We can then change our artifact upload step to use these new names:

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ${{ env.distname }}
          path: ${{ steps.buildoutput.outputs.build-output-dir }}/**/*.so

As a bit of a sidetrack, you can also use checks like this to conditionally skip (or execute) steps depending on the architecture, using a if statement:

      - name: Mystep
        uses: actions/myaction@v4
        if: ${{ matrix.osver != 'ubuntu-24.04-arm' }}
        steps: |
          echo Hello world

In the end, it’s good that this GitHub feature finally landed. Before that, you had to use “large” runners which can cost quite a bit in the end.