Files
fchengyan a1e550f0f0 fix(skills): retry pip installs without --break-system-packages when unsupported
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
2026-09-03 10:18:12 +08:00

50 lines
1.8 KiB
Go

package skills
import (
"bytes"
"context"
"os/exec"
)
// pipBreakSystemPackagesFlag is the PEP 668 opt-out flag accepted by pip >= 23.0.
// Older pip builds (and the `list` subcommand on every pip) reject it with
// "no such option: --break-system-packages", which breaks skill dependency
// installs on macOS Command Line Tools Python and other legacy environments.
const pipBreakSystemPackagesFlag = "--break-system-packages"
// pipRejectsBSPFlag reports whether pip's combined output indicates the
// --break-system-packages flag was rejected as an unknown option. pip < 23.0
// predates PEP 668 and emits exactly this error (see issue #956).
func pipRejectsBSPFlag(combinedOut []byte) bool {
return bytes.Contains(combinedOut, []byte("no such option: "+pipBreakSystemPackagesFlag))
}
// dropPipBSPFlag returns args with every --break-system-packages occurrence
// removed, preserving order of the remaining tokens. Used to retry an install
// on pip builds that do not support the flag.
func dropPipBSPFlag(args []string) []string {
out := make([]string, 0, len(args))
for _, a := range args {
if a == pipBreakSystemPackagesFlag {
continue
}
out = append(out, a)
}
return out
}
// pipRunInstall runs `pip3 <args...>` where args includes the PEP 668
// --break-system-packages flag. If pip rejects the flag as an unknown option
// (pip < 23.0, issue #956), the command is retried once without it. Returns
// the combined output of the final attempt. Success-path overhead is zero
// extra subprocesses.
func pipRunInstall(ctx context.Context, args []string) ([]byte, error) {
cmd := exec.CommandContext(ctx, pipBinary, args...)
out, err := cmd.CombinedOutput()
if err != nil && pipRejectsBSPFlag(out) {
cmd = exec.CommandContext(ctx, pipBinary, dropPipBSPFlag(args)...)
out, err = cmd.CombinedOutput()
}
return out, err
}