From: Greg Steuck Subject: Re: devel/cabal: add port update tools To: Klemens Nanni Cc: ports@openbsd.org, kili@openbsd.org Date: Sun, 11 Jan 2026 15:34:07 -0800 Klemens Nanni 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 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 [options] +# +# Options: +# --version 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