What kind of request is this?
None
What is your request or suggestion?
Problem Statement
Dalec's current model is one spec = one build = one package = one container image. This creates three practical problems:
-
Redundant builds: When a single source tree produces multiple deliverables (e.g., a Bazel workspace that outputs standard, contrib, and debug binaries, or a Go monorepo with multiple commands), each deliverable requires its own spec with its own full build. The compilation is identical across specs — only artifact selection differs.
-
No supplemental packages: Linux packaging systems (RPM and deb) natively support subpackages — a single build producing multiple related packages (e.g., foo, foo-devel, foo-dbg). Dalec has no way to express this. (See #607)
Current Spec Structure (Relevant Parts)
For reference, the parts of today's spec that would be affected:
name: foo
version: 1.0.0
# ... metadata ...
sources: { ... }
build: { ... }
artifacts: # Single artifact set → single package
binaries: { ... }
libs: { ... }
dependencies:
build: { ... }
runtime: { ... }
image: # Single image config
entrypoint: ...
post:
symlinks: { ... }
targets: # Per-distro overrides (artifacts, image, deps, etc.)
azlinux3:
artifacts: { ... }
image: { ... }
dependencies: { ... }
jammy:
dependencies: { ... }
The targets map lets you override artifacts, image, dependencies, conflicts, provides, replaces, package_config, and tests per distro. But it cannot produce multiple packages — it's a customization mechanism, not a multiplexing mechanism.
How RPM and Deb Handle Subpackages
Before proposing dalec changes, it's worth understanding how existing packaging systems solve this problem, since dalec ultimately produces RPMs and debs.
RPM Subpackages
In an RPM spec file, subpackages are declared with %package <name> sections. The build is shared — there is one %build section and one %install section. Each subpackage gets its own %files section that selects which installed files go into that package.
Each subpackage can define its own Summary, Requires, Provides, Conflicts, Obsoletes, and %description. It inherits Version, Release, License, Vendor, and URL from the parent spec.
By default, the subpackage name is <mainname>-<subname>. The -n flag allows specifying an arbitrary name instead.
There is no implicit dependency between subpackages and the main package. If foo-devel needs foo, it must explicitly declare Requires: foo = %{version}-%{release}.
Debian Binary Packages
In debian/control, the first stanza defines the source package (with Build-Depends). Each subsequent stanza defines a binary package with its own Package, Architecture, Description, Depends, Conflicts, Provides, etc.
The build runs once via debian/rules. The debian/<package>.install files determine which built files go into which binary package.
There is no implicit dependency between binary packages from the same source. If libfoo-dev needs libfoo0, it must declare Depends: libfoo0 (= ${binary:Version}).
Why No Implicit Dependencies
This is intentional in both systems. Different subpackage relationships require different dependency declarations:
foo-devel → depends on foo (needs the runtime libraries to be useful)
foo-docs → no dependency on foo (documentation is standalone)
foo-debug → depends on foo (debug symbols for the installed binary)
foo-contrib → conflicts with foo (both provide /usr/bin/foo; only one can be installed)
Implicit dependencies would be wrong for at least some of these cases. Dalec should follow the same pattern: supplemental packages declare their own dependencies explicitly.
Proposed Change: Multiple Image Definitions
Separating Images from Packages
Images and packages are different concerns:
- Packages define what goes into
.rpm/.deb files — artifact selection, runtime deps, metadata. These are packaging concerns.
- Images define container configurations — which packages to install, entrypoint, env, post-install symlinks. These are container concerns.
Today, the spec has a single image field that configures the one container image for the one package. With supplemental packages, we need a way to define multiple named image configurations, each specifying which packages to install.
Supplemental packages do not get their own image field. Instead, image definitions are separate and explicitly declare which packages they contain.
Independent Namespaces
The packages map and the images map within a target are completely independent namespaces. Package names and image names do not need to correspond:
- A supplemental package
contrib does not imply an image called contrib exists.
- An image called
distroless does not imply a supplemental package called distroless exists.
- An image called
full might install packages foo and foo-debug — no package called full needs to exist.
In target paths, only container targets support name suffixes:
azlinux3/rpm — produces all packages (primary + supplemental), no sub-path selection
azlinux3/container/<name> — <name> refers to a key in targets.azlinux3.images
The Root-Level image Field
The root-level image field is unchanged from today. It configures the primary package's container image and is built via <distro>/container (no name suffix). This is pure backward compatibility — existing specs with image work identically.
The root-level image can also serve as a YAML anchor source for named images, but it has no special inheritance relationship with named images. It is not "shared defaults" — it is the primary package's container config.
image alone (no targets.<distro>.images): works exactly as today — <distro>/container builds the primary package's container.
targets.<distro>.images present: named images are self-contained definitions within each target. The root-level image continues to define the primary package's container, built via <distro>/container.
Where Named Images Go
Named images are defined inside targets.<distro>.images, not at the root level. Each target independently declares which named images it supports:
# Root-level image — primary package's container config (unchanged)
image: &base-image
entrypoint: /usr/bin/foo
post:
symlinks:
/usr/bin/foo:
path: /usr/local/bin/foo
/usr/bin/su-exec:
path: /usr/local/bin/su-exec
# YAML anchors for sharing image definitions
x-images:
with-contrib: &with-contrib-image
<<: *base-image
packages: [foo-contrib]
distroless: &distroless-image
entrypoint: /usr/bin/foo
packages: [foo]
# no symlinks — distroless doesn't need compat paths
targets:
azlinux3:
images:
with-contrib: *with-contrib-image
distroless: *distroless-image
jammy:
images:
with-contrib: *with-contrib-image
distroless: *distroless-image
Each named image definition is self-contained — it specifies everything about the container (entrypoint, packages, post, etc.) without inheriting from the root-level image. Sharing is via YAML anchors, using the root image as an anchor source if desired (as shown with &base-image above).
Image Definition Structure
Each entry in targets.<distro>.images can specify:
- packages — which packages from this spec to install in the container
- All existing
ImageConfig fields: entrypoint, cmd, env, labels, post (symlinks), user, working_dir, stop_signal, volumes, base/bases
- tests — image-specific tests (see "Testing" below)
Package Selection Semantics
packages field omitted — install all packages defined in that target (primary + all supplemental for that target).
packages field present — install exactly what's listed, nothing else. No implicit inclusion of the primary package. An omitted package is not installed.
Entries in the packages list use the resolved package name — the name the package will actually have after building. For the primary package, this is the spec's name field. For supplemental packages, this is either the explicit name field if set, or the default <parent>-<key>. For example, if a supplemental package has map key gorunner and name: kubernetes-gorunner, you reference it as kubernetes-gorunner, not gorunner. Dalec validates that every entry in the list matches a package produced by this spec for the current target.
The "explicit when specified" rule follows the RPM/deb precedent: package managers never implicitly pull in sibling packages from the same source build. If you're being specific, you're being specific.
Backward compatibility: A spec with only image (no targets.<distro>.images, no supplemental packages) works identically to today — <distro>/container builds the primary package's container. The "install all packages" default only matters when there are actually multiple packages to install.
Container Target Paths
<distro>/container — builds the primary package's container (from root-level image), unchanged from today.
<distro>/container/<name> — builds the named image from targets.<distro>.images.<name>.
Each docker build invocation produces one image. Multiple images require multiple invocations, but BuildKit caches the shared build steps, so the actual compilation happens only once.
Testing
Dalec tests run against containers, not against package files directly. Even for package targets, dalec creates a container from the built packages and runs tests against it. This doesn't change with supplemental packages and named images.
How tests work today:
- Root-level
tests defines baseline tests.
targets.<distro>.tests adds distro-specific tests, merged with the root tests.
- The tests run against a container with the package installed.
How tests work with supplemental packages and named images:
-
Package targets (azlinux3/rpm) — dalec creates a test container with all packages for that target installed (primary + all supplemental defined in that target) and runs the root + target tests against it.
-
Primary container target (azlinux3/container) — identical to today: root + target tests against the primary package's container.
-
Named container targets (azlinux3/container/distroless) — dalec builds the specific named image and runs tests from three levels appended together:
- Root-level
tests (baseline)
targets.<distro>.tests (distro-specific)
targets.<distro>.images.<name>.tests (image-specific)
This follows the same append semantics as the existing root + target test merge — tests are never replaced, only accumulated. If root-level tests would be inappropriate for a particular image (e.g., testing su-exec in a distroless image that doesn't have it), the spec author should move those tests to image-specific test sections instead.
-
Specs with only image (no targets.<distro>.images) — identical to today, no changes.
No per-package tests — supplemental packages do not get their own tests section. If you need to test a specific package in isolation, define a named image that installs only that package and put tests on the image. This keeps the testing model simple: tests always run against a container.
targets:
azlinux3:
images:
default:
packages: [foo, foo-utils]
# no tests — uses root + target tests only
minimal:
packages: [foo]
tests:
- name: no utils
files:
/usr/bin/foo-util:
not_exist: true
# Root tests — only test what's common to ALL images
tests:
- name: foo runs
steps:
- command: /usr/bin/foo --version
In this example, the root test (foo runs) applies to both images. The minimal image adds its own test verifying that the utils binary is absent. Tests that only apply to default (e.g., testing foo-util) would go in targets.azlinux3.images.default.tests, not in root tests.****
Are you willing to submit PRs to contribute to this feature request?
What kind of request is this?
None
What is your request or suggestion?
Problem Statement
Dalec's current model is one spec = one build = one package = one container image. This creates three practical problems:
Redundant builds: When a single source tree produces multiple deliverables (e.g., a Bazel workspace that outputs standard, contrib, and debug binaries, or a Go monorepo with multiple commands), each deliverable requires its own spec with its own full build. The compilation is identical across specs — only artifact selection differs.
No supplemental packages: Linux packaging systems (RPM and deb) natively support subpackages — a single build producing multiple related packages (e.g.,
foo,foo-devel,foo-dbg). Dalec has no way to express this. (See #607)Current Spec Structure (Relevant Parts)
For reference, the parts of today's spec that would be affected:
The
targetsmap lets you overrideartifacts,image,dependencies,conflicts,provides,replaces,package_config, andtestsper distro. But it cannot produce multiple packages — it's a customization mechanism, not a multiplexing mechanism.How RPM and Deb Handle Subpackages
Before proposing dalec changes, it's worth understanding how existing packaging systems solve this problem, since dalec ultimately produces RPMs and debs.
RPM Subpackages
In an RPM spec file, subpackages are declared with
%package <name>sections. The build is shared — there is one%buildsection and one%installsection. Each subpackage gets its own%filessection that selects which installed files go into that package.Each subpackage can define its own
Summary,Requires,Provides,Conflicts,Obsoletes, and%description. It inheritsVersion,Release,License,Vendor, andURLfrom the parent spec.By default, the subpackage name is
<mainname>-<subname>. The-nflag allows specifying an arbitrary name instead.There is no implicit dependency between subpackages and the main package. If
foo-develneedsfoo, it must explicitly declareRequires: foo = %{version}-%{release}.Debian Binary Packages
In
debian/control, the first stanza defines the source package (withBuild-Depends). Each subsequent stanza defines a binary package with its ownPackage,Architecture,Description,Depends,Conflicts,Provides, etc.The build runs once via
debian/rules. Thedebian/<package>.installfiles determine which built files go into which binary package.There is no implicit dependency between binary packages from the same source. If
libfoo-devneedslibfoo0, it must declareDepends: libfoo0 (= ${binary:Version}).Why No Implicit Dependencies
This is intentional in both systems. Different subpackage relationships require different dependency declarations:
foo-devel→ depends onfoo(needs the runtime libraries to be useful)foo-docs→ no dependency onfoo(documentation is standalone)foo-debug→ depends onfoo(debug symbols for the installed binary)foo-contrib→ conflicts withfoo(both provide/usr/bin/foo; only one can be installed)Implicit dependencies would be wrong for at least some of these cases. Dalec should follow the same pattern: supplemental packages declare their own dependencies explicitly.
Proposed Change: Multiple Image Definitions
Separating Images from Packages
Images and packages are different concerns:
.rpm/.debfiles — artifact selection, runtime deps, metadata. These are packaging concerns.Today, the spec has a single
imagefield that configures the one container image for the one package. With supplemental packages, we need a way to define multiple named image configurations, each specifying which packages to install.Supplemental packages do not get their own
imagefield. Instead, image definitions are separate and explicitly declare which packages they contain.Independent Namespaces
The
packagesmap and theimagesmap within a target are completely independent namespaces. Package names and image names do not need to correspond:contribdoes not imply an image calledcontribexists.distrolessdoes not imply a supplemental package calleddistrolessexists.fullmight install packagesfooandfoo-debug— no package calledfullneeds to exist.In target paths, only container targets support name suffixes:
azlinux3/rpm— produces all packages (primary + supplemental), no sub-path selectionazlinux3/container/<name>—<name>refers to a key intargets.azlinux3.imagesThe Root-Level
imageFieldThe root-level
imagefield is unchanged from today. It configures the primary package's container image and is built via<distro>/container(no name suffix). This is pure backward compatibility — existing specs withimagework identically.The root-level
imagecan also serve as a YAML anchor source for named images, but it has no special inheritance relationship with named images. It is not "shared defaults" — it is the primary package's container config.imagealone (notargets.<distro>.images): works exactly as today —<distro>/containerbuilds the primary package's container.targets.<distro>.imagespresent: named images are self-contained definitions within each target. The root-levelimagecontinues to define the primary package's container, built via<distro>/container.Where Named Images Go
Named images are defined inside
targets.<distro>.images, not at the root level. Each target independently declares which named images it supports:Each named image definition is self-contained — it specifies everything about the container (entrypoint, packages, post, etc.) without inheriting from the root-level
image. Sharing is via YAML anchors, using the rootimageas an anchor source if desired (as shown with&base-imageabove).Image Definition Structure
Each entry in
targets.<distro>.imagescan specify:ImageConfigfields:entrypoint,cmd,env,labels,post(symlinks),user,working_dir,stop_signal,volumes,base/basesPackage Selection Semantics
packagesfield omitted — install all packages defined in that target (primary + all supplemental for that target).packagesfield present — install exactly what's listed, nothing else. No implicit inclusion of the primary package. An omitted package is not installed.Entries in the
packageslist use the resolved package name — the name the package will actually have after building. For the primary package, this is the spec'snamefield. For supplemental packages, this is either the explicitnamefield if set, or the default<parent>-<key>. For example, if a supplemental package has map keygorunnerandname: kubernetes-gorunner, you reference it askubernetes-gorunner, notgorunner. Dalec validates that every entry in the list matches a package produced by this spec for the current target.The "explicit when specified" rule follows the RPM/deb precedent: package managers never implicitly pull in sibling packages from the same source build. If you're being specific, you're being specific.
Backward compatibility: A spec with only
image(notargets.<distro>.images, no supplemental packages) works identically to today —<distro>/containerbuilds the primary package's container. The "install all packages" default only matters when there are actually multiple packages to install.Container Target Paths
<distro>/container— builds the primary package's container (from root-levelimage), unchanged from today.<distro>/container/<name>— builds the named image fromtargets.<distro>.images.<name>.Each
docker buildinvocation produces one image. Multiple images require multiple invocations, but BuildKit caches the shared build steps, so the actual compilation happens only once.Testing
Dalec tests run against containers, not against package files directly. Even for package targets, dalec creates a container from the built packages and runs tests against it. This doesn't change with supplemental packages and named images.
How tests work today:
testsdefines baseline tests.targets.<distro>.testsadds distro-specific tests, merged with the root tests.How tests work with supplemental packages and named images:
Package targets (
azlinux3/rpm) — dalec creates a test container with all packages for that target installed (primary + all supplemental defined in that target) and runs the root + target tests against it.Primary container target (
azlinux3/container) — identical to today: root + target tests against the primary package's container.Named container targets (
azlinux3/container/distroless) — dalec builds the specific named image and runs tests from three levels appended together:tests(baseline)targets.<distro>.tests(distro-specific)targets.<distro>.images.<name>.tests(image-specific)This follows the same append semantics as the existing root + target test merge — tests are never replaced, only accumulated. If root-level tests would be inappropriate for a particular image (e.g., testing
su-execin a distroless image that doesn't have it), the spec author should move those tests to image-specific test sections instead.Specs with only
image(notargets.<distro>.images) — identical to today, no changes.No per-package tests — supplemental packages do not get their own
testssection. If you need to test a specific package in isolation, define a named image that installs only that package and put tests on the image. This keeps the testing model simple: tests always run against a container.In this example, the root test (
foo runs) applies to both images. Theminimalimage adds its own test verifying that the utils binary is absent. Tests that only apply todefault(e.g., testingfoo-util) would go intargets.azlinux3.images.default.tests, not in roottests.****Are you willing to submit PRs to contribute to this feature request?