@@ -24,6 +24,10 @@ func (c *Config) BuildContainer(ctx context.Context, client gwclient.Client, sOp
2424 Opts : opts ,
2525 }
2626
27+ if c .DefaultOutputImage == "" {
28+ return bootstrapContainer (ctx , input )
29+ }
30+
2731 baseImg := baseImageFromSpec (llb .Image (c .DefaultOutputImage , llb .WithMetaResolver (sOpt .Resolver ), dalec .WithConstraints (opts ... )), input )
2832
2933 if len (c .BasePackages ) > 0 {
@@ -107,7 +111,7 @@ func extraRepos(input buildContainerInput) llb.RunOption {
107111
108112func installPackagesInContainer (input buildContainerInput , ro []llb.RunOption ) llb.StateOption {
109113 return func (baseImg llb.State ) llb.State {
110- opts := append (input .Opts , dalec .ProgressGroup ("Install spec package " ))
114+ opts := append (input .Opts , dalec .ProgressGroup ("Install DEB Packages " ))
111115
112116 debug := llb .Scratch ().File (llb .Mkfile ("debug" , 0o644 , []byte (`debug=2` )), opts ... )
113117
@@ -136,3 +140,159 @@ func installPackagesInContainer(input buildContainerInput, ro []llb.RunOption) l
136140 With (dalec .InstallPostSymlinks (input .Spec .GetImagePost (input .Target ), input .Worker , opts ... ))
137141 }
138142}
143+
144+ func bootstrapContainer (ctx context.Context , input buildContainerInput ) llb.State {
145+ opts := input .Opts
146+
147+ baseImgOpts := append (opts , dalec .ProgressGroup ("Bootstrap Base Image" ))
148+
149+ baseImg := llb .Scratch ().File (llb .Mkdir ("/etc" , 0o755 ), baseImgOpts ... ).
150+ File (llb .Mkdir ("/etc/apt" , 0o755 ), baseImgOpts ... ).
151+ File (llb .Mkdir ("/etc/apt/apt.conf.d" , 0o755 ), baseImgOpts ... ).
152+ File (llb .Mkdir ("/etc/apt/preferences.d" , 0o755 ), baseImgOpts ... ).
153+ File (llb .Mkdir ("/etc/apt/sources.list.d" , 0o755 ), baseImgOpts ... ).
154+ File (llb .Mkdir ("/var" , 0o755 ), baseImgOpts ... ).
155+ File (llb .Mkdir ("/var/cache" , 0o755 ), baseImgOpts ... ).
156+ File (llb .Mkdir ("/var/cache/apt" , 0o755 ), baseImgOpts ... ).
157+ File (llb .Mkdir ("/var/cache/apt/archives" , 0o755 ), baseImgOpts ... ).
158+ File (llb .Mkdir ("/var/lib" , 0o755 ), baseImgOpts ... ).
159+ File (llb .Mkdir ("/var/lib/dpkg" , 0o755 ), baseImgOpts ... ).
160+ File (llb .Mkfile ("/var/lib/dpkg/status" , 0o644 , []byte {}), baseImgOpts ... )
161+
162+ installScript := `#!/bin/sh
163+ set -exu
164+
165+ rootfs=/tmp/rootfs
166+ apt_archives=/var/cache/apt/archives
167+
168+ # Make sure any cached data from local repos is purged since this should not
169+ # be shared between builds.
170+ rm -f /var/lib/apt/lists/_*
171+ # autoclean removes cached deb files which are no longer available in any configured repository.
172+ apt autoclean -y
173+
174+ # Remove any previously failed attempts to get repo data
175+ rm -rf /var/lib/apt/lists/partial/*
176+
177+ # Ensure package index is up to date, required when cache is empty.
178+ apt update
179+
180+ # Select essential packages, since those will be used as a base for the image.
181+ #
182+ # We can't use ?essential since some distros we support have too old apk which does not support patterns.
183+ essential_packages=$(dpkg-query -Wf '${Package} ${Essential}\n' | awk '$2 == "yes" {print $1}')
184+
185+ local_package_files=$(ls /base-packages/*.deb /spec-packages/*.deb)
186+
187+ # Get names of local packages so we can exclude them from apt-get install.
188+ local_package_names=$(for f in ${local_package_files}; do dpkg-deb -f "${f}" Package 2>/dev/null; done | sort -u)
189+
190+ # Extract dependencies of local packages, since we need to download those as well.
191+ #
192+ # Spec packages may depend on base packages, so we need to filter to only download remaining packages, since downloading local packages
193+ # would fail.
194+ dependencies_to_download=$(for f in ${local_package_files}; do dpkg-deb -f "${f}" Depends 2>/dev/null; done | tr ',' '\n' | sed 's/([^)]*)//g; s/|.*//; s/ //g' | grep -v '^$' | sort -u | grep -vxF "${local_package_names}")
195+
196+ # Get the exact filenames apt needs by using --print-uris with an empty cache dir.
197+ # This forces apt to report ALL needed packages (not just uncached ones), giving
198+ # us exact filenames including correct version and architecture suffixes.
199+ # --print-uris output format: 'URL' filename size hash
200+ # We extract the second field (the filename).
201+ needed_filenames=$(apt-get -o Dir::State::status="${rootfs}/var/lib/dpkg/status" \
202+ -o Dir::Cache::Archives=/tmp \
203+ --yes --print-uris install ${essential_packages} ${dependencies_to_download} \
204+ | grep '\.deb ' | awk '{print $2}')
205+
206+ mkdir -p "${rootfs}${apt_archives}"/partial
207+ cp ${local_package_files} "${rootfs}${apt_archives}"/
208+
209+ # Copy already-cached needed .deb files from the persistent apt cache into the
210+ # rootfs cache. This avoids picking up stale .deb files from previous unrelated
211+ # builds that remain in the persistent cache.
212+ for filename in ${needed_filenames}; do
213+ if [ -f "${apt_archives}/${filename}" ]; then
214+ cp "${apt_archives}/${filename}" "${rootfs}${apt_archives}"/
215+ fi
216+ done
217+
218+ # Download remaining needed packages directly into the rootfs cache.
219+ # apt skips packages already present, so only missing ones are fetched.
220+ apt-get -o Dir::State::status="${rootfs}/var/lib/dpkg/status" \
221+ -o Dir::Cache::Archives="${rootfs}${apt_archives}" \
222+ --yes --download-only install ${essential_packages} ${dependencies_to_download}
223+
224+ deb_files=$(ls "${rootfs}${apt_archives}"/*.deb)
225+
226+ # Extract all packages into the target rootfs.
227+ #
228+ # Extract base-files first to establish merged-usr symlinks (/bin -> usr/bin, etc.)
229+ # before other packages create those paths as real directories, which would
230+ # cause tar to fail when base-files tries to create the symlinks later.
231+ base_files_package=$(echo "${deb_files}" | tr ' ' '\n' | grep '/base-files_' || true)
232+ for f in ${base_files_package} $(echo "${deb_files}" | tr ' ' '\n' | grep -v '/base-files_'); do
233+ dpkg-deb --extract "${f}" "${rootfs}"
234+ done
235+
236+ # Fix merged-usr: on Noble+, /bin, /sbin, /lib should be symlinks to usr/bin, usr/sbin, usr/lib
237+ # but dpkg-deb --extract may recreate them as real directories.
238+ #
239+ # This is required so we can actually run shell using target image to re-install packages for running post-install scripts.
240+ for dir in bin sbin lib; do
241+ if [ -d "${rootfs}/usr/${dir}" ] && [ -d "${rootfs}/${dir}" ] && [ ! -L "${rootfs}/${dir}" ]; then
242+ cp -a "${rootfs}/${dir}"/* "${rootfs}/usr/${dir}/" 2>/dev/null || true
243+ rm -rf "${rootfs}/${dir}"
244+ ln -s "usr/${dir}" "${rootfs}/${dir}"
245+ fi
246+ done
247+
248+ # dpkg-deb --extract doesn't run postinst scripts, so the /bin/sh symlink
249+ # normally created by update-alternatives is missing. Create it manually.
250+ if [ ! -e "${rootfs}/usr/bin/sh" ] && [ ! -e "${rootfs}/bin/sh" ]; then
251+ ln -s dash "${rootfs}/usr/bin/sh"
252+ fi
253+
254+ # Remove usrmerge package - our merged-usr fixup above already handles this,
255+ # and usrmerge's postinst fails on overlayfs (which BuildKit uses).
256+ # Create a fake dpkg status entry so dpkg thinks it's installed.
257+ #
258+ # This only runs when usrmerge package is not installed in the base image, since only then the deb file will be downloaded.
259+ for f in $(echo "${deb_files}" | tr ' ' '\n' | grep -E '/(usrmerge|usr-is-merged)_' || true); do
260+ pkg=$(dpkg-deb -f "${f}" Package)
261+ ver=$(dpkg-deb -f "${f}" Version)
262+ arch=$(dpkg-deb -f "${f}" Architecture)
263+ printf 'Package: %s\nStatus: install ok installed\nVersion: %s\nArchitecture: %s\nDescription: faked by dalec\n\n' "${pkg}" "${ver}" "${arch}" >> "${rootfs}/var/lib/dpkg/status"
264+
265+ # Remove the deb file so it won't be re-installed.
266+ rm "${f}"
267+ done
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+ # A 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/"
274+ `
275+
276+ opts = append (opts , dalec .ProgressGroup ("Fetch DEB Packages" ))
277+
278+ script := llb .Scratch ().File (llb .Mkfile ("install.sh" , 0o755 , []byte (installScript )), opts ... )
279+
280+ // Use worker to download all packages + deps and install into baseImg.
281+ baseImg = input .Worker .Run (
282+ dalec .WithConstraints (opts ... ),
283+ llb .AddMount ("/tmp/install.sh" , script , llb .SourcePath ("install.sh" )),
284+ llb .AddMount ("/base-packages" , basePackages (ctx , input ), llb .Readonly ),
285+ llb .AddMount ("/spec-packages" , input .SpecPackages , llb .Readonly ),
286+ extraRepos (input ),
287+ dalec .WithMountedAptCache (input .Config .AptCachePrefix , opts ... ),
288+ llb .AddEnv ("DEBIAN_FRONTEND" , "noninteractive" ),
289+ dalec .ShArgs ("/tmp/install.sh" ),
290+ frontend .IgnoreCache (input .Client , targets .IgnoreCacheKeyContainer ),
291+ ).AddMount ("/tmp/rootfs" , baseImageFromSpec (baseImg , input ))
292+
293+ return baseImg .With (installPackagesInContainer (input , []llb.RunOption {
294+ dalec .ProgressGroup ("Install DEB Packages" ),
295+ llb .AddEnv ("DEBIAN_FRONTEND" , "noninteractive" ),
296+ llb .Args ([]string {"/usr/bin/sh" , "-c" , "dpkg --install --force-depends /var/cache/apt/archives/*.deb && rm -rf /var/cache/apt/archives/*.deb" }),
297+ }))
298+ }
0 commit comments