| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
| 10 | var classLabel = map[class]string{ |
| 11 | classKeepBusiness: "keep-business (保留业务实现)", |
| 12 | classMigrateHost: "migrate-host (迁移宿主实现)", |
| 13 | classDeleteShell: "delete-shell (删除旧壳专用实现)", |
| 14 | } |
| 15 | |
| 16 | var kindTitle = map[string]string{ |
| 17 | kindCommand: "Desktop commands (Go `App` methods bound to the UI)", |
| 18 | kindNativeCall: "Native toolkit calls in Go", |
| 19 | kindEvent: "Events emitted by Go", |
| 20 | kindFrontendNative: "Frontend native bridge uses", |
| 21 | kindFrontendEvent: "Frontend event subscriptions", |
| 22 | kindCSSMarker: "Shell CSS markers", |
| 23 | kindPersistence: "Persisted desktop files", |
| 24 | kindShellFile: "Shell-specific Go files", |
| 25 | kindArtifact: "Release artifacts", |
| 26 | kindCIJob: "CI and release jobs", |
| 27 | } |
| 28 | |
| 29 | // migrationBaseline is the deliberately frozen main-v2 checkpoint. Do not |
| 30 | // derive it from checkout refs: shallow CI clones and synthetic PR merge |
| 31 | // commits have different histories despite containing identical source files. |
| 32 | // Advance this checkpoint explicitly when integrating another mainline baseline. |
| 33 | const migrationBaseline = "fa018e4109268c912063c8cc619302fccdb57d74" |
| 34 | |
| 35 | func build(root string) (*inventory, error) { |
| 36 | inv := &inventory{Baseline: migrationBaseline} |
| 37 | if err := scanGo(root, inv); err != nil { |
| 38 | return nil, err |
| 39 | } |
| 40 | if err := scanFrontend(root, inv); err != nil { |
| 41 | return nil, err |
| 42 | } |
| 43 | if err := scanSources(root, inv); err != nil { |
| 44 | return nil, err |
| 45 | } |
| 46 | inv.Entries = inv.sorted() |
| 47 | return inv, nil |
| 48 | } |
| 49 | |
| 50 | func render(inv *inventory) (markdown, jsonOut []byte, err error) { |
| 51 | jsonOut, err = json.MarshalIndent(inv, "", " ") |
| 52 | if err != nil { |
| 53 | return nil, nil, err |
| 54 | } |
| 55 | jsonOut = append(jsonOut, '\n') |
| 56 | |
| 57 | var b bytes.Buffer |
| 58 | fmt.Fprintf(&b, "# Desktop shell migration inventory\n\n") |
| 59 | fmt.Fprintf(&b, "Generated by `go run ./tools/desktopinventory` from baseline `%s`. Do not edit; `-check` fails when this file is stale.\n\n", inv.Baseline) |
| 60 | fmt.Fprintf(&b, "Every entry carries one class: `keep-business` keeps its Go implementation and crosses the new host RPC unchanged, `migrate-host` moves its native step to the Electron host behind the contract, `delete-shell` left with the retired Wails/WebView2/WebKitGTK shell (none remain).\n\n") |
| 61 | counts := inv.counts() |
| 62 | fmt.Fprintf(&b, "| Kind | keep-business | migrate-host | delete-shell | total |\n| --- | ---: | ---: | ---: | ---: |\n") |
| 63 | total := 0 |
| 64 | for _, kind := range kindOrder { |
| 65 | c := counts[kind] |
| 66 | sum := c[classKeepBusiness] + c[classMigrateHost] + c[classDeleteShell] |
| 67 | total += sum |
| 68 | fmt.Fprintf(&b, "| %s | %d | %d | %d | %d |\n", kind, c[classKeepBusiness], c[classMigrateHost], c[classDeleteShell], sum) |
| 69 | } |
| 70 | fmt.Fprintf(&b, "| **all** | | | | **%d** |\n", total) |
| 71 | for _, kind := range kindOrder { |
| 72 | fmt.Fprintf(&b, "\n## %s\n\n| Name | Detail | Location | Class | New owner |\n| --- | --- | --- | --- | --- |\n", kindTitle[kind]) |
| 73 | for _, e := range inv.Entries { |
| 74 | if e.Kind != kind { |
| 75 | continue |
| 76 | } |
| 77 | fmt.Fprintf(&b, "| `%s` | %s | %s | %s | %s |\n", e.Name, cell(e.Detail), cell(e.Location), classLabel[e.Class], cell(e.Owner)) |
| 78 | } |
| 79 | } |
| 80 | return b.Bytes(), jsonOut, nil |
| 81 | } |
| 82 | |
| 83 | func cell(s string) string { |
| 84 | s = strings.ReplaceAll(s, "|", "\\|") |
| 85 | s = strings.ReplaceAll(s, "\n", " ") |
| 86 | if s == "" { |
| 87 | return "" |
| 88 | } |
| 89 | if strings.ContainsAny(s, "()*<>") { |
| 90 | return "`" + strings.ReplaceAll(s, "`", "'") + "`" |
| 91 | } |
| 92 | return s |
| 93 | } |
| 94 |