@@ -265,12 +265,6 @@ for f in $(echo "${deb_files}" | tr ' ' '\n' | grep -E '/(usrmerge|usr-is-merged
265265 # Remove the deb file so it won't be re-installed.
266266 rm "${f}"
267267done
268-
269- # Copy apt sources from worker into rootfs so the final container can install packages. Do we want that?
270- # There is no guarantee that the final image will have access to the same sources worker had (e.g. with mounted repos).
271- #
272- # At the moment this is necessary so we can for example install test dependencies without using worker image.
273- #cp -ar /etc/apt/sources.list* "${rootfs}/etc/apt/"
274268`
275269
276270 opts = append (opts , dalec .ProgressGroup ("Fetch DEB Packages" ))
@@ -290,9 +284,124 @@ done
290284 frontend .IgnoreCache (input .Client , targets .IgnoreCacheKeyContainer ),
291285 ).AddMount ("/tmp/rootfs" , baseImageFromSpec (baseImg , input ))
292286
293- return baseImg .With (installPackagesInContainer (input , []llb.RunOption {
287+ result := baseImg .With (installPackagesInContainer (input , []llb.RunOption {
294288 dalec .ProgressGroup ("Install DEB Packages" ),
295289 llb .AddEnv ("DEBIAN_FRONTEND" , "noninteractive" ),
296290 llb .Args ([]string {"/usr/bin/sh" , "-c" , "dpkg --install --force-depends /var/cache/apt/archives/*.deb && rm -rf /var/cache/apt/archives/*.deb" }),
297291 }))
292+
293+ result = cleanupBootstrapContainer (result , input , opts ... )
294+
295+ // Squash all layers into one by copying the final filesystem into a fresh
296+ // scratch state. Without this, files extracted in the bootstrap layer but
297+ // removed during cleanup still occupy space in the earlier layer.
298+ squashOpts := append (opts , dalec .ProgressGroup ("Squash container layers" ))
299+ return llb .Scratch ().File (llb .Copy (result , "/" , "/" , & llb.CopyInfo {
300+ CopyDirContentsOnly : true ,
301+ CreateDestPath : true ,
302+ AllowWildcard : true ,
303+ }), squashOpts ... )
304+ }
305+
306+ // cleanupBootstrapContainer removes package manager infrastructure, unnecessary
307+ // packages, and caches from the container image.
308+ func cleanupBootstrapContainer (st llb.State , input buildContainerInput , opts ... llb.ConstraintsOpt ) llb.State {
309+ cleanupOpts := append (opts , dalec .ProgressGroup ("Cleanup Bootstrap Container" ))
310+
311+ hasDocs := input .Spec .GetArtifacts (input .Target ).HasDocs ()
312+
313+ // Directories to remove.
314+ rmDirsCmd := `rm -rf /var/lib/apt /var/cache/apt /var/log/apt /var/log/dpkg.log \
315+ /var/cache/debconf /etc/apt \
316+ /usr/lib/apt /usr/share/apt \
317+ /usr/share/debconf /usr/share/bug /usr/share/lintian /usr/share/bash-completion \
318+ /usr/share/locale`
319+ if ! hasDocs {
320+ rmDirsCmd += ` \
321+ /usr/share/doc /usr/share/man /usr/share/info`
322+ }
323+
324+ script := `#!/bin/sh
325+
326+ # --- Remove directories ---
327+ ` + rmDirsCmd + `
328+
329+ # --- Remove unnecessary packages ---
330+
331+
332+ # Helper: collect installed packages from a candidate list.
333+ collect_installed() {
334+ result=""
335+ for pkg in $@; do
336+ if dpkg-query -W -f='${Status}' "${pkg}" 2>/dev/null | grep -q 'install ok installed'; then
337+ result="${result} ${pkg}"
338+ fi
339+ done
340+ echo "${result}"
341+ }
342+
343+ # Phase 1: Purge apt and packages whose removal scripts may load shared libs.
344+ phase1=$(collect_installed \
345+ apt apt-utils libapt-pkg6.0 libapt-pkg7.0 \
346+ debconf debconf-i18n libdebconfclient0 \
347+ debian-archive-keyring ubuntu-keyring \
348+ passwd login.defs base-passwd \
349+ bash-completion manpages sensible-utils sqv \
350+ ncurses-base ncurses-bin \
351+ perl-base libtext-charwidth-perl libtext-iconv-perl libtext-wrapi18n-perl \
352+ liblocale-gettext-perl \
353+ bash sed debianutils findutils mawk \
354+ bsdutils bsdextrautils util-linux sysvinit-utils \
355+ init-system-helpers \
356+ libpam-modules libpam-modules-bin libpam-runtime libpam0g \
357+ libdb5.3 libdb5.3t64 libsqlite3-0 \
358+ libaudit-common libaudit1 \
359+ liblastlog2-2 \
360+ )
361+
362+ if [ -n "${phase1}" ]; then
363+ dpkg --purge --force-depends --force-remove-essential ${phase1} || true
364+ fi
365+
366+ # Phase 2: Remove low-level libraries that phase 1 packages depended on.
367+ phase2=$(collect_installed \
368+ libsemanage-common libsemanage2 \
369+ libsystemd0 libudev1 \
370+ libseccomp2 \
371+ libmount1 libblkid1 libsmartcols1 \
372+ libcap-ng0 \
373+ libtinfo6 \
374+ grep \
375+ hostname \
376+ gzip
377+ )
378+
379+ if [ -n "${phase2}" ]; then
380+ dpkg --purge --force-depends --force-remove-essential ${phase2} || true
381+ fi
382+
383+ # Phase 3: Remove dpkg, its libraries, coreutils, dash, and remaining tools
384+ # in a single dpkg invocation. Once dpkg is loaded in memory, it won't
385+ # re-read the shared libraries, so removing them in the same call is safe.
386+ # dash is the shell running this script — the kernel keeps the fd open.
387+ # dpkg uses --remove (not --purge) to preserve /var/lib/dpkg/status
388+ # for security scanners.
389+ dpkg --remove --force-depends --force-remove-essential \
390+ diffutils \
391+ libbsd0 libmd0 libselinux1 libsepol2 libpcre2-8-0 \
392+ libacl1 libattr1 libcap2 \
393+ libbz2-1.0 liblz4-1 liblzma5 libxxhash0 \
394+ libuuid1 libcrypt1 tar dash coreutils \
395+ dpkg || true
396+ `
397+
398+ scriptSt := llb .Scratch ().File (llb .Mkfile ("cleanup.sh" , 0o755 , []byte (script )), cleanupOpts ... )
399+
400+ st = st .Run (
401+ dalec .WithConstraints (cleanupOpts ... ),
402+ llb .AddMount ("/tmp/dalec-cleanup.sh" , scriptSt , llb .SourcePath ("cleanup.sh" ), llb .Readonly ),
403+ llb .Args ([]string {"/usr/bin/sh" , "/tmp/dalec-cleanup.sh" }),
404+ ).Root ()
405+
406+ return st
298407}
0 commit comments