【问题标题】:sbt always recompiles full project in CI, even with caching?sbt 总是在 CI 中重新编译整个项目,即使有缓存?
【发布时间】:2019-08-12 10:30:16
【问题描述】:

我正在努力将 SBT 用于具有此基本工作流程的 CI 流程:

  1. 编译测试
  2. 缓存~/.sbt~/.ivy2/cache
  3. 缓存我项目中的所有target目录

在后续步骤中:

  1. 恢复~/.sbt~/.ivy2/cache
  2. 恢复整个项目,包括先前生成的target 目录,其中包含.class 文件和相同的源代码(应该是相同结帐)
  3. 通过sbt test运行测试

100% 的时间,sbt test 重新编译整个项目。我想了解或调试为什么会这样,因为自上次编译以来没有任何变化(嗯,应该没有任何变化,那么是什么导致它相信某些事情发生了?)

我目前正在使用带有 docker 执行器的 circleci。这意味着有一个新的 docker 实例,来自同一个镜像,运行每个步骤,尽管我希望缓存能够解决这个问题。

.circleci/config.yml 的相关部分(如果你不使用圆圈,这应该仍然可以理解;我已经注释了我能做的):

---
version: 2

jobs:
  # compile and cache compilation
  test-compile:
    working_directory: /home/circleci/myteam/myproj
    docker:
      - image: myorg/myimage:sbt-1.2.8
    steps:
      # the directory to be persisted (cached/restored) to the next step
      - attach_workspace:
          at: /home/circleci/myteam
      # git pull to /home/circleci/myteam/myproj
      - checkout
      - restore_cache:
          # look for a pre-existing set of ~/.ivy2/cache, ~/.sbt dirs 
          # from a prior build
          keys:
            - sbt-artifacts-{{ checksum "project/build.properties"}}-{{ checksum "build.sbt" }}-{{ checksum "project/Dependencies.scala" }}-{{ checksum "project/plugins.sbt" }}-{{ .Branch }}
      - restore_cache:
          # look for pre-existing set of 'target' dirs from a prior build
          keys:
            - build-{{ checksum "project/build.properties"}}-{{ checksum "build.sbt" }}-{{ checksum "project/Dependencies.scala" }}-{{ checksum "project/plugins.sbt" }}-{{ .Branch }}
      - run:
          # the compile step
          working_directory: /home/circleci/myteam/myproj
          command: sbt test:compile
      # per: https://www.scala-sbt.org/1.0/docs/Travis-CI-with-sbt.html
      # Cleanup the cached directories to avoid unnecessary cache updates
      - run:
          working_directory: /home/circleci
          command: |
            rm -rf /home/circleci/.ivy2/.sbt.ivy.lock
            find /home/circleci/.ivy2/cache -name "ivydata-*.properties" -print -delete
            find /home/circleci/.sbt -name "*.lock" -print -delete
      - save_cache:
          # cache ~/.ivy2/cache and ~/.sbt for subsequent builds
          key: sbt-artifacts-{{ checksum "project/build.properties"}}-{{ checksum "build.sbt" }}-{{ checksum "project/Dependencies.scala" }}-{{ checksum "project/plugins.sbt" }}-{{ .Branch }}-{{ .Revision }}
          paths:
            - /home/circleci/.ivy2/cache
            - /home/circleci/.sbt
      - save_cache:
          # cache the `target` dirs for subsequenet builds
          key: build-{{ checksum "project/build.properties"}}-{{ checksum "build.sbt" }}-{{ checksum "project/Dependencies.scala" }}-{{ checksum "project/plugins.sbt" }}-{{ .Branch }}-{{ .Revision }}
          paths:
            - /home/circleci/myteam/myproj/target
            - /home/circleci/myteam/myproj/project/target
            - /home/circleci/myteam/myproj/project/project/target
      # in circle, a 'workflow' undergoes several jobs, this first one 
      # is 'compile', the next will run the tests (see next 'job' section
      # 'test-run' below). 
      # 'persist to workspace' takes any files from this job and ensures 
      # they 'come with' the workspace to the next job in the workflow
      - persist_to_workspace:
          root: /home/circleci/myteam
          # bring the git checkout, including all target dirs
          paths:
            - myproj
      - persist_to_workspace:
          root: /home/circleci
          # bring the big stuff
          paths:
            - .ivy2/cache
            - .sbt

  # actually runs the tests compiled in the previous job
  test-run:
    environment:
      SBT_OPTS: -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions  -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Duser.timezone=Etc/UTC -Duser.language=en -Duser.country=US
    docker:
      # run tests in the same image as before, but technically 
      # a different instance
      - image: myorg/myimage:sbt-1.2.8
    steps:
      # bring over all files 'persist_to_workspace' in the last job
      - attach_workspace:
          at: /home/circleci/myteam
      # restore ~/.sbt and ~/.ivy2/cache via `mv` from the workspace 
      # back to the home dir
      - run:
          working_directory: /home/circleci/myteam
          command: |
            [[ ! -d /home/circleci/.ivy2 ]] && mkdir /home/circleci/.ivy2

            for d in .ivy2/cache .sbt; do
              [[ -d "/home/circleci/$d" ]] && rm -rf "/home/circleci/$d"
              if [ -d "$d"  ]; then
                mv -v "$d" "/home/circleci/$d"
              else
                echo "$d does not exist" >&2
                ls -la . >&2
                exit 1
              fi
            done
      - run:
          # run the tests, already compiled
          # note: recompiles everything every time!
          working_directory: /home/circleci/myteam/myproj
          command: sbt test
          no_output_timeout: 3900s

