Docker, CI & Mbed OS

Hi there! –

Following the theft of our CI computers, I’ve been working on setting up docker images to package all the needed tools for Mbed OS development.

Getting familiar with Docker was quite easy, keeping the images lean and clean is another challenge!

But I managed to make it work with a pretty decent image size (~260MB compressed) that only takes ~20 seconds to initialize on Github Actions.

We can compile our whole firmware with that.

If you’re interested, you can follow the development here:

:warning: Please not it’s still a wip, things might break, change unexpectedly and I might force push to main if I feel like it :wink:

Features :white_check_mark::

  • clang-format
  • arm-none-eabi-gcc

Upcoming features :construction::

  • unit tests with clang
  • sonarcloud analysis

The project using it is not public yet, but I can share samples of code if needed to setup Github Actions.

As always, feedback are more than welcome!

Best,
– Ladislas

2 Likes

Quick update:

The goal was to use different “small” docker images on Github Actions to run a variety of tasks:

  • compile code for the target mcu
  • compile and run unit tests
  • run clang-format
  • run clang-tidy on added/modified files
  • run sonarcloud & codecov

Turns out that the process with docker was very interesting but it did not work out as expected:

  • building docker images is tedious
  • building small docker images is even more so
  • the workflow is complex and hard to automate when starting up
  • initializing containers takes time
  • installing needed tools makes the images bigger
  • initializing big images takes even more time…
  • it was hard to setup all the tools and toolchains
  • running a docker image inside Github actions is running docker inside docker, which seems strange

It also turns out that Github Actions’s ubuntu runner has almost everything needed to be up and running in not time.

I’m happy to share our workflow files for those interested.

Thanks for sharing your experience. This is on my backlog too. I actually tried to do a build once but mbed-tools had some features missing to automate the build back then. It’s a tough choice between Actions and Docker, on the one hand you have good integration with GitHub, on the other hand you have a more portable approach. I would actually prefer doing it using Docker due to portability, but as your experience shows it has many drawbacks…

Quick question regarding clang-format - recall one of your cmake repos, there was clang format following google style format. Is this own style or similar to what Mbed OS has (K&R with 2 exceptions) ?

I think Docker would be better if you run your own server, jenkins or something else. Then you have full control and you don’t care about the size of the image as you can cache it locally.
Same goes for development toolchains that you can share through a docker image to make sure the whole team uses the same software.

What I did not expect was the amount of stuff you need to install to actually build something on a fresh ubuntu:latest docker image. And don’t get me started with Alpine, I first tried that but it’s a nightmare…

You end up installing tens of packages, and the important ones often don’t have the version I’m actually using on my machine (I run macOS). For example cmake, you need to add the upstream deb repository. Same with the clang toolchain, gcc, etc.

Then you have python. Oh gosh, that’s heavy and something mbed absolutely needs. It makes me wish we can drop it for just building software and optionally install it for not important stuff like pretty table.

As we are using Github Actions and have no plan to change that, using their ubuntu:latest image was actually the easiest. You get many things for free, even homebrew/linuxbrew! So super easy to install new packages.

Using cache is also very easy and their actions ecosystem is really useful.

This is what we have for cross compiling. With cache enabled, it takes about a minute from start to finish.

# CI Workflow

name: Cross-compilation

on:
  push:
    branches:
      - develop
      - master

  pull_request:
    branches:
      - develop
      - "feature/**"
      - "bugfix/**"

