Stack(Haskell)使用GitHub Actions构建源文件的缓存



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

示例

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

以下是示例:

  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

我可以从日志(verbose Stack(中观察到缓存中的某些内容正在更新,但我完全不清楚是什么以及为什么。它正确地发现只有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文件(发生更改时,不会按预期生成任何内容。

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

希望重新编译未更改的源文件的原因现在是有意义的。我们该如何解决这个问题。获得它的唯一真正方法是恢复更改特定文件的最后一次git提交的时间戳。我很早以前就注意到了这一点,谷歌搜索给了我一些关于SO的答案,我认为其中之一是:恢复文件';s在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脚本,而不需要任何非核心依赖项。我在所有的项目中都使用它,我会在这里嵌入它的精华,但也会提供一个指向永久要点的链接:

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本身来运行脚本
  • 使用与项目完全相同的l解析器

以上两点将确保不会安装多余的依赖项或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相结合,则该源文件将被重新编译。

综上所述:

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

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

最新更新