From 52fdee5c2f14349471eeadd5049a8a527eb182fc Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Tue, 19 Nov 2024 11:23:12 -0800 Subject: [PATCH 1/8] jammy: move some things around to prepare for more versions This is just shifting some bits raound to make more sense for adding other Ubuntu versions. Signed-off-by: Brian Goff --- cmd/frontend/main.go | 4 +- frontend/jammy/handler.go | 42 ------------ frontend/mux.go | 55 +++++++++++---- frontend/ubuntu/common.go | 33 +++++++++ frontend/ubuntu/jammy.go | 25 +++++++ test/jammy_test.go | 125 --------------------------------- test/ubuntu_test.go | 141 ++++++++++++++++++++++++++++++++++++++ test/windows_test.go | 8 +-- 8 files changed, 246 insertions(+), 187 deletions(-) delete mode 100644 frontend/jammy/handler.go create mode 100644 frontend/ubuntu/common.go create mode 100644 frontend/ubuntu/jammy.go delete mode 100644 test/jammy_test.go create mode 100644 test/ubuntu_test.go diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index 268a41658..36d3e65e3 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -7,7 +7,7 @@ import ( "github.com/Azure/dalec/frontend" "github.com/Azure/dalec/frontend/azlinux" "github.com/Azure/dalec/frontend/debug" - "github.com/Azure/dalec/frontend/jammy" + "github.com/Azure/dalec/frontend/ubuntu" "github.com/Azure/dalec/frontend/windows" "github.com/moby/buildkit/frontend/gateway/grpcclient" "github.com/moby/buildkit/util/appcontext" @@ -35,7 +35,7 @@ func main() { frontend.WithBuiltinHandler(azlinux.Mariner2TargetKey, azlinux.NewMariner2Handler()), frontend.WithBuiltinHandler(azlinux.AzLinux3TargetKey, azlinux.NewAzlinux3Handler()), frontend.WithBuiltinHandler(windows.DefaultTargetKey, windows.Handle), - frontend.WithBuiltinHandler(jammy.DefaultTargetKey, jammy.Handle), + ubuntu.Handlers, frontend.WithTargetForwardingHandler, )); err != nil { bklog.L.WithError(err).Fatal("error running frontend") diff --git a/frontend/jammy/handler.go b/frontend/jammy/handler.go deleted file mode 100644 index dd6a5b9ec..000000000 --- a/frontend/jammy/handler.go +++ /dev/null @@ -1,42 +0,0 @@ -package jammy - -import ( - "context" - - "github.com/Azure/dalec/frontend/deb" - "github.com/Azure/dalec/frontend/deb/distro" - gwclient "github.com/moby/buildkit/frontend/gateway/client" -) - -const ( - DefaultTargetKey = "jammy" - AptCachePrefix = "jammy" - JammyWorkerContextName = "dalec-jammy-worker" - - jammyRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:jammy" - versionID = "ubuntu22.04" -) - -func Handle(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { - cfg := &distro.Config{ - ImageRef: jammyRef, - AptCachePrefix: AptCachePrefix, - VersionID: versionID, - ContextRef: JammyWorkerContextName, - DefaultOutputImage: jammyRef, - BuilderPackages: []string{ - "aptitude", - "dpkg-dev", - "devscripts", - "equivs", - "fakeroot", - "dh-make", - "build-essential", - "dh-apparmor", - "dh-make", - "dh-exec", - "debhelper-compat=" + deb.DebHelperCompat, - }, - } - return cfg.Handle(ctx, client) -} diff --git a/frontend/mux.go b/frontend/mux.go index c329921f6..50c25134b 100644 --- a/frontend/mux.go +++ b/frontend/mux.go @@ -586,25 +586,52 @@ func WithTargetForwardingHandler(ctx context.Context, client gwclient.Client, m // These are only added if the target is in the spec OR the spec has no explicit targets. func WithBuiltinHandler(key string, bf gwclient.BuildFunc) func(context.Context, gwclient.Client, *BuildMux) error { return func(ctx context.Context, client gwclient.Client, m *BuildMux) error { - spec, err := m.loadSpec(ctx, client) - if err != nil { - return err + if !shouldLoadTarget(ctx, client, m, key) { + return nil } + m.Add(key, bf, nil) + return nil + } +} - if len(spec.Targets) > 0 { - t, ok := spec.Targets[key] - if !ok { - bklog.G(ctx).WithField("spec targets", maps.Keys(spec.Targets)).WithField("targetKey", key).Info("Target not in the spec, skipping") - return nil - } +// shouldLoadTarget is used to determine if the spec is overriding the built-in +// target with the same targetKey. +func shouldLoadTarget(ctx context.Context, client gwclient.Client, mux *BuildMux, targetKey string) bool { + spec, err := mux.loadSpec(ctx, client) + if err != nil { + Warnf(ctx, client, llb.Scratch(), "Cannot load target %s due to error loading spec: %v", targetKey, err) + return false + } + + if len(spec.Targets) == 0 { + return true + } - if t.Frontend != nil { - bklog.G(ctx).WithField("targetKey", key).Info("Target has custom frontend, skipping builtin-handler") - return nil + t, ok := spec.Targets[targetKey] + if !ok { + bklog.G(ctx).WithField("spec targets", maps.Keys(spec.Targets)).WithField("targetKey", targetKey).Info("Target not in the spec, skipping") + Warnf(ctx, client, llb.Scratch(), "Skipping loading of built-in target %q since it is not in the list of targets in the spec", targetKey) + return false + } + + if t.Frontend != nil { + bklog.G(ctx).WithField("targetKey", targetKey).Info("Target has custom frontend, skipping builtin-handler") + Warnf(ctx, client, llb.Scratch(), "Built-in target %q overwritten by target in spec", targetKey) + return false + } + + return true +} + +// LoadBuiltinTargets is like [WithBuiltinHandler] but accepts a mapping of handlers +// instead of one at a time. +func LoadBuiltinTargets(targets map[string]gwclient.BuildFunc) func(context.Context, gwclient.Client, *BuildMux) error { + return func(ctx context.Context, client gwclient.Client, mux *BuildMux) error { + for target, handler := range targets { + if shouldLoadTarget(ctx, client, mux, target) { + mux.Add(target, handler, nil) } } - - m.Add(key, bf, nil) return nil } } diff --git a/frontend/ubuntu/common.go b/frontend/ubuntu/common.go new file mode 100644 index 000000000..a81b9a309 --- /dev/null +++ b/frontend/ubuntu/common.go @@ -0,0 +1,33 @@ +package ubuntu + +import ( + "context" + + "github.com/Azure/dalec/frontend" + "github.com/Azure/dalec/frontend/deb" + gwclient "github.com/moby/buildkit/frontend/gateway/client" +) + +var ( + basePackages = []string{ + "aptitude", + "dpkg-dev", + "devscripts", + "equivs", + "fakeroot", + "dh-make", + "build-essential", + "dh-apparmor", + "dh-make", + "dh-exec", + "debhelper-compat=" + deb.DebHelperCompat, + } + + targets = map[string]gwclient.BuildFunc{ + JammyDefaultTargetKey: JammyConfig.Handle, // 22.04 + } +) + +func Handlers(ctx context.Context, client gwclient.Client, m *frontend.BuildMux) error { + return frontend.LoadBuiltinTargets(targets)(ctx, client, m) +} diff --git a/frontend/ubuntu/jammy.go b/frontend/ubuntu/jammy.go new file mode 100644 index 000000000..2590a5746 --- /dev/null +++ b/frontend/ubuntu/jammy.go @@ -0,0 +1,25 @@ +package ubuntu + +import ( + "github.com/Azure/dalec/frontend/deb/distro" +) + +const ( + JammyDefaultTargetKey = "jammy" + JammyAptCachePrefix = "jammy" + JammyWorkerContextName = "dalec-jammy-worker" + + jammyRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:jammy" + jammyVersionID = "ubuntu22.04" +) + +var ( + JammyConfig = &distro.Config{ + ImageRef: jammyRef, + AptCachePrefix: JammyAptCachePrefix, + VersionID: jammyVersionID, + ContextRef: JammyWorkerContextName, + DefaultOutputImage: jammyRef, + BuilderPackages: basePackages, + } +) diff --git a/test/jammy_test.go b/test/jammy_test.go deleted file mode 100644 index fdd295b09..000000000 --- a/test/jammy_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package test - -import ( - "fmt" - "testing" - - "github.com/Azure/dalec" - "github.com/Azure/dalec/frontend/jammy" - "github.com/moby/buildkit/client/llb" - ocispecs "github.com/opencontainers/image-spec/specs-go/v1" -) - -func signRepoJammy(gpgKey llb.State) llb.StateOption { - // key should be a state that has a public key under /public.key - return func(in llb.State) llb.State { - // assuming in is the state that has the repo files under / including - // Release file - return in.Run( - dalec.ShArgs("gpg --import < /tmp/gpg/private.key"), - llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), - dalec.ProgressGroup("Importing gpg key")). - Run( - dalec.ShArgs(`ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2) && \ - gpg --list-keys --keyid-format LONG && \ - gpg --default-key $ID -abs -o /opt/repo/Release.gpg /opt/repo/Release && \ - gpg --default-key "$ID" --clearsign -o /opt/repo/InRelease /opt/repo/Release`), - llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), - dalec.ProgressGroup("signing repo"), - ).Root() - } -} - -var jammyTestRepoConfig = func(name string) map[string]dalec.Source { - return map[string]dalec.Source{ - "local.list": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: fmt.Sprintf(`deb [signed-by=/usr/share/keyrings/%s] copy:/opt/repo/ /`, name), - }, - }, - }, - } -} - -func TestJammy(t *testing.T) { - t.Parallel() - - ctx := startTestSpan(baseCtx, t) - testLinuxDistro(ctx, t, testLinuxConfig{ - Target: targetConfig{ - Container: "jammy/testing/container", - Package: "jammy/deb", - Worker: "jammy/worker", - FormatDepEqual: func(ver, rev string) string { - return ver + "-ubuntu22.04u" + rev - }, - ListExpectedSignFiles: func(spec *dalec.Spec, platform ocispecs.Platform) []string { - base := fmt.Sprintf("%s_%s-%su%s", spec.Name, spec.Version, "ubuntu22.04", spec.Revision) - sourceBase := fmt.Sprintf("%s_%s.orig", spec.Name, spec.Version) - - out := []string{ - base + ".debian.tar.xz", - base + ".dsc", - fmt.Sprintf("%s_%s.deb", base, platform.Architecture), - base + "_source.buildinfo", - base + "_source.changes", - sourceBase + ".tar.xz", - } - - for src := range spec.Sources { - out = append(out, fmt.Sprintf("%s-%s.tar.gz", sourceBase, src)) - } - - return out - }, - }, - LicenseDir: "/usr/share/doc", - SystemdDir: struct { - Units string - Targets string - }{ - Units: "/lib/systemd", - Targets: "/etc/systemd/system", - }, - Worker: workerConfig{ - ContextName: jammy.JammyWorkerContextName, - // /pkg1.deb ... - CreateRepo: func(pkg llb.State, opts ...llb.StateOption) llb.StateOption { - repoFile := []byte(` -deb [trusted=yes] copy:/opt/repo/ / -`) - return func(in llb.State) llb.State { - withRepo := in.Run( - dalec.ShArgs("apt-get update && apt-get install -y apt-utils gnupg2"), - dalec.WithMountedAptCache(jammy.AptCachePrefix), - ).File(llb.Copy(pkg, "/", "/opt/repo")). - Run( - llb.Dir("/opt/repo"), - dalec.ShArgs("apt-ftparchive packages . > Packages"), - ). - Run( - llb.Dir("/opt/repo"), - dalec.ShArgs("apt-ftparchive release . > Release"), - ).Root() - - for _, opt := range opts { - withRepo = opt(withRepo) - } - - return withRepo. - File(llb.Mkfile("/etc/apt/sources.list.d/test-dalec-local-repo.list", 0o644, repoFile)) - } - }, - SignRepo: signRepoJammy, - TestRepoConfig: jammyTestRepoConfig, - Constraints: constraintsSymbols{ - Equal: "=", - GreaterThan: ">>", - GreaterThanOrEqual: ">=", - LessThan: "<<", - LessThanOrEqual: "<=", - }, - }, - }) -} diff --git a/test/ubuntu_test.go b/test/ubuntu_test.go new file mode 100644 index 000000000..7332131db --- /dev/null +++ b/test/ubuntu_test.go @@ -0,0 +1,141 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend/deb/distro" + "github.com/Azure/dalec/frontend/ubuntu" + "github.com/moby/buildkit/client/llb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" +) + +var ( + debConstraintsSymbols = constraintsSymbols{ + Equal: "=", + GreaterThan: ">>", + GreaterThanOrEqual: ">=", + LessThan: "<<", + LessThanOrEqual: "<=", + } +) + +func debLinuxTestConfigFor(targetKey string, cfg *distro.Config) testLinuxConfig { + return testLinuxConfig{ + Target: targetConfig{ + Container: targetKey + "/testing/container", + Package: targetKey + "/deb", + Worker: targetKey + "/worker", + FormatDepEqual: func(ver, rev string) string { + return ver + "-" + cfg.VersionID + "u" + rev + }, + ListExpectedSignFiles: debExpectedFiles(cfg.VersionID), + }, + LicenseDir: "/usr/share/doc", + SystemdDir: struct { + Units string + Targets string + }{ + Units: "/lib/systemd", + Targets: "/etc/systemd/system", + }, + Worker: workerConfig{ + ContextName: cfg.ContextRef, + // /pkg1.deb ... + CreateRepo: ubuntuCreateRepo(cfg), + SignRepo: signRepoUbuntu, + TestRepoConfig: ubuntuTestRepoConfig, + Constraints: debConstraintsSymbols, + }, + } +} + +func ubuntuCreateRepo(cfg *distro.Config) func(pkg llb.State, opts ...llb.StateOption) llb.StateOption { + return func(pkg llb.State, opts ...llb.StateOption) llb.StateOption { + repoFile := []byte(` +deb [trusted=yes] copy:/opt/repo/ / +`) + return func(in llb.State) llb.State { + withRepo := in.Run( + dalec.ShArgs("apt-get update && apt-get install -y apt-utils gnupg2"), + dalec.WithMountedAptCache(cfg.AptCachePrefix), + ).File(llb.Copy(pkg, "/", "/opt/repo")). + Run( + llb.Dir("/opt/repo"), + dalec.ShArgs("apt-ftparchive packages . > Packages"), + ). + Run( + llb.Dir("/opt/repo"), + dalec.ShArgs("apt-ftparchive release . > Release"), + ).Root() + + for _, opt := range opts { + withRepo = opt(withRepo) + } + + return withRepo. + File(llb.Mkfile("/etc/apt/sources.list.d/test-dalec-local-repo.list", 0o644, repoFile)) + } + } +} + +func signRepoUbuntu(gpgKey llb.State) llb.StateOption { + // key should be a state that has a public key under /public.key + return func(in llb.State) llb.State { + // assuming in is the state that has the repo files under / including + // Release file + return in.Run( + dalec.ShArgs("gpg --import < /tmp/gpg/private.key"), + llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), + dalec.ProgressGroup("Importing gpg key")). + Run( + dalec.ShArgs(`ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2) && \ + gpg --list-keys --keyid-format LONG && \ + gpg --default-key $ID -abs -o /opt/repo/Release.gpg /opt/repo/Release && \ + gpg --default-key "$ID" --clearsign -o /opt/repo/InRelease /opt/repo/Release`), + llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), + dalec.ProgressGroup("signing repo"), + ).Root() + } +} + +func ubuntuTestRepoConfig(name string) map[string]dalec.Source { + return map[string]dalec.Source{ + "local.list": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: fmt.Sprintf(`deb [signed-by=/usr/share/keyrings/%s] copy:/opt/repo/ /`, name), + }, + }, + }, + } +} + +func debExpectedFiles(ver string) func(*dalec.Spec, ocispecs.Platform) []string { + return func(spec *dalec.Spec, platform ocispecs.Platform) []string { + base := fmt.Sprintf("%s_%s-%su%s", spec.Name, spec.Version, ver, spec.Revision) + sourceBase := fmt.Sprintf("%s_%s.orig", spec.Name, spec.Version) + + out := []string{ + base + ".debian.tar.xz", + base + ".dsc", + fmt.Sprintf("%s_%s.deb", base, platform.Architecture), + base + "_source.buildinfo", + base + "_source.changes", + sourceBase + ".tar.xz", + } + + for src := range spec.Sources { + out = append(out, fmt.Sprintf("%s-%s.tar.gz", sourceBase, src)) + } + return out + } +} + +func TestJammy(t *testing.T) { + t.Parallel() + + ctx := startTestSpan(baseCtx, t) + testLinuxDistro(ctx, t, debLinuxTestConfigFor(ubuntu.JammyDefaultTargetKey, ubuntu.JammyConfig)) +} diff --git a/test/windows_test.go b/test/windows_test.go index 99810cbbf..e5c6fae1f 100644 --- a/test/windows_test.go +++ b/test/windows_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/dalec" - "github.com/Azure/dalec/frontend/jammy" + "github.com/Azure/dalec/frontend/ubuntu" "github.com/Azure/dalec/frontend/windows" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" @@ -49,8 +49,8 @@ func TestWindows(t *testing.T) { wcfg := workerConfig{ ContextName: windows.WindowscrossWorkerContextName, - SignRepo: signRepoJammy, - TestRepoConfig: jammyTestRepoConfig, + SignRepo: signRepoUbuntu, + TestRepoConfig: ubuntuTestRepoConfig, Platform: &windowsAmd64, Constraints: constraintsSymbols{ Equal: "=", @@ -66,7 +66,7 @@ deb [trusted=yes] copy:/opt/repo/ / `) withRepo := in.Run( dalec.ShArgs("apt-get update && apt-get install -y apt-utils gnupg2"), - dalec.WithMountedAptCache(jammy.AptCachePrefix), + dalec.WithMountedAptCache(ubuntu.JammyAptCachePrefix), ).File(llb.Copy(pkg, "/", "/opt/repo")). Run( llb.Dir("/opt/repo"), From d0d08667825e3434c21c6aa53b58ee0a9ddf19b3 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 20 Nov 2024 11:59:23 -0800 Subject: [PATCH 2/8] Add support for older go versions Older versions of Go that supported go modules didn't neccessarily support the `GOMODCACHE` env var. This change works around this limitation by linking our fetched go modules into the configured `$GOPATH` when this is the case. Additionally updates the gomod generator to not rely on `$GOMODCACHE`. Signed-off-by: Brian Goff --- frontend/deb/debroot.go | 10 ++++++++++ frontend/rpm/handle_sources.go | 8 ++++++++ generator_gomod.go | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/deb/debroot.go b/frontend/deb/debroot.go index abe499740..ae5685edc 100644 --- a/frontend/deb/debroot.go +++ b/frontend/deb/debroot.go @@ -223,6 +223,16 @@ func fixupSources(spec *dalec.Spec) []byte { fmt.Fprintln(buf) } + if spec.HasGomods() { + // Older go versions did not have support for the `GOMODCACHE` var + // This is a hack to try and make the build work by linking the go modules + // we've already fetched into to module dir under $GOPATH + // The default GOMODCACHE value is ${GOPATH}/pkg/mod. + fmt.Fprintf(buf, `test -n "$(go env GOMODCACHE)" || (GOPATH="$(go env GOPATH)"; mkdir -p "${GOPATH}/pkg" && ln -s "$(pwd)/%s" "${GOPATH}/pkg/mod")`, gomodsName) + // Above command does not have a newline due to quoting issues, so add that here. + fmt.Fprint(buf, "\n") + } + return buf.Bytes() } diff --git a/frontend/rpm/handle_sources.go b/frontend/rpm/handle_sources.go index d46ddacc8..80274e619 100644 --- a/frontend/rpm/handle_sources.go +++ b/frontend/rpm/handle_sources.go @@ -77,6 +77,14 @@ func buildScript(spec *dalec.Spec) string { fmt.Fprintln(b, "set -e") if spec.HasGomods() { + // Older go versions did not have support for the `GOMODCACHE` var + // This is a hack to try and make the build work by linking the go modules + // we've already fetched into to module dir under $GOPATH + // The default GOMODCACHE value is ${GOPATH}/pkg/mod. + fmt.Fprintf(b, `test -n "$(go env GOMODCACHE)" || (GOPATH="$(go env GOPATH)"; mkdir -p "${GOPATH}/pkg" && ln -s "$(pwd)/%s" "${GOPATH}/pkg/mod")`, gomodsName) + // Above command does not have a newline due to quoting issues, so add that here. + fmt.Fprint(b, "\n") + fmt.Fprintln(b, "export GOMODCACHE=\"$(pwd)/"+gomodsName+"\"") } diff --git a/generator_gomod.go b/generator_gomod.go index f4953c3f6..7b23ce1cb 100644 --- a/generator_gomod.go +++ b/generator_gomod.go @@ -44,7 +44,7 @@ func withGomod(g *SourceGenerator, srcSt, worker llb.State, opts ...llb.Constrai for _, path := range paths { in = worker.Run( ShArgs("go mod download"), - llb.AddEnv("GOMODCACHE", gomodCacheDir), + llb.AddEnv("GOPATH", "/go"), llb.Dir(filepath.Join(joinedWorkDir, path)), srcMount, WithConstraints(opts...), From d3c93fb542f1543850efee78df00b328d8dcb21c Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 20 Nov 2024 12:45:30 -0800 Subject: [PATCH 3/8] deb: Explicitly set $PATH to what's in worker image This prepares the deb packaging to allow worker images to update $PATH and make sure that is passed along to the build scripts so that the source package is more likely to be reproducible. This will be useful for supporting non-standard go packages in Ubuntu such as `golang-` which do not add the go bins anywhere in the standard $PATH. Signed-off-by: Brian Goff --- frontend/deb/debroot.go | 40 +++++++++++++++++++--------------- frontend/deb/distro/install.go | 2 +- frontend/deb/distro/pkg.go | 4 ++-- frontend/deb/pkg.go | 5 +++-- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/frontend/deb/debroot.go b/frontend/deb/debroot.go index ae5685edc..4b80f79ef 100644 --- a/frontend/deb/debroot.go +++ b/frontend/deb/debroot.go @@ -75,7 +75,7 @@ func sourcePatchesDir(sOpt dalec.SourceOpts, base llb.State, dir, name string, s // an upgrade even if it is technically the same underlying source. // It may be left blank but is highly recommended to set this. // Use [ReadDistroVersionID] to get a suitable value. -func Debroot(sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, target, dir, distroVersionID string, opts ...llb.ConstraintsOpt) (llb.State, error) { +func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, target, dir, distroVersionID string, opts ...llb.ConstraintsOpt) (llb.State, error) { control, err := controlFile(spec, in, target, dir) if err != nil { return llb.Scratch(), errors.Wrap(err, "error generating control file") @@ -122,10 +122,15 @@ func Debroot(sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, targ dalecDir := base. File(llb.Mkdir(filepath.Join(dir, "dalec"), 0o755), opts...) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/build.sh"), 0o700, createBuildScript(spec)), opts...)) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/patch.sh"), 0o700, createPatchScript(spec)), opts...)) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_sources.sh"), 0o700, fixupSources(spec)), opts...)) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_perms.sh"), 0o700, fixupArtifactPerms(spec)), opts...)) + pathVar, _, err := worker.GetEnv(ctx, "PATH", opts...) + if err != nil { + return in, fmt.Errorf("error looking up $PATH in worker image: %w", err) + } + + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/build.sh"), 0o700, createBuildScript(spec, pathVar)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/patch.sh"), 0o700, createPatchScript(spec, pathVar)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_sources.sh"), 0o700, fixupSources(spec, pathVar)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_perms.sh"), 0o700, fixupArtifactPerms(spec, pathVar)), opts...)) customEnable, err := customDHInstallSystemdPostinst(spec) if err != nil { @@ -161,12 +166,9 @@ func Debroot(sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, targ return dalec.MergeAtPath(in, states, "/"), nil } -func fixupArtifactPerms(spec *dalec.Spec) []byte { +func fixupArtifactPerms(spec *dalec.Spec, pathVar string) []byte { buf := bytes.NewBuffer(nil) - - fmt.Fprintln(buf, "#!/usr/bin/env sh") - fmt.Fprintln(buf, "set -ex") - fmt.Fprintln(buf) + writeScriptHeader(buf, pathVar) basePath := filepath.Join("debian", spec.Name) @@ -201,9 +203,9 @@ func fixupArtifactPerms(spec *dalec.Spec) []byte { // to bring those back. // // This is called from `debian/rules` after the source tarball has been extracted. -func fixupSources(spec *dalec.Spec) []byte { +func fixupSources(spec *dalec.Spec, pathVar string) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf) + writeScriptHeader(buf, pathVar) // now, we need to find all the sources that are file-backed and fix them up for name, src := range spec.Sources { @@ -236,17 +238,21 @@ func fixupSources(spec *dalec.Spec) []byte { return buf.Bytes() } -func writeScriptHeader(buf io.Writer) { +func writeScriptHeader(buf io.Writer, pathVar string) { fmt.Fprintln(buf, "#!/usr/bin/env sh") fmt.Fprintln(buf) fmt.Fprintln(buf, "set -ex") + + if pathVar != "" { + fmt.Fprintln(buf, `export PATH="`+pathVar+`"`) + } } -func createPatchScript(spec *dalec.Spec) []byte { +func createPatchScript(spec *dalec.Spec, pathVar string) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf) + writeScriptHeader(buf, pathVar) for name, patches := range spec.Patches { for _, patch := range patches { @@ -258,9 +264,9 @@ func createPatchScript(spec *dalec.Spec) []byte { return buf.Bytes() } -func createBuildScript(spec *dalec.Spec) []byte { +func createBuildScript(spec *dalec.Spec, pathVar string) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf) + writeScriptHeader(buf, pathVar) sorted := dalec.SortMapKeys(spec.Build.Env) for _, k := range sorted { diff --git a/frontend/deb/distro/install.go b/frontend/deb/distro/install.go index 6e777e754..08f370ef7 100644 --- a/frontend/deb/distro/install.go +++ b/frontend/deb/distro/install.go @@ -134,7 +134,7 @@ func (d *Config) InstallBuildDeps(sOpt dalec.SourceOpts, spec *dalec.Spec, targe opts := append(opts, dalec.ProgressGroup("Install build dependencies")) opts = append([]llb.ConstraintsOpt{dalec.WithConstraint(c)}, opts...) - srcPkg, err := deb.SourcePackage(sOpt, in, depsSpec, targetKey, "", opts...) + srcPkg, err := deb.SourcePackage(ctx, sOpt, in, depsSpec, targetKey, "", opts...) if err != nil { return in, err } diff --git a/frontend/deb/distro/pkg.go b/frontend/deb/distro/pkg.go index b8a8ac45e..5f2b3ddc3 100644 --- a/frontend/deb/distro/pkg.go +++ b/frontend/deb/distro/pkg.go @@ -26,7 +26,7 @@ func (d *Config) BuildDeb(ctx context.Context, worker llb.State, sOpt dalec.Sour } worker = worker.With(d.InstallBuildDeps(sOpt, spec, targetKey)) - srcPkg, err := deb.SourcePackage(sOpt, worker, spec, targetKey, versionID, opts...) + srcPkg, err := deb.SourcePackage(ctx, sOpt, worker, spec, targetKey, versionID, opts...) if err != nil { return worker, err } @@ -141,7 +141,7 @@ func (cfg *Config) HandleSourcePkg(ctx context.Context, client gwclient.Client) } worker = worker.With(cfg.InstallBuildDeps(sOpt, spec, targetKey, pg)) - st, err := deb.SourcePackage(sOpt, worker, spec, targetKey, versionID, pg) + st, err := deb.SourcePackage(ctx, sOpt, worker, spec, targetKey, versionID, pg) if err != nil { return nil, nil, errors.Wrap(err, "error building source package") } diff --git a/frontend/deb/pkg.go b/frontend/deb/pkg.go index eed4edaea..6c2671ecd 100644 --- a/frontend/deb/pkg.go +++ b/frontend/deb/pkg.go @@ -1,6 +1,7 @@ package deb import ( + "context" "fmt" "path/filepath" "strings" @@ -70,11 +71,11 @@ func createPatches(spec *dalec.Spec, sources map[string]llb.State, worker llb.St return patches } -func SourcePackage(sOpt dalec.SourceOpts, worker llb.State, spec *dalec.Spec, targetKey, distroVersionID string, opts ...llb.ConstraintsOpt) (llb.State, error) { +func SourcePackage(ctx context.Context, sOpt dalec.SourceOpts, worker llb.State, spec *dalec.Spec, targetKey, distroVersionID string, opts ...llb.ConstraintsOpt) (llb.State, error) { if err := validateSpec(spec); err != nil { return llb.Scratch(), err } - dr, err := Debroot(sOpt, spec, worker, llb.Scratch(), targetKey, "", distroVersionID) + dr, err := Debroot(ctx, sOpt, spec, worker, llb.Scratch(), targetKey, "", distroVersionID) if err != nil { return llb.Scratch(), err } From edccf37f5c1e88639aba4a14282a99f97c8cf7a1 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 20 Nov 2024 12:50:17 -0800 Subject: [PATCH 4/8] deb: Support looking up alternative go binaries Sometimes the `golang` package is just very old, but there may be other packages available like `golang-`, however those do not typically install the `go` binary such that it would be available in $PATH. This autoamtically looks up go and sets $PATH to include go in that path, but only when there is a `golang-` package in the dependency list. Signed-off-by: Brian Goff --- frontend/deb/distro/pkg.go | 66 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/frontend/deb/distro/pkg.go b/frontend/deb/distro/pkg.go index 5f2b3ddc3..1ea0488fb 100644 --- a/frontend/deb/distro/pkg.go +++ b/frontend/deb/distro/pkg.go @@ -2,10 +2,14 @@ package distro import ( "context" + "io/fs" + "path/filepath" + "strings" "github.com/Azure/dalec" "github.com/Azure/dalec/frontend" "github.com/Azure/dalec/frontend/deb" + "github.com/Azure/dalec/frontend/pkg/bkfs" "github.com/containerd/platforms" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" @@ -26,7 +30,7 @@ func (d *Config) BuildDeb(ctx context.Context, worker llb.State, sOpt dalec.Sour } worker = worker.With(d.InstallBuildDeps(sOpt, spec, targetKey)) - srcPkg, err := deb.SourcePackage(ctx, sOpt, worker, spec, targetKey, versionID, opts...) + srcPkg, err := deb.SourcePackage(ctx, sOpt, worker.With(ensureGolang(client, spec, targetKey, opts...)), spec, targetKey, versionID, opts...) if err != nil { return worker, err } @@ -40,6 +44,64 @@ func (d *Config) BuildDeb(ctx context.Context, worker llb.State, sOpt dalec.Sour return frontend.MaybeSign(ctx, client, st, spec, targetKey, sOpt) } +// ensureGolang is a work-around for the case where the base distro golang package +// is too old, but other packages are provided (e.g. `golang-1.22`) and those +// other packages don't actually add go tools to $PATH. +// It assumes if you added one of these go packages and there is no `go` in $PATH +// that you probably wanted to use that version of go. +func ensureGolang(client gwclient.Client, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption { + return func(in llb.State) llb.State { + deps := spec.GetBuildDeps(targetKey) + if _, hasNormalGo := deps["golang"]; hasNormalGo { + return in + } + + return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { + var candidates []string + for dep := range deps { + if strings.HasPrefix(dep, "golang-") { + // Get the base version component + _, ver, _ := strings.Cut(dep, "-") + // Trim off any potential extra stuff like `golang-1.20-go` (ie the `-go` bit) + // This is just for having definitive search paths to check it should + // not be an issue if this is not like the above example and its + // something else like `-doc` since we are still going to check the + // binary exists anyway (plus this would be highly unlikely in any case). + ver, _, _ = strings.Cut(ver, "-") + candidates = append(candidates, "usr/lib/go-"+ver+"/bin") + } + } + + if len(candidates) == 0 { + return in, nil + } + + opts := []llb.ConstraintsOpt{dalec.WithConstraint(c), dalec.WithConstraints(opts...)} + + pathVar, _, err := in.GetEnv(ctx, "PATH", opts...) + if err != nil { + return in, err + } + + stfs, err := bkfs.FromState(ctx, &in, client, opts...) + if err != nil { + return in, err + } + + for _, p := range candidates { + _, err := fs.Stat(stfs, filepath.Join(p, "go")) + if err == nil { + // bkfs does not allow a leading `/` in the stat path per spec for [fs.FS] + // Add that in here + p := "/" + p + return in.AddEnv("PATH", p+":"+pathVar), nil + } + } + return in, nil + }) + } +} + func (cfg *Config) HandleDeb(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) { sOpt, err := frontend.SourceOptFromClient(ctx, client) @@ -141,7 +203,7 @@ func (cfg *Config) HandleSourcePkg(ctx context.Context, client gwclient.Client) } worker = worker.With(cfg.InstallBuildDeps(sOpt, spec, targetKey, pg)) - st, err := deb.SourcePackage(ctx, sOpt, worker, spec, targetKey, versionID, pg) + st, err := deb.SourcePackage(ctx, sOpt, worker.With(ensureGolang(client, spec, targetKey, pg)), spec, targetKey, versionID, pg) if err != nil { return nil, nil, errors.Wrap(err, "error building source package") } From 467cc2ede6233cb02f45ef14d29a1527728605e2 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 20 Nov 2024 12:54:32 -0800 Subject: [PATCH 5/8] deb: Remove debhelper-compat from worker base install The package is not available on all debian distros but is instead listed as a `Provides` from the `debhelper` package. The debhelper-compat build dep is still part of the generated debian control file so the compat level is still ensured. This is required to support distros that do not have an explicit package called `debhelper-compat` and instead only lists it in the `Provides` section of the `debhelper` package. Signed-off-by: Brian Goff --- frontend/ubuntu/common.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/ubuntu/common.go b/frontend/ubuntu/common.go index a81b9a309..d3254703c 100644 --- a/frontend/ubuntu/common.go +++ b/frontend/ubuntu/common.go @@ -4,7 +4,6 @@ import ( "context" "github.com/Azure/dalec/frontend" - "github.com/Azure/dalec/frontend/deb" gwclient "github.com/moby/buildkit/frontend/gateway/client" ) @@ -20,7 +19,6 @@ var ( "dh-apparmor", "dh-make", "dh-exec", - "debhelper-compat=" + deb.DebHelperCompat, } targets = map[string]gwclient.BuildFunc{ From 223d0575a4b46fa52d64fa68016ae5e90439fd16 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 20 Nov 2024 15:25:28 -0800 Subject: [PATCH 6/8] Add support for more Ubuntu verisons Adds 18.04, 20.04, and 24.04 (in addition to 22.04 which was already there). For tests, given that Ubuntu <= 20.04 have a "golang" package that is too old to run the tests, I added a way to override the package name used in the test. Another minor modification due to Bionic not supporting `execute_after_dh_fixperms`. Signed-off-by: Brian Goff --- frontend/deb/pkg.go | 2 +- frontend/deb/template_rules.go | 7 ++++- frontend/mux.go | 10 +++++++ frontend/ubuntu/bionic.go | 25 +++++++++++++++++ frontend/ubuntu/common.go | 5 +++- frontend/ubuntu/focal.go | 25 +++++++++++++++++ frontend/ubuntu/noble.go | 25 +++++++++++++++++ frontend/windows/handler.go | 3 +- test/azlinux_test.go | 16 +++++++++-- test/ubuntu_test.go | 40 ++++++++++++++++++++++++-- website/docs/examples/targets.md | 37 ++++++++++++++++++++++++ website/docs/targets.md | 48 ++++++++++++++++++-------------- 12 files changed, 212 insertions(+), 31 deletions(-) create mode 100644 frontend/ubuntu/bionic.go create mode 100644 frontend/ubuntu/focal.go create mode 100644 frontend/ubuntu/noble.go create mode 100644 website/docs/examples/targets.md diff --git a/frontend/deb/pkg.go b/frontend/deb/pkg.go index 6c2671ecd..2378a0afd 100644 --- a/frontend/deb/pkg.go +++ b/frontend/deb/pkg.go @@ -15,7 +15,7 @@ const ( // Unique name that would not normally be in the spec // This will get used to create the source tar for go module deps gomodsName = "xxxdalecGomodsInternal" - DebHelperCompat = "13" + DebHelperCompat = "11" ) func mountSources(sources map[string]llb.State, dir string, mod func(string) string) llb.RunOption { diff --git a/frontend/deb/template_rules.go b/frontend/deb/template_rules.go index 4a901d0a4..599e88b54 100644 --- a/frontend/deb/template_rules.go +++ b/frontend/deb/template_rules.go @@ -81,7 +81,12 @@ func (w *rulesWrapper) OverridePerms() fmt.Stringer { } if fixPerms { - b.WriteString("execute_after_dh_fixperms:\n") + // Normally this should be `execute_after_dh_fixperms`, however this doesn't + // work on Ubuntu 18.04. + // Instead we need to override dh_fixperms and run it ourselves and then + // our extra script. + b.WriteString("override_dh_fixperms:\n") + b.WriteString("\tdh_fixperms\n") b.WriteString("\tdebian/dalec/fix_perms.sh\n\n") } diff --git a/frontend/mux.go b/frontend/mux.go index 50c25134b..8ff47ba81 100644 --- a/frontend/mux.go +++ b/frontend/mux.go @@ -596,6 +596,16 @@ func WithBuiltinHandler(key string, bf gwclient.BuildFunc) func(context.Context, // shouldLoadTarget is used to determine if the spec is overriding the built-in // target with the same targetKey. +// +// When there is nothing specified in `spec.Targets` this always returns true. +// +// When `spec.Targets` is populated but the provided targetKey does not appear +// in `spec.Targets` this returns false. +// +// When the provided tgargetKey is in `spec.Targets` but the target spec defines +// a frontend to forward to, this returns false. +// +// Otherwise true. func shouldLoadTarget(ctx context.Context, client gwclient.Client, mux *BuildMux, targetKey string) bool { spec, err := mux.loadSpec(ctx, client) if err != nil { diff --git a/frontend/ubuntu/bionic.go b/frontend/ubuntu/bionic.go new file mode 100644 index 000000000..5ec5ba67f --- /dev/null +++ b/frontend/ubuntu/bionic.go @@ -0,0 +1,25 @@ +package ubuntu + +import ( + "github.com/Azure/dalec/frontend/deb/distro" +) + +const ( + BionicDefaultTargetKey = "bionic" + BionicAptCachePrefix = "bionic" + BionicWorkerContextName = "dalec-bionic-worker" + + bionicRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:bionic" + bionicVersionID = "ubuntu18.04" +) + +var ( + BionicConfig = &distro.Config{ + ImageRef: bionicRef, + AptCachePrefix: BionicAptCachePrefix, + VersionID: bionicVersionID, + ContextRef: BionicWorkerContextName, + DefaultOutputImage: bionicRef, + BuilderPackages: basePackages, + } +) diff --git a/frontend/ubuntu/common.go b/frontend/ubuntu/common.go index d3254703c..e594b0cbc 100644 --- a/frontend/ubuntu/common.go +++ b/frontend/ubuntu/common.go @@ -22,7 +22,10 @@ var ( } targets = map[string]gwclient.BuildFunc{ - JammyDefaultTargetKey: JammyConfig.Handle, // 22.04 + BionicDefaultTargetKey: BionicConfig.Handle, // 18.04 + FocalDefaultTargetKey: FocalConfig.Handle, // 20.04 + JammyDefaultTargetKey: JammyConfig.Handle, // 22.04 + NobleDefaultTargetKey: NobleConfig.Handle, // 24.04 } ) diff --git a/frontend/ubuntu/focal.go b/frontend/ubuntu/focal.go new file mode 100644 index 000000000..554ad830f --- /dev/null +++ b/frontend/ubuntu/focal.go @@ -0,0 +1,25 @@ +package ubuntu + +import ( + "github.com/Azure/dalec/frontend/deb/distro" +) + +const ( + FocalDefaultTargetKey = "focal" + FocalAptCachePrefix = "focal" + FocalWorkerContextName = "dalec-focal-worker" + + focalRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:focal" + focalVersionID = "ubuntu20.04" +) + +var ( + FocalConfig = &distro.Config{ + ImageRef: focalRef, + AptCachePrefix: FocalAptCachePrefix, + VersionID: focalVersionID, + ContextRef: FocalWorkerContextName, + DefaultOutputImage: focalRef, + BuilderPackages: basePackages, + } +) diff --git a/frontend/ubuntu/noble.go b/frontend/ubuntu/noble.go new file mode 100644 index 000000000..97f3ba8c7 --- /dev/null +++ b/frontend/ubuntu/noble.go @@ -0,0 +1,25 @@ +package ubuntu + +import ( + "github.com/Azure/dalec/frontend/deb/distro" +) + +const ( + NobleDefaultTargetKey = "noble" + NobleAptCachePrefix = "noble" + NobleWorkerContextName = "dalec-noble-worker" + + nobleRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:noble" + nobleVersionID = "ubuntu24.04" +) + +var ( + NobleConfig = &distro.Config{ + ImageRef: nobleRef, + AptCachePrefix: NobleAptCachePrefix, + VersionID: nobleVersionID, + ContextRef: NobleWorkerContextName, + DefaultOutputImage: nobleRef, + BuilderPackages: basePackages, + } +) diff --git a/frontend/windows/handler.go b/frontend/windows/handler.go index 5ec62a58e..01a7102e5 100644 --- a/frontend/windows/handler.go +++ b/frontend/windows/handler.go @@ -6,7 +6,6 @@ import ( "github.com/Azure/dalec" "github.com/Azure/dalec/frontend" - "github.com/Azure/dalec/frontend/deb" "github.com/Azure/dalec/frontend/deb/distro" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb/sourceresolver" @@ -40,7 +39,7 @@ var ( "zip", "aptitude", "dpkg-dev", - "debhelper-compat=" + deb.DebHelperCompat, + "debhelper", }, } ) diff --git a/test/azlinux_test.go b/test/azlinux_test.go index 9c0019e4c..0b00445e8 100644 --- a/test/azlinux_test.go +++ b/test/azlinux_test.go @@ -225,6 +225,10 @@ type targetConfig struct { // Given a spec, list all files (including the full path) that are expected // to be sent to be signed. ListExpectedSignFiles func(*dalec.Spec, ocispecs.Platform) []string + + // PackageOverrides is useful for replacing packages used in tests (such as `golang`) + // with alternative ones. + PackageOverrides map[string]string } type testLinuxConfig struct { @@ -243,6 +247,14 @@ type OSRelease struct { VersionID string } +func (cfg *testLinuxConfig) GetPackage(name string) string { + updated := cfg.Target.PackageOverrides[name] + if updated != "" { + return updated + } + return name +} + func testLinuxDistro(ctx context.Context, t *testing.T, testConfig testLinuxConfig) { t.Run("Fail when non-zero exit code during build", func(t *testing.T) { t.Parallel() @@ -875,9 +887,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot }, Dependencies: &dalec.PackageDependencies{ Build: map[string]dalec.PackageConstraints{ - // TODO: This works at least for now, but is distro specific and - // could break on new distros (though that is still unlikely). - "golang": {}, + testConfig.GetPackage("golang"): {}, }, }, Build: dalec.ArtifactBuild{ diff --git a/test/ubuntu_test.go b/test/ubuntu_test.go index 7332131db..294545eec 100644 --- a/test/ubuntu_test.go +++ b/test/ubuntu_test.go @@ -21,8 +21,18 @@ var ( } ) -func debLinuxTestConfigFor(targetKey string, cfg *distro.Config) testLinuxConfig { - return testLinuxConfig{ +func withPackageOverride(oldPkg, newPkg string) func(cfg *testLinuxConfig) { + return func(cfg *testLinuxConfig) { + if cfg.Target.PackageOverrides == nil { + cfg.Target.PackageOverrides = make(map[string]string) + } + + cfg.Target.PackageOverrides[oldPkg] = newPkg + } +} + +func debLinuxTestConfigFor(targetKey string, cfg *distro.Config, opts ...func(*testLinuxConfig)) testLinuxConfig { + tlc := testLinuxConfig{ Target: targetConfig{ Container: targetKey + "/testing/container", Package: targetKey + "/deb", @@ -49,6 +59,11 @@ func debLinuxTestConfigFor(targetKey string, cfg *distro.Config) testLinuxConfig Constraints: debConstraintsSymbols, }, } + + for _, o := range opts { + o(&tlc) + } + return tlc } func ubuntuCreateRepo(cfg *distro.Config) func(pkg llb.State, opts ...llb.StateOption) llb.StateOption { @@ -139,3 +154,24 @@ func TestJammy(t *testing.T) { ctx := startTestSpan(baseCtx, t) testLinuxDistro(ctx, t, debLinuxTestConfigFor(ubuntu.JammyDefaultTargetKey, ubuntu.JammyConfig)) } + +func TestNoble(t *testing.T) { + t.Parallel() + + ctx := startTestSpan(baseCtx, t) + testLinuxDistro(ctx, t, debLinuxTestConfigFor(ubuntu.NobleDefaultTargetKey, ubuntu.NobleConfig)) +} + +func TestFocal(t *testing.T) { + t.Parallel() + + ctx := startTestSpan(baseCtx, t) + testLinuxDistro(ctx, t, debLinuxTestConfigFor(ubuntu.FocalDefaultTargetKey, ubuntu.FocalConfig, withPackageOverride("golang", "golang-1.22"))) +} + +func TestBionic(t *testing.T) { + t.Parallel() + + ctx := startTestSpan(baseCtx, t) + testLinuxDistro(ctx, t, debLinuxTestConfigFor(ubuntu.BionicDefaultTargetKey, ubuntu.BionicConfig, withPackageOverride("golang", "golang-1.18"))) +} diff --git a/website/docs/examples/targets.md b/website/docs/examples/targets.md new file mode 100644 index 000000000..6033e48a6 --- /dev/null +++ b/website/docs/examples/targets.md @@ -0,0 +1,37 @@ +TARGET DESCRIPTION +azlinux3/container (default) Builds a container image for Azure Linux 3 +azlinux3/container/depsonly Builds a container image with only the runtime dependencies installed. +azlinux3/rpm Builds an rpm and src.rpm. +azlinux3/rpm/debug/buildroot Outputs an rpm buildroot suitable for passing to rpmbuild. +azlinux3/rpm/debug/sources Outputs all the sources specified in the spec file in the format given to rpmbuild. +azlinux3/rpm/debug/spec Outputs the generated RPM spec file +azlinux3/worker Builds the base worker image responsible for building the rpm +bionic/deb (default) Builds a deb package. +bionic/dsc Builds a Debian source package. +bionic/testing/container Builds a container image for testing purposes only. +bionic/worker Builds the worker image. +debug/gomods Outputs all the gomodule dependencies for the spec +debug/resolve Outputs the resolved dalec spec file with build args applied. +debug/sources Outputs all sources from a dalec spec file. +focal/deb (default) Builds a deb package. +focal/dsc Builds a Debian source package. +focal/testing/container Builds a container image for testing purposes only. +focal/worker Builds the worker image. +jammy/deb (default) Builds a deb package. +jammy/dsc Builds a Debian source package. +jammy/testing/container Builds a container image for testing purposes only. +jammy/worker Builds the worker image. +mariner2/container (default) Builds a container image for CBL-Mariner 2 +mariner2/container/depsonly Builds a container image with only the runtime dependencies installed. +mariner2/rpm Builds an rpm and src.rpm. +mariner2/rpm/debug/buildroot Outputs an rpm buildroot suitable for passing to rpmbuild. +mariner2/rpm/debug/sources Outputs all the sources specified in the spec file in the format given to rpmbuild. +mariner2/rpm/debug/spec Outputs the generated RPM spec file +mariner2/worker Builds the base worker image responsible for building the rpm +noble/deb (default) Builds a deb package. +noble/dsc Builds a Debian source package. +noble/testing/container Builds a container image for testing purposes only. +noble/worker Builds the worker image. +windowscross/container (default) Builds binaries and installs them into a Windows base image +windowscross/worker Builds the base worker image responsible for building the package +windowscross/zip Builds binaries combined into a zip file diff --git a/website/docs/targets.md b/website/docs/targets.md index c9d7666e8..4f88a2b2f 100644 --- a/website/docs/targets.md +++ b/website/docs/targets.md @@ -11,29 +11,35 @@ Many components, such as package dependencies and base images, are specific to a To print a list of available build targets: ```shell -GET DESCRIPTION -azlinux3/container (default) Builds a container image with azlinux3 as the base image. -azlinux3/container/depsonly Builds a container image with only the runtime dependencies installed. -azlinux3/rpm Builds an rpm and src.rpm. -azlinux3/rpm/debug/buildroot Outputs an rpm buildroot suitable for passing to rpmbuild. -azlinux3/rpm/debug/sources Outputs all the sources specified in the spec file in the format given to rpmbuild. -azlinux3/rpm/debug/spec Outputs the generated RPM spec file -azlinux3/worker Builds the base worker image responsible for building the rpm -debug/gomods Outputs all the gomodule dependencies for the spec -debug/resolve Outputs the resolved dalec spec file with build args applied. -debug/sources Outputs all sources from a dalec spec file. -mariner2/container (default) Builds a container image with mariner2 as the base image. -mariner2/container/depsonly Builds a container image with only the runtime dependencies installed. -mariner2/rpm Builds an rpm and src.rpm. -mariner2/rpm/debug/buildroot Outputs an rpm buildroot suitable for passing to rpmbuild. -mariner2/rpm/debug/sources Outputs all the sources specified in the spec file in the format given to rpmbuild. -mariner2/rpm/debug/spec Outputs the generated RPM spec file -mariner2/worker Builds the base worker image responsible for building the rpm -windowscross/container (default) Builds binaries and installs them into a Windows base image -windowscross/worker Builds the base worker image responsible for building the rpm -windowscross/zip Builds binaries combined into a zip file +$ docker buildx build --call targets --build-arg BUILDKIT_SYNTAX=ghcr.io/azure/dalec/frontend:latest - <<< "null" ``` +import TargetsCLIOut from './examples/targets.md' + +
+DALEC targets list output +
+
+ +:::note +The above command is passing in a "null" value as the build spec and telling +buildkit to use the latest dalec version. +This output can change depending on version or spec you provide. +::: + +To check the targets available for a specific spec you can just add `--call targets` +to your normal `docker build` command: + +```shell +$ docker buildx build --call targets -f ./path/to/spec . +``` + +If the `--target=` flag is set, the list of targets will be filtered based +on ``. + +Likewise if the spec file contains items in the `targets` section then the list +of available targets will be filtered to just the targets in the spec. + ## Dependencies Instead of specifying a package dependency at the root of the spec, you can specify it under a target. From 82ca3dba1dcc831d32f7ce3504c7133bf60fb824 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 20 Nov 2024 17:33:45 -0800 Subject: [PATCH 7/8] Add support for Debian distros This adds Debian Bullseye and Bookworm support. The only real change needed here was to update the distro config to allow us to enable the `bullseye-backports` repo so the tests can install a more recent version of go (and as such pass the test). Signed-off-by: Brian Goff --- cmd/frontend/main.go | 2 ++ frontend/deb/distro/container.go | 5 +++- frontend/deb/distro/distro.go | 5 ++++ frontend/deb/distro/install.go | 11 ++++++-- frontend/deb/distro/worker.go | 1 + frontend/debian/bookworm.go | 25 ++++++++++++++++++ frontend/debian/bullseye.go | 44 ++++++++++++++++++++++++++++++++ frontend/debian/common.go | 32 +++++++++++++++++++++++ helpers.go | 14 ++++++++++ spec.go | 12 --------- test/debian_test.go | 21 +++++++++++++++ website/docs/examples/targets.md | 8 ++++++ 12 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 frontend/debian/bookworm.go create mode 100644 frontend/debian/bullseye.go create mode 100644 frontend/debian/common.go create mode 100644 test/debian_test.go diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index 36d3e65e3..6940ea153 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -6,6 +6,7 @@ import ( "github.com/Azure/dalec/frontend" "github.com/Azure/dalec/frontend/azlinux" + "github.com/Azure/dalec/frontend/debian" "github.com/Azure/dalec/frontend/debug" "github.com/Azure/dalec/frontend/ubuntu" "github.com/Azure/dalec/frontend/windows" @@ -36,6 +37,7 @@ func main() { frontend.WithBuiltinHandler(azlinux.AzLinux3TargetKey, azlinux.NewAzlinux3Handler()), frontend.WithBuiltinHandler(windows.DefaultTargetKey, windows.Handle), ubuntu.Handlers, + debian.Handlers, frontend.WithTargetForwardingHandler, )); err != nil { bklog.L.WithError(err).Fatal("error running frontend") diff --git a/frontend/deb/distro/container.go b/frontend/deb/distro/container.go index 20a6a08c2..d3f74ee98 100644 --- a/frontend/deb/distro/container.go +++ b/frontend/deb/distro/container.go @@ -23,7 +23,10 @@ func (c *Config) BuildContainer(worker llb.State, sOpt dalec.SourceOpts, client opts = append(opts, dalec.ProgressGroup("Build Container Image")) - withRepos, err := c.RepoMounts(spec.GetInstallRepos(targetKey), sOpt, opts...) + repos := dalec.GetExtraRepos(c.ExtraRepos, "install") + repos = append(repos, spec.GetInstallRepos(targetKey)...) + + withRepos, err := c.RepoMounts(repos, sOpt, opts...) if err != nil { return llb.Scratch(), err } diff --git a/frontend/deb/distro/distro.go b/frontend/deb/distro/distro.go index 9e41efe01..42ab0be30 100644 --- a/frontend/deb/distro/distro.go +++ b/frontend/deb/distro/distro.go @@ -32,6 +32,11 @@ type Config struct { RepoPlatformConfig *dalec.RepoPlatformConfig DefaultOutputImage string + + // ExtraRepos is used by distributions that want to enable extra repositories + // that are not inthe base worker config. + // A prime example of this is adding Debian backports on debian distrubutions. + ExtraRepos []dalec.PackageRepositoryConfig } func (cfg *Config) BuildImageConfig(ctx context.Context, resolver llb.ImageMetaResolver, spec *dalec.Spec, platform *ocispecs.Platform, targetKey string) (*dalec.DockerImageSpec, error) { diff --git a/frontend/deb/distro/install.go b/frontend/deb/distro/install.go index 08f370ef7..4d1811c51 100644 --- a/frontend/deb/distro/install.go +++ b/frontend/deb/distro/install.go @@ -144,7 +144,10 @@ func (d *Config) InstallBuildDeps(sOpt dalec.SourceOpts, spec *dalec.Spec, targe return in, errors.Wrap(err, "error creating intermediate package for installing build dependencies") } - customRepos, err := d.RepoMounts(spec.GetBuildRepos(targetKey), sOpt, opts...) + repos := dalec.GetExtraRepos(d.ExtraRepos, "build") + repos = append(repos, spec.GetBuildRepos(targetKey)...) + + customRepos, err := d.RepoMounts(repos, sOpt, opts...) if err != nil { return in, err } @@ -153,6 +156,7 @@ func (d *Config) InstallBuildDeps(sOpt dalec.SourceOpts, spec *dalec.Spec, targe dalec.WithConstraints(opts...), customRepos, InstallLocalPkg(pkg, opts...), + dalec.WithMountedAptCache(d.AptCachePrefix), ).Root(), nil }) } @@ -166,7 +170,10 @@ func (d *Config) InstallTestDeps(sOpt dalec.SourceOpts, targetKey string, spec * return func(in llb.State) llb.State { return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { - withRepos, err := d.RepoMounts(spec.GetTestRepos(targetKey), sOpt, opts...) + repos := dalec.GetExtraRepos(d.ExtraRepos, "test") + repos = append(repos, spec.GetTestRepos(targetKey)...) + + withRepos, err := d.RepoMounts(repos, sOpt, opts...) if err != nil { return in, err } diff --git a/frontend/deb/distro/worker.go b/frontend/deb/distro/worker.go index 8bdf19cc8..b532e4cf7 100644 --- a/frontend/deb/distro/worker.go +++ b/frontend/deb/distro/worker.go @@ -79,6 +79,7 @@ func (cfg *Config) Worker(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (ll Run( dalec.WithConstraints(opts...), AptInstall(cfg.BuilderPackages, opts...), + dalec.WithMountedAptCache(cfg.AptCachePrefix), ). // This file prevents installation of things like docs in ubuntu // containers We don't want to exclude this because tests want to diff --git a/frontend/debian/bookworm.go b/frontend/debian/bookworm.go new file mode 100644 index 000000000..8eeac6e98 --- /dev/null +++ b/frontend/debian/bookworm.go @@ -0,0 +1,25 @@ +package debian + +import ( + "github.com/Azure/dalec/frontend/deb/distro" +) + +const ( + BookwormDefaultTargetKey = "bookworm" + BookwormAptCachePrefix = "bookworm" + BookwormWorkerContextName = "dalec-bookworm-worker" + + bookwormRef = "mcr.microsoft.com/mirror/docker/library/debian:bookworm" + bookwormVersionID = "debian12" +) + +var ( + BookwormConfig = &distro.Config{ + ImageRef: bookwormRef, + AptCachePrefix: BookwormAptCachePrefix, + VersionID: bookwormVersionID, + ContextRef: BookwormWorkerContextName, + DefaultOutputImage: bookwormRef, + BuilderPackages: basePackages, + } +) diff --git a/frontend/debian/bullseye.go b/frontend/debian/bullseye.go new file mode 100644 index 000000000..276f0d16d --- /dev/null +++ b/frontend/debian/bullseye.go @@ -0,0 +1,44 @@ +package debian + +import ( + "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend/deb/distro" +) + +const ( + BullseyeDefaultTargetKey = "bullseye" + BullseyeAptCachePrefix = "bullseye" + BullseyeWorkerContextName = "dalec-bullseye-worker" + + bullseyeRef = "mcr.microsoft.com/mirror/docker/library/debian:bullseye" + bullseyeVersionID = "debian11" +) + +var ( + BullseyeConfig = &distro.Config{ + ImageRef: bullseyeRef, + AptCachePrefix: BullseyeAptCachePrefix, + VersionID: bullseyeVersionID, + ContextRef: BullseyeWorkerContextName, + DefaultOutputImage: bullseyeRef, + BuilderPackages: basePackages, + + // Ubuntu typically has backports repos already in it but Debian does not. + // Without this the go modules test will fail since there is no viable + // version of go except with the backports repository added. + ExtraRepos: []dalec.PackageRepositoryConfig{ + { + Envs: []string{"build", "test", "install"}, + Config: map[string]dalec.Source{ + "backports.list": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "deb http://deb.debian.org/debian bullseye-backports main", + }, + }, + }, + }, + }, + }, + } +) diff --git a/frontend/debian/common.go b/frontend/debian/common.go new file mode 100644 index 000000000..f54b2d11a --- /dev/null +++ b/frontend/debian/common.go @@ -0,0 +1,32 @@ +package debian + +import ( + "context" + + "github.com/Azure/dalec/frontend" + gwclient "github.com/moby/buildkit/frontend/gateway/client" +) + +var ( + basePackages = []string{ + "aptitude", + "dpkg-dev", + "devscripts", + "equivs", + "fakeroot", + "dh-make", + "build-essential", + "dh-apparmor", + "dh-make", + "dh-exec", + } + + targets = map[string]gwclient.BuildFunc{ + BookwormDefaultTargetKey: BookwormConfig.Handle, + BullseyeDefaultTargetKey: BullseyeConfig.Handle, + } +) + +func Handlers(ctx context.Context, client gwclient.Client, m *frontend.BuildMux) error { + return frontend.LoadBuiltinTargets(targets)(ctx, client, m) +} diff --git a/helpers.go b/helpers.go index cf6f1ea2b..12f9fe11b 100644 --- a/helpers.go +++ b/helpers.go @@ -605,3 +605,17 @@ func BaseImageConfig(platform *ocispecs.Platform) *DockerImageSpec { return img } + +func (p *PackageDependencies) GetExtraRepos(env string) []PackageRepositoryConfig { + return GetExtraRepos(p.ExtraRepos, env) +} + +func GetExtraRepos(repos []PackageRepositoryConfig, env string) []PackageRepositoryConfig { + var out []PackageRepositoryConfig + for _, repo := range repos { + if slices.Contains(repo.Envs, env) { + out = append(repos, repo) + } + } + return out +} diff --git a/spec.go b/spec.go index a788c19cf..c26a0c34f 100644 --- a/spec.go +++ b/spec.go @@ -5,7 +5,6 @@ import ( "fmt" "io/fs" "regexp" - "slices" "strings" "time" @@ -359,17 +358,6 @@ type PackageDependencies struct { ExtraRepos []PackageRepositoryConfig `yaml:"extra_repos,omitempty" json:"extra_repos,omitempty"` } -func (p *PackageDependencies) GetExtraRepos(env string) []PackageRepositoryConfig { - var repos []PackageRepositoryConfig - for _, repo := range p.ExtraRepos { - if slices.Contains(repo.Envs, env) { - repos = append(repos, repo) - } - } - - return repos -} - // PackageRepositoryConfig type PackageRepositoryConfig struct { // Keys are the list of keys that need to be imported to use the configured diff --git a/test/debian_test.go b/test/debian_test.go new file mode 100644 index 000000000..5815976d9 --- /dev/null +++ b/test/debian_test.go @@ -0,0 +1,21 @@ +package test + +import ( + "testing" + + "github.com/Azure/dalec/frontend/debian" +) + +func TestBookworm(t *testing.T) { + t.Parallel() + + ctx := startTestSpan(baseCtx, t) + testLinuxDistro(ctx, t, debLinuxTestConfigFor(debian.BookwormDefaultTargetKey, debian.BookwormConfig)) +} + +func TestBullseye(t *testing.T) { + t.Parallel() + + ctx := startTestSpan(baseCtx, t) + testLinuxDistro(ctx, t, debLinuxTestConfigFor(debian.BullseyeDefaultTargetKey, debian.BullseyeConfig, withPackageOverride("golang", "golang-1.19"))) +} diff --git a/website/docs/examples/targets.md b/website/docs/examples/targets.md index 6033e48a6..7ae6eb303 100644 --- a/website/docs/examples/targets.md +++ b/website/docs/examples/targets.md @@ -10,6 +10,14 @@ bionic/deb (default) Builds a deb package. bionic/dsc Builds a Debian source package. bionic/testing/container Builds a container image for testing purposes only. bionic/worker Builds the worker image. +bookworm/deb (default) Builds a deb package. +bookworm/dsc Builds a Debian source package. +bookworm/testing/container Builds a container image for testing purposes only. +bookworm/worker Builds the worker image. +bullseye/deb (default) Builds a deb package. +bullseye/dsc Builds a Debian source package. +bullseye/testing/container Builds a container image for testing purposes only. +bullseye/worker Builds the worker image. debug/gomods Outputs all the gomodule dependencies for the spec debug/resolve Outputs the resolved dalec spec file with build args applied. debug/sources Outputs all sources from a dalec spec file. From a76c31e1be4792213ed75bbfb4c24bf8dae7921d Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 21 Nov 2024 13:14:27 -0800 Subject: [PATCH 8/8] deb: Add prepend/append for paths instead of replacing full $PATH This makes the build a bit more friendly to building outside of DALEC where we don't mess with *their* $PATH except by adding the paths we need instead of replacing the whole thing. Signed-off-by: Brian Goff --- frontend/deb/debroot.go | 68 +++++++++++------ frontend/deb/distro/install.go | 2 +- frontend/deb/distro/pkg.go | 131 +++++++++++++++++++++------------ frontend/deb/pkg.go | 4 +- 4 files changed, 134 insertions(+), 71 deletions(-) diff --git a/frontend/deb/debroot.go b/frontend/deb/debroot.go index 4b80f79ef..1f8026972 100644 --- a/frontend/deb/debroot.go +++ b/frontend/deb/debroot.go @@ -65,6 +65,25 @@ func sourcePatchesDir(sOpt dalec.SourceOpts, base llb.State, dir, name string, s return append(states, series), nil } +type SourcePkgConfig struct { + // PrependPath is a list of paths to be prepended to the $PATH var in build + // scripts. + PrependPath []string + // AppendPath is a list of paths to be appended to the $PATH var in build + // scripts. + AppendPath []string +} + +// Addpath creates a SourcePkgConfig where the first argument is sets +// [SourcePkgConfig.PrependPath] and the 2nd argument sets +// [SourcePkgConfig.AppendPath] +func AddPath(pre, post []string) SourcePkgConfig { + return SourcePkgConfig{ + PrependPath: pre, + AppendPath: post, + } +} + // Debroot creates a debian root directory suitable for use with debbuild. // This does not include sources in case you want to mount sources (instead of copying them) later. // @@ -75,7 +94,7 @@ func sourcePatchesDir(sOpt dalec.SourceOpts, base llb.State, dir, name string, s // an upgrade even if it is technically the same underlying source. // It may be left blank but is highly recommended to set this. // Use [ReadDistroVersionID] to get a suitable value. -func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, target, dir, distroVersionID string, opts ...llb.ConstraintsOpt) (llb.State, error) { +func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, target, dir, distroVersionID string, cfg SourcePkgConfig, opts ...llb.ConstraintsOpt) (llb.State, error) { control, err := controlFile(spec, in, target, dir) if err != nil { return llb.Scratch(), errors.Wrap(err, "error generating control file") @@ -122,15 +141,10 @@ func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worke dalecDir := base. File(llb.Mkdir(filepath.Join(dir, "dalec"), 0o755), opts...) - pathVar, _, err := worker.GetEnv(ctx, "PATH", opts...) - if err != nil { - return in, fmt.Errorf("error looking up $PATH in worker image: %w", err) - } - - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/build.sh"), 0o700, createBuildScript(spec, pathVar)), opts...)) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/patch.sh"), 0o700, createPatchScript(spec, pathVar)), opts...)) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_sources.sh"), 0o700, fixupSources(spec, pathVar)), opts...)) - states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_perms.sh"), 0o700, fixupArtifactPerms(spec, pathVar)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/build.sh"), 0o700, createBuildScript(spec, &cfg)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/patch.sh"), 0o700, createPatchScript(spec, &cfg)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_sources.sh"), 0o700, fixupSources(spec, &cfg)), opts...)) + states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_perms.sh"), 0o700, fixupArtifactPerms(spec, &cfg)), opts...)) customEnable, err := customDHInstallSystemdPostinst(spec) if err != nil { @@ -166,9 +180,9 @@ func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worke return dalec.MergeAtPath(in, states, "/"), nil } -func fixupArtifactPerms(spec *dalec.Spec, pathVar string) []byte { +func fixupArtifactPerms(spec *dalec.Spec, cfg *SourcePkgConfig) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf, pathVar) + writeScriptHeader(buf, cfg) basePath := filepath.Join("debian", spec.Name) @@ -203,9 +217,9 @@ func fixupArtifactPerms(spec *dalec.Spec, pathVar string) []byte { // to bring those back. // // This is called from `debian/rules` after the source tarball has been extracted. -func fixupSources(spec *dalec.Spec, pathVar string) []byte { +func fixupSources(spec *dalec.Spec, cfg *SourcePkgConfig) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf, pathVar) + writeScriptHeader(buf, cfg) // now, we need to find all the sources that are file-backed and fix them up for name, src := range spec.Sources { @@ -238,21 +252,33 @@ func fixupSources(spec *dalec.Spec, pathVar string) []byte { return buf.Bytes() } -func writeScriptHeader(buf io.Writer, pathVar string) { +func setupPathVar(pre, post []string) string { + if len(pre) == 0 && len(post) == 0 { + return "" + } + + full := append(pre, "$PATH") + full = append(full, post...) + return strings.Join(full, ":") +} + +func writeScriptHeader(buf io.Writer, cfg *SourcePkgConfig) { fmt.Fprintln(buf, "#!/usr/bin/env sh") fmt.Fprintln(buf) fmt.Fprintln(buf, "set -ex") - if pathVar != "" { - fmt.Fprintln(buf, `export PATH="`+pathVar+`"`) + if cfg != nil { + if pathVar := setupPathVar(cfg.PrependPath, cfg.AppendPath); pathVar != "" { + fmt.Fprintln(buf, "export PATH="+pathVar) + } } } -func createPatchScript(spec *dalec.Spec, pathVar string) []byte { +func createPatchScript(spec *dalec.Spec, cfg *SourcePkgConfig) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf, pathVar) + writeScriptHeader(buf, cfg) for name, patches := range spec.Patches { for _, patch := range patches { @@ -264,9 +290,9 @@ func createPatchScript(spec *dalec.Spec, pathVar string) []byte { return buf.Bytes() } -func createBuildScript(spec *dalec.Spec, pathVar string) []byte { +func createBuildScript(spec *dalec.Spec, cfg *SourcePkgConfig) []byte { buf := bytes.NewBuffer(nil) - writeScriptHeader(buf, pathVar) + writeScriptHeader(buf, cfg) sorted := dalec.SortMapKeys(spec.Build.Env) for _, k := range sorted { diff --git a/frontend/deb/distro/install.go b/frontend/deb/distro/install.go index 4d1811c51..111985ec1 100644 --- a/frontend/deb/distro/install.go +++ b/frontend/deb/distro/install.go @@ -134,7 +134,7 @@ func (d *Config) InstallBuildDeps(sOpt dalec.SourceOpts, spec *dalec.Spec, targe opts := append(opts, dalec.ProgressGroup("Install build dependencies")) opts = append([]llb.ConstraintsOpt{dalec.WithConstraint(c)}, opts...) - srcPkg, err := deb.SourcePackage(ctx, sOpt, in, depsSpec, targetKey, "", opts...) + srcPkg, err := deb.SourcePackage(ctx, sOpt, in, depsSpec, targetKey, "", deb.SourcePkgConfig{}, opts...) if err != nil { return in, err } diff --git a/frontend/deb/distro/pkg.go b/frontend/deb/distro/pkg.go index 1ea0488fb..87d529819 100644 --- a/frontend/deb/distro/pkg.go +++ b/frontend/deb/distro/pkg.go @@ -30,7 +30,14 @@ func (d *Config) BuildDeb(ctx context.Context, worker llb.State, sOpt dalec.Sour } worker = worker.With(d.InstallBuildDeps(sOpt, spec, targetKey)) - srcPkg, err := deb.SourcePackage(ctx, sOpt, worker.With(ensureGolang(client, spec, targetKey, opts...)), spec, targetKey, versionID, opts...) + + var cfg deb.SourcePkgConfig + extraPaths, err := prepareGo(ctx, client, &cfg, worker, spec, targetKey, opts...) + if err != nil { + return worker, err + } + + srcPkg, err := deb.SourcePackage(ctx, sOpt, worker.With(extraPaths), spec, targetKey, versionID, cfg, opts...) if err != nil { return worker, err } @@ -44,60 +51,83 @@ func (d *Config) BuildDeb(ctx context.Context, worker llb.State, sOpt dalec.Sour return frontend.MaybeSign(ctx, client, st, spec, targetKey, sOpt) } -// ensureGolang is a work-around for the case where the base distro golang package -// is too old, but other packages are provided (e.g. `golang-1.22`) and those -// other packages don't actually add go tools to $PATH. -// It assumes if you added one of these go packages and there is no `go` in $PATH -// that you probably wanted to use that version of go. -func ensureGolang(client gwclient.Client, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption { - return func(in llb.State) llb.State { - deps := spec.GetBuildDeps(targetKey) - if _, hasNormalGo := deps["golang"]; hasNormalGo { - return in +func noOpStateOpt(in llb.State) llb.State { + return in +} + +func prepareGo(ctx context.Context, client gwclient.Client, cfg *deb.SourcePkgConfig, worker llb.State, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) (llb.StateOption, error) { + goBin, err := searchForAltGolang(ctx, client, spec, targetKey, worker, opts...) + if err != nil { + return noOpStateOpt, errors.Wrap(err, "error while looking for alternate go bin path") + } + + if goBin == "" { + return noOpStateOpt, nil + } + cfg.PrependPath = append(cfg.PrependPath, goBin) + return addPaths([]string{goBin}, opts...), nil +} + +func searchForAltGolang(ctx context.Context, client gwclient.Client, spec *dalec.Spec, targetKey string, in llb.State, opts ...llb.ConstraintsOpt) (string, error) { + if !spec.HasGomods() { + return "", nil + } + var candidates []string + + deps := spec.GetBuildDeps(targetKey) + if _, hasNormalGo := deps["golang"]; hasNormalGo { + return "", nil + } + + for dep := range deps { + if strings.HasPrefix(dep, "golang-") { + // Get the base version component + _, ver, _ := strings.Cut(dep, "-") + // Trim off any potential extra stuff like `golang-1.20-go` (ie the `-go` bit) + // This is just for having definitive search paths to check it should + // not be an issue if this is not like the above example and its + // something else like `-doc` since we are still going to check the + // binary exists anyway (plus this would be highly unlikely in any case). + ver, _, _ = strings.Cut(ver, "-") + candidates = append(candidates, "usr/lib/go-"+ver+"/bin") } + } - return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { - var candidates []string - for dep := range deps { - if strings.HasPrefix(dep, "golang-") { - // Get the base version component - _, ver, _ := strings.Cut(dep, "-") - // Trim off any potential extra stuff like `golang-1.20-go` (ie the `-go` bit) - // This is just for having definitive search paths to check it should - // not be an issue if this is not like the above example and its - // something else like `-doc` since we are still going to check the - // binary exists anyway (plus this would be highly unlikely in any case). - ver, _, _ = strings.Cut(ver, "-") - candidates = append(candidates, "usr/lib/go-"+ver+"/bin") - } - } + if len(candidates) == 0 { + return "", nil + } - if len(candidates) == 0 { - return in, nil - } + stfs, err := bkfs.FromState(ctx, &in, client, opts...) + if err != nil { + return "", err + } - opts := []llb.ConstraintsOpt{dalec.WithConstraint(c), dalec.WithConstraints(opts...)} + for _, p := range candidates { + _, err := fs.Stat(stfs, filepath.Join(p, "go")) + if err == nil { + // bkfs does not allow a leading `/` in the stat path per spec for [fs.FS] + // Add that in here + p := "/" + p + return p, nil + } + } - pathVar, _, err := in.GetEnv(ctx, "PATH", opts...) - if err != nil { - return in, err - } + return "", nil +} - stfs, err := bkfs.FromState(ctx, &in, client, opts...) +// prepends the provided values to $PATH +func addPaths(paths []string, opts ...llb.ConstraintsOpt) llb.StateOption { + return func(in llb.State) llb.State { + if len(paths) == 0 { + return in + } + return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { + opts := []llb.ConstraintsOpt{dalec.WithConstraint(c), dalec.WithConstraints(opts...)} + pathEnv, _, err := in.GetEnv(ctx, "PATH", opts...) if err != nil { return in, err } - - for _, p := range candidates { - _, err := fs.Stat(stfs, filepath.Join(p, "go")) - if err == nil { - // bkfs does not allow a leading `/` in the stat path per spec for [fs.FS] - // Add that in here - p := "/" + p - return in.AddEnv("PATH", p+":"+pathVar), nil - } - } - return in, nil + return in.AddEnv("PATH", strings.Join(append(paths, pathEnv), ":")), nil }) } } @@ -203,7 +233,14 @@ func (cfg *Config) HandleSourcePkg(ctx context.Context, client gwclient.Client) } worker = worker.With(cfg.InstallBuildDeps(sOpt, spec, targetKey, pg)) - st, err := deb.SourcePackage(ctx, sOpt, worker.With(ensureGolang(client, spec, targetKey, pg)), spec, targetKey, versionID, pg) + + var cfg deb.SourcePkgConfig + extraPaths, err := prepareGo(ctx, client, &cfg, worker, spec, targetKey, pg) + if err != nil { + return nil, nil, err + } + + st, err := deb.SourcePackage(ctx, sOpt, worker.With(extraPaths), spec, targetKey, versionID, cfg, pg) if err != nil { return nil, nil, errors.Wrap(err, "error building source package") } diff --git a/frontend/deb/pkg.go b/frontend/deb/pkg.go index 2378a0afd..0907388d7 100644 --- a/frontend/deb/pkg.go +++ b/frontend/deb/pkg.go @@ -71,11 +71,11 @@ func createPatches(spec *dalec.Spec, sources map[string]llb.State, worker llb.St return patches } -func SourcePackage(ctx context.Context, sOpt dalec.SourceOpts, worker llb.State, spec *dalec.Spec, targetKey, distroVersionID string, opts ...llb.ConstraintsOpt) (llb.State, error) { +func SourcePackage(ctx context.Context, sOpt dalec.SourceOpts, worker llb.State, spec *dalec.Spec, targetKey, distroVersionID string, cfg SourcePkgConfig, opts ...llb.ConstraintsOpt) (llb.State, error) { if err := validateSpec(spec); err != nil { return llb.Scratch(), err } - dr, err := Debroot(ctx, sOpt, spec, worker, llb.Scratch(), targetKey, "", distroVersionID) + dr, err := Debroot(ctx, sOpt, spec, worker, llb.Scratch(), targetKey, "", distroVersionID, cfg, opts...) if err != nil { return llb.Scratch(), err }