使用 GitLab CI/CD 为 Linux 创建 RPM 包(一)

大家好!我是大聪明-PLUS!

各位同事,大家好。前段时间,我们团队接到一个任务,开发一个用于与外围设备协同工作的服务,该服务将作为服务在 Linux 操作系统工作站上运行。

到目前为止,我们部门的所有桌面应用程序都是专门为 Windows 开发的。当前的任务对团队来说既是一个挑战,也是一个学习新知识的机会。我的团队对这个项目的准备标准是编写程序本身并将其发布为 RPM 包以供分发。

技术栈:

  • 返回 NET Core 8

  • 操作系统 Alt Linux 工作站 K 10.4。

  • GitLab

  • Docker

问题分解

如果你把问题分解成各个部分,并分别处理,那么解决问题就会容易得多。我列出了以下步骤:

  • 使用.NET Core 8开发一个简单的控制台程序。

  • 手动构建 RPM 包。

  • 设置 CI/CD。

  • 为 GitLab 创建一个工作管道并获取 RPM 包作为输出。

开发一个简单的程序

如果您尝试立即打包一个包含许多项目和依赖项的现成程序,您可能会遇到意想不到的问题,从而分散您对目标的注意力。

在我看来,打包一个简单的程序要容易得多。仔细检查所有步骤,并解决所有与打包相关的问题。当需要打包目标程序时,您可以轻松扩展现有的流程,并且任何错误都只与二进制文件编译问题有关。

说干就干。一个简单的控制台程序就开发出来了------一个计数器,每秒显示一个 +1 的数字。

复制代码
`
Console.WriteLine("Start counting");
await Counter();

Task Counter()
{
    int i = 0;
    while (true)
    {
        Console.WriteLine(i);
        i++;
        Thread.Sleep(1000);
    }
}`

工作成果

因此,如果我们在构建软件包、在全新操作系统上安装并从命令行运行后得到相同的结果,则一切正常。如果这一切都是通过管道自动完成的,那就太棒了。

构建 RPM 包

从现在开始,我将描述具体步骤,假设你已经熟悉 spec 文件及其工作原理的术语和基本原理。

要构建 RPM 包,您需要按顺序执行以下步骤:

  • 安装 rpm-build 实用程序

  • 组装目录的配置。

  • 生成 spec 文件

  • 准备文件并将其移动到所需的目录。

  • 运行命令 rpmbuild -ba <spec 文件名>

安装 rpm-build 实用程序

复制代码
`sudo apt-get update && apt-get install -y rpm-build`

组装目录的配置

这在现阶段很方便,但在流程中并不方便。

复制代码
`rpmdev-setuptree`

**请注意:**绝大多数文章,甚至任何神经网络,都会告诉你 /home/user/.rpmbuild 目录已创建。Alt Linux 的配置似乎有所不同,它创建了 /home/user/RPM 目录。这并不重要,而且我还没有测试如果通过 .rpmbuild 文件夹执行所有操作,它是否能在管道中正常工作。我决定保持现状。

此外,您的主目录根目录下会创建一个 .rpmmacros 文件,您需要手动编辑该文件以删除所有不必要的文本。您可以在上面的链接中阅读更多相关信息。

**结论:**在管道中,我们将手动创建一个目录结构并放置一个预先生成的.rpmmacros文件,该文件将位于项目的根目录。

生成 spec 文件

构建包最重要的一步是创建正确的规范文件。

spec 文件本身在本质上与管道描述文件非常相似。它是一个清单和一组指令,脚本必须执行这些指令才能生成编译后的程序,以及在安装过程中在哪里以及如何编写它。

**请注意:**我因为一个相当愚蠢的错误浪费了一些时间。SPEC 文件最初是在 Windows 中生成的,换行符用 \r\n 标记。我构建的 Linux 系统不接受这个标记,并报出了"无效返回码"的错误。我尝试使用 exit 0 等命令返回正确的返回码,结果浪费了不少时间。我删除了文件中的 \r 后,一切正常。

一个有趣的观察是,如果你在 Git 中提交文件,这些问题就不会发生。它似乎会立即更改它们并发出警告。

Spec 文件示例

在生成 spec 文件时,我假设程序应该位于 /opt/{name} 目录中,并且应该有一个指向 /usr/bin/{name} 中可执行文件的链接,以便可以在不指定路径的情况下运行它。

