@@ -7,18 +7,26 @@ ROOT=$(cd "$SELF/../.." && pwd)
77# shellcheck source=/dev/null
88source " $SELF /versions.env"
99FLAVORS=${FLAVORS:- prod}
10- usage=" Usage: $0 [--no-cache] {image|repro-check|lint} [build-dir]"
10+ usage=" Usage: $0 [--no-cache] [--archive] {image|repro-check|lint} [build-dir]"
1111# The component cache is keyed on every declared input of each component, so a
1212# hit reproduces the same output a cold build would have produced. Reusing it is
1313# therefore the sensible default; --no-cache forces the cold path for a release
1414# build or when the key itself is what needs auditing. repro-check ignores both
1515# and always builds cold, because a cache hit would answer the wrong question.
1616cache=${DSTACK_COMPONENT_CACHE:- 1}
17+ # The two release tarballs are ~58 s of gzip over artifacts that already exist
18+ # unpacked beside them. A cached build is an iteration build, and iterating on
19+ # the guest wants disk.raw and the measurements, not a redistributable archive
20+ # -- so a cached build skips them and a cold build still produces them. Pass
21+ # --archive to get them out of a cached build anyway. Left unset here so the
22+ # default can be derived from the final value of $cache below.
23+ archive=${DSTACK_TAR_RELEASE:- }
1724action=
1825BUILD_DIR=
1926while [ $# -gt 0 ]; do
2027 case " $1 " in
2128 --no-cache) cache=0 ;;
29+ --archive) archive=1 ;;
2230 -h|--help) echo " $usage " ; exit 0 ;;
2331 -* ) echo " Unknown option: $1 " >&2 ; echo " $usage " >&2 ; exit 2 ;;
2432 * )
@@ -40,6 +48,18 @@ if [[ $action == repro-check ]]; then cache=0; fi
4048[[ $cache == 1 || $cache == 0 ]] || {
4149 echo " DSTACK_COMPONENT_CACHE must be 0 or 1, got: $cache " >&2 ; exit 2;
4250}
51+ # Derived after repro-check has forced the cold path, so repro-check always
52+ # archives: it compares the two release tarballs, and skipping them would leave
53+ # it comparing nothing and passing vacuously.
54+ archive=${archive:- $(( cache == 1 ? 0 : 1 ))}
55+ # Not merely defaulted: forced. repro-check compares the two release tarballs,
56+ # so an environment that switched archiving off would leave it comparing files
57+ # that do not exist -- a check that fails for the wrong reason, or worse, is
58+ # read as "no difference found".
59+ if [[ $action == repro-check ]]; then archive=1; fi
60+ [[ $archive == 1 || $archive == 0 ]] || {
61+ echo " DSTACK_TAR_RELEASE must be 0 or 1, got: $archive " >&2 ; exit 2;
62+ }
4363if [[ $action == lint ]]; then exec " $SELF /tests/acceptance.sh" ; fi
4464command -v mkosi > /dev/null || { echo " mkosi $MKOSI_VERSION is required" >&2 ; exit 1; }
4565actual=$( mkosi --version | awk ' {print $2}' | cut -d. -f1)
@@ -81,6 +101,39 @@ if [[ $cache == 0 ]]; then
81101 mkosi --directory " $SELF " --output-directory=" $BUILD_DIR /out" clean -f
82102fi
83103
104+ # Digest of every input that determines the incrementally cached tree: the
105+ # package lists and distribution pins in the configs, and the skeleton files
106+ # copied in before packages are installed. Deliberately not the dstack sources
107+ # or the component definitions -- those are consumed by the build script, which
108+ # runs after the cache is restored, and folding them in would defeat the cache
109+ # on every source edit. Sorted for a stable digest, and the file list itself is
110+ # hashed too so that deleting a skeleton file also moves the key.
111+ base_inputs_digest () {
112+ local flavor=$1 skeleton
113+ local skeletons=(" $SELF /mkosi.skeleton" )
114+ if [[ -d $SELF /mkosi.profiles/$flavor /mkosi.skeleton ]]; then
115+ skeletons+=(" $SELF /mkosi.profiles/$flavor /mkosi.skeleton" )
116+ fi
117+ {
118+ # Hash relative names, types and modes as well as contents. Relative names
119+ # let worktrees share a cache; types and modes cover inputs that sha256sum
120+ # alone cannot distinguish.
121+ for skeleton in " ${skeletons[@]} " ; do
122+ echo " -- skeleton ${skeleton# " $SELF " / } "
123+ (cd " $skeleton " && find . -mindepth 1 -printf ' %P %y %m\n' | sort)
124+ (cd " $skeleton " && find . -type f -print0 | sort -z | xargs -0 -r sha256sum)
125+ (cd " $skeleton " && find . -type l -print0 | sort -z | \
126+ while IFS= read -r -d ' ' link; do
127+ printf ' %s -> %s\n' " ${link# ./ } " " $( readlink " $link " ) "
128+ done)
129+ done
130+ echo " -- mkosi.conf"
131+ cat " $SELF /mkosi.conf" " $SELF /mkosi.tools.conf"
132+ echo " -- profile/$flavor /mkosi.conf"
133+ cat " $SELF /mkosi.profiles/$flavor /mkosi.conf"
134+ } | sha256sum | cut -c1-32
135+ }
136+
84137build_one () {
85138 local out=$1 flavor=$2 jobs=${3:- ${JOBS:- $(nproc)} }
86139 mkdir -p " $out "
@@ -91,6 +144,7 @@ build_one() {
91144 --profile=" $flavor "
92145 --source-date-epoch=" $SOURCE_DATE_EPOCH "
93146 --environment=" DSTACK_COMPONENT_CACHE=$cache "
147+ --environment=" DSTACK_TAR_RELEASE=$archive "
94148 --environment=" DSTACK_BUILD_GIT_REVISION=$DSTACK_BUILD_GIT_REVISION "
95149 --environment=" DSTACK_SOURCE_REVISION=$revision "
96150 --environment=" JOBS=$jobs "
@@ -109,6 +163,25 @@ build_one() {
109163 mkosi_args+=(--build-directory=" $cache_root /build" )
110164 mkosi_args+=(--build-sources=" $cache_root /manifest:component-cache" )
111165 mkosi_args+=(--environment=" DSTACK_SOURCE_MANIFEST=/work/src/component-cache/source-manifest" )
166+ # mkosi's incremental cache captures the tree after the distribution and
167+ # build packages are installed and before any build script runs, which is
168+ # exactly the ~70 s this build otherwise repeats verbatim every time.
169+ #
170+ # It cannot be enabled as-is. mkosi keys the cache on CacheKey=, whose
171+ # default is &d~&r~&a~&I -- distribution, release, architecture, image id --
172+ # and whose specifier set contains no digest of the inputs that actually
173+ # determine that tree. Editing Packages= in mkosi.conf, or any file under
174+ # mkosi.skeleton/, leaves the key untouched, so mkosi would restore the
175+ # stale tree and the change would silently not be in the image. Mixing the
176+ # digest below into the key restores the invalidation mkosi does not do.
177+ base_digest=$( base_inputs_digest " $flavor " )
178+ mkosi_args+=(--incremental=yes)
179+ mkosi_args+=(--cache-directory=" $cache_root /incremental" )
180+ mkosi_args+=(--cache-key=" &d~&r~&a~&I~$base_digest " )
181+ # Package downloads are content-addressed by the pinned Snapshot=, so this
182+ # only avoids refetching identical files. Release builds still take the
183+ # cold path and fetch from the snapshot themselves.
184+ mkosi_args+=(--package-cache-directory=" $cache_root /packages" )
112185 fi
113186 mkosi " ${mkosi_args[@]} " build
114187 # mkosi's own output is a plain Debian rootfs: unmeasured, not part of the
0 commit comments