使用 GitHub Actions 为 R 包配置持续集成服务
2022年4月11日
编程
2020年的时候我写过一篇《[使用 GitLab Runner 为 R 包配置持续集成服务](https://hpdell.github.io/%E7%BC%96%E7%A8%8B/RPackage-GitlabRunner/)》的博客,主要介绍了在 GitLab 平台上,通过部署 Docker 容器并在中期内安装依赖环境的方法进行R包持续集成服务的配置。随着对 Docker 理解的加深,以及进阶用法的掌握,之前文章中所写的方法已经比较过时了。而且现在 GitHub Actions 已经十分强大,也支持私有库,也支持自己部署 Runners,并提供 macOS 和 Windows 等环境,因此完全可以将 R 包的持续集成方法迁移到 GitHub 上。本文就介绍一下如何使用 GitHub Actions 进行 R 包的持续集成。 # GitHub Actions 配置简介 GitHub Actions 中,也是通过任务(jobs)来指定流水线(workflow)工作的方式。详细说明可以参考[官方文档](https://docs.github.com/en/enterprise-cloud@latest/actions/quickstart)。简单地讲,一个 GitHub Actions 文件(yml 格式)是一个流水线,一个流水线上有很多任务,每个任务有很多步骤,可以运行在不同的操作系统上。因此,一个流水线配置文件一般是这样的: ```yml name: GitHub Actions Demo on: [push] jobs: Explore-GitHub-Actions: runs-on: ubuntu-latest steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - name: Check out repository code uses: actions/checkout@v3 - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner." - name: List files in the repository run: | ls ${{ github.workspace }} - run: echo "🍏 This job's status is ${{ job.status }}." ``` 这个文件中,`Explore-GitHub-Actions` 就是名为 `GitHub Actions Demo` 的流水线上的一个任务。这个任务的配置中,通过 `runs-on` 指定任务运行的主机系统,可以选择[由 GitHub 提供的虚拟环境](https://github.com/actions/virtual-environments),也可以在自己的主机上部署一个 Runner,具体的方法可以参考[相关文档](https://docs.github.com/en/enterprise-cloud@latest/actions/hosting-your-own-runners/about-self-hosted-runners)。 > 任务的配置中也可以通过 `container` 选项指定环境,此时这个任务就运行在容器中,而不是主机上。此处我们无需使用这种方式。 每一个任务都由很多步骤(steps)组成,每一个步骤中可以是通过 `run` 选项指定的一系列脚本,也可以通过 `uses` 选项指定一个已经发布的 Action ,或者使用 `uses` 指定一个 Docker 镜像。 * 如果使用 `run` 指定脚本,那么可以后面直接跟一行脚本内容,也可以使用 `|` 引导多行脚本内容。 * 如果使用 `uses` 指定 Action ,格式为 `所有者/仓库名@提交或标签` ,例如 `actions/checkout@v3` 就是官方提供的用于检出仓库的 Action。这里也可以自定义 Action 把一组脚本封装起来,以实现一定的功能。自定义的 Action 既可以由一些命令组成,也可以由 Dockerfile 和相应的脚本文件组成。如果是后者,在运行任务之前会先执行镜像构建。 * 如果使用 `uses` 指定 Docker 镜像,格式为 `docker://镜像标签` 。此时,`runs-on` 必须指定 Linux 环境。镜像可以发布在 Docker Hub 中,也可以发布在 GitHub Packages Container registry 中。 下面我们详细介绍以下各种持续集成的使用方式。 # 基于 actions 的持续集成 这种方式适用于任何系统。在 GitHub Runners 提供的 [macOS](https://github.com/actions/virtual-environments/blob/main/images/macos/macos-11-Readme.md) 和 [Windows](https://github.com/actions/virtual-environments/blob/main/images/win/Windows2022-Readme.md) 环境上,我们可以借助以下几个 action 编写测试: - [r-lib/actions/setup-r](https://github.com/r-lib/actions) 部署 R 环境 - [r-lib/actions/setup-r-dependicies](https://github.com/r-lib/actions/tree/v2-branch/setup-r-dependencies) 安装 R 依赖库(Linux 需要提前安装第三方依赖库) - [r-lib/actions/check-r-package](https://github.com/r-lib/actions/tree/v2-branch/check-r-package) 进行 R 包编译和测试 以 macOS 为例,我们可以编写这样一个任务 ```yml jobs: build_macos: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Set up R release uses: r-lib/actions/setup-r@v2 with: r-version: 'release' - name: Install R dependencies uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: any::rcmdcheck, any::roxygen2 needs: check - name: R build and check uses: r-lib/actions/check-r-package@v2 with: args: 'c("--no-manual", "--as-cran")' build_args: 'c("--no-manual", "--resave-data")' error-on: '"error"' check-dir: '"check"' ``` 这种方法的优势是比较方便,泛用性强,即插即用,而且可以复用。但是在 Linux 上,在安装 R 依赖包的时候,由于需要编译安装,需要耗费较长的时间。如果要缩短时间,一种方法是可以使用 cache 功能把依赖包缓存起来。但这样需要耗费较大的空间(GitHub 缓存空间大小有一定限制)。另一种方法是下面介绍的使用 Docker 容器进行持续集成。 # 基于 Docker 的持续集成 由于 macOS 和 Windows 的限制,这种方法基本上只能用于 Linux 系统。目前 CRAN 主要检查 Debian 和 Fedora 两个系统,因此只要创建这两个系统送的镜像即可。这里以 Debian 为例。 ## 构建编译环境 在之前的文章中,我们是通过直接部署一个 Debian 的容器,然后在容器控制台中进行环境部署。事实上,这个过程可以通过编写 Dockerfile 完成。在这个 Dockerfile 文件中,我们通过脚本进行 R 的安装与依赖库的安装。 下面是一个示例的 Dockerfile。 ```dockerfile FROM debian:10 ARG DEBIAN_FRONTEND=noninteractive # 设定当前终端为非交互模式,避免 apt 进入交互 ENV TZ=Etc/UTC # 设定当前时区选项,避免在安装相关库时出现时区选择情况 RUN apt-get -qq update && \ apt-get -qq install -y \ gnupg \ build-essential \ gfortran \ devscripts ### 以下两行用于添加 R 镜像源 RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-key '95C0FAF38DB3CCAD0C080A7BDC78B2DDEABC47B7' RUN echo "deb http://cloud.r-project.org/bin/linux/debian buster-cran40/" >> /etc/apt/sources.list ### 安装 R 以及其他依赖库 RUN apt-get -qq update && \ apt-get -qq install -y \ r-base r-base-dev \ libgsl-dev libgit2-dev \ libcurl4-openssl-dev \ libxml2-dev \ libssl-dev ### 安装依赖的 R 包 RUN Rscript -e "install.packages(c('devtools', 'Rcpp', 'RcppArmadillo'))" COPY entrypoint.sh /entrypoint.sh ENTRYPOINT [ "/entrypoint.sh" ] ``` 如果要部署 Fedora 的环境,无非就是使用 dnf 包管理器,同时依赖库的安装会发生一些变化。 需要注意的是,这个镜像我们没有指定 `WORKDIR` ,这是因为 GitHub Actions 在运行容器的时候会指定工作目录,且指定为仓库所在的目录。所以这里无需指定工作目录。 ## 执行测试 假设我们构建的镜像标签为 `myrpackage-test-debian-release:latest` ,我们可以使用两种方式执行测试。 ### 使用 r-libs/action/check-r-package 对于一般的R包,可以使用现成的 [r-libs/action/check-r-package](https://github.com/r-lib/actions/tree/v2-branch/check-r-package) 来执行测试脚本。如果我们使用自定义的容器进行测试,那么就需要让这个 action 运行在我们的容器中。可以使用 `jobs.<job-id>.container` 选项来实现这一点。例如我们可以编写下面这行的 job。 ```yml job: check-linux: runs-on: ubuntu-latest container: image: myrpackage-test-debian-release:latest steps: - name: Checkout uses: actions/checkout@v3 - name: Build and Check package uses: r-lib/actions/check-r-package@v2 with: args: 'c("--no-manual", "--as-cran")' build_args: 'c("--no-manual", "--resave-data")' error-on: '"error"' check-dir: '"check"' ``` ### 使用自定义测试脚本 如果在执行 R 包检查前有一些额外的步骤,那么我们可以将这些步骤连同执行 R 包检查的部分写在 `entrypoint.sh` 的脚本中,在容器创建完成后运行脚本进行检查。下面是一个具体的示例。 ```shell #!/bin/sh -l mkdir build cd build cmake .. -DWITH_R=ON && \ cmake --build . --config Release --target r_package && \ ctest -R Test_R_Package --output-on-failure exit $? ``` 由于这是一个使用 CMake 进行管理的 R 包,所以全都是用 CMake 进行处理的。对于一般的 R 包,就可以写如下脚本 ```shell #!/bin/sh -l R CMD build PCAKGE && R CMD check PACKAGE_VERSION.tar.gz --as-cran exit $? ``` 只要能够保证工作目录是正确的,并且命令可以运行即可。 由于我们已经通过 Dockerfile 构建好了镜像,在配置流水线任务时,只需要采用 `uses` 选项指定 Docker 镜像即可。当然,首先还是要使用 `actions/checkout@v3` 检出仓库。因此这个任务的编写如下: ```yml jobs: build_debian: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - uses: docker://myrpackage-test-debian-release:latest ``` 这样,我们就创建了一个由两个步骤组成的任务,第一个步骤检出仓库,第二个步骤构建 Docker 容器并执行相应的操作。由于我们已经在 `entrypoint.sh` 中编写好了操作,容器运行起来后就会执行脚本,以实现持续集成的目的。 # 基于自定义 Runner 的持续集成 事实上,虽然 [r-lib/actions/setup-r](https://github.com/r-lib/actions) 可以在 Windows 上安装 R 环境,但是如果 R 包使用了一些其他库(例如 gsl 等),环境的部署就会非常麻烦了。即使 GitHub Runner 的 Windows 环境安装了 vcpkg 和 conda 等包管理器,但是编译 R 包需要将依赖库安装在 R 的根目录上,使用脚本依然比较困难。但是由于 GitHub 支持自定义 Runner,我们可以在一台 Windows 主机或者虚拟机上部署自定义 Runner 并执行任务。 > 这里说“需要将依赖库安装在 R 的根目录上”,主要是指例如 GSL 等第三方依赖库,需要将这些依赖库安装在 R_HOME 路径下。这在 Windodws 系统上是比较难以做到的,并不是说不行。主要有两种方式: > > - 由于 Windows Server 2022 自带了 zip tar gzip 等解压缩工具,可以下载预编译的二进制库并放到 R_HOME 中。 > - 在 RTools 4.2 版本中已经由官方提供了 GSL 等库,所以可以使用 RTools 4.2。 > > 但如果以上条件并不满足,还是使用自定义的 Runner 进行持续集成比较好。 部署 Runner 的方法很简单,进入仓库的设置界面,选择 Actions > Runner ,点击 New self-hosted runner 按钮,就会出现非常详细的说明。 ![image.png](/media/d1ffff96-7840-47cb-a90f-a9c0e2068757_image.png) 部署好后,按照以下方法配置任务 ```yml jobs: build_windows: runs-on: [self-hosted, Windows, X64] steps: - uses: actions/checkout@v3 - name: R build and check run: | R CMD build . R CMD check 包名_版本.tar.gz ``` 其中 `runs-on` 中列出的是自定义 Runner 的标签,通过这些标签 GitHub 会选出相应的 Runner 去执行任务。这些标签会在部署时由部署脚本提示输入。 # 流水线触发条件 通常我们可以使用如下触发条件,以支持在 master 分支更新或者有 Pull Request 时进行自动检查。 ```yml on: push: branches: [ master ] tags: [ 'v*.*.*' ] pull_request: branches: [ master ] ``` 这样的话,就最好遵循 Git 或 GitHub 推荐的工作流进行开发。 如果希望通过流水线自动将 R 包发布在 CRAN 上,可以编写相应的任务,并在任务配置中添加一个条件 ```yml jobs: upload_cran: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: ... ``` 这样就只有在打 Tag 的时候才会进行上传。 # 使用 Matrix 在多种环境下执行持续集成 事实上,CRAN 不仅要求 R 包在 Debian Fedora Windows macOS 系统上测试,还要分别测试 release devel old-release 三个 R 版本,甚至还要求分别测试 gcc 和 clang 两种编译器。但是不同版本的 R 进行测试的脚本是一样,只是环境不一样而已。所以我们可以使用 Matrix 让我们的测试运行在不同的环境下。 以 Linux 上测试 release 和 devel 为例,我们的测试需要以下两个维度: - OS: Debian, Fedora - R Version: release, devel 那么我们可以这样编写 job ```yml job: check-linux: runs-on: ubuntu-latest strategy: matrix: os: [debian, fedora] version: [release, devel] fail-fast: false container: image: myrpackage-test-${{ matrix.os }}-${{ matrix.version }}:latest steps: - name: Checkout uses: actions/checkout@v3 - name: Build and Check package uses: r-lib/actions/check-r-package@v2 with: args: 'c("--no-manual", "--as-cran")' build_args: 'c("--no-manual", "--resave-data")' error-on: '"error"' check-dir: '"check"' ``` 为了执行这个 job ,我们需要提前构建好以下四个镜像 - myrpackage-test-debian-release:latest - myrpackage-test-debian-devel:latest - myrpackage-test-fedora-release:latest - myrpackage-test-fedora-devel:latest 这样 GitHub 会同时根据我们的配置在以上四个容器中进行测试。
感谢您的阅读。本网站 MyZone 对本文保留所有权利。