这个问题的罪魁祸首是堆栈使用时间戳(就像许多其他工具一样)来确定源文件是否已更改。当您在 CI 上恢复缓存并且操作正确时,不会重建任何依赖项,但源文件的问题是,当 CI 提供程序为您克隆存储库时,存储库中所有文件的时间戳都会被设置到克隆的日期和时间。
希望重新编译未更改的源文件的原因现在有意义。我们如何解决这个问题。获取它的唯一真正方法是恢复更改特定文件的最后一次 git 提交的时间戳。我很久以前就注意到了这一点,谷歌搜索给了我一些答案,我认为这是其中之一:在 Git 中恢复文件的修改时间 https://stackoverflow.com/questions/2458042/restore-a-files-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 以与操作系统无关的方式解决这个问题。因此,我编写了一个适用于所有 GHC-8.2 版本及更高版本的 Haskell 脚本,无需任何非核心依赖项。我将它用于我的所有项目,我将在这里嵌入它的精华,但也提供链接到永久要点 https://gist.github.com/lehins/fd36a8cc8bf853173437b17f6b6426ad:
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
您将如何在没有太多开销的情况下使用它?诀窍是:
- use
stack
本身运行脚本
- 使用与项目完全相同的解析器。
以上两点将确保不会安装多余的依赖项或 ghc 版本。总而言之,只需要两件事:stack
和类似的东西curl
or wget
它将跨平台工作:
# 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 https://github.com/lehins/massiv-io
Edit@Simon Michael 在评论中提到他无法在本地重现这个问题。原因是 CI 上的一切与本地的并不相同。很多时候,绝对路径是不同的,例如,可能是我现在想不到的其他东西。这些东西与源文件时间戳一起导致源文件的重新编译。
例如,按照以下步骤操作,您会发现您的项目将被重新编译:
~/tmp$ git clone [email protected] /cdn-cgi/l/email-protection: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
源文件的,该源文件将被重新编译。
把它们加起来:
- 环境可能导致项目需要重建
- 源文件的内容可能会导致源文件(以及依赖于它的其他文件)被重新编译
- 环境以及源文件内容或时间戳更改可能会导致项目以及该源文件被重新编译