Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 1 addition & 37 deletions docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ group "default" {
}

group "test" {
targets = ["runc-test", "test-deps-only"]
targets = ["runc-test"]
}

variable "FRONTEND_REF" {
Expand Down Expand Up @@ -227,42 +227,6 @@ target "examples" {
tags = ["local/dalec/examples/${f}:${distro}"]
}

target "deps-only" {
name = "deps-only-${distro}"
matrix = {
distro = ["mariner2"]
}
dockerfile-inline = <<EOT
dependencies:
runtime:
patch: {}
bash: {}
EOT
args = {
"BUILDKIT_SYNTAX" = "dalec_frontend"
}
contexts = {
"dalec_frontend" = "target:frontend"
}
target = "${distro}/container/depsonly"
tags = ["local/dalec/deps-only:${distro}"]
}

target "test-deps-only" {
dockerfile-inline = <<EOT
FROM deps-only-context
# Make sure the deps-only target has the runtime dependencies we expect and not, for instance, "rpm"
RUN command -v bash
RUN command -v patch
RUN if command -v rpm; then echo should be a distroless image but rpm binary is installed; exit 1; fi
EOT

contexts = {
"deps-only-context" = "target:deps-only-mariner2"
}
}


variable "CI_FRONTEND_CACHE_SCOPE" {
default = "dalec/frontend/ci"
}
Expand Down
34 changes: 27 additions & 7 deletions targets/linux/rpm/distro/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,36 @@ func (cfg *Config) HandleDepsOnly(ctx context.Context, client gwclient.Client) (
}

pc := dalec.Platform(platform)
worker := cfg.Worker(sOpt, pg, pc)

deps := dalec.SortMapKeys(rtDeps)
// NOTE: Deps-only allows bare specs, ie specs with just the runtime deps included.
// This means we may need to fill in some of the details that are required by the package manager.
depsSpec := &dalec.Spec{
Name: spec.Name + "-runtime-deps",
License: spec.License,
Version: spec.Version,
Revision: spec.Revision,
Description: "Runtime dependencies meta package",
Dependencies: &dalec.PackageDependencies{
Runtime: rtDeps,
},
}

withDownloads := worker.Run(dalec.ShArgs("set -ex; mkdir -p /tmp/rpms/RPMS/$(uname -m)")).
Run(cfg.Install(deps,
DnfDownloadAllDeps("/tmp/rpms/RPMS/$(uname -m)")), pg).Root()
rpmDir := llb.Scratch().File(llb.Copy(withDownloads, "/tmp/rpms", "/", dalec.WithDirContentsOnly()), pg)
if depsSpec.Name == "-runtime-deps" {
// Name cannot start with "-"
depsSpec.Name = "dalec-user" + depsSpec.Name
}
if depsSpec.Version == "" {
depsSpec.Version = "0.0.1"
}
if depsSpec.Revision == "" {
depsSpec.Revision = "1"
}
if depsSpec.License == "" {
depsSpec.License = "MIT"
}

ctr := cfg.BuildContainer(ctx, client, sOpt, spec, targetKey, rpmDir, pg, pc)
pkg := cfg.BuildPkg(ctx, client, sOpt, depsSpec, targetKey, pg)
ctr := cfg.BuildContainer(ctx, client, sOpt, spec, targetKey, pkg, pg)

def, err := ctr.Marshal(ctx, pc)
if err != nil {
Expand Down
44 changes: 13 additions & 31 deletions targets/linux/rpm/distro/dnf_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ type dnfInstallConfig struct {

constraints []llb.ConstraintsOpt

downloadOnly bool

allDeps bool

downloadDir string

// When true, don't omit docs from the installed RPMs.
includeDocs bool

Expand Down Expand Up @@ -82,14 +76,6 @@ func DnfForceArch(arch string) DnfInstallOpt {
}
}

func DnfDownloadAllDeps(dest string) DnfInstallOpt {
return func(cfg *dnfInstallConfig) {
cfg.downloadOnly = true
cfg.allDeps = true
cfg.downloadDir = dest
}
}

func IncludeDocs(v bool) DnfInstallOpt {
return func(cfg *dnfInstallConfig) {
cfg.includeDocs = v
Expand All @@ -110,18 +96,6 @@ func dnfInstallFlags(cfg *dnfInstallConfig) string {
cmdOpts += " --setopt=reposdir=/etc/yum.repos.d"
}

if cfg.downloadOnly {
cmdOpts += " --downloadonly"
}

if cfg.allDeps {
cmdOpts += " --alldeps"
}

if cfg.downloadDir != "" {
cmdOpts += " --downloaddir " + cfg.downloadDir
}

if !cfg.includeDocs {
cmdOpts += " --setopt=tsflags=nodocs"
}
Expand Down Expand Up @@ -183,12 +157,18 @@ if [ -x "$import_keys_path" ]; then
"$import_keys_path"
fi

if [ "$cmd" = "tdnf" ] && command -v dnf &>/dev/null; then
# tdnf has a lot of limitations that cause issues (no --forcearch, issues with gpg keys on local file installs)
# We already have dnf, so prefer that.
cmd="dnf"
fi

if [ -n "$force_arch" ]; then
if [ "$cmd" = "tdnf" ]; then
echo "tdnf does not support --forcearch; cross-arch installs must use dnf" >&2
exit 70
fi
install_flags="$install_flags --forcearch=$force_arch"
if [ "$cmd" = "tdnf" ]; then
echo "tdnf does not support --forcearch; cross-arch installs must use dnf" >&2
exit 70
fi
install_flags="$install_flags --forcearch=$force_arch"
fi

$cmd $dnf_sub_cmd $install_flags "${@}"
Expand Down Expand Up @@ -276,6 +256,8 @@ func DnfInstall(cfg *dnfInstallConfig, releaseVer string, pkgs []string) llb.Run
return dnfCommand(cfg, releaseVer, "dnf", append([]string{"install"}, pkgs...), nil)
}

// TdnfInstall uses tdnf to install packages
// NOTE: tdnf will be automatically upgraded to dnf to work around tdnf limitations *if* dnf is available
func TdnfInstall(cfg *dnfInstallConfig, releaseVer string, pkgs []string) llb.RunOption {
return dnfCommand(cfg, releaseVer, "tdnf", append([]string{"install"}, pkgs...), nil)
}
Expand Down
9 changes: 9 additions & 0 deletions targets/linux/rpm/distro/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package distro
import (
"context"
"encoding/json"
"slices"

"github.com/containerd/platforms"
"github.com/moby/buildkit/client/llb"
Expand Down Expand Up @@ -142,6 +143,14 @@ func (cfg *Config) workerWithBuildPlatform(sOpt dalec.SourceOpts, buildPlat ocis
}

if samePlatform(targetPlat, buildPlat) {
if slices.Contains(cfg.BuilderPackages, "dnf") {
// Install dnf first since this will be bootstrapped with a different package manager
// This keeps the package cache for the bootstrap mananager separate from the other base packages we use.
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "bootstrap mananager" should be "bootstrap manager".

Suggested change
// This keeps the package cache for the bootstrap mananager separate from the other base packages we use.
// This keeps the package cache for the bootstrap manager separate from the other base packages we use.

Copilot uses AI. Check for mistakes.
targetBase = targetBase.Run(
dalec.WithConstraints(append(opts, llb.Platform(targetPlat))...),
cfg.Install([]string{"dnf"}, installOpts...),
).Root()
}
return targetBase.Run(
dalec.WithConstraints(append(opts, llb.Platform(targetPlat))...),
cfg.Install(cfg.BuilderPackages, installOpts...),
Expand Down
91 changes: 91 additions & 0 deletions test/linux_target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type targetConfig struct {
Package string
// Container is the target for creating a container
Container string
// DepsOnly is the target for creating a deps-only container (no package built, only runtime deps installed).
DepsOnly string
// Worker is the target for creating the worker image.
Worker string
// Sysext is the target for creating a systemd system extension.
Expand Down Expand Up @@ -718,6 +720,16 @@ index 0000000..5260cb1
t.Run("container", func(t *testing.T) {
t.Parallel()

t.Run("depsonly", func(t *testing.T) {
if testConfig.Target.DepsOnly == "" {
t.Skip("depsonly target not defined")
}

t.Parallel()
ctx := startTestSpan(ctx, t)
testDepsOnly(ctx, t, testConfig)
})

t.Run("creates_post_install_symlinks", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -5403,6 +5415,85 @@ echo "This is a third test binary"
})
}

func testDepsOnly(ctx context.Context, t *testing.T, testConfig testLinuxConfig) {
t.Run("minimal spec", func(t *testing.T) {
t.Parallel()
ctx := startTestSpan(ctx, t)

spec := &dalec.Spec{
Dependencies: &dalec.PackageDependencies{
Runtime: map[string]dalec.PackageConstraints{
"curl": {},
},
},
}

testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
req := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(testConfig.Target.DepsOnly))
res := solveT(ctx, t, client, req)

ref, err := res.SingleRef()
assert.NilError(t, err)

_, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/bin/curl"})
assert.NilError(t, err)
})
})

t.Run("full spec", func(t *testing.T) {
t.Parallel()
ctx := startTestSpan(ctx, t)

// Full spec includes sources, build steps, and a shell script artifact.
// The deps-only target should install only runtime deps (curl) and NOT
// include the built artifact (/usr/bin/my-script) or its implicit dep.
spec := fillMetadata("test-deps-only-full", &dalec.Spec{
Sources: map[string]dalec.Source{
"my-script": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "#!/usr/bin/env bash\necho hello from deps-only test\n",
Permissions: 0o700,
},
},
},
},
Build: dalec.ArtifactBuild{
Steps: []dalec.BuildStep{
{Command: "/bin/true"},
},
},
Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"my-script": {},
},
},
Dependencies: &dalec.PackageDependencies{
Runtime: map[string]dalec.PackageConstraints{
"curl": {},
},
},
})

testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
req := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(testConfig.Target.DepsOnly))
res := solveT(ctx, t, client, req)

ref, err := res.SingleRef()
assert.NilError(t, err)

// Runtime dep should be installed.
_, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/bin/curl"})
assert.NilError(t, err)

// The shell script artifact should NOT be present — deps-only
// never builds the package, so no artifacts are installed.
_, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/bin/my-script"})
assert.ErrorContains(t, err, "no such file")
})
})
}

func testLinuxSpec(t *testing.T, userSpec dalec.Spec) dalec.Spec {
t.Helper()

Expand Down
Loading
Loading