diff -r 71503088f51b -r f880f219c566 openpkg/build.pl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openpkg/build.pl Tue Jul 31 12:23:42 2012 +0200 @@ -0,0 +1,2603 @@ +## +## build.pl -- OpenPKG Package Building and Installing +## Copyright (c) 2000-2012 OpenPKG GmbH +## +## This software is property of the OpenPKG GmbH, DE MUC HRB 160208. +## All rights reserved. Licenses which grant limited permission to use, +## copy, modify and distribute this software are available from the +## OpenPKG GmbH. +## +## 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. +## + +############################################################################# +## +## MAIN PROCEDURE +## +############################################################################# + +require 5; +#use strict; +my ( + $opt_h, + $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_H, $opt_i, + $opt_D, $opt_p, $opt_q, $opt_s, $opt_S, $opt_X, $opt_M, + $opt_L, $opt_W, $opt_K, $opt_e, $opt_b, $opt_B, $opt_g, + $opt_k +); + +# global context variables +my $prg = "openpkg build"; +my %env = ('' => {}); + +## +## OPTION PARSING +## + +# parse command line options +my $getopts = 'hR:r:f:uUaAzZP:N:E:H:iD:p:qsSXMLWKebBgk'; +getopts($getopts); + +# parse configuration script options +if (open(FH, "<$ENV{'HOME'}/.openpkg/build")) { + my ($env) = $env{''}; + my ($go) = $getopts; + $go =~ s/[^a-zA-Z]//g; + while (my $line = ) { + if ($line =~ m/^\s*\[([^\]]*)\]/) { + $env{$1} = {} unless ($env{$1}); + $env = $env{$1}; + } elsif (my ($opt, $val) = ($line =~ m/^\-([$go])\s*(.*?)\s*$/)) { + $val = 1 unless (defined($val)); + if (exists($env->{$opt})) { + $env->{$opt} .= " " . $val; + } else { + $env->{$opt} = $val; + } + } + } + close(FH); +} + +# usage sanity check and usage help +sub usage { + my ($rc) = @_; + my $usage = + "openpkg:build:USAGE: $prg [options] [pattern ...]\n" . + " -a operate on all installed packages\n" . + " -A operate on all repository packages\n" . + " -R path to \"openpkg rpm\" command\n" . + " -r URL to package repository directory\n" . + " -f URL to package repository index file\n" . + " -u ignore local binary RPMs\n" . + " -U upgrade all selected packages including dependencies\n" . + " -z rebuild from zero all selected installed packages\n" . + " -Z rebuild from zero all selected available packages\n" . + " -i ignore errors in the generated script\n" . + " -q ignore all reverse dependencies\n" . + " -s generate status map instead of shell script\n" . + " -S generate status map instead of shell script (including new)\n" . + " -X use external XML/RDF parser instead of internal one\n" . + " -M generate short dependency map instead of shell script\n" . + " -L generate list of packages in repository depending on target\n" . + " -W include dependencies as if all build options are enabled\n" . + " -K keep temporarily installed packages\n" . + " -k keep temporarily downloaded packages\n" . + " -e rebuild exact version of a package from repository\n" . + " -b build-time check existing binaries for dependencies only\n" . + " -B build-time check existing binaries for dependencies and target\n" . + " -g rebuild packages even when most recent version is installed\n" . + " -P command prefix for privileged commands\n" . + " -N command prefix for non-privileged commands\n" . + " -p match platform against repository index for binary packages\n" . + " -E exclude package\n" . + " -H hint about packages to resolve ambiquity\n" . + " -D [=] set build option for packages\n"; + if ($rc == 0) { + print STDOUT $usage; + } + else { + print STDERR $usage; + } + exit($rc); +} +if ($opt_h) { + usage(0); +} +if (not ( ($#ARGV >= 0 && !($opt_a || $opt_A)) + || ($#ARGV == -1 && ($opt_a || $opt_A)))) { + usage(1); +}; + +# determine RPM run-time information +my $config = rpm_runtime_info(); + +# override command line options with configuration script options +# now that the effectively used OpenPKG RPM command is known +foreach my $env (sort { $a cmp $b } grep { + $config->{"rpm"} =~ m/^\Q$_\E/ # compatibility + or $config->{"prefix"} =~ m/^\Q$_\E/ # regular +} keys %env) { + while (my ($opt, $val) = each(%{$env{$env}})) { + eval "\$opt_$opt = '$val' unless defined \$opt_$opt;"; + } +} + +## +## OPTION POST-PROCESSING +## + +my ($url, $repository, $installed, $env, $list, $bonly, $clist); +my ($pattern, %with, %exclude, %hint); + +# determine package goal pattern +if ($opt_a) { + $pattern = undef; +} else { + $pattern = join(' ', @ARGV); +} +if ($opt_A) { + $pattern = '*'; +} + +# parse build options +%with = map { + m/([^\s=]+)(?:\=(\S+))?/ + ? ($1 => (defined($2) ? $2 : 'yes')) + : () +} split(/\s+/, $opt_D); + +# split accumulated option values +%exclude = map { $_ => 1 } split(/\s+/, $opt_E); +%hint = map { $_ => 1 } split(/\s+/, $opt_H); + +if (defined($opt_p)) { + $config->{platform} = $opt_p; +} + +# determine RPM package repository information +if (defined $opt_r) { + $url = $opt_r; + $url .= '/' unless $url =~ m/\/$/; +} else { + $url = rpm_release_url(); +} +# 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) and not defined($opt_r)) { + $url .= 'SRC/'; +} + +# determine information about INSTALLED packages (virtual and regular), +# including their options, provides and requirements +my $installed = get_installed(); + +# SPECIAL CASE post-processing for +# -Z (ignore installed packages) +# -a (operate for all installed packages) +if ($opt_a and $opt_Z) { + # This allows one to correctly upgrade an existing OpenPKG + # instance to a newer major version by querying all installed + # packages and their options (-a) but then ignore them (-Z) during + # the later processing and instead perform more or less a fresh + # rebuild from scratch. This ensures that during the process the + # installed packages are effectively picked up as dependencies + # only after they in turn were already updated. + foreach my $package (keys(%{$installed})) { + next if ($package =~ m/::/); + if (exists($installed->{$package}->{""})) { + # virtual package + $hint{$installed->{$package}->{""}->[0]->{"name"}} = 1 + if (exists($installed->{$package}->{""}->[0]->{"name"})); + } + else { + # regular package + $pattern .= " $package"; + foreach my $version (keys(%{$installed->{$package}})) { + foreach my $rec (@{$installed->{$package}->{$version}}) { + if (defined($rec->{"OPTIONS"})) { + my $options = $rec->{"OPTIONS"}; + foreach my $option (keys(%{$options})) { + $with{$package."::".$option} = $options->{$option}; + } + } + } + } + } + } +} +if ($opt_Z) { + $installed = {}; +} + +# determine information about AVAILABLE packages +# by fetching and parsing a package repository XML/RDF index +$repository = get_index( + $url . '00INDEX.rdf', + $opt_f, + $opt_X, + $config->{platform}, + $installed +); + +# assemble together all determined environment information +$env = { + config => $config, + installed => $installed, + repository => $repository, + built => {}, + revdep => undef, + with => \%with, + exclude => \%exclude, + hint => \%hint, + upgrade => ($opt_a || $opt_U), + zero => ($opt_z || $opt_Z), + exact => $opt_e, + quick => ($opt_q || $opt_z || $opt_Z), + status => ($opt_s || $opt_S), + fatal => [], + goals => $opt_g, + sourceonly => ($opt_u || $opt_U || $opt_z || $opt_Z) +}; + +## +## PERFORM REQUESTED OPERATION +## + +if ($opt_L) { + # case 1: calculate dependencies only and + # print packages depending on target + ($list) = build_deps($pattern, $env); + print_deps($list); +} else { + # case 2: calculate build commands and + # print results in different formats + ($list, $bonly, $clist) = build_list($pattern, $env); + die "openpkg:build:FATAL: cannot find package\n" if (not defined($list)); + if ($opt_M) { + print_map($installed, $repository, $list, $bonly, $clist); + } elsif ($opt_S) { + print_status($installed, $repository, $list, $bonly, $clist); + } elsif ($opt_s) { + print_status($installed, {}, $list, $bonly, $clist); + } else { + if (@{$env->{fatal}}) { + die "openpkg:build:FATAL: errors occured while building:\n", @{$env->{fatal}}, "\n"; + } + print_list1($list, $config, $opt_a || $opt_u || $opt_U, $env->{with}, $opt_i, $opt_b, $opt_B); + print_list2($bonly, $config) if (not $opt_K); + } +} + +# die gracefully +exit(0); + +############################################################################# +## +## FUNCTIONS: PARSING & RUN-TIME INFORMATION +## +############################################################################# + +# home-brewn getopt(3) style option parser +sub getopts ($) { + my ($opts) = @_; + my (%optf) = map { m/(\w)/; $1 => $_ } $opts =~ m/(\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 "openpkg:build:WARNING: unknown option $_\n"; + } + $_ = "-$2"; + } + } + } + if (defined $optarg) { + warn "openpkg:build:WARNING: option $optarg requires an argument\n"; + } + foreach (keys %opts) { + eval '$opt_'.$_.' = "'.quotemeta($opts{$_}).'";'; + } + @ARGV = @argv; +} + +# determine RPM run-time information +sub rpm_runtime_info () { + # determine OpenPKG instance prefix via + # 1. the environment of the "openpkg build" framework + # 2. the installation path of the script + # 3. the installation path of the Perl interpreter + # 4. the path of the "openpkg" command in $PATH + my $l_prefix = $ENV{'OPENPKG_PREFIX'}; + if (not $l_prefix) { + ($l_prefix) = ($0 =~ m/^(.+)\/lib(exec)?\/openpkg(-tools)?\/build(\.pl)?$/); + } + if (not $l_prefix) { + ($l_prefix) = ($^X =~ m/^(.+)\/bin\/perl.*$/); + } + if (not $l_prefix) { + $l_prefix = (`(which openpkg) 2>/dev/null` =~ m/^(.+)\/bin\/openpkg$/); + } + if (not -x "$l_prefix/bin/openpkg") { + die "openpkg:build:FATAL: cannot determine OpenPKG instance prefix"; + } + print "# operating with OpenPKG instance $l_prefix\n"; + + # determine OpenPKG RPM command + my $rpm = $opt_R || $env{''}->{'R'} || + ((-f "$l_prefix/bin/openpkg" && -f "$l_prefix/libexec/openpkg/rpm") ? + "$l_prefix/bin/openpkg rpm" : "$l_prefix/bin/rpm"); + $rpm = (`(which $rpm) 2>/dev/null` =~ m{^(/.*)})[0] if ($rpm !~ m|^/|); + die "openpkg:build:FATAL: cannot locate OpenPKG RPM in path" unless ($rpm =~ m{^/}); + print "# operating with OpenPKG RPM $rpm\n"; + + # determine additional tools + my $mkp = "$l_prefix/bin/openpkg makeproxy"; + my $rel = "$l_prefix/bin/openpkg release"; + my $bzip2 = $rpm; + $bzip2 =~ s/\/bin\/openpkg rpm$/\/lib\/openpkg\/bzip2/; + my $curl = $rpm; + $curl =~ s/\/bin\/openpkg rpm$/\/lib\/openpkg\/curl/; + $curl = "$l_prefix/bin/openpkg curl" if (system("$l_prefix/bin/openpkg curl file://$l_prefix/etc/openpkg/platform >/dev/null 2>&1") == 0); + + # expand RPM macros holding information + my $c = run("$rpm --eval '%{_rpmdir} %{_rpmfilename} %{_target_os} %{_target_cpu} %{_srcrpmdir}'"); + + # parse and post-process information + chomp($c); + my (@q) = split(/\s+/, $c); + $q[1] =~ s/%{OS}/$q[2]/; + $q[1] =~ s/%{ARCH}/$q[3]/; + + # expand RPM rc information about tools + $c = run("$rpm --showrc"); + my @g = ($c =~ m/\%\{l_tool_locate\s+([^\s\}]+)/g); + + # return accumulated information + return { + rpm => $rpm, + mkp => $mkp, + rel => $rel, + bzip2 => $bzip2, + curl => $curl, + rpmdir => $q[0], + srcrpmdir=> $q[4], + template => $q[1], + platform => '', + prefix => $l_prefix, + optreg => '(?:'. join('|', map { "\Quse_$_\E" } @g) .')' + }; +} + +# determine RPM release URL +sub rpm_release_url ($$) { + my ($rel, $url); + + # determine the release URL the newer way + $url = run("(".$config->{"rel"}." --fmt='%u') 2>/dev/null || true") || ""; + $url =~ s/^\s+//s; + $url =~ s/\s+$//s; + + # use a local jumpstart RDF + if (-f $config->{"prefix"}."/etc/openpkg/build.rdf") { + $url = "file://".$config->{"prefix"}."/etc/openpkg/build.rdf" + } + + return $url; +} + +############################################################################# +## +## FUNCTIONS: VERSION STRING HANDLING +## +############################################################################# + +# compare two package versions +# - "openpkg rpm": +# splits according to !isalnum(3) ([a-zA-Z0-9]) +# and between isdigit(3) ([0-9]) and isalpha(3) ([a-zA-Z]) +# - "openpkg build" (this): +# splits on "." characters +sub vcmp_version ($$) { + my ($a, $b) = @_; + my (@a, @b, $c); + my ($ax, $bx); + + # split according to dots + @a = split(/\./, $a); + @b = split(/\./, $b); + + # compare as long as components exist + while (@a && @b) { + if ($a[0] =~ m/^\d+$/ && $b[0] =~ m/^\d+$/) { + # numerical comparison + $c = $a[0] <=> $b[0]; + } elsif ((($a, $ax) = $a[0] =~ m/^(\d+)(.*)$/) && + (($b, $bx) = $b[0] =~ m/^(\d+)(.*)$/)) { + # numerical comparison for prefix, + # string comparison for remainder + $c = $a <=> $b; + $c = $ax cmp $bx unless ($c); + } else { + # string comparison + $c = $a[0] cmp $b[0]; + } + + # stop comparison if components already mismatched + return $c if ($c != 0); + + # else reduce by one component level + shift(@a); + shift(@b); + } + + # finally compare number of remaining components + # (in case one is more specific) + $c = (scalar(@a) <=> scalar(@b)); + return $c; +} + +# compare two package releases +# - "openpkg rpm": +# uses "vcmp_version" semantics 1:1 again +# - "openpkg build" (this): +# uses "vcmp_version" semantics 1:1 again (>= 20060719) +# trivial string comparison (<= 20060719) +sub vcmp_release ($$) { + my ($a, $b) = @_; + + return vcmp_version($a, $b); +} + +# compare two package "version" or "version-release" strings +# - "openpkg rpm": +# compares "epoch", "version", "release" (in this order) +# - "openpkg build" (this): +# compares "release", "version", (in this order) +sub vcmp ($$) { + my ($a, $b) = @_; + my ($c); + + # short-circuit comparison for simple case + return 0 if ($a eq $b); + + # split into "version" and "release" + my ($av, $ar) = ($a =~ m/^(.*?)(?:\-([\d\.]+))?$/); + my ($bv, $br) = ($b =~ m/^(.*?)(?:\-([\d\.]+))?$/); + + # compare "release" + if (defined($ar) and defined($br)) { + $c = vcmp_release($ar, $br); + return $c if ($c); # short-circuit + } + + # compare "version" + if (defined($av) && defined($bv)) { + $c = vcmp_version($av, $bv); + return $c if ($c); # short-circuit + } + + # equality + return 0; +} + +# create "version" or "version-release" string +# from a provide object (see parse_provides) +sub vs ($) { + my ($t) = @_; + return ( + defined($t->{release}) + ? "$t->{version}-$t->{release}" + : $t->{version} + ); +} + +# create "name-version" or "name-version-release" string +# from a provide object (see parse_provides) +sub vsn ($) { + my ($t) = @_; + return "$t->{name}-".vs($t); +} + +############################################################################# +## +## FUNCTIONS: INSTALLATION INFORMATION +## +############################################################################# + +# parse OpenPKG RPM 'provide' string +# "" (virtual package) +# " = -" (regular package) +# "::