复制代码
`Name:           utestrpm
Version:        0.0.0 
Release:        1%{?dist}

License:        MIT
Group:			Other
URL:            https://your-domain.com
Source0:        source_file.tar.gz 

BuildArch:      x86_64 
BuildRequires:  dotnet-8.0 



%prep
%setup -q

%build

export DOTNET_NUGET_SIGNATURE_VERIFICATION=false 


dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true

%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/opt/%{name}
mkdir -p %{buildroot}%{_bindir}

install -m 755 %{name}/bin/Release/net8.0/linux-x64/publish/%{name} %{buildroot}/opt/%{name}/%{name}


ln -sf /opt/%{name}/%{name} %{buildroot}%{_bindir}/%{name}

%post

%preun

%files

/opt/%{name}/%{name}

%{_bindir}/%{name}


# /opt/%{name}/config/appsettings.json

%changelog

`

这份清单简洁明了,每一行的功能都一目了然。我想强调几点。

**%{buildroot}**是文件中最重要的宏。构建软件包时,会创建一个虚拟根系统,该宏指向它。安装软件包时,它将指向根系统的链接。通常情况下,该宏的值为"/"。

尽可能多地使用宏也很重要。例如,Alt Linux 中的 %{_bindir} 是"/usr/bin"。在其他存储库中,它可能只是"/bin"或其他名称。因此,宏越多越好。但是,我找不到指向 /opt 的宏。

在 Linux 上构建 .NET Core 解决方案时,我在更新 NuGet 包时遇到了证书验证问题。网上有很多解决这个问题的技巧。禁用验证对我有帮助。我认为这是可以接受的,也很方便。请记住,我们的目标是从管道运行所有内容,因此复杂的解决方案可能不合适。

复制代码
`export DOTNET_NUGET_SIGNATURE_VERIFICATION=false `

我还想指出构建 .NET 应用程序的一些细微差别。我决定将整个解决方案打包成一个可执行文件。这大大简化了 spec 文件的创建。但这可能会导致以后出现问题,在这种情况下,我们将不得不采用传统的方式构建,复制所有内容,而不是直接复制一个文件。

复制代码
`dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true`

关于 spec 文件,我最后想指出的是,如果你仔细观察,会发现第 48 行(也就是写入配置文件的部分)被注释掉了。这是为了在开发过程中,当我把程序变得更复杂,以便调试它在 Linux 下运行的所有细微差别时,留出一些余地。

准备文件

重要!请注意:我认为最重要的细节是从头开始阅读文章!如果你还没读过,也请从头开始。否则,你可能会看不清楚。

因此,我们有一个必需的 Version 字段和一个 Source 字段。在示例中,Source 通常这样描述:

复制代码
`Source: %name-%version.tar`

这可以很容易地改变,然后在管道中完成,为静态名称,这在管道阶段之间形成工件时更加方便。

但是!这里有个陷阱!源文件可以任意命名,但它的结构严格依赖于 %name 和 %version 宏。

rpmbuild 脚本解压存档并运行命令

复制代码
`cd ~/RPM/BUILD/%name-%version`

当然,从 spec 文件中替换所需的值。如果找不到此目录,脚本将因错误而终止。

下面是我们的程序需要的 tar.gz 存档的示例,其名称为 utestrpm,版本为 1.0.2,例如:

因此,仅仅将程序打包到存档中是不够的。您需要创建一个名为 %name-%version 的目录。将源文件放入其中,然后将该目录打包到存档中。

我几乎可以肯定这种行为是可以定制的。但何必呢?把这个功能纳入到流程中要容易得多。

召集团队

这是最简单的部分。我们调用命令并等待它成功完成。

复制代码
`rpmbuild -ba utestrpm.spec`

**请注意:**需要注意的是,默认情况下,此命令无法从特权用户(root 或 sudo)调用。我在网上找到了一篇文章,解释了为什么这是正确的。我没有读过这篇文章,所以就照搬了。

**结论:**您必须在管道中创建一个非特权用户并在其下执行命令。

设置 CI/CD

原则上,如果 GitLab 和运行器已经设置好了,可以跳过这一步。但是,为了让一切按我想要的方式运行,我需要 80 次提交。而且,在我完成所有实验并准备好将所有工作应用到目标项目之前,还需要大约 80 次提交。我不想让我的主仓库被这些垃圾弄得乱七八糟。

我决定在我的办公电脑上使用 Docker 设置 GitLab 和一个 Runner。

安装 GitLab

复制代码
`docker run --detach \
  --hostname 172.17.0.2 \
  --publish 443:443 --publish 80:80 --publish 22:22 \
  --name gitlab \
  --restart always \
  gitlab/gitlab-ce:latest`

注意主机名。您可能需要使用其他值。我将在下面解释具体值以及原因。

安装 GitLab 后(启动需要几分钟)

登录名为 root。可以通过运行以下命令找到密码

复制代码
` docker exec -it gitlab cat /etc/gitlab/initial_root_password`

**请注意:**快速执行命令很重要,因为文件会在短时间后被删除。

