do not edit — generated by btf.
git.druid.rocksindexdruid520mpsrc/mplib/portformat.pm

src/mplib/portformat.pm


use v5.16;
package mplib::portformat;
use strict;
use warnings FATAL => 'all';
use Exporter 'import';
use mplib::config qw($CFG first_repo_file);
use mplib::util qw(fail note pconf_lookup delta_or_literal);
 
our @EXPORT_OK = qw(read_port resolve_port write_phase_scripts parse_pkg_use enabled_use apply_use_patches resolve_slot USE_NONE);
 
# persisted as USE=%s when mp.use toggles OFF the last still-enabled flag
# of a port that has at least one "+flag" default-on -- an ordinary empty
# string can't represent that state at all: mplib::config::set_pkgconf
# (like every other per-package config key) treats an empty value as "no
# override configured", which for a port with no defaults is harmless
# (empty override == no override == nothing enabled, same result either
# way) but for a defaults-having port would silently make enabled_use's
# own fallback chain skip straight past the "really, truly nothing"
# override and re-derive the DEFAULT set instead -- exactly backwards
# from what "mp use pkg -lastflag" just asked for. not a real flag name
# (parse_pkg_use never produces one shaped like this), so it can never
# collide with anything a port actually declares.
use constant USE_NONE => '-none-';
 
