Skip to content

Commit 6ded0a4

Browse files
authored
Merge pull request #188 from infosiftr/reproducible-rootfs
Adjust tarball creation to be reproducible
2 parents e704abc + 7e39d61 commit 6ded0a4

File tree

64 files changed

+751
-73
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+751
-73
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
run: |
3636
strategy="$(GENERATE_STACKBREW_LIBRARY='.github/workflows/fake-gsl.sh' "$BASHBREW_SCRIPTS/github-actions/generate.sh")"
3737
strategy="$(.github/workflows/munge-build.sh -c <<<"$strategy")"
38-
strategy="$(.github/workflows/munge-debian-unstable.sh -c <<<"$strategy")"
38+
strategy="$(.github/workflows/munge-unstable.sh -c <<<"$strategy")"
3939
4040
EOF="EOF-$RANDOM-$RANDOM-$RANDOM"
4141
echo "strategy<<$EOF" >> "$GITHUB_OUTPUT"
@@ -47,8 +47,11 @@ jobs:
4747
strategy: ${{ fromJson(needs.generate-jobs.outputs.strategy) }}
4848
name: ${{ matrix.name }}
4949
runs-on: ${{ matrix.os }}
50+
env:
51+
BASHBREW_ARCH: amd64 # TODO consider using "$BASHBREW_SCRIPTS/bashbrew-host-arch.sh" ? (would make it harder to force i386 in our matrix too, so explicit is probably better)
5052
steps:
5153
- uses: actions/checkout@v3
54+
- uses: docker-library/bashbrew@HEAD # build.sh needs bashbrew
5255
- name: Prepare Environment
5356
run: ${{ matrix.runs.prepare }}
5457
- name: Pull Dependencies
@@ -61,3 +64,11 @@ jobs:
6164
run: ${{ matrix.runs.test }}
6265
- name: '"docker images"'
6366
run: ${{ matrix.runs.images }}
67+
- name: Git Diff # see "munge-build.sh"
68+
run: |
69+
if git diff --exit-code */*/Dockerfile.builder; then # see "hack-unstable.sh" (and "munge-unstable.sh")
70+
git diff --exit-code
71+
else
72+
# for unstable builds, let's leave this in but purely informational (instead of causing CI to fail)
73+
git diff
74+
fi

.github/workflows/munge-build.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ set -Eeuo pipefail
33

44
jq '
55
.matrix.include |= map(
6-
.runs.build = "./build.sh " + (.meta.entries[].directory | @sh) + "\n" + (.runs.build | sub(" --file [^ ]+ "; " "))
6+
.runs.build = (
7+
[
8+
"dir=" + (.meta.entries[].directory | @sh),
9+
"rm -rf \"$dir/$BASHBREW_ARCH\"", # make sure our OCI directory is clean so we can "git diff --exit-code" later
10+
"./build.sh \"$dir\"",
11+
(.runs.build | sub(" --file [^ ]+ "; " ")),
12+
empty
13+
] | join("\n")
14+
)
715
)
816
' "$@"

.github/workflows/munge-debian-unstable.sh

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
set -Eeuo pipefail
3+
4+
# see also "hack-unstable.sh"
5+
jq '
6+
.matrix.include += [
7+
.matrix.include[]
8+
| select(.name | test(" [(].+[)]") | not) # ignore any existing munged builds
9+
| select(.os | startswith("windows-") | not)
10+
| select(.meta.froms | any(startswith("debian:") or startswith("alpine:")))
11+
| .name += " (unstable)"
12+
| .runs.prepare += ([
13+
"./hack-unstable.sh " + (.meta.entries[].directory | @sh),
14+
"if git diff --exit-code; then exit 1; fi", # trust, but verify (if hack-unstable did not modify anything, we want to bail quickly)
15+
empty
16+
] | map("\n" + .) | add)
17+
| .runs.pull = "" # pulling images does not make sense here (we just changed them)
18+
]
19+
' "$@"

Dockerfile-builder.template

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ RUN set -eux; \
1313
musl-dev \
1414
patch \
1515
tzdata \
16+
# busybox's tar ironically does not maintain mtime of directories correctly (which we need for SOURCE_DATE_EPOCH / reproducibility)
17+
tar \
1618
;
1719
{{ ) else ( -}}
1820
FROM debian:bookworm-slim
@@ -234,22 +236,36 @@ RUN set -eux; \
234236
curl -fL -o busybox.tar.bz2 "https://busybox.net/downloads/$tarball"; \
235237
echo "$BUSYBOX_SHA256 *busybox.tar.bz2" | sha256sum -c -; \
236238
gpg --batch --verify busybox.tar.bz2.sig busybox.tar.bz2; \
237-
mkdir -p /usr/src/busybox; \
238-
tar -xf busybox.tar.bz2 -C /usr/src/busybox --strip-components 1; \
239-
rm busybox.tar.bz2*
239+
# Alpine... 😅
240+
mkdir -p /usr/src; \
241+
tar -xf busybox.tar.bz2 -C /usr/src "busybox-$BUSYBOX_VERSION"; \
242+
mv "/usr/src/busybox-$BUSYBOX_VERSION" /usr/src/busybox; \
243+
rm busybox.tar.bz2*; \
244+
\
245+
# save the tarball's filesystem timestamp persistently (in case building busybox modifies it) so we can use it for reproducible rootfs later
246+
SOURCE_DATE_EPOCH="$(stat -c '%Y' /usr/src/busybox | tee /usr/src/busybox.SOURCE_DATE_EPOCH)"; \
247+
date="$(date -d "@$SOURCE_DATE_EPOCH" '+%Y%m%d%H%M.%S')"; \
248+
touch -t "$date" /usr/src/busybox.SOURCE_DATE_EPOCH; \
249+
# for logging validation/edification
250+
date --date "@$SOURCE_DATE_EPOCH" --rfc-2822
240251

241252
WORKDIR /usr/src/busybox
242253

243254
RUN set -eux; \
244255
\
256+
# build date/time gets embedded in the BusyBox binary -- SOURCE_DATE_EPOCH should override that
257+
SOURCE_DATE_EPOCH="$(cat /usr/src/busybox.SOURCE_DATE_EPOCH)"; \
258+
export SOURCE_DATE_EPOCH; \
259+
# (has to be set in the config stage for making sure "AUTOCONF_TIMESTAMP" is embedded correctly)
260+
\
245261
setConfs=' \
246262
CONFIG_AR=y \
247263
CONFIG_FEATURE_AR_CREATE=y \
248264
CONFIG_FEATURE_AR_LONG_FILENAMES=y \
249265
# CONFIG_LAST_SUPPORTED_WCHAR: see https://github.com/docker-library/busybox/issues/13 (UTF-8 input)
250266
CONFIG_LAST_SUPPORTED_WCHAR=0 \
251267
{{ if env.variant == "glibc" then ( -}}
252-
# As long as we rely on libnss (see below), we have to have libc.so anyhow, so we've removed CONFIG_STATIC here... :cry:
268+
# As long as we rely on libnss (see below), we have to have libc.so anyhow, so we've removed CONFIG_STATIC here... 😭
253269
{{ ) else ( -}}
254270
CONFIG_STATIC=y \
255271
{{ ) end -}}
@@ -352,15 +368,17 @@ RUN set -eux; \
352368
done; \
353369
{{ ) elif env.variant == "musl" then ( -}}
354370
# copy simplified getconf port from Alpine
355-
aportsVersion="v$(cat /etc/alpine-release)"; \
371+
# https://github.com/alpinelinux/aports/commits/HEAD/main/musl/getconf.c
356372
curl -fsSL \
357-
"https://github.com/alpinelinux/aports/raw/$aportsVersion/main/musl/getconf.c" \
373+
"https://github.com/alpinelinux/aports/raw/48b16204aeeda5bc1f87e49c6b8e23d9abb07c73/main/musl/getconf.c" \
358374
-o /usr/src/getconf.c \
359375
; \
376+
echo 'd87d0cbb3690ae2c5d8cc218349fd8278b93855dd625deaf7ae50e320aad247c */usr/src/getconf.c' | sha256sum -c -; \
360377
gcc -o rootfs/bin/getconf -static -Os /usr/src/getconf.c; \
361378
{{ ) else "" end -}}
362379
chroot rootfs /bin/getconf _NPROCESSORS_ONLN; \
363380
\
381+
# TODO make this create symlinks instead so the output tarball is cleaner (but "-s" outputs absolute symlinks which is kind of annoying to deal with -- we should also consider letting busybox determine the "install paths"; see "busybox --list-full")
364382
chroot rootfs /bin/busybox --install /bin
365383
366384
# install a few extra files from buildroot (/etc/passwd, etc)