让我们安装 GitLab。创建一个组和一个项目。它看起来应该像这样。

在 GitLab 中创建 Runner

需要注意的是,这可能会造成混淆。在 GitLab 中,您需要创建一个名为 Runner 的实体。该实体将拥有所有权(Runner 可以是本地的、组的等等)。它还包含一些设置------何时运行、在哪些项目上运行等等。

在本文中,我创建了一个单组运行器。要创建它,请转到组部分。在我的示例中,它被称为 lnx,然后选择"Build/Runners"。

你会看到跑步者。我有一个,这就够了。还有一个按钮可以创建新的runner。

让我们创建一个可以在所有提交上运行的新运行器。

注意,点击"创建跑步者"后,它会将我重定向到哪个地址。

这个 IP 的出现是因为我分配了那个主机名。只需在浏览器中将其替换为 localhost,我们就能找到所需的内容。这只是暂时的不便,理想情况下我们只需要忍受一次,但它会为我们节省很多后续的精力。我很快会解释原因。

**请注意:**请务必复制标有感叹号的文本。您将无法再次访问它。它是最终配置所必需的。

如果我们回到runner名单页面,我们会看到一位注册跑步者的状态为"从未联系过"

创建并注册 gitlab-runner 容器

正如我之前所写,运行器既是 GitLab 中的一个实体,也是安装在特定工作站上的特殊程序。该程序将执行管道命令。要确定 GitLab 将调用哪个工作站,您需要将 GitLab 运行器与该工作站关联起来。

虚拟机通常是创建运行器的理想选择。您可以创建多个虚拟机,并为其分配所需的资源等等。要运行管道,只需一个安装了运行器程序的 Docker 镜像即可。要安装它,请运行以下命令。

复制代码
`docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest`

因此,我们有两个 docker 镜像

最后一步是注册参赛者。你需要给参赛队伍打电话。

复制代码
`docker exec -it gitlab-runner /bin/bash`

我们将连接到 Docker 容器控制台。将复制的文本粘贴到控制台中。每次操作的令牌都会有所不同。

复制代码
`gitlab-runner register  --url http://172.17.0.2  --token glrt-570XNtiakleL5Dn_tzWUrmc6MwpvOjEKdDoyCnU6MQ8.01.171afrob3`

接下来,系统会要求我们指定地址------我们保留默认地址。为运行器命名。指定运行器的启动方式------输入 docker。并指定默认镜像。我使用了 alt:p10。

GItlab 中的运行器应该会变成绿色,并显示"在线"状态。好啦,我们快完成了。现在只剩下编写管道了。

为什么 172.17.0.2 这个地址受到如此多的关注?

在继续之前,我们有必要解释一下主机名 172.17.0.2 的由来。我将介绍一种快速简便(尽管略显不妥)的方法来配置运行器,使其正常工作,而无需任何不必要的工作。这仅适用于测试假设和作为临时措施。仅此而已。

问题是,Docker 容器通过它们自己的网络相互通信,而这个网络由 Docker 管理。你可以在 Docker 中添加自己的网络,分配一系列 IP 地址,并将每个镜像与该网络内的特定 IP 地址关联。这没问题。但我们正在构建一个 MVP 解决方案,它只服务于一个目的。在我看来,花时间完美配置 Docker 是没有必要的。

解决方案是查看默认网络,找出分配给 GitLab 容器的 IP 地址。让我们运行以下命令:

复制代码
`docker network ls`

就我而言,我得到了以下结果

请注意名为"bridge"的网络。除非您删除它,否则它始终存在。您创建的任何容器都将链接到此网络。

要查看哪个 IP 地址与容器关联,可以运行命令

复制代码
`docker network inspect bridge`

这是这个 IP 地址。

**请注意:**由于我们没有为 gitlab 容器分配此 IP 地址,重启计算机后,gitlab-runner 容器很可能会先启动,然后它会占用此 IP 地址。这个问题可以轻松解决,只需停止所有容器,然后先启动 gitlab,再启动 gitlab-runner 即可。

文字太多了,仍然不明白为什么这如此重要。

这一点很重要,因为 gitlab-runner 容器内部也包含 Docker。我们的流水线将在内部 Docker 中运行,下载 alt:p10 镜像,并在每一步上传源文件,等等。

这只是 Docker in Docker 方法的一个经典示例。

通过指定主机名,我们配置了 GitLab,以便运行器能够通过 IP 地址而不是域名访问它。这样就可以正常工作了。例如http://172.17.0.2/groups/lnx/utestrpm。

如果我们的 GitLab 配置正确,并分配了一个可在您的网络上访问的有效域名,那么就不会有问题。我们可以使用正确的主机名成功访问它。

