- commit current state of bindings
[platform/upstream/libsolv.git] / examples / p5solv
1 #!/usr/bin/perl -w
2
3 use POSIX;
4 use Fcntl;
5 use Config::IniFiles;
6 use File::FnMatch;
7 use Data::Dumper;
8 use solv;
9 use Devel::Peek;
10 use FileHandle;
11 use strict;
12
13 package Repo::generic;
14
15 sub new {
16   my ($class, $attr) = @_;
17   my $r = { %$attr };
18   return bless $r, $class;
19 }
20
21 sub cachepath {
22   my ($self, $ext) = @_;
23   my $path = $self->{'alias'};
24   $path =~ s/^\./_/s;
25   $path .= $ext ? "_$ext.solvx" : '.solv';
26   $path =~ s/\//_/gs;
27   return "/var/cache/solv/$path";
28 }
29
30 sub load {
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();
37 }
38
39 sub download {
40   my ($self, $file, $uncompress, $chksum, $markincomplete) = @_;
41   if (!$self->{'baseurl'}) {
42     print "$self->{'alias'}: no baseurl\n";
43     return undef;
44   }
45   my $url = $self->{'baseurl'};
46   $url =~ s/\/$//;
47   $url .= "/$file";
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)) {
52     return undef;
53   }
54   POSIX::lseek(fileno($f), 0, POSIX::SEEK_SET);
55   if ($st) {
56     print "$file: download error #$st\n";
57     $self->{'incomplete'} = 1 if $markincomplete;
58     return undef;
59   }
60   if ($chksum) {
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;
66       return undef;
67     }
68   }
69   if ($uncompress) {
70     return solv::xfopen_fd($file, POSIX::dup(fileno($f)));
71   } else {
72     return solv::xfopen_fd('', POSIX::dup(fileno($f)));
73   }
74 }
75
76 sub usecachedrepo {
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)) {
86       solv::xfclose($f);
87       return undef;
88     }
89     solv::xfclose($f);
90     return 1;
91   }
92   return undef;
93 }
94
95 package Repo::rpmmd;
96
97 our @ISA = ('Repo::generic');
98
99 package Repo::susetags;
100
101 our @ISA = ('Repo::generic');
102
103 package Repo::unknown;
104
105 our @ISA = ('Repo::generic');
106
107 sub load {
108   my ($self, $pool) = @_;
109   print "unsupported repo '$self->{'alias'}': skipped\n";
110   return 0;
111 }
112
113 package Repo::system;
114
115 our @ISA = ('Repo::generic');
116
117 sub calc_cookie_file {
118   my ($self, $filename) = @_;
119   my $chksum = solv::Chksum->new($solv::REPOKEY_TYPE_SHA256);
120   $chksum->add("1.1");
121   $chksum->add_stat($filename);
122   return $chksum->raw();
123 }
124
125 sub load {
126   my ($self, $pool) = @_;
127
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()) {
134     print "cached\n";
135     return 1;
136   }
137   return undef;
138 }
139
140 package main;
141
142 sub validarch {
143   my ($pool, $arch) = @_;
144   return undef unless $arch;
145   my $id = $pool->str2id($arch, 0);
146   return $id && $pool->isknownarch($id) ? 1 : undef;
147 }
148
149 sub depglob {
150   my ($pool, $name, $globname, $globdep) = @_;
151   my $id = $pool->str2id($name, 0);
152   if ($id) {
153     my $match;
154     for my $s ($pool->providers($id)) {
155       return $pool->Job($solv::Job::SOLVER_SOLVABLE_NAME, $id) if $globname && $s->{'nameid'} == $id;
156       $match = 1;
157     }
158     if ($match) {
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);
162     }
163   }
164   return unless $name =~ /[[*?]/;
165   if ($globname) {
166     my %idmatches;
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();
170     }
171     if (%idmatches) {
172       return map {$pool->Job($solv::Job::SOLVER_SOLVABLE_NAME, $_)} sort(keys %idmatches);
173     }
174   }
175   if ($globdep) {
176     my @idmatches = $pool->matchprovidingids($name, $solv::Dataiterator::SEARCH_GLOB);
177     if (@idmatches) {
178       print "[using capability match for '$name']\n";
179       return map {$pool->Job($solv::Job::SOLVER_SOLVABLE_PROVIDES, $_)} sort(@idmatches);
180     }
181   }
182   return;
183 }
184
185 sub limitjobs {
186   my ($pool, $jobs, $flags, $evr) = @_;
187   my @jobs;
188   for my $j (@$jobs) {
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;
196     }
197     push @jobs, $pool->Job($how, $what);
198   }
199   return @jobs;
200 }
201
202 sub limitjobs_arch {
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);
208   }
209   return limitjobs($pool, $jobs, $flags, $pool->str2id($evrstr));
210 }
211
212 sub mkjobs_rel {
213   my ($pool, $cmd, $name, $rel, $evr) = @_;
214   my $flags = 0;
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)) {
221     my $arch = $2;
222     @jobs = depglob($pool, $1, 1, 1);
223     if (@jobs) {
224       @jobs = limitjobs($pool, \@jobs, $solv::REL_ARCH, $pool->str2id($arch));
225       return limitjobs($pool, \@jobs, $flags, $pool->str2id($evr));
226     }
227   }
228   return ();
229 }
230
231 sub mkjobs_nevra {
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)) {
236     my $arch = $2;
237     @jobs = depglob($pool, $1, 1, 1);
238     return limitjobs($pool, \@jobs, $solv::REL_ARCH, $pool->str2id($arch)) if @jobs;
239   }
240   if ($arg =~ /^(.+)-(.+?)$/s) {
241     my $evr = $2;
242     @jobs = depglob($pool, $1, 1, 0);
243     return limitjobs_arch($pool, \@jobs, $solv::REL_EQ, $evr) if @jobs;
244   }
245   if ($arg =~ /^(.+)-(.+?-.+?)$/s) {
246     my $evr = $2;
247     @jobs = depglob($pool, $1, 1, 0);
248     return limitjobs_arch($pool, \@jobs, $solv::REL_EQ, $evr) if @jobs;
249   }
250   return ();
251 }
252
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;
257   my $di;
258   if ($cmd eq 'erase') {
259     $di = $pool->{'installed'}->dataiterator_new(0, $solv::SOLVABLE_FILELIST, $arg, $type);
260   } else {
261     $di = $pool->dataiterator_new(0, $solv::SOLVABLE_FILELIST, $arg, $type);
262   }
263   my @matches;
264   for my $d (@$di) {
265     my $s = $d->{'solvable'};
266     next unless $s && $s->installable();
267     push @matches, $s->{'id'};
268     tied(@$di)->iter()->skip_solvable();
269   }
270   return () unless @matches;
271   print "[using file list match for '$arg']\n";
272   if (@matches > 1) {
273     return $pool->Job($solv::Job::SOLVER_SOLVABLE_ONE_OF, $pool->towhatprovides(\@matches));
274   } else {
275     return $pool->Job($solv::Job::SOLVER_SOLVABLE | $solv::Job::SOLVER_NOAUTOSET, $matches[0]);
276   }
277 }
278
279 sub mkjobs {
280   my ($pool, $cmd, $arg) = @_;
281   if ($arg && $arg =~ /^\//) {
282     my @jobs = mkjobs_filelist($pool, $cmd, $arg);
283     return @jobs if @jobs;
284   }
285   if ($arg =~ /^(.+?)\s*([<=>]+)\s*(.+?)$/s) {
286     return mkjobs_rel($pool, $cmd, $1, $2, $3);
287   } else {
288     return mkjobs_nevra($pool, $cmd, $arg);
289   }
290 }
291
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';
299
300 my $pool = solv::Pool->new();
301 $pool->setarch((POSIX::uname())[4]);
302 my @repos;
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);
312       }
313       my $repo;
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);
318       } else {
319         $repo = Repo::unknown->new($repoattr);
320       }
321       push @repos, $repo;
322     }
323   }
324 }
325
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'};
330 }
331
332 if ($cmd eq 'search') {
333   my %matches;
334   my $di = $pool->dataiterator_new(0, $solv::SOLVABLE_NAME, $ARGV[0], $solv::Dataiterator::SEARCH_SUBSTRING | $solv::Dataiterator::SEARCH_NOCASE);
335   for my $d (@$di) {
336     $matches{$d->{'solvid'}} = 1;
337   }
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";
341   }
342   exit(0);
343 }
344
345 my @addedprovides =  $pool->addfileprovides_ids();
346 $pool->createwhatprovides();
347
348 my @jobs;
349 for my $arg (@ARGV) {
350   my @njobs = mkjobs($pool, $cmd, $arg);
351   die("nothing matches '$arg'\n") unless @njobs;
352   push @jobs, @njobs;
353 }
354
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);
368       } else {
369         printf "  - %s [%s]\n", $s->str(), $s->{'repo'}->{'name'};
370         printf "    %s\n", $s->lookup_str($solv::SOLVABLE_SUMMARY);
371       }
372     }
373   }
374   exit 0;
375 }
376
377 if ($cmd eq 'install' || $cmd eq 'erase' || $cmd eq 'up' || $cmd eq 'dup' || $cmd eq 'verify') {
378   if (!@jobs) {
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");
383     }
384   }
385   for my $job (@jobs) {
386     if ($cmd eq 'up') {
387       if ($job->{'how'} == $solv::Job::SOLVER_SOLVABLE_ALL || grep {$_->isinstalled()} $pool->jobsolvables($job)) {
388         $job->{'how'} |= $solv::Job::SOLVER_UPDATE;
389       } else {
390         $job->{'how'} |= $solv::Job::SOLVER_INSTALL;
391       }
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;
400     }
401   }
402   my $solver;
403   while (1) {
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;
416     }
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();
422       my $ri = $r->info();
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";
434             } else {
435               print "  - install ".$element->{'solvable'}->str()." despite the inferior architecture\n";
436             }
437           } elsif ($etype == $solv::Solver::SOLVER_SOLUTION_DISTUPGRADE) {
438             if ($element->{'solvable'}->isinstalled()) {
439               print "  - keep obsolete ".$element->{'solvable'}->str()."\n";
440             } else {
441               print "  - install ".$element->{'solvable'}->str()." from excluded repository\n";
442             }
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";
447           } else {
448             print "  - allow something else\n";
449           }
450         }
451       }
452       my $sol;
453       while (1) {
454         print "Please choose a solution: ";
455         $sol = <STDIN>;
456         chomp $sol;
457         last if $sol eq 's' || $sol eq 'q' || ($sol =~ /^\d+$/ && $sol >= 1 && $sol <= @solutions);
458       }
459       next if $sol eq 's';
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);
466         } else {
467           my $newjob = $element->Job();
468           push @jobs, $newjob if $newjob && !grep {$_->{'how'} == $newjob->{'how'} && $_->{'what'} == $newjob->{'what'}} @jobs;
469         }
470       }
471     }
472   }
473   my $trans = $solver->transaction();
474   undef $solver;
475   if ($trans->isempty()) {
476     print "Nothing to do.\n";
477     exit 0;
478   }
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);
498     } else {
499       next;
500     }
501     for my $p (@$pkgs) {
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();
505       } else {
506         printf "  - %s\n", $p->str();
507       }
508     }
509     print "\n";
510   }
511   printf "install size change: %d K\n\n", $trans->calc_installsizechange();
512   while (1) {
513     print("OK to continue (y/n)? ");
514     my $yn = <STDIN>;
515     chomp $yn;
516     last if $yn eq 'y';
517     exit(1) if $yn eq 'n';
518   }
519   my @newpkgs = $trans->newpackages();
520   my %newpkgsfps;
521   if (@newpkgs) {
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";
531       }
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;
536       print ".";
537       STDOUT->flush();
538     }
539     print "\n";
540   }
541   print "Committing transaction:\n\n";
542   $trans->order(0);
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");
556       solv::xfclose($f);
557       delete $newpkgsfps{$p->{'id'}};
558     }
559   }
560 }
561
562 exit 0;