workflows:
  version: 2
  build-and-test:
    jobs:
      - test-compile
      - test-run:
          requires:
            - test-compile

第二阶段的输出通常如下所示:

#!/bin/bash -eo pipefail
sbt test

[info] Loading settings for project myproj-build from native-packager.sbt,plugins.sbt ...
[info] Loading project definition from /home/circleci/myorg/myproj/project
[info] Updating ProjectRef(uri("file:/home/circleci/myorg/myproj/project/"), "myproj-build")...
[info] Done updating.
[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
[info] Compiling 1 Scala source to /home/circleci/myorg/myproj/project/target/scala-2.12/sbt-1.0/classes ...
[info] Done compiling.
[info] Loading settings for project root from build.sbt ...
[info] Set current project to Piranha (in build file:/home/circleci/myorg/myproj/)
[info] Compiling 1026 Scala sources to /home/circleci/myorg/myproj/target/scala-2.12/classes ...

我可以做些什么来确定为什么第二次重新编译所有源并缓解它?

我在 linux 容器中运行 sbt 1.2.8 和 scala 2.12.8。


更新

我还没有解决这个问题,但我想我会分享一个解决我最糟糕问题的方法。

主要问题:将“测试编译”与“测试运行”分开 次要问题:构建速度更快,无需在每次推送时重新编译所有内容

我没有办法解决次要问题。对于初级:

我可以通过 scala -cp ... org.scalatest.tools.Runner 而不是通过 sbt test 从 CLI 运行 scalatest runner,以避免任何重新编译的尝试。运行器可以对.class 文件的目录进行操作。

变更摘要:

  1. 更新 docker 容器以包含 scala cli 安装。 (很遗憾,我现在需要让这些版本保持同步)
  2. 构建阶段:sbt test:compile 'inspect run' 'export test:fullClasspath' | tee >(grep -F '.jar' > ~test-classpath.txt)
    • 编译但也记录一个可复制的类路径字符串,适合传递给scala -cp VALUE_HERE以运行测试
  3. 测试阶段:scala -cp "$(cat test-classpath.txt)" org.scalatest.tools.Runner -R target/scala-2.12/test-classes/ -u target/test-reports -oD
    • 通过 runner 运行 scalatest,使用在 target/scala-2.12/test-classes 中编译的 .class 文件,使用在编译阶段报告的类路径,并将 printint 打印到标准输出以及报告目录

我不喜欢这个,它有一些问题,但我想我会分享这个解决方法。

【问题讨论】:

    标签: scala continuous-integration sbt circleci circleci-workflows


    【解决方案1】:

    如果您使用比 1.0.4 更新的 sbt 版本,缓存将无法为您工作,因为编译器将始终使所有内容无效。 此锌编译器问题已在此处报告:https://github.com/sbt/sbt/issues/4168

    我的建议是为 CI 降级 sbt 版本。还要检查和验证 CI 是否正在更改 .sbt 或 .ivy2 文件时间戳。如果它们被更改,请通过压缩和解压缩它们分别缓存它们。

    我在 Bitbucket Pipelines CI 上遇到了同样的问题,并成功地使其工作here

    【讨论】:

      【解决方案2】:

      我也在 gitlab 工作中使用 sbt 1.2.8 遇到了这个问题。以前(在 sbt 0.13 中)缓存 target 目录工作正常。

      现在我正在尝试通过设置手动调试:

      logLevel := Level.Debug,
      incOptions := incOptions.value.withApiDebug(true).withRelationsDebug(true),
      

      在我的构建中。这应该打印失效的原因。它产生的输出太多,无法在 CI 中运行,所以我无法重现我看到问题的确切条件。

      【讨论】:

      • 目标目录缓存工作正常的 sbt 的确切版本是什么?我刚试过0.13.17,不管缓存的目标目录如何,它仍然会为我重新编译所有内容。
      【解决方案3】:

      SBT 对重新编译非常挑剔,Docker 给它带来了特别的麻烦。

      看看:

      【讨论】:

        【解决方案4】:

        我在构建 travis 时遇到了类似的问题,我怀疑这个解决方案也适用于 circle-ci。根本原因是缓存存储为tar文件,文件的修改时间只有第二个分辨率。您可以指定具有足够分辨率的格式。我的解决方案是创建一个小脚本travis_tar.sh

        #!/bin/bash
        /bin/tar-orig --format=posix $@
        

        然后用这个脚本替换系统tar:

        sudo mv /bin/tar /bin/tar-orig
        sudo mv .travis/travis_tar.sh /bin/tar
        sudo chmod +x /bin/tar
        

        这可能发生在缓存加载后,原版系统 tar 解压 posix 格式的 tar 文件就好了。

        【讨论】:

          【解决方案5】:

          我也有同样的问题。我放弃了尝试让所有时间戳匹配,最终发现我可以使用:

          sbt 'set  Compile / compile / skip := true' 'test'
          

          它仍然不完美,sourceGenerators 可能还有一些其他的东西仍然可以运行,但它肯定比没有它要好得多。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-01-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多