]> git.ipfire.org Git - pakfire.git/blame - python/pakfire/builder.py
Create an extra namespace for build environments and private network.
[pakfire.git] / python / pakfire / builder.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
MT
21
22import fcntl
23import grp
d59bde4c 24import math
47a4cb89
MT
25import os
26import re
27import shutil
2b6cc06d 28import signal
fc4d4177 29import socket
0184de6e 30import tempfile
47a4cb89 31import time
f02283bb 32import uuid
47a4cb89 33
7c8f2953 34import base
2b6cc06d 35import cgroup
9eac53ba 36import logger
47a4cb89 37import packages
3817ae8e 38import packages.file
e0636b31 39import packages.packager
fa6d335b 40import repository
64ffcd37 41import shell
47a4cb89 42import util
3817ae8e 43import _pakfire
47a4cb89 44
8b6bc023
MT
45import logging
46log = logging.getLogger("pakfire")
47
854d8ccf 48from config import ConfigBuilder
c62d93f1 49from system import system
47a4cb89 50from constants import *
4496b160 51from i18n import _
e9c20259 52from errors import BuildError, BuildRootLocked, Error
47a4cb89
MT
53
54
9eac53ba
MT
55BUILD_LOG_HEADER = """
56 ____ _ __ _ _ _ _ _
57| _ \ __ _| | __/ _(_)_ __ ___ | |__ _ _(_) | __| | ___ _ __
58| |_) / _` | |/ / |_| | '__/ _ \ | '_ \| | | | | |/ _` |/ _ \ '__|
59| __/ (_| | <| _| | | | __/ | |_) | |_| | | | (_| | __/ |
60|_| \__,_|_|\_\_| |_|_| \___| |_.__/ \__,_|_|_|\__,_|\___|_|
61
9eac53ba 62 Version : %(version)s
c62d93f1
MT
63 Host : %(hostname)s (%(host_arch)s)
64 Time : %(time)s
9eac53ba
MT
65
66"""
67
c07a3ca7 68class BuildEnviron(object):
47a4cb89
MT
69 # The version of the kernel this machine is running.
70 kernel_version = os.uname()[2]
71
392371f7 72 def __init__(self, pakfire, filename=None, distro_name=None, build_id=None, logfile=None, release_build=True, **kwargs):
36b328f2 73 self.pakfire = pakfire
f22069bb 74
36b328f2
MT
75 # Check if the given pakfire instance is of the correct type.
76 assert isinstance(self.pakfire, base.PakfireBuilder)
9eac53ba 77
36b328f2
MT
78 # Check if this host can build the requested architecture.
79 if not system.host_supports_arch(self.arch):
80 raise BuildError, _("Cannot build for %s on this host.") % self.arch
9eac53ba 81
013eb9f2
MT
82 # Save the build id and generate one if no build id was provided.
83 if not build_id:
84 build_id = "%s" % uuid.uuid4()
85
86 self.build_id = build_id
87
88 # Setup the logging.
89 self.init_logging(logfile)
90
91 # Initialize a cgroup (if supported).
92 self.init_cgroup()
93
36b328f2
MT
94 # This build is a release build?
95 self.release_build = release_build
2b6cc06d 96
36b328f2
MT
97 if self.release_build:
98 # Disable the local build repository in release mode.
99 self.pakfire.repos.disable_repo("build")
2b6cc06d 100
36b328f2
MT
101 # Log information about pakfire and some more information, when we
102 # are running in release mode.
8330691a 103 logdata = {
c62d93f1
MT
104 "host_arch" : system.arch,
105 "hostname" : system.hostname,
106 "time" : time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()),
107 "version" : "Pakfire %s" % PAKFIRE_VERSION,
8330691a
MT
108 }
109
110 for line in BUILD_LOG_HEADER.splitlines():
111 self.log.info(line % logdata)
9eac53ba 112
992b09d6 113 # Settings array.
36b328f2 114 self.settings = {
992b09d6
MT
115 "enable_loop_devices" : self.config.get_bool("builder", "use_loop_devices", True),
116 "enable_ccache" : self.config.get_bool("builder", "use_ccache", True),
117 "enable_icecream" : self.config.get_bool("builder", "use_icecream", False),
118 "sign_packages" : False,
119 "buildroot_tmpfs" : self.config.get_bool("builder", "use_tmpfs", False),
392371f7 120 "private_network" : self.config.get_bool("builder", "private_network", False),
36b328f2 121 }
7c8f2953 122
27f55929
MT
123 # Get ccache settings.
124 if self.settings.get("enable_ccache", False):
125 self.settings.update({
126 "ccache_compress" : self.config.get_bool("ccache", "compress", True),
127 })
128
36b328f2
MT
129 # Try to get the configured host key. If it is available,
130 # we will automatically sign all packages with it.
131 if self.keyring.get_host_key(secret=True):
132 self.settings["sign_packages"] = True
cd6a9920 133
392371f7
MT
134 # Add settings from keyword arguments.
135 self.settings.update(kwargs)
136
0db30904 137 # Where do we put the result?
36b328f2 138 self.resultdir = os.path.join(self.pakfire.path, "result")
0db30904 139
3817ae8e
MT
140 # Open package.
141 # If we have a plain makefile, we first build a source package and go with that.
142 if filename:
3817ae8e
MT
143 # Open source package.
144 self.pkg = packages.SourcePackage(self.pakfire, None, filename)
145 assert self.pkg, filename
146
147 # Log the package information.
148 self.log.info(_("Package information:"))
149 for line in self.pkg.dump(long=True).splitlines():
150 self.log.info(" %s" % line)
151 self.log.info("")
152
153 # Path where we extract the package and put all the source files.
3ce6a8ad 154 self.build_dir = os.path.join(self.path, "usr/src/packages", self.pkg.friendly_name)
3817ae8e
MT
155 else:
156 # No package :(
157 self.pkg = None
062699ee 158
47a4cb89
MT
159 # Lock the buildroot
160 self._lock = None
47a4cb89 161
fc4d4177 162 # Save the build time.
36b328f2 163 self.build_time = time.time()
f02283bb 164
96ccd5d5
MT
165 def setup_signal_handlers(self):
166 pass
167
8930b228 168 def start(self):
36b328f2
MT
169 assert not self.pakfire.initialized, "Pakfire has already been initialized"
170
392371f7
MT
171 # Unshare namepsace.
172 # If this fails because the kernel has no support for CLONE_NEWIPC or CLONE_NEWUTS,
173 # we try to fall back to just set CLONE_NEWNS.
174 try:
175 _pakfire.unshare(_pakfire.SCHED_CLONE_NEWNS|_pakfire.SCHED_CLONE_NEWIPC|_pakfire.SCHED_CLONE_NEWUTS)
176 except RuntimeError, e:
177 _pakfire.unshare(_pakfire.SCHED_CLONE_NEWNS)
178
8930b228
MT
179 # Mount the directories.
180 self._mountall()
181
36b328f2
MT
182 # Lock the build environment.
183 self.lock()
184
185 # Initialize pakfire instance.
186 self.pakfire.initialize()
187
392371f7
MT
188 # Optionally enable private networking.
189 if self.settings.get("private_network", None):
190 _pakfire.unshare(_pakfire.SCHED_CLONE_NEWNET)
191
9a416dbc
MT
192 # Populate /dev.
193 self.populate_dev()
194
83e5f0da
MT
195 # Setup domain name resolution in chroot.
196 self.setup_dns()
8930b228
MT
197
198 # Extract all needed packages.
199 self.extract()
200
201 def stop(self):
2b6cc06d
MT
202 if self.cgroup:
203 # Kill all still running processes in the cgroup.
204 self.cgroup.kill_and_wait()
205
206 # Remove cgroup and all parent cgroups if they are empty.
207 self.cgroup.migrate_task(self.cgroup.root, os.getpid())
208 self.cgroup.destroy()
209
210 parent = self.cgroup.parent
211 while parent:
212 if not parent.is_empty(recursive=True):
213 break
214
215 parent.destroy()
216 parent = parent.parent
217
218 else:
219 util.orphans_kill(self.path)
8930b228 220
36b328f2
MT
221 # Shut down pakfire instance.
222 self.pakfire.destroy()
223
224 # Unlock build environment.
225 self.unlock()
8930b228
MT
226
227 # Umount the build environment.
228 self._umountall()
229
230 # Remove all files.
231 self.destroy()
232
992b09d6
MT
233 @property
234 def config(self):
235 """
236 Proxy method for easy access to the configuration.
237 """
238 return self.pakfire.config
239
36b328f2
MT
240 @property
241 def distro(self):
242 """
243 Proxy method for easy access to the distribution.
244 """
245 return self.pakfire.distro
246
247 @property
248 def path(self):
249 """
250 Proxy method for easy access to the path.
251 """
252 return self.pakfire.path
253
7c8f2953
MT
254 @property
255 def arch(self):
256 """
257 Inherit architecture from distribution configuration.
258 """
36b328f2 259 return self.pakfire.distro.arch
7c8f2953 260
64ffcd37
MT
261 @property
262 def personality(self):
263 """
264 Gets the personality from the distribution configuration.
265 """
36b328f2 266 return self.pakfire.distro.personality
64ffcd37 267
fc4d4177
MT
268 @property
269 def info(self):
270 return {
271 "build_date" : time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(self.build_time)),
272 "build_host" : socket.gethostname(),
273 "build_id" : self.build_id,
274 "build_time" : self.build_time,
275 }
276
1389d9d5
MT
277 @property
278 def keyring(self):
279 """
280 Shortcut to access the pakfire keyring.
1389d9d5 281 """
1389d9d5
MT
282 return self.pakfire.keyring
283
47a4cb89
MT
284 def lock(self):
285 filename = os.path.join(self.path, ".lock")
286
287 try:
288 self._lock = open(filename, "a+")
289 except IOError, e:
290 return 0
291
292 try:
293 fcntl.lockf(self._lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
294 except IOError, e:
295 raise BuildRootLocked, "Buildroot is locked"
296
297 return 1
298
299 def unlock(self):
300 if self._lock:
301 self._lock.close()
302 self._lock = None
303
36b328f2
MT
304 def init_cgroup(self):
305 """
306 Initialize cgroup (if the system supports it).
307 """
308 if not cgroup.supported():
309 self.cgroup = None
310 return
311
312 self.cgroup = cgroup.CGroup("pakfire/builder/%s" % self.build_id)
313
314 # Attach the pakfire-builder process to the parent group.
315 self.cgroup.parent.attach()
316
317 def init_logging(self, logfile):
318 if logfile:
319 self.log = log.getChild(self.build_id)
320 # Propage everything to the root logger that we will see something
321 # on the terminal.
322 self.log.propagate = 1
323 self.log.setLevel(logging.INFO)
324
325 # Add the given logfile to the logger.
326 h = logging.FileHandler(logfile)
327 self.log.addHandler(h)
328
329 # Format the log output for the file.
330 f = logger.BuildFormatter()
331 h.setFormatter(f)
332 else:
333 # If no logile was given, we use the root logger.
334 self.log = logging.getLogger("pakfire")
335
47a4cb89
MT
336 def copyin(self, file_out, file_in):
337 if file_in.startswith("/"):
338 file_in = file_in[1:]
339
340 file_in = self.chrootPath(file_in)
341
342 #if not os.path.exists(file_out):
343 # return
344
345 dir_in = os.path.dirname(file_in)
346 if not os.path.exists(dir_in):
347 os.makedirs(dir_in)
348
8b6bc023 349 self.log.debug("%s --> %s" % (file_out, file_in))
47a4cb89
MT
350
351 shutil.copy2(file_out, file_in)
352
353 def copyout(self, file_in, file_out):
354 if file_in.startswith("/"):
355 file_in = file_in[1:]
356
357 file_in = self.chrootPath(file_in)
358
359 #if not os.path.exists(file_in):
360 # return
361
362 dir_out = os.path.dirname(file_out)
363 if not os.path.exists(dir_out):
364 os.makedirs(dir_out)
365
8b6bc023 366 self.log.debug("%s --> %s" % (file_in, file_out))
47a4cb89
MT
367
368 shutil.copy2(file_in, file_out)
369
370 def copy_result(self, resultdir):
7187b3fe
MT
371 # XXX should use find_result_packages
372
47a4cb89
MT
373 dir_in = self.chrootPath("result")
374
375 for dir, subdirs, files in os.walk(dir_in):
376 basename = os.path.basename(dir)
377 dir = dir[len(self.chrootPath()):]
378 for file in files:
379 file_in = os.path.join(dir, file)
380
381 file_out = os.path.join(
382 resultdir,
383 basename,
384 file,
385 )
386
387 self.copyout(file_in, file_out)
388
7187b3fe
MT
389 def find_result_packages(self):
390 ret = []
391
392 for dir, subdirs, files in os.walk(self.resultdir):
393 for file in files:
394 if not file.endswith(".%s" % PACKAGE_EXTENSION):
395 continue
396
397 file = os.path.join(dir, file)
398 ret.append(file)
399
400 return ret
401
36b328f2 402 def extract(self, requires=None):
47a4cb89
MT
403 """
404 Gets a dependency set and extracts all packages
405 to the environment.
406 """
9c2ad426
MT
407 if not requires:
408 requires = []
5be98997 409
8c7abfb6
MT
410 # Add neccessary build dependencies.
411 requires += BUILD_PACKAGES
3d960a21 412
33f4679b
MT
413 # If we have ccache enabled, we need to extract it
414 # to the build chroot.
415 if self.settings.get("enable_ccache"):
9c2ad426 416 requires.append("ccache")
33f4679b 417
5be98997
MT
418 # If we have icecream enabled, we need to extract it
419 # to the build chroot.
420 if self.settings.get("enable_icecream"):
9c2ad426 421 requires.append("icecream")
47a4cb89
MT
422
423 # Get build dependencies from source package.
3817ae8e
MT
424 if self.pkg:
425 for req in self.pkg.requires:
426 requires.append(req)
47a4cb89 427
9c2ad426 428 # Install all packages.
3817ae8e 429 self.log.info(_("Install packages needed for build..."))
9c2ad426 430 self.install(requires)
47a4cb89
MT
431
432 # Copy the makefile and load source tarballs.
3817ae8e
MT
433 if self.pkg:
434 self.pkg.extract(_("Extracting"), prefix=self.build_dir)
47a4cb89 435
bf3bbed5
MT
436 # Add an empty line at the end.
437 self.log.info("")
438
4fffe3c4 439 def install(self, requires, **kwargs):
9c2ad426
MT
440 """
441 Install everything that is required in requires.
442 """
c9ec78ca
MT
443 # If we got nothing to do, we quit immediately.
444 if not requires:
445 return
446
4fffe3c4
MT
447 kwargs.update({
448 "interactive" : False,
449 "logger" : self.log,
450 })
c9ec78ca 451
4fffe3c4
MT
452 if not kwargs.has_key("allow_downgrade"):
453 kwargs["allow_downgrade"] = True
1389d9d5 454
4fffe3c4
MT
455 # Install everything.
456 self.pakfire.install(requires, **kwargs)
c9ec78ca 457
47a4cb89
MT
458 def chrootPath(self, *args):
459 # Remove all leading slashes
460 _args = []
461 for arg in args:
462 if arg.startswith("/"):
463 arg = arg[1:]
464 _args.append(arg)
465 args = _args
466
467 ret = os.path.join(self.path, *args)
468 ret = ret.replace("//", "/")
469
470 assert ret.startswith(self.path)
471
472 return ret
473
9a416dbc 474 def populate_dev(self):
47a4cb89 475 nodes = [
9a416dbc
MT
476 "/dev/null",
477 "/dev/zero",
478 "/dev/full",
479 "/dev/random",
480 "/dev/urandom",
481 "/dev/tty",
482 "/dev/ptmx",
483 "/dev/kmsg",
484 "/dev/rtc0",
485 "/dev/console",
47a4cb89
MT
486 ]
487
488 # If we need loop devices (which are optional) we create them here.
489 if self.settings["enable_loop_devices"]:
490 for i in range(0, 7):
9a416dbc 491 nodes.append("/dev/loop%d" % i)
47a4cb89 492
47a4cb89 493 for node in nodes:
9a416dbc
MT
494 # Stat the original node of the host system and copy it to
495 # the build chroot.
496 node_stat = os.stat(node)
497
498 self._create_node(node, node_stat.st_mode, node_stat.st_rdev)
47a4cb89
MT
499
500 os.symlink("/proc/self/fd/0", self.chrootPath("dev", "stdin"))
501 os.symlink("/proc/self/fd/1", self.chrootPath("dev", "stdout"))
502 os.symlink("/proc/self/fd/2", self.chrootPath("dev", "stderr"))
503 os.symlink("/proc/self/fd", self.chrootPath("dev", "fd"))
504
83e5f0da 505 def setup_dns(self):
18973967
MT
506 """
507 Add DNS resolution facility to chroot environment by copying
508 /etc/resolv.conf and /etc/hosts.
509 """
510 for i in ("/etc/resolv.conf", "/etc/hosts"):
511 self.copyin(i, i)
47a4cb89
MT
512
513 def _create_node(self, filename, mode, device):
8b6bc023 514 self.log.debug("Create node: %s (%s)" % (filename, mode))
47a4cb89
MT
515
516 filename = self.chrootPath(filename)
517
518 # Create parent directory if it is missing.
519 dirname = os.path.dirname(filename)
520 if not os.path.exists(dirname):
521 os.makedirs(dirname)
522
523 os.mknod(filename, mode, device)
524
526c3e7f 525 def destroy(self):
8b6bc023 526 self.log.debug("Destroying environment %s" % self.path)
47a4cb89
MT
527
528 if os.path.exists(self.path):
529 util.rm(self.path)
530
e412b8dc 531 def cleanup(self):
8b6bc023 532 self.log.debug("Cleaning environemnt.")
e412b8dc 533
e412b8dc 534 # Remove the build directory and buildroot.
c44f9d6a 535 dirs = (self.build_dir, self.chrootPath("result"),)
e412b8dc
MT
536
537 for d in dirs:
e412b8dc
MT
538 if not os.path.exists(d):
539 continue
540
541 util.rm(d)
542 os.makedirs(d)
543
47a4cb89
MT
544 def _mountall(self):
545 self.log.debug("Mounting environment")
8930b228
MT
546 for src, dest, fs, options in self.mountpoints:
547 mountpoint = self.chrootPath(dest)
548 if options:
549 options = "-o %s" % options
550
551 # Eventually create mountpoint directory
552 if not os.path.exists(mountpoint):
553 os.makedirs(mountpoint)
554
64ffcd37 555 self.execute_root("mount -n -t %s %s %s %s" % (fs, options, src, mountpoint), shell=True)
47a4cb89
MT
556
557 def _umountall(self):
558 self.log.debug("Umounting environment")
8930b228
MT
559
560 mountpoints = []
561 for src, dest, fs, options in reversed(self.mountpoints):
36b328f2
MT
562 dest = self.chrootPath(dest)
563
8930b228
MT
564 if not dest in mountpoints:
565 mountpoints.append(dest)
566
36b328f2
MT
567 while mountpoints:
568 for mp in mountpoints:
569 try:
570 self.execute_root("umount -n %s" % mp, shell=True)
571 except ShellEnvironmentError:
572 pass
8930b228 573
36b328f2
MT
574 if not os.path.ismount(mp):
575 mountpoints.remove(mp)
47a4cb89
MT
576
577 @property
578 def mountpoints(self):
8930b228
MT
579 mountpoints = []
580
64ffcd37
MT
581 # Make root as a tmpfs if enabled.
582 if self.settings.get("buildroot_tmpfs"):
583 mountpoints += [
584 ("pakfire_root", "/", "tmpfs", "defaults"),
585 ]
8930b228
MT
586
587 mountpoints += [
588 # src, dest, fs, options
589 ("pakfire_proc", "/proc", "proc", "nosuid,noexec,nodev"),
590 ("/proc/sys", "/proc/sys", "bind", "bind"),
591 ("/proc/sys", "/proc/sys", "bind", "bind,ro,remount"),
592 ("/sys", "/sys", "bind", "bind"),
593 ("/sys", "/sys", "bind", "bind,ro,remount"),
594 ("pakfire_tmpfs", "/dev", "tmpfs", "mode=755,nosuid"),
595 ("/dev/pts", "/dev/pts", "bind", "bind"),
596 ("pakfire_tmpfs", "/run", "tmpfs", "mode=755,nosuid,nodev"),
4252a549 597 ("pakfire_tmpfs", "/tmp", "tmpfs", "mode=755,nosuid,nodev"),
47a4cb89
MT
598 ]
599
8930b228
MT
600 # If selinux is enabled.
601 if os.path.exists("/sys/fs/selinux"):
602 mountpoints += [
603 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind"),
604 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind,ro,remount"),
605 ]
47a4cb89 606
8930b228 607 # If ccache support is requested, we bind mount the cache.
33f4679b 608 if self.settings.get("enable_ccache"):
046278b2
MT
609 # Create ccache cache directory if it does not exist.
610 if not os.path.exists(CCACHE_CACHE_DIR):
611 os.makedirs(CCACHE_CACHE_DIR)
612
8930b228
MT
613 mountpoints += [
614 (CCACHE_CACHE_DIR, "/var/cache/ccache", "bind", "bind"),
615 ]
33f4679b 616
8930b228 617 return mountpoints
47a4cb89
MT
618
619 @property
620 def environ(self):
621 env = {
0cf10c2d
MT
622 # Add HOME manually, because it is occasionally not set
623 # and some builds get in trouble then.
a6659a32 624 "PATH" : "/usr/bin:/bin:/usr/sbin:/sbin",
0cf10c2d 625 "HOME" : "/root",
a6659a32 626 "TERM" : os.environ.get("TERM", "vt100"),
89ebac67 627 "PS1" : "\u:\w\$ ",
0cf10c2d 628
d64b975b
MT
629 # Sanitize language.
630 "LANG" : os.environ.setdefault("LANG", "en_US.UTF-8"),
631
936f6b37
MT
632 # Set the container that we can detect, if we are inside a
633 # chroot.
634 "container" : "pakfire-builder",
47a4cb89
MT
635 }
636
637 # Inherit environment from distro
638 env.update(self.pakfire.distro.environ)
639
27f55929
MT
640 # ccache environment settings
641 if self.settings.get("enable_ccache", False):
642 compress = self.settings.get("ccache_compress", False)
643 if compress:
644 env["CCACHE_COMPRESS"] = "1"
645
646 # Let ccache create its temporary files in /tmp.
647 env["CCACHE_TEMPDIR"] = "/tmp"
648
5be98997 649 # Icecream environment settings
c07a3ca7 650 if self.settings.get("enable_icecream", False):
5be98997
MT
651 # Set the toolchain path
652 if self.settings.get("icecream_toolchain", None):
653 env["ICECC_VERSION"] = self.settings.get("icecream_toolchain")
654
655 # Set preferred host if configured.
656 if self.settings.get("icecream_preferred_host", None):
657 env["ICECC_PREFERRED_HOST"] = \
658 self.settings.get("icecream_preferred_host")
659
77a330d5
MT
660 # Fake UTS_MACHINE, when we cannot use the personality syscall and
661 # if the host architecture is not equal to the target architecture.
662 if not self.pakfire.distro.personality and \
a6bd96bc 663 not system.native_arch == self.pakfire.distro.arch:
77a330d5
MT
664 env.update({
665 "LD_PRELOAD" : "/usr/lib/libpakfire_preload.so",
666 "UTS_MACHINE" : self.pakfire.distro.arch,
667 })
47a4cb89
MT
668
669 return env
670
c62d93f1
MT
671 @property
672 def installed_packages(self):
673 """
674 Returns an iterator over all installed packages in this build environment.
675 """
676 # Get the repository of all installed packages.
677 repo = self.pakfire.repos.get_repo("@system")
678
679 # Return an iterator over the packages.
680 return iter(repo)
681
854d8ccf
MT
682 def write_config(self):
683 # Cleanup everything in /etc/pakfire.
684 util.rm(self.chrootPath(CONFIG_DIR))
685
686 for i in (CONFIG_DIR, CONFIG_REPOS_DIR):
687 i = self.chrootPath(i)
688 if not os.path.exists(i):
689 os.makedirs(i)
690
691 # Write general.conf.
692 f = open(self.chrootPath(CONFIG_DIR, "general.conf"), "w")
693 f.close()
694
695 # Write builder.conf.
696 f = open(self.chrootPath(CONFIG_DIR, "builder.conf"), "w")
697 f.write(self.distro.get_config())
698 f.close()
699
700 # Create pakfire configuration files.
701 for repo in self.pakfire.repos:
702 conf = repo.get_config()
703
704 if not conf:
705 continue
706
707 filename = self.chrootPath(CONFIG_REPOS_DIR, "%s.repo" % repo.name)
708 f = open(filename, "w")
709 f.write("\n".join(conf))
710 f.close()
711
64ffcd37
MT
712 @property
713 def pkg_makefile(self):
714 return os.path.join(self.build_dir, "%s.%s" % (self.pkg.name, MAKEFILE_EXTENSION))
47a4cb89 715
64ffcd37
MT
716 def execute(self, command, logger=None, **kwargs):
717 """
718 Executes the given command in the build chroot.
719 """
8930b228
MT
720 # Environment variables
721 env = self.environ
47a4cb89 722
8930b228
MT
723 if kwargs.has_key("env"):
724 env.update(kwargs.pop("env"))
15398910 725
8b6bc023 726 self.log.debug("Environment:")
8930b228 727 for k, v in sorted(env.items()):
8b6bc023 728 self.log.debug(" %s=%s" % (k, v))
e360ea59 729
8930b228
MT
730 # Make every shell to a login shell because we set a lot of
731 # environment things there.
64ffcd37
MT
732 command = ["bash", "--login", "-c", command]
733
734 args = {
735 "chroot_path" : self.chrootPath(),
736 "cgroup" : self.cgroup,
737 "env" : env,
738 "logger" : logger,
739 "personality" : self.personality,
740 "shell" : False,
741 }
742 args.update(kwargs)
47a4cb89 743
64ffcd37
MT
744 # Run the shit.
745 shellenv = shell.ShellExecuteEnvironment(command, **args)
746 shellenv.execute()
747
748 return shellenv
749
750 def execute_root(self, command, **kwargs):
751 """
752 Executes the given command outside the build chroot.
753 """
754 shellenv = shell.ShellExecuteEnvironment(command, **kwargs)
755 shellenv.execute()
756
757 return shellenv
47a4cb89 758
47230b23 759 def build(self, install_test=True, prepare=False):
3817ae8e
MT
760 if not self.pkg:
761 raise BuildError, _("You cannot run a build when no package was given.")
c157d1e2 762
c44f9d6a 763 # Search for the package file in build_dir and raise BuildError if it is not present.
64ffcd37
MT
764 if not os.path.exists(self.pkg_makefile):
765 raise BuildError, _("Could not find makefile in build root: %s") % self.pkg_makefile
c44f9d6a 766
854d8ccf
MT
767 # Write pakfire configuration into the chroot.
768 self.write_config()
c07a3ca7
MT
769
770 # Create the build command, that is executed in the chroot.
a6bd96bc
MT
771 build_command = [
772 "/usr/lib/pakfire/builder",
773 "--offline",
774 "build",
64ffcd37 775 "/%s" % os.path.relpath(self.pkg_makefile, self.chrootPath()),
a6bd96bc
MT
776 "--arch", self.arch,
777 "--nodeps",
a6bd96bc
MT
778 "--resultdir=/result",
779 ]
c07a3ca7 780
64ffcd37 781 # Check if only the preparation stage should be run.
47230b23
MT
782 if prepare:
783 build_command.append("--prepare")
784
36b328f2
MT
785 build_command = " ".join(build_command)
786
c07a3ca7 787 try:
64ffcd37 788 self.execute(build_command, logger=self.log)
c07a3ca7 789
4fffe3c4 790 # Perform the install test after the actual build.
47230b23 791 if install_test and not prepare:
4fffe3c4
MT
792 self.install_test()
793
64ffcd37 794 except ShellEnvironmentError:
64ffcd37
MT
795 self.log.error(_("Build failed"))
796
64252a9d 797 except KeyboardInterrupt:
64252a9d
MT
798 self.log.error(_("Build interrupted"))
799
800 raise
801
64ffcd37
MT
802 # Catch all other errors.
803 except:
516fc709
MT
804 self.log.error(_("Build failed."), exc_info=True)
805
64ffcd37
MT
806 else:
807 # Don't sign packages in prepare mode.
808 if prepare:
809 return
c07a3ca7 810
64ffcd37
MT
811 # Sign all built packages with the host key (if available).
812 self.sign_packages()
47230b23 813
64ffcd37
MT
814 # Dump package information.
815 self.dump()
1389d9d5 816
64ffcd37 817 return
1389d9d5 818
64ffcd37
MT
819 # End here in case of an error.
820 raise BuildError, _("The build command failed. See logfile for details.")
1389d9d5 821
4fffe3c4
MT
822 def install_test(self):
823 self.log.info(_("Running installation test..."))
824
825 # Install all packages that were built.
532142c6 826 self.install(self.find_result_packages(), allow_vendorchange=True,
9b68f47c 827 allow_uninstall=True, signatures_mode="disabled")
4fffe3c4
MT
828
829 self.log.info(_("Installation test succeeded."))
830 self.log.info("")
831
47a4cb89 832 def shell(self, args=[]):
e9c20259 833 if not util.cli_is_interactive():
8b6bc023 834 self.log.warning("Cannot run shell on non-interactive console.")
e9c20259
MT
835 return
836
9c2ad426
MT
837 # Install all packages that are needed to run a shell.
838 self.install(SHELL_PACKAGES)
839
47a4cb89 840 # XXX need to set CFLAGS here
83e5f0da
MT
841 command = "/usr/sbin/chroot %s %s %s" % \
842 (self.chrootPath(), SHELL_SCRIPT, " ".join(args))
47a4cb89 843
e360ea59
MT
844 # Add personality if we require one
845 if self.pakfire.distro.personality:
a7596ccf
MT
846 command = "%s %s" % (self.pakfire.distro.personality, command)
847
848 for key, val in self.environ.items():
849 command = "%s=\"%s\" " % (key, val) + command
e360ea59 850
47a4cb89 851 # Empty the environment
a7596ccf 852 command = "env -i - %s" % command
47a4cb89 853
8b6bc023 854 self.log.debug("Shell command: %s" % command)
47a4cb89 855
8930b228
MT
856 shell = os.system(command)
857 return os.WEXITSTATUS(shell)
c07a3ca7 858
64ffcd37
MT
859 def sign_packages(self, keyfp=None):
860 # Do nothing if signing is not requested.
861 if not self.settings.get("sign_packages"):
862 return
863
864 # Get key, that should be used for signing.
865 if not keyfp:
866 keyfp = self.keyring.get_host_key_id()
1389d9d5
MT
867
868 # Find all files to process.
869 files = self.find_result_packages()
870
871 # Create a progressbar.
4fffe3c4
MT
872 print _("Signing packages...")
873 p = util.make_progress(keyfp, len(files))
1389d9d5
MT
874 i = 0
875
876 for file in files:
877 # Update progressbar.
878 if p:
879 i += 1
880 p.update(i)
881
882 # Open package file.
883 pkg = packages.open(self.pakfire, None, file)
884
885 # Sign it.
886 pkg.sign(keyfp)
887
888 # Close progressbar.
889 if p:
890 p.finish()
4fffe3c4 891 print "" # Print an empty line.
1389d9d5
MT
892
893 def dump(self):
894 pkgs = []
895
896 for file in self.find_result_packages():
897 pkg = packages.open(self.pakfire, None, file)
898 pkgs.append(pkg)
899
900 # If there are no packages, there is nothing to do.
901 if not pkgs:
902 return
903
904 pkgs.sort()
905
906 self.log.info(_("Dumping package information:"))
907 for pkg in pkgs:
908 dump = pkg.dump(long=True)
909
910 for line in dump.splitlines():
911 self.log.info(" %s" % line)
912 self.log.info("") # Empty line.
913
56f5e5ff 914
ff9299d0 915class Builder(object):
c07a3ca7
MT
916 def __init__(self, pakfire, filename, resultdir, **kwargs):
917 self.pakfire = pakfire
918
919 self.filename = filename
920
921 self.resultdir = resultdir
922
923 # Open package file.
924 self.pkg = packages.Makefile(self.pakfire, self.filename)
925
c07a3ca7 926 self._environ = {
0d96815e 927 "LANG" : "C",
c07a3ca7
MT
928 }
929
0d96815e
MT
930 @property
931 def buildroot(self):
932 return self.pkg.buildroot
933
c07a3ca7
MT
934 @property
935 def distro(self):
936 return self.pakfire.distro
937
938 @property
939 def environ(self):
940 environ = os.environ
3c45a6af
MT
941
942 # Get all definitions from the package.
943 environ.update(self.pkg.exports)
944
945 # Overwrite some definitions by default values.
c07a3ca7
MT
946 environ.update(self._environ)
947
948 return environ
949
64ffcd37
MT
950 def execute(self, command, logger=None, **kwargs):
951 if logger is None:
08e95360 952 logger = logging.getLogger("pakfire")
c07a3ca7
MT
953
954 # Make every shell to a login shell because we set a lot of
955 # environment things there.
64ffcd37
MT
956 command = ["bash", "--login", "-c", command]
957
958 args = {
959 "cwd" : "/%s" % LOCAL_TMP_PATH,
960 "env" : self.environ,
961 "logger" : logger,
962 "personality" : self.distro.personality,
963 "shell" : False,
964 }
965 args.update(kwargs)
2b6cc06d
MT
966
967 try:
64ffcd37
MT
968 shellenv = shell.ShellExecuteEnvironment(command, **args)
969 shellenv.execute()
2b6cc06d 970
64ffcd37 971 except ShellEnvironmentError:
2b6cc06d
MT
972 logger.error("Command exited with an error: %s" % command)
973 raise
c07a3ca7 974
64ffcd37
MT
975 return shellenv
976
08e95360
MT
977 def run_script(self, script, *args):
978 if not script.startswith("/"):
979 script = os.path.join(SCRIPT_DIR, script)
980
981 assert os.path.exists(script), "Script we should run does not exist: %s" % script
982
983 cmd = [script,]
984 for arg in args:
985 cmd.append(arg)
986 cmd = " ".join(cmd)
987
988 # Returns the output of the command, but the output won't get
989 # logged.
64ffcd37
MT
990 exe = self.execute(cmd, record_output=True, log_output=False)
991
992 # Return the output of the command.
993 if exe.exitcode == 0:
994 return exe.output
08e95360 995
c07a3ca7
MT
996 def create_icecream_toolchain(self):
997 try:
64ffcd37
MT
998 exe = self.execute(
999 "icecc --build-native 2>/dev/null",
1000 record_output=True, record_stderr=False,
1001 log_output=False, log_errors=False,
1002 cwd="/tmp",
1003 )
1004 except ShellEnvironmentError:
c07a3ca7
MT
1005 return
1006
64ffcd37 1007 for line in exe.output.splitlines():
c07a3ca7
MT
1008 m = re.match(r"^creating ([a-z0-9]+\.tar\.gz)", line)
1009 if m:
dc99cccd 1010 self._environ["ICECC_VERSION"] = "/tmp/%s" % m.group(1)
c07a3ca7
MT
1011
1012 def create_buildscript(self, stage):
c07a3ca7
MT
1013 # Get buildscript from the package.
1014 script = self.pkg.get_buildscript(stage)
1015
1016 # Write script to an empty file.
0184de6e 1017 f = tempfile.NamedTemporaryFile(mode="w", delete=False)
c07a3ca7
MT
1018 f.write("#!/bin/sh\n\n")
1019 f.write("set -e\n")
1020 f.write("set -x\n")
1021 f.write("\n%s\n" % script)
1022 f.write("exit 0\n")
1023 f.close()
c07a3ca7 1024
0184de6e
MT
1025 # Make the script executable.
1026 os.chmod(f.name, 700)
1027
1028 return f.name
c07a3ca7 1029
47230b23 1030 def build(self, stages=None):
7abe644a
MT
1031 # Create buildroot and remove all content if it was existant.
1032 util.rm(self.buildroot)
1033 os.makedirs(self.buildroot)
c07a3ca7
MT
1034
1035 # Build icecream toolchain if icecream is installed.
1036 self.create_icecream_toolchain()
1037
013eb9f2
MT
1038 # Process stages in order.
1039 for stage in ("prepare", "build", "test", "install"):
1040 # Skip unwanted stages.
1041 if stages and not stage in stages:
1042 continue
47230b23 1043
013eb9f2 1044 # Run stage.
c07a3ca7
MT
1045 self.build_stage(stage)
1046
013eb9f2
MT
1047 # Stop if install stage has not been processed.
1048 if stages and not "install" in stages:
47230b23
MT
1049 return
1050
87745c01 1051 # Run post-build stuff.
1b51609b 1052 self.post_compress_man_pages()
87745c01 1053 self.post_remove_static_libs()
3ce6a8ad 1054 self.post_extract_debuginfo()
87745c01 1055
c07a3ca7
MT
1056 # Package the result.
1057 # Make all these little package from the build environment.
8b6bc023 1058 log.info(_("Creating packages:"))
56f5e5ff 1059 pkgs = []
c07a3ca7 1060 for pkg in reversed(self.pkg.packages):
5dda54e4
MT
1061 packager = packages.packager.BinaryPackager(self.pakfire, pkg,
1062 self, self.buildroot)
56f5e5ff
MT
1063 pkg = packager.run(self.resultdir)
1064 pkgs.append(pkg)
8b6bc023 1065 log.info("")
56f5e5ff 1066
c07a3ca7
MT
1067 def build_stage(self, stage):
1068 # Get the buildscript for this stage.
1069 buildscript = self.create_buildscript(stage)
1070
1071 # Execute the buildscript of this stage.
8b6bc023 1072 log.info(_("Running stage %s:") % stage)
c07a3ca7 1073
75bb74a7 1074 try:
64ffcd37 1075 self.execute(buildscript)
75bb74a7
MT
1076
1077 finally:
1078 # Remove the buildscript.
1079 if os.path.exists(buildscript):
1080 os.unlink(buildscript)
c07a3ca7 1081
87745c01
MT
1082 def post_remove_static_libs(self):
1083 keep_libs = self.pkg.lexer.build.get_var("keep_libraries")
1084 keep_libs = keep_libs.split()
1085
1086 try:
64ffcd37 1087 self.execute("%s/remove-static-libs %s %s" % \
87745c01 1088 (SCRIPT_DIR, self.buildroot, " ".join(keep_libs)))
64ffcd37 1089 except ShellEnvironmentError, e:
8b6bc023 1090 log.warning(_("Could not remove static libraries: %s") % e)
87745c01 1091
1b51609b
MT
1092 def post_compress_man_pages(self):
1093 try:
64ffcd37
MT
1094 self.execute("%s/compress-man-pages %s" % (SCRIPT_DIR, self.buildroot))
1095 except ShellEnvironmentError, e:
8b6bc023 1096 log.warning(_("Compressing man pages did not complete successfully."))
1b51609b 1097
3ce6a8ad
MT
1098 def post_extract_debuginfo(self):
1099 args = []
1100
1101 # Check if we need to run with strict build-id.
1102 strict_id = self.pkg.lexer.build.get_var("debuginfo_strict_build_id", "true")
1103 if strict_id in ("true", "yes", "1"):
1104 args.append("--strict-build-id")
1105
1106 args.append("--buildroot=%s" % self.pkg.buildroot)
1107 args.append("--sourcedir=%s" % self.pkg.sourcedir)
1108
1109 # Get additional options to pass to script.
1110 options = self.pkg.lexer.build.get_var("debuginfo_options", "")
1111 args += options.split()
1112
1113 try:
64ffcd37
MT
1114 self.execute("%s/extract-debuginfo %s %s" % (SCRIPT_DIR, " ".join(args), self.pkg.buildroot))
1115 except ShellEnvironmentError, e:
3ce6a8ad
MT
1116 log.error(_("Extracting debuginfo did not complete with success. Aborting build."))
1117 raise
1118
08e95360
MT
1119 def find_prerequires(self, scriptlet_file):
1120 assert os.path.exists(scriptlet_file), "Scriptlet file does not exist: %s" % scriptlet_file
1121
1122 res = self.run_script("find-prerequires", scriptlet_file)
1123 prerequires = set(res.splitlines())
1124
1125 return prerequires
1126
c07a3ca7
MT
1127 def cleanup(self):
1128 if os.path.exists(self.buildroot):
1129 util.rm(self.buildroot)