使用GitHub Action堆栈(Haskell)构建源文件缓存

时间:2020-03-28 20:04:51

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

使用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文件)时,则未按预期构建任何文件。

2 个答案:

答案 0 :(得分:3)

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

希望重新编译未更改的源文件的原因现在很有意义。我们如何处理此问题。获得它的唯一真实方法是恢复更改特定文件的最后一个git commit的时间戳。我很早以前就注意到了这一点,在Google上进行了一些谷歌搜索后得到了一些答案,我认为这是其中之一:Restore a file's modification time in Git

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

  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解决与操作系统无关的问题。因此,我编写了一个Haskell脚本,该脚本适用于所有GHC-8.2和更高版本,而无需任何非核心依赖。我将其用于我的所有项目,并将其嵌入到这里,但还要提供一个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版本。总之,仅需要两件事就是stack和类似curlwget的东西,它将跨平台工作:

# 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

编辑 @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结合使用,则将重新编译该源文件。

总结一下:

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

答案 1 :(得分:0)

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