Перейти к содержанию

CI/CD в embedded. Jenkins + GitLab + HeadlessBuild.

Прежде всего – хотелось избавить себя от рутинных операций, которые необходимо прокручивать в каждый релиз артефакта каждого проекта. Второе – понять, нужен ли мне вообще такой подход у разработке, сколько от него профита. Третье – узнать новое.

Вот несколько ссылок, где есть некоторая информация по этому вопросу:

Не забываем, что мне совершенно неизвестно как делать правильно. Универсального рецепта не нашлось, поэтому собираю своего Кракена на win-сервере(local).

Прежде всего, я отказался от слоя абстракции связанной с контейнеризацией. Сорян, любители Docker, мне он пока не зашел. Отличная идея использовать контейнеры, но духоты предвижу и так с головой – лучше не усложнять “архитектуру” еще одним слоем. Оставим на лучшее время. Трезво оценив возможности своих познаний я понял, что слои из Groovy pipeline-скриптов, batch-скриптов предоставляют уже немаленький интерфейсный ад.

Классическая разработка под MCU:

  1. Синхронизация с GitLab(GitHub, SVN…)
  2. Внесение изменений в прошивку
  3. Сборка прошивки (необходимо подготовленное рабочее окружение)
  4. Подготовка артефактов сборки к публикации
  5. Публикация прошивок на сервере
  6. Ручное добавление прошивок на новых релизах при поставке их в составе комплекса ПО

Как я это вижу с CI/CD:

  1. Синхронизация с Git
  2. Внесение изменений в прошивку
  3. Добавление тега текущей версии
  4. Пуш в Git

Автоматизированная сборка и публикация:

Вижу тут некоторые плюсы:

  • Автоматизация задач:
    • Сборка прошивок
    • Прогон тестов
    • Подготовка прошивок к публикации (подпись, правка, изменение имени)
    • Публикация на сервер
  • Использование единого набора инструментов для сборки
  • Снижение сложности входа для новых сотрудников (спорно, но все же в перспективе)
  • Возможность быстро получить новую версию прошивки без необходимости настраивать рабочее окружение (тоже спорно, но иногда очень бывает нужно для хотфикса)

Да, я вполне понимаю, что подобная система даст наибольшую отдачу при использовании Make/CMake и контейнеров (для изолирования среды и работы с уже подготовленными средами). Но имеем, что имеем – переписывать весь ворох проектов ради своего интереса в нерабочее время, это выше моих сил.

Дальнейшее описание будет для системы контроля версий GitLab.

