mirror of
https://github.com/tinygo-org/net.git
synced 2026-07-26 08:18:39 +00:00
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:
@@ -23,3 +23,5 @@ go.work
|
||||
# Upgrade script work/report directories
|
||||
.upgrade-work/
|
||||
.upgrade-report/
|
||||
|
||||
./tools/upgrade/upgrade
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/tinygo-org/net/tools/upgrade
|
||||
|
||||
go 1.22
|
||||
@@ -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
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user