@@ -118,6 +118,45 @@ jobs:
118118 ./os/mkosi/build.sh image "$RUNNER_TEMP/mkosi-cross-host"
119119 sudo chown -R "$USER:$USER" "$RUNNER_TEMP/mkosi-cross-host/out"
120120
121+ - name : Record canonical rootfs tree
122+ if : always()
123+ run : |
124+ root="$RUNNER_TEMP/mkosi-cross-host/work-prod/rootfs"
125+ test -d "$root" || exit 0
126+ sudo python3 - "$root" > "$RUNNER_TEMP/rootfs.manifest" <<'PY'
127+ import hashlib
128+ import os
129+ import stat
130+ import sys
131+
132+ root = sys.argv[1]
133+ hardlinks = {}
134+ for base, dirs, files in os.walk(root, topdown=True, followlinks=False):
135+ dirs.sort()
136+ files.sort()
137+ for name in dirs + files:
138+ path = os.path.join(base, name)
139+ rel = os.path.relpath(path, root)
140+ info = os.lstat(path)
141+ mode = stat.S_IMODE(info.st_mode)
142+ if stat.S_ISREG(info.st_mode):
143+ digest = hashlib.sha256()
144+ with open(path, "rb") as stream:
145+ for block in iter(lambda: stream.read(1024 * 1024), b""):
146+ digest.update(block)
147+ key = (info.st_dev, info.st_ino)
148+ first = hardlinks.setdefault(key, rel)
149+ detail = f"sha256={digest.hexdigest()} hardlink={first}"
150+ elif stat.S_ISLNK(info.st_mode):
151+ detail = f"target={os.readlink(path)}"
152+ elif stat.S_ISCHR(info.st_mode) or stat.S_ISBLK(info.st_mode):
153+ detail = f"device={os.major(info.st_rdev)}:{os.minor(info.st_rdev)}"
154+ else:
155+ detail = "-"
156+ print(f"{rel}\t{stat.S_IFMT(info.st_mode):o}\t{mode:o}\t"
157+ f"{info.st_uid}:{info.st_gid}\t{info.st_size}\t{detail}")
158+ PY
159+
121160 - name : Compare with local byte-for-byte baseline
122161 run : |
123162 cd "$RUNNER_TEMP/mkosi-cross-host/out/prod"
@@ -135,5 +174,7 @@ jobs:
135174 uses : actions/upload-artifact@v4
136175 with :
137176 name : mkosi-cross-host-sha256
138- path : ${{ runner.temp }}/mkosi-cross-host/out/prod/cross-host.sha256
177+ path : |
178+ ${{ runner.temp }}/mkosi-cross-host/out/prod/cross-host.sha256
179+ ${{ runner.temp }}/rootfs.manifest
139180 retention-days : 30
0 commit comments