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