在我们的例子中,在本地机器上,我们会遇到一些麻烦。我们必须在 hosts 文件中指定主机名,重启,还要确保为容器分配正确的 IP 地址,或者在 Docker 中创建自定义网络等等。在我看来,将 IP 地址指定为主机名更简单,而且一切正常。

创建管道

你已经了解了什么是管道以及它的用途。如果你不知道,任何神经网络都会很乐意告诉你。我们将提供一个 GitLab 管道示例,它将构建一个 RPM 包,并涵盖其中的关键点。

复制代码

准备档案(prepare_tar)

此阶段旨在生成成功执行 rpmbuild 脚本所需的工件。

第一步是确定应用程序版本。在实际开发中,版本增量非常重要。版本值放在项目中包含的 env.json 文件中。

复制代码
`{
  "service": "1.0.2"
}
`

安装了 jq 实用程序并用于查找版本。该版本被放置在 $VERSION 变量中。为了方便起见,还确定了未来存档的根文件夹名称。

复制代码
`
    - VERSION=$(jq -r '.service' utestrpm/env.json)
    - echo "VERSION=$VERSION" > version.env
    - FOLDER="utestrpm-$VERSION"`

下一步是生成归档文件。tar 命令非常复杂且用途广泛,因此我将在评论中解释其工作原理。

复制代码
`
    - tar -czf source_file.tar.gz --transform "s,^,$FOLDER/," utestrpm.sln utestrpm/`

下一步是生成与当前版本匹配的 spec 文件。最简单的方法是直接替换基础模板文件中的版本,并相应地重命名。

复制代码
`
    - sed "s/^Version:.*/Version:$VERSION/" utestrpm_template.spec > utestrpm.spec`

我们需要发送到下一步的只是正确的存档和正确的 spec 文件,以及版本信息。.rpmmacros 文件已存在于项目中,永远不会被更改。我们会在构建过程中将其复制到正确的位置。

发布 RPM(build_rpm)

最后的润色。首先,让我们安装所需的依赖项。这些包括 rpm-build 包本身、dotnet-sdk 环境和 su 实用程序,我们需要它来防止脚本崩溃(您无法以 root 身份运行它)。

下一步是创建一个用户、他们的主文件夹以及其中的正确目录结构。

复制代码
`
    - useradd -m -s /bin/bash $RPM_USER
    - mkdir -p $RPM_DIR/{SOURCES,SPECS,RPMS,SRPMS}`

需要复制这些文件并更改所有者。目前是 root 用户,但应该是我们的服务用户。

TODO:路径是硬编码的 - 需要修复:)

复制代码
`
    - cp source_file.tar.gz $RPM_DIR/SOURCES/
    - cp utestrpm.spec $RPM_DIR/SPECS/
    - cp .rpmmacros /home/$RPM_USER
    

    - chown -R pipe_builder:pipe_builder /home/pipe_builder/
    `

好吧,最好的部分是 - 我们运行脚本

复制代码
`
    - su - $RPM_USER -c "cd $RPM_DIR/SPECS && rpmbuild -ba utestrpm.spec"
    
 
    - cp $RPM_DIR/RPMS/x86_64/*.rpm ./
    - ls -al *.rpm`

**请注意:**直接运行 su 会失败,因为我们在当前线程中仍然是 root 权限。您可以运行 whoami 命令来验证这一点。

-c 选项可以解决问题,立即在我们需要的用户下执行命令。

结果

我不知道你的情况,但我一切正常。RPM 包已创建,版本号也正是我需要的。包可以下载了。在实际项目中,我会添加测试、检查/复检、发布并部署到沙盒,以及将包保存到 Nexus,但那是另一回事。

研究阶段尚未完成。然而,研究结果对本文来说已经足够了。在将这些研究成果转化为实际项目之前,我们需要完成以下步骤:

  • 将程序设为服务,配置自动启动(systemctl enable),编辑 spec 文件以便在包安装期间进行配置。

  • 正确输入日志,以便可以通过 journalctl 读取

  • 添加配置文件,例如 appsettings.json。确保它们已安装并应用。

我可能会写另一篇文章,但肯定会更短。

感谢您的关注。希望本文能对大家有所帮助。

相关推荐
女神下凡8 分钟前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
蓝天居士1 小时前
EtherCAT从入门到精通(10)
机器人·嵌入式·ethercat
且从容.4 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道4 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
不脱发的程序猿5 小时前
GD32串口数据收进内存之后DMA如何处理?
单片机·嵌入式硬件·dma·嵌入式·串口通信·gd32
且从容.7 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落9 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨9 小时前
WSL2的安装与使用
linux·wsl
微小的鱼9 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng9 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器