| 1 | // Command desktopinventory enumerates every desktop shell entry point the |
| 2 | // Electron migration must account for and assigns each one a migration class. |
| 3 | // It reads the desktop Go package, the frontend sources, the packaging script |
| 4 | // and the CI workflows, then writes docs/desktop-migration/{INVENTORY.md, |
| 5 | // inventory.json}. -check fails when migration entries drift or remain |
| 6 | // unclassified; navigation-only source line changes do not block qualification. |
| 7 | package main |
| 8 | |
| 9 | import ( |
| 10 | "bytes" |
| 11 | "flag" |
| 12 | "fmt" |
| 13 | "os" |
| 14 | "path/filepath" |
| 15 | "regexp" |
| 16 | ) |
| 17 | |
| 18 | func main() { |
| 19 | root := flag.String("root", ".", "repository root") |
| 20 | out := flag.String("out", "docs/desktop-migration", "output directory (relative to root)") |
| 21 | check := flag.Bool("check", false, "verify the checked-in inventory is current instead of writing it") |
| 22 | flag.Parse() |
| 23 | |
| 24 | inv, err := build(*root) |
| 25 | if err != nil { |
| 26 | fmt.Fprintln(os.Stderr, "desktopinventory:", err) |
| 27 | os.Exit(2) |
| 28 | } |
| 29 | if unclassified := inv.unclassified(); len(unclassified) > 0 { |
| 30 | for _, u := range unclassified { |
| 31 | fmt.Fprintf(os.Stderr, "unclassified: %s\n", u) |
| 32 | } |
| 33 | os.Exit(2) |
| 34 | } |
| 35 | md, js, err := render(inv) |
| 36 | if err != nil { |
| 37 | fmt.Fprintln(os.Stderr, "desktopinventory:", err) |
| 38 | os.Exit(2) |
| 39 | } |
| 40 | dir := filepath.Join(*root, *out) |
| 41 | mdPath := filepath.Join(dir, "INVENTORY.md") |
| 42 | jsPath := filepath.Join(dir, "inventory.json") |
| 43 | if *check { |
| 44 | stale := false |
| 45 | for path, want := range map[string][]byte{mdPath: md, jsPath: js} { |
| 46 | have, err := os.ReadFile(path) |
| 47 | if err != nil || !sameInventory(have, want) { |
| 48 | fmt.Fprintf(os.Stderr, "stale: %s (run: go run ./tools/desktopinventory)\n", path) |
| 49 | stale = true |
| 50 | } |
| 51 | } |
| 52 | if stale { |
| 53 | os.Exit(1) |
| 54 | } |
| 55 | fmt.Printf("desktop inventory current: %d entries\n", inv.count()) |
| 56 | return |
| 57 | } |
| 58 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 59 | fmt.Fprintln(os.Stderr, "desktopinventory:", err) |
| 60 | os.Exit(2) |
| 61 | } |
| 62 | for path, data := range map[string][]byte{mdPath: md, jsPath: js} { |
| 63 | if err := os.WriteFile(path, data, 0o644); err != nil { |
| 64 | fmt.Fprintln(os.Stderr, "desktopinventory:", err) |
| 65 | os.Exit(2) |
| 66 | } |
| 67 | } |
| 68 | fmt.Printf("wrote %s and %s (%d entries)\n", mdPath, jsPath, inv.count()) |
| 69 | } |
| 70 | |
| 71 | // Source line numbers are navigation hints, not migration contracts. Moving |
| 72 | // unrelated code must not fail qualification; paths, entries, classifications, |
| 73 | // ownership, signatures and all other generated content remain exact. |
| 74 | var sourceLineHint = regexp.MustCompile(`((?:desktop|scripts|\.github)/[^\s"` + "`" + `|<>]+\.(?:go|[cm]?[jt]sx?|sh|ya?ml)):[0-9]+\b`) |
| 75 | |
| 76 | func sameInventory(have, want []byte) bool { |
| 77 | return bytes.Equal(sourceLineHint.ReplaceAll(have, []byte("${1}")), sourceLineHint.ReplaceAll(want, []byte("${1}"))) |
| 78 | } |
| 79 |