jobs:
  leka_os_and_al:
    name: LekaOS & al.
    runs-on: ubuntu-latest

    env:
      CLICOLOR_FORCE: true # ninja

      CCACHE_DIR: /home/runner/work/ccache
      CCACHE_COMPRESS: true
      CCACHE_COMPRESSLEVEL: 6

      ARM_TOOLCHAIN_URL: "https://developer.arm.com/-/media/Files/downloads/gnu-rm/10-2020q4/gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux.tar.bz2"
      ARM_TOOLCHAIN_FILENAME: "gcc-arm-none-eabi-*-x86_64-linux.tar.bz2"
      ARM_TOOLCHAIN_EXTRACT_DIRECTORY: "gcc-arm-none-eabi-*"

    strategy:
      fail-fast: false
      matrix:
        custom_target: ["LEKA_V1_2_DEV"]

    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

      #
      # Mark: - Create cache variables
      #

      - name: Create cache variables
        id: cache_variables
        shell: bash
        run: |
          echo "::set-output name=date::$(date +%s)"

          sha=($(echo -n ${{ env.ARM_TOOLCHAIN_URL }} | sha1sum ))
          echo "::set-output name=arm_toolchain_url_sha::$sha"

      #
      # Mark: - Install ARM GCC Toolchain
      #

      - name: Cache ARM GCC Toolchain
        id: cache_arm_toolchain
        uses: actions/cache@v2
        with:
          path: ~/gcc-arm-none-eabi
          key: ${{ runner.os }}-global_cache-arm_toolchain-${{ steps.cache_variables.outputs.arm_toolchain_url_sha }}

      - name: Install ARM GCC Toolchain
        if: steps.cache_arm_toolchain.outputs.cache-hit != 'true'
        run: |
          wget ${{ env.ARM_TOOLCHAIN_URL }}
          tar -xjf ${{ env.ARM_TOOLCHAIN_FILENAME }} && rm -rf ${{ env.ARM_TOOLCHAIN_FILENAME }}
          mv ${{ env.ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} ~/gcc-arm-none-eabi

      - name: Add ARM GCC Toolchain to path
        run: |
          echo "~/gcc-arm-none-eabi/bin" >> $GITHUB_PATH

      - name: Test ARM GCC Toolchain
        run: |
          ls -al ~/gcc-arm-none-eabi/bin
          arm-none-eabi-gcc -v

      #
      # Mark: - Install misc. tools
      #

      - name: Install tools
        run: |
          sudo apt-get install -y --no-install-recommends ninja-build ccache

      #
      # Mark: - Download mbed-os & ccache
      #

      - name: Cache Mbed OS
        id: cache_mbed_os
        uses: actions/cache@v2
        with:
          path: extern/mbed-os
          key: ${{ runner.os }}-global_cache-mbed_os-${{ hashFiles('extern/mbed-os/platform/include/platform/mbed_version.h', '.mbed_version') }}

      - name: Curl Mbed OS
        if: steps.cache_mbed_os.outputs.cache-hit != 'true'
        run: |
          make mbed_curl

      - name: Cache ccache
        id: cache_ccache
        uses: actions/cache@v2
        with:
          path: ${{ env.CCACHE_DIR}}
          key: ${{ runner.os }}-cache-cross_compilation-${{ matrix.custom_target }}-ccache-${{ steps.cache_variables.outputs.date }}
          restore-keys: |
            ${{ runner.os }}-cache-cross_compilation-${{ matrix.custom_target }}-ccache-

      #
      # Mark: - Install python/pip dependencies
      #

      - name: Setup Python
        uses: actions/setup-python@v2
        with:
          python-version: "3.x"

      - name: Cache pip dependencies
        id: cache_pip_dependencies
        uses: actions/cache@v2
        with:
          path: |
            ~/.cache/pip
            ${{ env.pythonLocation }}
          key: ${{ runner.os }}-global_cache-pip_dependencies-${{ env.pythonLocation }}-${{ hashFiles('extern/mbed-os/requirements.txt', '.mbed_version') }}

      - name: Install pip packages
        run: |
          pip install --upgrade --upgrade-strategy eager mbed-cli
          pip install --upgrade --upgrade-strategy eager -r ./extern/mbed-os/requirements.txt

      - name: Test pip packages
        run: |
          pip list -v
          mbed-cli --help

      #
      # Mark: - Config & build
      #

      - name: Ccache pre build
        run: |
          make ccache_prebuild

      - name: Config, build LekaOS & al.
        run: |
          make config TARGET_BOARD=${{ matrix.custom_target }}
          make TARGET_BOARD=${{ matrix.custom_target }}

      - name: Ccache post build
        run: |
          make ccache_postbuild
          ccache -z

      - name: Upload build artifacts
        if: |
          contains(matrix.custom_target, 'LEKA_V1_2_DEV') &&
          (contains(github.ref, 'develop') || contains(github.ref, 'master'))
        uses: actions/upload-artifact@v2
        with:
          name: LEKA_V1_2_DEV-Build-Artifacts
          retention-days: 7
          path: |
            _build/LEKA_V1_2_DEV/**/*.bin
            _build/LEKA_V1_2_DEV/**/*.hex
            _build/LEKA_V1_2_DEV/**/*.elf

I think it’s us, the style is based on google with customization. Clang-format can flag issues with formatting and also has a way to fix the issues without breaking the code. It’s amazingly simple to use and we were able to format our whole codebase using that. You can of course exclude folders.

Here is the style: (and documentation Clang-Format Style Options — Clang 12 documentation)

BasedOnStyle: Google
IndentWidth: 4
TabWidth: 4
ColumnLimit: 120

Language: Cpp
Standard: Latest

IncludeBlocks: Regroup

IncludeCategories:
  - Regex:           '(<|"mbed)'
    Priority:        0
    SortPriority:    0
  - Regex:           '"(drivers|rtos|events|platform)'
    Priority:        2
    SortPriority:    0
  - Regex:           'PinNames.h'
    Priority:        1
    SortPriority:    0
  - Regex:           '"(connectivity|features|storage|ble)'
    Priority:        3
    SortPriority:    0

AccessModifierOffset: -2

Cpp11BracedListStyle: true

DerivePointerAlignment: false
PointerAlignment: Right
AlignAfterOpenBracket: true
AlignConsecutiveAssignments: true

FixNamespaceComments: true
IndentCaseLabels: true

AlignEscapedNewlines: Right

SpacesBeforeTrailingComments: 3
AlignTrailingComments: true
ReflowComments: true

AlignConsecutiveMacros: true

NamespaceIndentation: Inner
SpaceBeforeCpp11BracedList: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: false

UseTab: Always
KeepEmptyLinesAtTheStartOfBlocks: false

AllowShortFunctionsOnASingleLine: Inline
AllowShortBlocksOnASingleLine: Never
AllowShortLambdasOnASingleLine: All

BreakBeforeBraces: Custom
BraceWrapping:
  AfterClass: true
  AfterFunction: true
  AfterEnum: true
  AfterNamespace: false
  BeforeLambdaBody: false

IndentPPDirectives: BeforeHash

And here is the script:

@Kojto and this is how it looks, it’s all automated formatting:

// Leka - LekaOS
// Copyright 2021 APF France handicap
// SPDX-License-Identifier: Apache-2.0

#ifndef _LEKA_OS_DRIVER_LK_CORE_PWM_OUT_H_
#define _LEKA_OS_DRIVER_LK_CORE_PWM_OUT_H_

#include "drivers/PwmOut.h"

#include "interface/drivers/PwmOut.h"

namespace leka {

class CorePwm : public interface::PwmOut
{
  public:
	explicit CorePwm(mbed::PwmOut &pwm) : _pwm {pwm} {};

	auto read() -> float final;
	void write(float value) final;

  private:
	mbed::PwmOut &_pwm;
};

}	// namespace leka

#endif	 //_LEKA_OS_DRIVER_LK_CORE_PWM_OUT_H_
// Leka - LekaOS
// Copyright 2021 APF France handicap
// SPDX-License-Identifier: Apache-2.0

#include "CorePwm.h"

using namespace leka;

auto CorePwm::read() -> float
{
	return _pwm.read();
}

void CorePwm::write(float value)
{
	_pwm.write(value);
}

What about “official” Mbed OS Docker Image?

Worth giving it a try!