## ## openpkg-build.pl -- create build scripts from package index ## ## Copyright (c) 2000-2003 Cable & Wireless Deutschland GmbH ## Copyright (c) 2000-2003 The OpenPKG Project ## Copyright (c) 2000-2003 Ralf S. Engelschall ## ## Permission to use, copy, modify, and distribute this software for ## any purpose with or without fee is hereby granted, provided that ## the above copyright notice and this permission notice appear in all ## copies. ## ## THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED ## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR ## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF ## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT ## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ## SUCH DAMAGE. ## require 5; $|=1; # autoflush use strict; use vars qw/$opt_R $opt_r $opt_f $opt_u $opt_U $opt_a $opt_A $opt_z $opt_Z $opt_P $opt_N $opt_E $opt_i $opt_D $opt_p $opt_q $opt_s $opt_S $opt_X/; my $getopts = 'R:r:f:uUaAzZP:N:E:iD:p:qsSX'; getopts($getopts); ########################################################################## sub getopts ($) { my($opts) = @_; my(%optf) = map { /(\w)/; $1 => $_ } $opts =~ /(\w:|\w)/g; my(%opts,@argv,$optarg); foreach (@ARGV) { if (@argv) { push @argv, $_; } elsif (defined $optarg) { if (exists $opts{$optarg}) { $opts{$optarg} .= " $_"; } else { $opts{$optarg} = $_; } $optarg = undef; } elsif (!/^[-]/) { push @argv, $_; } else { while (/^\-(\w)(.*)/) { if (exists $optf{$1}) { if (length($optf{$1}) > 1) { if ($2 ne '') { if (exists $opts{$1}) { $opts{$1} .= " $2"; } else { $opts{$1} = $2; } } else { $optarg = $1; } last; } else { $opts{$1} = 1; } } else { warn "warning: unknown option $_\n"; } $_ = "-$2"; } } } if (defined $optarg) { warn "warning: option $optarg requires an argument\n"; } foreach (keys %opts) { eval '$opt_'.$_.' = "'.quotemeta($opts{$_}).'";'; } @ARGV = @argv; } my(%env) = ( '' => { } ); if (open(FH, "< $ENV{'HOME'}/.openpkg/build")) { my($env) = $env{''}; my($go) = $getopts; $go =~ s/[^a-zA-Z]//g; while () { if (/^\s*\[([^\]]*)\]/) { $env{$1} = { } unless $env{$1}; $env = $env{$1}; } elsif (my($opt,$val) = /^\-([$go])\s*(.*?)\s*$/) { $val = 1 unless defined $val; $env->{$opt} = $val; } } close(FH); } die "openpkg:build:USAGE: $0 [-R rpm] [-r repository] [-f index.rdf] [-uUzZiqS] [-P priv-cmd] [-N non-priv-cmd] [-p platform] [-Dwith ...] [-Ename ...] ( [-aA] | patternlist )\n" unless $#ARGV >= 0 || ($#ARGV == -1 && ($opt_a || $opt_A)); ########################################################################## # # evaluate a condition attribute from an option set # sub conditional ($$) { my($cond,$with) = @_; my(@s,$res); return 1 if $cond eq ''; foreach (split(/\s+/,$cond)) { if ($_ eq '+') { die "FATAL: stack underflow in: $cond\n" if scalar(@s)<2; my($a) = pop @s; my($b) = pop @s; push @s, $a && $b; } elsif ($_ eq '|') { die "FATAL: stack underflow in: $cond\n" if scalar(@s)<2; my($a) = pop @s; my($b) = pop @s; push @s, $a || $b; } elsif ($_ eq '!') { die "FATAL: stack underflow in: $cond\n" if scalar(@s)<1; my($a) = pop @s; push @s, !$a; } else { push @s, ($with->{$_} eq 'yes') ? 1 : 0; } } die "FATAL: stack underflow in: $cond\n" if scalar(@s)<1; $res = pop @s; die "FATAL: stack not empty in: $cond\n" if scalar(@s)>0; return $res; } ########################################################################## my($RPM,$RPM_PRIV,$RPM_NPRIV,$CURL,$PROG); $RPM = $opt_R || $env{''}->{opt}->{'R'} || '@l_prefix@/bin/rpm'; $RPM = (`which $RPM` =~ m{^(/.*)})[0]; die "FATAL: cannot locate rpm in path\n" unless $RPM =~ m{^/}; # augment command line parameters foreach my $env (sort { $a cmp $b } grep { $RPM =~ /^\Q$_\E/ } keys %env) { while (my($opt,$val) = each %{$env{$env}}) { eval "\$opt_$opt = '$val' unless defined \$opt_$opt;"; } } $RPM_PRIV = ($opt_P ? $opt_P." ".$RPM : $RPM); $RPM_NPRIV = ($opt_N ? $opt_N." ".$RPM : $RPM); $CURL = $RPM; $CURL =~ s/\/bin\/rpm$/\/lib\/openpkg\/curl/ or die "FATAL: cannot deduce curl path from $RPM\n"; ($PROG) = $0 =~ /(?:.*\/)?(.*)/; sub version_cmp ($$) { my($a,$b) = @_; my(@a,@b,$c); my($ax,$bx); @a = split(/\./, $a); @b = split(/\./, $b); while (@a && @b) { if ($a[0] =~ /^\d+$/ && $b[0] =~ /^\d+$/) { $c = $a[0] <=> $b[0]; } elsif ((($a,$ax) = $a[0] =~ /^(\d+)(.*)$/) && (($b,$bx) = $b[0] =~ /^(\d+)(.*)$/)) { $c = $a <=> $b; $c = $ax cmp $bx unless $c; } else { $c = $a[0] cmp $b[0]; } return $c if $c; shift @a; shift @b; } $c = scalar(@a) <=> scalar(@b); return $c; } sub release_cmp ($$) { my($a,$b) = @_; return $a cmp $b; } sub vcmp ($$) { my($a,$b) = @_; return 0 if $a eq $b; my($av,$ar) = $a =~ /^(.*?)(?:\-([\d\.]+))?$/; my($bv,$br) = $b =~ /^(.*?)(?:\-([\d\.]+))?$/; my($c); if ((defined $ar) && (defined $br)) { $c = release_cmp($ar,$br); return $c if $c; } if ((defined $av) && (defined $bv)) { $c = version_cmp($av,$bv); return $c if $c; } return 0; } sub vs ($) { my($t) = @_; return "$t->{version}-$t->{release}"; } sub vsn ($) { my($t) = @_; return "$t->{name}-$t->{version}-$t->{release}"; } ########################################################################## sub get_config () { my($c,@q,@g); $c = `$RPM_NPRIV --eval '%{_rpmdir} %{_rpmfilename} %{_target_os} %{_target_cpu} %{_target_platform} %{_prefix}'`; chomp($c); (@q) = split(/\s+/,$c); $q[1] =~ s/%{OS}/$q[2]/; $q[1] =~ s/%{ARCH}/$q[3]/; $c = `$RPM_NPRIV --showrc`; @g = $c =~ /\%\{l_tool_locate\s+([^\s\}]+)/g; return { rpmdir => $q[0], template => $q[1], platform => $q[4], prefix => $q[5], optreg => '(?:'.join('|', map { "\Qwith_$_\E" } @g).')' }; } sub get_release () { my($rel,$url); ($rel) =`$RPM_NPRIV -qi openpkg` =~ /Version:\s*(\S+)/m; if ($rel =~ /^\d+$/) { print "# $PROG current($rel)\n"; print "# using '$RPM_NPRIV' (build) and '$RPM_PRIV' (install)\n"; $url = "ftp://ftp.openpkg.org/current/"; } elsif ($rel =~ /^(\d+\.\d+)/) { $rel = $1; print "# $PROG release($rel)\n"; $url = "ftp://ftp.openpkg.org/release/$rel/"; } else { die "FATAL: don't know how to handle this release\n"; } return $url; } sub parse_provides ($) { my($s) = @_; my($nam,$val,$pre,$with,$pxy,$ver,$rel); ($nam,$val) = $s =~ /^(\S+)\s*(?:=\s*(\S*?))?$/; # # build options are encoded as a Requirement # :: = # # since the value is interpreted as a version number # you can only do equality tests # if (($pre,$with) = $nam =~ /^(\S+?)::(\S*)$/) { $val =~ s/(?:\%([0-9a-fA-F][0-9a-fA-F]))/chr(hex($1))/eg; ($ver,$rel,$pxy) = ($val, '', undef); } else { ($ver,$rel,$pxy) = $val =~ /^([^\s\-]+)-([^\s\+]+)(\+PROXY)?$/; } return { name => $nam, # the full name of the resource version => $ver, # the version (or value) release => $rel, # and release number proxy => $pxy, # wether the resource is a PROXY resource prefix => $pre, # the packagename (if resource is an option) with => $with # the buildoption (if resource is an option) }; } sub parse_depends ($) { my($dep) = @_; my($name, $op, $val); if (ref $dep) { # # dependency from new index stored as a node # # content of the node is the name # certain attributes denote the comparison operator # the value of such an attribute is the comparison operand # # the operator (and operand) are optional and there can # only be one # $name = $dep->{content}; $op = undef; $op = 'equ' if exists $dep->{equ}; $op = 'geq' if exists $dep->{geq}; $op = 'leq' if exists $dep->{leq}; $op = 'gt' if exists $dep->{gt}; $op = 'lt' if exists $dep->{lt}; if (defined $op) { $val = $dep->{$op}; } } elsif ($dep =~ /\S/) { # # dependency from old index stored as text string # # "name operator operand" # or # "name" # ($name,$op,$val) = $dep =~ /(\S+)\s*(?:(\S+)\s*(\S+))?\s*$/; if (defined $op) { $op = { '==' => 'equ', '=' => 'equ', '>=' => 'geq', '=>' => 'geq', '<=' => 'leq', '=<' => 'leq', '>' => 'gt', '<' => 'lt' }->{$op}; unless (defined $op) { print "# don't know how to handle dependency: $dep\n"; return; } } } return { name => $name, op => $op, val => $val }; } sub depends2provides ($) { my($dep) = @_; my($ver,$rel,$pxy,$pre,$with); ($ver,$rel,$pxy) = $dep->{val} =~ /^([^\s\-]+)-([^\s\+]+)(\+PROXY)?$/; ($pre,$with) = $dep->{name} =~ /^(\S+?)::(\S*)$/; return { name => $dep->{name}, version => (defined $ver ? $ver : $dep->{val}), release => $rel, proxy => $pxy, prefix => $pre, with => $with } } # # convert parser output to dependency records # sub depend_list ($) { my($deps) = @_; foreach (@$deps) { $_ = parse_depends($_); } return $deps; } # # compute list of package names from dependency list # sub depends2pkglist ($) { my($deps) = @_; return map { $_->{name} } @$deps; } # # retrieve the local installed base # # for packages that provide option resources (packagename::buildoption) # the options are parsed into the OPTIONS hash # # other packages will query options on demand # sub get_installed () { my(%map); my(@l) = `$RPM_NPRIV --provides -qa`; my($p); my($nam,$val,%options); foreach (@l) { $p = parse_provides($_); if (defined $p->{with}) { $options{$p->{prefix}}->{$p->{with}} = $p->{version} } push @{$map{$p->{name}}->{"$p->{version}-$p->{release}"}}, { name => $p->{name}, version => (defined $p->{version} ? $p->{version} : '*'), release => (defined $p->{release} ? $p->{release} : '*'), PROXY => $p->{proxy} }; } # # options are provided for a package # apply them to all instances of the package # foreach $nam (keys %options) { foreach $val (keys %{$map{$nam}}) { foreach (@{$map{$nam}->{$val}}) { $_->{OPTIONS} = $options{$nam}; } } } return \%map; } # # compute reverse dependency map # # sub get_revdep ($) { my($env) = @_; my($i) = $env->{'installed'}; my($r) = $env->{'repository'}; my($pkg, %dep, %dlist, %rev); my(@vers,$t); print "# computing reverse dependencies\n"; foreach $pkg (keys %$i) { unless ($r->{$pkg}) { print "# ATTENTION: $pkg has no upgrade path\n"; next; } # # get list of package versions from repository # @vers = get_versions($r->{$pkg}, sub { 1; }); # # get forward dependencies from repository packages # # dep{a}{b} is true if b depends directly on a # dlist{a} is list of packages that depend on a # foreach (@vers) { foreach $t (@{$r->{$pkg}->{$_}}) { next unless $i->{$t->{name}}; next unless $t->{depends}; foreach (depends2pkglist($t->{depends})) { $dep{$_}{$t->{name}} = 1; push @{$dlist{$_}}, $t; } } } } # # sort reverse dependencies # foreach $pkg (keys %dep) { $rev{$pkg} = [ sort { $dep{$a->{name}}{$b->{name}} || -$dep{$b->{name}}{$a->{name}} || $a->{name} cmp $b->{name} } @{$dlist{$pkg}} ]; } return \%rev; } # # parse option from rpm output # sub parse_options ($) { my($l) = @_; $l = join("\n", @$l) if ref $l; return if ($l !~ m/(--define|\%option\s+)/s); my $with = {}; $l =~ s/--define\s*'(\S+)\s+(\S+?)'/$with->{$1} = $2, ''/sge; # before openpkg-20021230 $l =~ s/\%option\s+(\S+)\s+(\S+)/$with->{$1} = $2, ''/sge; # after openpkg-20021230 return $with; } # # copy options from new to old # where option already exists in old or option key # matches regular expression # sub override_options ($$$) { my($old, $new, $reg) = @_; foreach my $k (keys %$new) { $old->{$k} = $new->{$k} if exists $old->{$k} || $k =~ /^$reg$/; } } # # pull in OPTIONS for a package or an RPM file # sub get_with ($;$) { my($t,$fn) = @_; my(@l,%with); unless ($t->{OPTIONS}) { if (defined $fn) { @l = `$RPM_NPRIV -qi -p $fn`; } else { @l = `$RPM_NPRIV -qi $t->{name}`; } $t->{OPTIONS} = parse_options(\@l); } return $t->{OPTIONS}; } # # compute absolute paths # # (url, fn) point to a base document # the location is the file path fn if fn is # defined, otherwise it is url. # # augment the pointer with suburl # # suburl can be an absolute url # then the new pointer is (suburl, undef) # # suburl can be a absolute file path # then the new pointer is (suburl, suburl) # # suburl can be a relative path # then it augments url or fn accordingly # sub relurl ($$$) { my($url,$fn,$suburl) = @_; my($subfn); if ($suburl =~ /^\w+:\/\//) { # NOP } elsif ($suburl =~ /^\//) { $subfn = $suburl; } else { if (defined $fn) { $subfn = $fn; $subfn =~ s/\/[^\/]*$//; $subfn .= '/' unless $subfn =~ /\/$/; $subfn .= $suburl; $suburl = $subfn; } else { $subfn = $url; $subfn =~ s/\/[^\/]*$//; $subfn .= '/' unless $subfn =~ /\/$/; $suburl = "$subfn$suburl"; $subfn = undef; } } return ($suburl, $subfn); } # # return node value from XML parser # sub xel($) { my($a) = @_; my($l) = $a->[0]; return '' if ref $l; return $l; } # # grep XML Bag against condition # return as flat list # sub with_list ($$) { my($bags,$with) = @_; my($bag,$li,$el); my(@out); foreach $bag (@$bags) { next unless conditional($bag->{'cond'}, $with); foreach $li (@{$bag->{'rdf:bag'}}) { $el = $li->{'resource'} || $li->{'rdf:li'}; push @out, @$el; } } return \@out; } sub simple_text_parser ($$$$) { my($fh,$url,$with,$map) = @_; my(@include); my($section); my($name,$version); my($href,$release,$desc); my(@prereq,@bprereq); my(@provides,@conflicts,@source,@nosource); my(%options); my($platform,$prefix); my($rec); my($tag,$cond,$attrname,$attrval,$body); my($useit); print "# using simple text parser\n"; while (<$fh>) { s/>/>/g; s/</ (.*?) (?:<\/\1>)? $ /mx; $useit = conditional($cond,$with); if ($tag eq 'Description') { $section = 'description'; } elsif ($tag eq '/Description') { $section = undef; } elsif ($section eq 'description') { $desc .= $_; } elsif ($tag eq 'PreReq') { $section = 'prereq' if $useit; } elsif ($tag eq '/PreReq') { $section = undef; } elsif ($tag eq 'BuildPreReq') { $section = 'bprereq' if $useit; } elsif ($tag eq '/BuildPreReq') { $section = undef; } elsif ($tag eq 'Provides') { $section = 'provides' if $useit; } elsif ($tag eq '/Provides') { $section = undef; } elsif ($tag eq 'Conflicts') { $section = 'conflicts' if $useit; } elsif ($tag eq '/Conflicts') { $section = undef; } elsif ($tag eq 'NoSource') { $section = 'nosource' if $useit; } elsif ($tag eq '/NoSource') { $section = undef; } elsif ($tag eq 'Source') { $section = 'source' if $useit; } elsif ($tag eq '/Source') { $section = undef; } elsif ($tag eq 'Name') { $name = $body; } elsif ($tag eq 'Version') { $version = $body; } elsif ($tag eq 'Release') { $release = $body; } elsif ($tag eq 'Platform') { $platform = $body; } elsif ($tag eq 'Prefixes') { $prefix = $body; } elsif ($tag eq 'rdf:li' || $tag eq 'resource') { if ($section eq 'prereq') { push(@prereq, $body); } elsif ($section eq 'bprereq') { push(@bprereq, $body); } elsif ($section eq 'provides') { push(@provides, $body); } elsif ($section eq 'conflicts') { push(@conflicts, $body); } elsif ($section eq 'source') { push(@source, $body); } elsif ($section eq 'nosource') { push(@nosource, $body); } } elsif ($tag eq '/rdf:Description') { if (defined $href && defined $name && defined $version && defined $release) { @provides = map { depends2provides(parse_depends($_)) } @provides; %options = map { ( $_->{with} => $_->{version} ) } grep { defined $_->{with} } @provides; unless (grep($_->{name} eq $name, @provides)) { push(@provides, { name => $name, version => $version, release => $release }); } $rec = { href => (relurl($url, undef, $href))[0], name => $name, version => $version, release => $release, depends => depend_list([ @bprereq ]), keeps => depend_list([ @prereq ]), conflicts => [ @conflicts ], source => [ @source ], nosource => [ @nosource ], desc => $desc, platform => $platform, prefix => $prefix }; $rec->{OPTIONS} = %options ? { %options } : parse_options($rec->{desc}); foreach (@provides) { push(@{$map->{$_->{name}}->{vs($_)}}, $rec); } } $href = undef; } } return \@include; } sub xml_parser ($$$$) { my($fh, $url, $with, $map) = @_; my(@include); my($xml,$desc,$sub); my($provides,@provides,%options,$rec); my($href,$name,$version,$release); print "# using XML parser\n"; $xml = XML::Simple::XMLin($fh, forcearray => 1); $desc = $xml->{'Repository'}->[0]->{'rdf:Description'}; $sub = $xml->{'Repository'}->[0]->{'Repository'}; foreach (@$desc) { $href = $_->{'href'}; $name = xel($_->{'Name'}); $version = xel($_->{'Version'}); $release = xel($_->{'Release'}); next unless defined $href && defined $name && defined $version && defined $release; $provides = $_->{'Provides'}->[0]->{'rdf:bag'}->[0]; if ($provides->{'rdf:li'}) { $provides = $provides->{'rdf:li'}; } else { $provides = $provides->{'resource'}; } @provides = map { depends2provides(parse_depends($_)) } @$provides; %options = map { ( $_->{with} => $_->{version} ) } grep { defined $_->{with} } @provides; unless (grep($_->{name} eq $name, @provides)) { push(@provides, { name => $name, version => $version, release => $release }); } $rec = { href => (relurl($url, undef, $href))[0], name => $name, version => $version, release => $release, platform => xel($_->{'Platform'}), prefix => xel($_->{'Prefixes'}), depends => depend_list(with_list($_->{'BuildPreReq'}, $with)), keeps => depend_list(with_list($_->{'PreReq'}, $with)), conflicts => with_list($_->{'Conflicts'}, $with), source => with_list($_->{'Source'}, $with), nosource => with_list($_->{'NoSource'}, $with), desc => xel($_->{'Description'}) }; $rec->{OPTIONS} = %options ? { %options } : parse_options($rec->{desc}); foreach (@provides) { push(@{$map->{$_->{name}}->{vs($_)}}, $rec); } } if ($sub) { @include = map { $_->{href} } @$sub; } return \@include; } sub open_index ($$) { my($url, $fn) = @_; my($fetch,$bzip2,$path); $fetch = defined $fn ? $fn : $url; $bzip2 = $RPM; $bzip2 =~ s/bin\/rpm$/lib\/openpkg\/bzip2/ or die "FATAL: cannot deduce bzip2 path from $RPM\n"; $fetch !~ /\.bz2$/ || -x $bzip2 or die "FATAL: $bzip2 not found\n"; if ($fetch =~ /^\w+:/) { # looks like URL scheme print "# curling index $fetch\n"; if ($fetch =~ /\.bz2$/) { $path = "$CURL -q -s -o - \"$fetch\" | $bzip2 -dc |"; } else { $path = "$CURL -q -s -o - \"$fetch\" |"; } } else { print "# reading index file $fn\n"; if ($fetch =~ /\.bz2$/) { $path = "$bzip2 -dc $fetch |"; } else { $path = "< $fetch"; } } open(RFH, $path) or die "FATAL: cannot open '$fetch' ($!)\n"; } # # fetch index from file or URL # recursively fetch sub-indexes # sub get_index ($$$$) { my($url,$fn,$with,$noxml) = @_; my(%map,$include); open_index($url,$fn); unless ($noxml) { eval { require XML::Simple; }; $noxml = 1 if $@; } if ($noxml) { $include = simple_text_parser(\*RFH, $url, $with, \%map); } else { $include = xml_parser(\*RFH, $url, $with, \%map); } close(RFH) or die "FATAL: an I/O error occured\n"; # # cannot do real recursions on file handles, so we simply append # all sub-RDFs, the result is flattend into a big hash anyway # foreach (@$include) { my($submap); my($suburl,$subfn) = relurl($url,$fn,$_); $submap = get_index($suburl,$subfn,$with,$noxml); while (my($name,$vmap) = each %$submap) { while (my($vs,$recs) = each %$vmap) { push @{$map{$name}->{$vs}}, @$recs; } } } return \%map; } ############################################################################ # # grep all versions of a name that # satisfy a condition # sub get_versions ($$) { my($relmap, $cond) = @_; return grep { $cond->($_); } sort { vcmp($a,$b); } keys %$relmap; } # # there can be multiple sources for a target release # sub chose_source ($$@) { my($env, $name, $vmap, @vers) = @_; my(@recs,@nrecs,$rec); return unless @vers; @recs = grep { $env->{sourceonly} ? ( !(defined $_->{'prefix'}) ) : ( !(defined $_->{'prefix'}) || ( defined $_->{'platform'} && $_->{'platform'} eq $env->{config}->{platform} && $_->{'prefix'} eq $env->{config}->{prefix} ) ) } map { @{$vmap->{$_}} } @vers; return unless @recs; if (scalar(@recs) > 1) { @nrecs = grep { $env->{built}->{$_->{name}} || $env->{installed}->{$_->{name}} } @recs; @recs = @nrecs if @nrecs; } if (scalar(@recs) > 1 && !$env->{sourceonly}) { @nrecs = grep { defined $_->{'platform'} } @recs; @recs = @nrecs if @nrecs; } if (scalar(@recs) > 1) { print "# ambigous sources for $name\n"; my($i) = 0; foreach (@recs) { print "# $i: ".vsn($_)." = $_->{href}\n"; $i++; } return; } else { if ($env->{upgrade}) { $rec = $recs[-1]; } else { $rec = $recs[0]; } } print "# source for $name is ".vsn($rec)."\n"; return $rec; } # # see wether target is in map # sub target_exists ($$) { my($target, $map) = @_; my($vmap) = $map->{$target->{name}}; return unless $vmap; return !defined $target->{version} || defined $vmap->{vs($target)}; } # # find target in map # sub find_target ($$) { my($name, $map) = @_; my($vmap) = $map->{$name}; my(@vs); return unless $vmap; @vs = sort { vcmp($b,$a) } keys %$vmap; return $vmap->{$vs[0]}->[-1]; } # # see wether target has conflicts in map # sub target_conflicts ($$) { my($target, $map) = @_; my($t); foreach (@{$target->{conflicts}}) { $t = find_target($_, $map); return $t if $t; } return; } # # retrieve build dependencies for target in map # sub target_depends ($$) { my($target, $map) = @_; my($vmap,$vers); die "FATAL: ",vsn($target)," not in depend map\n" unless ( $vmap = $map->{$target->{name}} ) && ( defined $target->{version} ) && ( $vers = $vmap->{vs($target)} ) && @$vers; return $vers->[0]->{depends}; } # # retrieve runtime dependencies for target in map # sub target_keeps ($$) { my($target, $map) = @_; my($vmap,$vers); die "FATAL: ",vsn($target)," not in keep map\n" unless ( $vmap = $map->{$target->{name}} ) && ( defined $target->{version} ) && ( $vers = $vmap->{vs($target)} ) && @$vers; return $vers->[0]->{keeps}; } # # strip doubles from depend/keep lists # and a return a map name => depend/keep # sub unique_map { my(%out); foreach (@_) { foreach (@$_) { $out{$_->{name}} = $_; } } return %out; } # # test wether target could be upgraded # sub target_newer ($$) { my($target, $map) = @_; my($vs) = vs($target); my($vmap) = $map->{$target->{name}}; return 1 unless $vmap; return !grep { vcmp($vs, $_) <= 0; } keys %$vmap; } # # check wether installed package matches # build options # sub target_suitable ($$) { my($target, $with) = @_; my($iwith); my($k,$v); $iwith = $target->{OPTIONS}; while (($k,$v) = each %$with) { if (exists $iwith->{$k}) { return 0 if $iwith->{$k} ne $with->{$k}; } } return 1; } # # record target status # sub target_setstatus ($$$) { my($target, $status, $pri) = @_; if ($pri > $target->{STATUSPRI}) { $target->{STATUSPRI} = $pri; $target->{STATUS} = $status; } } # # report options that are not used for # sub warn_about_options ($$$) { my($target, $with, $c) = @_; my($iwith) = $target->{OPTIONS}; my($k,$v); return unless defined $iwith; while (($k,$v) = each %$with) { if (!exists $iwith->{$k} && $k !~ $c->{optreg}) { print "# ATTENTION: $target->{name} ignores option '$k'\n"; } } } ############################################################################ # # LOGIC # # # locate target for a dependency # sub dep2target ($$) { my($dep, $env) = @_; my($name,$op,@vers); my($i,$r,$b,$cond,$version); my($t,$tdef); ($name, $op, $version) = ($dep->{name}, $dep->{op}, $dep->{val}); $i = $env->{installed}->{$name}; $r = $env->{repository}->{$name}; $b = $env->{built}->{$name}; return unless $i || $r || $b; if (!defined $op) { $cond = sub { 1; }; } elsif ($op eq 'geq') { $cond = sub { vcmp($_[0],$version) >= 0; }; } elsif ($op eq 'leq') { $cond = sub { vcmp($_[0],$version) <= 0; }; } elsif ($op eq 'gt') { $cond = sub { vcmp($_[0],$version) > 0; }; } elsif ($op eq 'lt') { $cond = sub { vcmp($_[0],$version) < 0; }; } elsif ($op eq 'equ') { $cond = sub { vcmp($_[0],$version) == 0; }; } else { die "FATAL: internal error in dep2target\n"; } $tdef = undef; if ($i && (@vers = get_versions($i, $cond))) { foreach (@vers) { $t = $i->{$_}->[0]; get_with($t); if (target_suitable($t, $env->{with})) { $tdef = $t; unless ($env->{upgrade}) { return ($t, 1); } } } } if ($b && (@vers = get_versions($b, $cond))) { return ($b->{$vers[0]}->[0], 1); } $t = chose_source($env, $name, $r, get_versions($r, $cond)); if ($t) { if (!$tdef || ($env->{upgrade} && target_newer($t, $env->{installed}))) { return ($t, 0); } } if ($tdef) { return ($tdef, 1); } return; } # # # sub make_dep ($$$$$$) { my($target,$depth,$env,$list,$blist,$clist) = @_; my($d,$k,%d,%k,$t,$old); my(@deps,$conflict); if (target_exists($target, $env->{built})) { print "# $target->{name} is already in list\n"; return; } if ($t = target_conflicts($target, $env->{installed})) { target_setstatus($target,'CONFLICT',4); push(@$clist,$target); print "# $target->{name} conflicts with ",vsn($t),"\n"; return; } if ($t = target_conflicts($target, $env->{built})) { target_setstatus($target,'CONFLICT',4); push(@$clist,$target); print "# $target->{name} conflicts with ",vsn($t),"\n"; return; } # # see if a target is already installed and requires a rebuild # if ($t = find_target($target->{name}, $env->{installed})) { if (exists $env->{exclude}->{$target->{name}}) { print "# excluding $target->{name} (no upgrade allowed)\n"; return; } # pull in options get_with($t); if ($target->{REBUILD}) { target_setstatus($target,'DEPEND',1); print "# rebuilding $target->{name} (dependency)\n"; } elsif ($env->{zero}) { target_setstatus($target,'ZERO',1); print "# rebuilding $target->{name} (zero)\n"; } elsif (target_newer($target, $env->{installed})) { target_setstatus($target,'UPGRADE',3); print "# rebuilding $target->{name} (upgrade)\n"; } elsif (!target_suitable($t, $env->{with})) { target_setstatus($target,'MISMATCH',2); print "# rebuilding $target->{name} (parameter mismatch)\n"; } else { print "# $target->{name} is already installed\n"; return; } # use options from installed base override_options(get_with($target), get_with($t), $env->{config}->{optreg}); # remember this is a rebuild for a proxy package $target->{PROXY} = $t->{PROXY}; $target->{REBUILD} = 1; } else { target_setstatus($target,'ADD',3); } if (exists $env->{exclude}->{$target->{name}}) { die "FATAL: target ".vsn($target)." is forbidden\n"; } # mark this as a target before reverse dependencies trigger # it again push(@{$env->{built}->{$target->{name}}->{vs($target)}}, $target); $d = target_depends($target, $env->{repository}); $k = target_keeps($target, $env->{repository}); # # recurse over dependencies # if (@$d || @$k) { %d = unique_map($d, $k); %k = unique_map($k); @deps = (); $conflict = 0; foreach (keys %d) { # old index misses a OpenPKG provider in the index... skip it next if $_ eq 'OpenPKG'; ($t,$old) = dep2target($d{$_}, $env); if ($t) { if ($old) { print "# $target->{name} uses ".vsn($t)." for $_\n"; next; } # record which targets to keep in blist if ($k{$_}) { push @$blist,$t; print "# $target->{name} installs ".vsn($t)." for $_\n"; } else { print "# $target->{name} requires ".vsn($t)." for $_\n"; } push @deps, $t; } else { print "# $target->{name} searches a frood called '$_'\n"; push(@{$env->{fatal}},vsn($target)); target_setstatus($target,'UNDEF',4); push @$clist, $target; $conflict = 1; } } unless ($conflict) { foreach $t (@deps) { make_dep($t,$depth+1,$env,$list,$blist,$clist); } } } print "# adding ".vsn($target)." to list\n"; push(@$list, $target); foreach (@{$target->{nosource}}) { print "# ATTENTION: unpackaged source $_: $target->{source}->[$_]\n"; } # # a dependency could not be resolved, don't bother with reverse # dependencies for this target # return if $conflict; if (!$env->{quick} && $target->{name} ne 'openpkg' && $target->{REBUILD}) { unless ($env->{revdep}) { $env->{revdep} = get_revdep($env); } foreach $t (@{$env->{revdep}->{$target->{name}}}) { # this is a rebuild, triggering further revdeps $t->{REBUILD} = 1; # this is a rebuild, keep this installed push(@$blist, $t); print "# rebuilding revdep ".vsn($t)."\n"; make_dep($t,$depth+1,$env,$list,$blist,$clist); } } } # # generate build lists for targets matched by pattern # # all input and output is passed in 'env' hash # sub build_list ($$) { my($pattern, $env) = @_; my(@goals,@targets,@keeps,@conflicts,@bonly,$t); my($name,$r,$i,@vers); my(@todo,%keep); # # handle various patterns # if (defined $pattern) { @todo = (); foreach (split(/\s+/,$pattern)) { next unless /\S/; if (s/\*+$//) { push @todo, '^'.quotemeta($_).''; } else { push @todo, '^'.quotemeta($_).'$'; } } @todo = map { my($p) = $_; grep(/$p/, keys %{$env->{repository}}) } @todo; } else { # # undefined pattern means -a option that selects # all packages from repository that are installed # @todo = grep { my($n) = $_; (ref $env->{installed}->{$n}) && grep { $_ ne '-' } keys %{$env->{installed}->{$n}} } keys %{$env->{repository}}; } # # chose sources for goals from repository # foreach $name (@todo) { $t = undef; # # keeping installed packages for goals is ugly # -> we currently do not support installed source RPMs # -> source RPMs might already have expired from repository # # consequence: # -> goals are always upgraded to repository versions # #unless ($env->{upgrade}) { # $i = $env->{installed}->{$name}; # if (@vers = get_versions($i, sub { 1; })) { # $t = chose_source($env, $name, $i, @vers); # } #} unless ($t) { $r = $env->{repository}->{$name}; if (@vers = get_versions($r, sub { 1; })) { $t = chose_source($env, $name, $r, @vers); } } if ($t) { warn_about_options($t, $env->{with}, $env->{config}); push(@goals, $t); } else { if ($env->{status}) { print "# dropping goal '$name'\n"; } else { die "FATAL: cannot find source for '$name'\n"; } } } return unless @goals; @targets = (); @keeps = @goals; foreach $t (@goals) { print "# recursing over dependencies for ".vsn($t)."\n"; make_dep($t,0,$env,\@targets,\@keeps,\@conflicts); } %keep = map { $_ => 1 } @keeps; @bonly = grep { !$keep{$_} && !$env->{installed}->{$_->{name}}->{vs($_)}; } @targets; return (\@targets, \@bonly, \@conflicts); } ####################################################################### # # OUTPUT # # # compute path to binary RPM from rpm config and target data # sub target2rpm ($$) { my($target,$c) = @_; my($tmpl) = $c->{template}; my($popt) = $target->{PROXY} ? '+PROXY' : ''; $tmpl =~ s/%{NAME}/$target->{name}/; $tmpl =~ s/%{VERSION}/$target->{version}/; $tmpl =~ s/%{RELEASE}/$target->{release}$popt/; return $c->{rpmdir}.'/'.$tmpl; } # # compute new target based on old target augmented with options from # a binary RPM file # sub binary_target ($$) { my($t, $fn) = @_; my(%target) = %$t; # pull in options from binary RPM file get_with(\%target, $fn); return \%target; } # # return path to master package for a proxy package # sub find_proxy ($$) { my($t,$bpkg) = @_; my(@l) = `$RPM_NPRIV -ql $t->{name}`; my($link) = (grep { $_ =~ /\/\.prefix-$t->{name}$/ } @l)[0]; return unless defined $link; chomp $link; my($prefix) = readlink($link); return unless defined $prefix; $bpkg =~ s/.*\///; $bpkg =~ s/\+PROXY(\.[^-]+-[^-]+)-[^-]+\.rpm$/$1-*.rpm/; return (glob("$prefix/RPM/PKG/$bpkg"))[0]; } # # merge parameters from installed package # with new parameter set and global parameters # from configuration # # then map the result to --define command line arguments # suitable for rpm # sub make_defines ($$$) { my($old, $new, $c) = @_; my($with); # # override old parameters with new parameters # drop new parameters that do not exist in old set # # if there is no old set at all (which happens if there # is no template and no installed package), just use the # new parameters and assume these are useful. # if ($old) { $old = { %$old }; override_options($old, $new, $c->{optreg}); } else { $old = $new; } # # convert parameters to --define command line options # skip parameter templates from index # $with = join(' ',map { "--define '$_ $old->{$_}'" } sort grep { $old->{$_} !~ /^%/ } keys %$old); $with = ' '.$with if $with ne ''; return $with; } # # print commands from package build list # # c -> configuration to derive paths from # uncond -> always do the --rebuild # with -> parameter set passed to build tool # ignore -> generate script that does not stop on error # sub print_list1 ($$$@$) { my($list,$c,$uncond,$with,$ignore) = @_; my($spkg,$bpkg,$ppkg); my($opt); my($cmd1, $cmd2, $mark); $mark = '::::'; foreach (@$list) { $spkg = $_->{href}; $bpkg = target2rpm($_, $c); # # rebuild binary package IF # # 'unconditional' option # OR there is no binary package # OR dependency check found that installed package is not suitable # OR existing binary package doesn't satisfy wanted options # $cmd1 = undef; if ($uncond || !-f $bpkg || $_->{REBUILD} || !target_suitable(binary_target($_, $bpkg),$with)) { $opt = make_defines($_->{OPTIONS}, $with, $c); # # proxy packages are rebuilt from their maste # hierachy # # someone preferred a binary from the repository # just copy it to the local store # if ($_->{PROXY}) { $ppkg = find_proxy($_,$bpkg) or die "FATAL: proxy package ",vsn($_)," does not exist\n"; # # rpm doesn't support additional parameters to the # mkproxy script # $cmd1 = "$RPM_NPRIV$opt --makeproxy $ppkg -- -o $bpkg"; # $cmd1 = "( cd $c->{rpmdir} && $RPM_NPRIV$opt --makeproxy $ppkg )"; } elsif (defined $_->{prefix}) { $cmd1 = "$CURL -q -s -o $bpkg $spkg"; } else { $cmd1 = "$RPM_NPRIV$opt --rebuild $spkg"; } } # # if package exist force rpm to copy over new files # better than erasing everything and losing configuration # files # $opt = $_->{REBUILD} ? ' --force' : ''; $cmd2 = "$RPM_PRIV$opt -Uvh $bpkg"; if ($ignore) { $cmd2 = "$cmd1 && \\\n$cmd2" if defined $cmd1; } else { if (defined $cmd1) { $cmd2 = "$cmd1 || exit \$?\n$cmd2 || exit \$?" } else { $cmd2 = "$cmd2 || exit \$?"; } } print "echo $mark $spkg $mark\n$cmd2\necho $mark $spkg = \$? $mark\n"; } } # # print commands for the temporary package list # # temporary packages are only used for building other packages # and are removed when everything is done # sub print_list2 ($$) { my($list,$c) = @_; my($pkg); foreach (@$list) { $pkg = "$_->{name}-$_->{version}-$_->{release}"; print "$RPM_PRIV -e $pkg\n"; } } # # instead of printing a command list, print a status map # that shows all packages and how the build process would # change their status # sub print_status ($$$$$) { my($installed,$repository,$list,$bonly,$clist) = @_; my(%bonly) = map { $_ => 1 } @$bonly; my(%map,$n,@names,$t); my($old,$tag,$new); foreach (@$list, @$clist) { next unless defined $_->{release}; $map{$_->{name}} = { rel => "$_->{version}-$_->{release}", status => $_->{STATUS} }; } foreach (@$bonly) { $map{$_->{name}} = { rel => "$_->{version}-$_->{release}", status => 'TEMP' }; } @names = keys %map; foreach $n (keys %$installed) { next if $n =~ /::/; next if exists $map{$n}; next unless grep { $_ ne '-' } keys %{$installed->{$n}}; $map{$n}->{'status'} = 'OK'; push @names,$n; } foreach $n (keys %$repository) { next if $n =~ /::/; next if exists $map{$n}; next unless grep { $_ ne '-' } keys %{$repository->{$n}}; $t = find_target($n, $repository); $map{$n}->{'status'} = 'NEW'; $map{$n}->{'rel'} = vs($t); push @names,$n; } foreach $n (sort @names) { $old = join ',', map { "$n-$_" } sort grep { $_ ne '-' } keys %{$installed->{$n}}; $old = $n if $old eq ''; $tag = $map{$n}->{status}; $new = defined $map{$n}->{rel} ? " $n-$map{$n}->{rel}" : ''; printf "%-35s %-8s%s\n", $old, $tag, $new; } } ####################################################################### my($config,$url,$repository,$installed,$env,$list,$bonly,$clist); my($pattern,%with,%exclude); if ($opt_a) { $pattern = undef; } else { $pattern = join(' ', @ARGV); } if ($opt_A) { $pattern = '*'; } %with = map { /([^\s=]+)(?:\=(\S+))?/ ? ($1 => (defined $2 ? $2 : 'yes')) : () } split(/\s+/, $opt_D); %exclude = map { $_ => 1 } split(/\s+/, $opt_E); $config = get_config(); if (defined $opt_p) { $config->{platform} = $opt_p; } if (defined $opt_r) { $url = $opt_r; $url .= '/' unless $url =~ /\/$/; } else { $url = get_release(); } # if we read the index from a file we can no longer deduce # repository paths from index paths. For now lets assume # that everything is below SRC/ to be compatible with # existing file indexes. if (defined $opt_f && !defined $opt_r) { $url .= 'SRC/'; } $installed = $opt_Z ? {} : get_installed(); $repository = get_index($url.'00INDEX.rdf',$opt_f,\%with,$opt_X); $env = { config => $config, installed => $installed, repository => $repository, built => {}, revdep => undef, with => \%with, exclude => \%exclude, upgrade => ($opt_a || $opt_U), zero => ($opt_z || $opt_Z), quick => $opt_q, status => ($opt_s || $opt_S), fatal => [], sourceonly => ($opt_u || $opt_U || $opt_z || $opt_Z || scalar(%with) > 0 ) }; ($list,$bonly,$clist) = build_list($pattern, $env); die "FATAL: cannot find package\n" unless defined $list; if ($opt_S) { print_status($installed,$repository,$list,$bonly,$clist); } elsif ($opt_s) { print_status($installed,{},$list,$bonly,$clist); } else { if (@{$env->{fatal}}) { die "FATAL errors occured while building:\n", join (',', @{$env->{fatal}}), "\n"; } print_list1($list,$config,$opt_a || $opt_u || $opt_U,\%with,$opt_i); print_list2($bonly,$config); }