#============================================================================= # Package: PPM::PPD # Purpose: Exposes a simple, object-oriented interfaces to PPDs. # Notes: # Author: Neil Watkiss #============================================================================= package PPM::PPD; use strict; use Data::Dumper; use XML::Simple (); $PPM::PPD::VERSION = '3.05'; sub new { my $this = shift; my $ppd = shift; my $rep = shift; # the repository object that retrieved this PPD my $id = shift; # the unique key in the repository of this PPD die "Error: PPM::PPD constructor called with undef ppd\n" . Dumper(caller(0)) unless defined $ppd; my $class = ref($this) || $this; my $self = bless { rep => $rep, id => $id, from => 'unknown', }, $class; $self->init($ppd); return $self; } sub repository { my $o = shift; $o->{rep}; } sub id { my $o = shift; $o->{id}; } # Whenever PPM::Repository or a subclass creates a PPM::PPD object, it sets # 'from' to "Repository". This allows the client to decide whether a PPM::PPD # object is the result of a query, a search, or a describe. Etc. sub from { my $o = shift; $o->{from}; } sub is_complete { my $o = shift; $o->{is_complete}; } sub find_impl_raw { my $o = shift; my $target = shift; for my $impl ($o->implementations) { my $match = 1; for my $field (keys %$impl) { next if ref($impl->{$field}); my $value = $target->config_get($field); if ($value && ref($value) && eval { $value->isa("PPM::Result") }) { next unless $value->is_success; $match &&= ($value->result eq $impl->{$field}); } else { next unless defined $value; $match &&= ($value eq $impl->{$field}); } } return $impl if $match == 1; } return undef; } sub find_impl { my $o = shift; my $target = shift; my $impl = $o->find_impl_raw($target); # We must not 'use' this, because the ppminst code also uses PPM::PPD, and # it doesn't have PPM::Result available. require PPM::Result; return PPM::Result::Ok($impl) if $impl; PPM::Result::Error("no suitable implementation found for '" . $o->name . "'."); } sub name { my $o = shift; my $r = $o->{parsed}{NAME}; return defined $r ? $r : ""; } sub title { my $o = shift; my $r = $o->{parsed}{TITLE}; return defined $r ? $r : ""; } sub version_osd { my $o = shift; my $r = $o->{parsed}{VERSION}; return defined $r ? $r : ""; } sub version { my $o = shift; my $v = $o->version_osd; printify($v); } sub printify { my $v = shift; $v =~ s/(?:[\.,]0)*$//; $v .= '.0' unless ($v =~ /[\.,]/ or $v eq ''); $v = "(any version)" if $v eq ''; $v =~ tr/,/./; $v; } # This sub returns 1 if $ver is >= to $o->version. It returns 0 otherwise. # Note: this is only used if the repository doesn't know how to compare # version numbers. The PPM3Server knows how to do it, the others don't. sub uptodate { my $o = shift; my $ver = shift; return 1 if $ver eq $o->version_osd; # shortcut my @required = split /[\.,]/, $o->version_osd; my @proposed = split /[\.,]/, $ver; for (my $i=0; $i<@required; $i++) { no warnings; return 0 if $proposed[$i] < $required[$i]; # too old return 1 if $proposed[$i] > $required[$i]; # even newer } return 1; # They're equal } sub abstract { my $o = shift; my $r = $o->{parsed}{ABSTRACT}; return defined $r ? $r : ""; } sub author { my $o = shift; my $r = $o->{parsed}{AUTHOR}; return defined $r ? $r : ""; } sub implementations { my $o = shift; return @{$o->{parsed}{IMPLEMENTATION} || []}; } sub ppd { my $o = shift; return $o->{ppd}; } sub init { my $o = shift; my $ppd = shift; if ($ppd =~ /{ppd} = $ppd; $o->{source} = caller; } elsif ($ppd !~ m![\n]! && -f $ppd) { $o->loadfile($ppd); $o->{source} = $ppd; } else { die "PPM::PPD::init: not a PPD and not a file:\n$ppd"; } $o->parse; } sub loadfile { my $o = shift; my $file = shift; open FILE, $file || die "can't read $file: $!"; $o->{ppd} = do { local $/; }; close FILE || die "can't close $file: $!"; } sub parse { my $o = shift; my $parser = XML::Simple->new( forcearray => 1, forcecontent => 1, keyattr => [], suppressempty => undef, ); my $tree = eval { $parser->XMLin($o->{ppd}) }; die "error: can't parse $o->{ppd}: $@" if $@; # First: SOFTPKG attributes: $o->{parsed}{NAME} = $o->conv($tree->{NAME}); $o->{parsed}{VERSION} = $o->conv($tree->{VERSION}); # Next: childless elements: $o->{parsed}{ABSTRACT} = $o->conv($tree->{ABSTRACT}[0]{content}); $o->{parsed}{AUTHOR} = $o->conv($tree->{AUTHOR}[0]{content}); $o->{parsed}{TITLE} = $o->conv($tree->{TITLE}[0]{content}); # Next: IMPLEMENTATION: my @impls; for my $impl (@{$tree->{IMPLEMENTATION}}) { my $i = PPM::PPD::Implementation->new({}); for my $key (keys %$impl) { # Next: DEPENDENCY: if ($key eq 'DEPENDENCY') { my @deps = @{$impl->{$key}}; $i->{DEPENDENCY} = [map { PPM::PPD::Dependency->new($_) } @deps]; next; } # Next: LANGUAGE: if ($key eq 'LANGUAGE') { my $v = $impl->{$key}[0]; my $lang = { NAME => $o->conv($v->{NAME}), VERSION => $o->conv( $v->{COMPAT}[0]{VERSION} || $v->{VERSION} ), TYPE => $o->conv($v->{COMPAT}[0]{TYPE}), }; $i->{LANGUAGE} = PPM::PPD::Language->new($lang); next; } # Next: INSTALL or UNINSTALL. if ($key eq 'INSTALL' or $key eq 'UNINSTALL') { my $v = $impl->{$key}[0]; $i->{"${key}_SCRIPT"} = PPM::PPD::Script->new({ EXEC => $o->conv($v->{EXEC}), HREF => $o->conv($v->{HREF}), SCRIPT => $o->conv($v->{content}), }); } # Next: CODEBASE, OS, OSVERSION, etc. my @keys = qw(NAME VALUE); push @keys, qw(HREF) if $key eq 'CODEBASE'; for (@keys) { next unless exists $impl->{$key}[0]{$_}; $i->{$key} = $o->conv($impl->{$key}[0]{$_}); last; } } push @impls, $i; } $o->{parsed}{IMPLEMENTATION} = \@impls; $o->{is_complete} = @impls; } sub conv { use Unicode::String qw(utf8); my $o = shift; my $u = utf8(shift(@_) || ''); my $use_utf8 = 0; for my $env (qw(LC_ALL LC_CTYPE LANG PPM_LANG)) { $use_utf8 = 1, last if $ENV{$env} and $ENV{$env} =~ /UTF-8/; } # silence "Data outside latin1 range" warnings local $SIG{__WARN__} = sub {}; $u->stringify_as('latin1') unless $use_utf8; "$u"; } package PPM::PPD::Base; sub new { my $cls = shift; my $obj = shift; bless $obj, $cls; } sub AUTOLOAD { my $method = $PPM::PPD::Base::AUTOLOAD; $method =~ s/^.+:://; my $o = shift; my $r = $o->{uc($method)}; defined $r ? $r : ''; } sub version_printable { die } sub osversion_printable { die } #============================================================================= # PPM::PPD::Implementation. # Exposes the following methods: # # architecture # codebase # os # osversion_osd # osversion # perlcore # install_script # uninstall_script # pythoncore # # prereqs # returns a list of PPM::PPD::Dependency objects # language # returns a PPM::PPD::Language object #============================================================================= package PPM::PPD::Implementation; our @ISA = qw(PPM::PPD::Base); sub osversion_osd { my $o = shift; my $r = $o->{OSVERSION}; defined $r ? $r : ''; } sub osversion { my $o = shift; my $r = $o->osversion_osd; PPM::PPD::printify($r); } sub prereqs { my $o = shift; return @{$o->{DEPENDENCY} || []}; } sub language { my $o = shift; $o->{LANGUAGE}; } #============================================================================= # PPM::PPD::Script # Exposes the following methods: # # exec # a shell/interpreter to use to run the script # href # a script to download # script # the content of the script (if href not specified) #============================================================================= package PPM::PPD::Script; our @ISA = qw(PPM::PPD::Base); #============================================================================= # PPM::PPD::Language. # Exposes the following methods: # # name # version # no OSD version for LANGUAGE tag # type # one of 'SYNTAX' or 'BINARY' # # matches_target($target) # returns 1 if $target can install PPD, else 0 #============================================================================= package PPM::PPD::Language; our @ISA = qw(PPM::PPD::Base); sub matches_target { my $o = shift; my $t = shift; $t->can_install($o->name, $o->version, $o->type); } #============================================================================= # PPM::PPD::Dependency. # Exposes the following methods: # # name # version # version_osd # # uptodate($ppd) # returns 1 if the given PPM::PPD object satisfies the # # dependency, or 0 otherwise. #============================================================================= package PPM::PPD::Dependency; our @ISA = qw(PPM::PPD::Base); sub version_osd { my $o = shift; my $r = $o->{VERSION}; defined $r ? $r : ''; } sub version { goto &PPM::PPD::version; } sub uptodate { goto &PPM::PPD::uptodate; } package PPM::PPD::Search; @PPM::PPD::Search::ISA = 'PPM::Search'; use Data::Dumper; sub matchimpl { my $self = shift; my ($impl, $field, $re) = @_; if ($field eq 'OS') { return $impl->os =~ $re } elsif ($field eq 'OSVERSION') { return $impl->osversion =~ $re } elsif ($field eq 'ARCHITECTURE') { return $impl->architecture =~ $re} elsif ($field eq 'CODEBASE') { return $impl->codebase =~ $re } elsif ($field eq 'PYTHONCORE') { return $impl->pythoncore =~ $re } elsif ($field eq 'PERLCORE') { return $impl->perlcore =~ $re } else { warn "unknown search field '$field'" if $^W; } } sub match { my $self = shift; my ($ppd, $field, $match) = @_; my $re = qr/$match/; $field = uc($field); if ($field eq 'NAME') { return $ppd->name =~ $re } if ($field eq 'AUTHOR') { return $ppd->author =~ $re } if ($field eq 'ABSTRACT') { return $ppd->abstract =~ $re } if ($field eq 'TITLE') { return $ppd->title =~ $re } if ($field eq 'VERSION') { return $ppd->version_printable =~ $re } return (grep { $_ } map { $self->matchimpl($_, $field, $re) } $ppd->implementations); } 1;