I've grown tired of toiling, so here's a bit of automation which does
all the easy parts of updating cabal-module(5) based ports.

I have a sequence of commits extracting MODCABAL_MANIFEST into cabal.inc
files which I want to commit as a follow-up.  Here's the sequence of
commits, look for "extract cabal.inc" commits:
https://github.com/blackgnezdo/ports/commits/automate-openbsd-cabal-updates/

Not making a secret that this was made with help of Claude Opus. I read
and iterated on the code, so I stand behind this code. If people like
it, I'm happy to get an OK and maintain it in the tree. Otherwise, I can
leave it out and run myself.

Thanks
Greg

>From 5abcdd33e5defdbb0091b258d7a3ac1362b90ad7 Mon Sep 17 00:00:00 2001
From: Claude <[email protected]>
Date: Mon, 29 Dec 2025 02:32:21 +0000
Subject: [PATCH] devel/cabal: add port update tools

Add update-cabal-port.pl and update-all-cabal-ports.sh to help
maintain cabal ports. The Perl script uses cabal database to fetch
version and generates cabal.inc files with dependency manifests.

The batch script finds all ports using devel/cabal module and can update
them in sequence, with optional git commits per port.

devel/cabal: use make show= and simplify update script

- Use OpenBSD ports `make show=VAR` instead of parsing Makefiles
- Handle MODCABAL_REVISION from cabal-bundler output
- Remove REVISION variable on port updates
---
 devel/cabal/tools/update-all-cabal-ports.sh |  74 +++++++++
 devel/cabal/tools/update-cabal-port.pl      | 169 ++++++++++++++++++++
 2 files changed, 243 insertions(+)
 create mode 100755 devel/cabal/tools/update-all-cabal-ports.sh
 create mode 100755 devel/cabal/tools/update-cabal-port.pl

