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