From a3417d034fd89cbc0b698f6abb58de356f751435 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 13 Apr 2026 17:36:12 +0200 Subject: [PATCH] Add upgrade automation script and tooling - upgrade.sh: automates backporting upstream Go net package changes - Downloads Go source tarballs for CUR and UPSTREAM versions - Copies unmodified files directly from upstream - Performs 3-way merge (diff3) for TinyGo-modified files - Detects per-file CUR version from TINYGO headers - Generates detailed diff reports in .upgrade-report/ - Supports --dry-run mode for previewing changes - .gitignore: add .upgrade-work/ and .upgrade-report/ directories --- .gitignore | 4 + upgrade.sh | 515 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100755 upgrade.sh diff --git a/.gitignore b/.gitignore index 3b735ec..6c3af2b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ # Go workspace file go.work + +# Upgrade script work/report directories +.upgrade-work/ +.upgrade-report/ diff --git a/upgrade.sh b/upgrade.sh new file mode 100755 index 0000000..5b6b51b --- /dev/null +++ b/upgrade.sh @@ -0,0 +1,515 @@ +#!/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 "$@"