]> git.ipfire.org Git - people/stevee/pakfire.git/blame - python/pakfire/base.py
Replace the dependency tracker.
[people/stevee/pakfire.git] / python / pakfire / base.py
CommitLineData
47a4cb89 1#!/usr/bin/python
b792d887
MT
2###############################################################################
3# #
4# Pakfire - The IPFire package management system #
5# Copyright (C) 2011 Pakfire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
47a4cb89 21
7c8f2953
MT
22import os
23import random
24import string
25
0ca71090 26import actions
18edfe75 27import builder
53bb7960 28import config
7c8f2953 29import distro
862bea4d 30import filelist
7c8f2953 31import logger
ae20b05f 32import packages
0ca71090 33import repository
c605d735 34import satsolver
0ca71090 35import transaction
7c8f2953
MT
36import util
37
8b6bc023
MT
38import logging
39log = logging.getLogger("pakfire")
40
7c8f2953 41from constants import *
7c8f2953
MT
42from i18n import _
43
7c8f2953 44class Pakfire(object):
018127aa
MT
45 RELATIONS = (
46 (">=", satsolver.REL_GE,),
47 ("<=", satsolver.REL_LE,),
48 ("=" , satsolver.REL_EQ,),
49 ("<" , satsolver.REL_LT,),
50 (">" , satsolver.REL_GT,),
51 )
52
eb34496a 53 def __init__(self, mode=None, path="/", configs=[],
715d7009 54 enable_repos=None, disable_repos=None,
6a509182 55 distro_config=None, **kwargs):
6557ff4c
MT
56
57 # Set the mode.
6a509182 58 assert mode in ("normal", "builder", "server",)
6557ff4c
MT
59 self.mode = mode
60
7c8f2953
MT
61 # Check if we are operating as the root user.
62 self.check_root_user()
63
64 # The path where we are operating in.
6557ff4c
MT
65 self.path = path
66
67 # Configure the instance of Pakfire we just started.
68 if mode == "builder":
7c8f2953 69 self.path = os.path.join(BUILD_ROOT, util.random_string())
7c8f2953 70
6557ff4c 71 elif mode == "normal":
7b1e25fd 72 # check if we are actually running on an ipfire system.
0891edd2
MT
73 if self.path == "/":
74 self.check_is_ipfire()
7c8f2953
MT
75
76 # Read configuration file(s)
6557ff4c 77 self.config = config.Config(type=mode)
7c8f2953
MT
78 for filename in configs:
79 self.config.read(filename)
6a509182
MT
80 # Assume, that all other keyword arguments are configuration
81 # parameters.
82 self.config.update(kwargs)
7c8f2953
MT
83
84 # Setup the logger
85 logger.setup_logging(self.config)
86 self.config.dump()
87
88 # Get more information about the distribution we are running
89 # or building
53bb7960 90 self.distro = distro.Distribution(self, distro_config)
c605d735 91 self.pool = satsolver.Pool(self.distro.arch)
ae20b05f
MT
92 self.repos = repository.Repositories(self,
93 enable_repos=enable_repos, disable_repos=disable_repos)
7c8f2953 94
60845a36
MT
95 def __del__(self):
96 # Reset logging.
97 logger.setup_logging()
98
c605d735
MT
99 def create_solver(self):
100 return satsolver.Solver(self, self.pool)
101
063606f6
MT
102 def create_request(self, builder=False):
103 request = satsolver.Request(self.pool)
104
105 # Add multiinstall information.
106 for solv in PAKFIRE_MULTIINSTALL:
107 request.noobsoletes(solv)
108
109 return request
7c8f2953 110
018127aa
MT
111 def create_relation(self, s):
112 assert s
113
862bea4d
MT
114 if isinstance(s, filelist._File):
115 return satsolver.Relation(self.pool, s.name)
116
117 elif s.startswith("/"):
018127aa
MT
118 return satsolver.Relation(self.pool, s)
119
120 for pattern, type in self.RELATIONS:
121 if not pattern in s:
122 continue
123
124 name, version = s.split(pattern, 1)
125
63029754
MT
126 # Trim spaces from strings.
127 name = name.strip()
128 version = version.strip()
129
018127aa
MT
130 return satsolver.Relation(self.pool, name, version, type)
131
132 return satsolver.Relation(self.pool, s)
133
7c8f2953
MT
134 def destroy(self):
135 if not self.path == "/":
136 util.rm(self.path)
137
c07a3ca7
MT
138 @property
139 def environ(self):
140 env = {}
141
142 # Get distribution information.
143 env.update(self.distro.environ)
144
145 return env
146
7c8f2953
MT
147 @property
148 def supported_arches(self):
3ad4bb5a 149 return self.config.supported_arches
7c8f2953 150
6a509182
MT
151 @property
152 def offline(self):
153 """
154 A shortcut that indicates if the system is running in offline mode.
155 """
156 return self.config.get("offline", False)
157
7c8f2953
MT
158 def check_root_user(self):
159 if not os.getuid() == 0 or not os.getgid() == 0:
160 raise Exception, "You must run pakfire as the root user."
161
162 def check_build_mode(self):
163 """
164 Check if we are running in build mode.
165 Otherwise, raise an exception.
166 """
6557ff4c 167 if not self.mode == "builder":
7c8f2953
MT
168 raise BuildError, "Cannot build when not in build mode."
169
170 def check_host_arch(self, arch):
171 """
172 Check if we can build for arch.
173 """
7c8f2953
MT
174 # If no arch was given on the command line we build for our
175 # own arch which should always work.
176 if not arch:
177 return True
178
3ad4bb5a 179 if not self.config.host_supports_arch(arch):
7c8f2953
MT
180 raise BuildError, "Cannot build for the target architecture: %s" % arch
181
182 raise BuildError, arch
18edfe75 183
7b1e25fd 184 def check_is_ipfire(self):
c07a3ca7
MT
185 return # XXX disabled for now
186
7b1e25fd
MT
187 ret = os.path.exists("/etc/ipfire-release")
188
189 if not ret:
190 raise NotAnIPFireSystemError, "You can run pakfire only on an IPFire system"
191
6557ff4c
MT
192 @property
193 def builder(self):
194 # XXX just backwards compatibility
195 return self.mode == "builder"
196
b25a3d84
MT
197 def resolvdep(self, requires):
198 # Create a new request.
199 request = self.create_request()
200 for req in requires:
201 req = self.create_relation(req)
202 request.install(req)
203
204 # Do the solving.
205 solver = self.create_solver()
206 t = solver.solve(request)
207
208 if t:
209 t.dump()
210 else:
8b6bc023 211 log.info(_("Nothing to do"))
b25a3d84 212
c07a3ca7
MT
213 def install(self, requires, interactive=True, logger=None, **kwargs):
214 if not logger:
8b6bc023 215 logger = logging.getLogger("pakfire")
c07a3ca7 216
ae20b05f 217 # Create a new request.
84680c15 218 request = self.create_request()
c07a3ca7
MT
219
220 # Expand all groups.
18edfe75 221 for req in requires:
c07a3ca7
MT
222 if req.startswith("@"):
223 reqs = self.grouplist(req[1:])
224 else:
225 reqs = [req,]
226
227 for req in reqs:
228 if not isinstance(req, packages.BinaryPackage):
229 req = self.create_relation(req)
230
231 request.install(req)
18edfe75 232
ae20b05f 233 # Do the solving.
84680c15 234 solver = self.create_solver()
c07a3ca7 235 t = solver.solve(request, **kwargs)
18edfe75 236
ae20b05f 237 if not t:
c07a3ca7
MT
238 if not interactive:
239 raise DependencyError
240
8b6bc023 241 log.info(_("Nothing to do"))
18edfe75
MT
242 return
243
c07a3ca7
MT
244 if interactive:
245 # Ask if the user acknowledges the transaction.
246 if not t.cli_yesno():
247 return
248
249 else:
250 t.dump(logger=logger)
c0fd807c
MT
251
252 # Run the transaction.
ae20b05f 253 t.run()
18edfe75 254
325e6065 255 def localinstall(self, files, yes=None, allow_uninstall=False):
de7fba8f 256 repo_name = repo_desc = "localinstall"
e0b99370
MT
257
258 # Create a new repository that holds all packages we passed on
259 # the commandline.
022c792a
MT
260 repo = repository.RepositoryDir(self, repo_name, repo_desc,
261 os.path.join(LOCAL_TMP_PATH, "repo_%s" % util.random_string()))
e0b99370 262
022c792a
MT
263 # Register the repository.
264 self.repos.add_repo(repo)
e0b99370 265
022c792a
MT
266 try:
267 # Add all packages to the repository index.
268 for file in files:
269 repo.collect_packages(file)
e0b99370 270
022c792a
MT
271 # Break if no packages were added at all.
272 if not len(repo):
8b6bc023 273 log.critical(_("There are no packages to install."))
022c792a 274 return
e0b99370 275
022c792a
MT
276 # Create a new request that installs all solvables from the
277 # repository.
278 request = self.create_request()
279 for solv in [p.solvable for p in repo]:
280 request.install(solv)
e0b99370 281
022c792a 282 solver = self.create_solver()
325e6065 283 t = solver.solve(request, uninstall=allow_uninstall)
e0b99370 284
022c792a
MT
285 # If solving was not possible, we exit here.
286 if not t:
8b6bc023 287 log.info(_("Nothing to do"))
022c792a 288 return
c0fd807c 289
08d60af8
MT
290 if yes is None:
291 # Ask the user if this is okay.
292 if not t.cli_yesno():
293 return
294 elif yes:
295 t.dump()
296 else:
022c792a
MT
297 return
298
299 # If okay, run the transcation.
300 t.run()
301
302 finally:
303 # Remove the temporary copy of the repository we have created earlier.
304 repo.remove()
75bb74a7 305 self.repos.rem_repo(repo)
e0b99370 306
0ca71090
MT
307 def reinstall(self, pkgs, strict=False):
308 """
309 Reinstall one or more packages.
310
311 If strict is True, only a package with excatly the same UUID
312 will replace the currently installed one.
313 """
314 # XXX it is possible to install packages without fulfulling
315 # all dependencies.
316
317 reinstall_pkgs = []
318 for pattern in pkgs:
319 _pkgs = []
320 for pkg in self.repos.whatprovides(pattern):
321 # Do not reinstall non-installed packages.
322 if not pkg.is_installed():
323 continue
324
325 _pkgs.append(pkg)
326
327 if not _pkgs:
8b6bc023 328 log.warning(_("Could not find any installed package providing \"%s\".") \
0ca71090
MT
329 % pattern)
330 elif len(_pkgs) == 1:
331 reinstall_pkgs.append(_pkgs[0])
332 #t.add("reinstall", _pkgs[0])
333 else:
8b6bc023 334 log.warning(_("Multiple reinstall candidates for \"%s\": %s") \
0ca71090
MT
335 % (pattern, ", ".join(p.friendly_name for p in sorted(_pkgs))))
336
337 if not reinstall_pkgs:
8b6bc023 338 log.info(_("Nothing to do"))
0ca71090
MT
339 return
340
341 # Packages we want to replace.
342 # Contains a tuple with the old and the new package.
343 pkgs = []
344
345 # Find the package that is installed in a remote repository to
346 # download it again and re-install it. We need that.
347 for pkg in reinstall_pkgs:
348 # Collect all candidates in here.
349 _pkgs = []
350
351 provides = "%s=%s" % (pkg.name, pkg.friendly_version)
352 for _pkg in self.repos.whatprovides(provides):
353 if _pkg.is_installed():
354 continue
355
356 if strict:
357 if pkg.uuid == _pkg.uuid:
358 _pkgs.append(_pkg)
359 else:
360 _pkgs.append(_pkg)
361
362 if not _pkgs:
8b6bc023 363 log.warning(_("Could not find package %s in a remote repository.") % \
0ca71090
MT
364 pkg.friendly_name)
365 else:
366 # Sort packages to reflect repository priorities, etc...
367 # and take the best (first) one.
368 _pkgs.sort()
369
370 # Re-install best package and cleanup the old one.
371 pkgs.append((pkg, _pkgs[0]))
372
373 # Eventually, create a request.
374 request = None
375
376 _pkgs = []
377 for old, new in pkgs:
378 if old.uuid == new.uuid:
379 _pkgs.append((old, new))
380 else:
381 if request is None:
382 # Create a new request.
383 request = self.create_request()
384
385 # Install the new package, the old will
386 # be cleaned up automatically.
387 request.install(new.solvable)
388
389 if request:
390 solver = self.create_solver()
391 t = solver.solve(request)
392 else:
393 # Create new transaction.
394 t = transaction.Transaction(self)
395
396 for old, new in _pkgs:
397 # Install the new package and remove the old one.
398 t.add(actions.ActionReinstall.type, new)
399 t.add(actions.ActionCleanup.type, old)
400
401 t.sort()
402
403 if not t:
8b6bc023 404 log.info(_("Nothing to do"))
0ca71090
MT
405 return
406
407 if not t.cli_yesno():
408 return
409
410 t.run()
411
3817ae8e 412 def update(self, pkgs=None, check=False, excludes=None, interactive=True, logger=None, **kwargs):
e38914be
MT
413 """
414 check indicates, if the method should return after calculation
415 of the transaction.
416 """
84680c15 417 request = self.create_request()
0c1a80b8 418
6c395339
MT
419 # If there are given any packets on the command line, we will
420 # only update them. Otherwise, we update the whole system.
421 if pkgs:
422 update = False
423 for pkg in pkgs:
b34c426d 424 pkg = self.create_relation(pkg)
6c395339
MT
425 request.update(pkg)
426 else:
427 update = True
0c1a80b8 428
8834f7c9
MT
429 # Exclude packages that should not be updated.
430 if excludes:
431 for exclude in excludes:
8b6bc023 432 log.info(_("Excluding %s.") % exclude)
8834f7c9
MT
433
434 exclude = self.create_relation(exclude)
435 request.lock(exclude)
436
84680c15 437 solver = self.create_solver()
3817ae8e 438 t = solver.solve(request, update=update, **kwargs)
0c1a80b8
MT
439
440 if not t:
8b6bc023 441 log.info(_("Nothing to do"))
e38914be
MT
442
443 # If we are running in check mode, we return a non-zero value to
444 # indicate, that there are no updates.
445 if check:
446 return 1
447 else:
448 return
449
450 # Just exit here, because we won't do the transaction in this mode.
451 if check:
3817ae8e 452 t.dump(logger=logger)
0c1a80b8
MT
453 return
454
c0fd807c 455 # Ask the user if the transaction is okay.
3817ae8e 456 if interactive and not t.cli_yesno():
c0fd807c 457 return
0c1a80b8 458
c0fd807c 459 # Run the transaction.
0c1a80b8 460 t.run()
18edfe75 461
67d1ddbd
MT
462 def downgrade(self, pkgs, allow_vendorchange=False, allow_archchange=False):
463 assert pkgs
464
465 # Create a new request.
466 request = self.create_request()
467
468 # Fill request.
469 for pattern in pkgs:
470 best = None
471 for pkg in self.repos.whatprovides(pattern):
472 # Only consider installed packages.
473 if not pkg.is_installed():
474 continue
475
476 if best and pkg > best:
477 best = pkg
478 elif best is None:
479 best = pkg
480
481 if best is None:
8b6bc023 482 log.warning(_("\"%s\" package does not seem to be installed.") % pattern)
67d1ddbd
MT
483 else:
484 rel = self.create_relation("%s<%s" % (best.name, best.friendly_version))
485 request.install(rel)
486
487 # Solve the request.
488 solver = self.create_solver()
489 t = solver.solve(request, allow_downgrade=True,
490 allow_vendorchange=allow_vendorchange,
491 allow_archchange=allow_archchange)
492
493 if not t:
8b6bc023 494 log.info(_("Nothing to do"))
67d1ddbd
MT
495 return
496
497 if not t.cli_yesno():
498 return
499
500 t.run()
501
a39fd08b
MT
502 def remove(self, pkgs):
503 # Create a new request.
84680c15 504 request = self.create_request()
a39fd08b 505 for pkg in pkgs:
b34c426d 506 pkg = self.create_relation(pkg)
a39fd08b
MT
507 request.remove(pkg)
508
509 # Solve the request.
84680c15 510 solver = self.create_solver()
6eb8f338 511 t = solver.solve(request, uninstall=True)
a39fd08b
MT
512
513 if not t:
8b6bc023 514 log.info(_("Nothing to do"))
a39fd08b
MT
515 return
516
c0fd807c
MT
517 # Ask the user if okay.
518 if not t.cli_yesno():
519 return
520
521 # Process the transaction.
a39fd08b
MT
522 t.run()
523
c1e7f927 524 def info(self, patterns):
18edfe75
MT
525 pkgs = []
526
1f27e8fe
MT
527 # For all patterns we run a single search which returns us a bunch
528 # of solvables which are transformed into Package objects.
18edfe75 529 for pattern in patterns:
5dda5744 530 if os.path.exists(pattern) and not os.path.isdir(pattern):
c07a3ca7
MT
531 pkg = packages.open(self, self.repos.dummy, pattern)
532 if pkg:
533 pkgs.append(pkg)
1f27e8fe 534
c07a3ca7
MT
535 else:
536 solvs = self.pool.search(pattern, satsolver.SEARCH_GLOB, "solvable:name")
e1972777 537
c07a3ca7
MT
538 for solv in solvs:
539 pkg = packages.SolvPackage(self, solv)
540 if pkg in pkgs:
541 continue
542
543 pkgs.append(pkg)
18edfe75 544
e1972777 545 return sorted(pkgs)
18edfe75
MT
546
547 def search(self, pattern):
548 # Do the search.
c2e67297 549 pkgs = {}
884cd2fb 550 for solv in self.pool.search(pattern, satsolver.SEARCH_STRING|satsolver.SEARCH_FILES):
c2e67297 551 pkg = packages.SolvPackage(self, solv)
18edfe75 552
c2e67297
MT
553 # Check, if a package with the name is already in the resultset
554 # and always replace older ones by more recent ones.
555 if pkgs.has_key(pkg.name):
556 if pkgs[pkg.name] < pkg:
557 pkgs[pkg.name] = pkg
558 else:
559 pkgs[pkg.name] = pkg
560
561 # Return a list of the packages, alphabetically sorted.
562 return sorted(pkgs.values())
18edfe75 563
c07a3ca7
MT
564 def groupinstall(self, group, **kwargs):
565 self.install("@%s" % group, **kwargs)
18edfe75
MT
566
567 def grouplist(self, group):
fec9a917
MT
568 pkgs = []
569
570 for solv in self.pool.search(group, satsolver.SEARCH_SUBSTRING, "solvable:group"):
571 pkg = packages.SolvPackage(self, solv)
18edfe75 572
fec9a917
MT
573 if group in pkg.groups and not pkg.name in pkgs:
574 pkgs.append(pkg.name)
18edfe75 575
fec9a917 576 return sorted(pkgs)
18edfe75
MT
577
578 @staticmethod
1710ccec 579 def build(pkg, resultdirs=None, shell=False, install_test=True, after_shell=False, **kwargs):
18edfe75
MT
580 if not resultdirs:
581 resultdirs = []
582
ff9299d0 583 b = builder.BuildEnviron(pkg, **kwargs)
18edfe75
MT
584 p = b.pakfire
585
586 # Always include local repository.
587 resultdirs.append(p.repos.local_build.path)
588
589 try:
8930b228
MT
590 # Start to prepare the build environment by mounting
591 # the filesystems and extracting files.
592 b.start()
593
1710ccec
MT
594 try:
595 # Build the package.
596 b.build(install_test=install_test)
597 except BuildError:
598 # Raise the error, if the user does not want to
599 # have a shell.
600 if not shell:
601 raise
18edfe75 602
1710ccec
MT
603 # Run a shell to debug the issue.
604 b.shell()
605
606 # If the user requests a shell after a successful build,
607 # we run it here.
608 if after_shell:
609 b.shell()
610
611 # Copy-out all resultfiles if the build was successful.
18edfe75
MT
612 for resultdir in resultdirs:
613 if not resultdir:
614 continue
615
616 b.copy_result(resultdir)
18edfe75 617 finally:
8930b228 618 b.stop()
18edfe75 619
c07a3ca7 620 def _build(self, pkg, resultdir, nodeps=False, **kwargs):
ff9299d0 621 b = builder.Builder(self, pkg, resultdir, **kwargs)
c07a3ca7
MT
622
623 try:
624 b.build()
625 except Error:
626 raise BuildError, _("Build command has failed.")
0d96815e
MT
627
628 # If the build was successful, cleanup all temporary files.
629 b.cleanup()
c07a3ca7 630
18edfe75
MT
631 @staticmethod
632 def shell(pkg, **kwargs):
ff9299d0 633 b = builder.BuildEnviron(pkg, **kwargs)
18edfe75
MT
634
635 try:
8930b228 636 b.start()
18edfe75
MT
637 b.shell()
638 finally:
8930b228 639 b.stop()
18edfe75 640
c07a3ca7 641 def dist(self, pkgs, resultdirs=None):
269c59f3 642 assert resultdirs
18edfe75 643
c07a3ca7
MT
644 for pkg in pkgs:
645 pkg = packages.Makefile(self, pkg)
50f96897 646
c07a3ca7 647 pkg.dist(resultdirs)
18edfe75
MT
648
649 def provides(self, patterns):
650 pkgs = []
651 for pattern in patterns:
e1972777
MT
652 for pkg in self.repos.whatprovides(pattern):
653 if pkg in pkgs:
654 continue
18edfe75 655
e1972777 656 pkgs.append(pkg)
18edfe75 657
e1972777 658 return sorted(pkgs)
18edfe75 659
8276111d
MT
660 def repo_create(self, path, input_paths, type="binary"):
661 assert type in ("binary", "source",)
662
c605d735 663 repo = repository.RepositoryDir(
18edfe75
MT
664 self,
665 name="new",
666 description="New repository.",
667 path=path,
8276111d 668 type=type,
18edfe75
MT
669 )
670
671 for input_path in input_paths:
c605d735 672 repo.collect_packages(input_path)
18edfe75
MT
673
674 repo.save()
675
8276111d
MT
676 return repo
677
18edfe75 678 def repo_list(self):
c605d735 679 return [r for r in self.repos]
31267a64
MT
680
681 def clean_all(self):
8b6bc023 682 log.debug("Cleaning up everything...")
31267a64
MT
683
684 # Clean up repository caches.
685 self.repos.clean()
35d89fd7
MT
686
687 def check(self, downgrade=True, uninstall=True):
688 """
689 Try to fix any errors in the system.
690 """
691 # Detect any errors in the dependency tree.
692 # For that we create an empty request and solver and try to solve
693 # something.
694 request = self.create_request()
695 solver = self.create_solver()
696
697 # XXX the solver does crash if we call it with fix_system=1,
698 # allow_downgrade=1 and uninstall=1. Need to fix this.
699 allow_downgrade = False
700 uninstall = False
701
702 t = solver.solve(request, fix_system=True, allow_downgrade=downgrade,
703 uninstall=uninstall)
704
705 if not t:
8b6bc023 706 log.info(_("Everything is fine."))
35d89fd7
MT
707 return
708
709 # Ask the user if okay.
710 if not t.cli_yesno():
711 return
712
713 # Process the transaction.
714 t.run()
3817ae8e
MT
715
716 @staticmethod
717 def cache_create(**kwargs):
718 # Create a build environment that we are going to pack into
719 # a shiny tarball.
720 b = builder.BuildEnviron(**kwargs)
721 p = b.pakfire
722
723 # Get filename of the file from builder instance.
724 filename = b.cache_file
725
726 try:
727 b.start()
728
729 # Create directory if not existant.
730 dirname = os.path.dirname(filename)
731 if not os.path.exists(dirname):
732 os.makedirs(dirname)
733
734 b.cache_export(filename)
735 finally:
736 b.stop()