13 package Repo::generic;
16 my ($class, $attr) = @_;
18 return bless $r, $class;
22 my ($self, $ext) = @_;
23 my $path = $self->{'alias'};
25 $path .= $ext ? "_$ext.solvx" : '.solv';
27 return "/var/cache/solv/$path";
31 my ($self, $pool) = @_;
32 $self->{'handle'} = $pool->add_repo($self->{'alias'});
33 $self->{'handle'}->{'appdata'} = $self;
34 $self->{'handle'}->{'priority'} = 99 - $self->{'priority'};
35 $self->{'cookie'} = '';
36 $self->usecachedrepo();
40 my ($self, $file, $uncompress, $chksum, $markincomplete) = @_;
41 if (!$self->{'baseurl'}) {
42 print "$self->{'alias'}: no baseurl\n";
45 my $url = $self->{'baseurl'};
48 open(my $f, '+>', undef) || die;
49 fcntl($f, Fcntl::F_SETFD, 0);
50 my $st = system('curl', '-f', '-s', '-L', '-o', "/dev/fd/".fileno($f), '--', $url);
51 if (POSIX::lseek(fileno($f), 0, POSIX::SEEK_END) == 0 && ($st == 0 || !$chksum)) {
54 POSIX::lseek(fileno($f), 0, POSIX::SEEK_SET);
56 print "$file: download error #$st\n";
57 $self->{'incomplete'} = 1 if $markincomplete;
61 my $fchksum = solv::Chksum->new($chksum->{'type'});
62 $fchksum->add_fd(fileno($f));
63 if (!$fchksum->matches($chksum)) {
64 print "$file: checksum error\n";
65 $self->{'incomplete'} = 1 if $markincomplete;
70 return solv::xfopen_fd($file, POSIX::dup(fileno($f)));
72 return solv::xfopen_fd('', POSIX::dup(fileno($f)));
77 my ($self, $ext, $mark) = @_;
78 my $cookie = $ext ? $self->{'extcookie'} : $self->{'cookie'};
79 my $handle = $self->{'handle'};
80 my $cachepath = $self->cachepath();
81 if (sysopen(my $f, $cachepath, POSIX::O_RDONLY)) {
82 $f = solv::xfopen_fd('', POSIX::dup(fileno($f)));
83 my $flags = $ext ? $solv::Repo::REPO_USE_LOADING|$solv::Repo::REPO_EXTEND_SOLVABLES : 0;
84 $flags |= $solv::Repo::REPO_LOCALPOOL if $ext && $ext ne 'DL';
85 if (!$self->{'handle'}->add_solv($f, $flags)) {
97 our @ISA = ('Repo::generic');
99 package Repo::susetags;
101 our @ISA = ('Repo::generic');
103 package Repo::unknown;
105 our @ISA = ('Repo::generic');
108 my ($self, $pool) = @_;
109 print "unsupported repo '$self->{'alias'}': skipped\n";
113 package Repo::system;
115 our @ISA = ('Repo::generic');
117 sub calc_cookie_file {
118 my ($self, $filename) = @_;
119 my $chksum = solv::Chksum->new($solv::REPOKEY_TYPE_SHA256);
121 $chksum->add_stat($filename);
122 return $chksum->raw();
126 my ($self, $pool) = @_;
128 $self->{'handle'} = $pool->add_repo($self->{'alias'});
129 $self->{'handle'}->{'appdata'} = $self;
130 $pool->{'installed'} = $self->{'handle'};
131 print "rpm database: ";
132 $self->{'cookie'} = $self->calc_cookie_file('/var/lib/rpm/Packages');
133 if ($self->usecachedrepo()) {
143 my ($pool, $arch) = @_;
144 return undef unless $arch;
145 my $id = $pool->str2id($arch, 0);
146 return $id && $pool->isknownarch($id) ? 1 : undef;
150 my ($pool, $name, $globname, $globdep) = @_;
151 my $id = $pool->str2id($name, 0);
154 for my $s ($pool->providers($id)) {
155 return $pool->Job($solv::Job::SOLVER_SOLVABLE_NAME, $id) if $globname && $s->{'nameid'} == $id;
159 print "[using capability match for '$name']\n" if $globname && $globdep;
160 my @j = $pool->Job($solv::Job::SOLVER_SOLVABLE_PROVIDES, $id);
161 return $pool->Job($solv::Job::SOLVER_SOLVABLE_PROVIDES, $id);
164 return unless $name =~ /[[*?]/;
167 for my $d (@{$pool->dataiterator_new(0, $solv::SOLVABLE_NAME, $name, $solv::Dataiterator::SEARCH_GLOB)}) {
168 my $s = $d->{'solvable'};
169 $idmatches{$s->{'nameid'}} = 1 if $s->installable();
172 return map {$pool->Job($solv::Job::SOLVER_SOLVABLE_NAME, $_)} sort(keys %idmatches);
176 my @idmatches = $pool->matchprovidingids($name, $solv::Dataiterator::SEARCH_GLOB);
178 print "[using capability match for '$name']\n";
179 return map {$pool->Job($solv::Job::SOLVER_SOLVABLE_PROVIDES, $_)} sort(@idmatches);
186 my ($pool, $jobs, $flags, $evr) = @_;
189 my $how = $j->{'how'};
190 my $sel = $how & $solv::Job::SOLVER_SELECTMASK;
191 my $what = $pool->rel2id($j->{'what'}, $evr, $flags);
192 if ($flags == $solv::REL_ARCH) {
193 $how |= $solv::Job::SOLVER_SETARCH;
194 } elsif ($flags == $solv::REL_EQ && $sel == $solv::Job::SOLVER_SOLVABLE_NAME) {
195 $how |= $pool->id2str($evr) =~ /-/ ? $solv::Job::SOLVER_SETEVR : $solv::Job::SOLVER_SETEV;
197 push @jobs, $pool->Job($how, $what);
203 my ($pool, $jobs, $flags, $evrstr) = @_;
204 if ($evrstr =~ /^(.+)\.(.+?)$/ && validarch($pool, $2)) {
205 my $evr = $pool->str2id($1);
206 my @jobs = limitjobs($pool, $jobs, $solv::REL_ARCH, $pool->str2id($2));
207 return limitjobs($pool, \@jobs, $flags, $evr);
209 return limitjobs($pool, $jobs, $flags, $pool->str2id($evrstr));
213 my ($pool, $cmd, $name, $rel, $evr) = @_;
215 $flags |= $solv::REL_LT if $rel =~ /</;
216 $flags |= $solv::REL_EQ if $rel =~ /=/;
217 $flags |= $solv::REL_GT if $rel =~ />/;
218 my @jobs = depglob($pool, $name, 1, 1);
219 return limitjobs($pool, \@jobs, $flags, $pool->str2id($evr)) if @jobs;
220 if (($name =~ /^(.+)\.(.+?)$/s) && validarch($pool, $2)) {
222 @jobs = depglob($pool, $1, 1, 1);
224 @jobs = limitjobs($pool, \@jobs, $solv::REL_ARCH, $pool->str2id($arch));
225 return limitjobs($pool, \@jobs, $flags, $pool->str2id($evr));
232 my ($pool, $cmd, $arg) = @_;
233 my @jobs = depglob($pool, $arg, 1, 1);
234 return @jobs if @jobs;
235 if (($arg =~ /^(.+)\.(.+?)$/s) && validarch($pool, $2)) {
237 @jobs = depglob($pool, $1, 1, 1);
238 return limitjobs($pool, \@jobs, $solv::REL_ARCH, $pool->str2id($arch)) if @jobs;
240 if ($arg =~ /^(.+)-(.+?)$/s) {
242 @jobs = depglob($pool, $1, 1, 0);
243 return limitjobs_arch($pool, \@jobs, $solv::REL_EQ, $evr) if @jobs;
245 if ($arg =~ /^(.+)-(.+?-.+?)$/s) {
247 @jobs = depglob($pool, $1, 1, 0);
248 return limitjobs_arch($pool, \@jobs, $solv::REL_EQ, $evr) if @jobs;
253 sub mkjobs_filelist {
254 my ($pool, $cmd, $arg) = @_;
255 my $type = ($arg =~ /[[*?]/) ? $solv::Dataiterator::SEARCH_GLOB : $solv::Dataiterator::SEARCH_STRING;
256 $type |= $solv::Dataiterator::SEARCH_FILES | $solv::Dataiterator::SEARCH_COMPLETE_FILELIST;
258 if ($cmd eq 'erase') {
259 $di = $pool->{'installed'}->dataiterator_new(0, $solv::SOLVABLE_FILELIST, $arg, $type);
261 $di = $pool->dataiterator_new(0, $solv::SOLVABLE_FILELIST, $arg, $type);
265 my $s = $d->{'solvable'};
266 next unless $s && $s->installable();
267 push @matches, $s->{'id'};
268 tied(@$di)->iter()->skip_solvable();
270 return () unless @matches;
271 print "[using file list match for '$arg']\n";
273 return $pool->Job($solv::Job::SOLVER_SOLVABLE_ONE_OF, $pool->towhatprovides(\@matches));
275 return $pool->Job($solv::Job::SOLVER_SOLVABLE | $solv::Job::SOLVER_NOAUTOSET, $matches[0]);
280 my ($pool, $cmd, $arg) = @_;
281 if ($arg && $arg =~ /^\//) {
282 my @jobs = mkjobs_filelist($pool, $cmd, $arg);
283 return @jobs if @jobs;
285 if ($arg =~ /^(.+?)\s*([<=>]+)\s*(.+?)$/s) {
286 return mkjobs_rel($pool, $cmd, $1, $2, $3);
288 return mkjobs_nevra($pool, $cmd, $arg);
292 die("Usage: p5solv COMMAND [ARGS]\n") unless @ARGV;
293 my $cmd = shift @ARGV;
294 $cmd = 'list' if $cmd eq 'li';
295 $cmd = 'install' if $cmd eq 'in';
296 $cmd = 'erase' if $cmd eq 'rm';
297 $cmd = 'verify' if $cmd eq 've';
298 $cmd = 'search' if $cmd eq 'se';
300 my $pool = solv::Pool->new();
301 $pool->setarch((POSIX::uname())[4]);
303 for my $reposdir ('/etc/zypp/repos.d') {
304 next unless -d $reposdir;
305 next unless opendir(DIR, $reposdir);
306 for my $reponame (sort(grep {/\.repo$/} readdir(DIR))) {
307 my $cfg = new Config::IniFiles('-file' => "$reposdir/$reponame");
308 for my $alias ($cfg->Sections()) {
309 my $repoattr = {'alias' => $alias, 'enabled' => 0, 'priority' => 99, 'autorefresh' => 1, 'type' => 'rpm-md', 'metadata_expire' => 900};
310 for my $p ($cfg->Parameters($alias)) {
311 $repoattr->{$p} = $cfg->val($alias, $p);
314 if ($repoattr->{'type'} eq 'rpm-md') {
315 $repo = Repo::rpmmd->new($repoattr);
316 } elsif ($repoattr->{'type'} eq 'yast2') {
317 $repo = Repo::susetags->new($repoattr);
319 $repo = Repo::unknown->new($repoattr);
326 my $sysrepo = Repo::system->new({'alias' => '@System', 'type' => 'system'});
327 $sysrepo->load($pool);
328 for my $repo (@repos) {
329 $repo->load($pool) if $repo->{'enabled'};
332 if ($cmd eq 'search') {
334 my $di = $pool->dataiterator_new(0, $solv::SOLVABLE_NAME, $ARGV[0], $solv::Dataiterator::SEARCH_SUBSTRING | $solv::Dataiterator::SEARCH_NOCASE);
336 $matches{$d->{'solvid'}} = 1;
338 for my $solvid (sort keys %matches) {
339 my $s = $pool->{'solvables'}->[$solvid];
340 print "- ".$s->str()." [$s->{'repo'}->{'name'}] ".$s->lookup_str($solv::SOLVABLE_SUMMARY)."\n";
345 my @addedprovides = $pool->addfileprovides_ids();
346 $pool->createwhatprovides();
349 for my $arg (@ARGV) {
350 my @njobs = mkjobs($pool, $cmd, $arg);
351 die("nothing matches '$arg'\n") unless @njobs;
355 if ($cmd eq 'list' || $cmd eq 'info') {
356 die("no package matched.\n") unless @jobs;
357 for my $job (@jobs) {
358 for my $s ($pool->jobsolvables($job)) {
359 if ($cmd eq 'info') {
360 printf "Name: %s\n", $s->str();
361 printf "Repo: %s\n", $s->{'repo'}->{'name'};
362 printf "Summary: %s\n", $s->lookup_str($solv::SOLVABLE_SUMMARY);
363 my $str = $s->lookup_str($solv::SOLVABLE_URL);
364 printf "Url: %s\n", $str if $str;
365 $str = $s->lookup_str($solv::SOLVABLE_LICENSE);
366 printf "License: %s\n", $str if $str;
367 printf "Description:\n%s\n", $s->lookup_str($solv::SOLVABLE_DESCRIPTION);
369 printf " - %s [%s]\n", $s->str(), $s->{'repo'}->{'name'};
370 printf " %s\n", $s->lookup_str($solv::SOLVABLE_SUMMARY);
377 if ($cmd eq 'install' || $cmd eq 'erase' || $cmd eq 'up' || $cmd eq 'dup' || $cmd eq 'verify') {
379 if ($cmd eq 'up' || $cmd eq 'verify') {
380 push @jobs, $pool->Job($solv::Job::SOLVER_SOLVABLE_ALL, 0);
381 } elsif ($cmd ne 'dup') {
382 die("no package matched.\n");
385 for my $job (@jobs) {
387 if ($job->{'how'} == $solv::Job::SOLVER_SOLVABLE_ALL || grep {$_->isinstalled()} $pool->jobsolvables($job)) {
388 $job->{'how'} |= $solv::Job::SOLVER_UPDATE;
390 $job->{'how'} |= $solv::Job::SOLVER_INSTALL;
392 } elsif ($cmd eq 'install') {
393 $job->{'how'} |= $solv::Job::SOLVER_INSTALL;
394 } elsif ($cmd eq 'erase') {
395 $job->{'how'} |= $solv::Job::SOLVER_ERASE;
396 } elsif ($cmd eq 'dup') {
397 $job->{'how'} |= $solv::Job::SOLVER_DISTUPGRADE;
398 } elsif ($cmd eq 'verify') {
399 $job->{'how'} |= $solv::Job::SOLVER_VERIFY;
404 $solver = $pool->create_solver();
405 $solver->{'ignorealreadyrecommended'} = 1;
406 $solver->{'allowuninstall'} = 1 if $cmd eq 'erase';
407 if ($cmd eq 'dup' && !@jobs) {
408 $solver->{'distupgrade'} = 1;
409 $solver->{'updatesystem'} = 1;
410 $solver->{'allowdowngrade'} = 1;
411 $solver->{'allowvendorchange'} = 1;
412 $solver->{'allowarchchange'} = 1;
413 $solver->{'dosplitprovides'} = 1;
414 } elsif ($cmd eq 'up' and @jobs == 1 and $jobs[0]->{'how'} == ($solv::Job::SOLVER_UPDATE | $solv::Job::SOLVER_SOLVABLE_ALL)) {
415 $solver->{'dosplitprovides'} = 1;
417 my @problems = $solver->solve(\@jobs);
418 last unless @problems;
419 for my $problem (@problems) {
420 print "Problem $problem->{'id'}/".@problems.":\n";
421 my $r = $problem->findproblemrule();
423 print $ri->problemstr()."\n";
424 my @solutions = $problem->solutions();
425 for my $solution (@solutions) {
426 print " Solution $solution->{'id'}:\n";
427 for my $element ($solution->elements()) {
428 my $etype = $element->{'type'};
429 if ($etype == $solv::Solver::SOLVER_SOLUTION_JOB) {
430 print " - do not ask to ".$jobs[$element->{'jobidx'}]->str()."\n";
431 } elsif ($etype == $solv::Solver::SOLVER_SOLUTION_INFARCH) {
432 if ($element->{'solvable'}->isinstalled()) {
433 print " - keep ".$element->{'solvable'}->str()." despite the inferior architecture\n";
435 print " - install ".$element->{'solvable'}->str()." despite the inferior architecture\n";
437 } elsif ($etype == $solv::Solver::SOLVER_SOLUTION_DISTUPGRADE) {
438 if ($element->{'solvable'}->isinstalled()) {
439 print " - keep obsolete ".$element->{'solvable'}->str()."\n";
441 print " - install ".$element->{'solvable'}->str()." from excluded repository\n";
443 } elsif ($etype == $solv::Solver::SOLVER_SOLUTION_REPLACE) {
444 print " - allow replacement of ".$element->{'solvable'}->str()." with ".$element->{'replacement'}->str()."\n";
445 } elsif ($etype == $solv::Solver::SOLVER_SOLUTION_ERASE) {
446 print " - allow deinstallation of ".$element->{'solvable'}->str()."\n";
448 print " - allow something else\n";
454 print "Please choose a solution: ";
457 last if $sol eq 's' || $sol eq 'q' || ($sol =~ /^\d+$/ && $sol >= 1 && $sol <= @solutions);
460 exit(1) if $sol eq 'q';
461 my $solution = $solutions[$sol - 1];
462 for my $element ($solution->elements()) {
463 my $etype = $element->{'type'};
464 if ($etype == $solv::Solver::SOLVER_SOLUTION_JOB) {
465 $jobs[$element->{'jobidx'}] = $pool->Job($solv::Job::SOLVER_NOOP, 0);
467 my $newjob = $element->Job();
468 push @jobs, $newjob if $newjob && !grep {$_->{'how'} == $newjob->{'how'} && $_->{'what'} == $newjob->{'what'}} @jobs;
473 my $trans = $solver->transaction();
475 if ($trans->isempty()) {
476 print "Nothing to do.\n";
479 print "\nTransaction summary:\n\n";
480 for my $c ($trans->classify()) {
481 my ($ctype, $pkgs, $fromid, $toid) = @$c;
482 if ($ctype == $solv::Transaction::SOLVER_TRANSACTION_ERASE) {
483 printf "%d erased packages:\n", scalar(@$pkgs);
484 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_INSTALL) {
485 printf "%d installed packages:\n", scalar(@$pkgs);
486 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_REINSTALLED) {
487 printf "%d reinstalled packages:\n", scalar(@$pkgs);
488 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED) {
489 printf "%d downgraded packages:\n", scalar(@$pkgs);
490 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_CHANGED) {
491 printf "%d changed packages:\n", scalar(@$pkgs);
492 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_UPGRADED) {
493 printf "%d upgraded packages:\n", scalar(@$pkgs);
494 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_VENDORCHANGE) {
495 printf "%d vendor changes from '%s' to '%s':\n", scalar(@$pkgs), $pool->id2str($fromid), $pool->id2str($toid);
496 } elsif ($ctype == $solv::Transaction::SOLVER_TRANSACTION_ARCHCHANGE) {
497 printf "%d arch changes from '%s' to '%s':\n", scalar(@$pkgs), $pool->id2str($fromid), $pool->id2str($toid);
502 if ($ctype == $solv::Transaction::SOLVER_TRANSACTION_UPGRADED || $ctype == $solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED) {
503 my $other = $trans->othersolvable($p);
504 printf " - %s -> %s\n", $p->str(), $other->str();
506 printf " - %s\n", $p->str();
511 printf "install size change: %d K\n\n", $trans->calc_installsizechange();
513 print("OK to continue (y/n)? ");
517 exit(1) if $yn eq 'n';
519 my @newpkgs = $trans->newpackages();
522 my $downloadsize = 0;
523 $downloadsize += $_->lookup_num($solv::SOLVABLE_DOWNLOADSIZE) for @newpkgs;
524 printf "Downloading %d packages, %d K\n", scalar(@newpkgs), $downloadsize;
525 for my $p (@newpkgs) {
526 my $repo = $p->{'repo'}->{'appdata'};
527 my ($location, $medianr) = $p->lookup_location();
528 next unless $location;
529 if ($repo->{'type'} eq 'yast2') {
530 $location = ($repo->{'handle'}->lookup_str($solv::SOLVID_META, $solv::SUSETAGS_DATADIR) || 'suse') ."/$location";
532 my $chksum = $p->lookup_checksum($solv::SOLVABLE_CHECKSUM);
533 my $f = $repo->download($location, 0, $chksum);
534 die("\n$repo->{'alias'}: $location not found in repository\n") unless $f;
535 $newpkgsfps{$p->{'id'}} = $f;
541 print "Committing transaction:\n\n";
543 for my $p ($trans->steps()) {
544 my $steptype = $trans->steptype($p, $solv::Transaction::SOLVER_TRANSACTION_RPM_ONLY);
545 if ($steptype == $solv::Transaction::SOLVER_TRANSACTION_ERASE) {
546 print "erase ".$p->str()."\n";
547 next unless $p->lookup_num($solv::RPM_RPMDBID);
548 my $evr = $p->{'evr'};
549 $evr =~ s/^[0-9]+://; # strip epoch
550 system('rpm', '-e', '--nodeps', '--nodigest', '--nosignature', "$p->{'name'}-$evr.$p->{'arch'}") && die("rpm failed: $?\n");
551 } elsif ($steptype == $solv::Transaction::SOLVER_TRANSACTION_INSTALL || $steptype == $solv::Transaction::SOLVER_TRANSACTION_MULTIINSTALL) {
552 print "install ".$p->str()."\n";
553 my $f = $newpkgsfps{$p->{'id'}};
554 my $mode = $steptype == $solv::Transaction::SOLVER_TRANSACTION_INSTALL ? '-U' : '-i';
555 system('rpm', $mode, '--force', '--nodeps', '--nodigest', '--nosignature', "/dev/fd/".solv::xfileno($f)) && die("rpm failed: $?\n");
557 delete $newpkgsfps{$p->{'id'}};