Skip to content

Commit 2bf1f4d

Browse files
committed
targets/linux/deb/distro: add minimal images target for DEB distros
This commit adds a new target for building minimal images for DEB-based distros. In new implementation, base image for DEB-based distros is not required, although still supported. New implementation in a nutshell downloads all required DEB packages into a volume using work image, extracts them into a target image then runs dpkg --install to run post-install scripts in the proper environment. It also moves container-related tests to a separate function so they can be executed to test both implementations while we keep the old, experimental implementation around. Closes #448 Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com>
1 parent deb2d1f commit 2bf1f4d

8 files changed

Lines changed: 1686 additions & 1607 deletions

File tree

targets/linux/deb/debian/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ var (
2727
// base image.
2828
basePackages = []string{
2929
"ca-certificates",
30+
"passwd",
31+
"apt",
3032
}
3133

3234
targets = map[string]gwclient.BuildFunc{

targets/linux/deb/distro/container.go

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

108112
func 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

Comments
 (0)