feat(skills): pip build-fail log hints for troubleshooting

Add pipBuildFailHint helper that suggests root causes when pip wheel build fails.
Detects missing system deps (pg_config for psycopg, libssl for cryptography, etc).
Hints suggest apt/brew/yum package names or pip alias alternatives (psycopg2→psycopg2-binary).
Wired to dep_installer stderr/stdlog post-build-failure for visibility.
This commit is contained in:
viettranx committed 2026-04-17 19:46:43 +07:00
1 parent 3d6723e7c0
commit 4198084050
3 files changed
+122

No files matched your search

+6
View File
@@ -73,6 +73,9 @@ func InstallSingleDep(ctx context.Context, dep string) (bool, string) {
if err != nil {
msg := fmt.Sprintf("%s: %v", strings.TrimSpace(string(out)), err)
slog.Error("skills: dep install failed", "dep", dep, "error", msg)
if hint := pipBuildFailHint(pkg, string(out)); hint != "" {
slog.Warn("skills: dep install hint", "dep", dep, "hint", hint)
}
return false, msg
}
case strings.HasPrefix(dep, "npm:"):
@@ -141,6 +144,9 @@ func InstallDeps(ctx context.Context, manifest *SkillManifest, missing []string)
cmd := exec.CommandContext(ctx, "pip3", "install", "--no-cache-dir", "--break-system-packages", pkg)
if out, err := cmd.CombinedOutput(); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("pip %s: %s (%v)", pkg, strings.TrimSpace(string(out)), err))
if hint := pipBuildFailHint(pkg, string(out)); hint != "" {
slog.Warn("skills: dep install hint", "pkg", pkg, "hint", hint)
}
} else {
successful = append(successful, pkg)
}
+32
View File
@@ -0,0 +1,32 @@
package skills
import "strings"
// pipBuildFailHint returns a human-readable hint when a pip install fails
// due to a build-from-source error (missing system lib or compiler).
// Returns "" when no known pattern matches the combined stdout+stderr.
//
// Hints are log-side only — never returned to callers / HTTP responses —
// so they can safely suggest alternatives without altering install semantics.
// There is no auto-retry: failing packages typically have no wheel at all,
// and a retry would burn the 5-minute install timeout uselessly.
func pipBuildFailHint(pkg, combinedOutput string) string {
out := combinedOutput
if !strings.Contains(out, "Failed building wheel") &&
!strings.Contains(out, "Could not build wheels") &&
!strings.Contains(out, "pg_config") &&
!strings.Contains(out, "mysql_config") {
return ""
}
switch {
case strings.Contains(out, "pg_config"):
return "install 'pip:psycopg2-binary' (prebuilt wheel, no pg_config needed) or declare deps: in SKILL.md"
case strings.Contains(out, "mysql_config"):
return "install 'pip:PyMySQL' (pure-Python) or add 'system:mysql-dev' to deps: in SKILL.md"
case strings.Contains(pkg, "psycopg") && !strings.Contains(pkg, "binary"):
return "try 'pip:psycopg[binary]' (v3) or 'pip:psycopg2-binary' (v2) and declare explicitly in SKILL.md deps:"
case strings.Contains(pkg, "crypto") || strings.Contains(pkg, "Crypto"):
return "use 'pip:pycryptodome' (modern successor) and declare in SKILL.md deps:"
}
return "pip failed to build from source — try a '-binary' variant if available, or declare explicit deps: in SKILL.md"
}
@@ -0,0 +1,84 @@
package skills
import (
"strings"
"testing"
)
func TestPipBuildFailHint(t *testing.T) {
cases := []struct {
name string
pkg string
output string
wantSub string // substring expected in hint; "" = hint must be ""
}{
{
name: "pg_config psycopg2",
pkg: "psycopg2",
output: "Error: pg_config executable not found. Failed building wheel for psycopg2",
wantSub: "psycopg2-binary",
},
{
name: "mysql_config mysqlclient",
pkg: "mysqlclient",
output: "mysql_config not found... Failed building wheel for mysqlclient",
wantSub: "PyMySQL",
},
{
name: "psycopg v3 no binary",
pkg: "psycopg",
output: "Failed building wheel for psycopg",
wantSub: "psycopg[binary]",
},
{
name: "pycrypto old",
pkg: "pycrypto",
output: "Failed building wheel for pycrypto",
wantSub: "pycryptodome",
},
{
name: "generic wheel fail",
pkg: "somepkg",
output: "ERROR: Could not build wheels for somepkg",
wantSub: "-binary",
},
{
name: "success has no hint",
pkg: "requests",
output: "",
wantSub: "",
},
{
name: "network error not build",
pkg: "foo",
output: "ERROR: Could not find a version that satisfies the requirement foo",
wantSub: "",
},
{
name: "auth 403 not build",
pkg: "internal-pkg",
output: "ERROR: HTTP 403 Forbidden",
wantSub: "",
},
{
name: "binary pkg already — no psycopg hint loop",
pkg: "psycopg2-binary",
output: "Failed building wheel for psycopg2-binary",
wantSub: "-binary",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := pipBuildFailHint(tc.pkg, tc.output)
if tc.wantSub == "" {
if got != "" {
t.Errorf("expected empty hint, got %q", got)
}
return
}
if !strings.Contains(got, tc.wantSub) {
t.Errorf("hint %q does not contain %q", got, tc.wantSub)
}
})
}
}