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
This commit is contained in:
deadprogram
2026-04-13 17:36:12 +02:00
committed by Ron Evans
parent fbef30f4ed
commit a3417d034f
2 changed files with 519 additions and 0 deletions
Executable
+515
View File
@@ -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 "$@"