【问题标题】:Stack (Haskell) build cache of source files with GitHub ActionsStack (Haskell) 使用 GitHub Actions 构建源文件缓存
【发布时间】:2020-03-28 20:04:51
【问题描述】:

当使用stack build 在本地构建我的 Haskell 项目时,仅重新编译更改的源文件。不幸的是,我无法让 Stack 在 GitHub Actions 上表现得像这样。请问有什么建议吗?

示例

我用Lib.hsFib.hs 创建了一个简单的示例,我什至检查了缓存的.stack-work 文件夹在构建之间是否更新,但它总是编译这两个文件,即使只更改了一个文件。

示例如下:

  1. (不使用缓存,构建Lib.hsFib.hs + 依赖项):https://github.com/MarekSuchanek/stack-test/runs/542163994
  2. (仅Lib.hs 更改,同时构建Lib.hsFib.hs):https://github.com/MarekSuchanek/stack-test/runs/542174351

我可以从日志(详细堆栈)中观察到缓存中的某些内容正在更新,但我完全不清楚是什么以及为什么。它正确地发现只有 Lib.hs 被更改:“stack-test-0.1.0.0: unregistering (local file changes: src/Lib.hs)”所以我不明白为什么所有都被编译。我注意到在 2.Fib.hi 中没有更新 .stack-work 但其他人(Fib.oFib.dyn_hiFib.dyn_o)是。

注意

缓存 ~/.stack 是可以的,当没有更改源文件时也可以不构建。当然,这是一个虚拟示例,但我们有不同的项目,其中包含更多源文件,这将显着加快构建速度。更改非源文件(例如 README 文件)时,不会按预期构建任何内容。

【问题讨论】:

  • 正如我所见,没有人知道 Stack 实际上是如何“工作”的 ????
  • 查看我提供的答案 ;) 我想有些人对它的工作原理有所了解。 ;P

标签: haskell caching cabal haskell-stack github-actions


【解决方案1】:

这个问题的罪魁祸首是堆栈使用时间戳(就像许多其他工具一样)来确定源文件是否已更改。当您在 CI 上恢复缓存并正确执行时,不会重建任何依赖项,但源文件的问题是,当 CI 提供程序为您克隆存储库时,存储库中所有文件的时间戳都已设置到它被克隆的日期和时间。

希望重新编译未更改的源文件的原因现在是有意义的。我们如何解决这个问题。获得它的唯一真正方法是恢复更改特定文件的最后一次 git 提交的时间戳。我很久以前就注意到了这一点,并且在谷歌上搜索了一些关于 SO 的答案,我认为这是其中之一:Restore a file's modification time in Git

A 对其进行了一些修改以满足我的需要,这就是我最终得到的结果:

  git ls-tree -r --name-only HEAD | while read filename; do
    TS="$(git log -1 --format="%ct" -- ${filename})"
    touch "${filename}" -mt "$(date --date="@$TS" "+%Y%m%d%H%M.%S")"
  done

那个工人在 Ubuntu CI 上对我来说有一段时间了,但是当我需要设置 Azure CI 时,我不想用 bash 以与操作系统无关的方式解决这个问题。出于这个原因,我编写了一个适用于所有 GHC-8.2 和更新版本的 Haskell 脚本,而不需要任何非核心依赖项。我将它用于我所有的项目,我将在这里嵌入它的汁液,但也提供一个link to a permanent gist

main = do
  args <- getArgs
  let rev = case args of
        [] -> "HEAD"
        (x:_) -> x
  fs <- readProcess "git" ["ls-tree", "-r", "-t", "--full-name", "--name-only", rev] ""
  let iso8601 = iso8601DateFormat (Just "%H:%M:%S%z")
      restoreFileModtime fp = do
        modTimeStr <- readProcess "git" ["log", "--pretty=format:%cI", "-1", rev, "--", fp] ""
        modTime <- parseTimeM True defaultTimeLocale iso8601 modTimeStr
        setModificationTime fp modTime
        putStrLn $ "[" ++ modTimeStr ++ "] " ++ fp
  putStrLn "Restoring modification time for all these files:"
  mapM_ restoreFileModtime $ lines fs

您将如何在没有太多开销的情况下使用它。诀窍是:

  • 使用stack 本身来运行脚本
  • 使用与项目完全相同的解析器。

以上两点将确保不会安装多余的依赖项或 ghc 版本。总而言之,只需要两件事是stackcurlwget 之类的东西,它可以跨平台工作:

