Using Docker and GitHub actions to build an embedded project.

These scripts have been added to to the modern C++ RISC-V blinky project to implement build on push.

The general flow is:

  • The github/workflows/main.yml has a trigger on push, this invokes an action in action.yml.
  • The action.yml action will create the Docker image and execute
  • The Dockerfile builds an image that includes to tool-chain and a docker_entrypoint.sh script to call CMake.
  • The docker image is loaded and passed the path to the GITHUB_WORKSPACE that has a clone of the git repo.
  • The arguments passed to docker and forwarded to the docker_entrypoint.sh script and it changes to that folder and builds using CMake.
  • On exit github/workflows/main.yml packages up the build artifacts.

Refer to the github documentation:

  • (https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action)
  • (https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts)

The Dockerfile

This simply takes the instructions from the tool-chain and scriptifies them. (although updating the PATH like this is not recommended by the xpack tools maintainer..)

RUN npm install --global xpm@latest
RUN xpm install --global --verbose  @xpack-dev-tools/riscv-none-embed-gcc@10.1.0-1.1.1   
ENV PATH=/root/.xpack/repos/@xpack-dev-tools/riscv-none-embed-gcc/10.1.0-1.1.1/.content/bin:/bin/:/usr/bin

It then configures the ENTRYPOINT script that is run when the docker image is started.

# Copies your code file from your action repository to the filesystem path `/` of the container
COPY docker_entrypoint.sh /docker_entrypoint.sh

# Code file to execute when the docker container starts up (`entrypoint.sh`)
ENTRYPOINT ["/docker_entrypoint.sh"]

The ENTRYPOINT script.

The first argument is the path to the cloned git repo.

if [ -d $1 ] ; then
    echo "Changing to working directory: $1"
    cd $1
else
    echo "Not changing directory: $1"
fi

# Remove $1 from args.
shift 

This iterates over each argument, using it to determine the path to the project.

while (( "$#" )) ; do

    PROJECT_SRC=$1
    BUILD_DIR=${BUILD_BASE}/$(basename ${PROJECT_SRC/\/src})


    #... CMake work ...


    shift

done

The work is done by CMake.

Create the build scripts: (-B is the output directory, -S is the source).

The RISC-V toolchain file is from five-embeddev.com.

    rm -rf  ${BUILD_DIR}
    mkdir -p ${BUILD_DIR}

    cmake \
	-DCMAKE_MAKE_PROGRAM=make \
	-DCMAKE_TOOLCHAIN_FILE=../../${CMAKE_DIR}/riscv.cmake \
	-G "Unix Makefiles" \
	-B ${BUILD_DIR} \
	-S ${PROJECT_SRC} \

Run then and check the output:

    cmake --build ${BUILD_DIR}
    
    if [ "$?" != "0" ] ; then
	echo "CMAKE: ${PROJECT_SRC}; Build failed: $?"
	rc = $[$rc + 1]
    else
	echo "CMAKE: ${PROJECT_SRC}; Build success"
    fi

Github Actions

Modified from here.

The syntax is here

The inputs.workspace is defined as the docker file can also be tested locally by changing this argument.

The start_time and end_time are not really needed, they are inherited from the github example.

# action.yml
name: 'Build'
description: 'Build and record the time'
inputs:
  workspace:
    description: Path to repo
outputs:
  start_time: # id of output
    description: 'before build'
  end_time: # id of output
    description: 'after build'
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - $\{\{inputs.workspace\}\}
    - blinky/src

Looking at the github log it is translated into this (when run on a repo called: github-workflows-test).

/usr/bin/docker run \
    --name fa4e14a6f1948d972d5ce0546cd13fe8f27891_23dc6d \
    --label fa4e14 \
    --workdir /github/workspace \
    --rm \
    -e INPUT_WORKSPACE \
    -e HOME \
    -e GITHUB_JOB \
    -e GITHUB_REF \
    -e GITHUB_SHA \
    -e GITHUB_REPOSITORY \
    -e GITHUB_REPOSITORY_OWNER \
    -e GITHUB_RUN_ID \
    -e GITHUB_RUN_NUMBER \
    -e GITHUB_RETENTION_DAYS \
    -e GITHUB_RUN_ATTEMPT \
    -e GITHUB_ACTOR \
    -e GITHUB_WORKFLOW \
    -e GITHUB_HEAD_REF \
    -e GITHUB_BASE_REF \
    -e GITHUB_EVENT_NAME \
    -e GITHUB_SERVER_URL \
    -e GITHUB_API_URL \
    -e GITHUB_GRAPHQL_URL \
    -e GITHUB_WORKSPACE \
    -e GITHUB_ACTION \
    -e GITHUB_EVENT_PATH \
    -e GITHUB_ACTION_REPOSITORY
    -e GITHUB_ACTION_REF \
    -e GITHUB_PATH \
    -e GITHUB_ENV \
    -e RUNNER_OS \
    -e RUNNER_NAME \
    -e RUNNER_TOOL_CACHE \
    -e RUNNER_TEMP \
    -e RUNNER_WORKSPACE \
    -e ACTIONS_RUNTIME_URL \
    -e ACTIONS_RUNTIME_TOKEN \
    -e ACTIONS_CACHE_URL \
    -e GITHUB_ACTIONS=true \
    -e CI=true \
    -v "/var/run/docker.sock":"/var/run/docker.sock" \
    -v "/home/runner/work/_temp/_github_home":"/github/home" \
    -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" \
    -v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands" \
    -v "/home/runner/work/github-workflows-test/github-workflows-test":"/github/workspace" fa4e14:a6f1948d972d5ce0546cd13fe8f27891  \
    "/home/runner/work/github-workflows-test/github-workflows-test" \
    "blinky/src"

Github Workflow

This file was created by the github template. The additions are below.

The interface to actions are.

  • Path to actions.yml : uses: ./
  • Arguments to the action: with: workspace: $\{\{ github.workspace \}\}
  • Results from the actions: $\{\{ steps.build.outputs.start_time \}\}
      # Runs a single command using the runners shell
      - name: Docker Build
        uses: ./
        id: Build
        with:
          workspace: $\{\{ github.workspace \}\}
      - name: Get the output time
        run: echo "The time was $\{\{ steps.build.outputs.start_time \}\} -> $\{\{ steps.build.outputs.end_time \}\}"   

Another action saves the build output:

      - name: Archive production artifacts
        uses: actions/upload-artifact@v2
        with:
          name: build-files
          path: |
            build/**/*.elf
            build/**/*.hex
            build/**/*.disasm
            build/**/*.map