mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
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:
1 parent
3d6723e7c0
commit
4198084050
3 files changed
+122
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user