Files
yay/pkg/sync/build/installer.go
T
Ferdinand Bachmann ec837c831d fix(installer): Fixes the same pkgbase being built multiple times (#2534)
* fix(installer): Fixes the same pkgbase being built multiple times

When building a PKGBUILD pkgbase with multiple pkgnames,
installAURPackages() invokes buildPkg() multiple times for the same
pkgbase. This causes prepare() to be run multiple times for the same
pkgbase, since detection of already built packages happens after
prepare().

Additionally, detection of already built packages can fail if the split
debug packages are enabled and the package does not contain any
binaries, causing no -debug package to be created by makepkg even though
it is listed by makepkg --packagelist.

This commit fixes this by keeping track of the pkgdests built by
buildPkg() and avoiding rebuilds of the same pkgbase in the same yay
invocation.

Fixes #2340.

Signed-off-by: Ferdinand Bachmann <ferdinand.bachmann@yrlf.at>

* fix(installer): Fixes buildPkg() isTarget param being order-dependent

Previously, the buildPkg invocation for a pkgbase only considered
whether the current pkgname is part of installer.origTargets. This made
the decision whether to rebuild the package order-dependent.

This commit fixes this by keeping track of which pkgbases are part of
installer.origTargets and rebuilding the pkgbase if any of its pkgnames
is part of origTargets.

* fix(tests): Test that installing split packages avoids rebuilds

The previous two commits changed how split packages (packages with the same
pkgbase) are built, ensuring that those packages aren't built multiple
times.

This commit updates the lists of commands that the tests expect to be
run so that `makepkg` isn't run multiple times per pkgbase.

---------

Signed-off-by: Ferdinand Bachmann <ferdinand.bachmann@yrlf.at>
2024-11-19 11:08:28 +01:00

472 lines
13 KiB
Go

package build
import (
"context"
"fmt"
"os"
"github.com/Jguer/yay/v12/pkg/db"
"github.com/Jguer/yay/v12/pkg/dep"
"github.com/Jguer/yay/v12/pkg/multierror"
"github.com/Jguer/yay/v12/pkg/settings"
"github.com/Jguer/yay/v12/pkg/settings/exe"
"github.com/Jguer/yay/v12/pkg/settings/parser"
"github.com/Jguer/yay/v12/pkg/text"
"github.com/Jguer/yay/v12/pkg/vcs"
mapset "github.com/deckarep/golang-set/v2"
"github.com/leonelquinteros/gotext"
)
type (
PostInstallHookFunc func(ctx context.Context) error
Installer struct {
dbExecutor db.Executor
postInstallHooks []PostInstallHookFunc
failedAndIgnored map[string]error
exeCmd exe.ICmdBuilder
vcsStore vcs.Store
targetMode parser.TargetMode
rebuildMode parser.RebuildMode
origTargets mapset.Set[string]
downloadOnly bool
log *text.Logger
manualConfirmRequired bool
}
)
func NewInstaller(dbExecutor db.Executor,
exeCmd exe.ICmdBuilder, vcsStore vcs.Store, targetMode parser.TargetMode,
rebuildMode parser.RebuildMode, downloadOnly bool, logger *text.Logger,
) *Installer {
return &Installer{
dbExecutor: dbExecutor,
postInstallHooks: []PostInstallHookFunc{},
failedAndIgnored: map[string]error{},
exeCmd: exeCmd,
vcsStore: vcsStore,
targetMode: targetMode,
rebuildMode: rebuildMode,
downloadOnly: downloadOnly,
log: logger,
manualConfirmRequired: true,
}
}
func (installer *Installer) CompileFailedAndIgnored() (map[string]error, error) {
if len(installer.failedAndIgnored) == 0 {
return installer.failedAndIgnored, nil
}
return installer.failedAndIgnored, &FailedIgnoredPkgError{
pkgErrors: installer.failedAndIgnored,
}
}
func (installer *Installer) AddPostInstallHook(hook PostInstallHookFunc) {
if hook == nil {
return
}
installer.postInstallHooks = append(installer.postInstallHooks, hook)
}
func (installer *Installer) RunPostInstallHooks(ctx context.Context) error {
var errMulti multierror.MultiError
for _, hook := range installer.postInstallHooks {
if err := hook(ctx); err != nil {
errMulti.Add(err)
}
}
return errMulti.Return()
}
func (installer *Installer) Install(ctx context.Context,
cmdArgs *parser.Arguments,
targets []map[string]*dep.InstallInfo,
pkgBuildDirs map[string]string,
excluded []string,
manualConfirmRequired bool,
) error {
installer.log.Debugln("manualConfirmRequired:", manualConfirmRequired)
installer.manualConfirmRequired = manualConfirmRequired
installer.origTargets = mapset.NewThreadUnsafeSet[string]()
for _, targetString := range cmdArgs.Targets {
installer.origTargets.Add(dep.ToTarget(targetString).Name)
}
installer.log.Debugln("origTargets:", installer.origTargets)
// Reorganize targets into layers of dependencies
var errMulti multierror.MultiError
for i := len(targets) - 1; i >= 0; i-- {
lastLayer := i == 0
errI := installer.handleLayer(ctx, cmdArgs, targets[i], pkgBuildDirs, lastLayer, excluded)
if errI == nil && lastLayer {
// success after rollups
return nil
}
if errI != nil {
errMulti.Add(errI)
if lastLayer {
break
}
// rollup
installer.log.Warnln(gotext.Get("Failed to install layer, rolling up to next layer."), "error:", errI)
targets[i-1] = mergeLayers(targets[i-1], targets[i])
}
}
return errMulti.Return()
}
func mergeLayers(layer1, layer2 map[string]*dep.InstallInfo) map[string]*dep.InstallInfo {
for name, info := range layer2 {
layer1[name] = info
}
return layer1
}
func (installer *Installer) appendNoConfirm() bool {
return !installer.manualConfirmRequired || settings.NoConfirm
}
func (installer *Installer) handleLayer(ctx context.Context,
cmdArgs *parser.Arguments,
layer map[string]*dep.InstallInfo,
pkgBuildDirs map[string]string,
lastLayer bool,
excluded []string,
) error {
// Install layer
nameToBaseMap := make(map[string]string, 0)
syncDeps, syncExp, syncGroups := mapset.NewThreadUnsafeSet[string](),
mapset.NewThreadUnsafeSet[string](), mapset.NewThreadUnsafeSet[string]()
aurDeps, aurExp, aurOrigTargetBases := mapset.NewThreadUnsafeSet[string](),
mapset.NewThreadUnsafeSet[string](), mapset.NewThreadUnsafeSet[string]()
upgradeSync := false
for name, info := range layer {
switch info.Source {
case dep.AUR, dep.SrcInfo:
nameToBaseMap[name] = *info.AURBase
if installer.origTargets.Contains(name) {
aurOrigTargetBases.Add(*info.AURBase)
}
switch info.Reason {
case dep.Explicit:
if cmdArgs.ExistsArg("asdeps", "asdep") {
aurDeps.Add(name)
} else {
aurExp.Add(name)
}
case dep.Dep, dep.MakeDep, dep.CheckDep:
aurDeps.Add(name)
}
case dep.Sync:
if info.Upgrade {
upgradeSync = true
continue // do not add to targets, let pacman handle it
}
compositePkgName := fmt.Sprintf("%s/%s", *info.SyncDBName, name)
if info.IsGroup {
syncGroups.Add(compositePkgName)
continue
}
switch info.Reason {
case dep.Explicit:
if cmdArgs.ExistsArg("asdeps", "asdep") {
syncDeps.Add(compositePkgName)
} else {
syncExp.Add(compositePkgName)
}
case dep.Dep, dep.MakeDep, dep.CheckDep:
syncDeps.Add(compositePkgName)
}
}
}
installer.log.Debugln("syncDeps", syncDeps, "SyncExp", syncExp,
"aurDeps", aurDeps, "aurExp", aurExp, "upgrade", upgradeSync)
errShow := installer.installSyncPackages(ctx, cmdArgs, syncDeps, syncExp, syncGroups,
excluded, upgradeSync, installer.appendNoConfirm())
if errShow != nil {
return ErrInstallRepoPkgs
}
errAur := installer.installAURPackages(ctx, cmdArgs, aurDeps, aurExp,
aurOrigTargetBases, nameToBaseMap, pkgBuildDirs, true, lastLayer,
installer.appendNoConfirm())
return errAur
}
func (installer *Installer) installAURPackages(ctx context.Context,
cmdArgs *parser.Arguments,
aurDepNames, aurExpNames, aurOrigTargetBases mapset.Set[string],
nameToBase, pkgBuildDirsByBase map[string]string,
installIncompatible bool,
lastLayer bool,
noConfirm bool,
) error {
all := aurDepNames.Union(aurExpNames).ToSlice()
if len(all) == 0 {
return nil
}
builtPkgDests := make(map[string]map[string]string)
deps, exps := make([]string, 0, aurDepNames.Cardinality()), make([]string, 0, aurExpNames.Cardinality())
pkgArchives := make([]string, 0, len(exps)+len(deps))
for _, name := range all {
base := nameToBase[name]
dir := pkgBuildDirsByBase[base]
pkgdests, ok := builtPkgDests[base]
if ok {
installer.log.Debugln("skipping built pkgbase", base, "package", name)
} else {
var errMake error
installer.log.Debugln("building pkgbase", base, "package", name)
pkgdests, errMake = installer.buildPkg(ctx, dir, base,
installIncompatible, cmdArgs.ExistsArg("needed"), aurOrigTargetBases.Contains(base))
if errMake != nil {
if !lastLayer {
return fmt.Errorf("%s - %w", gotext.Get("error making: %s", base), errMake)
}
installer.failedAndIgnored[name] = errMake
installer.log.Errorln(gotext.Get("error making: %s", base), "-", errMake)
continue
}
builtPkgDests[base] = pkgdests
}
if len(pkgdests) == 0 {
installer.log.Warnln(gotext.Get("nothing to install for %s", text.Cyan(base)))
continue
}
newPKGArchives, hasDebug, err := installer.getNewTargets(pkgdests, name)
if err != nil {
return err
}
pkgArchives = append(pkgArchives, newPKGArchives...)
if isDep := installer.isDep(cmdArgs, aurExpNames, name); isDep {
deps = append(deps, name)
} else {
exps = append(exps, name)
}
if hasDebug {
deps = append(deps, name+"-debug")
}
}
if err := installPkgArchive(ctx, installer.exeCmd, installer.targetMode,
installer.vcsStore, cmdArgs, pkgArchives, noConfirm); err != nil {
return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
}
if err := setInstallReason(ctx, installer.exeCmd, installer.targetMode, cmdArgs, deps, exps); err != nil {
return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
}
return nil
}
func (installer *Installer) buildPkg(ctx context.Context,
dir, base string,
installIncompatible, needed, isTarget bool,
) (map[string]string, error) {
args := []string{"--nobuild", "-f"}
if !installer.exeCmd.GetKeepSrc() {
args = append(args, "-C")
}
if installIncompatible {
args = append(args, "--ignorearch")
}
// pkgver bump
if err := installer.exeCmd.Show(
installer.exeCmd.BuildMakepkgCmd(ctx, dir, args...)); err != nil {
return nil, err
}
pkgdests, pkgVersion, errList := parsePackageList(ctx, installer.exeCmd, dir)
if errList != nil {
return nil, errList
}
switch {
case needed && installer.pkgsAreAlreadyInstalled(pkgdests, pkgVersion) || installer.downloadOnly:
args = []string{"--nobuild", "--noextract", "--ignorearch"}
pkgdests = map[string]string{}
installer.log.Warnln(gotext.Get("%s is up to date -- skipping", text.Cyan(base+"-"+pkgVersion)))
case installer.skipAlreadyBuiltPkg(isTarget, pkgdests):
args = []string{"--nobuild", "--noextract", "--ignorearch"}
installer.log.Warnln(gotext.Get("%s already made -- skipping build", text.Cyan(base+"-"+pkgVersion)))
default:
args = []string{"-f", "--noconfirm", "--noextract", "--noprepare", "--holdver"}
if installIncompatible {
args = append(args, "--ignorearch")
}
}
if !installer.exeCmd.GetKeepSrc() {
args = append(args, "-c")
}
errMake := installer.exeCmd.Show(
installer.exeCmd.BuildMakepkgCmd(ctx,
dir, args...))
if errMake != nil {
return nil, errMake
}
if installer.downloadOnly {
return map[string]string{}, nil
}
return pkgdests, nil
}
func (installer *Installer) pkgsAreAlreadyInstalled(pkgdests map[string]string, pkgVersion string) bool {
for pkgName := range pkgdests {
if !installer.dbExecutor.IsCorrectVersionInstalled(pkgName, pkgVersion) {
return false
}
}
return true
}
func pkgsAreBuilt(logger *text.Logger, pkgdests map[string]string) bool {
for _, pkgdest := range pkgdests {
if _, err := os.Stat(pkgdest); err != nil {
logger.Debugln("pkgIsBuilt:", pkgdest, "does not exist")
return false
}
}
return true
}
func (installer *Installer) skipAlreadyBuiltPkg(isTarget bool, pkgdests map[string]string) bool {
switch installer.rebuildMode {
case parser.RebuildModeNo:
return pkgsAreBuilt(installer.log, pkgdests)
case parser.RebuildModeYes:
return !isTarget && pkgsAreBuilt(installer.log, pkgdests)
// case parser.RebuildModeTree: // TODO
// case parser.RebuildModeAll: // TODO
default:
// same as RebuildModeNo
return pkgsAreBuilt(installer.log, pkgdests)
}
}
func (*Installer) isDep(cmdArgs *parser.Arguments, aurExpNames mapset.Set[string], name string) bool {
switch {
case cmdArgs.ExistsArg("asdeps", "asdep"):
return true
case cmdArgs.ExistsArg("asexplicit", "asexp"):
return false
case aurExpNames.Contains(name):
return false
}
return true
}
func (installer *Installer) getNewTargets(pkgdests map[string]string, name string,
) (archives []string, good bool, err error) {
pkgdest, ok := pkgdests[name]
if !ok {
return nil, false, &PkgDestNotInListError{name: name}
}
pkgArchives := make([]string, 0, 2)
if _, errStat := os.Stat(pkgdest); os.IsNotExist(errStat) {
return nil, false, &FindPkgDestError{name: name, pkgDest: pkgdest}
}
pkgArchives = append(pkgArchives, pkgdest)
debugName := name + "-debug"
pkgdestDebug, ok := pkgdests[debugName]
if ok {
if _, errStat := os.Stat(pkgdestDebug); errStat == nil {
pkgArchives = append(pkgArchives, pkgdestDebug)
} else {
ok = false
}
}
return pkgArchives, ok, nil
}
func (installer *Installer) installSyncPackages(ctx context.Context, cmdArgs *parser.Arguments,
syncDeps, // repo targets that are deps
syncExp mapset.Set[string], // repo targets that are exp
syncGroups mapset.Set[string], // repo targets that are groups
excluded []string,
upgrade bool, // run even without targets
noConfirm bool,
) error {
repoTargets := syncDeps.Union(syncExp).Union(syncGroups).ToSlice()
if len(repoTargets) == 0 && !upgrade {
return nil
}
arguments := cmdArgs.Copy()
arguments.DelArg("asdeps", "asdep")
arguments.DelArg("asexplicit", "asexp")
arguments.DelArg("i", "install")
arguments.Op = "S"
arguments.ClearTargets()
arguments.AddTarget(repoTargets...)
// Don't upgrade all repo packages if only AUR upgrades are specified
if installer.targetMode == parser.ModeAUR {
arguments.DelArg("u", "upgrades")
}
if len(excluded) > 0 {
arguments.CreateOrAppendOption("ignore", excluded...)
}
errShow := installer.exeCmd.Show(installer.exeCmd.BuildPacmanCmd(ctx,
arguments, installer.targetMode, noConfirm))
if errShow != nil {
return errShow
}
if errD := asdeps(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncDeps.ToSlice()); errD != nil {
return errD
}
if errE := asexp(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncExp.ToSlice()); errE != nil {
return errE
}
return nil
}