# low-level parser for one port file (or one class file, same grammar):
# unindented key="value" lines are metadata; an unindented "phase:" line
# opens a phase whose indented lines are appended verbatim (one leading
# whitespace char stripped) as that phase's raw shell body, until the next
# unindented line. returns (\%meta, \%phases), or (undef, undef) if unreadable.
sub read_port {
	my ($path) = @_;
	my (%meta, %phases);
	my $fh;
	my $phase;
	my $heredoc_term;
 
	return (undef, undef) unless open($fh, '<', $path);
	while(my $line = <$fh>)
	{
		chomp $line;
		# a shell heredoc's content is conventionally unindented (flush
		# left), which without this check gets misread by the plain
		# indentation rule below as either new global metadata (if it
		# happens to look like key=value) or a line closing the phase --
		# either way silently truncating the rest of the phase body with
		# no diagnostic. best-effort (a regex, not a real shell parser):
		# recognizes a single "<<[-]['"]?WORD['"]?" opener per body line
		# and keeps appending verbatim, unindented and unreinterpreted,
		# until the exact terminator line.
		if(defined $heredoc_term)
		{
			$phases{$phase} .= "$line\n";
			undef $heredoc_term if $line =~ /^\Q$heredoc_term\E\s*$/;
			next;
		}
		if($line =~ /^[ \t]/)
		{
			next unless defined $phase;
			(my $body = $line) =~ s/^[ \t]//;
			$phases{$phase} .= "$body\n";
			$heredoc_term = $2 if $body =~ /<<-?\s*(['"]?)(\w+)\1/;
			next;
		}
		next if $line !~ /\S/ || $line =~ /^\s*#/;
		if($line =~ /^([a-z][a-z0-9_]*):\s*(?:#.*)?$/)
		{
			$phase = $1;
			$phases{$phase} = '' unless exists $phases{$phase};
			next;
		}
		if($line =~ /^([A-Za-z0-9_]+)=(.*)$/)
		{
			my ($k, $v) = ($1, $2);
			$v =~ s/^"(.*)"$/$1/;
			$v =~ s/^'(.*)'$/$1/;
			$meta{$k} = $v;
			$phase = undef;
		}
	}
	close($fh);
	return (\%meta, \%phases);
}
 
sub read_lines_raw {
	my ($path) = @_;
	my $fh;
	local $/;
 
	return undef unless open($fh, '<', $path);
	my $text = <$fh>;
	close($fh);
	return $text;
}
 
# resolve one port: its own pkg.conf, merged with any inherit="class ..."
# classes (port's own phases always win; later class wins among classes),
# falling back to the legacy add.sh/del.sh pair as the whole "install"/
# "remove" phase body when the port declares no phases of its own at all,
# so an unmigrated port behaves exactly as it always has.
sub resolve_port {
	my ($portdir, $verover) = @_;
	my ($meta, $phases) = read_port("$portdir/pkg.conf");
	return undef unless $meta;
 
	my %merged;
	# list-like fields get UNIONED across every class plus the port's own
	# declaration (classes first, in inherit= order, port's own appended
	# last) -- previously only pkg_deps got this treatment, so a class
	# declaring e.g. pkg_bdepend (a very natural thing to share across a
	# whole family of ports via inherit=) was silently completely inert.
	# scalar fields instead take the port's own value if it declared one
	# at all, else the LAST class in inherit= order that declared one
	# (matching the "later class wins among classes" rule the phase
	# merge below already uses) -- a class contributes only a fallback
	# default, never overriding the port's own explicit choice.
	my @LISTFIELDS = qw(pkg_deps pkg_bdepend pkg_rdepend pkg_deps_soft pkg_tags pkg_use);
	my @SCALARFIELDS = qw(pkg_pref pkg_slot pkg_slot_use pkg_keywords pkg_fetch pkg_license pkg_manifest_cleanup);
	my %classlist;
	my %classscalar;
	for my $cls (split ' ', $meta->{inherit} || '')
	{
		# searched across every configured repo in priority order (same
		# as a port/profile/sysroot-overlay lookup) -- a class can ship
		# from any repo, not only the highest-priority one.
		my $cpath = first_repo_file($CFG->{REPOS}, "classes/$cls.mp");
		my ($cmeta, $cphases) = defined $cpath ? read_port($cpath) : (undef, undef);
		unless($cphases)
		{
			note("inherit=\"$cls\" not found (searched: "
			    . join(', ', map { "$_->{dir}/classes/$cls.mp" } @{$CFG->{REPOS}}) . ")");
			next;
		}
		$merged{$_} = $cphases->{$_} for keys %$cphases;
		for my $k (@LISTFIELDS)
		{
			push @{$classlist{$k}}, $cmeta->{$k} if defined $cmeta->{$k} && $cmeta->{$k} ne '';
		}
		for my $k (@SCALARFIELDS)
		{
			$classscalar{$k} = $cmeta->{$k} if defined $cmeta->{$k} && $cmeta->{$k} ne '';
		}
	}
	$merged{$_} = $phases->{$_} for keys %$phases;
	for my $k (@LISTFIELDS)
	{
		next unless $classlist{$k};
		(my $joined = join(' ', @{$classlist{$k}}, defined $meta->{$k} ? $meta->{$k} : '')) =~ s/^\s+|\s+$//g;
		$joined =~ s/\s+/ /g;
		$meta->{$k} = $joined;
	}
	for my $k (@SCALARFIELDS)
	{
		$meta->{$k} = $classscalar{$k} if defined $classscalar{$k} && !(defined $meta->{$k} && $meta->{$k} ne '');
	}
 
	# legacy ports (no phase: blocks anywhere in the port or its classes)
	# fall back to add.sh/del.sh verbatim. those scripts already do their
	# own complete cd-into/out-of the source dir for the one shell
	# invocation they were written for, so they get no auto srcname cd
	# (see write_phase_scripts) unlike genuine new-format phases below.
	my $legacy = 0;
	unless(%merged)
	{
		my $add = read_lines_raw("$portdir/add.sh");
		my $del = read_lines_raw("$portdir/del.sh");
		$merged{install} = $add if defined $add;
		$merged{remove}  = $del if defined $del;
		$legacy = 1;
	}
 
	for my $k (qw(pkg_deps pkg_tags pkg_conflicts pkg_use pkg_slot pkg_bdepend pkg_rdepend
	    pkg_keywords pkg_fetch pkg_deps_soft pkg_license))
	{
		$meta->{$k} = '' unless defined $meta->{$k};
	}
	$meta->{pkg_pref} = 50 unless defined $meta->{pkg_pref} && $meta->{pkg_pref} ne '';
	$meta->{_legacy} = $legacy;
 
	# a caller pinning to a specific version (a "=X" dep/CLI constraint, or
	# HOLD_VERSION) overrides the port's own declared pkg_ver, both for what
	# gets fetched below and what mp records as installed.
	$meta->{pkg_ver} = $verover if defined $verover && $verover ne '';
 
	# a declarative pkg_fetch synthesizes a default fetch phase (git clone,
	# or tar fetch+extract, landing in a dir named after the port) unless
	# the port or one of its classes already wrote an explicit one.
	if(!exists $merged{fetch} && $meta->{pkg_fetch} ne '')
	{
		my $name = $meta->{pkg_name};
		if($meta->{pkg_fetch} =~ /^git:([^@]+)(?:@(.+))?$/)
		{
			my ($url, $reftmpl) = ($1, $2);
			$merged{fetch} = "git clone $url $name\n";
			if(defined $reftmpl)
			{
				(my $ref = $reftmpl) =~ s/<VER_>/$meta->{pkg_ver}/g;
				$merged{fetch} .= "cd $name\ngit checkout $ref\ncd ..\n";
			}
		}
		elsif($meta->{pkg_fetch} =~ /^tar:([^,]+),(.+)$/)
		{
			my ($url, $extractdir) = ($1, $2);
			my ($archive) = $url =~ m{([^/]+)$};
			$merged{fetch} = "curl -LO $url\ntar xf $archive\nmv $extractdir $name\nrm -f $archive\n";
		}
	}
	# a patches/ dir next to pkg.conf synthesizes a default patch phase
	# (patch -p1 < each patches/*.patch, sorted) unless the port or one of
	# its classes already wrote an explicit one. new-format ports only:
	# every other phase gets an automatic "cd $srcname" (see
	# write_phase_scripts) that legacy ports deliberately opt out of since
	# their one add.sh/del.sh script already does its own complete cd, so
	# a synthesized patch phase would run in the wrong directory there.
	if(!$legacy && !exists $merged{patch} && -d "$portdir/patches")
	{
		# mpx stage copies the whole portdir (patches/ included) into the
		# stage dir, a sibling of the fetched source dir every other
		# phase auto-cds into, hence the "../patches/" relative path.
		my @patches = sort map { s{.*/}{}; $_ } glob("$portdir/patches/*.patch");
		if(@patches)
		{
			$merged{patch} = join('', map { "patch -p1 < ../patches/$_\n" } @patches);
			# the raw ordered list (not just the flattened shell text) so
			# a later optional patches.conf (see mplib::resolve::
			# apply_patch_manifest) can filter/reorder by relative patch
			# path and regenerate the phase from the result.
			$meta->{_patchlist} = [ @patches ];
		}
	}
	$meta->{_phases} = \%merged;
	$meta->{_portdir} = $portdir;
	return $meta;
}
 
# use-flag-conditional patches, checked in addition to (and applied after)
# the unconditional patches/*.patch handled above: patches/use-<flag>/*.patch
# applies only when <flag> is enabled for $canon, patches/nouse-<flag>/*.patch
# only when it's disabled (including never declared). same "../patches/..."
# relative path as the unconditional case (mpx stage copies the whole
# portdir, patches/ included, into a sibling of the fetched source dir).
# new-format ports only, same as the unconditional case -- and $canon must
# be known (it's derived from $pc's own pkg_name/pkg_slot, so this can only
# run after resolve_port returns, not from inside it).
sub apply_use_patches {
	my ($pc, $canon) = @_;
	return if $pc->{_legacy};
	my $portdir = $pc->{_portdir};
	return unless defined $portdir;
 
	my $enabled = enabled_use($canon, parse_pkg_use($pc->{pkg_use}));
	my @extra;
	for my $dir (sort glob("$portdir/patches/use-*"))
	{
		next unless -d $dir;
		my ($flag) = $dir =~ m{/use-([^/]+)$};
		next unless defined $flag && $enabled->{$flag};
		push @extra, sort map { s{.*/}{}; "use-$flag/$_" } glob("$dir/*.patch");
	}
	for my $dir (sort glob("$portdir/patches/nouse-*"))
	{
		next unless -d $dir;
		my ($flag) = $dir =~ m{/nouse-([^/]+)$};
		next unless defined $flag && !$enabled->{$flag};
		push @extra, sort map { s{.*/}{}; "nouse-$flag/$_" } glob("$dir/*.patch");
	}
	return unless @extra;
	$pc->{_phases}{patch} = join('', ($pc->{_phases}{patch} || ''),
	    map { "patch -p1 < ../patches/$_\n" } @extra);
	push @{$pc->{_patchlist} ||= []}, @extra;
	return;
}
 
# write each named phase's resolved body to $dir/<phase>.sh (set -e
# prefixed, matching the existing add.sh/del.sh convention) for every
# phase that has non-empty content; returns the list of phase names
# actually written, in the given order. each phase is its own process
# (mpx runsh chdirs to $dir fresh every time), so every phase but fetch
# gets an automatic "cd $srcname" so it lands back in the tree fetch
# checked out, matching the legacy single-script add.sh/del.sh behaviour.
sub write_phase_scripts {
	my ($dir, $phases, $srcname, @order) = @_;
	my @written;
 
	for my $p (@order)
	{
		my $body = $phases->{$p};
		next unless defined $body && $body =~ /\S/;
		my $fh;
		open($fh, '>', "$dir/$p.sh") or fail("cant write $dir/$p.sh: $!");
		my $cdline = ($p ne 'fetch' && $srcname) ? "cd $srcname 2>/dev/null || true\n" : '';
		print $fh "set -e\n\n$cdline$body";
		close($fh);
		push @written, $p;
	}
	return @written;
}
 
# parse a pkg_use string like "ssl:%openssl x11:libxcb,libx11 oss" into a
# map flag => {build=>0|1, default=>0|1, deps=>[...]}; a flag with no deps
# is just a boolean feature bit. "flag:b:dep1,dep2" marks the deps
# build-only. a leading "+" on the flag name itself ("+flag", same
# convention Portage's own IUSE uses) marks it enabled by default when
# nothing (no per-package or global USE=) says otherwise -- see
# enabled_use's own comment for exactly where that fallback applies.
sub parse_pkg_use {
	my ($s) = @_;
	my %use;
 
	for my $tok (split ' ', $s || '')
	{
		my ($f, $d) = split /:/, $tok, 2;
		next unless defined $f && length $f;
		my $default = $f =~ s/^\+//;
		my $build = defined $d && $d =~ s/^b://;
		$use{$f} = { build => ($build ? 1 : 0), default => ($default ? 1 : 0),
		    deps => [ grep { /\S/ } map { split /,/ } (defined $d ? $d : '') ] };
	}
	return \%use;
}
 
# which USE flags are enabled for a package: per-pkg section USE=, else
# the global USE=, else (only when NEITHER is set at all -- a per-package
# or global USE= always wins outright, even an empty one) whatever this
# port's own pkg_use declares "+flag"-default-on. flags the port does not
# declare are simply ignored. $declared, if passed (parse_pkg_use's own
# return value -- most callers already have $pc handy and can pass
# $pc->{pkg_use} through parse_pkg_use, or just skip it: every call site
# that predates default-on flags keeps working identically, since a
# missing $declared just means no defaults get applied, same as before
# this existed).
sub enabled_use {
	my ($cur, $declared) = @_;
	# pconf_lookup (mplib::util): a slot-qualified canon (e.g.
	# "curl:nossl") with no USE= of its own falls back to its bare name's
	# section before the global default -- the bare name is the only key
	# knowable before a pkg_slot_use-composed slot exists (see
	# resolve_slot below), so a single "curl: USE=nossl" section drives
	# both slot selection and everything else (deps/patches/exports) for
	# the resulting slot. a no-op for every unslotted port (base eq cur
	# already).
	my $defaults = $declared
	    ? join(' ', grep { $declared->{$_}{default} } keys %$declared)
	    : '';
	my $perpkg = $cur ? pconf_lookup($cur, 'USE') : undef;
	my $sel;
	if(defined $perpkg)
	{
		# a per-package override is deliberate and SPECIFIC to this one
		# package. a plain bare-name list is the traditional, complete/
		# authoritative form -- exactly this set, nothing else (same as
		# before default-on flags existed at all). but a raw USE= line
		# can ALSO be written the same way "mp use pkg +flag -flag"
		# takes it on the command line -- mp use is just a convenience
		# wrapper that computes and persists this same result, it isn't
		# the only way to express it, so hand-editing mp.conf with +/-
		# syntax is equally valid, applied atop this package's own
		# declared defaults exactly like a first toggle would be.
		$sel = $perpkg eq USE_NONE ? $perpkg : delta_or_literal($perpkg, $defaults, "USE= for '$cur'");
	}
	else
	{
		# global USE= was never written with THIS package's own defaults
		# in mind -- it's one flat, system-wide list meant to flip on a
		# same-named flag on WHATEVER port happens to declare it (the
		# original, pre-existing design), not a statement about every
		# other unrelated flag on every other port. a bare list ADDS to
		# declared defaults rather than replacing them outright, or a
		# global USE= set for one thing (say "ssl", for curl) would
		# silently turn off EVERY "+flag" default on EVERY OTHER package
		# too -- reproduced for real on an install with nothing globally
		# configured except one unrelated USE= line. a +/- token in a
		# global value is a delta over declared defaults the same as a
		# per-package one -- "USE=-ssl" globally turns ssl off even where
		# it's on by default, without needing a per-package override.
		my $global = $CFG->{conf}{USE};
		$sel = !defined $global ? $defaults
		    : (grep { /^[+-]/ } grep { /\S/ } split ' ', $global) ? delta_or_literal($global, $defaults, "global USE=")
		    : join(' ', $defaults, $global);
	}
	$sel = '' if $sel eq USE_NONE;
	my %enabled = map { $_ => 1 } grep { /\S/ } split ' ', $sel;
	return \%enabled;
}
 
# a port's effective SLOT: its static pkg_slot plus, for each flag listed in
# pkg_slot_use (declared order) that's enabled for its bare name (the only
# key knowable before the slot itself exists), a "-flag" suffix. lets two
# installs of the same port differing only in USE flags coexist instead of
# colliding on one slot -- disabled flags (including the entire feature, if
# pkg_slot_use is unset) contribute nothing, so this is a no-op for every
# port that doesn't opt in.
sub resolve_slot {
	my ($pc, $basename) = @_;
	my $slot = $pc->{pkg_slot} || '';
	return $slot unless $pc->{pkg_slot_use};
	my $en = enabled_use($basename, parse_pkg_use($pc->{pkg_use}));
	my @suffix = grep { $en->{$_} } split ' ', $pc->{pkg_slot_use};
	return join('-', grep { $_ ne '' } ($slot, @suffix));
}
 
1;
powered by btf.