From 0cd72bfdc87251b5d438e614f8693b7d8519f58d Mon Sep 17 00:00:00 2001 From: Herby Gillot Date: Thu, 12 Mar 2026 02:56:42 -0400 Subject: [PATCH] portbump: replace sed with targeted Tcl-based checksum and revision handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop sed(1) entirely from portbump. Instead, read the Portfile into a line list and use three new procs for targeted modifications: - replace_checksums: match hash values as whole words using \y word-boundary anchors, so a size value like 123 cannot accidentally clobber a larger value such as 12345; targets only the correct lines even with multiple subports, conditional branches, or variant-scoped checksums. - find_revision_line: two-phase strategy — first check for an inline revision on the subport declaration line (e.g. llvm pattern), then fall back to the nearest standalone revision line, excluding lines inside subport { } blocks when bumping the parent port. - reset_revision: simple regsub to set revision to 0, preserving indentation and surrounding text. Fixes a bug where both_checksums was reset on each distfile iteration, causing only the last distfile's checksums to be updated in Portfiles with multiple distfiles (e.g. git with git-manpages and git-htmldocs). Fixes a bug where already-correct checksums were shown in the "We will bump these:" output. Also fixes a bug where patch mode applied file attributes to the Portfile instead of the patch file. Removes dead code left over from the sed-based implementation. Adds comprehensive test suite covering real-world Portfile patterns (git, go, llvm-16, gradle), including tests for whole-word size matching, subport blocks with nested braces, multiple distfiles, partial mismatch, and the no-revision-line code path. Closes: https://trac.macports.org/ticket/65601 Closes: https://trac.macports.org/ticket/73593 Co-Authored-By: Claude Sonnet 4.6 --- src/port1.0/portbump.tcl | 241 ++++++++++---- src/port1.0/tests/portbump.test | 572 ++++++++++++++++++++++++++++++++ 2 files changed, 752 insertions(+), 61 deletions(-) create mode 100644 src/port1.0/tests/portbump.test diff --git a/src/port1.0/portbump.tcl b/src/port1.0/portbump.tcl index 71eaf1055..bc7bae72f 100644 --- a/src/port1.0/portbump.tcl +++ b/src/port1.0/portbump.tcl @@ -41,6 +41,137 @@ target_prerun ${org.macports.bump} portbump::bump_start namespace eval portbump { } +# replace_checksums +# +# Replace old checksum hash values with new ones in a list of lines. +# Returns a list of two elements: the modified lines and a list of +# line indices where replacements were made. +# +# Checksum values (hex hashes and integer sizes) are matched as whole +# words using \y word-boundary anchors (Tcl ARE syntax), so a size +# like 123 cannot accidentally match inside a larger value such as +# 12345. All checksum values are alphanumeric ([0-9a-f]+ or [0-9]+) +# and contain no regex metacharacters, so they are safe to interpolate +# directly into the pattern. +# +# lines - list of Portfile lines +# both_checksums - list of {type old_sum new_sum} triples +proc portbump::replace_checksums {lines both_checksums} { + set checksum_lines [list] + foreach {type sum calculated_sum} $both_checksums { + if {$sum eq $calculated_sum} { + continue + } + for {set i 0} {$i < [llength $lines]} {incr i} { + set line [lindex $lines $i] + if {[regsub -all "\\y${sum}\\y" $line $calculated_sum new_line]} { + lset lines $i $new_line + lappend checksum_lines $i + } + } + } + return [list $lines $checksum_lines] +} + +# find_revision_line +# +# Find the line index of the revision that should be reset for the +# given subport. Handles Portfiles with multiple revision lines across +# subport blocks, conditional branches, or inline subport declarations +# (e.g. "subport foo { revision 3 }"). +# +# Strategy: +# 1. If the current subport has an inline revision on its subport +# declaration line, use that. +# 2. Otherwise, use the standalone revision line nearest to the +# anchor (typically the first checksum line that was modified). +# When bumping the parent port, revision lines inside subport { } +# blocks are excluded. +# +# lines - list of Portfile lines +# subport - current subport name +# portname - parent port name +# anchor - line index to measure proximity from +# +# Returns: line index, or -1 if no revision line found +proc portbump::find_revision_line {lines subport portname anchor} { + # For a subport, prefer an inline revision on its subport declaration line + # (e.g. "subport clang-16 { revision 9 }"). + if {$subport ne $portname} { + set escaped_subport [string map {\\ \\\\ . \\. * \\* + \\+ ? \\? ( \\( ) \\) \{ \\\{ \} \\\} \[ \\\[ \] \\\]} $subport] + for {set i 0} {$i < [llength $lines]} {incr i} { + if {[regexp "^\\s*subport\\s+\\S*${escaped_subport}\\S*\\s+.*revision\\s+\\d+" [lindex $lines $i]]} { + return $i + } + } + } + + # When bumping the parent port, build an exclusion set of lines inside + # subport { } blocks so their revision lines are not mistakenly targeted. + set in_subport_block [dict create] + if {$subport eq $portname} { + set brace_depth 0 + set in_subport 0 + for {set i 0} {$i < [llength $lines]} {incr i} { + set line [lindex $lines $i] + if {!$in_subport && [regexp {^\s*subport\s+} $line]} { + set in_subport 1 + set opens [regexp -all {\{} $line] + set closes [regexp -all {\}} $line] + set brace_depth [expr {$opens - $closes}] + dict set in_subport_block $i 1 + if {$brace_depth <= 0} { + set in_subport 0 + set brace_depth 0 + } + } elseif {$in_subport} { + dict set in_subport_block $i 1 + set opens [regexp -all {\{} $line] + set closes [regexp -all {\}} $line] + incr brace_depth [expr {$opens - $closes}] + if {$brace_depth <= 0} { + set in_subport 0 + set brace_depth 0 + } + } + } + } + + # Find the nearest standalone revision line, skipping any inside subport blocks. + set best_line -1 + set best_dist 2147483647 + for {set i 0} {$i < [llength $lines]} {incr i} { + if {[regexp {^\s*revision\s+\d+} [lindex $lines $i]]} { + if {[dict exists $in_subport_block $i]} { + continue + } + set dist [expr {abs($i - $anchor)}] + if {$dist < $best_dist} { + set best_dist $dist + set best_line $i + } + } + } + + return $best_line +} + +# reset_revision +# +# Reset the revision value to 0 on the specified line. +# Returns the modified list of lines. +# +# lines - list of Portfile lines +# line_idx - index of the line to modify +proc portbump::reset_revision {lines line_idx} { + set old_line [lindex $lines $line_idx] + set new_line [regsub {(revision\s+)\d+} $old_line {\10}] + if {$old_line ne $new_line} { + lset lines $line_idx $new_line + } + return $lines +} + # bump_start # # Target prerun procedure; simply prints a message about what we're doing. @@ -67,18 +198,17 @@ proc portbump::bump_main {args} { # So far, no mismatches yet. set mismatch no + set wrong_mimetype no - # Set the list of checksums as the option checksums. + # Read the declared checksums from the port options. set checksums_str [option checksums] - # Store the calculated checksums to avoid repeated calculations - array set calculated_checksums_array {} - # If everything is fine with the syntax, keep on and check the checksum of # the distfiles. if {[portchecksum::parse_checksums $checksums_str] eq "yes"} { global distpath + set both_checksums [list] foreach distfile $all_dist_files { ui_info "$UI_PREFIX [format [msgcat::mc "Checksumming %s"] $distfile]" @@ -95,14 +225,10 @@ proc portbump::bump_main {args} { } else { # Retrieve the list of types/values from the array. set portfile_checksums $checksums_array($distfile) - set calculated_checksums [list] - set both_checksums [list] # Iterate on this list to check the actual values. foreach {type sum} $portfile_checksums { set calculated_sum [portchecksum::calc_$type $fullpath] - lappend calculated_checksums $type - lappend calculated_checksums $calculated_sum lappend both_checksums $type $sum $calculated_sum if {$sum eq $calculated_sum} { @@ -116,9 +242,6 @@ proc portbump::bump_main {args} { } } - # Save our calculated checksums in case we need them later - set calculated_checksums_array($distfile) $calculated_checksums - if {![regexp {\.html?$} ${distfile}] && ![catch {strsed [exec [findBinary file $portutil::autoconf::file_path] $fullpath --brief --mime] {s/;.*$//}} mimetype] && "text/html" eq $mimetype} { @@ -150,70 +273,61 @@ proc portbump::bump_main {args} { ui_notice "" ui_notice "***" ui_notice "The file has been moved to: $htmlfile_path" - - return -code error "[msgcat::mc "Unable to verify file checksums"]" - } elseif {![info exists both_checksums]} { - return -code error "[msgcat::mc "No checksums found in Portfile"]" - } else { - # Show the desired checksum line for easy cut-paste - # based on the previously calculated values, plus our default types - global version revision subport + return -code error "[msgcat::mc "Unable to verify file checksums"]" + } else { + global version subport ui_msg "We will bump these:" foreach {type sum calculated_sum} $both_checksums { + if {$sum eq $calculated_sum} { + continue + } ui_msg [format "Old %-8s %s" ${type}: $sum] ui_msg [format "New %-8s %s" ${type}: $calculated_sum] } - set patterns [list] - - set whitespace {[[:space:]\\]+} - # Create substitution pattern(s) for checksum - foreach {type sum calculated_sum} $both_checksums { - lappend patterns "s/(${type}${whitespace})${sum}/\\1${calculated_sum}/g" - } - # Create substitution pattern for revision (reset to 0) - lappend patterns {s/(revision[[:space:]\\]+)[0-9]+/\10/g} - - # Construct sed command - set cmdline [list] - lappend cmdline $portutil::autoconf::sed_command -E - foreach pattern $patterns { - lappend cmdline -e $pattern - } - # Get the uid of Portfile owner set owneruid [name_to_uid [file attributes ${portfile} -owner]] # root -> owner id exec_as_uid $owneruid { - # Create temporary Portfile_XXXXXX.bump - if {[catch {set tmpfd [file tempfile tmpfile ${portpath}/Portfile.bump]} error]} { - ui_debug $::errorInfo - ui_error "file tempfile: $error" - return -code error "file tempfile failed" - } + # Read the Portfile + set fd [open $portfile r] + set lines [split [read $fd] \n] + close $fd # Get Portfile attributes set attributes [file attributes $portfile] - # Direct sed command output to tempfile - lappend cmdline "<${portfile}" ">@$tmpfd" - - # Run sed command and write to Portfile.bump - ui_info "$UI_PREFIX [format [msgcat::mc "Patching %s: %s"] $portfile $patterns]" - if {[catch {exec -ignorestderr -- {*}$cmdline} error]} { - ui_debug $::errorInfo - ui_error "sed: $error" - file delete "$tmpfile" - close $tmpfd - return -code error "sed sed(1) failed" + lassign [portbump::replace_checksums $lines $both_checksums] lines checksum_lines + foreach cl $checksum_lines { + ui_debug "Replaced checksum on line [expr {$cl + 1}]" } + if {[llength $checksum_lines] > 0} { + set rev_line [portbump::find_revision_line $lines $subport [option name] [lindex $checksum_lines 0]] + if {$rev_line >= 0} { + set old_rev [lindex $lines $rev_line] + set lines [portbump::reset_revision $lines $rev_line] + if {[lindex $lines $rev_line] ne $old_rev} { + ui_debug "Reset revision to 0 on line [expr {$rev_line + 1}]" + } + } + } + + set new_content [join $lines \n] + if {[tbool ports_bump_patch]} { # Patch mode - # Set Potfile.patch path + if {[catch {set tmpfd [file tempfile tmpfile ${portpath}/Portfile.bump]} error]} { + ui_debug $::errorInfo + ui_error "file tempfile: $error" + return -code error "file tempfile failed" + } + puts -nonewline $tmpfd $new_content + close $tmpfd + set patchfile "${portpath}/Portfile.patch" set patchfd [open $patchfile w] @@ -224,17 +338,25 @@ proc portbump::bump_main {args} { # Create and write diff to Portfile.patch if {[catch {exec -ignorestderr -- {*}$diffcmd} error]} { - # Copy Portfile attributes to Portfile.patch - file attributes $portfile {*}$attributes - ui_msg "Portfile.patch successfully created at $patchfile" + file attributes $patchfile {*}$attributes + ui_msg "Portfile.patch successfully created at $patchfile" } else { ui_msg "No changes needed." file delete "$patchfile" close $patchfd } + + file delete "$tmpfile" } else { # Overwrite mode - # Replace Portfile with Portfile.bump + if {[catch {set tmpfd [file tempfile tmpfile ${portpath}/Portfile.bump]} error]} { + ui_debug $::errorInfo + ui_error "file tempfile: $error" + return -code error "file tempfile failed" + } + puts -nonewline $tmpfd $new_content + close $tmpfd + if {[catch {move -force $tmpfile $portfile} error]} { ui_debug $::errorInfo ui_error "bump: $error" @@ -248,9 +370,6 @@ proc portbump::bump_main {args} { ui_msg "Checksums successfully bumped. Suggested commit message:" ui_msg [format "%-8s%s: update to %s" "" ${subport} $version] } - - # Delete Portfile.bump - file delete "$tmpfile" } return 0 diff --git a/src/port1.0/tests/portbump.test b/src/port1.0/tests/portbump.test new file mode 100644 index 000000000..67718e886 --- /dev/null +++ b/src/port1.0/tests/portbump.test @@ -0,0 +1,572 @@ +# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 + +package require tcltest 2 +namespace import tcltest::* + +set pwd [file dirname [file normalize $argv0]] + +source ../port_test_autoconf.tcl +package require macports 1.0 + +array set ui_options {} +mportinit ui_options + +# Provide a stub for the port callback mechanism +namespace eval port { + proc register_callback {args} {} + proc run_callbacks {args} {} +} + +package require portbump 1.0 + +# ============================================================ +# Tests for portbump::replace_checksums +# ============================================================ + +test replace_checksums_basic { + Replace a single checksum value +} -body { + set lines [list \ + "PortSystem 1.0" \ + "name foo" \ + "version 1.0" \ + "checksums rmd160 aaa111 \\" \ + " sha256 bbb222 \\" \ + " size 12345" \ + ] + set both {rmd160 aaa111 ccc333 sha256 bbb222 ddd444} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + set rmd_line [lindex $result_lines 3] + set sha_line [lindex $result_lines 4] + list [string first ccc333 $rmd_line] [string first ddd444 $sha_line] [llength $result_indices] +} -result {17 17 2} + +test replace_checksums_no_change { + Skip checksums that already match +} -body { + set lines [list \ + "checksums rmd160 aaa111 sha256 bbb222" \ + ] + set both {rmd160 aaa111 aaa111 sha256 bbb222 bbb222} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + list [lindex $result_lines 0] [llength $result_indices] +} -result {{checksums rmd160 aaa111 sha256 bbb222} 0} + +test replace_checksums_unique_match { + Only replace the line containing the specific hash, not other lines +} -body { + # Simulates a Portfile with checksums in two different conditional branches + set lines [list \ + "if cond1 \{" \ + " checksums sha256 aaaa1111" \ + "\}" \ + "if cond2 \{" \ + " checksums sha256 bbbb2222" \ + "\}" \ + ] + # Only replace aaaa1111, leave bbbb2222 alone + set both {sha256 aaaa1111 cccc3333} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + list [string first cccc3333 [lindex $result_lines 1]] \ + [string first bbbb2222 [lindex $result_lines 4]] \ + $result_indices +} -result {21 21 1} + +test replace_checksums_size_whole_word { + Size values are matched as whole words, not substrings +} -body { + # Replacing size 12345 must not clobber 1234567 (12345 is a prefix) + # or 9912345 (12345 is a suffix). + set lines [list \ + "checksums size 12345 \\" \ + " size 1234567 \\" \ + " size 9912345" \ + ] + set both {size 12345 99999 size 1234567 1234567 size 9912345 9912345} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + list [string first 99999 [lindex $result_lines 0]] \ + [string first 1234567 [lindex $result_lines 1]] \ + [string first 9912345 [lindex $result_lines 2]] \ + [llength $result_indices] +} -result {15 15 15 1} + +test replace_checksums_size_field { + Replace size field alongside hash checksums +} -body { + set lines [list \ + "checksums rmd160 aaa111bbb222 \\" \ + " sha256 ccc333ddd444 \\" \ + " size 9876543" \ + ] + set both {rmd160 aaa111bbb222 xxx111yyy222 sha256 ccc333ddd444 zzz333www444 size 9876543 1234567} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + list [string first xxx111yyy222 [lindex $result_lines 0]] \ + [string first zzz333www444 [lindex $result_lines 1]] \ + [string first 1234567 [lindex $result_lines 2]] \ + [llength $result_indices] +} -result {17 17 17 3} + +test replace_checksums_mixed_implicit_and_explicit_distfiles { + Replace checksums in a block with an implicit distfile followed by an explicit one with variable substitution +} -body { + # Simulates a Portfile where the primary distfile is implicit (no filename + # in the checksums block) and a secondary distfile uses Tcl variable + # substitution in its name. replace_checksums matches on hash values only + # and is unaffected by whether or how distfile names appear in the block. + set lines [list \ + "checksums rmd160 oldrmd1 \\" \ + " sha256 oldsha1 \\" \ + " size 11111 \\" \ + " \${name}-docs-\${version}.tar.gz \\" \ + " rmd160 oldrmd2 \\" \ + " sha256 oldsha2 \\" \ + " size 22222" \ + ] + set both {rmd160 oldrmd1 newrmd1 sha256 oldsha1 newsha1 rmd160 oldrmd2 newrmd2 sha256 oldsha2 newsha2} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + set implicit_rmd [expr {[string first newrmd1 [lindex $result_lines 0]] >= 0}] + set implicit_sha [expr {[string first newsha1 [lindex $result_lines 1]] >= 0}] + set filename_unchanged [expr {[string first {${name}-docs-${version}.tar.gz} [lindex $result_lines 3]] >= 0}] + set explicit_rmd [expr {[string first newrmd2 [lindex $result_lines 4]] >= 0}] + set explicit_sha [expr {[string first newsha2 [lindex $result_lines 5]] >= 0}] + list $implicit_rmd $implicit_sha $filename_unchanged $explicit_rmd $explicit_sha +} -result {1 1 1 1 1} + +test replace_checksums_multiple_distfiles { + Replace checksums for multiple distfiles on separate lines +} -body { + # Simulates git Portfile pattern with multiple distfiles + set lines [list \ + "checksums git-2.53.0.tar.xz \\" \ + " rmd160 f125aaa \\" \ + " sha256 5818bbb \\" \ + " size 7993096 \\" \ + " git-manpages-2.53.0.tar.xz \\" \ + " rmd160 dbc6ccc \\" \ + " sha256 957fddd \\" \ + " size 614136" \ + ] + set both {rmd160 f125aaa new_rmd1 sha256 5818bbb new_sha1 rmd160 dbc6ccc new_rmd2 sha256 957fddd new_sha2} + lassign [portbump::replace_checksums $lines $both] result_lines result_indices + set has_new_rmd1 [expr {[string first new_rmd1 [lindex $result_lines 1]] >= 0}] + set has_new_sha1 [expr {[string first new_sha1 [lindex $result_lines 2]] >= 0}] + set has_new_rmd2 [expr {[string first new_rmd2 [lindex $result_lines 5]] >= 0}] + set has_new_sha2 [expr {[string first new_sha2 [lindex $result_lines 6]] >= 0}] + list $has_new_rmd1 $has_new_sha1 $has_new_rmd2 $has_new_sha2 +} -result {1 1 1 1} + +# ============================================================ +# Tests for portbump::find_revision_line +# ============================================================ + +test find_revision_simple { + Find the only revision line in a simple Portfile +} -body { + set lines [list \ + "PortSystem 1.0" \ + "name foo" \ + "version 1.0" \ + "revision 2" \ + "checksums sha256 aaa111" \ + ] + portbump::find_revision_line $lines "foo" "foo" 4 +} -result 3 + +test find_revision_nearest_to_checksums { + Find the revision nearest to the checksum anchor, not the first one +} -body { + # Simulates git Portfile: parent revision on line 2, subport revision on line 6 + set lines [list \ + "name git" \ + "version 2.53.0" \ + "revision 0" \ + "" \ + "subport git-devel \{" \ + " version 2.53.0" \ + " revision 0" \ + "\}" \ + "" \ + "if \{name eq subport\} \{" \ + " checksums sha256 aaa111" \ + "\} else \{" \ + " checksums sha256 bbb222" \ + "\}" \ + ] + # Bumping git-devel: checksums on line 12, nearest revision is line 6 + portbump::find_revision_line $lines "git-devel" "git" 12 +} -result 6 + +test find_revision_parent_port { + Find revision for the parent port (subport == name) +} -body { + set lines [list \ + "name git" \ + "version 2.53.0" \ + "revision 1" \ + "" \ + "subport git-devel \{" \ + " revision 3" \ + "\}" \ + "" \ + "checksums sha256 aaa111" \ + ] + # Bumping git: checksums on line 8, nearest revision is line 2 + portbump::find_revision_line $lines "git" "git" 8 +} -result 2 + +test find_revision_inline_subport { + Find inline revision on a subport declaration line (llvm pattern) +} -body { + set lines [list \ + "name llvm-16" \ + "revision 3" \ + "subport mlir-16 \{ revision 1 \}" \ + "subport clang-16 \{ revision 9 \}" \ + "subport lldb-16 \{ revision 4 \}" \ + "" \ + "checksums sha256 aaa111" \ + ] + # Bumping clang-16: should find the inline revision on line 3 + portbump::find_revision_line $lines "clang-16" "llvm-16" 6 +} -result 3 + +test find_revision_inline_prefers_over_nearest { + Inline revision is preferred even if a standalone one is closer +} -body { + set lines [list \ + "name llvm-16" \ + "revision 3" \ + "subport clang-16 \{ revision 9 \}" \ + "" \ + "checksums sha256 aaa111" \ + ] + # The standalone revision on line 1 is closer to checksums (line 4), + # but the inline on line 2 should win for clang-16 + portbump::find_revision_line $lines "clang-16" "llvm-16" 4 +} -result 2 + +test find_revision_conditional_branches { + Find correct revision among multiple conditional branches (go pattern) +} -body { + # Simulates go Portfile with revision in nested conditionals + set lines [list \ + "if \{subport eq name\} \{" \ + " if \{os.major < 17\} \{" \ + " version 1.11.13" \ + " revision 0" \ + " \} else \{" \ + " version 1.24.8" \ + " revision 0" \ + " \}" \ + "\}" \ + "subport go-devel \{" \ + " version 1.26.0" \ + " revision 2" \ + "\}" \ + "" \ + "if \{subport eq go-devel\} \{" \ + " checksums sha256 ddd444" \ + "\} else \{" \ + " if \{os.major <= 20\} \{" \ + " checksums sha256 bbb222" \ + " \} else \{" \ + " checksums sha256 ccc333" \ + " \}" \ + "\}" \ + ] + # Bumping go on macOS 12+: checksums on line 20, nearest revision is line 6 + set rev_go [portbump::find_revision_line $lines "go" "go" 20] + # Bumping go-devel: checksums on line 15, nearest revision is line 11 + set rev_devel [portbump::find_revision_line $lines "go-devel" "go" 15] + list $rev_go $rev_devel +} -result {6 11} + +test find_revision_subport_nested_braces { + Skip revision inside subport block with nested braces when bumping parent +} -body { + # Subport block contains an inner if {} block — tests brace depth counter > 1 + set lines [list \ + "name foo" \ + "version 1.0" \ + "revision 5" \ + "" \ + "subport foo-doc \{" \ + " if \{\$os.major >= 20\} \{" \ + " revision 2" \ + " \}" \ + "\}" \ + "" \ + "checksums sha256 aaa111" \ + ] + # Bumping foo (parent): the revision on line 6 is inside the subport block + # and must be excluded; the parent revision on line 2 should be found. + portbump::find_revision_line $lines "foo" "foo" 10 +} -result 2 + +test find_revision_subport_no_inline { + Subport without an inline revision falls back to the nearest standalone +} -body { + set lines [list \ + "name foo" \ + "version 1.0" \ + "revision 3" \ + "" \ + "subport foo-doc \{\}" \ + "" \ + "checksums sha256 aaa111" \ + ] + # Bumping foo-doc: no inline revision on the subport line, nearest standalone is line 2 + portbump::find_revision_line $lines "foo-doc" "foo" 6 +} -result 2 + +test find_revision_no_revision { + Return -1 when no revision line exists +} -body { + set lines [list \ + "PortSystem 1.0" \ + "name foo" \ + "checksums sha256 aaa111" \ + ] + portbump::find_revision_line $lines "foo" "foo" 2 +} -result -1 + +# ============================================================ +# Tests for portbump::reset_revision +# ============================================================ + +test reset_revision_basic { + Reset a standalone revision line to 0 +} -body { + set lines [list "name foo" "revision 5" "checksums sha256 aaa"] + set lines [portbump::reset_revision $lines 1] + lindex $lines 1 +} -result "revision 0" + +test reset_revision_already_zero { + No change when revision is already 0 +} -body { + set lines [list "name foo" "revision 0" "checksums sha256 aaa"] + set result [portbump::reset_revision $lines 1] + lindex $result 1 +} -result "revision 0" + +test reset_revision_multidigit { + Reset a multi-digit revision value to 0 +} -body { + set lines [list "name foo" "revision 12" "checksums sha256 aaa"] + set lines [portbump::reset_revision $lines 1] + lindex $lines 1 +} -result "revision 0" + +test reset_revision_inline { + Reset an inline revision on a subport declaration line +} -body { + set lines [list \ + "name llvm-16" \ + "subport clang-16 \{ revision 9 \}" \ + ] + set lines [portbump::reset_revision $lines 1] + lindex $lines 1 +} -result "subport clang-16 \{ revision 0 \}" + +test reset_revision_preserves_indentation { + Preserve leading whitespace when resetting revision +} -body { + set lines [list "if cond \{" " revision 7" "\}"] + set lines [portbump::reset_revision $lines 1] + lindex $lines 1 +} -result " revision 0" + +# ============================================================ +# Integration-style tests combining all three procs +# ============================================================ + +test integration_partial_mismatch { + Only incorrect checksums are updated; already-correct ones are left as-is +} -body { + set lines [list \ + "PortSystem 1.0" \ + "name foo" \ + "version 2.0" \ + "revision 1" \ + "" \ + "checksums sha256 correcthash_aaa111bbb222ccc333ddd444 \\" \ + " size 9999" \ + ] + # sha256 is already correct; only size needs updating + set both {sha256 correcthash_aaa111bbb222ccc333ddd444 correcthash_aaa111bbb222ccc333ddd444 \ + size 9999 12345} + + lassign [portbump::replace_checksums $lines $both] lines checksum_lines + + if {[llength $checksum_lines] > 0} { + set rev_line [portbump::find_revision_line $lines "foo" "foo" [lindex $checksum_lines 0]] + if {$rev_line >= 0} { + set lines [portbump::reset_revision $lines $rev_line] + } + } + + set sha_unchanged [expr {[string first correcthash_aaa111bbb222ccc333ddd444 [lindex $lines 5]] >= 0}] + set size_updated [expr {[string first 12345 [lindex $lines 6]] >= 0}] + set rev_reset [expr {[lindex $lines 3] eq "revision 0"}] + list $sha_unchanged $size_updated $rev_reset +} -result {1 1 1} + +test integration_simple_portfile { + Full bump of a simple single-subport Portfile +} -body { + set lines [list \ + "PortSystem 1.0" \ + "name foo" \ + "version 2.0" \ + "revision 1" \ + "" \ + "checksums rmd160 oldrmdhash1234567890abcdef01234567 \\" \ + " sha256 oldshahash1234567890abcdef01234567890abcdef01234567890abcdef0123" \ + ] + set both {rmd160 oldrmdhash1234567890abcdef01234567 newrmdhash1234567890abcdef01234567 sha256 oldshahash1234567890abcdef01234567890abcdef01234567890abcdef0123 newshahash1234567890abcdef01234567890abcdef01234567890abcdef0123} + + lassign [portbump::replace_checksums $lines $both] lines checksum_lines + + if {[llength $checksum_lines] > 0} { + set rev_line [portbump::find_revision_line $lines "foo" "foo" [lindex $checksum_lines 0]] + if {$rev_line >= 0} { + set lines [portbump::reset_revision $lines $rev_line] + } + } + + set has_new_rmd [expr {[string first newrmdhash [lindex $lines 5]] >= 0}] + set has_new_sha [expr {[string first newshahash [lindex $lines 6]] >= 0}] + set rev_reset [expr {[lindex $lines 3] eq "revision 0"}] + list $has_new_rmd $has_new_sha $rev_reset +} -result {1 1 1} + +test integration_subport_with_shared_checksums { + Bump subport with shared checksums (llvm pattern) resets only the inline revision +} -body { + set lines [list \ + "name llvm-16" \ + "revision 3" \ + "subport mlir-16 \{ revision 1 \}" \ + "subport clang-16 \{ revision 9 \}" \ + "" \ + "checksums sha256 oldhash_aaa111bbb222ccc333ddd444eee555fff666" \ + ] + set both {sha256 oldhash_aaa111bbb222ccc333ddd444eee555fff666 newhash_111aaa222bbb333ccc444ddd555eee666fff} + + lassign [portbump::replace_checksums $lines $both] lines checksum_lines + set rev_line [portbump::find_revision_line $lines "clang-16" "llvm-16" [lindex $checksum_lines 0]] + set lines [portbump::reset_revision $lines $rev_line] + + set parent_rev [lindex $lines 1] + set clang_line [lindex $lines 3] + set mlir_line [lindex $lines 2] + list $parent_rev $clang_line $mlir_line +} -result {{revision 3} {subport clang-16 { revision 0 }} {subport mlir-16 { revision 1 }}} + +test integration_variant_checksums { + Bump with checksums inside a variant block (gradle pattern) +} -body { + set lines [list \ + "name gradle" \ + "version 8.12" \ + "revision 0" \ + "" \ + "checksums sha256 basehash_aaa111bbb222ccc333ddd444" \ + "" \ + "variant all \{" \ + " checksums sha256 varianthash_eee555fff666aaa111bbb222" \ + "\}" \ + ] + # Bumping with +all variant active — only the variant hash changes + set both {sha256 varianthash_eee555fff666aaa111bbb222 newhash_999888777666555444333222} + + lassign [portbump::replace_checksums $lines $both] lines checksum_lines + set rev_line [portbump::find_revision_line $lines "gradle" "gradle" [lindex $checksum_lines 0]] + set lines [portbump::reset_revision $lines $rev_line] + + set base_unchanged [expr {[string first basehash_ [lindex $lines 4]] >= 0}] + set variant_changed [expr {[string first newhash_ [lindex $lines 7]] >= 0}] + list $base_unchanged $variant_changed [lindex $lines 2] +} -result {1 1 {revision 0}} + +test integration_multiple_distfiles_all_replaced { + Bump with three distfiles replaces checksums in all of them (git pattern) +} -body { + # Simulates git Portfile with three distfiles: git, git-manpages, git-htmldocs. + # This specifically tests that both_checksums is accumulated across all distfiles, + # not reset per distfile (the bug that caused only the last distfile to be updated). + set lines [list \ + "PortSystem 1.0" \ + "name git" \ + "version 2.51.0" \ + "revision 0" \ + "" \ + "checksums git-2.51.0.tar.xz \\" \ + " rmd160 old_rmd_git111 \\" \ + " sha256 old_sha_git222 \\" \ + " size 7993096 \\" \ + " git-manpages-2.51.0.tar.xz \\" \ + " rmd160 old_rmd_man111 \\" \ + " sha256 old_sha_man222 \\" \ + " size 614136 \\" \ + " git-htmldocs-2.51.0.tar.xz \\" \ + " rmd160 old_rmd_html111 \\" \ + " sha256 old_sha_html222 \\" \ + " size 1675212" \ + ] + + # Simulate what bump_main's loop builds when both_checksums is accumulated + # correctly (outside the foreach distfile loop) + set both_checksums [list] + # distfile 1: git + lappend both_checksums rmd160 old_rmd_git111 new_rmd_git111 + lappend both_checksums sha256 old_sha_git222 new_sha_git222 + # distfile 2: git-manpages + lappend both_checksums rmd160 old_rmd_man111 new_rmd_man111 + lappend both_checksums sha256 old_sha_man222 new_sha_man222 + # distfile 3: git-htmldocs + lappend both_checksums rmd160 old_rmd_html111 new_rmd_html111 + lappend both_checksums sha256 old_sha_html222 new_sha_html222 + + lassign [portbump::replace_checksums $lines $both_checksums] lines checksum_lines + + set rev_line [portbump::find_revision_line $lines "git" "git" [lindex $checksum_lines 0]] + set lines [portbump::reset_revision $lines $rev_line] + + set git_rmd [expr {[string first new_rmd_git111 [lindex $lines 6]] >= 0}] + set git_sha [expr {[string first new_sha_git222 [lindex $lines 7]] >= 0}] + set man_rmd [expr {[string first new_rmd_man111 [lindex $lines 10]] >= 0}] + set man_sha [expr {[string first new_sha_man222 [lindex $lines 11]] >= 0}] + set html_rmd [expr {[string first new_rmd_html111 [lindex $lines 14]] >= 0}] + set html_sha [expr {[string first new_sha_html222 [lindex $lines 15]] >= 0}] + set rev_reset [expr {[lindex $lines 3] eq "revision 0"}] + list $git_rmd $git_sha $man_rmd $man_sha $html_rmd $html_sha $rev_reset +} -result {1 1 1 1 1 1 1} + +test integration_no_revision_line { + Bump checksums when no revision line exists: checksums updated, no error +} -body { + set lines [list \ + "PortSystem 1.0" \ + "name foo" \ + "version 2.0" \ + "" \ + "checksums sha256 oldshahash1234567890abcdef01234567890abcdef01234567890abcdef0123" \ + ] + set both {sha256 oldshahash1234567890abcdef01234567890abcdef01234567890abcdef0123 newshahash1234567890abcdef01234567890abcdef01234567890abcdef0123} + + lassign [portbump::replace_checksums $lines $both] lines checksum_lines + + if {[llength $checksum_lines] > 0} { + set rev_line [portbump::find_revision_line $lines "foo" "foo" [lindex $checksum_lines 0]] + if {$rev_line >= 0} { + set lines [portbump::reset_revision $lines $rev_line] + } + } + + set has_new_sha [expr {[string first newshahash [lindex $lines 4]] >= 0}] + list $has_new_sha [llength $checksum_lines] +} -result {1 1} + +cleanupTests