在 GitHub Actions 工作流程中缓存 APT 包

Posted

技术标签:

【中文标题】在 GitHub Actions 工作流程中缓存 APT 包【英文标题】:Caching APT packages in GitHub Actions workflow 【发布时间】:2020-04-03 19:04:03 【问题描述】:

我将以下 Github Actions 工作流用于我的 C 项目。工作流在大约 40 秒内完成,但其中一半以上的时间用于安装 valgrind 包及其依赖项。

我相信缓存可以帮助我加快工作流程。我不介意多等几秒钟,但这似乎是对 GitHub 资源的毫无意义的浪费。

name: C Workflow

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1

    - name: make
      run: make

    - name: valgrind
      run: |
        sudo apt-get install -y valgrind
        valgrind -v --leak-check=full --show-leak-kinds=all ./bin

运行sudo apt-get install -y valgrind 会安装以下软件包:

gdb gdbserver libbabeltrace1 libc6-dbg libipt1 valgrind

我知道 Actions 支持缓存特定目录(并且已经有几个关于此的 SO 问题和文章已得到解答),但我不确定 apt 安装的所有不同软件包的最终位置。我假设 /bin//usr/bin/ 不是受安装包影响的唯一目录。

是否有一种优雅的方式来缓存已安装的系统包以供未来工作流运行?

【问题讨论】:

【参考方案1】:

此答案的目的是展示如何使用 github 操作完成缓存。不一定要显示如何缓存valgrind,它确实显示了,但更要表明并非所有内容都可以/应该被缓存,并且需要考虑缓存和恢复缓存与重新安装依赖项的权衡帐户。


您将使用actions/cache 操作来执行此操作。

将其添加为一个步骤(在您需要使用 valgrind 之前):

- name: Cache valgrind
  uses: actions/cache@v2
  id: cache-valgrind
  with:
      path: "~/valgrind"
      key: $secrets.VALGRIND_VERSION

下一步应该尝试安装缓存版本(如果有)或从存储库安装:

