installer.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. package build
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "github.com/Jguer/yay/v12/pkg/db"
  7. "github.com/Jguer/yay/v12/pkg/dep"
  8. "github.com/Jguer/yay/v12/pkg/multierror"
  9. "github.com/Jguer/yay/v12/pkg/settings"
  10. "github.com/Jguer/yay/v12/pkg/settings/exe"
  11. "github.com/Jguer/yay/v12/pkg/settings/parser"
  12. "github.com/Jguer/yay/v12/pkg/text"
  13. "github.com/Jguer/yay/v12/pkg/vcs"
  14. mapset "github.com/deckarep/golang-set/v2"
  15. "github.com/leonelquinteros/gotext"
  16. )
  17. type (
  18. PostInstallHookFunc func(ctx context.Context) error
  19. Installer struct {
  20. dbExecutor db.Executor
  21. postInstallHooks []PostInstallHookFunc
  22. failedAndIgnored map[string]error
  23. exeCmd exe.ICmdBuilder
  24. vcsStore vcs.Store
  25. targetMode parser.TargetMode
  26. rebuildMode parser.RebuildMode
  27. origTargets mapset.Set[string]
  28. downloadOnly bool
  29. log *text.Logger
  30. manualConfirmRequired bool
  31. }
  32. )
  33. func NewInstaller(dbExecutor db.Executor,
  34. exeCmd exe.ICmdBuilder, vcsStore vcs.Store, targetMode parser.TargetMode,
  35. rebuildMode parser.RebuildMode, downloadOnly bool, logger *text.Logger,
  36. ) *Installer {
  37. return &Installer{
  38. dbExecutor: dbExecutor,
  39. postInstallHooks: []PostInstallHookFunc{},
  40. failedAndIgnored: map[string]error{},
  41. exeCmd: exeCmd,
  42. vcsStore: vcsStore,
  43. targetMode: targetMode,
  44. rebuildMode: rebuildMode,
  45. downloadOnly: downloadOnly,
  46. log: logger,
  47. manualConfirmRequired: true,
  48. }
  49. }
  50. func (installer *Installer) CompileFailedAndIgnored() (map[string]error, error) {
  51. if len(installer.failedAndIgnored) == 0 {
  52. return installer.failedAndIgnored, nil
  53. }
  54. return installer.failedAndIgnored, &FailedIgnoredPkgError{
  55. pkgErrors: installer.failedAndIgnored,
  56. }
  57. }
  58. func (installer *Installer) AddPostInstallHook(hook PostInstallHookFunc) {
  59. if hook == nil {
  60. return
  61. }
  62. installer.postInstallHooks = append(installer.postInstallHooks, hook)
  63. }
  64. func (installer *Installer) RunPostInstallHooks(ctx context.Context) error {
  65. var errMulti multierror.MultiError
  66. for _, hook := range installer.postInstallHooks {
  67. if err := hook(ctx); err != nil {
  68. errMulti.Add(err)
  69. }
  70. }
  71. return errMulti.Return()
  72. }
  73. func (installer *Installer) Install(ctx context.Context,
  74. cmdArgs *parser.Arguments,
  75. targets []map[string]*dep.InstallInfo,
  76. pkgBuildDirs map[string]string,
  77. excluded []string,
  78. manualConfirmRequired bool,
  79. ) error {
  80. installer.log.Debugln("manualConfirmRequired:", manualConfirmRequired)
  81. installer.manualConfirmRequired = manualConfirmRequired
  82. installer.origTargets = mapset.NewThreadUnsafeSet[string]()
  83. for _, targetString := range cmdArgs.Targets {
  84. installer.origTargets.Add(dep.ToTarget(targetString).Name)
  85. }
  86. installer.log.Debugln("origTargets:", installer.origTargets)
  87. // Reorganize targets into layers of dependencies
  88. var errMulti multierror.MultiError
  89. for i := len(targets) - 1; i >= 0; i-- {
  90. lastLayer := i == 0
  91. errI := installer.handleLayer(ctx, cmdArgs, targets[i], pkgBuildDirs, lastLayer, excluded)
  92. if errI == nil && lastLayer {
  93. // success after rollups
  94. return nil
  95. }
  96. if errI != nil {
  97. errMulti.Add(errI)
  98. if lastLayer {
  99. break
  100. }
  101. // rollup
  102. installer.log.Warnln(gotext.Get("Failed to install layer, rolling up to next layer."), "error:", errI)
  103. targets[i-1] = mergeLayers(targets[i-1], targets[i])
  104. }
  105. }
  106. return errMulti.Return()
  107. }
  108. func mergeLayers(layer1, layer2 map[string]*dep.InstallInfo) map[string]*dep.InstallInfo {
  109. for name, info := range layer2 {
  110. layer1[name] = info
  111. }
  112. return layer1
  113. }
  114. func (installer *Installer) appendNoConfirm() bool {
  115. return !installer.manualConfirmRequired || settings.NoConfirm
  116. }
  117. func (installer *Installer) handleLayer(ctx context.Context,
  118. cmdArgs *parser.Arguments,
  119. layer map[string]*dep.InstallInfo,
  120. pkgBuildDirs map[string]string,
  121. lastLayer bool,
  122. excluded []string,
  123. ) error {
  124. // Install layer
  125. nameToBaseMap := make(map[string]string, 0)
  126. syncDeps, syncExp, syncGroups := mapset.NewThreadUnsafeSet[string](),
  127. mapset.NewThreadUnsafeSet[string](), mapset.NewThreadUnsafeSet[string]()
  128. aurDeps, aurExp, aurOrigTargetBases := mapset.NewThreadUnsafeSet[string](),
  129. mapset.NewThreadUnsafeSet[string](), mapset.NewThreadUnsafeSet[string]()
  130. upgradeSync := false
  131. for name, info := range layer {
  132. switch info.Source {
  133. case dep.AUR, dep.SrcInfo:
  134. nameToBaseMap[name] = *info.AURBase
  135. if installer.origTargets.Contains(name) {
  136. aurOrigTargetBases.Add(*info.AURBase)
  137. }
  138. switch info.Reason {
  139. case dep.Explicit:
  140. if cmdArgs.ExistsArg("asdeps", "asdep") {
  141. aurDeps.Add(name)
  142. } else {
  143. aurExp.Add(name)
  144. }
  145. case dep.Dep, dep.MakeDep, dep.CheckDep:
  146. aurDeps.Add(name)
  147. }
  148. case dep.Sync:
  149. if info.Upgrade {
  150. upgradeSync = true
  151. continue // do not add to targets, let pacman handle it
  152. }
  153. compositePkgName := fmt.Sprintf("%s/%s", *info.SyncDBName, name)
  154. if info.IsGroup {
  155. syncGroups.Add(compositePkgName)
  156. continue
  157. }
  158. switch info.Reason {
  159. case dep.Explicit:
  160. if cmdArgs.ExistsArg("asdeps", "asdep") {
  161. syncDeps.Add(compositePkgName)
  162. } else {
  163. syncExp.Add(compositePkgName)
  164. }
  165. case dep.Dep, dep.MakeDep, dep.CheckDep:
  166. syncDeps.Add(compositePkgName)
  167. }
  168. }
  169. }
  170. installer.log.Debugln("syncDeps", syncDeps, "SyncExp", syncExp,
  171. "aurDeps", aurDeps, "aurExp", aurExp, "upgrade", upgradeSync)
  172. errShow := installer.installSyncPackages(ctx, cmdArgs, syncDeps, syncExp, syncGroups,
  173. excluded, upgradeSync, installer.appendNoConfirm())
  174. if errShow != nil {
  175. return ErrInstallRepoPkgs
  176. }
  177. errAur := installer.installAURPackages(ctx, cmdArgs, aurDeps, aurExp,
  178. aurOrigTargetBases, nameToBaseMap, pkgBuildDirs, true, lastLayer,
  179. installer.appendNoConfirm())
  180. return errAur
  181. }
  182. func (installer *Installer) installAURPackages(ctx context.Context,
  183. cmdArgs *parser.Arguments,
  184. aurDepNames, aurExpNames, aurOrigTargetBases mapset.Set[string],
  185. nameToBase, pkgBuildDirsByBase map[string]string,
  186. installIncompatible bool,
  187. lastLayer bool,
  188. noConfirm bool,
  189. ) error {
  190. all := aurDepNames.Union(aurExpNames).ToSlice()
  191. if len(all) == 0 {
  192. return nil
  193. }
  194. builtPkgDests := make(map[string]map[string]string)
  195. deps, exps := make([]string, 0, aurDepNames.Cardinality()), make([]string, 0, aurExpNames.Cardinality())
  196. pkgArchives := make([]string, 0, len(exps)+len(deps))
  197. for _, name := range all {
  198. base := nameToBase[name]
  199. dir := pkgBuildDirsByBase[base]
  200. pkgdests, ok := builtPkgDests[base]
  201. if ok {
  202. installer.log.Debugln("skipping built pkgbase", base, "package", name)
  203. } else {
  204. var errMake error
  205. installer.log.Debugln("building pkgbase", base, "package", name)
  206. pkgdests, errMake = installer.buildPkg(ctx, dir, base,
  207. installIncompatible, cmdArgs.ExistsArg("needed"), aurOrigTargetBases.Contains(base))
  208. if errMake != nil {
  209. if !lastLayer {
  210. return fmt.Errorf("%s - %w", gotext.Get("error making: %s", base), errMake)
  211. }
  212. installer.failedAndIgnored[name] = errMake
  213. installer.log.Errorln(gotext.Get("error making: %s", base), "-", errMake)
  214. continue
  215. }
  216. builtPkgDests[base] = pkgdests
  217. }
  218. if len(pkgdests) == 0 {
  219. installer.log.Warnln(gotext.Get("nothing to install for %s", text.Cyan(base)))
  220. continue
  221. }
  222. newPKGArchives, hasDebug, err := installer.getNewTargets(pkgdests, name)
  223. if err != nil {
  224. return err
  225. }
  226. pkgArchives = append(pkgArchives, newPKGArchives...)
  227. if isDep := installer.isDep(cmdArgs, aurExpNames, name); isDep {
  228. deps = append(deps, name)
  229. } else {
  230. exps = append(exps, name)
  231. }
  232. if hasDebug {
  233. deps = append(deps, name+"-debug")
  234. }
  235. }
  236. if err := installPkgArchive(ctx, installer.exeCmd, installer.targetMode,
  237. installer.vcsStore, cmdArgs, pkgArchives, noConfirm); err != nil {
  238. return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
  239. }
  240. if err := setInstallReason(ctx, installer.exeCmd, installer.targetMode, cmdArgs, deps, exps); err != nil {
  241. return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
  242. }
  243. return nil
  244. }
  245. func (installer *Installer) buildPkg(ctx context.Context,
  246. dir, base string,
  247. installIncompatible, needed, isTarget bool,
  248. ) (map[string]string, error) {
  249. args := []string{"--nobuild", "-f"}
  250. if !installer.exeCmd.GetKeepSrc() {
  251. args = append(args, "-C")
  252. }
  253. if installIncompatible {
  254. args = append(args, "--ignorearch")
  255. }
  256. // pkgver bump
  257. if err := installer.exeCmd.Show(
  258. installer.exeCmd.BuildMakepkgCmd(ctx, dir, args...)); err != nil {
  259. return nil, err
  260. }
  261. pkgdests, pkgVersion, errList := parsePackageList(ctx, installer.exeCmd, dir)
  262. if errList != nil {
  263. return nil, errList
  264. }
  265. switch {
  266. case needed && installer.pkgsAreAlreadyInstalled(pkgdests, pkgVersion) || installer.downloadOnly:
  267. args = []string{"--nobuild", "--noextract", "--ignorearch"}
  268. pkgdests = map[string]string{}
  269. installer.log.Warnln(gotext.Get("%s is up to date -- skipping", text.Cyan(base+"-"+pkgVersion)))
  270. case installer.skipAlreadyBuiltPkg(isTarget, pkgdests):
  271. args = []string{"--nobuild", "--noextract", "--ignorearch"}
  272. installer.log.Warnln(gotext.Get("%s already made -- skipping build", text.Cyan(base+"-"+pkgVersion)))
  273. default:
  274. args = []string{"-f", "--noconfirm", "--noextract", "--noprepare", "--holdver"}
  275. if installIncompatible {
  276. args = append(args, "--ignorearch")
  277. }
  278. }
  279. if !installer.exeCmd.GetKeepSrc() {
  280. args = append(args, "-c")
  281. }
  282. errMake := installer.exeCmd.Show(
  283. installer.exeCmd.BuildMakepkgCmd(ctx,
  284. dir, args...))
  285. if errMake != nil {
  286. return nil, errMake
  287. }
  288. if installer.downloadOnly {
  289. return map[string]string{}, nil
  290. }
  291. return pkgdests, nil
  292. }
  293. func (installer *Installer) pkgsAreAlreadyInstalled(pkgdests map[string]string, pkgVersion string) bool {
  294. for pkgName := range pkgdests {
  295. if !installer.dbExecutor.IsCorrectVersionInstalled(pkgName, pkgVersion) {
  296. return false
  297. }
  298. }
  299. return true
  300. }
  301. func pkgsAreBuilt(logger *text.Logger, pkgdests map[string]string) bool {
  302. for _, pkgdest := range pkgdests {
  303. if _, err := os.Stat(pkgdest); err != nil {
  304. logger.Debugln("pkgIsBuilt:", pkgdest, "does not exist")
  305. return false
  306. }
  307. }
  308. return true
  309. }
  310. func (installer *Installer) skipAlreadyBuiltPkg(isTarget bool, pkgdests map[string]string) bool {
  311. switch installer.rebuildMode {
  312. case parser.RebuildModeNo:
  313. return pkgsAreBuilt(installer.log, pkgdests)
  314. case parser.RebuildModeYes:
  315. return !isTarget && pkgsAreBuilt(installer.log, pkgdests)
  316. // case parser.RebuildModeTree: // TODO
  317. // case parser.RebuildModeAll: // TODO
  318. default:
  319. // same as RebuildModeNo
  320. return pkgsAreBuilt(installer.log, pkgdests)
  321. }
  322. }
  323. func (*Installer) isDep(cmdArgs *parser.Arguments, aurExpNames mapset.Set[string], name string) bool {
  324. switch {
  325. case cmdArgs.ExistsArg("asdeps", "asdep"):
  326. return true
  327. case cmdArgs.ExistsArg("asexplicit", "asexp"):
  328. return false
  329. case aurExpNames.Contains(name):
  330. return false
  331. }
  332. return true
  333. }
  334. func (installer *Installer) getNewTargets(pkgdests map[string]string, name string,
  335. ) (archives []string, good bool, err error) {
  336. pkgdest, ok := pkgdests[name]
  337. if !ok {
  338. return nil, false, &PkgDestNotInListError{name: name}
  339. }
  340. pkgArchives := make([]string, 0, 2)
  341. if _, errStat := os.Stat(pkgdest); os.IsNotExist(errStat) {
  342. return nil, false, &FindPkgDestError{name: name, pkgDest: pkgdest}
  343. }
  344. pkgArchives = append(pkgArchives, pkgdest)
  345. debugName := name + "-debug"
  346. pkgdestDebug, ok := pkgdests[debugName]
  347. if ok {
  348. if _, errStat := os.Stat(pkgdestDebug); errStat == nil {
  349. pkgArchives = append(pkgArchives, pkgdestDebug)
  350. } else {
  351. ok = false
  352. }
  353. }
  354. return pkgArchives, ok, nil
  355. }
  356. func (installer *Installer) installSyncPackages(ctx context.Context, cmdArgs *parser.Arguments,
  357. syncDeps, // repo targets that are deps
  358. syncExp mapset.Set[string], // repo targets that are exp
  359. syncGroups mapset.Set[string], // repo targets that are groups
  360. excluded []string,
  361. upgrade bool, // run even without targets
  362. noConfirm bool,
  363. ) error {
  364. repoTargets := syncDeps.Union(syncExp).Union(syncGroups).ToSlice()
  365. if len(repoTargets) == 0 && !upgrade {
  366. return nil
  367. }
  368. arguments := cmdArgs.Copy()
  369. arguments.DelArg("asdeps", "asdep")
  370. arguments.DelArg("asexplicit", "asexp")
  371. arguments.DelArg("i", "install")
  372. arguments.Op = "S"
  373. arguments.ClearTargets()
  374. arguments.AddTarget(repoTargets...)
  375. // Don't upgrade all repo packages if only AUR upgrades are specified
  376. if installer.targetMode == parser.ModeAUR {
  377. arguments.DelArg("u", "upgrades")
  378. }
  379. if len(excluded) > 0 {
  380. arguments.CreateOrAppendOption("ignore", excluded...)
  381. }
  382. errShow := installer.exeCmd.Show(installer.exeCmd.BuildPacmanCmd(ctx,
  383. arguments, installer.targetMode, noConfirm))
  384. if errShow != nil {
  385. return errShow
  386. }
  387. if errD := asdeps(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncDeps.ToSlice()); errD != nil {
  388. return errD
  389. }
  390. if errE := asexp(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncExp.ToSlice()); errE != nil {
  391. return errE
  392. }
  393. return nil
  394. }