У них есть отличная документация по интеграции GitLab с Jenkins (https://docs.gitlab.com/ee/integration/jenkins.html), лучше делать по ней, а сюда лишь посматривать – вдруг чего будет интересного

Про Jenkins


Суть его – лишь скрипт-раннер и место хранения
Дженкинс может подхватывать веб-хуки с гитлаба и сам начинать выполнять скрипты
Лучше всего, опять же, почитать документацию(https://www.jenkins.io/doc/book/using/) – здесь будет лишь очень поверхностная информация
В Jenkins прописывается:

  • git ссылочка до проекта с кодом и бранчем
  • Откуда брать файл со скриптом конфигурации дженкинса (В этом скрипте прописываются шаги и какие батнички выполнить – достаточно просто)

То есть, практически вся работа у Jenkins будет стянуть сборку, сбилдить и запустить пару батников.

Есть два варианта использования:

Нажимаем создать Item (Item – это один проект для сборки)

Тут нужно вписать название задачи и выбрать тип. Со свободной конфигурацией и Pipeline.

Далее нужно вписать ссылку на проект и данные авторизации

Триггеры сборки определяют по какому из событий будет выполнена задача

Свободная конфигурация позволяет настроить все шаги сборки через веб-GUI. Вот пример

Pipeline – более гибкая в настройке система, которая позволяет исполнять скрипт сборки при наступлении события-триггера. Шаги сборки определяет скрипт на языке Groovy. Триггеры сборки и git-авторизация остаются теми же для обеих систем.

Pipeline Script позволяет записать скрипт в окне в веб-GUI. Pipeline Script from SCM – скачивает и исполняет Groovy-скрипт(Jenkinsfile) напрямую из репозитория с проектом.

Обратите внимание, что имя файла-скрипта должно быть указано в Script Path

Шаги для настройки своего проекта

Скачал Jenkins с официального сайта

Запустил, установил, ничего сложного

Установил GitLab, Pipeline: Stage View, Pipeline, Folders, Credentials, CppCheck и еще пару плагинов.

Перезапустил Jenkins

В конфигурации системы Jenkins в разделе GitLab выставил ссылку на ip GitLab и добавил авторизацию по токену GitLab API.

Токен генерируется в GitLab и для него выставил доступ к API.

В настройках GitLab-проекта для сборки в settings->integrations включил Jenkins CL

Во вкладке settings->webhooks можно очень похожим образом настроить все тоже самое, с небольшой разницей в степени интеграции Jenkins и GitLab.

Jenkins URL: http://MY_PC_IP:8080/

Project Name: TEST_BUILDER (Должно совпадать с именем подготовленного задания на Jenkins)

Username и пароль – от моего Jenkins (логин: admin и пароль, который находится, примерно, тут C:\ProgramData\Jenkins.jenkins\secrets\initialAdminPassword)

На Jenkins создал Item Pipeline с настройкой Pipeline Script from SCM

Теперь можно брать Jenkinsfile из проекта на гите

Так же в Jenkins прописал глобальные переменные с особыми путями (IDE, cppcheck, папка с bat-файлами и т.д.)

Вот и все. При пуше в репозиторий с тегом, будет проведена сборка проекта согласно скрипту Jenkinsfile. Артефакты сборки можно будет закинуть на файловый сервер/другой репозиторий с помощью bat файлов.

Вот так выглядит пайплайн проекта:

Видно, что есть замечания от cppcheck – смотрим:

Предупреждения кликабельны – можно посмотреть какой участок кода вызвал проблему прям в вебе:

Ну, и артефакты сборки пушаться на файловый сервер с smb-протоколом и на git, т.к. весят достаточно мало.

Немного примеров и хавту

Дженкинс по дефолту идет как локальная служба – нужно сделать его админом, иначе он не сможет вылезать в сетку

Для того, чтобы собрать проект через консоль, нужно использовать фичу ide, которая называется HeadlessBuild. Т.к. CubeIDE основана на Eclipse, то и команды очень похожие. Сама команда есть в примере файла build_stm_prj.bat. Как именно запускать stm32cubeide через терминал и какие параметры она принимает – можно узнать, выполнив такую команду:

\stm32cubeidec.exe -nosplash  --launcher.suppressErrors -application org.eclipse.cdt.managedbuilder.core.headlessbuild  -help

Пример команды сборки проекта для Atmel Studio:

if exist "build_log.txt" del "build_log.txt"
del /q AVR_PROJ\Release\

"E:\Program Files (x86)\Atmel\Atmel Studio 6.2\atmelstudio.exe" AVR_PROJ.atsln /rebuild Release /out build_log.txt
type build_log.txt

Пример содержания Jenkinsfile. Глобальные переменные (${FILE_SERVER_ADDR} ${CPP_CHECK} ${BAT_TOOLS}) задаются в настройках системы у Jenkins

def RESULT_FILE_NAME
def TAGS_VERSION

pipeline{

    agent any

    environment{
        PROJ_NAME = "test_prj"
        PROJ_DIR = "test_prj"
        PROJ_ARTIFACTS_TYPE = "srec"
        PROJ_SOURCES = "${PROJ_DIR}\\Core\\Src"
        PROJ_ARTIFACTS_DIR = "${PROJ_DIR}\\Release"
        PROJ_ARTIFACTS_TYPE = "srec"
        SERVER_ARTIFACTS_DIR = "${FILE_SERVER_ADDR}\\FW_Folder"
        GITLAB_ARTIFACTS_DIR = "test_prj1"
        GITLAB_TARGET_NAME = "test_fw.${PROJ_ARTIFACTS_TYPE}"
    }

    stages{
        stage('Check CPP'){
            steps{
                bat "${CPP_CHECK} ${PROJ_SOURCES} 2> cppcheck-result.xml"
                publishCppcheck pattern:'cppcheck-result.xml'
            }
        }
        stage('Unit tests'){
            steps{
                updateGitlabCommitStatus name: 'test', state: 'pending'
                dir("${PROJ_DIR}"){
                    bat "${CEEDLING}"
                }
                updateGitlabCommitStatus name: 'test', state: 'success'
            }
        }
        stage('Building'){
            steps{
                updateGitlabCommitStatus name: 'build', state: 'pending'
                bat "${BAT_TOOLS}\\build_stm_prj.bat ${WORKSPACE} ${PROJ_NAME}"
                updateGitlabCommitStatus name: 'build', state: 'success'
            }
        }
        stage('Publish Artifact'){
            steps{
                updateGitlabCommitStatus name: 'Artifact publication', state: 'pending'
                script {
                    TAGS_VERSION = getCommandOutput("${BAT_TOOLS}\\get_tag.bat ${WORKSPACE}")
                    echo "Tags is ${TAGS_VERSION}"
                }
                bat "${BAT_TOOLS}\\publish_file_to_gitlab.bat ${WORKSPACE} ${PROJ_ARTIFACTS_DIR} ${PROJ_NAME} ${GITLAB_ARTIFACTS_DIR} ${GITLAB_TARGET_NAME} \"${GITLAB_ARTIFACTS_DIR} project updated to version ${TAGS_VERSION}\""
                bat "${BAT_TOOLS}\\publish_file_to_server.bat ${WORKSPACE} ${PROJ_ARTIFACTS_DIR} ${PROJ_NAME} ${SERVER_ARTIFACTS_DIR}"
                updateGitlabCommitStatus name: 'Artifact publication', state: 'success'
            }
        }
    }

    post{
        
        success{
            echo "Build succes"
        }
        unsuccessful{
            updateGitlabCommitStatus name: 'build', state: 'failed'
            updateGitlabCommitStatus name: 'Artifact publication', state: 'failed'
            echo "ouhh..."
        }
    }
}

def getCommandOutput(cmd) {
    if (isUnix()){
         return sh(returnStdout:true , script: '#!/bin/sh -e\n' + cmd).trim()
     } else{
       stdout = bat(returnStdout:true , script: cmd).trim()
       result = stdout.readLines().drop(1).join(" ")       
       return result
    } 
}

Содержание файла get_tag.bat:

@echo off
set work_dir=%1
cd /d %work_dir%
setlocal enableextensions
for /F "tokens=1-2 delims=." %%I in (' git describe --tags ^| grep -Eo "[0-9]+\.[0-9]+"') do (
	set tag_main=%%I
	set tag_minor=%%J
	)
echo %tag_main% %tag_minor%

Содержание файла build_stm_prj.bat:

@echo off
set proj_dir=%1
set project=%2

echo %proj_dir%
echo %project%

cd /d "%proj_dir%"
if exist "C:\STM32CubeIDE_headlessBuilds" rd /q /s "C:\STM32CubeIDE_headlessBuilds"
C:\ST\STM32CubeIDE_1.3.0\STM32CubeIDE\stm32cubeidec.exe --launcher.suppressErrors -nosplash -application org.eclipse.cdt.managedbuilder.core.headlessbuild -data "C:\STM32CubeIDE_headlessBuilds" -import %project% -cleanBuild all

Содержание файла publish_file_to_gitlab.bat:

SetLocal EnableExtensions

set work_dir=%1
set firm_file_dir=%2
set firm_file_name=%3
set project_git_name=%4
set target_firm_file_name=%5
set commit_str=%6

cd /d %work_dir%
cd /d %firm_file_dir%

::forced delete folder 
rd /q /s gitlab_fw_dir 2>nul

git clone [email protected]:FW/gitlab_fw_dir.git

::create project folder if not exist
if not exist "gitlab_fw_dir\%project_git_name%\" mkdir "gitlab_fw_dir\%project_git_name%"


::delete prev fw if exist
if exist "gitlab_fw_dir\%project_git_name%\%target_firm_file_name%" del "gitlab_fw_dir\%project_git_name%\%target_firm_file_name%"

xcopy %firm_file_name% gitlab_fw_dir\%project_git_name%\%target_firm_file_name%* >NUL

cd /d gitlab_fw_dir

git add -A
git commit -a -m %commit_str% >nul
git push >nul

cd /d ..
::forced delete folder 
rd /q /s gitlab_dir 2>nul

exit 0

Содержание файла publish_file_to_server.bat (добавлен человекочитаемый вывод лога git на сервер в виде файла Changelog.txt):

SetLocal EnableExtensions
chcp 65001

set work_dir=%1
set firm_file_dir=%2
set firm_file_name=%3
set dest_folder=%4

cd /d %work_dir%
cd /d %firm_file_dir%

net.exe use %dest_folder% /persistent:no >NUL
if exist %dest_folder%\%firm_file_name% (
		echo FILE ALREADY EXIST
		net.exe use %dest_folder% /delete >NUL
		exit /b 1
	) else (
		xcopy /s /i /c %firm_file_name% %dest_folder%\%firm_file_name%* >NUL
		if exist %dest_folder%\Changelog.txt (
			del %dest_folder%\Changelog.txt
		)
		cd /d %work_dir%
		git log --pretty=format:"%%ah, %%an внес следующие изменения:%%n%%B %%n%%n***********************************%%n" > %dest_folder%\Changelog.txt
	)

if exist "%firm_file_name%" del "%firm_file_name%"
net.exe use %dest_folder% /delete >NUL

Comment

programel