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