組み込みシステムにおける継続的インテグレーション(CI)構築のサンプルコードです。
継続的インテグレーションについては、Web上に詳しく説明した資料が多数ありますので、そちらを参照して下さい。 組み込みにおけるCIについては、私のブログにて説明しています。 https://iot-entrance.blog.jp/archives/8718967.html
サンプルではArduino UNO用のソフトウェアを構築しています。 ただし、実動作部分はほとんど何も実装していません。 実装しているのは主にmakefileとJenkinsスクリプトになります。
CIツールはOSSのJenkinsを使用しています。 ジョブはスクリプトファイル化しているので、Jenkins UI上ではこれらのファイルを呼び出す動作のみを行います。
- 動かないソースを記述します。
#include <avr/io.h>
#include "led.h"
int main() {
a = 1; // <-- Type is not specified!!
return 0;
}
- 該当するコードをGitにコミットします。
git commit -am "Probably works"
- 数秒後デスクトップに通知が届きます。
このCIシステムでは下記が行えます。
- コミットトリガによるJenkinsジョブの起動。
- Jenkinsによる下記の各処理(ジョブ)の実行
- ビルド
- 単体テスト
- カバレッジ測定
- 開発マシンへのインストール
- ジョブ失敗時、slack経由で開発者へリアルタイムにフィードバック。
この資料で利用する用語を説明します。
- 開発マシン ... プログラムを実行するハードウェアを指します。今回の場合はArduino UNOが開発マシンとなります。実業務では、様々なプロダクト本体や、試作機等が開発マシンとなります。
- ビルドマシン ... プログラムのコーディング・ビルド・テストの実行と、開発マシンへのプログラムの転送を行うハードウェアを指します。今回の場合はWindowsを使用しています。実業務ではLinuxであることも多いです。また、コーディングとビルドマシンが分かれている場合もありますが、今回は取り扱いません(改造すれば対応は可能です)。
- プロダクトコード ... 開発マシン上で動作させるためにコーディングするソースコードをプロダクトコードと呼びます。プロダクトの開発目的を直接達成する成果物です。
- テストコード ... プロダクトコードが正常に動作するかをテストするためにコーヂングするコードをテストコードと呼びます。テストコードは開発マシンには転送されません。
- イメージ ... 開発マシンに直接転送可能なプログラムをイメージと呼びます。組み込みソフトウェア開発では、クロスコンパイラでソースコードをまずイメージに変換し、それを開発マシンに転送するという手順を取ります。
以下は一般的な用語ですが、知らない方のために簡単に解説します。
- テストフレームワーク ... 自動化可能なテストを簡単に記述するための様々な機能を持ったライブラリのことです。複数のテストの結果を集計する機能の他、戻り値が特定の値になっていない場合にテストを失敗させたり、特定の関数をテスト用の関数に入れ替えるというような機能(mock機能)などを持ちます。今回はC言語・C++言語の開発において一般的に使用されているCppUTestを利用します。
- カバレッジ ... テストの網羅率です。一般的には様々な定義がありますが、今回はプロダクトコード上の全ての行を1回以上実行した場合にカバレッジが100%となるルールを使用します。ちなみに、これは条件網羅でも分岐網羅でも条件分岐網羅でもありません。
- make ... makeはソフトウェアのビルドを効果的にビルドするために考案されたソフトウェアです。Makefileというファイルに成果物を構築するためのルールを記述し、makeプログラムがそれをパースすることで、実行ファイルやイメージを構築することができます。
括弧()
が付与されたファイル・ディレクトリは、ビルド・テスト・Jenkinsジョブ起動後などに生成されることを表します。
GitHub上のファイルには含まれていません。
+ embedded_ci
+ .vscode ... VSCodeの設定。
| + launch.json ... 実行用コマンドを記述。ビルド・転送を行う。
| + tasks.json ... ビルド単体用のコマンドと、テスト用コマンドを記述。
+ doc ... README.md用の画像ファイルを配置。
+ jenkins
| + script
| | + build.sh ... ビルド単体を行うシェルスクリプト。
| | + install.sh ... Arduinoへのイメージの転送を行うシェルスクリプト。
| | + test.sh ... 単体テストとカバレッジ測定を実行するシェルスクリプト。
| | + pipeline.groovy ... Jenkinsパイプライン用のスクリプト(*1)。
| + (work) ... Jenkinsでビルド・テストを行う際のワークディレクトリ(*2)
| + (build) ... buildジョブ用ワークスペース。
| + (install) ... installジョブ用ワークスペース。
| + (test) ... testジョブ用ワークスペース。
+ modules ... プロダクトコードが格納されたディレクトリ。モジュール別に分離されている。
| + Led ... LED制御モジュールのディレクトリ。
| | + include ... プロダクトコード用のヘッダファイル。
| | | + led.h
| | + (lib) ... ビルドするとLedモジュールの静的ライブラリが生成される。
| | | + (libLed.a)
| | + (obj) ... ビルド用一時ファイルを格納するディレクトリ。
| | | + (led.o)
| | + src .. プロダクトコードのソースファイル。
| | + led.c
| + Makefile ... 下位ディレクトリのmakeを再帰的に呼び出してHEXファイルを生成する。
| + main.c ... プロダクトコードのmain()関数のあるソースファイル。
+ test ... 単体テスト・カバレッジ測定用のコードが格納されたディレクトリ。
| + Led ... 同名のディレクトリに対応するテストコードが格納されたディレクトリ。
| | + (exe) ... テスト用の実行ファイルが格納されるディレクトリ。
| | | + (TestLed.exe)
| | + include ... mock対象のヘッダが格納されたディレクトリ。
| | | + avr
| | | + io.h .. "avr/io.h"のモック用ヘッダ。
| | + src ... テストコードが格納されたディレクトリ。
| | | + io.c ... "avr/io.h"のモックコード。
| | | + main.c ... テストプログラムのエントリポイントが含まれるファイル。
| | | + test_led.c ... Ledモジュールのテストコード。
| | + Makefile ... このモジュールの単体テストとカバレッジ測定を行うMakefile。
| + definitions_project.mk ... プロジェクト共通設定が記載されたMakefile。
| + definitions_test_build_tools.mk ... テスト用のビルド設定が記載されたMakefile。
| + definitions_test_framework.mk ... テストフレームワーク(CppUTest)に関する設定。
| + Makefile ... 下位ディレクトリのmakeを再帰的に呼び出して全テストを実行する。
+ (output) ... Arduinoに転送するHEXファイルと、それの生成元のELFファイルが含まれる。
| + (arduino.elf)
| + (arduino.hex)
+ .gitignore ... 一時ファイルを追跡しないようにするGit用設定ファイル。
+ Makefile ... プロダクト全体のビルド・テスト・転送を実行するMakefile。
+ README.md ... 説明書ファイル(今見ているもの)。
- (*1)... build.sh, install.sh, test.shを呼び出すJenkinsジョブを起動するスクリプトです。
- (*2)... build.sh, install.sh, test.shを実行すると生成されます。このディレクトリは作業用の一時ファイルが格納されているディレクトリであり、.gitignoreに登録してありますのでGitHub上にはありません。
プロダクトコード・テストコードのビルドは、makefileにより各モジュールごとに独立して実行されます。 これにより、変更されていないモジュールのビルドは省略され、ビルド時間が短縮されます。 また、モジュール単位でのビルド・テストも可能になっています。
- Platform
- Arduino UNO ... 開発マシン。イメージをこの上で動作させる。
- Windows 10 ... ビルドマシン。プロダクトコードのコンパイル・転送と、単体テストを実行する。
- CI tools
- Build tools
- GNU make 3.81 ... ビルド・単体テスト・カバレッジ測定のコマンドを実行させる。
- Arduino IDE
- avr-gcc ... AVRマイコン用のCクロスコンパイラ。Arduino IDEに付属。
- avr-ar ... スタティックライブラリを生成するために利用。Arduino IDEに付属。
- avr-objcopy ... ELFファイルをイメージ(HEXファイル)に変換する。Arduino IDEに付属。
- avrdude ... イメージ(HEXファイル)をArduinoに転送する。Arduino IDEに付属。
- MinGW ... Windows用のC言語/C++言語用コンパイラ。
- g++ 8.1.0 ... Windows上でArduino用コードの単体テストを実行するために利用。カバレッジ測定用のプログラムも埋め込む。
- gcov ... カバレッジ測定用ツール。
- Test tools
- CppUTest 3.8 ... C言語/C++言語用テストフレームワーク。テスト時、テストコードにリンクして使用する。
- bc 1.06 ... カバレッジ測定結果を抽出する動作の一部に利用。
Requirementの各プログラムを順にインストールしていけばよいです。
- Windowsの場合、Jenkinsのワークスペースはデフォルトでは
C:\Windows\System32
ディレクトリ内に作成されます。このディレクトリはOSの設定ファイルなどが格納された重要なディレクトリであり、デフォルトではSYSTEMユーザーしかアクセスできず、このディレクトリ内のファイルの参照などが制限されます。これにより、このディレクトリ内でGitコマンドやmakeコマンドが失敗することがあります。これを回避するため、ワークスペースのディレクトリはCドライブ直下などに変更しておくとよいです。
下記のパスをPath環境変数を設定する必要があります。環境変数の追加方法は、Web上で検索してください。注意点として、JenkinsはSYSTEMユーザーで動作するので、ユーザー環境変数は参照されません。環境変数は全てシステム環境変数として設定する必要があります。
- GNUツール(make.exeとbc.exe)の実行ファイルがあるディレクトリ
- MinGWのコンパイラがあるディレクトリ
- AVRマイコンのクロスコンパイラがあるディレクトリ
- CppUTestのインクルードディレクトリ
- CppUTestのライブラリディレクトリ
ここでは、各ツールからビルド・テスト・開発マシンへの転送を行う方法と、自動化の構築方法を記載します。
シェル操作にはGit Bashを利用します。Git Bashは、Gitをインストールするとおまけでついてくるシェル環境です。 コマンドプロンプト・PowerShell・Cygwinなどとはディレクトリの表現やCドライブのパスが異なるため注意してください。
コマンドはembedded_ciディレクトリ直下で実行します。
$ cd embedded_ci
- ビルド
makeコマンドを実行するのみです。ビルドを行うだけでは開発マシンにイメージは転送されません。また、テストコードのビルドも行われません。
$ make
- テスト
make test
によりテストコードのビルド・テストの実行・カバレッジの測定が行われます。カバレッジ測定では、各モジュールのMakefile内に記載されたCOVERAGE_TARGET_RATE
以下の網羅率になるとテストが失敗します。
$ make test
- イメージの転送
イメージをArduinoに転送します。Arduinoをビルドマシンに接続している必要があります。接続されていない場合はmakeが失敗します。また、このコマンド実行時にまだプロダクトコードのビルドが行われていない場合、先にビルドが実行されます。
$ make install
- 一時ファイルの削除
make
やmake test
によって生成された下記のファイルを削除します。
- 静的ライブラリ生成時の一時ファイル(
.obj
) - 各モジュールの静的ライブラリ(
.lib
) - カバレッジ測定用の一時ファイル(
.gcno
・.gcna
・.gcov
・.gresult
) - Arduinoへの転送用のイメージ(
.hex
・.elf
) - テスト用の実行ファイル(
.exe
)
$ make clean
Windows上でのVSCodeのシェルはデフォルトではPowerShellになっていますが、makeがうまく動かないのでGit Bashに変更します。 シェルの変更方法はググってください。
これらはいずれも前述したmakeコマンドを実行します。
- ビルド :
Ctrl + Shift + B
- 転送 :
F5
- Arduino(開発マシン)がビルドマシン接続されていない場合失敗します。
- テスト :
Ctrl + Shift + P
- リストから
Tasks: Run Test Task
を選択します。
- それ以外 :
Ctrl + Shift + P
- リストから
Tasks: Run Task
を選択します。 - リストから実行したいタスク(cleanなど)を選択します。
- JenkinsのシェルをGit Bashに変更します。
- Jenkins上にジョブを作成します。
- Jenkinsの管理画面に戻ります。
- 画面左上の「新規ジョブ作成」を選択します。
- 「Enter an item name」の下にあるテキストボックスにジョブの名前を登録します。今回の場合、ビルド・転送・テストをそれぞれ行うジョブ合計3個と、これらを一括して実行するパイプラインジョブを1個作成しますので、それに相当する名前にするとよいでしょう。下記は命名例です。
- ジョブの種類は下記のように決定します。
- ビルド(
arduino_build
)・転送(arduino_install
)・テスト(arduino_test
)の各ジョブの場合は、「フリースタイル・プロジェクトのビルド」を選択します。 - 一括実行ジョブ(
arduino_all
)の場合は「パイプライン」を選択します。
- ビルド(
- 画面下部の「OK」をクリックします。
- ビルド・転送・テスト・一括実行の各ジョブが全て作成されるまで、1~5を繰り返します。
- ビルド・転送・テストジョブから
jenkins/script
ディレクトリ内のスクリプトファイルを呼び出すように設定します。- Jenkinsのトップ画面に戻ります。
- 手順2で作成したビルド(
arduino_build
)・転送(arduino_install
)・テスト(arduino_test
)ジョブのいずれかの名前をクリックします。 - そのジョブの詳細ページが表示されます。
- 画面左側の「設定」をクリックします。
- 「ビルド」→「ビルド手順の追加」→「シェルの実行」をクリックします。
- シェルスクリプトを記載するテキストボックスが現れますので、ここにjenkinsスクリプトファイルを実行するコマンドを記述します。コマンドは、Git Bashの文法に適合している必要があります。例えば、
C:\embedded_ci
にプロジェクトをcloneし、ビルドジョブの設定を行う場合は、/C/embedded_ci/jenkins/script/build.sh
と記載します。 - 「保存」を選択します。
- ビルド・転送・テストの各ジョブが全て設定されるまで、1~7を繰り返します。
- 一括実行ジョブからパイプラインスクリプトを呼び出すように設定します。
- Jenkinsのトップ画面に戻ります。
- 手順2で作成した一括実行ジョブ(
arduino_all
)の名前をクリックします。 - そのジョブの詳細ページが表示されます。
- 画面左側の「設定」をクリックします。
- 「パイプライン」→「定義」の下にあるリストボックスから、「Pipeline script from SCM」を選択します。
- パイプライン設定用の画面が表示されます。
- 「SCM」の下のリストボックスから「Git」を選択します。
- 「リポジトリURL」の下のテキストボックスに、このプロジェクトをcloneしたディレクトリへのパスを指定します。例えば、
C:\embedded_ci
にプロジェクトをcloneした場合は、C\embedded_ci
になります。 - 「認証情報」は「なし」を選択します。
- 「ビルドするブランチ」は「*/master」を指定します。
- 「リポジトリ・ブラウザ」は「(自動)」を選択します。
- 「Script Path」の下のテキストボックスに、パイプラインスクリプトのパスを指定します。プロジェクトのルートディレクトリからの相対パスであることに注意してください。今回の場合は、
jenkins/script/pipeline.groovy
となります。
- 実行したいジョブを選択して実行します。
- Jenkinsのトップ画面に戻ります。
- 画面右上のユーザー名をクリックします。
- ユーザーの画面が表示されたら、画面左側の「設定」をクリックします。
- 「APIトークン」→「トークン新規追加」を選択します。
- 適当な識別名を入力し「生成」をクリックします。
- API tokenが生成されるので、この値をテキストファイルなどにバックアップしておきます。なお、この値は画面遷移すると二度と確認できませんので注意してください。なお、下の図のtoken値は一例です。トークンは作成する度に異なる値になりますので、自分で生成したトークンを入力してください。
- Jenkinsのトップ画面に戻ります。
- Job access tokenを発行したいジョブを一覧から選びます。今回の場合、一括実行ジョブのJob access tokenを発行するので、これを選択します。命名例通りにジョブ名を指定していた場合は、
arduino_all
を選択することになります。 - 一括実行ジョブのページが表示されたら、画面左側の「設定」をクリックします。
- 「ビルドトリガ」の中にある「リモートからビルド」にチェックを入れます。
- 「認証トークン」に任意の文字列を設定します。これがJob access tokenになります。
- 画面下部の「保存」をクリックします。
- このプロジェクトをcloneしたディレクトリをエクスプローラ等で表示します。
- その直下にある「.git」ディレクトリを開きます。
- 隠しファイル扱いになっているので、あらかじめエクスプローラの設定を変更し全てのファイルが表示されるようにしてください。
- 「hocks」ディレクトリを開きます。
- 「pre-commit」というファイルを作成し、テキストエディタなどで表示します。なお、POSIXコマンドを記述するファイルですから、文字コードはUTF-8、改行コードはLFに統一しておくことをおすすめします。
- 「pre-commit」にJenkinsの一括実行ジョブを起動するスクリプトコードを記述します。
<Server address>
は、Jenkinsを起動しているサーバーのURLを指定します。Jenkinsサーバーとビルドマシンが同一ならば、localhost
となります。<Port>
には、Jenkinsがリクエストを受け付けているポート番号を指定します。初期設定ならば8080
となりますが、環境によっては変更される場合もあるので設定をよく確認してください。<User name>
にはビルドを行うJenkinsユーザー名を指定します。ここで指定するユーザーは、自動化したいジョブへのアクセス権限があるユーザーである必要があります。<Jenkins API Token>
には、<User name>
に対応するユーザーのAPI tokenの値を指定します。API tokenの作成方法は前述の通りです。<Job name>
には、コミット時に実行したいジョブの名前を指定します。今回の場合は、一括実行ジョブを指定します。例として、命名例通りにジョブの名前を付与していた場合は、arduino_all
を指定します。<Job Access Token>
には、<Job name>
で指定したジョブのJob access tokenの値を指定します。(ユーザー毎のAPI tokenとは異なります!)Access tokenの作成方法は前述の通りです。
#!/bin/sh
curl --user "<User name>:<Jenkins API Token>" http://<Server address>:<Port>/job/<Job name>/build?token=<Job Access Token>
ビルド失敗投稿用にSlackのworkspaceを開設する必要があります。ワークスペース名のプルダウンメニューを開き、「ワークスペースの追加」→「ワークスペースを新規作成」を選択して、ビルド結果通知用のワークスペースを作成してください。
なお、既存のワークスペースにあるチャンネルを投稿先とすることもできます。その場合は、該当するワークスペースにログインした状態にしてください。
例としてブラウザ版のslack設定を示します。デスクトップアプリ・モバイルアプリの場合は設定方法が異なりますので、slackのヘルプを参照してください。
- 右上のユーザーアイコンをクリックし、「環境設定」を選びます。
- 「通知」タブの「通知のタイミング」→「すべての新規メッセージ」を選びます。
- Jenkinsのトップ画面に戻ります。
- 「Jenkinsの管理」→「プラグインの管理」を選択します。
- 一覧から「Slack Notification」を発見し、左側にあるチェックボックスにチェックして画面下部の「Install without restart」を選択します。
- SlackのJenkins CI設定ページを開きます。ここでは、SlackにJenkins用の設定を施します。
- 「チャンネルへの投稿」のリストボックスから、Jenkinsのビルド失敗通知を投稿するチャンネル名を1つ選択し「Jenkins CIインテグレーションの追加」をクリックします。想定した投稿先チャンネルが表示されない場合は、「Slack workspaceの開設」の手順を確認し、通知用のチャンネルを作成して再度1から手順をやり直します。
- 画面中程にある、下記の項目をコピーしてテキストファイルなどにバックアップします。忘れると面倒なので注意してください。また、この値が攻撃者に発見されるとSlackをハックされます。くれぐれもGitなどにコミットしたりしないように注意してください。
- Jenkinsのトップ画面に戻ります。
- 「Jenkinsの管理」→「システムの設定」を選択します。
- ページ下部の「Slack」の項目を表示し、「workspace」に先程コピーした「チームサブドメイン」の値を入力します。
- 「Credential」の右にある「追加」から「Jenkins」を選択します。
- 「認証情報の追加」の画面が表示されますので、下記のように設定します。全て入力したら「追加」を選択します。
- 「Credential」の下のリストボックスから、手順8で設定した「ID」と同じ名前を選択します。先程の例の通りにIDを設定していた場合は、
Jenkins-token
を選びます。 - 画面下部の「保存」を選択します。
- Jenkinsのトップ画面に戻ります。
- ビルドジョブとテストジョブの設定画面をそれぞれ開き、「ビルド後の処理の追加」→「Slack notification」を選択します。命名例通りにジョブを設定している場合は、
arduino_build
とarduino_test
の2つのジョブに対して設定を行います。 - 「Notify Every Failure」にチェックを入れ、画面下部の「保存」を選択します。
Windows上でCUIベースビルドを実現するためには色々と準備が必要なのでやや敷居が高いかもしれません。
不明点は下記までどうぞ。
- 作成者 : BARANCE
- Twitter : https://twitter.com/BARANCE_TW