]> git.ipfire.org Git - people/stevee/pakfire.git/blob - python/pakfire/base.py
Implement distro-sync.
[people/stevee/pakfire.git] / python / pakfire / base.py
1 #!/usr/bin/python
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 ###############################################################################
21
22 import os
23 import random
24 import string
25
26 import actions
27 import builder
28 import config
29 import distro
30 import filelist
31 import keyring
32 import logger
33 import packages
34 import repository
35 import satsolver
36 import transaction
37 import util
38
39 import logging
40 log = logging.getLogger("pakfire")
41
42 from config import Config
43 from constants import *
44 from i18n import _
45
46 class Pakfire(object):
47 mode = None
48
49 def __init__(self, path="/", config=None, configs=None, arch=None, **kwargs):
50 # Indicates if this instance has already been initialized.
51 self.initialized = False
52
53 # Check if we are operating as the root user.
54 self.check_root_user()
55
56 # The path where we are operating in.
57 self.path = path
58
59 # check if we are actually running on an ipfire system.
60 if not self.mode and self.path == "/":
61 self.check_is_ipfire()
62
63 # Get the configuration.
64 if config:
65 self.config = config
66 else:
67 self.config = self._load_config(configs)
68
69 # Update configuration with additional arguments.
70 for section, settings in kwargs.items():
71 self.config.update(section, settings)
72
73 # Dump the configuration.
74 self.config.dump()
75
76 # Initialize the keyring.
77 self.keyring = keyring.Keyring(self)
78
79 # Get more information about the distribution we are running
80 # or building
81 self.distro = distro.Distribution(self.config.get_distro_conf())
82 if arch:
83 self.distro.arch = arch
84
85 self.pool = satsolver.Pool(self.distro.arch)
86 self.repos = repository.Repositories(self)
87
88 def initialize(self):
89 """
90 Initialize pakfire instance.
91 """
92 if self.initialized:
93 return
94
95 # Initialize repositories.
96 self.repos.initialize()
97
98 self.initialized = True
99
100 def _load_config(self, files=None):
101 """
102 This method loads all needed configuration files.
103 """
104 return config.Config(files=files)
105
106 def __del__(self):
107 # Reset logging.
108 logger.setup_logging()
109
110 def destroy(self):
111 self.repos.shutdown()
112
113 self.initialized = False
114
115 @property
116 def supported_arches(self):
117 return system.supported_arches
118
119 @property
120 def offline(self):
121 """
122 A shortcut that indicates if the system is running in offline mode.
123 """
124 return self.config.get("downloader", "offline", False)
125
126 def check_root_user(self):
127 if not os.getuid() == 0 or not os.getgid() == 0:
128 raise Exception, "You must run pakfire as the root user."
129
130 def check_host_arch(self, arch):
131 """
132 Check if we can build for arch.
133 """
134 # If no arch was given on the command line we build for our
135 # own arch which should always work.
136 if not arch:
137 return True
138
139 if not system.host_supports_arch(arch):
140 raise BuildError, "Cannot build for the target architecture: %s" % arch
141
142 raise BuildError, arch
143
144 def check_is_ipfire(self):
145 ret = os.path.exists("/etc/ipfire-release")
146
147 if not ret:
148 raise NotAnIPFireSystemError, "You can run pakfire only on an IPFire system"
149
150 @property
151 def builder(self):
152 # XXX just backwards compatibility
153 return self.mode == "builder"
154
155 def install(self, requires, interactive=True, logger=None, signatures_mode=None, **kwargs):
156 # Initialize this pakfire instance.
157 self.initialize()
158
159 if not logger:
160 logger = logging.getLogger("pakfire")
161
162 # Pointer to temporary repository.
163 repo = None
164
165 # Sort out what we got...
166 download_packages = []
167 local_packages = []
168 relations = []
169
170 for req in requires:
171 if isinstance(req, packages.Package):
172 relations.append(req)
173 continue
174
175 # This looks like a file.
176 elif req.endswith(".%s" % PACKAGE_EXTENSION) and os.path.exists(req) and os.path.isfile(req):
177 local_packages.append(req)
178 continue
179
180 # Remote files.
181 elif req.startswith("http://") or req.startswith("https://") or req.startswith("ftp://"):
182 download_packages.append(req)
183 continue
184
185 # We treat the rest as relations. The solver will return any errors.
186 relations.append(req)
187
188 # Redefine requires, which will be the list that will be passed to the
189 # solver.
190 requires = relations
191
192 try:
193 # If we have got files to install, we need to create a temporary repository
194 # called 'localinstall'.
195 # XXX FIX TMP PATH
196 if local_packages or download_packages:
197 repo = repository.RepositoryDir(self, "localinstall", _("Local install repository"),
198 os.path.join(LOCAL_TMP_PATH, "repo_%s" % util.random_string()))
199
200 # Register the repository.
201 self.repos.add_repo(repo)
202
203 # Download packages.
204 for download_package in download_packages:
205 repo.download_package(download_package)
206
207 # Add all packages to the repository index.
208 repo.add_packages(local_packages)
209
210 # Add all packages to the requires.
211 requires += repo
212
213 # Do the solving.
214 request = self.pool.create_request(install=requires)
215 solver = self.pool.solve(request, logger=logger, interactive=interactive, **kwargs)
216
217 # Create the transaction.
218 t = transaction.Transaction.from_solver(self, solver)
219 t.dump(logger=logger)
220
221 # Ask if the user acknowledges the transaction.
222 if interactive and not t.cli_yesno():
223 return
224
225 # Run the transaction.
226 t.run(logger=logger, signatures_mode=signatures_mode)
227
228 finally:
229 if repo:
230 # Remove the temporary repository we have created earlier.
231 repo.remove()
232 self.repos.rem_repo(repo)
233
234 def reinstall(self, pkgs, strict=False, logger=None):
235 """
236 Reinstall one or more packages.
237
238 If strict is True, only a package with excatly the same UUID
239 will replace the currently installed one.
240 """
241 # Initialize this pakfire instance.
242 self.initialize()
243
244 if logger is None:
245 logger = logging.getLogger("pakfire")
246
247 # XXX it is possible to install packages without fulfulling
248 # all dependencies.
249
250 reinstall_pkgs = []
251 for pattern in pkgs:
252 _pkgs = []
253 for pkg in self.repos.whatprovides(pattern):
254 # Do not reinstall non-installed packages.
255 if not pkg.is_installed():
256 continue
257
258 _pkgs.append(pkg)
259
260 if not _pkgs:
261 logger.warning(_("Could not find any installed package providing \"%s\".") \
262 % pattern)
263 elif len(_pkgs) == 1:
264 reinstall_pkgs.append(_pkgs[0])
265 #t.add("reinstall", _pkgs[0])
266 else:
267 logger.warning(_("Multiple reinstall candidates for \"%(pattern)s\": %(pkgs)s") \
268 % { "pattern" : pattern, "pkgs" : ", ".join(p.friendly_name for p in sorted(_pkgs)) })
269
270 if not reinstall_pkgs:
271 logger.info(_("Nothing to do"))
272 return
273
274 # Packages we want to replace.
275 # Contains a tuple with the old and the new package.
276 pkgs = []
277
278 # Find the package that is installed in a remote repository to
279 # download it again and re-install it. We need that.
280 for pkg in reinstall_pkgs:
281 # Collect all candidates in here.
282 _pkgs = []
283
284 provides = "%s=%s" % (pkg.name, pkg.friendly_version)
285 for _pkg in self.repos.whatprovides(provides):
286 if _pkg.is_installed():
287 continue
288
289 if strict:
290 if pkg.uuid == _pkg.uuid:
291 _pkgs.append(_pkg)
292 else:
293 _pkgs.append(_pkg)
294
295 if not _pkgs:
296 logger.warning(_("Could not find package %s in a remote repository.") % \
297 pkg.friendly_name)
298 else:
299 # Sort packages to reflect repository priorities, etc...
300 # and take the best (first) one.
301 _pkgs.sort()
302
303 # Re-install best package and cleanup the old one.
304 pkgs.append((pkg, _pkgs[0]))
305
306 # Eventually, create a request.
307 request = None
308
309 _pkgs = []
310 for old, new in pkgs:
311 if old.uuid == new.uuid:
312 _pkgs.append((old, new))
313 else:
314 if request is None:
315 # Create a new request.
316 request = self.pool.create_request()
317
318 # Install the new package, the old will
319 # be cleaned up automatically.
320 request.install(new.solvable)
321
322 if request:
323 solver = self.pool.solve(request)
324 t = transaction.Transaction.from_solver(self, solver)
325 else:
326 # Create new transaction.
327 t = transaction.Transaction(self)
328
329 for old, new in _pkgs:
330 # Install the new package and remove the old one.
331 t.add(actions.ActionReinstall.type, new)
332 t.add(actions.ActionCleanup.type, old)
333
334 t.sort()
335
336 if not t:
337 logger.info(_("Nothing to do"))
338 return
339
340 t.dump(logger=logger)
341
342 if not t.cli_yesno():
343 return
344
345 t.run(logger=logger)
346
347 def update(self, pkgs=None, check=False, excludes=None, interactive=True, logger=None, sync=False, **kwargs):
348 """
349 check indicates, if the method should return after calculation
350 of the transaction.
351 """
352 # Initialize this pakfire instance.
353 self.initialize()
354
355 if logger is None:
356 logger = logging.getLogger("pakfire")
357
358 # If there are given any packets on the command line, we will
359 # only update them. Otherwise, we update the whole system.
360 updateall = True
361 if pkgs:
362 updateall = False
363
364 request = self.pool.create_request(update=pkgs, updateall=updateall)
365
366 # Exclude packages that should not be updated.
367 for exclude in excludes or []:
368 logger.info(_("Excluding %s.") % exclude)
369
370 exclude = self.pool.create_relation(exclude)
371 request.lock(exclude)
372
373 # Update or downgrade to the latest version of all packages
374 # in the enabled repositories.
375 if sync:
376 kwargs.update({
377 "allow_downgrade" : True,
378 "allow_uninstall" : True,
379 })
380
381 solver = self.pool.solve(request, logger=logger, **kwargs)
382
383 if not solver.status:
384 logger.info(_("Nothing to do"))
385
386 # If we are running in check mode, we return a non-zero value to
387 # indicate, that there are no updates.
388 if check:
389 return 1
390 else:
391 return
392
393 # Create the transaction.
394 t = transaction.Transaction.from_solver(self, solver)
395 t.dump(logger=logger)
396
397 # Just exit here, because we won't do the transaction in this mode.
398 if check:
399 return
400
401 # Ask the user if the transaction is okay.
402 if interactive and not t.cli_yesno():
403 return
404
405 # Run the transaction.
406 t.run(logger=logger)
407
408 def downgrade(self, pkgs, logger=None, **kwargs):
409 assert pkgs
410
411 if logger is None:
412 logger = logging.getLogger("pakfire")
413
414 # Initialize this pakfire instance.
415 self.initialize()
416
417 # Create a new request.
418 request = self.pool.create_request()
419
420 # Fill request.
421 for pattern in pkgs:
422 best = None
423 for pkg in self.repos.whatprovides(pattern):
424 # Only consider installed packages.
425 if not pkg.is_installed():
426 continue
427
428 if best and pkg > best:
429 best = pkg
430 elif best is None:
431 best = pkg
432
433 if best is None:
434 logger.warning(_("\"%s\" package does not seem to be installed.") % pattern)
435 else:
436 rel = self.pool.create_relation("%s < %s" % (best.name, best.friendly_version))
437 request.install(rel)
438
439 # Solve the request.
440 solver = self.pool.solve(request, allow_downgrade=True, **kwargs)
441 assert solver.status is True
442
443 # Create the transaction.
444 t = transaction.Transaction.from_solver(self, solver)
445 t.dump(logger=logger)
446
447 if not t:
448 logger.info(_("Nothing to do"))
449 return
450
451 if not t.cli_yesno():
452 return
453
454 t.run()
455
456 def remove(self, pkgs, logger=None):
457 if logger is None:
458 logger = logging.getLogger("pakfire")
459
460 # Initialize this pakfire instance.
461 self.initialize()
462
463 # Create a new request.
464 request = self.pool.create_request(remove=pkgs)
465
466 # Solve the request.
467 solver = self.pool.solve(request, allow_uninstall=True)
468 assert solver.status is True
469
470 # Create the transaction.
471 t = transaction.Transaction.from_solver(self, solver)
472 t.dump()
473
474 if not t:
475 log.info(_("Nothing to do"))
476 return
477
478 # Ask the user if okay.
479 if not t.cli_yesno():
480 return
481
482 # Process the transaction.
483 t.run()
484
485 def info(self, patterns):
486 # Initialize this pakfire instance.
487 self.initialize()
488
489 pkgs = []
490
491 # For all patterns we run a single search which returns us a bunch
492 # of solvables which are transformed into Package objects.
493 for pattern in patterns:
494 if os.path.exists(pattern) and not os.path.isdir(pattern):
495 pkg = packages.open(self, self.repos.dummy, pattern)
496 if pkg:
497 pkgs.append(pkg)
498
499 else:
500 solvs = self.pool.search(pattern, satsolver.SEARCH_GLOB, "solvable:name")
501
502 for solv in solvs:
503 pkg = packages.SolvPackage(self, solv)
504 if pkg in pkgs:
505 continue
506
507 pkgs.append(pkg)
508
509 return sorted(pkgs)
510
511 def search(self, pattern):
512 # Initialize this pakfire instance.
513 self.initialize()
514
515 # Do the search.
516 pkgs = {}
517 for solv in self.pool.search(pattern, satsolver.SEARCH_STRING|satsolver.SEARCH_FILES):
518 pkg = packages.SolvPackage(self, solv)
519
520 # Check, if a package with the name is already in the resultset
521 # and always replace older ones by more recent ones.
522 if pkgs.has_key(pkg.name):
523 if pkgs[pkg.name] < pkg:
524 pkgs[pkg.name] = pkg
525 else:
526 pkgs[pkg.name] = pkg
527
528 # Return a list of the packages, alphabetically sorted.
529 return sorted(pkgs.values())
530
531 def groupinstall(self, group, **kwargs):
532 self.install("@%s" % group, **kwargs)
533
534 def grouplist(self, group):
535 # Initialize this pakfire instance.
536 self.initialize()
537
538 return self.pool.grouplist(group)
539
540 def provides(self, patterns):
541 # Initialize this pakfire instance.
542 self.initialize()
543
544 pkgs = []
545 for pattern in patterns:
546 for pkg in self.pool.whatprovides(self, pattern):
547 if pkg in pkgs:
548 continue
549
550 pkgs.append(pkg)
551
552 # Sort output.
553 pkgs.sort()
554
555 return pkgs
556
557 def resolvdep(self, pkg):
558 # Initialize this pakfire instance.
559 self.initialize()
560
561 return self.pool.resolvdep(self, pkg)
562
563 def repo_list(self):
564 # Initialize this pakfire instance.
565 self.initialize()
566
567 return [r for r in self.repos]
568
569 def clean_all(self):
570 # Initialize this pakfire instance.
571 self.initialize()
572
573 log.debug("Cleaning up everything...")
574
575 # Clean up repository caches.
576 self.repos.clean()
577
578 def check(self, allow_downgrade=True, allow_uninstall=True):
579 """
580 Try to fix any errors in the system.
581 """
582 # Initialize this pakfire instance.
583 self.initialize()
584
585 # Detect any errors in the dependency tree.
586 # For that we create an empty request and solver and try to solve
587 # something.
588 request = self.pool.create_request()
589 request.verify()
590
591 solver = self.pool.solve(
592 request,
593 allow_downgrade=allow_downgrade,
594 allow_uninstall=allow_uninstall,
595 )
596
597 if solver.status is False:
598 log.info(_("Everything is fine."))
599 return
600
601 # Create the transaction.
602 t = transaction.Transaction.from_solver(self, solver)
603 t.dump()
604
605 # Ask the user if okay.
606 if not t.cli_yesno():
607 return
608
609 # Process the transaction.
610 t.run()
611
612 def build(self, makefile, resultdir, stages=None, **kwargs):
613 b = builder.Builder(self, makefile, resultdir, **kwargs)
614
615 try:
616 b.build(stages=stages)
617
618 except Error:
619 raise BuildError, _("Build command has failed.")
620
621 else:
622 # If the build was successful, cleanup all temporary files.
623 b.cleanup()
624
625 def dist(self, pkg, resultdir):
626 pkg = packages.Makefile(self, pkg)
627
628 return pkg.dist(resultdir=resultdir)
629
630
631 class PakfireBuilder(Pakfire):
632 mode = "builder"
633
634 def __init__(self, distro_name=None, *args, **kwargs):
635 self.distro_name = distro_name
636
637 kwargs.update({
638 "path" : os.path.join(BUILD_ROOT, util.random_string()),
639 })
640
641 Pakfire.__init__(self, *args, **kwargs)
642
643 # Let's see what is our host distribution.
644 self.host_distro = distro.Distribution()
645
646 def _load_config(self, files=None):
647 c = config.ConfigBuilder(files=files)
648
649 if self.distro_name is None:
650 self.distro_name = c.get("builder", "distro", None)
651
652 if self.distro_name:
653 c.load_distro_config(self.distro_name)
654
655 if not c.has_distro_conf():
656 log.error(_("You have not set the distribution for which you want to build."))
657 log.error(_("Please do so in builder.conf or on the CLI."))
658 raise ConfigError, _("Distribution configuration is missing.")
659
660 return c
661
662 def build(self, pkg, resultdirs=None, shell=False, install_test=True, after_shell=False, **kwargs):
663 # As the BuildEnviron is only able to handle source packages, we must package makefiles.
664 if pkg.endswith(".%s" % MAKEFILE_EXTENSION):
665 pkg = self.dist(pkg, resultdir=LOCAL_TMP_PATH)
666
667 b = builder.BuildEnviron(self, pkg, **kwargs)
668
669 try:
670 # Start to prepare the build environment by mounting
671 # the filesystems and extracting files.
672 b.start()
673
674 try:
675 # Build the package.
676 b.build(install_test=install_test)
677
678 except BuildError:
679 # Raise the error, if the user does not want to
680 # have a shell.
681 if not shell:
682 raise
683
684 # Run a shell to debug the issue.
685 b.shell()
686
687 # Copy-out all resultfiles if the build was successful.
688 if not resultdirs:
689 resultdirs = []
690
691 # Always include local repository.
692 resultdirs.append(self.repos.local_build.path)
693
694 for resultdir in resultdirs:
695 if not resultdir:
696 continue
697
698 b.copy_result(resultdir)
699
700 # If the user requests a shell after a successful build,
701 # we run it here.
702 if after_shell:
703 b.shell()
704
705 finally:
706 b.stop()
707
708 def shell(self, pkg, **kwargs):
709 # As the BuildEnviron is only able to handle source packages, we must package makefiles.
710 if pkg and pkg.endswith(".%s" % MAKEFILE_EXTENSION):
711 pkg = self.dist(pkg, resultdir=LOCAL_TMP_PATH)
712
713 b = builder.BuildEnviron(self, pkg, **kwargs)
714
715 try:
716 b.start()
717
718 try:
719 b.build(prepare=True)
720 except BuildError:
721 pass
722
723 b.shell()
724 finally:
725 b.stop()
726
727
728 class PakfireClient(Pakfire):
729 mode = "client"
730
731
732 class PakfireServer(Pakfire):
733 mode = "server"
734
735 def repo_create(self, path, input_paths, name=None, key_id=None, type="binary"):
736 assert type in ("binary", "source",)
737
738 if not name:
739 name = _("New repository")
740
741 # Create new repository.
742 repo = repository.RepositoryDir(self, name=name, description="New repository.",
743 path=path, key_id=key_id)
744
745 # Add all packages.
746 repo.add_packages(input_paths)
747
748 # Write metadata to disk.
749 repo.save()
750
751 # Return the new repository.
752 return repo
753
754
755 class PakfireKey(Pakfire):
756 mode = "key"