diff --git a/cmd.go b/cmd.go index 1f5db4b13..fc8189934 100644 --- a/cmd.go +++ b/cmd.go @@ -10,6 +10,7 @@ import ( alpm "github.com/Jguer/dyalpm" "github.com/leonelquinteros/gotext" + "github.com/Jguer/yay/v12/pkg/chroot" "github.com/Jguer/yay/v12/pkg/completion" "github.com/Jguer/yay/v12/pkg/db" "github.com/Jguer/yay/v12/pkg/download" @@ -146,6 +147,38 @@ func handleCmd(ctx context.Context, run *runtime.Runtime, run.CmdBuilder.SudoLoop() } + // Full chroot handling (mirrors paru behaviour): create/update/run inside chroot + if run.Cfg.Chroot { + // lazy import of chroot package + c := &chroot.Chroot{ + Sudo: run.Cfg.SudoBin, + Path: run.Cfg.ChrootDir, + PacmanConf: run.Cfg.PacmanConf, + MakepkgConf: run.Cfg.MakepkgConf, + MFlags: strings.Fields(run.Cfg.MFlags), + Ro: []string{}, + Rw: run.PacmanConf.CacheDir, + RootPkgs: run.Cfg.RootChrootPkgs, + } + + if cmdArgs.ExistsArg("p", "print") { + run.Logger.Println(run.Cfg.ChrootDir) + return nil + } + + if !c.Exists() { + if err := c.Create(); err != nil { + return err + } + } + + if cmdArgs.ExistsArg("u", "sysupgrade") { + if err := c.Update(); err != nil { + return err + } + } + } + switch cmdArgs.Op { case "V", "version": handleVersion(run.Logger) diff --git a/local_install.go b/local_install.go index 478c7fbd4..1918a3960 100644 --- a/local_install.go +++ b/local_install.go @@ -87,6 +87,7 @@ func installLocalPKGBUILD( grapher := dep.NewGrapher(dbExecutor, aurCache, false, settings.NoConfirm, cmdArgs.ExistsDouble("d", "nodeps"), noCheck, cmdArgs.ExistsArg("needed"), run.Logger.Child("grapher")) + grapher.SetChrootMode(run.Cfg.Chroot) graph, err := grapher.GraphFromSrcInfos(ctx, nil, srcInfos) if err != nil { return err diff --git a/pkg/chroot/chroot.go b/pkg/chroot/chroot.go new file mode 100644 index 000000000..759c3bf7d --- /dev/null +++ b/pkg/chroot/chroot.go @@ -0,0 +1,150 @@ +package chroot + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type Chroot struct { + Sudo string + Path string + PacmanConf string + MakepkgConf string + MFlags []string + Ro []string + Rw []string + RootPkgs []string +} + +func (c *Chroot) Exists() bool { + p := filepath.Join(c.Path, "root") + return p != "" && fileExists(p) +} + +func fileExists(p string) bool { + _, err := os.Stat(p) + return err == nil +} + +func (c *Chroot) writePacmanConfTmp() (string, error) { + f, err := os.CreateTemp("/tmp", "pacman.conf.*") + if err != nil { + return "", err + } + defer f.Close() + + in, err := os.Open(c.PacmanConf) + if err != nil { + return "", err + } + defer in.Close() + + // copy, but filter DBPath lines which may break pacstrap/mkarchroot + scanner := bufio.NewScanner(in) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "DBPath") { + continue + } + if _, err := f.WriteString(line + "\n"); err != nil { + return "", err + } + } + if err := scanner.Err(); err != nil { + return "", err + } + + return f.Name(), nil +} + +func (c *Chroot) Create() error { + // create base dir + args := []string{"install", "-dm755", c.Path} + if c.Sudo != "" { + cmd := exec.Command(c.Sudo, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + } else { + if err := os.MkdirAll(c.Path, 0o755); err != nil { + return err + } + } + + tmp, err := c.writePacmanConfTmp() + if err != nil { + return err + } + + dir := filepath.Join(c.Path, "root") + + // mkarchroot -C tmp [-M makepkg_conf] dir [pkgs...] + mkargs := []string{"-C", tmp} + if c.MakepkgConf != "" { + mkargs = append(mkargs, "-M", c.MakepkgConf) + } + mkargs = append(mkargs, dir) + // ensure at least one package is provided (mkarchroot requires it) + if len(c.RootPkgs) == 0 { + c.RootPkgs = []string{"base-devel"} + } + mkargs = append(mkargs, c.RootPkgs...) + + var cmd *exec.Cmd + if c.Sudo != "" { + cmd = exec.Command(c.Sudo, append([]string{"mkarchroot"}, mkargs...)...) + } else { + cmd = exec.Command("mkarchroot", mkargs...) + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("mkarchroot failed: %w", err) + } + + return nil +} + +func (c *Chroot) Update() error { + // Run pacman -Syu inside the chroot using the same Run helper so + // the chroot path and pacman.conf handling are applied consistently. + return c.Run([]string{"pacman", "-Syu", "--noconfirm"}) +} + +func (c *Chroot) Run(args []string) error { + tmp, err := c.writePacmanConfTmp() + if err != nil { + return err + } + + cmdArgs := []string{"arch-nspawn", "-C", tmp, "-M", c.MakepkgConf, filepath.Join(c.Path, "root")} + + for _, f := range c.Ro { + cmdArgs = append(cmdArgs, "--bind-ro", f) + } + for _, f := range c.Rw { + cmdArgs = append(cmdArgs, "--bind", f) + } + + cmdArgs = append(cmdArgs, args...) + + var cmd *exec.Cmd + // If a privilege elevator is configured, run via it, otherwise run arch-nspawn directly + if c.Sudo != "" { + cmd = exec.Command(c.Sudo, cmdArgs...) + } else { + cmd = exec.Command(cmdArgs[0], cmdArgs[1:]...) + } + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/pkg/dep/dep_graph.go b/pkg/dep/dep_graph.go index a3079a9b5..f7196cf95 100644 --- a/pkg/dep/dep_graph.go +++ b/pkg/dep/dep_graph.go @@ -106,6 +106,9 @@ type Grapher struct { noDeps bool // If true, the graph will not include dependencies noCheckDeps bool // If true, the graph will not include check dependencies needed bool // If true, the graph will only include packages that are not installed + // chrootMode forces locally-installed AUR makedeps back into the dep graph so they + // are rebuilt and can be injected into the clean chroot via -I . + chrootMode bool } func NewGrapher(dbExecutor db.Executor, aurCache aurc.QueryClient, @@ -125,6 +128,14 @@ func NewGrapher(dbExecutor db.Executor, aurCache aurc.QueryClient, } } +// SetChrootMode enables chroot-aware dependency resolution. In this mode, +// locally-installed AUR packages (those not present in any sync repo) are not +// treated as satisfied: they are re-queried from AUR and added to the build +// graph so that fresh archives can be produced and injected into the chroot. +func (g *Grapher) SetChrootMode(enabled bool) { + g.chrootMode = enabled +} + func NewGraph() *topo.Graph[string, *InstallInfo] { return topo.New[string, *InstallInfo]() } @@ -667,6 +678,14 @@ func (g *Grapher) addNodes( } } + // In chroot mode, a locally-installed package that is NOT present in any + // sync repo is likely an AUR package. The clean chroot won't have it, so + // we must rebuild it and inject the archive via -I. Skip the "satisfied" + // shortcut and let the package fall through to the AUR lookup below. + if g.chrootMode && g.dbExecutor.SyncSatisfier(depString) == nil { + continue + } + targetsToFind.Remove(depString) } @@ -736,6 +755,19 @@ func (g *Grapher) addNodes( // Add missing to graph for _, depString := range targetsToFind.ToSlice() { depName, mod, ver := splitDep(depString) + + // In chroot mode we may have let locally-installed non-repo packages fall + // through the installed/sync checks hoping the AUR lookup would pick them + // up. If AUR lookup also missed them (e.g. they are from an unofficial + // repo), treat them as satisfied rather than blocking the entire install. + if g.chrootMode && g.dbExecutor.LocalSatisfierExists(depString) { + g.logger.Warnln(gotext.Get( + "chroot: %s is locally installed but not found in repos or AUR; "+ + "it may be missing from the chroot", depName)) + + continue + } + // no dep found. add as missing if err := graph.DependOn(depName, parentPkgName); err != nil { g.logger.Warnln("missing dep warn:", depString, parentPkgName, err) diff --git a/pkg/settings/args.go b/pkg/settings/args.go index 1ca3e0738..16220a322 100644 --- a/pkg/settings/args.go +++ b/pkg/settings/args.go @@ -136,6 +136,11 @@ func (c *Configuration) handleOption(option, value string) bool { c.MakepkgConf = "" case "pacman": c.PacmanBin = value + // chroot option + case "chroot": + c.Chroot = boolValue + case "chrootdir": + c.ChrootDir = value case "git": c.GitBin = value case "gpg": diff --git a/pkg/settings/config.go b/pkg/settings/config.go index 5d75eb231..fd9b02598 100644 --- a/pkg/settings/config.go +++ b/pkg/settings/config.go @@ -76,6 +76,10 @@ type Configuration struct { SaveConfig bool `json:"-" lua:"-"` Mode parser.TargetMode `json:"-" lua:"-"` ReBuild parser.RebuildMode `json:"rebuild" lua:"rebuild"` + + Chroot bool `json:"chroot"` + ChrootDir string `json:"chrootdir"` + RootChrootPkgs []string `json:"root_chroot_pkgs"` } // SaveConfig writes yay config to file. @@ -119,6 +123,7 @@ func (c *Configuration) expandEnv() { c.MakepkgConf = expandEnvOrHome(c.MakepkgConf) c.PacmanBin = expandEnvOrHome(c.PacmanBin) c.PacmanConf = expandEnvOrHome(c.PacmanConf) + c.ChrootDir = expandEnvOrHome(c.ChrootDir) c.GpgFlags = os.ExpandEnv(c.GpgFlags) c.MFlags = os.ExpandEnv(c.MFlags) c.GitFlags = os.ExpandEnv(c.GitFlags) @@ -202,6 +207,9 @@ func DefaultConfig(version string) *Configuration { PacmanBin: "pacman", PGPFetch: true, PacmanConf: "/etc/pacman.conf", + Chroot: false, + ChrootDir: "/var/lib/aurbuild", + RootChrootPkgs: []string{"base-devel"}, GpgFlags: "", MFlags: "", GitFlags: "", diff --git a/pkg/settings/parser/parser.go b/pkg/settings/parser/parser.go index 1f0927b73..9edec4292 100644 --- a/pkg/settings/parser/parser.go +++ b/pkg/settings/parser/parser.go @@ -438,6 +438,9 @@ func isArg(arg string) bool { case "singlelineresults": case "doublelineresults": case "separatesources": + // chroot option + case "chroot": + case "chrootdir": default: return false } @@ -532,6 +535,7 @@ func hasParam(arg string) bool { case "completioninterval": case "sortby": case "searchby": + case "chrootdir": default: return false } diff --git a/pkg/sync/build/installer.go b/pkg/sync/build/installer.go index c8f467f90..b6b876838 100644 --- a/pkg/sync/build/installer.go +++ b/pkg/sync/build/installer.go @@ -33,14 +33,16 @@ type ( downloadOnly bool installBuiltPackages bool log *text.Logger - manualConfirmRequired bool + chroot bool + chrootPath string + builtAURArchives []string } ) func NewInstaller(dbExecutor db.Executor, exeCmd exe.ICmdBuilder, vcsStore vcs.Store, targetMode parser.TargetMode, - rebuildMode parser.RebuildMode, downloadOnly bool, logger *text.Logger, + rebuildMode parser.RebuildMode, downloadOnly bool, chroot bool, chrootPath string, logger *text.Logger, ) *Installer { return &Installer{ dbExecutor: dbExecutor, @@ -54,6 +56,8 @@ func NewInstaller(dbExecutor db.Executor, installBuiltPackages: true, log: logger, manualConfirmRequired: true, + chroot: chroot, + chrootPath: chrootPath, } } @@ -234,7 +238,11 @@ func (installer *Installer) installAURPackages(ctx context.Context, lastLayer bool, noConfirm bool, ) error { - all := aurDepNames.Union(aurExpNames).ToSlice() + // Process deps before explicits so that dep archives are in builtAURArchives + // when building packages that depend on them (critical in rollup scenarios). + all := make([]string, 0, aurDepNames.Cardinality()+aurExpNames.Cardinality()) + all = append(all, aurDepNames.ToSlice()...) + all = append(all, aurExpNames.ToSlice()...) if len(all) == 0 { return nil } @@ -257,6 +265,11 @@ func (installer *Installer) installAURPackages(ctx context.Context, pkgdests, errMake = installer.buildPkg(ctx, dir, base, installIncompatible, cmdArgs.ExistsArg("needed"), aurOrigTargetBases.Contains(base)) builtPkgDests[base] = pkgdests + if errMake == nil && installer.chroot { + for _, archive := range pkgdests { + installer.builtAURArchives = append(installer.builtAURArchives, archive) + } + } if errMake != nil { if !lastLayer { return fmt.Errorf("%s - %w", gotext.Get("error making: %s", base), errMake) @@ -321,6 +334,12 @@ func (installer *Installer) buildPkg(ctx context.Context, args = append(args, "--ignorearch") } + if installer.chroot { + // In chroot mode, makedepends are handled inside the chroot. + // Skip host-side dep installation to avoid "target not found" for AUR makedeps. + args = append(args, "--nodeps") + } + // pkgver bump if err := installer.exeCmd.Show( installer.exeCmd.BuildMakepkgCmd(ctx, dir, args...)); err != nil { @@ -334,14 +353,35 @@ func (installer *Installer) buildPkg(ctx context.Context, 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): + pkgdests = map[string]string{} + if installer.chroot { + // makechrootpkg does not understand --nobuild; nothing to inject. + return pkgdests, nil + } args = []string{"--nobuild", "--noextract", "--ignorearch"} + case installer.skipAlreadyBuiltPkg(isTarget, pkgdests): installer.log.Warnln(gotext.Get("%s already made -- skipping build", text.Cyan(base+"-"+pkgVersion))) + if installer.chroot { + // makechrootpkg does not understand --nobuild. The archive already + // exists on disk; add it to the injection list and return it directly. + for _, archive := range pkgdests { + installer.builtAURArchives = append(installer.builtAURArchives, archive) + } + return pkgdests, nil + } + args = []string{"--nobuild", "--noextract", "--ignorearch"} default: - args = []string{"-f", "--noconfirm", "--noextract", "--noprepare", "--holdver"} + args = []string{} + if installer.chroot { + args = []string{"-r", installer.chrootPath} + for _, archive := range installer.builtAURArchives { + args = append(args, "-I", archive) + } + args = append(args, "--", "-f", "--noconfirm", "--holdver") + } else { + args = append(args, []string{"-f", "--noconfirm", "--noextract", "--noprepare", "--holdver"}...) + } if installIncompatible { args = append(args, "--ignorearch") } @@ -351,8 +391,17 @@ func (installer *Installer) buildPkg(ctx context.Context, args = append(args, "-c") } + buildExeCmd := installer.exeCmd + if installer.chroot { + if cb, ok := installer.exeCmd.(*exe.CmdBuilder); ok { + newCb := *cb + newCb.MakepkgBin = "makechrootpkg" + buildExeCmd = &newCb + } + } + errMake := installer.exeCmd.Show( - installer.exeCmd.BuildMakepkgCmd(ctx, + buildExeCmd.BuildMakepkgCmd(ctx, dir, args...)) if errMake != nil { return nil, errMake diff --git a/pkg/sync/build/installer_test.go b/pkg/sync/build/installer_test.go index 9229e0268..099fb685b 100644 --- a/pkg/sync/build/installer_test.go +++ b/pkg/sync/build/installer_test.go @@ -135,7 +135,7 @@ func TestInstaller_InstallNeeded(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddArg("needed") @@ -228,7 +228,7 @@ func TestInstaller_BuildOnlySkipsInstall(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) installer.SetInstallBuiltPackages(false) cmdArgs := parser.MakeArguments() @@ -505,7 +505,7 @@ func TestInstaller_InstallMixedSourcesAndLayers(t *testing.T) { cmdBuilder.Runner = mockRunner - installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, parser.RebuildModeNo, false, newTestLogger()) + installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddTarget("yay") @@ -559,7 +559,7 @@ func TestInstaller_RunPostHooks(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) called := false hook := func(ctx context.Context) error { @@ -691,7 +691,7 @@ func TestInstaller_CompileFailed(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddArg("needed") @@ -860,7 +860,7 @@ func TestInstaller_InstallSplitPackage(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddTarget("jellyfin") @@ -1000,7 +1000,7 @@ func TestInstaller_InstallDownloadOnly(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, true, newTestLogger()) + parser.RebuildModeNo, true, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddTarget("yay") @@ -1105,7 +1105,7 @@ func TestInstaller_InstallGroup(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, true, newTestLogger()) + parser.RebuildModeNo, true, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddTarget("kubernetes-tools") @@ -1324,7 +1324,7 @@ func TestInstaller_InstallRebuild(t *testing.T) { cmdBuilder.Runner = mockRunner installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - tc.rebuildOption, false, newTestLogger()) + tc.rebuildOption, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddTarget("yay") @@ -1410,7 +1410,7 @@ func TestInstaller_InstallUpgrade(t *testing.T) { } installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, tc.targetMode, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddArg("u", "upgrades") // Make sure both args are removed @@ -1521,7 +1521,7 @@ func TestInstaller_KeepSrc(t *testing.T) { } installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := parser.MakeArguments() cmdArgs.AddTarget("yay") @@ -1670,7 +1670,7 @@ func TestInstaller_InstallAsExplicit(t *testing.T) { } installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, - parser.RebuildModeNo, false, newTestLogger()) + parser.RebuildModeNo, false, false, "", newTestLogger()) cmdArgs := tc.cmdArgs() @@ -1703,3 +1703,138 @@ func TestInstaller_InstallAsExplicit(t *testing.T) { }) } } + +// TestInstaller_ChrootInjectsAURDepsWithI verifies that when building an AUR package +// in chroot mode, previously-built AUR dependency archives are passed via -I to +// makechrootpkg so they are available inside the clean chroot environment. +func TestInstaller_ChrootInjectsAURDepsWithI(t *testing.T) { + t.Parallel() + + makepkgBin := t.TempDir() + "/makepkg" + pacmanBin := t.TempDir() + "/pacman" + chrootDir := t.TempDir() + + for _, bin := range []string{makepkgBin, pacmanBin} { + f, err := os.OpenFile(bin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + } + + tmpDepDir := t.TempDir() + tmpMainDir := t.TempDir() + + depPkgTar := tmpDepDir + "/dep-pkg-1.0.0-1-x86_64.pkg.tar.zst" + mainPkgTar := tmpMainDir + "/main-pkg-1.0.0-1-x86_64.pkg.tar.zst" + + captureOverride := func(cmd *exec.Cmd) (string, string, error) { + if cmd.Dir == tmpDepDir { + return depPkgTar, "", nil + } + return mainPkgTar, "", nil + } + + // Show calls (in order): + // 1: makepkg --nobuild (pre-pass for dep-pkg) + // 2: makechrootpkg -r ... -- ... (build dep-pkg) ← creates depPkgTar + // 3: pacman -U (install dep-pkg) + // 4: pacman -D --asdep (mark dep-pkg) + // 5: makepkg --nobuild (pre-pass for main-pkg) + // 6: makechrootpkg -r ... -I depPkgTar -- ... (build main-pkg) ← creates mainPkgTar + // 7: pacman -U (install main-pkg) + // 8: pacman -D --asexplicit (mark main-pkg) + showCallIdx := 0 + showOverride := func(cmd *exec.Cmd) error { + showCallIdx++ + switch showCallIdx { + case 2: + f, err := os.OpenFile(depPkgTar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(t, err) + require.NoError(t, f.Close()) + case 6: + f, err := os.OpenFile(mainPkgTar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(t, err) + require.NoError(t, f.Close()) + } + return nil + } + + mockDB := &mock.DBExecutor{IsCorrectVersionInstalledFn: func(string, string) bool { return false }} + mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: makepkgBin, + SudoBin: "su", + PacmanBin: pacmanBin, + Runner: mockRunner, + SudoLoopEnabled: false, + } + + installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, + parser.RebuildModeNo, false, true, chrootDir, newTestLogger()) + + cmdArgs := parser.MakeArguments() + cmdArgs.AddTarget("main-pkg") + + pkgBuildDirs := map[string]string{ + "dep-pkg": tmpDepDir, + "main-pkg": tmpMainDir, + } + + depBase := "dep-pkg" + mainBase := "main-pkg" + + // targets[0] is processed last (the user target). + // targets[1] is processed first (the AUR dependency). + targets := []map[string]*dep.InstallInfo{ + { + "main-pkg": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "1.0.0-1", + AURBase: &mainBase, + }, + }, + { + "dep-pkg": { + Source: dep.AUR, + Reason: dep.Dep, + Version: "1.0.0-1", + AURBase: &depBase, + }, + }, + } + + errI := installer.Install(context.Background(), cmdArgs, targets, pkgBuildDirs, []string{}, false) + require.NoError(t, errI) + + require.Len(t, mockRunner.ShowCalls, 8) + require.Len(t, mockRunner.CaptureCalls, 2) + + normalize := func(s, dir, replacement string) string { + return strings.ReplaceAll(s, dir, replacement) + } + + // Show[1] = actual build of dep-pkg: must use makechrootpkg, no -I flag yet. + depBuildShow := mockRunner.ShowCalls[1].Args[0].(*exec.Cmd).String() + depBuildShow = normalize(depBuildShow, chrootDir, "/chrootdir") + assert.Contains(t, depBuildShow, "makechrootpkg", "dep build should use makechrootpkg") + assert.NotContains(t, depBuildShow, "-I", "dep build should have no -I flag") + + // Show[5] = actual build of main-pkg: must use makechrootpkg and -I depPkgTar. + mainBuildShow := mockRunner.ShowCalls[5].Args[0].(*exec.Cmd).String() + mainBuildShow = normalize(mainBuildShow, chrootDir, "/chrootdir") + assert.Contains(t, mainBuildShow, "makechrootpkg", "main build should use makechrootpkg") + assert.Contains(t, mainBuildShow, "-I", "main build should inject dep archive with -I") + assert.Contains(t, mainBuildShow, depPkgTar, "main build should reference the dep archive path") + + // Pre-pass calls (Show[0] and Show[4]) must still use the original makepkg binary + // and must include --nodeps so the host doesn't try to install AUR makedepends. + prePassDep := mockRunner.ShowCalls[0].Args[0].(*exec.Cmd).String() + assert.Contains(t, prePassDep, "--nobuild", "pre-pass should use --nobuild") + assert.Contains(t, prePassDep, "--nodeps", "pre-pass must skip host dep installation in chroot mode") + assert.NotContains(t, prePassDep, "makechrootpkg", "pre-pass must not use makechrootpkg") + + prePassMain := mockRunner.ShowCalls[4].Args[0].(*exec.Cmd).String() + assert.Contains(t, prePassMain, "--nobuild", "pre-pass should use --nobuild") + assert.Contains(t, prePassMain, "--nodeps", "pre-pass must skip host dep installation in chroot mode") + assert.NotContains(t, prePassMain, "makechrootpkg", "pre-pass must not use makechrootpkg") +} diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go index b03da10f0..fcc674546 100644 --- a/pkg/sync/sync.go +++ b/pkg/sync/sync.go @@ -52,7 +52,7 @@ func (o *OperationService) Run(ctx context.Context, run *runtime.Runtime, preparer := workdir.NewPreparer(o.dbExecutor, run.CmdBuilder, o.cfg, o.logger.Child("workdir")) installer := build.NewInstaller(o.dbExecutor, run.CmdBuilder, run.VCSStore, o.cfg.Mode, o.cfg.ReBuild, - cmdArgs.ExistsArg("w", "downloadonly"), run.Logger.Child("installer")) + cmdArgs.ExistsArg("w", "downloadonly"), o.cfg.Chroot, o.cfg.ChrootDir, run.Logger.Child("installer")) shouldInstall := !cmdArgs.ExistsArg("w", "downloadonly") if cmdArgs.Op == "B" && !cmdArgs.ExistsArg("i", "install") { diff --git a/sync.go b/sync.go index b9d958451..9c78a48b8 100644 --- a/sync.go +++ b/sync.go @@ -45,6 +45,7 @@ func syncInstall(ctx context.Context, grapher := dep.NewGrapher(dbExecutor, aurCache, false, settings.NoConfirm, noDeps, noCheck, cmdArgs.ExistsArg("needed"), run.Logger.Child("grapher")) + grapher.SetChrootMode(run.Cfg.Chroot) graph, err := grapher.GraphFromTargets(ctx, nil, cmdArgs.Targets) if err != nil {