03.01.2026 20:18, Greg Steuck пишет:
> 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
> 
> 
> 0001-devel-cabal-add-port-update-tools.patch

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

Either way, having such helpers in tree generally seems worthwile to me.

They help document the process and lower the bar for others new to haskell.

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.


See inline for nits and issues, but what you have already works,
then OK kn to add as-is and tweak in-tree, whatever works best.

They're just files added without anything using them, so low risk, imho.

> 
> 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.

So /usr/ports must be git, not CVS?

> 
> 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"

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.

> +     exit "${1:-0}"
> +}
> +
> +typeset -i RUN_PACKAGE=0 GIT_COMMIT=0

That's 'integer' in ksh(1), if you like.

> +
> +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

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

> +
> +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.

> +
> +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.

> +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

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 makes
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.

> +     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: ').

> +             [[ "$response" == [nN]* ]] && break

[['s left argument needs no quoting.

> +     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;
> +    }

Now I see where you got this from ;)

My Perl is weak, I'll leave this file to someone else.

> +    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