From f94629e4502f12228295b454068f8ebb9ab46fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Tue, 18 Aug 2026 11:23:04 +0000 Subject: [PATCH 1/5] fix: make the dev container work when the checkout is a git worktree In a linked git worktree `.git` is a file pointing at an admin directory under the main checkout's `.git`, an absolute path outside the checkout. `container-run.sh` mounted only the checkout, so that pointer dangled and every `git` invocation inside the container failed. Mount the main checkout's `.git` at the same absolute path so it resolves. Inside the container all worktrees look prunable, because their back-pointers name host paths that are not mounted. Reading is unaffected, but `git gc` -- which git runs automatically after commit/fetch -- prunes worktrees and would delete the host's admin directories, so turn automatic gc off for this case. Also stop the affected scripts from failing silently. `cd ""` returns 0 in bash and a command substitution inside `${X:-...}` does not trip errexit, so `ci/scripts/rust-lint.sh` linted nothing and still exited 0; `export X="$(...)"` swallows the substitution's exit status the same way. Assign first, then `cd`/`export`, so `set -e` fires. Co-Authored-By: Claude Opus 5 (1M context) --- ci/container/build-ic.sh | 9 +++++++-- ci/container/container-run.sh | 34 ++++++++++++++++++++++++++++++++++ ci/container/get-image-tag.sh | 4 +++- ci/scripts/rust-lint.sh | 7 ++++++- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/ci/container/build-ic.sh b/ci/container/build-ic.sh index 536f952d9327..2ae9f93875e0 100755 --- a/ci/container/build-ic.sh +++ b/ci/container/build-ic.sh @@ -10,7 +10,11 @@ if [ "${BUILD_IC_NESTED:-}" == 1 ]; then fi export BUILD_IC_NESTED=1 -export ROOT_DIR="$(git rev-parse --show-toplevel)" +# Assign, then export: `export X="$(...)"` swallows the command +# substitution's exit status, so a failing git would leave X empty and +# `set -e` would not notice. +ROOT_DIR="$(git rev-parse --show-toplevel)" +export ROOT_DIR # This script needs to be run inside the ic-build container # If it isn't, we drop into the correct container. @@ -105,7 +109,8 @@ if [ -n "$(git status --porcelain)" ]; then exit 1 fi -export VERSION="$(git rev-parse HEAD)" +VERSION="$(git rev-parse HEAD)" +export VERSION BAZEL_TARGETS=() diff --git a/ci/container/container-run.sh b/ci/container/container-run.sh index 0ca09a2b7d13..f75e9ccbe776 100755 --- a/ci/container/container-run.sh +++ b/ci/container/container-run.sh @@ -205,6 +205,40 @@ RUNTIME_RUN_ARGS=( --mount type=tmpfs,target="/tmp/containers" # expected by ic-os build ) +# When the checkout is a linked git worktree, its `.git` is a *file* holding an +# absolute path to an admin directory under the main checkout's `.git`, i.e. a +# path outside the checkout. Mounting only the checkout leaves that pointer +# dangling, so every `git` call inside the container fails -- including the +# `$(git rev-parse --show-toplevel)` that several ci scripts rely on. Mount the +# main checkout's `.git` at the very same absolute path so the pointer resolves. +if [ -f "${REPO_ROOT}/.git" ]; then + GIT_COMMON_DIR="$(git rev-parse --git-common-dir)" + GIT_COMMON_DIR="$(cd "$GIT_COMMON_DIR" && pwd)" # may be relative + case "$GIT_COMMON_DIR" in + "$REPO_ROOT" | "$REPO_ROOT"/*) + : # inside the checkout already, covered by the mount above + ;; + *) + eprintln "Checkout is a git worktree, also mounting '$GIT_COMMON_DIR'" + RUNTIME_RUN_ARGS+=( + --mount type=bind,source="${GIT_COMMON_DIR}",target="${GIT_COMMON_DIR}" + + # Each worktree admin directory holds a back-pointer to its + # worktree's *host* path, and those paths do not exist in the + # container (this checkout lives at $WORKDIR, the others are not + # mounted at all), so git considers every worktree prunable here. + # That is harmless for reading, but `git gc` -- which git runs + # automatically after commit/fetch/... -- prunes worktrees as + # part of its job and would delete the host's admin directories. + # Turn auto gc off; an explicit `git gc` remains the user's call. + -e GIT_CONFIG_COUNT=1 + -e GIT_CONFIG_KEY_0=gc.auto + -e GIT_CONFIG_VALUE_0=0 + ) + ;; + esac +fi + # Privilege/isolation flags required by the IC-OS guest build, per runtime. if [ "$RUNTIME" = docker ]; then # Under docker the IC-OS build runs (rootless) podman *inside* this diff --git a/ci/container/get-image-tag.sh b/ci/container/get-image-tag.sh index 01460712fa18..3ef0234dbe1f 100755 --- a/ci/container/get-image-tag.sh +++ b/ci/container/get-image-tag.sh @@ -2,7 +2,9 @@ set -eEuo pipefail -cd "$(git rev-parse --show-toplevel)" +# Assign first: `cd "$(...)"` would silently `cd ""` (exit 0) if git failed. +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" INPUT_FILES=( ci/container/Dockerfile diff --git a/ci/scripts/rust-lint.sh b/ci/scripts/rust-lint.sh index 7f8879d3a5fc..f26e9eeeee9f 100755 --- a/ci/scripts/rust-lint.sh +++ b/ci/scripts/rust-lint.sh @@ -1,7 +1,12 @@ #!/usr/bin/env bash set -xeuo pipefail -cd "${CI_PROJECT_DIR:-$(git rev-parse --show-toplevel)}" +# Note the assignment: `cd "$(git rev-parse ...)"` would `cd ""` and *succeed* +# (bash returns 0 for a null directory operand) if git failed, silently turning +# this script into a no-op that lints nothing and exits 0. An assignment, in +# contrast, takes the command substitution's exit status, so `set -e` fires. +REPO_ROOT="${CI_PROJECT_DIR:-$(git rev-parse --show-toplevel)}" +cd "$REPO_ROOT" cargo fmt -- --check From aea9b83fc3db28c46f93a941ed798921b4edd92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Tue, 18 Aug 2026 13:01:52 +0000 Subject: [PATCH 2/5] fix: protect worktrees with gc.worktreePruneExpire, not gc.auto Setting `gc.auto=0` stops automatic gc from pruning the host's worktree admin directories, but leaves two gaps: an explicit `git gc` inside the container still prunes them, and no object maintenance ever happens for the host repository. `gc.worktreePruneExpire=never` disables exactly the dangerous step, in both the automatic and the explicit path, and leaves the rest of gc alone. The risk is not theoretical: gc prunes a worktree whose index has not been touched within `gc.worktreePruneExpire` (three months by default), and a machine that has accumulated worktrees typically has several of those. Co-Authored-By: Claude Opus 5 (1M context) --- ci/container/container-run.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ci/container/container-run.sh b/ci/container/container-run.sh index f75e9ccbe776..134b6682601e 100755 --- a/ci/container/container-run.sh +++ b/ci/container/container-run.sh @@ -227,13 +227,16 @@ if [ -f "${REPO_ROOT}/.git" ]; then # worktree's *host* path, and those paths do not exist in the # container (this checkout lives at $WORKDIR, the others are not # mounted at all), so git considers every worktree prunable here. - # That is harmless for reading, but `git gc` -- which git runs - # automatically after commit/fetch/... -- prunes worktrees as - # part of its job and would delete the host's admin directories. - # Turn auto gc off; an explicit `git gc` remains the user's call. + # That is harmless for reading, but `git gc` prunes worktrees as + # part of its job -- and git runs gc automatically after + # commit/fetch/... -- so it would delete the host's admin + # directories. Disable worktree pruning itself rather than gc as + # a whole: object maintenance keeps working, and an explicit + # `git gc` is safe too. (`git worktree prune` ignores this + # setting, so still don't run that one in here.) -e GIT_CONFIG_COUNT=1 - -e GIT_CONFIG_KEY_0=gc.auto - -e GIT_CONFIG_VALUE_0=0 + -e GIT_CONFIG_KEY_0=gc.worktreePruneExpire + -e GIT_CONFIG_VALUE_0=never ) ;; esac From 79f10e1d708b93637936954f59b4ed8a76d88c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Tue, 18 Aug 2026 13:02:19 +0000 Subject: [PATCH 3/5] fix: reject worktrees whose .git pointer is relative Mounting the admin directory at its host path only helps while the checkout's `.git` names that path absolutely. With `worktree.useRelativePaths` (git >= 2.48) it holds a path relative to the checkout instead, which inside the container resolves against the mount point of the checkout and dangles wherever the admin directory is mounted. There is nothing to mount that would fix it, so report it, point at `git worktree repair`, and refuse -- rather than starting a container in which git silently does not work. Co-Authored-By: Claude Opus 5 (1M context) --- ci/container/container-run.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ci/container/container-run.sh b/ci/container/container-run.sh index 134b6682601e..0ab2a56c4d2b 100755 --- a/ci/container/container-run.sh +++ b/ci/container/container-run.sh @@ -219,6 +219,24 @@ if [ -f "${REPO_ROOT}/.git" ]; then : # inside the checkout already, covered by the mount above ;; *) + # Mounting at the same absolute path only helps if the pointer *is* + # absolute. With `worktree.useRelativePaths` (git >= 2.48) `.git` + # holds a path relative to the checkout, which inside the container + # resolves against $WORKDIR no matter where the admin directory is + # mounted, and so keeps dangling. Say so instead of handing over a + # container in which git does not work. + GITDIR_POINTER="$(sed -n 's/^gitdir: //p' "${REPO_ROOT}/.git")" + case "$GITDIR_POINTER" in + /*) ;; + *) + eprintln "'${REPO_ROOT}/.git' points at '$GITDIR_POINTER', a path relative to the checkout." + eprintln "The checkout is mounted at '$WORKDIR' in the container, so that path cannot be made to resolve there." + eprintln "Re-point it absolutely, e.g. from the main checkout:" + eprintln "> git -c worktree.useRelativePaths=false worktree repair '$REPO_ROOT'" + exit 1 + ;; + esac + eprintln "Checkout is a git worktree, also mounting '$GIT_COMMON_DIR'" RUNTIME_RUN_ARGS+=( --mount type=bind,source="${GIT_COMMON_DIR}",target="${GIT_COMMON_DIR}" From dbe87c9b7dcb6ec1c7b0dcc76023f22bc6a06a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Tue, 18 Aug 2026 13:02:42 +0000 Subject: [PATCH 4/5] docs: describe the `.git`-is-a-file case as it is A linked worktree is the case that motivated the mount, but it is not the only one that reaches it: a `--separate-git-dir` clone and a submodule also have a `.git` file pointing outside the checkout, and they are equally broken without it -- so say that, instead of announcing "checkout is a git worktree" for all three. Also note what the mount gives away: for a worktree the container gets read-write access to the main checkout's whole git directory, not just to this checkout. Co-Authored-By: Claude Opus 5 (1M context) --- ci/container/container-run.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ci/container/container-run.sh b/ci/container/container-run.sh index 0ab2a56c4d2b..4398fb066069 100755 --- a/ci/container/container-run.sh +++ b/ci/container/container-run.sh @@ -205,12 +205,17 @@ RUNTIME_RUN_ARGS=( --mount type=tmpfs,target="/tmp/containers" # expected by ic-os build ) -# When the checkout is a linked git worktree, its `.git` is a *file* holding an -# absolute path to an admin directory under the main checkout's `.git`, i.e. a -# path outside the checkout. Mounting only the checkout leaves that pointer +# Where `.git` is a *file* rather than a directory it holds a path to the real +# git directory -- typically a linked git worktree, whose `.git` names an admin +# directory under the main checkout's `.git`, but a `--separate-git-dir` clone +# or a submodule looks the same. Mounting only the checkout leaves that pointer # dangling, so every `git` call inside the container fails -- including the # `$(git rev-parse --show-toplevel)` that several ci scripts rely on. Mount the -# main checkout's `.git` at the very same absolute path so the pointer resolves. +# git directory at the very same absolute path so the pointer resolves. +# +# Note that for a worktree this hands the container read-write access to the +# main checkout's entire git directory, where for a plain clone only the +# checkout itself is exposed. if [ -f "${REPO_ROOT}/.git" ]; then GIT_COMMON_DIR="$(git rev-parse --git-common-dir)" GIT_COMMON_DIR="$(cd "$GIT_COMMON_DIR" && pwd)" # may be relative @@ -237,7 +242,7 @@ if [ -f "${REPO_ROOT}/.git" ]; then ;; esac - eprintln "Checkout is a git worktree, also mounting '$GIT_COMMON_DIR'" + eprintln "Git directory lies outside the checkout, also mounting '$GIT_COMMON_DIR'" RUNTIME_RUN_ARGS+=( --mount type=bind,source="${GIT_COMMON_DIR}",target="${GIT_COMMON_DIR}" From f1ff2969a25da08ccbc0287c7a8d087f12dac5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Tue, 18 Aug 2026 13:02:57 +0000 Subject: [PATCH 5/5] docs: state what `cd ""` actually does to rust-lint.sh `cd ""` does not move the shell, and this script is normally started with the repository root as the working directory, so the failure is not that nothing gets linted -- it is that a failure to locate the repository goes unnoticed and whatever directory the caller happened to be in gets linted in its place, with a successful exit either way. The fix is unchanged; only the comment was overstating the symptom. Co-Authored-By: Claude Opus 5 (1M context) --- ci/scripts/rust-lint.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ci/scripts/rust-lint.sh b/ci/scripts/rust-lint.sh index f26e9eeeee9f..1f2c2ad063be 100755 --- a/ci/scripts/rust-lint.sh +++ b/ci/scripts/rust-lint.sh @@ -2,9 +2,11 @@ set -xeuo pipefail # Note the assignment: `cd "$(git rev-parse ...)"` would `cd ""` and *succeed* -# (bash returns 0 for a null directory operand) if git failed, silently turning -# this script into a no-op that lints nothing and exits 0. An assignment, in -# contrast, takes the command substitution's exit status, so `set -e` fires. +# (bash returns 0 for a null directory operand) if git failed, leaving the shell +# wherever it happened to be and linting that instead -- so a run that never +# located the repository would still look like a successful lint of it. An +# assignment, in contrast, takes the command substitution's exit status, so +# `set -e` fires. REPO_ROOT="${CI_PROJECT_DIR:-$(git rev-parse --show-toplevel)}" cd "$REPO_ROOT"