# Script for restoring source files modification time from commit to avoid recompilation.
curl -sSkL https://gist.githubusercontent.com/lehins/fd36a8cc8bf853173437b17f6b6426ad/raw/4702d0252731ad8b21317375e917124c590819ce/git-modtime.hs -o git-modtime.hs
# Restore mod time and setup ghc, if it wasn't restored from cache
stack script --resolver ${RESOLVER} git-modtime.hs --package base --package time --package directory --package process

这是一个使用这种方法的真实项目,您可以深入了解它是如何工作的:massiv-io

编辑 cmets 中的@Simon Michael 提到他无法在本地重现此问题。这样做的原因是,并非所有 CI 上的内容都与本地相同。绝对路径通常是不同的,例如,可能是我现在想不到的其他事情。这些东西,连同源文件时间戳一起导致源文件的重新编译。

例如按照以下步骤,您会发现您的项目将被重新编译:

~/tmp$ git clone git@github.com:fpco/safe-decimal.git
~/tmp$ cd safe-decimal
~/tmp/safe-decimal$ stack build
safe-decimal> configure (lib)
[1 of 2] Compiling Main
...
Configuring safe-decimal-0.2.0.0...
safe-decimal> build (lib)
Preprocessing library for safe-decimal-0.2.0.0..
Building library for safe-decimal-0.2.0.0..
[1 of 3] Compiling Numeric.Decimal.BoundedArithmetic
[2 of 3] Compiling Numeric.Decimal.Internal
[3 of 3] Compiling Numeric.Decimal
...
~/tmp/safe-decimal$ cd ../
~/tmp$ mv safe-decimal safe-decimal-moved
~/tmp$ cd safe-decimal-moved/
~/tmp/safe-decimal-moved$ stack build
safe-decimal-0.2.0.0: unregistering (old configure information not found)
safe-decimal> configure (lib)
[1 of 2] Compiling Main
...

您会看到项目的位置触发了项目构建。尽管项目本身已经重建,但您会注意到没有重新编译任何源文件。现在,如果您将该过程与源文件的touch 结合起来,该源文件将被重新编译。

总结一下:

  • 环境可能导致项目被重建
  • 源文件的内容可能会导致源文件(以及依赖它的其他文件)被重新编译
  • 环境连同源文件内容或时间戳更改可能会导致项目连同该源文件一起被重新编译

【讨论】:

  • 我对此感到困惑,因为我似乎没有看到影响本地堆栈构建的时间戳。例如,如果我 touch 一个源文件,它不会被重建。
  • 同样,如果我触摸 .{dyn_hi,dyn_o,hi,o} 文件。
  • @SimonMichael 我在答案中添加了一个示例。简而言之,您需要触发项目的重建,以便时间戳触发重新编译。
  • 感谢您提供的详细信息,非常有帮助。正如您所说,我看到了:更改的路径(例如,重命名文件夹)会导致(a)Setup.hs 和(b)时间戳已更改的任何其他模块的重建。你知道github.com/commercialhaskell/stack/issues 有什么问题吗?
  • 谢谢!时间戳确实解决了这个问题,但另外,GitHub 操作默认情况下仅使用非常有限的提取,没有任何历史记录,因此必须将其调整为 fetch all history 才能正确恢复时间戳。
【解决方案2】:

我为此提供了PR 修复,因此不再依赖修改时间!

【讨论】:

  • 现在已合并到堆栈 2.5.1 - 谢谢@Andres S。不幸的是,即使使用堆栈 2.5.1,我仍然看到错误 Trouble loading CompilerPaths cache 将我带到此线程。对我来说,它是缓存键,没有正确识别:key: ${{ runner.os }}-${{ matrix.ghc }} 不起作用,key: ${{ runner.os }}-${{ matrix.ghc }}-stack 起作用。
  • @nevrome 与堆栈(和 cabal)现在正确地按内容缓存,不幸的是 ghc 本身不是。我在 ghc 构建代码中花了太多时间来实现这一点。如果我有一些额外的时间,我会写一份提案/公关来解决这个问题,但这将是一项任务。如果你用 ghc 编译一个简单的代码库,改变一个文件的修改时间,然后用 ghc 重新编译项目,你会注意到文件被重新编译了。
  • 我明白了——所以也许我关于依赖缓存的问题毕竟与这个线程完全无关。无论如何,我都会在这里留下评论,因为也许有人会像我一样遇到它。继续努力!
  • 好消息!刚刚针对 GHC gitlab.haskell.org/ghc/ghc/-/merge_requests/5130 打开了 WIP PR
猜你喜欢
  • 2020-02-09
  • 2020-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-29
  • 1970-01-01
  • 2021-07-12
  • 2023-01-27
相关资源
最近更新 更多