| git.druid.rocks | index | druid520 | mp | src/ | mplib/ | hooks.pm |
src/mplib/hooks.pm
use v5.16;
package mplib::hooks;
use strict;
use warnings FATAL => 'all';
use Exporter 'import';
use FindBin;
use mplib::config qw($CFG read_conf);
use mplib::util qw(note fail pconf_lookup pconf_section order_manifest delta_or_literal);
use mplib::portformat qw(enabled_use parse_pkg_use);
our @EXPORT_OK = qw(port_env_args run_hook);
# $pkg_use, if passed, is the CURRENT port's own raw pkg_use string (e.g.
# $pc->{pkg_use}) -- lets a "+flag"-default-on USE flag (see
# mplib::portformat::enabled_use) still get its USE_<flag>=1 exported to
# the phase script even with no per-package or global USE= configured at
# all. omitted (every call site that predates default-on flags), nothing
# changes: enabled_use falls back to its pre-existing empty-string
# default exactly as before.
sub port_env_args {
my ($cur, $pkg_use) = @_;
my @args;
# pconf_section (mplib::util): merges the bare name's section under
# the canon-specific one (specific wins key-by-key) -- a
# pkg_slot_use-composed canon (e.g. "combodep:foo") doesn't exist yet
# when a user writes a per-package section, so the bare name is the
# only key they could plausibly have used. shared with
# mplib::resolve::effective_deps' identical DEPS/DEPS+/DEPS- need,
# instead of two copies of the same merge.
my %sec = %{pconf_section($cur)};
# lets mpx look up a "<name>:" section of its own (mpx.conf) for a
# per-package SHELL/TARGET_*/JOBS/ARCH/MICROARCH/SANDBOX override,
# mirroring mp.conf's per-package sections.
push @args, "MP_PKG=$cur" if $cur;
# lets a phase script fire an arbitrary, port-author-chosen hook point
# mid-phase ("$MP_HOOK my-custom-step") -- not just the fixed set of
# pre_/post_<phase> boundaries run_hook is otherwise only ever called
# for -- by exec'ing mp.hook (src/hook.pl), a thin wrapper around the
# exact same run_hook() this file already implements, so a custom
# event name gets the identical hook_<name>=/HOOKS_DIR/<name>/*
# (with the same hooks.conf pipeline) as any built-in phase.
push @args, "MP_HOOK=$FindBin::Bin/mp.hook";
# mp.hook (a SEPARATE process invocation, re-running
# mplib::config::load from scratch) has no other way to learn that
# the phase script calling it is running inside a with_sysroot_scope
# callback -- without this, a custom hook fired via $MP_HOOK from a
# sysroot-scoped phase would silently resolve against the DEFAULT
# root's mp.conf/HOOKS_DIR instead of the active sysroot's.
push @args, "MP_SYSROOT=$CFG->{SYSROOT}" if defined $CFG->{SYSROOT} && $CFG->{SYSROOT} ne '';
# lets mpx (runsh.c's cmd_runsh) read a per-sysroot mpx.conf instead
# of the single global /etc/mpx.conf -- every other WD-derived
# concept (db/manifest/HOOKSDIR) already gets this same isolation,
# but mpx.conf's TARGET_CC/JOBS/MICROARCH/SANDBOX overrides were
# still silently shared by every sysroot and the default root alike.
# $CFG->{WD} is already the swapped, sysroot-specific path by the
# time with_sysroot_scope's callback runs, so this needs no extra
# path construction -- a sysroot with no mpx.conf override of its
# own just falls back to the global file, same as always.
push @args, "MP_MPXCONF=$CFG->{WD}/mpx.conf" if defined $CFG->{SYSROOT} && $CFG->{SYSROOT} ne '';
# a global (unindented) "ENV=" line in mp.conf -- mirrors the per-
# package "ENV=" handling just below, but applies to every port build
# instead of one. pushed BEFORE the per-package section's own args
# (including its own ENV=, if any) so a same-named per-package
# override still wins: runsh.c's generic "KEY=VALUE" trailing-arg
# handling is a plain setenv(..., 1) replace, not an append, so
# whichever entry for a given name comes LAST in @args is the one
# that sticks. added because this system's own static-pie build
# policy (CFLAGS/CXXFLAGS/LDFLAGS/CPATH/CONFIG_SITE/GIT_EXEC_PATH,
# see /etc/mpx.env) had been carried as ad hoc "export ..." lines
# retyped by hand before nearly every "mp update"/"mp reinstall"
# invocation instead of persisted anywhere -- fragile, and easy to
# forget on any one invocation (both hit for real this session).
# /etc/mpx.env (sourced directly by runsh.c's own wrapper shell,
# see its own comment) already covers "the same env for literally
# every build, no per-package awareness needed, no dependency on
# mp.conf parsing at all" -- this is for the complementary case: a
# global default that still needs to compose with mp.conf's own
# per-package sections/PROFILE=/OVERLAYS= precedence chain, the way
# every other mp.conf key already does.
if(defined $CFG->{conf}{ENV} && $CFG->{conf}{ENV} ne '')
{
push @args, grep { /\S/ } split ' ', $CFG->{conf}{ENV};
}
if(%sec)
{
for my $k (sort keys %sec)
{
next if $k =~ /^(?:TAG|TARGET)_/;
next if $k =~ /^(?:DEPS|HOLD|GLOBAL_DEPS|hook_)/;
next if $k eq 'USE';
next if $k eq 'SYSROOT';
if($k eq 'ENV')
{
push @args, grep { /\S/ } split ' ', $sec{$k};
next;
}
push @args, "$k=$sec{$k}";
}
}
# export one USE_<flag>=1 per enabled flag so build scripts can branch,
# plus the whole space-separated USE set in one variable.
my $en = enabled_use($cur, $pkg_use ? parse_pkg_use($pkg_use) : undef);
if(%$en)
{
push @args, "USE=" . join(' ', sort keys %$en);
for my $f (sort keys %$en)
{
push @args, "USE_$f=1";
}
}
return @args;
}
# paludis-style hook group for one phase: every hook runs, in order, and
# any one of them exiting nonzero vetoes the whole operation (fail()).
# four sources, all optional and all usable together:
# hook_<phase>= in mp.conf (global, or inside a "<name>:" section) -- one
# inline shell command, quick and simple.
# HOOKS_DIR/<phase>/* -- any executable file there runs directly, sorted
# by name, letting several independent hooks share a phase
# (e.g. HOOKS_DIR/post_install/10-logger, 20-notify).
# HOOKS_DIR/<phase>/hooks.conf -- optional declarative pipeline over that
# same file group, same section grammar (and the same
# order_manifest engine, mplib::util) as patches/
# patches.conf: "IF_USE=flag !otherflag" (mplib::portformat::
# enabled_use -- $canon's enabled USE flags, or the global
# USE= for a canon-less hook like pre_sync/post_sync) skips
# a hook file entirely, "AFTER=<file> <file>" reorders among
# the files actually running. filename sort order (today's
# only ordering) is still the default/tiebreak with no
# hooks.conf, or for any file it doesn't mention.
# HOOKS_<phase>= in mp.conf (global, or per-package, same fallback as
# hook_<phase>) -- a manual override on top of the
# IF_USE-computed set above, same +/- delta convention
# as USE=/"mp use": a bare list is the complete, explicit
# set of filenames to run (regardless of IF_USE); a
# +name/-name delta is applied atop the IF_USE baseline,
# so "-20-notify" disables one hook file without
# deleting it or touching hooks.conf, "+30-extra-check"
# forces one back on despite its own IF_USE. no dedicated
# command persists this today -- hand-edit mp.conf.
sub run_hook {
my ($phase, $canon) = @_;
my $key = "hook_$phase";
my @runs;
push @runs, ["config: $CFG->{conf}{$key}", $CFG->{SHELL}, '-c', $CFG->{conf}{$key}] if $CFG->{conf}{$key};
if($canon)
{
# bare-name fallback (pconf_lookup, mplib::util): a
# pkg_slot_use-composed canon doesn't exist yet when a hook_<phase>
# is written under a port's bare name, so that's the only key it
# could plausibly be configured under.
my $val = pconf_lookup($canon, $key);
push @runs, ["config[$canon]: $val", $CFG->{SHELL}, '-c', $val] if defined $val;
}
my $dir = "$CFG->{HOOKSDIR}/$phase";
if(opendir(my $dh, $dir))
{
my @cand;
for my $f (sort readdir($dh))
{
next if $f =~ /^\./ || $f eq 'hooks.conf';
my $path = "$dir/$f";
push @cand, $f if -f $path && -x _;
}
closedir($dh);
if(@cand)
{
# no $declared here (run_hook only ever gets a bare $canon, not
# the port's own pkg_use -- fixing that would need this file to
# resolve the port itself, risking a real circular import with
# mplib::resolve, which already imports FROM here): a "+flag"
# default-on USE flag (mplib::portformat::enabled_use) won't
# gate a hook's own IF_USE correctly unless a real per-package
# or global USE= has been set at least once. every OTHER
# default-on consumer (phase-script env export, dep/patch/slot
# resolution) doesn't have this gap.
my $enabled = enabled_use($canon);
my $ifuse_ok = sub {
my ($sec) = @_;
return 1 unless $sec;
for my $tok (split ' ', $sec->{IF_USE} || '')
{
my $wantit = ($tok =~ s/^!//) ? 0 : 1;
return 0 if (($enabled->{$tok} ? 1 : 0) != $wantit);
}
return 1;
};
# HOOKS_<phase>: read straight from @cand + hooks.conf's own
# sections -- a second, small read (order_manifest reads the
# same file again below for its own AFTER-ordering purposes),
# same precedent as apply_patch_manifest's DEPS+= pass
# (mplib::resolve): hooks.conf is small and this runs once per
# phase, not per file, and avoids changing order_manifest's
# shared $want contract for its other caller.
my $hooksconf = "$dir/hooks.conf";
my $hsec = {};
(undef, $hsec) = read_conf($hooksconf) if -f $hooksconf;
my $baseline = join(' ', grep { $ifuse_ok->($hsec->{$_}) } @cand);
my $hkey = "HOOKS_$phase";
my $hraw = $canon ? pconf_lookup($canon, $hkey) : undef;
$hraw = $CFG->{conf}{$hkey} unless defined $hraw;
my %active = map { $_ => 1 } grep { /\S/ } split ' ', (defined $hraw
? delta_or_literal($hraw, $baseline, $canon ? "$hkey for '$canon'" : "global $hkey")
: $baseline);
my @filtered = grep { $active{$_} } @cand;
push @runs, ["$dir/$_", "$dir/$_"] for order_manifest(\@filtered, $hooksconf, sub { 1 });
}
}
return unless @runs;
my %saved;
my $savedpkg = $ENV{MP_PKG};
$ENV{MP_PKG} = $canon if $canon;
if($canon)
{
for my $arg (port_env_args($canon))
{
my ($k, $v) = split /=/, $arg, 2;
$saved{$k} = $ENV{$k};
$ENV{$k} = $v;
}
}
for my $r (@runs)
{
my ($desc, @exec) = @$r;
note("hook $phase" . (defined $canon ? "[$canon]" : '') . ": $desc");
system(@exec) == 0 or fail("hook failed ($phase): $desc");
}
for my $k (keys %saved)
{
defined $saved{$k} ? ($ENV{$k} = $saved{$k}) : delete $ENV{$k};
}
defined $savedpkg ? ($ENV{MP_PKG} = $savedpkg) : delete $ENV{MP_PKG};
return;
}
1;