Skip to content

Commit 662c24c

Browse files
committed
fix(rebuild): keep PEP 440 '+' in frida-tools wheel filename (#36)
Hand-rolled wheels replaced '+' with '.' in the local version, so pip rejected frida_tools-14.10.4.frida.17.17.0.fridare.kxmwp-*.whl as Invalid wheel filename. Keep '+' in the filename and dist-info, and normalize already-built dotted names before catalog copy.
1 parent aa9d4de commit 662c24c

5 files changed

Lines changed: 278 additions & 8 deletions

File tree

‎CHANGELOG‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
## Unreleased
66

7+
### Fixed
8+
- 手搓 `frida-tools` wheel 文件名把 PEP 440 本地版本的 `+` 错写成 `.`,pip 报 `Invalid wheel filename (invalid version)`([issue #36](https://github.com/suifei/fridare/issues/36))。正确形式:`frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl`。已发布包把文件名里 `14.10.4.frida.` 改成 `14.10.4+frida.` 即可安装。
9+
710
## v4.0.8 - 2026-08-13
811

912
### Added

‎docs/kxmwp-17.17.0.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,27 @@ python/host/linux-x86_64/...
8383
python/host/linux-arm64/...
8484
python/host/macos-x86_64/...
8585
python/host/macos-arm64/...
86-
python/frida_tools-*.fridare.kxmwp-*.whl
86+
python/frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl
8787
```
8888

8989
```powershell
9090
python -m venv .venv
9191
.\.venv\Scripts\pip install --force-reinstall path\to\frida-17.17.0-cp37-abi3-win_amd64.whl
92-
.\.venv\Scripts\pip install --force-reinstall path\to\frida_tools-*.whl
92+
.\.venv\Scripts\pip install --force-reinstall --no-deps path\to\frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl
9393
```
9494

9595
Linux / macOS 把 `pip` 换成 `bin/pip`。
9696

97+
若 pip 报 `Invalid wheel filename (invalid version)`,是旧包把 PEP 440 的 `+` 写成了 `.`。先改名再装:
98+
99+
```powershell
100+
# 错误: frida_tools-14.10.4.frida.17.17.0.fridare.kxmwp-py3-none-any.whl
101+
# 正确: 把 14.10.4.frida. 改成 14.10.4+frida.
102+
Rename-Item frida_tools-14.10.4.frida.17.17.0.fridare.kxmwp-py3-none-any.whl `
103+
frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl
104+
pip install --force-reinstall --no-deps .\frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl
105+
```
106+
97107
### 3.2 启动服务端
98108

99109
始终带 `-l`,端口自定。下面用 `28888` 举例(可改成任意未被占用的端口)。本包默认仍是 27042,**不改默认也可以**,只要启动和连接写同一个数。

‎docs/kxmwp-17.17.1.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ Release:<https://github.com/suifei/fridare/releases/tag/kxmwp-17.17.1>
2222
# Windows 包仍是 kxmwp-server.exe;Linux 本轮是 ELF kxmwp-server
2323
.\kxmwp-server.exe -l 0.0.0.0:28888
2424
# 装 host-wheels 里同一 magic 的 frida + frida_tools
25+
# tools wheel 文件名必须带 PEP 440 的 '+'(不要用 14.10.4.frida.…)
26+
# pip install --force-reinstall --no-deps python/host/<你的平台>/frida-*.whl
27+
# pip install --force-reinstall --no-deps python/frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl
2528
frida -H 127.0.0.1:28888 -n <进程名>
2629
```
2730

31+
`host-wheels` 里 tools 文件名必须带 PEP 440 的 `+`:`frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl`。旧 zip 若仍是 `14.10.4.frida.`,pip 会报 `Invalid wheel filename`,把那段改成 `14.10.4+frida.` 再装。
32+
2833
默认监听仍是官方 **27042**。换端口用 `-l`。
2934

3035
## 下载

‎ui/internal/rebuild/tools_package.go‎

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ func BuildPatchedFridaToolsWheels(cfg JobConfig, catalogRoot string, entryDirs [
136136
if len(toolsBuilt) == 0 {
137137
toolsBuilt, _ = filepath.Glob(filepath.Join(outDir, "*.whl"))
138138
}
139+
// GitHub #36: older hand-rolled wheels used '.' instead of '+' in the
140+
// local version; pip rejects those filenames. Normalize before copy.
141+
toolsBuilt = sanitizeToolsWheelFilenames(toolsBuilt)
139142
}
140143
}
141144

@@ -577,6 +580,139 @@ func parseVersionFromArchiveName(name string) string {
577580
return name
578581
}
579582

583+
// pep440WheelFilenameVersion is the version segment written into a wheel filename.
584+
// PEP 440 local versions MUST keep '+' (e.g. 14.10.4+frida.17.17.0.fridare.kxmwp).
585+
// Replacing '+' with '.' or '_' makes packaging.utils.parse_wheel_filename fail:
586+
//
587+
// Invalid wheel filename (invalid version)
588+
//
589+
// See GitHub suifei/fridare#36.
590+
func pep440WheelFilenameVersion(version string) string {
591+
v := strings.ReplaceAll(strings.TrimSpace(version), " ", "")
592+
if isPEP440Version(v) {
593+
return v
594+
}
595+
if fixed := repairDottedFridaLocalVersion(v); isPEP440Version(fixed) {
596+
return fixed
597+
}
598+
return v
599+
}
600+
601+
func isPEP440Version(v string) bool {
602+
if v == "" {
603+
return false
604+
}
605+
pub, local, hasLocal := strings.Cut(v, "+")
606+
if !isPEP440Release(pub) {
607+
return false
608+
}
609+
if !hasLocal {
610+
return true
611+
}
612+
if local == "" {
613+
return false
614+
}
615+
for _, seg := range strings.Split(local, ".") {
616+
if !isAlnum(seg) {
617+
return false
618+
}
619+
}
620+
return true
621+
}
622+
623+
func isPEP440Release(v string) bool {
624+
if v == "" {
625+
return false
626+
}
627+
for _, seg := range strings.Split(v, ".") {
628+
if seg == "" {
629+
return false
630+
}
631+
for _, c := range seg {
632+
if c < '0' || c > '9' {
633+
return false
634+
}
635+
}
636+
}
637+
return true
638+
}
639+
640+
func isAlnum(s string) bool {
641+
if s == "" {
642+
return false
643+
}
644+
for _, c := range s {
645+
if (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') {
646+
return false
647+
}
648+
}
649+
return true
650+
}
651+
652+
// repairDottedFridaLocalVersion restores '+' after the public version when a
653+
// previous Fridare build wrote 14.10.4.frida.… or 14.10.4_frida.… .
654+
func repairDottedFridaLocalVersion(v string) string {
655+
for _, sep := range []string{".frida.", "_frida."} {
656+
if i := strings.Index(v, sep); i > 0 {
657+
return v[:i] + "+frida." + v[i+len(sep):]
658+
}
659+
}
660+
return v
661+
}
662+
663+
func splitWheelFilename(base string) (name, ver, py, abi, plat string, ok bool) {
664+
if !strings.HasSuffix(strings.ToLower(base), ".whl") || len(base) < 5 {
665+
return
666+
}
667+
stem := base[:len(base)-4]
668+
parts := strings.Split(stem, "-")
669+
if len(parts) < 5 {
670+
return
671+
}
672+
plat = parts[len(parts)-1]
673+
abi = parts[len(parts)-2]
674+
py = parts[len(parts)-3]
675+
name = parts[0]
676+
ver = strings.Join(parts[1:len(parts)-3], "-")
677+
if name == "" || ver == "" {
678+
return
679+
}
680+
ok = true
681+
return
682+
}
683+
684+
// ensurePEP440ToolsWheelFilename renames a wheel on disk if its version segment
685+
// is not PEP 440 (the historical '.'-local-version bug). pip only inspects the
686+
// outer filename; METADATA inside may already be correct.
687+
func ensurePEP440ToolsWheelFilename(path string) (string, error) {
688+
base := filepath.Base(path)
689+
name, ver, py, abi, plat, ok := splitWheelFilename(base)
690+
if !ok {
691+
return path, nil
692+
}
693+
newVer := pep440WheelFilenameVersion(ver)
694+
if newVer == ver {
695+
return path, nil
696+
}
697+
newPath := filepath.Join(filepath.Dir(path), fmt.Sprintf("%s-%s-%s-%s-%s.whl", name, newVer, py, abi, plat))
698+
if err := os.Rename(path, newPath); err != nil {
699+
return path, err
700+
}
701+
return newPath, nil
702+
}
703+
704+
func sanitizeToolsWheelFilenames(paths []string) []string {
705+
out := make([]string, 0, len(paths))
706+
for _, p := range paths {
707+
if nw, err := ensurePEP440ToolsWheelFilename(p); err == nil && nw != "" {
708+
out = append(out, nw)
709+
} else {
710+
out = append(out, p)
711+
}
712+
}
713+
return out
714+
}
715+
580716
// pinFridaToolsPackageVersion patches PKG-INFO / setup.py so the rebuilt wheel
581717
// reports a local version and pins install_requires to frida==sourceVer.
582718
// Returns the PEP 440 local version string (e.g. 14.10.4+frida.17.16.4.fridare.abcde).
@@ -634,11 +770,12 @@ func buildPureFridaToolsWheel(pkgRoot, outDir, version string) (string, error) {
634770
if st, err := os.Stat(pkgDir); err != nil || !st.IsDir() {
635771
return "", fmt.Errorf("frida_tools package dir missing under %s", pkgRoot)
636772
}
637-
// Normalize version for filename: local versions use + which becomes . in wheel tags sometimes;
638-
// PEP 427 allows + in version but filename uses the version as-is with + kept or replaced.
639-
// Use underscore-safe: replace + with _
640-
fileVer := strings.ReplaceAll(version, "+", ".")
641-
fileVer = strings.ReplaceAll(fileVer, " ", "")
773+
// Keep PEP 440 '+' in the filename. Do NOT replace with '.' or '_' —
774+
// pip/packaging parse the version as-is (GitHub #36).
775+
fileVer := pep440WheelFilenameVersion(version)
776+
if fileVer == "" {
777+
return "", fmt.Errorf("empty frida-tools wheel version")
778+
}
642779
whlName := fmt.Sprintf("frida_tools-%s-py3-none-any.whl", fileVer)
643780
dst := filepath.Join(outDir, whlName)
644781
_ = os.MkdirAll(outDir, 0755)
@@ -758,7 +895,7 @@ B) frida 原生扩展(按宿主机 OS/架构选 python/host/<id>/,共 6 个
758895
# Linux aarch64
759896
pip install --force-reinstall --no-deps "python/host/linux-arm64/frida-*.whl"
760897
761-
# 再装 frida-tools(纯 Python,任意宿主机):
898+
# 再装 frida-tools(纯 Python,任意宿主机)。文件名里的 '+' 是 PEP 440 本地版本,不要改成 '.'。
762899
pip install --force-reinstall --no-deps "%s"
763900
764901
验证:
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package rebuild
2+
3+
import (
4+
"archive/zip"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func TestPep440WheelFilenameVersion(t *testing.T) {
12+
cases := []struct {
13+
in, want string
14+
ok bool
15+
}{
16+
{"14.10.4+frida.17.17.0.fridare.kxmwp", "14.10.4+frida.17.17.0.fridare.kxmwp", true},
17+
{"14.10.4.frida.17.17.0.fridare.kxmwp", "14.10.4+frida.17.17.0.fridare.kxmwp", true},
18+
{"14.10.4_frida.17.17.0.fridare.kxmwp", "14.10.4+frida.17.17.0.fridare.kxmwp", true},
19+
{"14.10.4", "14.10.4", true},
20+
{" 14.10.4+frida.17.16.4.fridare.abcde ", "14.10.4+frida.17.16.4.fridare.abcde", true},
21+
}
22+
for _, c := range cases {
23+
got := pep440WheelFilenameVersion(c.in)
24+
if got != c.want {
25+
t.Errorf("pep440WheelFilenameVersion(%q)=%q want %q", c.in, got, c.want)
26+
}
27+
if isPEP440Version(got) != c.ok {
28+
t.Errorf("isPEP440Version(%q)=%v want %v", got, isPEP440Version(got), c.ok)
29+
}
30+
}
31+
if isPEP440Version("14.10.4.frida.17.17.0.fridare.kxmwp") {
32+
t.Fatal("dotted local version must not pass PEP 440")
33+
}
34+
}
35+
36+
func TestPinFridaToolsPackageVersionLocal(t *testing.T) {
37+
root := t.TempDir()
38+
got := pinFridaToolsPackageVersion(root, "17.17.0", "14.10.4", "kxmwp")
39+
want := "14.10.4+frida.17.17.0.fridare.kxmwp"
40+
if got != want {
41+
t.Fatalf("got %q want %q", got, want)
42+
}
43+
if !isPEP440Version(got) {
44+
t.Fatalf("not PEP 440: %s", got)
45+
}
46+
}
47+
48+
func TestBuildPureFridaToolsWheelKeepsPlus(t *testing.T) {
49+
root := t.TempDir()
50+
pkg := filepath.Join(root, "frida_tools")
51+
if err := os.MkdirAll(pkg, 0755); err != nil {
52+
t.Fatal(err)
53+
}
54+
if err := os.WriteFile(filepath.Join(pkg, "__init__.py"), []byte("# patched\n"), 0644); err != nil {
55+
t.Fatal(err)
56+
}
57+
ver := pinFridaToolsPackageVersion(root, "17.17.0", "14.10.4", "kxmwp")
58+
out := filepath.Join(root, "out")
59+
whl, err := buildPureFridaToolsWheel(root, out, ver)
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
base := filepath.Base(whl)
64+
want := "frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl"
65+
if base != want {
66+
t.Fatalf("wheel name %q want %q", base, want)
67+
}
68+
if strings.Contains(base, "14.10.4.frida.") {
69+
t.Fatal("must not emit dotted local version (pip rejects it)")
70+
}
71+
infoDir := "frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp.dist-info/"
72+
if !zipHasPrefix(t, whl, infoDir) {
73+
t.Fatalf("missing dist-info %s", infoDir)
74+
}
75+
if zipHasPrefix(t, whl, "frida_tools-14.10.4.frida.") {
76+
t.Fatal("dist-info still uses dotted local version")
77+
}
78+
}
79+
80+
func TestEnsurePEP440ToolsWheelFilename(t *testing.T) {
81+
dir := t.TempDir()
82+
old := filepath.Join(dir, "frida_tools-14.10.4.frida.17.17.0.fridare.kxmwp-py3-none-any.whl")
83+
if err := os.WriteFile(old, []byte("whl"), 0644); err != nil {
84+
t.Fatal(err)
85+
}
86+
got, err := ensurePEP440ToolsWheelFilename(old)
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
want := filepath.Join(dir, "frida_tools-14.10.4+frida.17.17.0.fridare.kxmwp-py3-none-any.whl")
91+
if got != want {
92+
t.Fatalf("got %q want %q", got, want)
93+
}
94+
if _, err := os.Stat(want); err != nil {
95+
t.Fatal(err)
96+
}
97+
if _, err := os.Stat(old); !os.IsNotExist(err) {
98+
t.Fatal("old invalid filename should be gone")
99+
}
100+
}
101+
102+
func zipHasPrefix(t *testing.T, whl, prefix string) bool {
103+
t.Helper()
104+
r, err := zip.OpenReader(whl)
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
defer r.Close()
109+
for _, f := range r.File {
110+
if strings.HasPrefix(f.Name, prefix) {
111+
return true
112+
}
113+
}
114+
return false
115+
}

0 commit comments

Comments
 (0)