mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 12:14:40 +00:00
pip >= 23.0 added the PEP 668 --break-system-packages flag. On older pip builds (e.g. macOS Command Line Tools Python) and on the pip `list` subcommand of every version, the flag is rejected with "no such option: --break-system-packages", breaking skill dependency install/check paths entirely. - dep_installer: route pip installs through pipRunInstall, which retries once without the flag when pip rejects it (issue #956) - pip_update_executor: same retry for the upgrade path - pip_update_checker: drop the flag from `pip3 list --outdated` - list is read-only, PEP 668 never applies, and no pip accepts the flag there - add pip_flags.go helpers + unit tests, and legacy-pip integration tests reproducing the original failure Fixes #956
105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
package skills
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os/exec"
|
|
"time"
|
|
)
|
|
|
|
// PipUpdateExecutor implements UpdateExecutor for the "pip" source.
|
|
// It upgrades a single package via `pip3 install --upgrade ...`.
|
|
// Thread-safe: no mutable state; concurrent package serialization is handled
|
|
// upstream by PackageLocker (injected via UpdateRegistry.Apply).
|
|
type PipUpdateExecutor struct{}
|
|
|
|
// NewPipUpdateExecutor returns a PipUpdateExecutor ready for use.
|
|
func NewPipUpdateExecutor() *PipUpdateExecutor { return &PipUpdateExecutor{} }
|
|
|
|
// Source returns "pip".
|
|
func (e *PipUpdateExecutor) Source() string { return "pip" }
|
|
|
|
// Update upgrades `name` to `toVersion` using pip3.
|
|
//
|
|
// Argument ordering matches UpdateExecutor interface: (ctx, name, toVersion, meta).
|
|
// `name` is validated via ValidatePipPackageName before any exec.
|
|
// `--pre` is appended when meta["preRelease"]==true OR IsPipPreRelease(toVersion).
|
|
// On success, cleanCaches is called for symmetry with dep_installer.go.
|
|
// On failure, stderr is classified via ClassifyPipStderr and a wrapped sentinel is returned.
|
|
func (e *PipUpdateExecutor) Update(ctx context.Context, name, toVersion string, meta map[string]any) error {
|
|
if err := ValidatePipPackageName(name); err != nil {
|
|
return err
|
|
}
|
|
|
|
cctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
|
defer cancel()
|
|
|
|
args := []string{"install", "--upgrade",
|
|
"--no-cache-dir", pipBreakSystemPackagesFlag,
|
|
"--upgrade-strategy", "only-if-needed",
|
|
}
|
|
|
|
// Determine whether pre-release flag is needed.
|
|
preRelease := false
|
|
if meta != nil {
|
|
if v, ok := meta["preRelease"].(bool); ok && v {
|
|
preRelease = true
|
|
}
|
|
}
|
|
if !preRelease && IsPipPreRelease(toVersion) {
|
|
preRelease = true
|
|
}
|
|
if preRelease {
|
|
args = append(args, "--pre")
|
|
}
|
|
args = append(args, name)
|
|
|
|
start := time.Now()
|
|
stderr, runErr := runPipUpdateCommand(cctx, args)
|
|
// If pip rejects the PEP 668 flag (pip < 23.0, issue #956), retry once
|
|
// without it so updates keep working on legacy environments.
|
|
if runErr != nil && pipRejectsBSPFlag([]byte(stderr)) {
|
|
stderr, runErr = runPipUpdateCommand(cctx, dropPipBSPFlag(args))
|
|
}
|
|
durationMs := time.Since(start).Milliseconds()
|
|
|
|
if runErr != nil {
|
|
sentinel, reason := ClassifyPipStderr(stderr)
|
|
if sentinel == nil {
|
|
sentinel = fmt.Errorf("pip install failed: %w", runErr)
|
|
}
|
|
slog.Warn("package.update.pip.outcome",
|
|
"name", name,
|
|
"status", "failed",
|
|
"err_class", fmt.Sprintf("%T:%v", sentinel, sentinel),
|
|
"reason", reason,
|
|
"duration_ms", durationMs)
|
|
return fmt.Errorf("%w: %s", sentinel, reason)
|
|
}
|
|
|
|
// Success path: purge caches for disk symmetry with dep_installer.go.
|
|
cleanCaches(cctx)
|
|
|
|
slog.Info("package.update.pip.outcome",
|
|
"name", name,
|
|
"to", toVersion,
|
|
"status", "success",
|
|
"duration_ms", durationMs)
|
|
return nil
|
|
}
|
|
|
|
// runPipUpdateCommand runs `pip3 <args...>` and returns stderr only (it feeds
|
|
// ClassifyPipStderr); stdout is captured and discarded. Command-level
|
|
// WaitDelay mirrors the previous inline exec.
|
|
func runPipUpdateCommand(ctx context.Context, args []string) (stderr string, runErr error) {
|
|
cmd := exec.CommandContext(ctx, pipBinary, args...)
|
|
cmd.WaitDelay = 2 * time.Second
|
|
var outBuf, errBuf bytes.Buffer
|
|
cmd.Stdout = &outBuf
|
|
cmd.Stderr = &errBuf
|
|
runErr = cmd.Run()
|
|
return errBuf.String(), runErr
|
|
}
|