portbump: replace sed with targeted Tcl-based checksum and revision handling

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 <noreply@anthropic.com>
This commit is contained in:
Herby Gillot
2026-03-12 02:56:42 -04:00
parent f75f634ec1
commit 0cd72bfdc8
2 changed files with 752 additions and 61 deletions
+180 -61
View File
@@ -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 "<https://trac.macports.org/wiki/MisbehavingServers>"
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
File diff suppressed because it is too large Load Diff