使用 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。确保它们已安装并应用。

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

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

相关推荐
边疆.7 小时前
【Linux】自动化构建工具make和Makefile和第一个系统程序—进度条
linux·运维·服务器·makefile·make
2021黑白灰8 小时前
windows11 vscode ssh远程linux服务器/虚拟机 免密登录
linux·服务器·ssh
z202305088 小时前
linux之PCIE 设备枚举流程分析
linux·运维·服务器
simple_whu8 小时前
编译tiff:arm64-linux-static报错 Could NOT find CMath (missing: CMath_pow)
linux·运维·c++
SundayBear8 小时前
Linux驱动开发指南
linux·驱动开发·嵌入式
大聪明-PLUS8 小时前
使用 GitLab CI/CD 为 Linux 构建 RPM 包(二)
linux·嵌入式·arm·smarc
yugi9878388 小时前
C语言多进程创建和回收
linux·c语言·算法
鸠摩智首席音效师8 小时前
如何在 Bash 命令中执行命令 (嵌套命令) ?
linux·bash
Bella的成长园地9 小时前
Linux 中sudo bash -i 和 su root 有什么区别?
linux·运维·bash