diff --git a/devel/cabal/tools/update-all-cabal-ports.sh b/devel/cabal/tools/update-all-cabal-ports.sh
new file mode 100755
index 00000000000..1f65810a6ad
--- /dev/null
+++ b/devel/cabal/tools/update-all-cabal-ports.sh
@@ -0,0 +1,74 @@
+#!/bin/ksh
+#
+# Update all cabal ports to latest versions
+#
+# Usage: update-all-cabal-ports.sh [options]
+#
+# Options:
+#   --package          Run 'make package' for each port
+#   --git-commit       Create git commit for each successful update
+#   --help             Show this help message
+#
+
+set -e
+
+usage() {
+	sed -n '2,/^$/s/^# \?//p' < "$0"
+	exit "${1:-0}"
+}
+
+typeset -i RUN_PACKAGE=0 GIT_COMMIT=0
+
+while [[ $# -gt 0 ]]; do
+	case "$1" in
+		--package)    RUN_PACKAGE=1 ;;
+		--git-commit) GIT_COMMIT=1 ;;
+		--help|-h)    usage 0 ;;
+		*)            print -u2 "Unknown option: $1"; usage 1 ;;
+	esac
+	shift
+done
+
+UPDATE_SCRIPT="./devel/cabal/tools/update-cabal-port.pl"
+[[ -x "$UPDATE_SCRIPT" ]] || { print -u2 "Update script not found: $UPDATE_SCRIPT"; exit 1; }
+
+CABAL_PORTS=$(grep -rl '^MODULES.*=.*devel/cabal' */*/Makefile | sed 's|/Makefile$||' | sort)
+typeset -i TOTAL=$(print "$CABAL_PORTS" | wc -l)
+
+print "Found $TOTAL cabal ports"
+
+typeset -i SUCCESS=0 SKIPPED=0 FAILED=0 CURRENT=0
+
+for port in $CABAL_PORTS; do
+	((++CURRENT))
+
+	print "==> [$CURRENT/$TOTAL] $port"
+
+	UPDATE_CMD="$UPDATE_SCRIPT $port"
+	((RUN_PACKAGE)) && UPDATE_CMD="$UPDATE_CMD --package"
+
+	if $UPDATE_CMD; then
+		cd "$port"
+		if git diff --quiet Makefile cabal.inc 2>/dev/null; then
+			((++SKIPPED))
+		else
+			((++SUCCESS))
+			if ((GIT_COMMIT)); then
+				VERSION=$(make show=MODCABAL_VERSION)
+				git add Makefile cabal.inc distinfo 2>/dev/null
+				git commit -m "$port: update to $VERSION"
+			fi
+		fi
+		cd - >/dev/null
+	else
+		print -u2 "==> FAILED: $port"
+		((++FAILED))
+		print -n "Continue? [Y/n] "
+		read response
+		[[ "$response" == [nN]* ]] && break
+	fi
+done
+
+print "==> Summary: $SUCCESS updated, $SKIPPED unchanged, $FAILED failed"
+((FAILED)) && exit 1
+exit 0
diff --git a/devel/cabal/tools/update-cabal-port.pl b/devel/cabal/tools/update-cabal-port.pl
new file mode 100755
index 00000000000..8e5eaab182d
--- /dev/null
+++ b/devel/cabal/tools/update-cabal-port.pl
@@ -0,0 +1,169 @@
+#!/usr/bin/perl
+#
+# Update a cabal port to a new version
+#
+# Usage: update-cabal-port.pl <port-directory> [options]
+#
+# Options:
+#   --version <ver>    Update to specific version (default: latest from Hackage)
+#   --package          Run 'make package' after update
+#   --help             Show this help message
+#
+
+use v5.36;
+use Getopt::Long qw(:config no_ignore_case);
+
+sub usage($exit_code = 0) {
+    open my $fh, '<', $0 or die "Cannot read $0: $!\n";
+    while (<$fh>) {
+        last if /^$/;
+        next unless s/^# ?//;
+        print;
+    }
+    exit $exit_code;
+}
+
+my %opt = (version => '', package => 0, help => 0);
+
+GetOptions(
+    'version=s'  => \$opt{version},
+    'package'    => \$opt{package},
+    'help|h'     => \$opt{help},
+) or usage(1);
+
+usage(0) if $opt{help};
+
+my $port_dir = shift @ARGV or do { say STDERR "Error: Port directory required"; usage(1) };
+
+chdir $port_dir or die "Cannot chdir to $port_dir: $!\n";
+
+# Extract configuration via make show=
+my $stem = make_show('MODCABAL_STEM') or die "MODCABAL_STEM not set\n";
+my $current_version = make_show('MODCABAL_VERSION');
+my $executables = make_show('MODCABAL_EXECUTABLES');
+
+# Determine target version
+my $target_version = $opt{version} || get_latest_version($stem);
+die "Error: Could not determine version for $stem\n" unless $target_version;
+
+if ($current_version && $current_version eq $target_version) {
+    say "==> $port_dir: already at $target_version";
+    exit 0;
+}
+
+say "==> $port_dir: $current_version -> $target_version";
+
+# Build cabal-bundler command
+my @bundler_args = ('--openbsd', "$stem-$target_version");
+if ($executables) {
+    my $exec = $executables =~ s/\$\{[^}]+\}//gr;
+    $exec =~ s/^\s+|\s+$//g;
+    if ($exec) {
+        push @bundler_args, '--executable', $_ for split /\s+/, $exec;
+    }
+}
+
+my $bundler_cmd = "cabal-bundler " . join(' ', @bundler_args);
+my $output = `$bundler_cmd 2>&1`;
+die "$bundler_cmd failed:\n$output\n" if $?;
+
+# Parse cabal-bundler output for MODCABAL_MANIFEST and MODCABAL_REVISION
+my (@deps, $revision);
+my $in_manifest = 0;
+for (split /\n/, $output) {
+    if (/^MODCABAL_REVISION\s*=\s*(\d+)/) {
+        $revision = $1;
+    } elsif (/^MODCABAL_MANIFEST\s*=\s*(.*)/) {
+        $in_manifest = 1;
+        push @deps, extract_deps($1);
+    } elsif ($in_manifest && /^\s+(.*)/) {
+        push @deps, extract_deps($1);
+        $in_manifest = 0 unless /\\$/;
+    } else {
+        $in_manifest = 0;
+    }
+}
+
+if (@deps) {
+    open my $fh, '>', 'cabal.inc' or die "Cannot write cabal.inc: $!\n";
+    say $fh "MODCABAL_MANIFEST\t= \\";
+    while (@deps >= 3) {
+        my ($pkg, $ver, $rev) = splice(@deps, 0, 3);
+        if (@deps) {
+            say $fh "\t$pkg\t$ver\t$rev\t\\";
+        } else {
+            say $fh "\t$pkg\t$ver\t$rev";
+        }
+    }
+    close $fh;
+}
+
+# Update Makefile: set MODCABAL_VERSION, MODCABAL_REVISION, remove REVISION
+open my $in, '<', 'Makefile' or die "Cannot read Makefile: $!\n";
+my @lines = <$in>;
+close $in;
+
+my $found_version = 0;
+my $found_revision = 0;
+for (@lines) {
+    if (/^MODCABAL_VERSION\s*=/) {
+        $_ = "MODCABAL_VERSION =\t$target_version\n";
+        $found_version = 1;
+    } elsif (/^MODCABAL_REVISION\s*=/) {
+        if (defined $revision) {
+            $_ = "MODCABAL_REVISION =\t$revision\n";
+        } else {
+            $_ = '';  # Remove if no revision in new version
+        }
+        $found_revision = 1;
+    } elsif (/^REVISION\s*=/) {
+        $_ = '';  # Remove REVISION on update
+    }
+}
+
+# Add MODCABAL_REVISION after MODCABAL_VERSION if needed
+if (defined $revision && !$found_revision) {
+    for my $i (0..$#lines) {
+        if ($lines[$i] =~ /^MODCABAL_VERSION\s*=/) {
+            splice @lines, $i+1, 0, "MODCABAL_REVISION =\t$revision\n";
+            last;
+        }
+    }
+}
+
+open my $out, '>', 'Makefile' or die "Cannot write Makefile: $!\n";
+print $out grep { $_ ne '' } @lines;
+close $out;
+
+# Run make makesum
+system('make', 'makesum') == 0 or die "make makesum failed\n";
+
+# Run make package
+if ($opt{package}) {
+    system('make', 'package') == 0 or warn "make package failed\n";
+}
+
+#
+# Helpers
+#
+
+sub make_show($var) {
+    my $val = `make show=$var`;
+    die "make show=$var failed\n" if $?;
+    chomp $val;
+    return $val eq '' ? undef : $val;
+}
+
+sub get_latest_version($package) {
+    my $output = `cabal list --simple-output '^$package\$' 2>/dev/null`;
+    return unless $? == 0 && $output;
+
+    my @lines = split /\n/, $output;
+    return $1 if @lines && $lines[-1] =~ /^\Q$package\E\s+(\S+)$/i;
+    return;
+}
+
+sub extract_deps($line) {
+    $line =~ s/\s*\\$//;
+    return split /\s+/, $line;
+}
-- 
2.51.2

Reply via email to