Klemens Nanni <[email protected]> writes:

> Kirill's suggestion makes sense, I'm just answering the inlined version
> here for convenience.

Much appreciate the detailed review!

> I haven't touched any haskell in ports so far and stuff like this,
> perhaps eventually exposed similar to go-module(5)'s go-* targets,
> will certainly appreciated should I ever have the need to do so.

Sure, though the leverage here is less due to respective ecosystem
sizes.

>> The batch script finds all ports using devel/cabal module and can
>> update
>> them in sequence, with optional git commits per port.
>
> So /usr/ports must be git, not CVS?

I don't use CVS outside of commits to cvs.o.o so I'm not the best person
to add such a mode.

> Run 'ksh -x ...' and $0 is "ksh", use 'function foo {' and it'll be
> "foo".
>
> The take-away here should be to either write a proper usage() with
> print[f]
> or enforce how the script is called.

I took this piece out, it's questionably useful.

>
>> +    exit "${1:-0}"
>> +}
>> +
>> +typeset -i RUN_PACKAGE=0 GIT_COMMIT=0
>
> That's 'integer' in ksh(1), if you like.

Done

> Could be 'geopts chp' for less hand-rolling, see the ksh(1) getopts
> and/or other shell scripts in base.

Cool!

>> +UPDATE_SCRIPT="./devel/cabal/tools/update-cabal-port.pl"
>> +[[ -x "$UPDATE_SCRIPT" ]] || { print -u2 "Update script not found:
>> $UPDATE_SCRIPT"; exit 1; }
>
> Reads like it must be run from PORTSDIR, in which case an up-front test
> that you're really in /usr/ports/ seems appropiate.

I'm not sure what that check would look like. The obvious `cd
/usr/ports` doesn't feel right.

>> +CABAL_PORTS=$(grep -rl '^MODULES.*=.*devel/cabal' */*/Makefile |
>> sed 's|/Makefile$||' | sort)
>
> Unless you want (or need?) what's in your working tree right now,
> you can use proper ports tooling to get such a list, e.g.
>
>       # pkg_add sqlports
>       $ sqlite3 -readonly /usr/local/share/sqlports '
>           select fullpkgpath from modules where value like "%devel/cabal%"
>       '
>       devel/alex
>       devel/cabal-bundler
>       devel/cpphs
>       devel/darcs
>       devel/git-annex
>       devel/happy
>       devel/hasktags
>       devel/shellcheck
>       net/matterhorn
>       productivity/hledger
>       textproc/pandoc
>       x11/xmobar
>       x11/xmonad
>
> If you stick with a pipeline in a subshell, make sure to use 'set -o pipefail'
> to catch failure, otherwise that script goes on when grep fails and does 
> whatever.

Added set -o pipefail. I don't want to require sqlports considering how
stable our set of haskell ports has been.

> Yup, at least --git-commit won't fly with CVS, in which case I'd do
> some
> heads-up check that this can work, otherwise going over all ports makkes
> little sense.
>
> I'm thinking some git-rev-parse(1) --is-* check right in the getopts
> case.
>
> ... and/or some bits in the usage making it clear that CVS won't do.

Added a quick abort if not in git repo and -g is given.

>
>> +    else
>> +            print -u2 "==> FAILED: $port"
>> +            ((++FAILED))
>> +            print -n "Continue? [Y/n] "
>> +            read response
>
> From ksh's read description:
>
>              The first parameter may have a question mark and a string
>              appended to it, in which case the string is used as a
> prompt
>              (printed to standard error before any input is read) if
> the input
>              is a tty(4) (e.g. read nfoo?'number of foos: ').

Done
 
>> +            [[ "$response" == [nN]* ]] && break
>
> [['s left argument needs no quoting.

Done

>From b865b0c973bc8d9e272827a76c85f79ed88c52c1 Mon Sep 17 00:00:00 2001
From: Claude <[email protected]>
Date: Mon, 29 Dec 2025 02:32:21 +0000
Subject: [PATCH] Add cabal-based port update tools

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

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

- Use OpenBSD ports `make show=VAR` instead of parsing Makefiles
- Handle MODCABAL_REVISION from cabal-bundler output
- Remove REVISION variable on port updates

Review and OK kn@
---
 infrastructure/bin/update-all-cabal-ports |  66 +++++++++
 infrastructure/bin/update-cabal-port      | 169 ++++++++++++++++++++++
 2 files changed, 235 insertions(+)
 create mode 100755 infrastructure/bin/update-all-cabal-ports
 create mode 100755 infrastructure/bin/update-cabal-port

diff --git a/infrastructure/bin/update-all-cabal-ports b/infrastructure/bin/update-all-cabal-ports
new file mode 100755
index 00000000000..4ce02c2ebb7
--- /dev/null
+++ b/infrastructure/bin/update-all-cabal-ports
@@ -0,0 +1,66 @@
+#!/bin/ksh
+#
+# Update all cabal ports to latest versions
+#
+# Usage: update-all-cabal-ports [options]
+#
+# Options:
+#   --p		Run 'make package' for each port
+#   --g		Create git commit for each successful update
+
+set -e -o pipefail
+
+integer RUN_PACKAGE=0 GIT_COMMIT=0
+
+while getopts gp name
+do
+	case "$name" in
+		p) RUN_PACKAGE=1 ;;
+		g) git rev-parse --is-inside-work-tree > /dev/null; GIT_COMMIT=1 ;;
+		?) print -u2 "Unknown option: $name, see script for options" ;;
+	esac
+done
+
+UPDATE_SCRIPT="$(dirname $0)/update-cabal-port"
+[[ -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)
+integer TOTAL=$(print "$CABAL_PORTS" | wc -l)
+
+print "Found $TOTAL cabal ports"
+
+integer 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 distinfo 2>/dev/null
+				git add cabal.inc 2>/dev/null || true
+				git commit -m "$port: update to $VERSION"
+			fi
+		fi
+		cd - >/dev/null
+	else
+		print -u2 "==> FAILED: $port"
+		((++FAILED))
+		read response?'Continue? [Y/n] '
+		[[ $response == [nN]* ]] && break
+	fi
+done
+
+print "==> Summary: $SUCCESS updated, $SKIPPED unchanged, $FAILED failed"
+((FAILED)) && exit 1
+exit 0
diff --git a/infrastructure/bin/update-cabal-port b/infrastructure/bin/update-cabal-port
new file mode 100755
index 00000000000..8e5eaab182d
--- /dev/null
+++ b/infrastructure/bin/update-cabal-port
@@ -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.52.0

Reply via email to