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 <ron@hybridgroup.com>
This commit is contained in:
deadprogram
2026-04-17 10:55:55 +02:00
parent b2d3a2b05b
commit e82bdb20d3
5 changed files with 868 additions and 515 deletions
+2
View File
@@ -23,3 +23,5 @@ go.work
# Upgrade script work/report directories
.upgrade-work/
.upgrade-report/
./tools/upgrade/upgrade
+76
View File
@@ -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 `<file>.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
+3
View File
@@ -0,0 +1,3 @@
module github.com/tinygo-org/net/tools/upgrade
go 1.22
+787
View File
@@ -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()
}
-515
View File
@@ -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 "$@"