From b4adf138870b7cbeaae915fd9ce63f702cff0c4a Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 31 Jul 2026 18:24:11 +0100 Subject: [PATCH 01/10] fix: strip mac xattrs in repack COPYFILE_DISABLE only drops the AppleDouble sidecars; the xattrs survive as pax headers and GNU tar warns about each one on unpack. --- demo/spread.yaml | 9 +++++---- inlined/bread-22.04.yaml | 7 ++++--- inlined/bread-24.04.yaml | 7 ++++--- inlined/bread-25.10.yaml | 7 ++++--- inlined/bread-26.04.yaml | 7 ++++--- inlined/bread-26.10.yaml | 7 ++++--- inlined/bread-chisel-releases-22.04.yaml | 7 ++++--- inlined/bread-chisel-releases-24.04.yaml | 7 ++++--- inlined/bread-chisel-releases-25.10.yaml | 7 ++++--- inlined/bread-chisel-releases-26.04.yaml | 7 ++++--- inlined/bread-chisel-releases-26.10.yaml | 7 ++++--- scripts/spread_repack_bread.sh | 7 ++++--- tests/spread.yaml | 7 ++++--- 13 files changed, 53 insertions(+), 40 deletions(-) diff --git a/demo/spread.yaml b/demo/spread.yaml index afd11c1..1022604 100644 --- a/demo/spread.yaml +++ b/demo/spread.yaml @@ -11,15 +11,16 @@ exclude: - .git - .github -# macOS tar packs an AppleDouble `._*` sidecar next to every file; they unpack as -# real files on the remote. `exclude:` can't drop them (tar synthesises them below -# its glob filter), so extract and repack with COPYFILE_DISABLE=1. +# macOS tar packs an AppleDouble `._*` sidecar next to every file and stores +# xattrs as pax headers; the sidecars unpack as real files on the remote and GNU +# tar warns about every xattr header. `exclude:` can't drop them (tar synthesises +# them below its glob filter), so extract and repack without either. repack: | if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml index 2671c1c..3126081 100644 --- a/inlined/bread-22.04.yaml +++ b/inlined/bread-22.04.yaml @@ -12,13 +12,14 @@ exclude: - .github repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-24.04.yaml b/inlined/bread-24.04.yaml index 810aec9..f214446 100644 --- a/inlined/bread-24.04.yaml +++ b/inlined/bread-24.04.yaml @@ -12,13 +12,14 @@ exclude: - .github repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-25.10.yaml b/inlined/bread-25.10.yaml index 98adee1..b6ab48f 100644 --- a/inlined/bread-25.10.yaml +++ b/inlined/bread-25.10.yaml @@ -12,13 +12,14 @@ exclude: - .github repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-26.04.yaml b/inlined/bread-26.04.yaml index 58a873a..79a04a5 100644 --- a/inlined/bread-26.04.yaml +++ b/inlined/bread-26.04.yaml @@ -12,13 +12,14 @@ exclude: - .github repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-26.10.yaml b/inlined/bread-26.10.yaml index 490932c..f267045 100644 --- a/inlined/bread-26.10.yaml +++ b/inlined/bread-26.10.yaml @@ -12,13 +12,14 @@ exclude: - .github repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml index fa3b434..3f3afc2 100644 --- a/inlined/bread-chisel-releases-22.04.yaml +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -19,13 +19,14 @@ exclude: - rootfs repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-24.04.yaml b/inlined/bread-chisel-releases-24.04.yaml index f6578c9..21844bf 100644 --- a/inlined/bread-chisel-releases-24.04.yaml +++ b/inlined/bread-chisel-releases-24.04.yaml @@ -19,13 +19,14 @@ exclude: - rootfs repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-25.10.yaml b/inlined/bread-chisel-releases-25.10.yaml index 8786ab9..148ebe2 100644 --- a/inlined/bread-chisel-releases-25.10.yaml +++ b/inlined/bread-chisel-releases-25.10.yaml @@ -19,13 +19,14 @@ exclude: - rootfs repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-26.04.yaml b/inlined/bread-chisel-releases-26.04.yaml index 9f3e26f..7fa2252 100644 --- a/inlined/bread-chisel-releases-26.04.yaml +++ b/inlined/bread-chisel-releases-26.04.yaml @@ -19,13 +19,14 @@ exclude: - rootfs repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-26.10.yaml b/inlined/bread-chisel-releases-26.10.yaml index 16f319b..3d08b4c 100644 --- a/inlined/bread-chisel-releases-26.10.yaml +++ b/inlined/bread-chisel-releases-26.10.yaml @@ -19,13 +19,14 @@ exclude: - rootfs repack: | - # macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as - # real files on the remote. Repack to remove them. + # macOS tar packs an AppleDouble ._* sidecar next to every file and stores + # xattrs as pax headers; the sidecars unpack as real files on the remote and + # GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/scripts/spread_repack_bread.sh b/scripts/spread_repack_bread.sh index 6389089..e1070fa 100644 --- a/scripts/spread_repack_bread.sh +++ b/scripts/spread_repack_bread.sh @@ -1,10 +1,11 @@ -# macOS tar packs an AppleDouble ._* sidecar next to every file; they unpack as -# real files on the remote. Repack to remove them. +# macOS tar packs an AppleDouble ._* sidecar next to every file and stores +# xattrs as pax headers; the sidecars unpack as real files on the remote and +# GNU tar warns about every xattr header. Repack to remove both. if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi diff --git a/tests/spread.yaml b/tests/spread.yaml index 45db283..66e36aa 100644 --- a/tests/spread.yaml +++ b/tests/spread.yaml @@ -12,14 +12,15 @@ exclude: - .git - .github -# macOS tar packs an AppleDouble `._*` sidecar next to every file; they unpack as -# real files on the remote. Repack to remove them. +# macOS tar packs an AppleDouble `._*` sidecar next to every file and stores +# xattrs as pax headers; the sidecars unpack as real files on the remote and GNU +# tar warns about every xattr header. Repack to remove both. repack: | if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4 + ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 else cat <&3 >&4 fi From 7291260c91f2e5df1015936cf527ce01c491ebfa Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 31 Jul 2026 18:24:38 +0100 Subject: [PATCH 02/10] fix: repack of a project with no top-level entries The dotglob glob is empty in that case and tar exits with 'no files or directories specified'. Packing '.' also drops the bash-ism. --- demo/spread.yaml | 2 +- inlined/bread-22.04.yaml | 2 +- inlined/bread-24.04.yaml | 2 +- inlined/bread-25.10.yaml | 2 +- inlined/bread-26.04.yaml | 2 +- inlined/bread-26.10.yaml | 2 +- inlined/bread-chisel-releases-22.04.yaml | 2 +- inlined/bread-chisel-releases-24.04.yaml | 2 +- inlined/bread-chisel-releases-25.10.yaml | 2 +- inlined/bread-chisel-releases-26.04.yaml | 2 +- inlined/bread-chisel-releases-26.10.yaml | 2 +- scripts/spread_repack_bread.sh | 2 +- tests/spread.yaml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/demo/spread.yaml b/demo/spread.yaml index 1022604..41d9ce6 100644 --- a/demo/spread.yaml +++ b/demo/spread.yaml @@ -20,7 +20,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml index 3126081..de99986 100644 --- a/inlined/bread-22.04.yaml +++ b/inlined/bread-22.04.yaml @@ -19,7 +19,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-24.04.yaml b/inlined/bread-24.04.yaml index f214446..67f2f1b 100644 --- a/inlined/bread-24.04.yaml +++ b/inlined/bread-24.04.yaml @@ -19,7 +19,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-25.10.yaml b/inlined/bread-25.10.yaml index b6ab48f..2960e86 100644 --- a/inlined/bread-25.10.yaml +++ b/inlined/bread-25.10.yaml @@ -19,7 +19,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-26.04.yaml b/inlined/bread-26.04.yaml index 79a04a5..576b3db 100644 --- a/inlined/bread-26.04.yaml +++ b/inlined/bread-26.04.yaml @@ -19,7 +19,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-26.10.yaml b/inlined/bread-26.10.yaml index f267045..4956bf6 100644 --- a/inlined/bread-26.10.yaml +++ b/inlined/bread-26.10.yaml @@ -19,7 +19,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml index 3f3afc2..ac26a46 100644 --- a/inlined/bread-chisel-releases-22.04.yaml +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -26,7 +26,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-24.04.yaml b/inlined/bread-chisel-releases-24.04.yaml index 21844bf..a8ce537 100644 --- a/inlined/bread-chisel-releases-24.04.yaml +++ b/inlined/bread-chisel-releases-24.04.yaml @@ -26,7 +26,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-25.10.yaml b/inlined/bread-chisel-releases-25.10.yaml index 148ebe2..b8bd27f 100644 --- a/inlined/bread-chisel-releases-25.10.yaml +++ b/inlined/bread-chisel-releases-25.10.yaml @@ -26,7 +26,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-26.04.yaml b/inlined/bread-chisel-releases-26.04.yaml index 7fa2252..c9efcbb 100644 --- a/inlined/bread-chisel-releases-26.04.yaml +++ b/inlined/bread-chisel-releases-26.04.yaml @@ -26,7 +26,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/inlined/bread-chisel-releases-26.10.yaml b/inlined/bread-chisel-releases-26.10.yaml index 3d08b4c..87ef6a4 100644 --- a/inlined/bread-chisel-releases-26.10.yaml +++ b/inlined/bread-chisel-releases-26.10.yaml @@ -26,7 +26,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/scripts/spread_repack_bread.sh b/scripts/spread_repack_bread.sh index e1070fa..c08c598 100644 --- a/scripts/spread_repack_bread.sh +++ b/scripts/spread_repack_bread.sh @@ -5,7 +5,7 @@ if [ "$(uname -s)" = Darwin ]; then tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi diff --git a/tests/spread.yaml b/tests/spread.yaml index 66e36aa..b9fc486 100644 --- a/tests/spread.yaml +++ b/tests/spread.yaml @@ -20,7 +20,7 @@ repack: | tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xf - -C "$tmp" <&3 - ( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - -- * ) >&4 + ( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4 else cat <&3 >&4 fi From 071641149d7ce9b46da8f3dbcd7c49cf8bc92c41 Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 31 Jul 2026 18:25:27 +0100 Subject: [PATCH 03/10] fix: keep the worker counter out of the project tree It was created in the cwd spread runs from, so it landed in the user's repo and got packed into the project tarball. --- demo/spread.yaml | 4 +++- inlined/bread-22.04.yaml | 4 +++- inlined/bread-24.04.yaml | 4 +++- inlined/bread-25.10.yaml | 4 +++- inlined/bread-26.04.yaml | 4 +++- inlined/bread-26.10.yaml | 4 +++- inlined/bread-chisel-releases-22.04.yaml | 4 +++- inlined/bread-chisel-releases-24.04.yaml | 4 +++- inlined/bread-chisel-releases-25.10.yaml | 4 +++- inlined/bread-chisel-releases-26.04.yaml | 4 +++- inlined/bread-chisel-releases-26.10.yaml | 4 +++- scripts/spread_allocate_bread-chisel-releases.sh | 4 +++- scripts/spread_allocate_bread.sh | 4 +++- tests/spread.yaml | 4 +++- 14 files changed, 42 insertions(+), 14 deletions(-) diff --git a/demo/spread.yaml b/demo/spread.yaml index 41d9ce6..ac6b14c 100644 --- a/demo/spread.yaml +++ b/demo/spread.yaml @@ -62,7 +62,9 @@ backends: # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up + # and shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml index de99986..8fd41b6 100644 --- a/inlined/bread-22.04.yaml +++ b/inlined/bread-22.04.yaml @@ -64,7 +64,9 @@ backends: # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-24.04.yaml b/inlined/bread-24.04.yaml index 67f2f1b..6fdfb9c 100644 --- a/inlined/bread-24.04.yaml +++ b/inlined/bread-24.04.yaml @@ -64,7 +64,9 @@ backends: # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-25.10.yaml b/inlined/bread-25.10.yaml index 2960e86..3b6fcfa 100644 --- a/inlined/bread-25.10.yaml +++ b/inlined/bread-25.10.yaml @@ -64,7 +64,9 @@ backends: # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-26.04.yaml b/inlined/bread-26.04.yaml index 576b3db..6c8741f 100644 --- a/inlined/bread-26.04.yaml +++ b/inlined/bread-26.04.yaml @@ -64,7 +64,9 @@ backends: # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-26.10.yaml b/inlined/bread-26.10.yaml index 4956bf6..e267ac1 100644 --- a/inlined/bread-26.10.yaml +++ b/inlined/bread-26.10.yaml @@ -64,7 +64,9 @@ backends: # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml index ac26a46..e93eb2b 100644 --- a/inlined/bread-chisel-releases-22.04.yaml +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -68,7 +68,9 @@ backends: if [ "$mode" = bridge ]; then sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-chisel-releases-24.04.yaml b/inlined/bread-chisel-releases-24.04.yaml index a8ce537..4f9840d 100644 --- a/inlined/bread-chisel-releases-24.04.yaml +++ b/inlined/bread-chisel-releases-24.04.yaml @@ -68,7 +68,9 @@ backends: if [ "$mode" = bridge ]; then sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-chisel-releases-25.10.yaml b/inlined/bread-chisel-releases-25.10.yaml index b8bd27f..17b5915 100644 --- a/inlined/bread-chisel-releases-25.10.yaml +++ b/inlined/bread-chisel-releases-25.10.yaml @@ -68,7 +68,9 @@ backends: if [ "$mode" = bridge ]; then sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-chisel-releases-26.04.yaml b/inlined/bread-chisel-releases-26.04.yaml index c9efcbb..aced024 100644 --- a/inlined/bread-chisel-releases-26.04.yaml +++ b/inlined/bread-chisel-releases-26.04.yaml @@ -68,7 +68,9 @@ backends: if [ "$mode" = bridge ]; then sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/inlined/bread-chisel-releases-26.10.yaml b/inlined/bread-chisel-releases-26.10.yaml index 87ef6a4..a37e39c 100644 --- a/inlined/bread-chisel-releases-26.10.yaml +++ b/inlined/bread-chisel-releases-26.10.yaml @@ -68,7 +68,9 @@ backends: if [ "$mode" = bridge ]; then sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/scripts/spread_allocate_bread-chisel-releases.sh b/scripts/spread_allocate_bread-chisel-releases.sh index 1a95d2a..850df79 100755 --- a/scripts/spread_allocate_bread-chisel-releases.sh +++ b/scripts/spread_allocate_bread-chisel-releases.sh @@ -31,7 +31,9 @@ echo "net mode: $mode" if [ "$mode" = bridge ]; then sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/scripts/spread_allocate_bread.sh b/scripts/spread_allocate_bread.sh index 7318ed6..6b2b459 100755 --- a/scripts/spread_allocate_bread.sh +++ b/scripts/spread_allocate_bread.sh @@ -34,7 +34,9 @@ if [ "$mode" = bridge ]; then # snippet thanks to @lengau # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 sleep 0.$RANDOM # Minimize chances of a race condition - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file diff --git a/tests/spread.yaml b/tests/spread.yaml index b9fc486..519f361 100644 --- a/tests/spread.yaml +++ b/tests/spread.yaml @@ -46,7 +46,9 @@ backends: # Unique container name per worker. sleep 0.$RANDOM - export counter_file=".spread-worker-num" + # Kept out of the project tree: whatever lives there is packed up and + # shipped to every container. + export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num" instance_num=$( flock -x $counter_file bash -c ' [ -s $counter_file ] || echo 0 > $counter_file From 9087da7c3b8f38a788cf9f195f16f45cdd0a2746 Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 31 Jul 2026 18:28:11 +0100 Subject: [PATCH 04/10] fix: bound the sshd readiness wait The unbounded loop kept polling a container that had already died, so the failure surfaced as spread's allocate timeout with nothing about why. --- demo/spread.yaml | 22 ++++++++++++++++++- inlined/bread-22.04.yaml | 22 ++++++++++++++++++- inlined/bread-24.04.yaml | 22 ++++++++++++++++++- inlined/bread-25.10.yaml | 22 ++++++++++++++++++- inlined/bread-26.04.yaml | 22 ++++++++++++++++++- inlined/bread-26.10.yaml | 22 ++++++++++++++++++- inlined/bread-chisel-releases-22.04.yaml | 22 ++++++++++++++++++- inlined/bread-chisel-releases-24.04.yaml | 22 ++++++++++++++++++- inlined/bread-chisel-releases-25.10.yaml | 22 ++++++++++++++++++- inlined/bread-chisel-releases-26.04.yaml | 22 ++++++++++++++++++- inlined/bread-chisel-releases-26.10.yaml | 22 ++++++++++++++++++- .../spread_allocate_bread-chisel-releases.sh | 22 ++++++++++++++++++- scripts/spread_allocate_bread.sh | 22 ++++++++++++++++++- tests/spread.yaml | 22 ++++++++++++++++++- 14 files changed, 294 insertions(+), 14 deletions(-) diff --git a/demo/spread.yaml b/demo/spread.yaml index ac6b14c..490dd5d 100644 --- a/demo/spread.yaml +++ b/demo/spread.yaml @@ -90,7 +90,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here + # until spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml index 8fd41b6..3693cf7 100644 --- a/inlined/bread-22.04.yaml +++ b/inlined/bread-22.04.yaml @@ -93,7 +93,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-24.04.yaml b/inlined/bread-24.04.yaml index 6fdfb9c..6bbbac3 100644 --- a/inlined/bread-24.04.yaml +++ b/inlined/bread-24.04.yaml @@ -93,7 +93,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-25.10.yaml b/inlined/bread-25.10.yaml index 3b6fcfa..7650bb2 100644 --- a/inlined/bread-25.10.yaml +++ b/inlined/bread-25.10.yaml @@ -93,7 +93,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-26.04.yaml b/inlined/bread-26.04.yaml index 6c8741f..bae26e6 100644 --- a/inlined/bread-26.04.yaml +++ b/inlined/bread-26.04.yaml @@ -93,7 +93,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-26.10.yaml b/inlined/bread-26.10.yaml index e267ac1..c4e6602 100644 --- a/inlined/bread-26.10.yaml +++ b/inlined/bread-26.10.yaml @@ -93,7 +93,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml index e93eb2b..4b6e3dc 100644 --- a/inlined/bread-chisel-releases-22.04.yaml +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -99,7 +99,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-chisel-releases-24.04.yaml b/inlined/bread-chisel-releases-24.04.yaml index 4f9840d..8cfb4a6 100644 --- a/inlined/bread-chisel-releases-24.04.yaml +++ b/inlined/bread-chisel-releases-24.04.yaml @@ -99,7 +99,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-chisel-releases-25.10.yaml b/inlined/bread-chisel-releases-25.10.yaml index 17b5915..e0b0f71 100644 --- a/inlined/bread-chisel-releases-25.10.yaml +++ b/inlined/bread-chisel-releases-25.10.yaml @@ -99,7 +99,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-chisel-releases-26.04.yaml b/inlined/bread-chisel-releases-26.04.yaml index aced024..5de817b 100644 --- a/inlined/bread-chisel-releases-26.04.yaml +++ b/inlined/bread-chisel-releases-26.04.yaml @@ -99,7 +99,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/inlined/bread-chisel-releases-26.10.yaml b/inlined/bread-chisel-releases-26.10.yaml index a37e39c..474c456 100644 --- a/inlined/bread-chisel-releases-26.10.yaml +++ b/inlined/bread-chisel-releases-26.10.yaml @@ -99,7 +99,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here until + # spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/scripts/spread_allocate_bread-chisel-releases.sh b/scripts/spread_allocate_bread-chisel-releases.sh index 850df79..b8bfbea 100755 --- a/scripts/spread_allocate_bread-chisel-releases.sh +++ b/scripts/spread_allocate_bread-chisel-releases.sh @@ -62,7 +62,27 @@ docker run \ --name "$container_name" \ -d "$image" -until docker exec "$container_name" pgrep sshd; do sleep 1; done +# sshd is up within a second or two. The bound and the liveness check are +# backstops: a container that dies on start would otherwise spin here until +# spread's own timeout, which reports nothing about the container. +sshd_up="" +for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 +done +if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 +fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/scripts/spread_allocate_bread.sh b/scripts/spread_allocate_bread.sh index 6b2b459..c08cbf3 100755 --- a/scripts/spread_allocate_bread.sh +++ b/scripts/spread_allocate_bread.sh @@ -63,7 +63,27 @@ docker run \ --name "$container_name" \ -d "$image" -until docker exec "$container_name" pgrep sshd; do sleep 1; done +# sshd is up within a second or two. The bound and the liveness check are +# backstops: a container that dies on start would otherwise spin here until +# spread's own timeout, which reports nothing about the container. +sshd_up="" +for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 +done +if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 +fi if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. diff --git a/tests/spread.yaml b/tests/spread.yaml index 519f361..b1d8df4 100644 --- a/tests/spread.yaml +++ b/tests/spread.yaml @@ -73,7 +73,27 @@ backends: --name "$container_name" \ -d "$image" - until docker exec "$container_name" pgrep sshd; do sleep 1; done + # sshd is up within a second or two. The bound and the liveness check are + # backstops: a container that dies on start would otherwise spin here + # until spread's own timeout, which reports nothing about the container. + sshd_up="" + for ((i = 0; i < 60; i++)); do + if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then + sshd_up=1 + break + fi + if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then + echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi + sleep 1 + done + if [ -z "$sshd_up" ]; then + echo "sshd did not come up in $container_name after 60s" >&2 + docker logs "$container_name" >&2 2>&1 || true + exit 1 + fi ADDRESS "$(docker inspect "$container_name" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}')" From 511bc522d19d10d86b25c98511afca407cc68bff Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 31 Jul 2026 18:31:04 +0100 Subject: [PATCH 05/10] refactor: look discard containers up by label The scan inspected every container on the host, twice each, on every discard. One docker call now covers the lookup, and the label keeps unrelated containers out of the match. --- demo/spread.yaml | 23 +++++++---------- inlined/bread-22.04.yaml | 25 ++++++++----------- inlined/bread-24.04.yaml | 25 ++++++++----------- inlined/bread-25.10.yaml | 25 ++++++++----------- inlined/bread-26.04.yaml | 25 ++++++++----------- inlined/bread-26.10.yaml | 25 ++++++++----------- inlined/bread-chisel-releases-22.04.yaml | 25 ++++++++----------- inlined/bread-chisel-releases-24.04.yaml | 25 ++++++++----------- inlined/bread-chisel-releases-25.10.yaml | 25 ++++++++----------- inlined/bread-chisel-releases-26.04.yaml | 25 ++++++++----------- inlined/bread-chisel-releases-26.10.yaml | 25 ++++++++----------- .../spread_allocate_bread-chisel-releases.sh | 1 + scripts/spread_allocate_bread.sh | 1 + .../spread_discard_bread-chisel-releases.sh | 24 +++++++----------- scripts/spread_discard_bread.sh | 24 +++++++----------- tests/spread.yaml | 14 +++++------ 16 files changed, 135 insertions(+), 202 deletions(-) diff --git a/demo/spread.yaml b/demo/spread.yaml index 490dd5d..96f0a49 100644 --- a/demo/spread.yaml +++ b/demo/spread.yaml @@ -87,6 +87,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -132,24 +133,18 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) + # Matched on the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml index 3693cf7..87dd3f5 100644 --- a/inlined/bread-22.04.yaml +++ b/inlined/bread-22.04.yaml @@ -90,6 +90,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -137,26 +138,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-24.04.yaml b/inlined/bread-24.04.yaml index 6bbbac3..d31d7e3 100644 --- a/inlined/bread-24.04.yaml +++ b/inlined/bread-24.04.yaml @@ -90,6 +90,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -137,26 +138,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-25.10.yaml b/inlined/bread-25.10.yaml index 7650bb2..716f818 100644 --- a/inlined/bread-25.10.yaml +++ b/inlined/bread-25.10.yaml @@ -90,6 +90,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -137,26 +138,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-26.04.yaml b/inlined/bread-26.04.yaml index bae26e6..7b19ab7 100644 --- a/inlined/bread-26.04.yaml +++ b/inlined/bread-26.04.yaml @@ -90,6 +90,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -137,26 +138,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-26.10.yaml b/inlined/bread-26.10.yaml index c4e6602..d80060e 100644 --- a/inlined/bread-26.10.yaml +++ b/inlined/bread-26.10.yaml @@ -90,6 +90,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -137,26 +138,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml index 4b6e3dc..d27430d 100644 --- a/inlined/bread-chisel-releases-22.04.yaml +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -96,6 +96,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -143,26 +144,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-chisel-releases-24.04.yaml b/inlined/bread-chisel-releases-24.04.yaml index 8cfb4a6..d21ce20 100644 --- a/inlined/bread-chisel-releases-24.04.yaml +++ b/inlined/bread-chisel-releases-24.04.yaml @@ -96,6 +96,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -143,26 +144,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-chisel-releases-25.10.yaml b/inlined/bread-chisel-releases-25.10.yaml index e0b0f71..f56f683 100644 --- a/inlined/bread-chisel-releases-25.10.yaml +++ b/inlined/bread-chisel-releases-25.10.yaml @@ -96,6 +96,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -143,26 +144,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-chisel-releases-26.04.yaml b/inlined/bread-chisel-releases-26.04.yaml index 5de817b..8bd4e5e 100644 --- a/inlined/bread-chisel-releases-26.04.yaml +++ b/inlined/bread-chisel-releases-26.04.yaml @@ -96,6 +96,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -143,26 +144,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/inlined/bread-chisel-releases-26.10.yaml b/inlined/bread-chisel-releases-26.10.yaml index 474c456..6adc33f 100644 --- a/inlined/bread-chisel-releases-26.10.yaml +++ b/inlined/bread-chisel-releases-26.10.yaml @@ -96,6 +96,7 @@ backends: -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -143,26 +144,20 @@ backends: container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/scripts/spread_allocate_bread-chisel-releases.sh b/scripts/spread_allocate_bread-chisel-releases.sh index b8bfbea..d483615 100755 --- a/scripts/spread_allocate_bread-chisel-releases.sh +++ b/scripts/spread_allocate_bread-chisel-releases.sh @@ -59,6 +59,7 @@ docker run \ -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" diff --git a/scripts/spread_allocate_bread.sh b/scripts/spread_allocate_bread.sh index c08cbf3..472e2a7 100755 --- a/scripts/spread_allocate_bread.sh +++ b/scripts/spread_allocate_bread.sh @@ -60,6 +60,7 @@ docker run \ -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ $publish_flag \ + --label spread-bread \ --name "$container_name" \ -d "$image" diff --git a/scripts/spread_discard_bread-chisel-releases.sh b/scripts/spread_discard_bread-chisel-releases.sh index e95e01f..6d3918d 100755 --- a/scripts/spread_discard_bread-chisel-releases.sh +++ b/scripts/spread_discard_bread-chisel-releases.sh @@ -11,26 +11,20 @@ echo "Discarding container for system: $SPREAD_SYSTEM_ADDRESS" container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/scripts/spread_discard_bread.sh b/scripts/spread_discard_bread.sh index e95e01f..6d3918d 100755 --- a/scripts/spread_discard_bread.sh +++ b/scripts/spread_discard_bread.sh @@ -11,26 +11,20 @@ echo "Discarding container for system: $SPREAD_SYSTEM_ADDRESS" container_name="" case "$SPREAD_SYSTEM_ADDRESS" in *:*) - # publish mode: match the container by its published host port. + # publish mode: match the container by its published host port, as it + # shows up in the ports column, e.g. "127.0.0.1:32768->22/tcp". target_port="${SPREAD_SYSTEM_ADDRESS##*:}" - for cid in $(docker ps -a --format '{{.ID}}'); do - if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then - container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') - break - fi - done + container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' | + awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }') not_found="No container found with published port: $target_port" ;; *) # bridge mode: match the container by its bridge IP address. - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" ;; esac diff --git a/tests/spread.yaml b/tests/spread.yaml index b1d8df4..3b7448b 100644 --- a/tests/spread.yaml +++ b/tests/spread.yaml @@ -70,6 +70,7 @@ backends: -e DEBIAN_FRONTEND=noninteractive \ -e "usr=$SPREAD_SYSTEM_USERNAME" \ -e "pass=$SPREAD_SYSTEM_PASSWORD" \ + --label spread-bread \ --name "$container_name" \ -d "$image" @@ -101,14 +102,11 @@ backends: set -e container_name="" - for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do - cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') - cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") - if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then - container_name="$cname" - break - fi - done + ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}') + if [ -n "$ids" ]; then + container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' | + awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }') + fi if [ -n "$container_name" ]; then echo "Removing outer container: $container_name" From 640a22791a3085dc0c2a2b6451f2954be192a15f Mon Sep 17 00:00:00 2001 From: lczyk Date: Sat, 1 Aug 2026 09:07:36 +0100 Subject: [PATCH 06/10] feat: bsdtar fallback for extraction Ubuntu 26.04's patched tar cannot extract anything below the top level under Rosetta, which spread reports as a failure to allocate the system. The shim probes once per container and only reroutes extraction; creation stays on GNU tar for --sort=name. --- hack/hash_inputs.sh | 1 + hack/tar-shim.sh | 67 +++++++++++++++++++++++++++++++++++ images/Dockerfile.bread-22.04 | 9 ++++- images/Dockerfile.bread-24.04 | 9 ++++- images/Dockerfile.bread-25.10 | 9 ++++- images/Dockerfile.bread-26.04 | 9 ++++- images/Dockerfile.bread-26.10 | 9 ++++- 7 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 hack/tar-shim.sh diff --git a/hack/hash_inputs.sh b/hack/hash_inputs.sh index 573a261..ad3edd4 100755 --- a/hack/hash_inputs.sh +++ b/hack/hash_inputs.sh @@ -39,6 +39,7 @@ case "$flavour" in "images/Dockerfile.bread-$ver" "hack/bread-warning.sh" "hack/banner.txt" + "hack/tar-shim.sh" ) ;; bread-chisel-releases) diff --git a/hack/tar-shim.sh b/hack/tar-shim.sh new file mode 100644 index 0000000..dbb18d7 --- /dev/null +++ b/hack/tar-shim.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# tar-shim: routes extraction to bsdtar on hosts where GNU tar cannot do it. +# +# Installed as /usr/bin/tar, with the real binary diverted to +# /usr/bin/tar.distrib. It has to sit on that path rather than earlier in +# PATH because spread invokes /bin/tar by absolute path. +# +# Ubuntu's patched tar (26.04 ships 1.35+dfsg-4ubuntu0.x) resolves extraction +# paths through a syscall Docker Desktop's Rosetta emulation does not +# implement, so in an amd64 container on Apple Silicon every entry below the +# top level fails with ENOSYS -- which spread reports as "cannot send project +# content", then as a failure to allocate the system. bsdtar is unaffected. +# +# Only extraction is routed: bsdtar has no --sort=name, which spread passes +# when packing artifacts. The probe runs once per container, so on unaffected +# hosts every call is plain GNU tar. + +_real=/usr/bin/tar.distrib +_checked=/run/bread-tar-checked +_broken=/run/bread-tar-broken + +_extracting() { + # Bare mode letters are only valid as the first argument; elsewhere an x + # is just as likely to be an option's value. + case "$1" in + (x*) return 0 ;; + esac + for _a in "$@"; do + case "$_a" in + (--) return 1 ;; + (--extract|--get) return 0 ;; + (--*) ;; + (-*x*) return 0 ;; + esac + done + return 1 +} + +_gnu_tar_broken() { + [ -e "$_broken" ] && return 0 + [ -e "$_checked" ] && return 1 + + _d=$(mktemp -d) || return 1 + _bad=0 + mkdir -p "$_d/a/b" && : > "$_d/a/b/f" \ + && "$_real" -cf "$_d/t.tar" -C "$_d" a && rm -rf "$_d/a" \ + && "$_real" -xf "$_d/t.tar" -C "$_d" 2>/dev/null && [ -f "$_d/a/b/f" ] \ + || _bad=1 + rm -rf "$_d" + + # Markers are a cache; a read-only /run just means every call probes. + : > "$_checked" 2>/dev/null + [ "$_bad" = 0 ] && return 1 + : > "$_broken" 2>/dev/null + return 0 +} + +if _extracting "$@" && command -v bsdtar >/dev/null 2>&1 && _gnu_tar_broken; then + # bsdtar has no bare mode letters, and spread sends with `tar xz`. + case "$1" in + (-*) ;; + (*) _mode="-$1"; shift; set -- "$_mode" "$@" ;; + esac + exec bsdtar --no-xattrs --no-mac-metadata "$@" +fi + +exec "$_real" "$@" diff --git a/images/Dockerfile.bread-22.04 b/images/Dockerfile.bread-22.04 index 15cf957..5194868 100644 --- a/images/Dockerfile.bread-22.04 +++ b/images/Dockerfile.bread-22.04 @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5ab RUN printf 'path-exclude /usr/share/man/*\npath-exclude /usr/share/doc/*\npath-exclude /usr/share/info/*\n' \ > /etc/dpkg/dpkg.cfg.d/01-nodoc && \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \ mkdir /var/run/sshd || true && \ echo 'root:bread' | chpasswd && \ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ @@ -33,5 +33,12 @@ COPY hack/bread-warning.sh /etc/profile.d/00-bread-warning.sh RUN chmod 0644 /etc/bread-banner.txt /etc/profile.d/00-bread-warning.sh && \ printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' >> /root/.bashrc +# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU +# tar. See hack/tar-shim.sh. +COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim +RUN chmod 0755 /usr/local/bin/bread-tar-shim && \ + dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \ + ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar + EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"] diff --git a/images/Dockerfile.bread-24.04 b/images/Dockerfile.bread-24.04 index b9b1804..0ef5c2b 100644 --- a/images/Dockerfile.bread-24.04 +++ b/images/Dockerfile.bread-24.04 @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0 RUN printf 'path-exclude /usr/share/man/*\npath-exclude /usr/share/doc/*\npath-exclude /usr/share/info/*\n' \ > /etc/dpkg/dpkg.cfg.d/01-nodoc && \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \ mkdir /var/run/sshd || true && \ echo 'root:bread' | chpasswd && \ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ @@ -33,5 +33,12 @@ COPY hack/bread-warning.sh /etc/profile.d/00-bread-warning.sh RUN chmod 0644 /etc/bread-banner.txt /etc/profile.d/00-bread-warning.sh && \ printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' >> /root/.bashrc +# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU +# tar. See hack/tar-shim.sh. +COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim +RUN chmod 0755 /usr/local/bin/bread-tar-shim && \ + dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \ + ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar + EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"] diff --git a/images/Dockerfile.bread-25.10 b/images/Dockerfile.bread-25.10 index 767eb82..8d5df43 100644 --- a/images/Dockerfile.bread-25.10 +++ b/images/Dockerfile.bread-25.10 @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:25.10@sha256:7cc5e35f6567ee8c66d2abb4aab0fd866669e RUN printf 'path-exclude /usr/share/man/*\npath-exclude /usr/share/doc/*\npath-exclude /usr/share/info/*\n' \ > /etc/dpkg/dpkg.cfg.d/01-nodoc && \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \ mkdir /var/run/sshd || true && \ echo 'root:bread' | chpasswd && \ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ @@ -33,5 +33,12 @@ COPY hack/bread-warning.sh /etc/profile.d/00-bread-warning.sh RUN chmod 0644 /etc/bread-banner.txt /etc/profile.d/00-bread-warning.sh && \ printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' >> /root/.bashrc +# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU +# tar. See hack/tar-shim.sh. +COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim +RUN chmod 0755 /usr/local/bin/bread-tar-shim && \ + dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \ + ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar + EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"] diff --git a/images/Dockerfile.bread-26.04 b/images/Dockerfile.bread-26.04 index 307eee7..59cab2b 100644 --- a/images/Dockerfile.bread-26.04 +++ b/images/Dockerfile.bread-26.04 @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:26.04@sha256:3131b4cc82a783df6c9df078f86e01819a135 RUN printf 'path-exclude /usr/share/man/*\npath-exclude /usr/share/doc/*\npath-exclude /usr/share/info/*\n' \ > /etc/dpkg/dpkg.cfg.d/01-nodoc && \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \ mkdir /var/run/sshd || true && \ echo 'root:bread' | chpasswd && \ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ @@ -33,5 +33,12 @@ COPY hack/bread-warning.sh /etc/profile.d/00-bread-warning.sh RUN chmod 0644 /etc/bread-banner.txt /etc/profile.d/00-bread-warning.sh && \ printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' >> /root/.bashrc +# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU +# tar. See hack/tar-shim.sh. +COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim +RUN chmod 0755 /usr/local/bin/bread-tar-shim && \ + dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \ + ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar + EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"] diff --git a/images/Dockerfile.bread-26.10 b/images/Dockerfile.bread-26.10 index 84f170a..2faf951 100644 --- a/images/Dockerfile.bread-26.10 +++ b/images/Dockerfile.bread-26.10 @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:26.10@sha256:694b773ee7e0d0b55ca74c095ac3309055589 RUN printf 'path-exclude /usr/share/man/*\npath-exclude /usr/share/doc/*\npath-exclude /usr/share/info/*\n' \ > /etc/dpkg/dpkg.cfg.d/01-nodoc && \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \ mkdir /var/run/sshd || true && \ echo 'root:bread' | chpasswd && \ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ @@ -33,5 +33,12 @@ COPY hack/bread-warning.sh /etc/profile.d/00-bread-warning.sh RUN chmod 0644 /etc/bread-banner.txt /etc/profile.d/00-bread-warning.sh && \ printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' >> /root/.bashrc +# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU +# tar. See hack/tar-shim.sh. +COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim +RUN chmod 0755 /usr/local/bin/bread-tar-shim && \ + dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \ + ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar + EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"] From 4fcf30cf28a77e16b01420ee74da38775d059fd5 Mon Sep 17 00:00:00 2001 From: lczyk Date: Sat, 1 Aug 2026 09:07:37 +0100 Subject: [PATCH 07/10] docs: tar shim and the rosetta breakage it works around --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index aa19df6..d50b10b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ BREAD_NET=publish spread # force port-publishing (e.g. to test the macOS path BREAD_NET=bridge spread # force bridge IPs ``` +### tar under emulation (amd64 on apple silicon) + +ubuntu 26.04's patched GNU tar (`1.35+dfsg-4ubuntu0.x`) resolves extraction paths through a syscall Docker Desktop's Rosetta emulation does not implement, so in an amd64 container on apple silicon every archive entry below the top level fails with `Function not implemented`. spread hits this when it unpacks the project, reports `cannot send project content`, and after three tries gives up with `Cannot allocate ... after too many retries`. 26.10 and everything at or below 25.10 are unaffected, as are the qemu-emulated arches. + +the images work around it: `/bin/tar` is a shim that probes GNU tar once per container and, if it is broken, routes *extraction* to `bsdtar` (see `hack/tar-shim.sh`). creation stays on GNU tar, which spread needs for `--sort=name` when it packs artifacts. on unaffected hosts the shim is inert. + ## install spread prefer a precompiled spread CLI over `go install`? same release ships statically-linked binaries for linux amd64 / arm64 / s390x / ppc64le: @@ -93,6 +99,7 @@ spread-bread/ hash_inputs.sh # per-image input hash (drives stamp invalidation) check_base.sh # detect upstream ubuntu base digest drift; rewrite @sha256 pins inline_scripts.rb # splice scripts/*.sh into yaml templates + tar-shim.sh # image /bin/tar; routes extraction to bsdtar where gnu tar is broken scripts/ # allocate / discard scripts, one pair per flavour images/ # one Dockerfile per (flavour, ubuntu version) templates/ # yaml templates with `source scripts/...` markers From 2c826541fdcb01fbf10e745b2907066f6fb1ce77 Mon Sep 17 00:00:00 2001 From: lczyk Date: Sat, 1 Aug 2026 09:13:18 +0100 Subject: [PATCH 08/10] feat: log the bsdtar fallback at allocation The shim swapping tar out from under the container was invisible; allocate now probes it (warming the cache too) and says so. --- demo/spread.yaml | 11 +++++++++++ hack/tar-shim.sh | 12 ++++++++++++ inlined/bread-22.04.yaml | 11 +++++++++++ inlined/bread-24.04.yaml | 11 +++++++++++ inlined/bread-25.10.yaml | 11 +++++++++++ inlined/bread-26.04.yaml | 11 +++++++++++ inlined/bread-26.10.yaml | 11 +++++++++++ inlined/bread-chisel-releases-22.04.yaml | 11 +++++++++++ inlined/bread-chisel-releases-24.04.yaml | 11 +++++++++++ inlined/bread-chisel-releases-25.10.yaml | 11 +++++++++++ inlined/bread-chisel-releases-26.04.yaml | 11 +++++++++++ inlined/bread-chisel-releases-26.10.yaml | 11 +++++++++++ scripts/spread_allocate_bread-chisel-releases.sh | 11 +++++++++++ scripts/spread_allocate_bread.sh | 11 +++++++++++ 14 files changed, 155 insertions(+) diff --git a/demo/spread.yaml b/demo/spread.yaml index 96f0a49..ef3b491 100644 --- a/demo/spread.yaml +++ b/demo/spread.yaml @@ -113,6 +113,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) [ -n "$port" ] || { echo "could not find published sshd port for $container_name" >&2; exit 1; } diff --git a/hack/tar-shim.sh b/hack/tar-shim.sh index dbb18d7..e7815f2 100644 --- a/hack/tar-shim.sh +++ b/hack/tar-shim.sh @@ -55,6 +55,18 @@ _gnu_tar_broken() { return 0 } +# Reports which backend extraction would use, and warms the probe cache. The +# allocate scripts call it so the fallback shows up in the spread log instead +# of happening silently. +if [ "$1" = --bread-probe ]; then + if command -v bsdtar >/dev/null 2>&1 && _gnu_tar_broken; then + echo bsdtar + else + echo gnu + fi + exit 0 +fi + if _extracting "$@" && command -v bsdtar >/dev/null 2>&1 && _gnu_tar_broken; then # bsdtar has no bare mode letters, and spread sends with `tar xz`. case "$1" in diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml index 87dd3f5..3708741 100644 --- a/inlined/bread-22.04.yaml +++ b/inlined/bread-22.04.yaml @@ -116,6 +116,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-24.04.yaml b/inlined/bread-24.04.yaml index d31d7e3..89955f2 100644 --- a/inlined/bread-24.04.yaml +++ b/inlined/bread-24.04.yaml @@ -116,6 +116,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-25.10.yaml b/inlined/bread-25.10.yaml index 716f818..dfcff8e 100644 --- a/inlined/bread-25.10.yaml +++ b/inlined/bread-25.10.yaml @@ -116,6 +116,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-26.04.yaml b/inlined/bread-26.04.yaml index 7b19ab7..715bc9c 100644 --- a/inlined/bread-26.04.yaml +++ b/inlined/bread-26.04.yaml @@ -116,6 +116,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-26.10.yaml b/inlined/bread-26.10.yaml index d80060e..058a0cb 100644 --- a/inlined/bread-26.10.yaml +++ b/inlined/bread-26.10.yaml @@ -116,6 +116,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml index d27430d..5839216 100644 --- a/inlined/bread-chisel-releases-22.04.yaml +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -122,6 +122,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-chisel-releases-24.04.yaml b/inlined/bread-chisel-releases-24.04.yaml index d21ce20..50093da 100644 --- a/inlined/bread-chisel-releases-24.04.yaml +++ b/inlined/bread-chisel-releases-24.04.yaml @@ -122,6 +122,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-chisel-releases-25.10.yaml b/inlined/bread-chisel-releases-25.10.yaml index f56f683..bf0d250 100644 --- a/inlined/bread-chisel-releases-25.10.yaml +++ b/inlined/bread-chisel-releases-25.10.yaml @@ -122,6 +122,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-chisel-releases-26.04.yaml b/inlined/bread-chisel-releases-26.04.yaml index 8bd4e5e..d95124e 100644 --- a/inlined/bread-chisel-releases-26.04.yaml +++ b/inlined/bread-chisel-releases-26.04.yaml @@ -122,6 +122,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/inlined/bread-chisel-releases-26.10.yaml b/inlined/bread-chisel-releases-26.10.yaml index 6adc33f..89f64a5 100644 --- a/inlined/bread-chisel-releases-26.10.yaml +++ b/inlined/bread-chisel-releases-26.10.yaml @@ -122,6 +122,17 @@ backends: exit 1 fi + # A container whose gnu tar cannot extract falls back to bsdtar (see + # hack/tar-shim.sh); say so rather than swapping the tool silently. + tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) + if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true + fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/scripts/spread_allocate_bread-chisel-releases.sh b/scripts/spread_allocate_bread-chisel-releases.sh index d483615..80d4999 100755 --- a/scripts/spread_allocate_bread-chisel-releases.sh +++ b/scripts/spread_allocate_bread-chisel-releases.sh @@ -85,6 +85,17 @@ if [ -z "$sshd_up" ]; then exit 1 fi +# A container whose gnu tar cannot extract falls back to bsdtar (see +# hack/tar-shim.sh); say so rather than swapping the tool silently. +tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) +if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true +fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) diff --git a/scripts/spread_allocate_bread.sh b/scripts/spread_allocate_bread.sh index 472e2a7..306590f 100755 --- a/scripts/spread_allocate_bread.sh +++ b/scripts/spread_allocate_bread.sh @@ -86,6 +86,17 @@ if [ -z "$sshd_up" ]; then exit 1 fi +# A container whose gnu tar cannot extract falls back to bsdtar (see +# hack/tar-shim.sh); say so rather than swapping the tool silently. +tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown) +if [ "$tar_backend" = bsdtar ]; then + note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar" + # spread buffers allocate output and only prints it on failure, so the + # terminal (when there is one) is the only channel a user actually reads. + echo "$note" + { [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true +fi + if [ "$mode" = publish ]; then # The ephemeral host port docker mapped to the container's sshd. port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) From 7d6f213d2ae54bbde328eadcb317231890a598af Mon Sep 17 00:00:00 2001 From: lczyk Date: Sat, 1 Aug 2026 09:15:03 +0100 Subject: [PATCH 09/10] test: tar shim contract in the inner suites --- .../tar-shim/task.yaml | 55 +++++++++++++++++++ tests/_inner-bread/tar-shim/task.yaml | 55 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 tests/_inner-bread-chisel-releases/tar-shim/task.yaml create mode 100644 tests/_inner-bread/tar-shim/task.yaml diff --git a/tests/_inner-bread-chisel-releases/tar-shim/task.yaml b/tests/_inner-bread-chisel-releases/tar-shim/task.yaml new file mode 100644 index 0000000..8c40255 --- /dev/null +++ b/tests/_inner-bread-chisel-releases/tar-shim/task.yaml @@ -0,0 +1,55 @@ +summary: tar shim contract (gnu tar diverted, every spread tar invocation works) + +execute: | + set -e + + test -x /usr/bin/tar.distrib || { + echo "gnu tar not diverted to /usr/bin/tar.distrib" + exit 1 + } + test -L /usr/bin/tar || { + echo "/usr/bin/tar is not the shim symlink" + exit 1 + } + bsdtar --version >/dev/null || { + echo "bsdtar missing" + exit 1 + } + /bin/tar --version | grep -q "GNU tar" || { + echo "tar --version not passed through to gnu tar" + exit 1 + } + + work=$(mktemp -d) + trap "rm -rf $work" EXIT + mkdir -p "$work/src/a/b" + : > "$work/src/a/b/f" + + # Nested entries are what breaks under a host whose emulation the patched + # gnu tar trips over. + /bin/tar -cf "$work/t.tar" -C "$work/src" a + mkdir "$work/out" + /bin/tar -xf "$work/t.tar" -C "$work/out" + test -f "$work/out/a/b/f" || { + echo "nested extraction through /bin/tar failed" + exit 1 + } + + # spread unpacks the project with bare mode letters (`tar xz`), which bsdtar + # does not accept unprefixed. + /bin/tar -C "$work/src" -czf "$work/t.tgz" a + mkdir "$work/out2" + ( cd "$work/out2" && /bin/tar xz < "$work/t.tgz" ) + test -f "$work/out2/a/b/f" || { + echo "bare-mode extraction failed" + exit 1 + } + + # ... and packs artifacts with flags only gnu tar has, so creation must never + # be routed to bsdtar. + /bin/tar -C "$work/src" -cz --sort=name --ignore-failed-read -- a >/dev/null || { + echo "gnu-only create flags rejected; shim misrouted creation" + exit 1 + } + + echo "ok: $SPREAD_SYSTEM ($(/usr/local/bin/bread-tar-shim --bread-probe))" diff --git a/tests/_inner-bread/tar-shim/task.yaml b/tests/_inner-bread/tar-shim/task.yaml new file mode 100644 index 0000000..8c40255 --- /dev/null +++ b/tests/_inner-bread/tar-shim/task.yaml @@ -0,0 +1,55 @@ +summary: tar shim contract (gnu tar diverted, every spread tar invocation works) + +execute: | + set -e + + test -x /usr/bin/tar.distrib || { + echo "gnu tar not diverted to /usr/bin/tar.distrib" + exit 1 + } + test -L /usr/bin/tar || { + echo "/usr/bin/tar is not the shim symlink" + exit 1 + } + bsdtar --version >/dev/null || { + echo "bsdtar missing" + exit 1 + } + /bin/tar --version | grep -q "GNU tar" || { + echo "tar --version not passed through to gnu tar" + exit 1 + } + + work=$(mktemp -d) + trap "rm -rf $work" EXIT + mkdir -p "$work/src/a/b" + : > "$work/src/a/b/f" + + # Nested entries are what breaks under a host whose emulation the patched + # gnu tar trips over. + /bin/tar -cf "$work/t.tar" -C "$work/src" a + mkdir "$work/out" + /bin/tar -xf "$work/t.tar" -C "$work/out" + test -f "$work/out/a/b/f" || { + echo "nested extraction through /bin/tar failed" + exit 1 + } + + # spread unpacks the project with bare mode letters (`tar xz`), which bsdtar + # does not accept unprefixed. + /bin/tar -C "$work/src" -czf "$work/t.tgz" a + mkdir "$work/out2" + ( cd "$work/out2" && /bin/tar xz < "$work/t.tgz" ) + test -f "$work/out2/a/b/f" || { + echo "bare-mode extraction failed" + exit 1 + } + + # ... and packs artifacts with flags only gnu tar has, so creation must never + # be routed to bsdtar. + /bin/tar -C "$work/src" -cz --sort=name --ignore-failed-read -- a >/dev/null || { + echo "gnu-only create flags rejected; shim misrouted creation" + exit 1 + } + + echo "ok: $SPREAD_SYSTEM ($(/usr/local/bin/bread-tar-shim --bread-probe))" From dec5e8cf78b809ed4be85db5c2e361a68368791d Mon Sep 17 00:00:00 2001 From: lczyk Date: Sat, 1 Aug 2026 09:15:48 +0100 Subject: [PATCH 10/10] fix: nuke-spread matching unrelated containers A grep for 'bread' in docker ps also matched containers this project never created. Match the allocate label, plus the bread- name prefix for containers allocated before the label existed. --- makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index a70c018..4570a58 100644 --- a/makefile +++ b/makefile @@ -156,7 +156,8 @@ shell: .stamp/bread-26.04-$(SELECTED_ARCH) ## Drop into a bread:26.04 shell (ho .PHONY: nuke-spread nuke-spread: ## Kill stray spread processes + force-remove bread containers -pkill spread - -$(DOCKER) ps | grep bread | cut -d' ' -f1 | xargs -r $(DOCKER) rm --force + -$(DOCKER) ps -aq --filter label=spread-bread | xargs -r $(DOCKER) rm --force + -$(DOCKER) ps -aq --filter name='^bread-' | xargs -r $(DOCKER) rm --force .PHONY: clean clean: ## Remove built images, stamps, generated inlined yamls, cached binaries