targets/linux/deb/distro/install.go (145 lines of code) (raw):

package distro import ( "context" "path/filepath" "strconv" "github.com/Azure/dalec" "github.com/Azure/dalec/packaging/linux/deb" "github.com/moby/buildkit/client/llb" "github.com/pkg/errors" ) // AptInstall returns an [llb.RunOption] that uses apt to install the provided // packages. // // This returns an [llb.RunOption] but it does create some things internally, // This is what the constraints opts are used for. // The constraints are applied after any constraint set on the [llb.ExecInfo] func AptInstall(packages []string, opts ...llb.ConstraintsOpt) llb.RunOption { return dalec.RunOptFunc(func(ei *llb.ExecInfo) { const installScript = `#!/usr/bin/env sh set -ex # Make sure any cached data from local repos is purged since this should not # be shared between builds. rm -f /var/lib/apt/lists/_* apt autoclean -y # Remove any previously failed attempts to get repo data rm -rf /var/lib/apt/lists/partial/* apt update apt install -y "$@" ` script := llb.Scratch().File( llb.Mkfile("install.sh", 0o755, []byte(installScript)), dalec.WithConstraint(&ei.Constraints), dalec.WithConstraints(opts...), ) p := "/tmp/dalec/internal/deb/install.sh" llb.AddMount(p, script, llb.SourcePath("install.sh")).SetRunOption(ei) llb.AddEnv("DEBIAN_FRONTEND", "noninteractive").SetRunOption(ei) llb.Args(append([]string{p}, packages...)).SetRunOption(ei) }) } // InstallLocalPkg installs all deb packages found in the root of the provided [llb.State] // // In some cases, with strict version constraints in the package's dependencies, // this will use `aptitude` to help resolve those dependencies since apt is // currently unable to handle strict constraints. // // This returns an [llb.RunOption] but it does create some things internally, // This is what the constraints opts are used for. // The constraints are applied after any constraint set on the [llb.ExecInfo] func InstallLocalPkg(pkg llb.State, upgrade bool, opts ...llb.ConstraintsOpt) llb.RunOption { return dalec.RunOptFunc(func(ei *llb.ExecInfo) { // The apt solver always tries to select the latest package version even // when constraints specify that an older version should be installed and // that older version is available in a repo. This leads the solver to // simply refuse to install our target package if the latest version of ANY // dependency package is incompatible with the constraints. To work around // this we first install the .deb for the package with dpkg, specifically // ignoring any dependencies so that we can avoid the constraints issue. // We then use aptitude to fix the (possibly broken) install of the // package, and we pass the aptitude solver a hint to REJECT any solution // that involves uninstalling the package. This forces aptitude to find a // solution that will respect the constraints even if the solution involves // pinning dependency packages to older versions. const installScript = `#!/usr/bin/env sh set -ex # Make sure any cached data from local repos is purged since this should not # be shared between builds. rm -f /var/lib/apt/lists/_* apt autoclean -y # Remove any previously failed attempts to get repo data rm -rf /var/lib/apt/lists/partial/* apt update if [ "${DALEC_UPGRADE}" = "true" ]; then apt dist-upgrade -y fi if apt install -y ${1}; then exit 0 fi if ! command -v aptitude > /dev/null; then needs_cleanup=1 apt install -y aptitude fi cleanup() { if [ "${needs_cleanup}" = "1" ]; then apt remove -y aptitude fi } trap cleanup EXIT dpkg -i --force-depends ${1} pkg_name="$(dpkg-deb -f ${1} | grep 'Package:' | awk -F': ' '{ print $2 }')" aptitude install -y -f -o "Aptitude::ProblemResolver::Hints::=reject ${pkg_name} :UNINST" ` script := llb.Scratch().File( llb.Mkfile("install.sh", 0o755, []byte(installScript)), dalec.WithConstraint(&ei.Constraints), dalec.WithConstraints(opts...), ) p := "/tmp/dalec/internal/deb/install-with-constraints.sh" debPath := "/tmp/dalec/internal/debs" llb.AddMount(p, script, llb.SourcePath("install.sh")).SetRunOption(ei) llb.AddMount(debPath, pkg, llb.Readonly).SetRunOption(ei) llb.AddEnv("DEBIAN_FRONTEND", "noninteractive").SetRunOption(ei) llb.AddEnv("DALEC_UPGRADE", strconv.FormatBool(upgrade)).SetRunOption(ei) args := []string{p, filepath.Join(debPath, "*.deb")} llb.Args(args).SetRunOption(ei) }) } func (d *Config) InstallBuildDeps(sOpt dalec.SourceOpts, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption { return func(in llb.State) llb.State { buildDeps := spec.GetBuildDeps(targetKey) if len(buildDeps) == 0 { return in } depsSpec := &dalec.Spec{ Name: spec.Name + "-build-deps", Packager: "Dalec", Version: spec.Version, Revision: spec.Revision, Dependencies: &dalec.PackageDependencies{ Runtime: buildDeps, }, Description: "Build dependencies for " + spec.Name, } return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { opts := append(opts, dalec.ProgressGroup("Install build dependencies")) opts = append([]llb.ConstraintsOpt{dalec.WithConstraint(c)}, opts...) debRoot, err := deb.Debroot(ctx, sOpt, depsSpec, in, llb.Scratch(), targetKey, "", d.VersionID, deb.SourcePkgConfig{}, opts...) if err != nil { return in, err } pkg, err := deb.BuildDebBinaryOnly(in, depsSpec, debRoot, "", opts...) if err != nil { return in, errors.Wrap(err, "error creating intermediate package for installing build dependencies") } 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 } return in.Run( dalec.WithConstraints(opts...), customRepos, InstallLocalPkg(pkg, false, opts...), dalec.WithMountedAptCache(d.AptCachePrefix), ).Root(), nil }) } } func (d *Config) InstallTestDeps(sOpt dalec.SourceOpts, targetKey string, spec *dalec.Spec, opts ...llb.ConstraintsOpt) llb.StateOption { deps := spec.GetTestDeps(targetKey) if len(deps) == 0 { return func(s llb.State) llb.State { return s } } return func(in llb.State) llb.State { return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { 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 } opts = append(opts, dalec.ProgressGroup("Install test dependencies")) return in.Run( dalec.WithConstraints(opts...), AptInstall(deps, opts...), withRepos, dalec.WithMountedAptCache(d.AptCachePrefix), ).Root(), nil }) } }