From e82bdb20d3ef865a22ebfe83a068adac3c0e21db Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 17 Apr 2026 10:55:55 +0200 Subject: [PATCH] Replace upgrade.sh with Go-based upgrade tool in tools/upgrade/ Rewrite the bash upgrade script as a standalone Go program for better portability and maintainability. The tool preserves all existing behavior: three-category file classification, dry-run mode, Go source resolution (gvm/GOROOT/download), 3-way merge via diff3, and colored output. Build: cd tools/upgrade && go build -o upgrade . Run: ./tools/upgrade/upgrade --dry-run Signed-off-by: deadprogram --- .gitignore | 2 + tools/upgrade/README.md | 76 ++++ tools/upgrade/go.mod | 3 + tools/upgrade/main.go | 787 ++++++++++++++++++++++++++++++++++++++++ upgrade.sh | 515 -------------------------- 5 files changed, 868 insertions(+), 515 deletions(-) create mode 100644 tools/upgrade/README.md create mode 100644 tools/upgrade/go.mod create mode 100644 tools/upgrade/main.go delete mode 100755 upgrade.sh diff --git a/.gitignore b/.gitignore index 6c3af2b..3acea24 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ go.work # Upgrade script work/report directories .upgrade-work/ .upgrade-report/ + +./tools/upgrade/upgrade diff --git a/tools/upgrade/README.md b/tools/upgrade/README.md new file mode 100644 index 0000000..657d951 --- /dev/null +++ b/tools/upgrade/README.md @@ -0,0 +1,76 @@ +# upgrade + +Automates the TinyGo `net` package upgrade process by backporting +changes from upstream Go stdlib and generating comparison reports. + +It handles three categories of files: + +- **TinyGo-only** (e.g. `netdev.go`, `tlssock.go`) — skipped entirely +- **Unmodified copies** — replaced directly from upstream +- **Modified files** (with `// TINYGO` markers) — 3-way merged via `diff3` + +## Prerequisites + +- `diff` and `diff3` (from GNU diffutils) +- Go source trees are resolved automatically: + 1. gvm installs at `~/.gvm/gos/` + 2. Active `GOROOT` if the version matches + 3. Downloaded from `https://go.dev/dl/` into `.upgrade-work/` + +## Build + +```sh +cd tools/upgrade +go build -o upgrade . +``` + +This produces the `upgrade` binary. It must be run from the `net` +package root directory (the repository root). + +## Usage + +```sh +# From the net package root: +./tools/upgrade/upgrade [flags] +``` + +### Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--dry-run` | `false` | Preview changes without modifying files | +| `--cur` | `1.21.4` | Go version the TinyGo net package is currently based on | +| `--upstream` | `1.26.2` | Target upstream Go version to upgrade to | +| `--file` | | Process a single file instead of all files | + +### Examples + +```sh +# Preview what would change +./tools/upgrade/upgrade --dry-run + +# Perform the upgrade +./tools/upgrade/upgrade --cur 1.21.4 --upstream 1.26.2 + +# Preview a single file +./tools/upgrade/upgrade --dry-run --file dial.go +``` + +## Output + +Reports are written to `.upgrade-report/` in the net package root: + +- `summary.txt` — one-line status per file +- `diffs/` — unified diffs showing upstream and TinyGo changes +- `merged/` — merge results (only in apply mode) + +Files with merge conflicts are saved as `.conflicted` alongside +the original. + +## After upgrading + +1. Review and resolve any `.conflicted` files +2. Check for files missing from upstream (may have been renamed/removed) +3. Verify `// TINYGO` comments are preserved +4. Test with TinyGo example/net examples +5. Update `README.md` version references diff --git a/tools/upgrade/go.mod b/tools/upgrade/go.mod new file mode 100644 index 0000000..7fcb7b4 --- /dev/null +++ b/tools/upgrade/go.mod @@ -0,0 +1,3 @@ +module github.com/tinygo-org/net/tools/upgrade + +go 1.22 diff --git a/tools/upgrade/main.go b/tools/upgrade/main.go new file mode 100644 index 0000000..d8b9564 --- /dev/null +++ b/tools/upgrade/main.go @@ -0,0 +1,787 @@ +package main + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "flag" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" +) + +// ── Defaults ──────────────────────────────────────────────────────────────── + +const ( + defaultCurVersion = "1.21.4" + defaultUpstreamVersion = "1.26.2" +) + +// ── Color helpers ─────────────────────────────────────────────────────────── + +const ( + colorRed = "\033[0;31m" + colorGreen = "\033[0;32m" + colorYellow = "\033[1;33m" + colorBlue = "\033[0;34m" + colorReset = "\033[0m" +) + +func info(msg string, args ...any) { + fmt.Fprintf(os.Stderr, colorBlue+"[INFO]"+colorReset+" "+msg+"\n", args...) +} +func success(msg string, args ...any) { + fmt.Fprintf(os.Stderr, colorGreen+"[OK]"+colorReset+" "+msg+"\n", args...) +} +func warn(msg string, args ...any) { + fmt.Fprintf(os.Stderr, colorYellow+"[WARN]"+colorReset+" "+msg+"\n", args...) +} +func errorf(msg string, args ...any) { + fmt.Fprintf(os.Stderr, colorRed+"[ERROR]"+colorReset+" "+msg+"\n", args...) +} + +// ── File classification ───────────────────────────────────────────────────── + +// Files entirely new to TinyGo (not from upstream) — never overwrite these. +var tinygoOnlyFiles = map[string]bool{ + "netdev.go": true, + "tlssock.go": true, + "README.md": true, + "LICENSE": true, +} + +// Files that are straight copies from upstream (no TINYGO modifications). +var unmodifiedFiles = []string{ + "ip.go", + "mac.go", + "mac_test.go", + "parse.go", + "pipe.go", + "http/clone.go", + "http/cookie.go", + "http/fs.go", + "http/http.go", + "http/jar.go", + "http/method.go", + "http/sniff.go", + "http/status.go", + "http/internal/ascii/print.go", + "http/internal/ascii/print_test.go", + "http/internal/chunked.go", + "http/internal/chunked_test.go", + "http/httptest/recorder.go", + "http/httputil/dump.go", + "http/httputil/httputil.go", + "http/httputil/persist.go", + "http/httputil/reverseproxy.go", + "http/pprof/pprof.go", +} + +// Files copied AND modified for TinyGo (contain // TINYGO markers). +// These need 3-way merge: upstream changes applied to TinyGo-modified version. +var modifiedFiles = []string{ + "dial.go", + "interface.go", + "iprawsock.go", + "ipsock.go", + "lookup.go", + "lookup_unix.go", + "lookup_windows.go", + "net.go", + "tcpsock.go", + "udpsock.go", + "unixsock.go", + "http/client.go", + "http/header.go", + "http/pattern.go", + "http/request.go", + "http/response.go", + "http/server.go", + "http/transfer.go", + "http/transport.go", + "http/httptest/httptest.go", + "http/httptest/server.go", + "http/httptrace/trace.go", +} + +var modifiedFilesSet map[string]bool + +func init() { + modifiedFilesSet = make(map[string]bool, len(modifiedFiles)) + for _, f := range modifiedFiles { + modifiedFilesSet[f] = true + } +} + +// ── Map TinyGo paths to upstream Go stdlib paths ─────────────────────────── + +func upstreamPath(file string) string { + return "src/net/" + file +} + +// ── Detect per-file CUR version from TINYGO header comment ───────────────── + +var versionRe = regexp.MustCompile(`Go (\d+\.\d+\.\d+)`) + +func detectCurVersion(filePath, fallback string) string { + f, err := os.Open(filePath) + if err != nil { + return fallback + } + defer f.Close() + + scanner := bufio.NewScanner(f) + if scanner.Scan() { + if m := versionRe.FindStringSubmatch(scanner.Text()); m != nil { + return m[1] + } + } + return fallback +} + +// ── Resolve Go source trees ──────────────────────────────────────────────── + +func resolveGoSource(version, workDir string) (string, error) { + // Try gvm install + gvmPath := filepath.Join(os.Getenv("HOME"), ".gvm", "gos", "go"+version) + if isDir(filepath.Join(gvmPath, "src", "net")) { + return gvmPath, nil + } + + // Try current GOROOT if active Go version matches + goroot, _ := execOutput("go", "env", "GOROOT") + goroot = strings.TrimSpace(goroot) + if goroot != "" && isDir(filepath.Join(goroot, "src", "net")) { + activeVer, _ := execOutput("go", "version") + if m := versionRe.FindStringSubmatch(activeVer); m != nil && m[1] == version { + return goroot, nil + } + } + + // Download + return downloadGoSource(version, workDir) +} + +func downloadGoSource(version, workDir string) (string, error) { + destDir := filepath.Join(workDir, "go"+version) + if isDir(filepath.Join(destDir, "src", "net")) { + info("Go %s source already cached at %s", version, destDir) + return destDir, nil + } + + url := fmt.Sprintf("https://go.dev/dl/go%s.src.tar.gz", version) + tarball := filepath.Join(workDir, fmt.Sprintf("go%s.src.tar.gz", version)) + + if err := os.MkdirAll(workDir, 0o755); err != nil { + return "", fmt.Errorf("creating work dir: %w", err) + } + + if _, err := os.Stat(tarball); os.IsNotExist(err) { + info("Downloading Go %s source from %s ...", version, url) + if err := downloadFile(tarball, url); err != nil { + return "", fmt.Errorf("downloading %s: %w", url, err) + } + } + + info("Extracting Go %s source...", version) + if err := extractTarGz(tarball, destDir); err != nil { + return "", fmt.Errorf("extracting tarball: %w", err) + } + + success("Go %s source ready at %s", version, destDir) + return destDir, nil +} + +func downloadFile(dest, url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url) + } + + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +func extractTarGz(tarball, destDir string) error { + f, err := os.Open(tarball) + if err != nil { + return err + } + defer f.Close() + + gz, err := gzip.NewReader(f) + if err != nil { + return err + } + defer gz.Close() + + tr := tar.NewReader(gz) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + // Strip first component ("go/") to match --strip-components=1 + parts := strings.SplitN(hdr.Name, "/", 2) + if len(parts) < 2 || parts[1] == "" { + continue + } + target := filepath.Join(destDir, parts[1]) + + switch hdr.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0o755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(out, tr); err != nil { + out.Close() + return err + } + out.Close() + } + } + return nil +} + +// ── Process unmodified (straight copy) files ──────────────────────────────── + +type summaryEntry struct { + status string + file string + detail string +} + +type upgrader struct { + curVersion string + upstreamVersion string + dryRun bool + tinygoNetDir string + workDir string + reportDir string + curRoot string + upstreamRoot string + summary []summaryEntry +} + +func (u *upgrader) addSummary(status, file, detail string) { + u.summary = append(u.summary, summaryEntry{status, file, detail}) +} + +func (u *upgrader) processUnmodified(file string) { + upath := upstreamPath(file) + upstreamFile := filepath.Join(u.upstreamRoot, upath) + tinygoFile := filepath.Join(u.tinygoNetDir, file) + + if !fileExists(upstreamFile) { + warn("MISSING in upstream %s: %s", u.upstreamVersion, upath) + u.addSummary("MISSING_UPSTREAM", file, "") + return + } + + curFile := filepath.Join(u.curRoot, upath) + + if fileExists(curFile) && filesEqual(curFile, upstreamFile) { + u.addSummary("UNCHANGED", file, "") + return + } + + if u.dryRun { + info("[DRY-RUN] Would replace %s with upstream %s copy", file, u.upstreamVersion) + if fileExists(curFile) { + diffPath := filepath.Join(u.reportDir, "diffs", file+".diff") + writeDiff(curFile, upstreamFile, diffPath) + lines := countLines(diffPath) + info(" Upstream diff: %d lines (see .upgrade-report/diffs/%s.diff)", lines, file) + } + u.addSummary("WOULD_COPY", file, "") + } else { + header := firstLine(tinygoFile) + if strings.HasPrefix(header, "// TINYGO") { + oldVer := detectCurVersion(tinygoFile, u.curVersion) + upstreamContent, err := os.ReadFile(upstreamFile) + if err != nil { + errorf("reading upstream file %s: %v", upstreamFile, err) + return + } + // Update version in header and prepend to upstream content (minus first line) + newHeader := strings.Replace(header, "Go "+oldVer, "Go "+u.upstreamVersion, 1) + // Find the first newline in upstream content to skip its first line + upstreamLines := string(upstreamContent) + if idx := strings.Index(upstreamLines, "\n"); idx >= 0 { + upstreamLines = upstreamLines[idx:] + } + if err := os.WriteFile(tinygoFile, []byte(newHeader+upstreamLines), 0o644); err != nil { + errorf("writing %s: %v", tinygoFile, err) + return + } + } else { + if err := copyFile(upstreamFile, tinygoFile); err != nil { + errorf("copying %s: %v", file, err) + return + } + } + u.addSummary("COPIED", file, "") + success("Copied %s from upstream %s", file, u.upstreamVersion) + } +} + +// ── Process modified (3-way merge) files ──────────────────────────────────── + +func (u *upgrader) processModified(file string) { + upath := upstreamPath(file) + upstreamFile := filepath.Join(u.upstreamRoot, upath) + tinygoFile := filepath.Join(u.tinygoNetDir, file) + + if !fileExists(upstreamFile) { + warn("MISSING in upstream %s: %s", u.upstreamVersion, upath) + u.addSummary("MISSING_UPSTREAM", file, "") + return + } + + // Detect the actual CUR version for this specific file + fileCurVersion := detectCurVersion(tinygoFile, u.curVersion) + fileCurRoot := u.curRoot + + if fileCurVersion != u.curVersion { + info("%s: based on Go %s (not default %s)", file, fileCurVersion, u.curVersion) + var err error + fileCurRoot, err = resolveGoSource(fileCurVersion, u.workDir) + if err != nil { + errorf("resolving Go %s source: %v", fileCurVersion, err) + return + } + } + + curFile := filepath.Join(fileCurRoot, upath) + + if !fileExists(curFile) { + warn("MISSING in CUR %s: %s — can only diff against current TinyGo version", fileCurVersion, upath) + u.addSummary("MISSING_CUR", file, "") + diffPath := filepath.Join(u.reportDir, "diffs", file+".upstream-vs-tinygo.diff") + writeDiff(tinygoFile, upstreamFile, diffPath) + return + } + + // Check if upstream even changed this file + if filesEqual(curFile, upstreamFile) { + u.addSummary("UNCHANGED", file, "") + return + } + + // Generate upstream diff (what changed from CUR -> UPSTREAM in official Go) + upstreamDiffPath := filepath.Join(u.reportDir, "diffs", file+".upstream-changes.diff") + writeDiff(curFile, upstreamFile, upstreamDiffPath) + + // Generate current tinygo diff (what TinyGo changed from CUR) + tinygoDiffPath := filepath.Join(u.reportDir, "diffs", file+".tinygo-changes.diff") + writeDiff(curFile, tinygoFile, tinygoDiffPath) + + upstreamLines := countLines(upstreamDiffPath) + + if u.dryRun { + info("[DRY-RUN] Would 3-way merge %s (Go %s → %s)", file, fileCurVersion, u.upstreamVersion) + info(" Upstream changes: %d diff lines", upstreamLines) + info(" See: .upgrade-report/diffs/%s.upstream-changes.diff", file) + info(" See: .upgrade-report/diffs/%s.tinygo-changes.diff", file) + u.addSummary("WOULD_MERGE", file, fmt.Sprintf("(%d upstream diff lines)", upstreamLines)) + } else { + // Attempt 3-way merge using diff3 + mergedPath := filepath.Join(u.reportDir, "merged", file) + if err := os.MkdirAll(filepath.Dir(mergedPath), 0o755); err != nil { + errorf("creating merge dir: %v", err) + return + } + + // diff3 -m tinygoFile curFile upstreamFile + cmd := exec.Command("diff3", "-m", tinygoFile, curFile, upstreamFile) + output, err := cmd.Output() + + // Write merged output regardless of exit code + if writeErr := os.WriteFile(mergedPath, output, 0o644); writeErr != nil { + errorf("writing merged file: %v", writeErr) + return + } + + if err == nil { + // Clean merge + if cpErr := copyFile(mergedPath, tinygoFile); cpErr != nil { + errorf("copying merged file: %v", cpErr) + return + } + + // Update version header + updateVersionHeader(tinygoFile, u.upstreamVersion) + + u.addSummary("MERGED_CLEAN", file, "") + success("Merged %s cleanly (Go %s → %s)", file, fileCurVersion, u.upstreamVersion) + } else { + // Merge conflicts + conflictedPath := tinygoFile + ".conflicted" + if cpErr := copyFile(mergedPath, conflictedPath); cpErr != nil { + errorf("copying conflicted file: %v", cpErr) + return + } + + conflicts := countOccurrences(mergedPath, "<<<<<<<") + + u.addSummary("CONFLICTS", file, fmt.Sprintf("(%d conflicts)", conflicts)) + warn("CONFLICTS in %s: %d conflict(s)", file, conflicts) + warn(" Conflicted merge saved to: %s.conflicted", file) + warn(" Upstream changes: .upgrade-report/diffs/%s.upstream-changes.diff", file) + warn(" TinyGo changes: .upgrade-report/diffs/%s.tinygo-changes.diff", file) + } + } +} + +func updateVersionHeader(filePath, newVersion string) { + data, err := os.ReadFile(filePath) + if err != nil { + return + } + lines := strings.SplitN(string(data), "\n", 2) + if len(lines) < 1 { + return + } + if m := versionRe.FindStringSubmatch(lines[0]); m != nil { + lines[0] = strings.Replace(lines[0], "Go "+m[1], "Go "+newVersion, 1) + os.WriteFile(filePath, []byte(strings.Join(lines, "\n")), 0o644) + } +} + +// ── Summary ───────────────────────────────────────────────────────────────── + +func (u *upgrader) printSummary() { + counts := make(map[string]int) + for _, e := range u.summary { + counts[e.status]++ + } + + fmt.Println() + fmt.Println("═══════════════════════════════════════════════════════════════") + fmt.Println(" Summary") + fmt.Println("═══════════════════════════════════════════════════════════════") + fmt.Println() + + fmt.Printf(" Unchanged (no upstream changes): %d\n", counts["UNCHANGED"]) + if u.dryRun { + fmt.Printf(" Would copy (unmodified): %d\n", counts["WOULD_COPY"]) + fmt.Printf(" Would merge (modified): %d\n", counts["WOULD_MERGE"]) + } else { + fmt.Printf(" Copied (unmodified): %d\n", counts["COPIED"]) + fmt.Printf(" Merged cleanly: %d\n", counts["MERGED_CLEAN"]) + fmt.Printf(" CONFLICTS (need manual fix): %d\n", counts["CONFLICTS"]) + } + if counts["MISSING_UPSTREAM"] > 0 { + fmt.Printf(" Missing in upstream: %d\n", counts["MISSING_UPSTREAM"]) + } + if counts["MISSING_CUR"] > 0 { + fmt.Printf(" Missing in CUR baseline: %d\n", counts["MISSING_CUR"]) + } + + if counts["CONFLICTS"] > 0 { + fmt.Println() + warn("Files with merge conflicts:") + for _, e := range u.summary { + if e.status == "CONFLICTS" { + fmt.Printf(" %s %s\n", e.file, e.detail) + } + } + } + + if u.dryRun && counts["WOULD_MERGE"] > 0 { + fmt.Println() + info("Modified files that need merging:") + for _, e := range u.summary { + if e.status == "WOULD_MERGE" { + fmt.Printf(" %s %s\n", e.file, e.detail) + } + } + } + + fmt.Println() + fmt.Printf(" Full report: %s/\n", u.reportDir) + fmt.Printf(" Diffs: %s/diffs/\n", u.reportDir) + if !u.dryRun { + fmt.Printf(" Merged: %s/merged/\n", u.reportDir) + } + fmt.Println() + + if u.dryRun { + info("Dry run complete. Review the report, then run without --dry-run to apply.") + } else { + info("Upgrade applied. Review changes, resolve any conflicts, then test.") + fmt.Println() + fmt.Println("Next steps:") + fmt.Println(" 1. Review and resolve any .conflicted files") + fmt.Println(" 2. Check for files missing from upstream (may have been renamed/removed)") + fmt.Println(" 3. Verify TINYGO comments are preserved") + fmt.Println(" 4. Test with TinyGo example/net examples") + fmt.Println(" 5. Update README.md version references") + } +} + +// ── Write summary.txt ─────────────────────────────────────────────────────── + +func (u *upgrader) writeSummaryFile() { + path := filepath.Join(u.reportDir, "summary.txt") + f, err := os.Create(path) + if err != nil { + return + } + defer f.Close() + for _, e := range u.summary { + if e.detail != "" { + fmt.Fprintf(f, "%s %s %s\n", e.status, e.file, e.detail) + } else { + fmt.Fprintf(f, "%s %s\n", e.status, e.file) + } + } +} + +// ── Utility functions ─────────────────────────────────────────────────────── + +func isDir(path string) bool { + fi, err := os.Stat(path) + return err == nil && fi.IsDir() +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func filesEqual(a, b string) bool { + dataA, errA := os.ReadFile(a) + dataB, errB := os.ReadFile(b) + if errA != nil || errB != nil { + return false + } + return string(dataA) == string(dataB) +} + +func firstLine(path string) string { + f, err := os.Open(path) + if err != nil { + return "" + } + defer f.Close() + scanner := bufio.NewScanner(f) + if scanner.Scan() { + return scanner.Text() + } + return "" +} + +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0o644) +} + +func writeDiff(fileA, fileB, outPath string) { + if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { + return + } + cmd := exec.Command("diff", "-u", fileA, fileB) + // diff returns exit code 1 when files differ, which is expected + output, _ := cmd.CombinedOutput() + os.WriteFile(outPath, output, 0o644) +} + +func countLines(path string) int { + f, err := os.Open(path) + if err != nil { + return 0 + } + defer f.Close() + count := 0 + scanner := bufio.NewScanner(f) + for scanner.Scan() { + count++ + } + return count +} + +func countOccurrences(path, needle string) int { + data, err := os.ReadFile(path) + if err != nil { + return 0 + } + return strings.Count(string(data), needle) +} + +func execOutput(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + out, err := cmd.Output() + return string(out), err +} + +// ── Main ──────────────────────────────────────────────────────────────────── + +func main() { + curVersion := flag.String("cur", defaultCurVersion, "Current Go version the TinyGo net package is based on") + upstreamVersion := flag.String("upstream", defaultUpstreamVersion, "Target upstream Go version to upgrade to") + dryRun := flag.Bool("dry-run", false, "Preview what would change without modifying files") + singleFile := flag.String("file", "", "Process only a single file") + help := flag.Bool("help", false, "Show help") + + flag.Parse() + + if *help { + fmt.Println(`TinyGo "net" package upgrade tool + +Automates Step 1 & Step 2 of the README upgrade process: + Step 1: Backport differences from Go UPSTREAM to current CUR + Step 2: Generate comparison report of NEW vs UPSTREAM + +Usage: + upgrade [--dry-run] [--cur VERSION] [--upstream VERSION] [--file FILE] + +Examples: + upgrade --dry-run # Preview what would change + upgrade --cur 1.21.4 --upstream 1.26.2 # Perform the upgrade + upgrade --dry-run --file dial.go # Preview single file`) + os.Exit(0) + } + + // Determine the TinyGo net directory (two levels up from tools/upgrade/) + exe, err := os.Executable() + if err != nil { + // Fall back to working directory + exe, _ = os.Getwd() + } else { + exe = filepath.Dir(filepath.Dir(filepath.Dir(exe))) + } + // Also support running via "go run" — use working directory if exe path looks wrong + tinygoNetDir := exe + if !fileExists(filepath.Join(tinygoNetDir, "netdev.go")) { + tinygoNetDir, _ = os.Getwd() + } + if !fileExists(filepath.Join(tinygoNetDir, "netdev.go")) { + errorf("Cannot find TinyGo net directory. Run from the net package root or build the tool first.") + os.Exit(1) + } + + u := &upgrader{ + curVersion: *curVersion, + upstreamVersion: *upstreamVersion, + dryRun: *dryRun, + tinygoNetDir: tinygoNetDir, + workDir: filepath.Join(tinygoNetDir, ".upgrade-work"), + reportDir: filepath.Join(tinygoNetDir, ".upgrade-report"), + } + + fmt.Println() + fmt.Println("═══════════════════════════════════════════════════════════════") + fmt.Println(" TinyGo net package upgrade") + fmt.Printf(" CUR: Go %s → UPSTREAM: Go %s\n", u.curVersion, u.upstreamVersion) + if u.dryRun { + fmt.Println(" Mode: DRY RUN (no files will be changed)") + } else { + fmt.Println(" Mode: APPLY CHANGES") + } + fmt.Println("═══════════════════════════════════════════════════════════════") + fmt.Println() + + // Set up report directory + os.RemoveAll(u.reportDir) + os.MkdirAll(filepath.Join(u.reportDir, "diffs"), 0o755) + os.MkdirAll(filepath.Join(u.reportDir, "merged"), 0o755) + + // Resolve Go source trees + info("Resolving Go %s source...", u.curVersion) + u.curRoot, err = resolveGoSource(u.curVersion, u.workDir) + if err != nil { + errorf("Failed to resolve Go %s: %v", u.curVersion, err) + os.Exit(1) + } + info(" → %s", u.curRoot) + + info("Resolving Go %s source...", u.upstreamVersion) + u.upstreamRoot, err = resolveGoSource(u.upstreamVersion, u.workDir) + if err != nil { + errorf("Failed to resolve Go %s: %v", u.upstreamVersion, err) + os.Exit(1) + } + info(" → %s", u.upstreamRoot) + fmt.Println() + + // Build file list + var filesToProcess []string + if *singleFile != "" { + filesToProcess = []string{*singleFile} + } else { + filesToProcess = append(filesToProcess, unmodifiedFiles...) + filesToProcess = append(filesToProcess, modifiedFiles...) + } + + // Deduplicate and sort for deterministic order + seen := make(map[string]bool) + var deduped []string + for _, f := range filesToProcess { + if !seen[f] { + seen[f] = true + deduped = append(deduped, f) + } + } + sort.Strings(deduped) + filesToProcess = deduped + + total := len(filesToProcess) + for idx, file := range filesToProcess { + fmt.Printf(colorBlue+"[%d/%d]"+colorReset+" Processing %s...\n", idx+1, total, file) + + // Skip TinyGo-only files + if tinygoOnlyFiles[file] { + info(" Skipping (TinyGo-only file)") + u.addSummary("SKIPPED_TINYGO_ONLY", file, "") + continue + } + + // Create diff directory structure + os.MkdirAll(filepath.Dir(filepath.Join(u.reportDir, "diffs", file)), 0o755) + + if modifiedFilesSet[file] { + u.processModified(file) + } else { + u.processUnmodified(file) + } + } + + u.writeSummaryFile() + u.printSummary() +} diff --git a/upgrade.sh b/upgrade.sh deleted file mode 100755 index 5b6b51b..0000000 --- a/upgrade.sh +++ /dev/null @@ -1,515 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -############################################################################## -# TinyGo "net" package upgrade script -# -# Automates Step 1 & Step 2 of the README upgrade process: -# Step 1: Backport differences from Go UPSTREAM to current CUR -# Step 2: Generate comparison report of NEW vs UPSTREAM -# -# Usage: -# ./upgrade.sh [--dry-run] [--cur VERSION] [--upstream VERSION] -# -# Examples: -# ./upgrade.sh --dry-run # Preview what would change -# ./upgrade.sh --cur 1.21.4 --upstream 1.26.2 # Perform the upgrade -# ./upgrade.sh --dry-run --file dial.go # Preview single file -############################################################################## - -# ── Defaults ──────────────────────────────────────────────────────────────── -CUR_VERSION="1.21.4" -UPSTREAM_VERSION="1.26.2" -DRY_RUN=false -SINGLE_FILE="" -TINYGO_NET_DIR="$(cd "$(dirname "$0")" && pwd)" -WORK_DIR="${TINYGO_NET_DIR}/.upgrade-work" -REPORT_DIR="${TINYGO_NET_DIR}/.upgrade-report" - -# ── Parse arguments ───────────────────────────────────────────────────────── -while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) DRY_RUN=true; shift ;; - --cur) CUR_VERSION="$2"; shift 2 ;; - --upstream) UPSTREAM_VERSION="$2"; shift 2 ;; - --file) SINGLE_FILE="$2"; shift 2 ;; - --help|-h) - sed -n '3,/^$/p' "$0" - exit 0 - ;; - *) echo "Unknown option: $1"; exit 1 ;; - esac -done - -# ── Color helpers ─────────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -info() { echo -e "${BLUE}[INFO]${NC} $*"; } -success() { echo -e "${GREEN}[OK]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -# ── File classification ───────────────────────────────────────────────────── -# Files entirely new to TinyGo (not from upstream) — never overwrite these. -TINYGO_ONLY_FILES=( - "netdev.go" - "tlssock.go" - "README.md" - "LICENSE" -) - -# Files that are straight copies from upstream (no TINYGO modifications). -# These can be replaced directly from upstream. -UNMODIFIED_FILES=( - "ip.go" - "mac.go" - "mac_test.go" - "parse.go" - "pipe.go" - "http/clone.go" - "http/cookie.go" - "http/fs.go" - "http/http.go" - "http/jar.go" - "http/method.go" - "http/sniff.go" - "http/status.go" - "http/internal/ascii/print.go" - "http/internal/ascii/print_test.go" - "http/internal/chunked.go" - "http/internal/chunked_test.go" - "http/httptest/recorder.go" - "http/httputil/dump.go" - "http/httputil/httputil.go" - "http/httputil/persist.go" - "http/httputil/reverseproxy.go" - "http/pprof/pprof.go" -) - -# Files copied AND modified for TinyGo (contain // TINYGO markers). -# These need 3-way merge: upstream changes applied to TinyGo-modified version. -MODIFIED_FILES=( - "dial.go" - "interface.go" - "iprawsock.go" - "ipsock.go" - "lookup.go" - "lookup_unix.go" - "lookup_windows.go" - "net.go" - "tcpsock.go" - "udpsock.go" - "unixsock.go" - "http/client.go" - "http/header.go" - "http/pattern.go" - "http/request.go" - "http/response.go" - "http/server.go" - "http/transfer.go" - "http/transport.go" - "http/httptest/httptest.go" - "http/httptest/server.go" - "http/httptrace/trace.go" -) - -# ── Map TinyGo paths to upstream Go stdlib paths ─────────────────────────── -# TinyGo net/ -> Go src/net/ -# TinyGo http/ -> Go src/net/http/ -upstream_path() { - local file="$1" - if [[ "$file" == http/* ]]; then - echo "src/net/${file}" - else - echo "src/net/${file}" - fi -} - -# ── Detect per-file CUR version from TINYGO header comment ───────────────── -detect_cur_version() { - local file="$1" - local header - header=$(head -1 "$file" 2>/dev/null || true) - if [[ "$header" =~ Go\ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then - echo "${BASH_REMATCH[1]}" - else - echo "$CUR_VERSION" - fi -} - -# ── Download Go source tarball ────────────────────────────────────────────── -download_go_source() { - local version="$1" - local dest_dir="${WORK_DIR}/go${version}" - - if [[ -d "$dest_dir/src/net" ]]; then - info "Go ${version} source already cached at ${dest_dir}" >&2 - return 0 - fi - - local tarball="${WORK_DIR}/go${version}.src.tar.gz" - local url="https://go.dev/dl/go${version}.src.tar.gz" - - mkdir -p "$WORK_DIR" - - if [[ ! -f "$tarball" ]]; then - info "Downloading Go ${version} source from ${url} ..." - curl -fSL -o "$tarball" "$url" - fi - - info "Extracting Go ${version} source..." - mkdir -p "$dest_dir" - tar xzf "$tarball" -C "$dest_dir" --strip-components=1 - success "Go ${version} source ready at ${dest_dir}" -} - -# Try to use local gvm install first, fall back to download -resolve_go_source() { - local version="$1" - local gvm_path="${HOME}/.gvm/gos/go${version}" - - if [[ -d "${gvm_path}/src/net" ]]; then - echo "$gvm_path" - return 0 - fi - - # Check GOROOT if the active Go matches - local goroot - goroot="$(go env GOROOT 2>/dev/null || true)" - if [[ -n "$goroot" && -d "${goroot}/src/net" ]]; then - local active_ver - active_ver="$(go version | grep -oP '\d+\.\d+\.\d+')" - if [[ "$active_ver" == "$version" ]]; then - echo "$goroot" - return 0 - fi - fi - - download_go_source "$version" >&2 - echo "${WORK_DIR}/go${version}" -} - -# ── Check if a file exists upstream ───────────────────────────────────────── -file_exists_upstream() { - local go_src_root="$1" - local file="$2" - local upath - upath="$(upstream_path "$file")" - [[ -f "${go_src_root}/${upath}" ]] -} - -# ── Process unmodified (straight copy) files ──────────────────────────────── -process_unmodified() { - local file="$1" - local upstream_root="$2" - local upath - upath="$(upstream_path "$file")" - local upstream_file="${upstream_root}/${upath}" - local tinygo_file="${TINYGO_NET_DIR}/${file}" - - if [[ ! -f "$upstream_file" ]]; then - warn "MISSING in upstream ${UPSTREAM_VERSION}: ${upath}" - echo "MISSING_UPSTREAM ${file}" >> "${REPORT_DIR}/summary.txt" - return - fi - - # Check if the file actually changed between versions - local cur_root="$3" - local cur_file="${cur_root}/${upath}" - - if [[ -f "$cur_file" ]] && diff -q "$cur_file" "$upstream_file" >/dev/null 2>&1; then - echo "UNCHANGED ${file}" >> "${REPORT_DIR}/summary.txt" - return - fi - - if $DRY_RUN; then - info "[DRY-RUN] Would replace ${file} with upstream ${UPSTREAM_VERSION} copy" - if [[ -f "$cur_file" ]]; then - diff -u "$cur_file" "$upstream_file" > "${REPORT_DIR}/diffs/${file}.diff" 2>/dev/null || true - local lines - lines=$(wc -l < "${REPORT_DIR}/diffs/${file}.diff" 2>/dev/null | tr -d ' ' || echo "0") - info " Upstream diff: ${lines} lines (see .upgrade-report/diffs/${file}.diff)" - fi - echo "WOULD_COPY ${file}" >> "${REPORT_DIR}/summary.txt" - else - # Update version header if it has one - local header - header=$(head -1 "$tinygo_file" 2>/dev/null || true) - if [[ "$header" =~ ^//\ TINYGO ]]; then - # Preserve TINYGO header, replace rest with upstream - local old_ver - old_ver=$(detect_cur_version "$tinygo_file") - head -1 "$tinygo_file" > "${tinygo_file}.tmp" - # Update version in header - sed -i "s/Go ${old_ver}/Go ${UPSTREAM_VERSION}/" "${tinygo_file}.tmp" - tail -n +2 "$upstream_file" >> "${tinygo_file}.tmp" - mv "${tinygo_file}.tmp" "$tinygo_file" - else - cp "$upstream_file" "$tinygo_file" - fi - echo "COPIED ${file}" >> "${REPORT_DIR}/summary.txt" - success "Copied ${file} from upstream ${UPSTREAM_VERSION}" - fi -} - -# ── Process modified (3-way merge) files ──────────────────────────────────── -process_modified() { - local file="$1" - local upstream_root="$2" - local cur_root="$3" - - local upath - upath="$(upstream_path "$file")" - local upstream_file="${upstream_root}/${upath}" - local tinygo_file="${TINYGO_NET_DIR}/${file}" - - if [[ ! -f "$upstream_file" ]]; then - warn "MISSING in upstream ${UPSTREAM_VERSION}: ${upath}" - echo "MISSING_UPSTREAM ${file}" >> "${REPORT_DIR}/summary.txt" - return - fi - - # Detect the actual CUR version for this specific file - local file_cur_version - file_cur_version=$(detect_cur_version "$tinygo_file") - - # Get the CUR source root for this specific file version - local file_cur_root - if [[ "$file_cur_version" != "$CUR_VERSION" ]]; then - info "${file}: based on Go ${file_cur_version} (not default ${CUR_VERSION})" - file_cur_root=$(resolve_go_source "$file_cur_version") - file_cur_root=$(echo "$file_cur_root" | tail -1) - else - file_cur_root="$cur_root" - fi - - local cur_file="${file_cur_root}/${upath}" - - if [[ ! -f "$cur_file" ]]; then - warn "MISSING in CUR ${file_cur_version}: ${upath} — can only diff against current TinyGo version" - echo "MISSING_CUR ${file}" >> "${REPORT_DIR}/summary.txt" - - # Still generate a diff of upstream vs tinygo for manual review - diff -u "$tinygo_file" "$upstream_file" > "${REPORT_DIR}/diffs/${file}.upstream-vs-tinygo.diff" 2>/dev/null || true - return - fi - - # Check if upstream even changed this file - if diff -q "$cur_file" "$upstream_file" >/dev/null 2>&1; then - echo "UNCHANGED ${file}" >> "${REPORT_DIR}/summary.txt" - return - fi - - # Generate upstream diff (what changed from CUR -> UPSTREAM in official Go) - local upstream_diff="${REPORT_DIR}/diffs/${file}.upstream-changes.diff" - diff -u "$cur_file" "$upstream_file" > "$upstream_diff" 2>/dev/null || true - - # Generate current tinygo diff (what TinyGo changed from CUR) - local tinygo_diff="${REPORT_DIR}/diffs/${file}.tinygo-changes.diff" - diff -u "$cur_file" "$tinygo_file" > "$tinygo_diff" 2>/dev/null || true - - local upstream_lines - upstream_lines=$(wc -l < "$upstream_diff" 2>/dev/null | tr -d ' ' || echo "0") - - if $DRY_RUN; then - info "[DRY-RUN] Would 3-way merge ${file} (Go ${file_cur_version} → ${UPSTREAM_VERSION})" - info " Upstream changes: ${upstream_lines} diff lines" - info " See: .upgrade-report/diffs/${file}.upstream-changes.diff" - info " See: .upgrade-report/diffs/${file}.tinygo-changes.diff" - echo "WOULD_MERGE ${file} (${upstream_lines} upstream diff lines)" >> "${REPORT_DIR}/summary.txt" - else - # Attempt 3-way merge using diff3 - # Base = CUR upstream, Ours = TinyGo modified, Theirs = new upstream - local merged="${REPORT_DIR}/merged/${file}" - mkdir -p "$(dirname "$merged")" - - # diff3: merge changes from cur_file->upstream_file into tinygo_file - # -m for merge mode, using tinygo_file as "ours" - if diff3 -m "$tinygo_file" "$cur_file" "$upstream_file" > "$merged" 2>/dev/null; then - # Clean merge - cp "$merged" "$tinygo_file" - - # Update version header - local old_ver - old_ver=$(detect_cur_version "$tinygo_file") - sed -i "1s/Go ${old_ver}/Go ${UPSTREAM_VERSION}/" "$tinygo_file" - - echo "MERGED_CLEAN ${file}" >> "${REPORT_DIR}/summary.txt" - success "Merged ${file} cleanly (Go ${file_cur_version} → ${UPSTREAM_VERSION})" - else - # Merge conflicts — save the conflicted file for manual resolution - cp "$merged" "${tinygo_file}.conflicted" - - # Count conflicts - local conflicts - conflicts=$(grep -c '^<<<<<<<' "$merged" 2>/dev/null || echo "0") - - echo "CONFLICTS ${file} (${conflicts} conflicts)" >> "${REPORT_DIR}/summary.txt" - warn "CONFLICTS in ${file}: ${conflicts} conflict(s)" - warn " Conflicted merge saved to: ${file}.conflicted" - warn " Upstream changes: .upgrade-report/diffs/${file}.upstream-changes.diff" - warn " TinyGo changes: .upgrade-report/diffs/${file}.tinygo-changes.diff" - fi - fi -} - -# ── Main ──────────────────────────────────────────────────────────────────── -main() { - echo "" - echo "═══════════════════════════════════════════════════════════════" - echo " TinyGo net package upgrade" - echo " CUR: Go ${CUR_VERSION} → UPSTREAM: Go ${UPSTREAM_VERSION}" - if $DRY_RUN; then - echo " Mode: DRY RUN (no files will be changed)" - else - echo " Mode: APPLY CHANGES" - fi - echo "═══════════════════════════════════════════════════════════════" - echo "" - - # Set up report directory - rm -rf "$REPORT_DIR" - mkdir -p "${REPORT_DIR}/diffs" "${REPORT_DIR}/merged" - - # Resolve Go source trees - info "Resolving Go ${CUR_VERSION} source..." - local cur_root - cur_root=$(resolve_go_source "$CUR_VERSION") - cur_root=$(echo "$cur_root" | tail -1) - info " → ${cur_root}" - - info "Resolving Go ${UPSTREAM_VERSION} source..." - local upstream_root - upstream_root=$(resolve_go_source "$UPSTREAM_VERSION") - upstream_root=$(echo "$upstream_root" | tail -1) - info " → ${upstream_root}" - - echo "" - - # Build file list - local files_to_process=() - if [[ -n "$SINGLE_FILE" ]]; then - files_to_process=("$SINGLE_FILE") - else - files_to_process=("${UNMODIFIED_FILES[@]}" "${MODIFIED_FILES[@]}") - fi - - # Process each file - local total=${#files_to_process[@]} - local idx=0 - - for file in "${files_to_process[@]}"; do - idx=$((idx + 1)) - echo -e "${BLUE}[${idx}/${total}]${NC} Processing ${file}..." - - # Skip TinyGo-only files - local is_tinygo_only=false - for tgo_file in "${TINYGO_ONLY_FILES[@]}"; do - if [[ "$file" == "$tgo_file" ]]; then - is_tinygo_only=true - break - fi - done - if $is_tinygo_only; then - info " Skipping (TinyGo-only file)" - echo "SKIPPED_TINYGO_ONLY ${file}" >> "${REPORT_DIR}/summary.txt" - continue - fi - - # Create diff directory structure - mkdir -p "$(dirname "${REPORT_DIR}/diffs/${file}")" - - # Is it a modified or unmodified file? - local is_modified=false - for mod_file in "${MODIFIED_FILES[@]}"; do - if [[ "$file" == "$mod_file" ]]; then - is_modified=true - break - fi - done - - if $is_modified; then - process_modified "$file" "$upstream_root" "$cur_root" - else - process_unmodified "$file" "$upstream_root" "$cur_root" - fi - done - - # ── Summary report ────────────────────────────────────────────────────── - echo "" - echo "═══════════════════════════════════════════════════════════════" - echo " Summary" - echo "═══════════════════════════════════════════════════════════════" - - if [[ -f "${REPORT_DIR}/summary.txt" ]]; then - local unchanged merged_clean conflicts_count would_copy would_merge copied missing_upstream missing_cur - unchanged=$(grep -c "^UNCHANGED " "${REPORT_DIR}/summary.txt" || true) - merged_clean=$(grep -c "^MERGED_CLEAN " "${REPORT_DIR}/summary.txt" || true) - conflicts_count=$(grep -c "^CONFLICTS " "${REPORT_DIR}/summary.txt" || true) - would_copy=$(grep -c "^WOULD_COPY " "${REPORT_DIR}/summary.txt" || true) - would_merge=$(grep -c "^WOULD_MERGE " "${REPORT_DIR}/summary.txt" || true) - copied=$(grep -c "^COPIED " "${REPORT_DIR}/summary.txt" || true) - missing_upstream=$(grep -c "^MISSING_UPSTREAM " "${REPORT_DIR}/summary.txt" || true) - missing_cur=$(grep -c "^MISSING_CUR " "${REPORT_DIR}/summary.txt" || true) - # Default to 0 if empty - : "${unchanged:=0}" "${merged_clean:=0}" "${conflicts_count:=0}" - : "${would_copy:=0}" "${would_merge:=0}" "${copied:=0}" - : "${missing_upstream:=0}" "${missing_cur:=0}" - - echo "" - echo " Unchanged (no upstream changes): ${unchanged}" - if $DRY_RUN; then - echo " Would copy (unmodified): ${would_copy}" - echo " Would merge (modified): ${would_merge}" - else - echo " Copied (unmodified): ${copied}" - echo " Merged cleanly: ${merged_clean}" - echo " CONFLICTS (need manual fix): ${conflicts_count}" - fi - [[ "$missing_upstream" -gt 0 ]] && echo " Missing in upstream: ${missing_upstream}" - [[ "$missing_cur" -gt 0 ]] && echo " Missing in CUR baseline: ${missing_cur}" - - if [[ "$conflicts_count" -gt 0 ]]; then - echo "" - warn "Files with merge conflicts:" - grep "^CONFLICTS " "${REPORT_DIR}/summary.txt" | while read -r _ f rest; do - echo " ${f} ${rest}" - done - fi - - if $DRY_RUN && [[ "$would_merge" -gt 0 ]]; then - echo "" - info "Modified files that need merging:" - grep "^WOULD_MERGE " "${REPORT_DIR}/summary.txt" | while read -r _ f rest; do - echo " ${f} ${rest}" - done - fi - fi - - echo "" - echo " Full report: ${REPORT_DIR}/" - echo " Diffs: ${REPORT_DIR}/diffs/" - if ! $DRY_RUN; then - echo " Merged: ${REPORT_DIR}/merged/" - fi - echo "" - - if $DRY_RUN; then - info "Dry run complete. Review the report, then run without --dry-run to apply." - else - info "Upgrade applied. Review changes, resolve any conflicts, then test." - echo "" - echo "Next steps:" - echo " 1. Review and resolve any .conflicted files" - echo " 2. Check for files missing from upstream (may have been renamed/removed)" - echo " 3. Verify TINYGO comments are preserved" - echo " 4. Test with TinyGo example/net examples" - echo " 5. Update README.md version references" - fi -} - -main "$@"