- name: Install valgrind
  env:
    CACHE_HIT: $steps.cache-valgrind.outputs.cache-hit
    VALGRIND_VERSION: $secrets.VALGRIND_VERSION
  run: |
      if [[ "$CACHE_HIT" == 'true' ]]; then
        sudo cp --verbose --force --recursive ~/valgrind/* /
      else
        sudo apt-get install --yes valgrind="$VALGRIND_VERSION"
        mkdir -p ~/valgrind
        sudo dpkg -L valgrind | while IFS= read -r f; do if test -f $f; then echo $f; fi; done | xargs cp --parents --target-directory ~/valgrind/
      fi

说明

VALGRIND_VERSION秘密设置为:

apt-cache policy valgrind | grep -oP '(?<=Candidate:\s)(.+)'

这将允许您在发布新版本时使缓存失效,只需更改密钥的值即可。

dpkg -L valgrind用于列出使用sudo apt-get install valgrind时安装的所有文件。

我们现在可以使用此命令将所有依赖项复制到我们的缓存文件夹:

dpkg -L valgrind | while IFS= read -r f; do if test -f $f; then echo $f; fi; done | xargs cp --parents --target-directory ~/valgrind/

此外

除了复制valgrind的所有组件外,可能还需要复制依赖项(如本例中的libc),但我不建议继续沿着这条路走,因为依赖链只是从那里增长。准确的说,最终拥有适合valgrind运行的环境需要复制的依赖如下:

libc6 libgcc1 gcc-8-base

要复制所有这些依赖项,您可以使用与上述相同的语法:

for dep in libc6 libgcc1 gcc-8-base; do
    dpkg -L $dep | while IFS= read -r f; do if test -f $f; then echo $f; fi; done | xargs cp --parents --target-directory ~/valgrind/
done

当安装valgrind 所需要的只是运行sudo apt-get install valgrind 时,所有这些工作真的值得吗?如果您的目标是加快构建过程,那么您还必须考虑恢复(下载和提取)缓存所花费的时间,而不是简单地再次运行命令来安装valgrind。 em>


最后要恢复缓存,假设存储在/tmp/valgrind,可以使用命令:

cp --force --recursive /tmp/valgrind/* /

这基本上会将所有文件从缓存复制到根分区。

除了上面的过程,我还有一个example的“缓存valgrind”,通过从源代码安装和编译。缓存现在大小约为 63MB(压缩后),仍然需要单独安装 libc,这与目的不符。


注意:这个问题的另一个answer 提出了我认为更安全的缓存依赖项的方法,方法是使用预先安装了依赖项的容器。最好的部分是您可以使用操作来使这些容器保持最新状态。

参考资料:

https://askubuntu.com/a/408785 https://unix.stackexchange.com/questions/83593/copy-specific-file-type-keeping-the-folder-structure

【讨论】:

哦,我明白了,这很巧妙。我不知道您可以安全地获取所有已安装的文件并将它们移动到另一个目录而不会破坏某些内容。我不确定它是否有效。我运行了 3 次工作流程,总是去Cache not found for input keys: ***.。我在“设置”>“秘密”中添加了VALGRIND_VERSION 秘密,对吗? 我现在设法获得缓存命中,但我从 valgrind 收到以下错误:--2906-- Reading syms from /lib/x86_64-linux-gnu/ld-2.27.so --2906-- Considering /lib/x86_64-linux-gnu/ld-2.27.so .. --2906-- .. CRC mismatch (computed 1b7c895e wanted 2943108a) --2906-- object doesn't have a symbol table @natiiix 有可能缓存 valgrind 导致在检索缓存时不会安装 libc 依赖项。我现在不在显示器附近,但我查看了您的错误,它似乎是带有 valgrind 的bug。您也可以尝试安装 libc 版本 6,看看是否有帮助。今天晚些时候我会更新答案 是的,看起来是这样。如果我添加sudo apt-get install -y libc6-dbg,那么它可以正常工作,但我也是我开始的地方,因为安装该软件包需要多花 30 秒。 @natiiix 似乎缓存 valgrind 的工作量可能比预期的要多,但至少这显示了如何在 ubuntu 上完成缓存。查看 valgrind 的依赖项,至少有 6 个依赖项,我认为如果要这样做,它们可能都需要缓存。【参考方案2】:

您可以创建一个预先安装了 valgrind 的 docker 映像并在其上运行您的工作流程。

创建一个Dockerfile 类似:

FROM ubuntu

RUN apt-get install -y valgrind

构建它并推送到 dockerhub:

docker build -t natiiix/valgrind .
docker push natiiix/valgrind

然后使用以下内容作为您的工作流程:

name: C Workflow

on: [push, pull_request]

jobs:
  build:
    container: natiiix/valgrind

    steps:
    - uses: actions/checkout@v1

    - name: make
      run: make

    - name: valgrind
      run: valgrind -v --leak-check=full --show-leak-kinds=all ./bin

完全未经测试,但你明白了。

【讨论】:

这是一个非常有趣的想法,但它有点破坏了让 GitHub Actions 缓存环境/工件以供将来运行的整个原则,而是需要我付出一些额外的努力。另一方面,一旦完成,这可能很容易重复使用。 由您决定什么最适合您,或者什么最需要您的支持¯_(ツ)_/¯ 投反对票,因为人们倾向于使用虚拟机的全部原因是为了避免 docker inside docker 问题。 我个人认为这是最明智的答案,因为其他显示缓存依赖项的答案手动显示了这是多么令人担忧。 老实说,我比我的回答更喜欢这个想法。您甚至可以创建一个单独的工作流,不断构建容器并部署到 github 本身【参考方案3】:

更新: 我创建了一个 GitHub 操作,作为这个解决方案,更少的代码和更好的优化。 Cache Anything New

此解决方案类似于投票最多的解决方案。我尝试了建议的解决方案,但它对我不起作用,因为我正在安装 texlive-latexpandoc,它们有许多依赖项和子依赖项。

我创建了一个可以帮助很多人的解决方案。一种情况是您安装了几个软件包(apt install),另一种解决方案是您make 一个程序并且需要一段时间。

解决方案:

    具有所有逻辑的步骤,它将缓存。 使用find 创建容器中所有文件的列表。 安装所有软件包或make 程序,无论您想缓存什么。 使用find 创建容器中所有文件的列表。 使用diff 获取新创建的文件。 将这些新文件添加到缓存目录。该目录将自动存储actions/cache@v2。 加载创建的缓存的步骤。 将缓存目录下的所有文件复制到主路径/。 将受益于缓存的步骤以及您需要的其他步骤。

什么时候用这个?

我没有使用缓存,安装包大约需要 2 分钟才能完成所有过程。 使用缓存,第一次创建需要7~10分钟。 使用缓存大约需要 1 分钟才能完成所有过程。 仅当您的主进程花费大量时间时才有用,如果您经常部署它也很方便。

实施:

源码:.github/workflows

我的操作登陆页面:workflows。

release.yml

name: CI - Release books

on:
  release:
    types: [ released ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v2

      - uses: actions/cache@v2
        id: cache-packages
        with:
          path: $ runner.temp /cache-linux
          key: $ runner.os -cache-packages-v2.1

      - name: Install packages
        if: steps.cache-packages.outputs.cache-hit != 'true'
        env:
          SOURCE: $ runner.temp /cache-linux
        run: |
          set +xv
          echo "# --------------------------------------------------------"
          echo "# Action environment variables"
          echo "github.workspace: $ github.workspace "
          echo "runner.workspace: $ runner.workspace "
          echo "runner.os: $ runner.os "
          echo "runner.temp: $ runner.temp "
          echo "# --------------------------------------------------------"
          echo "# Where am I?"
          pwd
          echo "SOURCE: $SOURCE"
          ls -lha /
          sudo du -h -d 1 / 2> /dev/null || true
          echo "# --------------------------------------------------------"
          echo "# APT update"
          sudo apt update
          echo "# --------------------------------------------------------"
          echo "# Set up snapshot"
          mkdir -p "$ runner.temp "/snapshots/
          echo "# --------------------------------------------------------"
          echo "# Install tools"
          sudo rm -f /var/lib/apt/lists/lock
          #sudo apt install -y vim bash-completion
          echo "# --------------------------------------------------------"
          echo "# Take first snapshot"
          sudo find / \
                -type f,l \
                -not \( -path "/sys*" -prune \) \
                -not \( -path "/proc*" -prune \) \
                -not \( -path "/mnt*" -prune \) \
                -not \( -path "/dev*" -prune \) \
                -not \( -path "/run*" -prune \) \
                -not \( -path "/etc/mtab*" -prune \) \
                -not \( -path "/var/cache/apt/archives*" -prune \) \
                -not \( -path "/tmp*" -prune \) \
                -not \( -path "/var/tmp*" -prune \) \
                -not \( -path "/var/backups*" \) \
                -not \( -path "/boot*" -prune \) \
                -not \( -path "/vmlinuz*" -prune \) \
                > "$ runner.temp "/snapshots/snapshot_01.txt 2> /dev/null \
                || true
          echo "# --------------------------------------------------------"
          echo "# Install pandoc and dependencies"
          sudo apt install -y texlive-latex-extra wget
          wget -q https://github.com/jgm/pandoc/releases/download/2.11.2/pandoc-2.11.2-1-amd64.deb
          sudo dpkg -i pandoc-2.11.2-1-amd64.deb
          rm -f pandoc-2.11.2-1-amd64.deb
          echo "# --------------------------------------------------------"
          echo "# Take second snapshot"
          sudo find / \
                -type f,l \
                -not \( -path "/sys*" -prune \) \
                -not \( -path "/proc*" -prune \) \
                -not \( -path "/mnt*" -prune \) \
                -not \( -path "/dev*" -prune \) \
                -not \( -path "/run*" -prune \) \
                -not \( -path "/etc/mtab*" -prune \) \
                -not \( -path "/var/cache/apt/archives*" -prune \) \
                -not \( -path "/tmp*" -prune \) \
                -not \( -path "/var/tmp*" -prune \) \
                -not \( -path "/var/backups*" \) \
                -not \( -path "/boot*" -prune \) \
                -not \( -path "/vmlinuz*" -prune \) \
                > "$ runner.temp "/snapshots/snapshot_02.txt 2> /dev/null \
                || true
          echo "# --------------------------------------------------------"
          echo "# Filter new files"
          diff -C 1 \
              --color=always \
              "$ runner.temp "/snapshots/snapshot_01.txt \
              "$ runner.temp "/snapshots/snapshot_02.txt \
              | grep -E "^\+" \
              | sed -E s/..// \
              > "$ runner.temp "/snapshots/snapshot_new_files.txt
          < "$ runner.temp "/snapshots/snapshot_new_files.txt wc -l
          ls -lha "$ runner.temp "/snapshots/
          echo "# --------------------------------------------------------"
          echo "# Make cache directory"
          rm -fR "$SOURCE"
          mkdir -p "$SOURCE"
          while IFS= read -r LINE
          do
            sudo cp -a --parent "$LINE" "$SOURCE"
          done < "$ runner.temp "/snapshots/snapshot_new_files.txt
          ls -lha "$SOURCE"
          echo ""
          sudo du -sh "$SOURCE" || true
          echo "# --------------------------------------------------------"

      - name: Copy cached packages
        if: steps.cache-packages.outputs.cache-hit == 'true'
        env:
          SOURCE: $ runner.temp /cache-linux
        run: |
          echo "# --------------------------------------------------------"
          echo "# Using Cached packages"
          ls -lha "$SOURCE"
          sudo cp --force --recursive "$SOURCE"/. /
          echo "# --------------------------------------------------------"

      - name: Generate release files and commit in GitHub
        run: |
          echo "# --------------------------------------------------------"
          echo "# Generating release files"
          git fetch --all
          git pull --rebase origin main
          git checkout main
          cd ./src/programming-from-the-ground-up
          ./make.sh
          cd ../../
          ls -lha release/
          git config --global user.name 'Israel Roldan'
          git config --global user.email 'israel.alberto.rv@gmail.com'
          git add .
          git status
          git commit -m "Automated Release."
          git push
          git status
          echo "# --------------------------------------------------------"

解释部分代码:

这里是动作缓存,表示一个key,它将被生成一次并在以后的执行中进行比较。 path 是生成缓存压缩文件的目录。

      - uses: actions/cache@v2
        id: cache-packages
        with:
          path: $ runner.temp /cache-linux
          key: $ runner.os -cache-packages-v2.1

这个对key缓存的条件搜索,如果它退出cache-hit是'true'。

if: steps.cache-packages.outputs.cache-hit != 'true'
if: steps.cache-packages.outputs.cache-hit == 'true'

这并不重要,但当du 命令第一次执行时,Linux 索引了所有文件(5~8 分钟),然后当我们使用find 时,只需约 50 秒即可获取所有文件文件。如果需要,您可以删除此行。

后缀命令|| true 可防止2&gt; /dev/null 返回错误,否则操作将停止,因为它会检测到您的脚本有错误输出。您将在脚本中看到几篇论文。

sudo du -h -d 1 / 2> /dev/null || true

这是神奇的部分,使用find生成实际文件的列表,不包括一些目录以优化缓存文件夹。它也将在安装和make 程序之后执行。在下一个快照中,文件名应该是不同的snapshot_02.txt

sudo find / \
      -type f,l \
      -not \( -path "/sys*" -prune \) \
      -not \( -path "/proc*" -prune \) \
      -not \( -path "/mnt*" -prune \) \
      -not \( -path "/dev*" -prune \) \
      -not \( -path "/run*" -prune \) \
      -not \( -path "/etc/mtab*" -prune \) \
      -not \( -path "/var/cache/apt/archives*" -prune \) \
      -not \( -path "/tmp*" -prune \) \
      -not \( -path "/var/tmp*" -prune \) \
      -not \( -path "/var/backups*" \) \
      -not \( -path "/boot*" -prune \) \
      -not \( -path "/vmlinuz*" -prune \) \
      > "$ runner.temp "/snapshots/snapshot_01.txt 2> /dev/null \
      || true

安装一些包和pandoc

sudo apt install -y texlive-latex-extra wget
wget -q https://github.com/jgm/pandoc/releases/download/2.11.2/pandoc-2.11.2-1-amd64.deb
sudo dpkg -i pandoc-2.11.2-1-amd64.deb
rm -f pandoc-2.11.2-1-amd64.deb

使用添加的新文件生成文本文件,文件也可以是符号文件。

diff -C 1 \
      "$ runner.temp "/snapshots/snapshot_01.txt \
      "$ runner.temp "/snapshots/snapshot_02.txt \
      | grep -E "^\+" \
      | sed -E s/..// \
      > "$ runner.temp "/snapshots/snapshot_new_files.txt

最后将所有文件复制到缓存目录中作为存档保留原始信息。

while IFS= read -r LINE
do
  sudo cp -a --parent "$LINE" "$SOURCE"
done < "$ runner.temp "/snapshots/snapshot_new_files.txt

步骤将所有缓存文件复制到主路径/

      - name: Copy cached packages
        if: steps.cache-packages.outputs.cache-hit == 'true'
        env:
          SOURCE: $ runner.temp /cache-linux
        run: |
          echo "# --------------------------------------------------------"
          echo "# Using Cached packages"
          ls -lha "$SOURCE"
          sudo cp --force --recursive "$SOURCE"/. /
          echo "# --------------------------------------------------------"

这一步是我使用缓存生成的已安装包的地方,./make.sh 脚本使用pandoc 进行一些转换。正如我所提到的,您可以创建使用缓存优势的其他步骤或不使用缓存的其他步骤。

      - name: Generate release files and commit in GitHub
        run: |
          echo "# --------------------------------------------------------"
          echo "# Generating release files"
          cd ./src/programming-from-the-ground-up
          ./make.sh

【讨论】:

以上是关于在 GitHub Actions 工作流程中缓存 APT 包的主要内容,如果未能解决你的问题,请参考以下文章

在 GitHub Actions 中缓存 node_modules

如何在 GitHub Actions 中缓存纱线包

在 Github 操作中未正确加载缓存

GitHub Actions:在 Windows 环境中缓存 Maven .m2 存储库 C\:\\Users\runneradmin\\.m2\repository:无法统计:没有这样的文件或目录

GitHub 缓存与子目录中的 Maven 项目

如何在 Github Actions 工作流中从 Github 包访问 Maven 依赖项?