Dockerfile.template

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# this isn't used for the official published images anymore, but is included for backwards compatibility
2+
# see https://github.com/docker-library/bashbrew/issues/51
13
FROM scratch
2-
ADD busybox.tar.xz /
4+
ADD busybox.tar.gz /
35
CMD ["sh"]

apply-templates.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ for version; do
3535
variants="$(jq -r '.[env.version].variants | map(@sh) | join(" ")' versions.json)"
3636
eval "variants=( $variants )"
3737

38+
# TODO somehow make sure this deletes any content we're not generating (without accidentally deleting potentialy generated tarballs for the things we *do* care about)
39+
3840
for variant in "${variants[@]}"; do
3941
export variant
4042

4143
echo "processing $version/$variant ..."
4244

45+
mkdir -p "$version/$variant"
46+
4347
{
4448
generated_warning
4549
gawk -f "$jqt" Dockerfile-builder.template

build.sh

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,131 @@ if [ "$#" -eq 0 ]; then
88
eval "set -- $dirs"
99
fi
1010

11+
[ -n "$BASHBREW_ARCH" ]
12+
platformString="$(bashbrew cat --format '{{ ociPlatform arch }}' <(echo 'Maintainers: empty hack (@example)'))"
13+
platform="$(bashbrew cat --format '{{ ociPlatform arch | json }}' <(echo 'Maintainers: empty hack (@example)'))"
14+
1115
for dir; do
12-
base="busybox:${dir////-}"
16+
variant="$(basename "$dir")"
17+
base="busybox:${dir////-}-$BASHBREW_ARCH"
18+
19+
froms="$(awk 'toupper($1) == "FROM" { print $2 }' "$dir/Dockerfile.builder")"
20+
for from in "$froms"; do
21+
if ! bashbrew remote arches --json "$from" | jq -e '.arches | has(env.BASHBREW_ARCH)' > /dev/null; then
22+
echo >&2 "warning: '$base' is 'FROM $from' which does not support '$BASHBREW_ARCH'; skipping"
23+
continue 2
24+
fi
25+
done
26+
1327
(
1428
set -x
15-
docker build -t "$base-builder" -f "$dir/Dockerfile.builder" "$dir"
16-
docker run --rm "$base-builder" tar cC rootfs . | xz -T0 -z9 > "$dir/busybox.tar.xz"
29+
30+
# TODO save the output of "bashbrew remote arches" above so we can "--build-context" here?
31+
docker buildx build \
32+
--progress=plain \
33+
--platform "$platformString" \
34+
--pull \
35+
--load \
36+
--tag "$base-builder" \
37+
--file "$dir/Dockerfile.builder" \
38+
"$dir"
39+
40+
oci="$dir/$BASHBREW_ARCH"
41+
rm -rf "$oci"
42+
mkdir "$oci" "$oci/blobs" "$oci/blobs/sha256"
43+
44+
docker run --rm "$base-builder" \
45+
tar \
46+
--create \
47+
--directory rootfs \
48+
--numeric-owner \
49+
--transform 's,^./,,' \
50+
--sort name \
51+
--mtime /usr/src/busybox.SOURCE_DATE_EPOCH --clamp-mtime \
52+
. \
53+
> "$oci/rootfs.tar"
54+
55+
# if we gzip separately, we can calculate the diffid without decompressing
56+
diffId="$(sha256sum "$oci/rootfs.tar" | cut -d' ' -f1)"
57+
diffId="sha256:$diffId"
58+
59+
# we need to use the container's gzip so it's more likely reproducible over time (and using busybox's own gzip is a cute touch 😀)
60+
docker run -i --rm "$base-builder" chroot rootfs gzip -c < "$oci/rootfs.tar" > "$oci/rootfs.tar.gz"
61+
rm "$oci/rootfs.tar"
62+
rootfs="$(sha256sum "$oci/rootfs.tar.gz" | cut -d' ' -f1)"
63+
ln -svfT --relative "$oci/rootfs.tar.gz" "$oci/blobs/sha256/$rootfs"
64+
rootfsSize="$(stat --format '%s' --dereference "$oci/blobs/sha256/$rootfs")"
65+
rootfs="sha256:$rootfs"
66+
67+
SOURCE_DATE_EPOCH="$(docker run --rm "$base-builder" cat /usr/src/busybox.SOURCE_DATE_EPOCH)"
68+
createdBy="$(docker run --rm --env variant="$variant" "$base-builder" sh -euc '. /etc/os-release && echo "BusyBox $BUSYBOX_VERSION ($variant)${BUILDROOT_VERSION:+, Buildroot $BUILDROOT_VERSION}, ${NAME%% *} ${VERSION_ID:-$VERSION_CODENAME}"')"
69+
jq -n --tab --arg SOURCE_DATE_EPOCH "$SOURCE_DATE_EPOCH" --arg diffId "$diffId" --arg createdBy "$createdBy" --argjson platform "$platform" '
70+
($SOURCE_DATE_EPOCH | tonumber | strftime("%Y-%m-%dT%H:%M:%SZ")) as $created
71+
| {
72+
config: {
73+
Cmd: [ "sh" ],
74+
},
75+
created: $created,
76+
history: [ {
77+
created: $created,
78+
created_by: $createdBy,
79+
} ],
80+
rootfs: {
81+
type: "layers",
82+
diff_ids: [ $diffId ],
83+
},
84+
} + $platform
85+
' > "$oci/image-config.json"
86+
config="$(sha256sum "$oci/image-config.json" | cut -d' ' -f1)"
87+
ln -svfT --relative "$oci/image-config.json" "$oci/blobs/sha256/$config"
88+
configSize="$(stat --format '%s' --dereference "$oci/blobs/sha256/$config")"
89+
config="sha256:$config"
90+
91+
version="$(cut <<<"$createdBy" -d' ' -f2)" # a better way to scrape the BusyBox version? maybe this is fine (want to avoid yet another container run)
92+
jq -n --tab --arg version "$version" --arg variant "$variant" --arg config "$config" --arg configSize "$configSize" --arg rootfs "$rootfs" --arg rootfsSize "$rootfsSize" '
93+
{
94+
schemaVersion: 2,
95+
mediaType: "application/vnd.oci.image.manifest.v1+json",
96+
config: {
97+
mediaType: "application/vnd.oci.image.config.v1+json",
98+
digest: $config,
99+
size: ($configSize | tonumber),
100+
},
101+
layers: [ {
102+
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
103+
digest: $rootfs,
104+
size: ($rootfsSize | tonumber),
105+
} ],
106+
annotations: {
107+
"org.opencontainers.image.url": "https://github.com/docker-library/busybox",
108+
"org.opencontainers.image.version": ($version + "-" + $variant),
109+
},
110+
}
111+
' > "$oci/image-manifest.json"
112+
manifest="$(sha256sum "$oci/image-manifest.json" | cut -d' ' -f1)"
113+
ln -svfT --relative "$oci/image-manifest.json" "$oci/blobs/sha256/$manifest"
114+
manifestSize="$(stat --format '%s' --dereference "$oci/blobs/sha256/$manifest")"
115+
manifest="sha256:$manifest"
116+
117+
jq -nc '{ imageLayoutVersion:"1.0.0" }' > "$oci/oci-layout"
118+
jq -n --tab --arg version "$version" --arg variant "$variant" --arg manifest "$manifest" --arg manifestSize "$manifestSize" --argjson platform "$platform" '
119+
{
120+
schemaVersion: 2,
121+
mediaType: "application/vnd.oci.image.index.v1+json",
122+
manifests: [ {
123+
mediaType: "application/vnd.oci.image.manifest.v1+json",
124+
digest: $manifest,
125+
size: ($manifestSize | tonumber),
126+
platform: $platform,
127+
annotations: {
128+
"org.opencontainers.image.ref.name": ("busybox:" + $version + "-" + $variant),
129+
"io.containerd.image.name": ("busybox:" + $version + "-" + $variant),
130+
},
131+
} ],
132+
}
133+
' > "$oci/index.json"
134+
135+
ln -svfT --relative "$oci/rootfs.tar.gz" "$dir/busybox.tar.gz"
17136
docker build -t "$base-test" "$dir"
18137
docker run --rm "$base-test" sh -xec 'true'
19138

generate-stackbrew-library.sh

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Maintainers: Tianon Gravi <admwiggin@gmail.com> (@tianon),
3434
Joseph Ferguson <yosifkit@gmail.com> (@yosifkit)
3535
GitRepo: $gitHubUrl.git
3636
GitCommit: $selfCommit
37+
Builder: oci-import
38+
File: index.json
3739
EOH
3840
for arch in "${arches[@]}"; do
3941
commit="${archCommits[$arch]}"
@@ -97,6 +99,7 @@ for version; do
9799
fi
98100
versionAliases+=( latest )
99101

102+
actualArches=()
100103
declare -A archLatestDir=()
101104
for variant in "${variants[@]}"; do
102105
dir="$version/$variant"
@@ -107,25 +110,31 @@ for version; do
107110
variantArches=()
108111
for arch in "${arches[@]}"; do
109112
archCommit="${archCommits[$arch]}"
110-
if wget --quiet --spider -O /dev/null -o /dev/null "$rawGitUrl/$archCommit/$dir/busybox.tar.xz"; then
113+
if wget --quiet --spider -O /dev/null -o /dev/null "$rawGitUrl/$archCommit/$dir/$arch/rootfs.tar.gz"; then
111114
variantArches+=( "$arch" )
112-
: "${archLatestDir[$arch]:=$dir}" # record the first supported directory per architecture for "latest" and friends
115+
if [ -z "${archLatestDir[$arch]:-}" ]; then
116+
# record the first supported directory per architecture for "latest" and friends
117+
archLatestDir["$arch"]="$dir/$arch"
118+
actualArches+=( "$arch" )
119+
fi
113120
fi
114121
done
115122

116-
if _tags "${variantAliases[@]}"; then
123+
if [ "${#variantArches[@]}" -gt 0 ] && _tags "${variantAliases[@]}"; then
117124
cat <<-EOE
118125
Architectures: $(join ', ' "${variantArches[@]}")
119-
Directory: $dir
120126
EOE
127+
for arch in "${variantArches[@]}"; do
128+
echo "$arch-Directory: $dir/$arch"
129+
done
121130
fi
122131
done
123132

124-
if _tags "${versionAliases[@]}"; then
133+
if [ "${#actualArches[@]}" -gt 0 ] && _tags "${versionAliases[@]}"; then
125134
cat <<-EOE
126-
Architectures: $(join ', ' "${arches[@]}")
135+
Architectures: $(join ', ' "${actualArches[@]}")
127136
EOE
128-
for arch in "${arches[@]}"; do
137+
for arch in "${actualArches[@]}"; do
129138
archDir="${archLatestDir[$arch]}"
130139
cat <<-EOA
131140
${arch}-Directory: $archDir

hack-unstable.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
set -Eeuo pipefail
3+
4+
if [ "$#" -eq 0 ]; then
5+
set -- */*/
6+
fi
7+
8+
set -x
9+
10+
# This is used to modify "Dockerfile.builder" for architectures that are not (yet) supported by stable releases (notably, riscv64).
11+
sed -ri \
12+
-e 's/^(FROM debian:)[^ -]+/\1unstable/g' \
13+
-e 's/^(FROM alpine:)[^ -]+/\1edge/g' \
14+
"${@/%//Dockerfile.builder}"

0 commit comments

Comments
 (0)