]> git.ipfire.org Git - pakfire.git/blame - pakfire/builder.py
autobuilder: Switch back to download source package from the webserver.
[pakfire.git] / pakfire / builder.py
CommitLineData
47a4cb89
MT
1#!/usr/bin/python
2
3import fcntl
4import grp
5import logging
d59bde4c 6import math
47a4cb89
MT
7import os
8import re
9import shutil
fc4d4177 10import socket
47a4cb89
MT
11import stat
12import time
f02283bb 13import uuid
47a4cb89 14
7c8f2953 15import base
93bd0aa4 16import chroot
47a4cb89
MT
17import depsolve
18import packages
fa6d335b 19import repository
47a4cb89
MT
20import transaction
21import util
22
23from constants import *
4496b160 24from i18n import _
e9c20259 25from errors import BuildError, BuildRootLocked, Error
47a4cb89
MT
26
27
28class Builder(object):
29 # The version of the kernel this machine is running.
30 kernel_version = os.uname()[2]
31
7c8f2953
MT
32 def __init__(self, pkg, distro_config=None, build_id=None, **pakfire_args):
33 pakfire_args.update({
34 "builder" : True,
35 })
47a4cb89 36
7c8f2953
MT
37 # Create pakfire instance.
38 self.pakfire = base.Pakfire(distro_config=distro_config, **pakfire_args)
39 self.distro = self.pakfire.distro
40 self.path = self.pakfire.path
41
42 # Open the package.
983f1a7d
MT
43 if pkg:
44 self.pkg = packages.open(self.pakfire, None, pkg)
a2e57fc2
MT
45
46 # Log the package information.
47 if not isinstance(self.pkg, packages.Makefile):
48 dump = self.pkg.dump(long=True)
49 self.log.info(dump)
983f1a7d
MT
50 else:
51 # No package was given.
52 self.pkg = None
7c8f2953
MT
53
54 # XXX need to make this configureable
47a4cb89
MT
55 self.settings = {
56 "enable_loop_devices" : True,
33f4679b 57 "enable_ccache" : True,
8c716255 58 "enable_icecream" : False,
47a4cb89 59 }
7c8f2953 60 #self.settings.update(settings)
47a4cb89
MT
61
62 self.buildroot = "/buildroot"
63
64 # Lock the buildroot
65 self._lock = None
66 self.lock()
67
fc4d4177
MT
68 # Save the build time.
69 self.build_time = int(time.time())
f02283bb
MT
70
71 # Save the build id and generate one if no build id was provided.
72 if not build_id:
73 build_id = uuid.uuid4()
74
fc4d4177
MT
75 self.build_id = build_id
76
7c8f2953
MT
77 @property
78 def arch(self):
79 """
80 Inherit architecture from distribution configuration.
81 """
82 return self.distro.arch
83
fc4d4177
MT
84 @property
85 def info(self):
86 return {
87 "build_date" : time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(self.build_time)),
88 "build_host" : socket.gethostname(),
89 "build_id" : self.build_id,
90 "build_time" : self.build_time,
91 }
92
47a4cb89
MT
93 def lock(self):
94 filename = os.path.join(self.path, ".lock")
95
96 try:
97 self._lock = open(filename, "a+")
98 except IOError, e:
99 return 0
100
101 try:
102 fcntl.lockf(self._lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
103 except IOError, e:
104 raise BuildRootLocked, "Buildroot is locked"
105
106 return 1
107
108 def unlock(self):
109 if self._lock:
110 self._lock.close()
111 self._lock = None
112
113 def copyin(self, file_out, file_in):
114 if file_in.startswith("/"):
115 file_in = file_in[1:]
116
117 file_in = self.chrootPath(file_in)
118
119 #if not os.path.exists(file_out):
120 # return
121
122 dir_in = os.path.dirname(file_in)
123 if not os.path.exists(dir_in):
124 os.makedirs(dir_in)
125
126 logging.debug("%s --> %s" % (file_out, file_in))
127
128 shutil.copy2(file_out, file_in)
129
130 def copyout(self, file_in, file_out):
131 if file_in.startswith("/"):
132 file_in = file_in[1:]
133
134 file_in = self.chrootPath(file_in)
135
136 #if not os.path.exists(file_in):
137 # return
138
139 dir_out = os.path.dirname(file_out)
140 if not os.path.exists(dir_out):
141 os.makedirs(dir_out)
142
143 logging.debug("%s --> %s" % (file_in, file_out))
144
145 shutil.copy2(file_in, file_out)
146
147 def copy_result(self, resultdir):
148 dir_in = self.chrootPath("result")
149
150 for dir, subdirs, files in os.walk(dir_in):
151 basename = os.path.basename(dir)
152 dir = dir[len(self.chrootPath()):]
153 for file in files:
154 file_in = os.path.join(dir, file)
155
156 file_out = os.path.join(
157 resultdir,
158 basename,
159 file,
160 )
161
162 self.copyout(file_in, file_out)
163
9c2ad426 164 def extract(self, requires=None, build_deps=True):
47a4cb89
MT
165 """
166 Gets a dependency set and extracts all packages
167 to the environment.
168 """
9c2ad426
MT
169 if not requires:
170 requires = []
5be98997 171
9c2ad426
MT
172 # Add neccessary build dependencies.
173 requires += BUILD_PACKAGES
3d960a21 174
33f4679b
MT
175 # If we have ccache enabled, we need to extract it
176 # to the build chroot.
177 if self.settings.get("enable_ccache"):
9c2ad426 178 requires.append("ccache")
33f4679b 179
5be98997
MT
180 # If we have icecream enabled, we need to extract it
181 # to the build chroot.
182 if self.settings.get("enable_icecream"):
9c2ad426 183 requires.append("icecream")
47a4cb89
MT
184
185 # Get build dependencies from source package.
186 if isinstance(self.pkg, packages.SourcePackage):
187 for req in self.pkg.requires:
9c2ad426 188 requires.append(req)
47a4cb89 189
9c2ad426
MT
190 # Install all packages.
191 self.install(requires)
47a4cb89
MT
192
193 # Copy the makefile and load source tarballs.
194 if isinstance(self.pkg, packages.Makefile):
195 self.pkg.extract(self)
196
4496b160
MT
197 elif isinstance(self.pkg, packages.SourcePackage):
198 self.pkg.extract(_("Extracting: %s (source)") % self.pkg.name,
199 prefix=os.path.join(self.path, "build"))
200
47a4cb89
MT
201 # If we have a makefile, we can only get the build dependencies
202 # after we have extracted all the rest.
203 if build_deps and isinstance(self.pkg, packages.Makefile):
204 requires = self.make_requires()
205 if not requires:
206 return
207
9c2ad426 208 self.install(requires)
47a4cb89 209
9c2ad426
MT
210 def install(self, requires):
211 """
212 Install everything that is required in requires.
213 """
c9ec78ca
MT
214 # If we got nothing to do, we quit immediately.
215 if not requires:
216 return
217
9c2ad426
MT
218 ds = depsolve.DependencySet(self.pakfire)
219 for r in requires:
c9ec78ca
MT
220 if isinstance(r, packages.BinaryPackage):
221 ds.add_package(r)
222 else:
223 ds.add_requires(r)
9c2ad426
MT
224 ds.resolve()
225 ds.dump()
226
227 ts = transaction.Transaction(self.pakfire, ds)
228 ts.run()
47a4cb89 229
c9ec78ca
MT
230 def install_test(self):
231 pkgs = []
232
233 # Connect packages to the FS repository.
234 r = repository.FileSystemRepository(self.pakfire)
235
236 for dir, subdirs, files in os.walk(self.chrootPath("result")):
237 for file in files:
238 file = os.path.join(dir, file)
239
240 if not file.endswith(PACKAGE_EXTENSION):
241 continue
242
243 p = packages.BinaryPackage(self.pakfire, r, file)
244 pkgs.append(p)
245
246 self.install(pkgs)
247
47a4cb89
MT
248 @property
249 def log(self):
250 # XXX for now, return the root logger
251 return logging.getLogger()
252
253 def chrootPath(self, *args):
254 # Remove all leading slashes
255 _args = []
256 for arg in args:
257 if arg.startswith("/"):
258 arg = arg[1:]
259 _args.append(arg)
260 args = _args
261
262 ret = os.path.join(self.path, *args)
263 ret = ret.replace("//", "/")
264
265 assert ret.startswith(self.path)
266
267 return ret
268
269 def prepare(self):
677ff42a
MT
270 prepared_tag = ".prepared"
271
272 if os.path.exists(self.chrootPath(prepared_tag)):
273 return
274
47a4cb89
MT
275 # Create directory.
276 if not os.path.exists(self.path):
277 os.makedirs(self.path)
278
279 # Create important directories.
280 dirs = [
281 "build",
282 self.buildroot,
283 "dev",
284 "dev/pts",
285 "dev/shm",
286 "etc",
287 "proc",
288 "result",
289 "sys",
290 "tmp",
291 "usr/src",
292 ]
33f4679b
MT
293
294 # Create cache dir if ccache is enabled.
295 if self.settings.get("enable_ccache"):
296 dirs.append("var/cache/ccache")
297
298 if not os.path.exists(CCACHE_CACHE_DIR):
299 os.makedirs(CCACHE_CACHE_DIR)
300
47a4cb89
MT
301 for dir in dirs:
302 dir = self.chrootPath(dir)
303 if not os.path.exists(dir):
304 os.makedirs(dir)
305
6378690e
MT
306 # Create neccessary files like /etc/fstab and /etc/mtab.
307 files = (
308 "etc/fstab",
677ff42a
MT
309 "etc/mtab",
310 prepared_tag,
6378690e
MT
311 )
312
313 for file in files:
314 file = self.chrootPath(file)
315 dir = os.path.dirname(file)
316 if not os.path.exists(dir):
317 os.makedirs(dir)
318 f = open(file, "w")
319 f.close()
320
47a4cb89 321 self._prepare_dev()
47a4cb89
MT
322 self._prepare_dns()
323
324 def _prepare_dev(self):
325 prevMask = os.umask(0000)
326
327 nodes = [
328 ("dev/null", stat.S_IFCHR | 0666, os.makedev(1, 3)),
329 ("dev/full", stat.S_IFCHR | 0666, os.makedev(1, 7)),
330 ("dev/zero", stat.S_IFCHR | 0666, os.makedev(1, 5)),
331 ("dev/random", stat.S_IFCHR | 0666, os.makedev(1, 8)),
332 ("dev/urandom", stat.S_IFCHR | 0444, os.makedev(1, 9)),
333 ("dev/tty", stat.S_IFCHR | 0666, os.makedev(5, 0)),
334 ("dev/console", stat.S_IFCHR | 0600, os.makedev(5, 1)),
335 ]
336
337 # If we need loop devices (which are optional) we create them here.
338 if self.settings["enable_loop_devices"]:
339 for i in range(0, 7):
340 nodes.append(("dev/loop%d" % i, stat.S_IFBLK | 0660, os.makedev(7, i)))
341
342 # Create all the nodes.
343 for node in nodes:
344 self._create_node(*node)
345
346 os.symlink("/proc/self/fd/0", self.chrootPath("dev", "stdin"))
347 os.symlink("/proc/self/fd/1", self.chrootPath("dev", "stdout"))
348 os.symlink("/proc/self/fd/2", self.chrootPath("dev", "stderr"))
349 os.symlink("/proc/self/fd", self.chrootPath("dev", "fd"))
350
351 # make device node for el4 and el5
352 if self.kernel_version < "2.6.19":
353 self._make_node("dev/ptmx", stat.S_IFCHR | 0666, os.makedev(5, 2))
354 else:
355 os.symlink("/dev/pts/ptmx", self.chrootPath("dev", "ptmx"))
356
357 os.umask(prevMask)
358
47a4cb89 359 def _prepare_dns(self):
18973967
MT
360 """
361 Add DNS resolution facility to chroot environment by copying
362 /etc/resolv.conf and /etc/hosts.
363 """
364 for i in ("/etc/resolv.conf", "/etc/hosts"):
365 self.copyin(i, i)
47a4cb89
MT
366
367 def _create_node(self, filename, mode, device):
368 logging.debug("Create node: %s (%s)" % (filename, mode))
369
370 filename = self.chrootPath(filename)
371
372 # Create parent directory if it is missing.
373 dirname = os.path.dirname(filename)
374 if not os.path.exists(dirname):
375 os.makedirs(dirname)
376
377 os.mknod(filename, mode, device)
378
526c3e7f 379 def destroy(self):
e412b8dc 380 logging.debug("Destroying environment %s" % self.path)
47a4cb89
MT
381
382 if os.path.exists(self.path):
383 util.rm(self.path)
384
e412b8dc
MT
385 def cleanup(self):
386 logging.debug("Cleaning environemnt.")
387
388 # Run make clean and let it cleanup its stuff.
389 self.make("clean")
390
391 # Remove the build directory and buildroot.
392 dirs = ("build", self.buildroot, "result")
393
394 for d in dirs:
395 d = self.chrootPath(d)
396 if not os.path.exists(d):
397 continue
398
399 util.rm(d)
400 os.makedirs(d)
401
402 # Clear make_info cache.
403 if hasattr(self, "_make_info"):
404 del self._make_info
405
47a4cb89
MT
406 def _mountall(self):
407 self.log.debug("Mounting environment")
408 for cmd, mountpoint in self.mountpoints:
409 cmd = "%s %s" % (cmd, self.chrootPath(mountpoint))
93bd0aa4 410 chroot.do(cmd, shell=True)
47a4cb89
MT
411
412 def _umountall(self):
413 self.log.debug("Umounting environment")
414 for cmd, mountpoint in self.mountpoints:
415 cmd = "umount -n %s" % self.chrootPath(mountpoint)
93bd0aa4 416 chroot.do(cmd, raiseExc=0, shell=True)
47a4cb89
MT
417
418 @property
419 def mountpoints(self):
420 ret = [
421 ("mount -n -t proc pakfire_chroot_proc", "proc"),
422 ("mount -n -t sysfs pakfire_chroot_sysfs", "sys"),
423 ]
424
425 mountopt = "gid=%d,mode=0620,ptmxmode=0666" % grp.getgrnam("tty").gr_gid
426 if self.kernel_version >= "2.6.29":
427 mountopt += ",newinstance"
428
429 ret.extend([
430 ("mount -n -t devpts -o %s pakfire_chroot_devpts" % mountopt, "dev/pts"),
431 ("mount -n -t tmpfs pakfire_chroot_shmfs", "dev/shm"),
432 ])
433
33f4679b
MT
434 if self.settings.get("enable_ccache"):
435 ret.append(("mount -n --bind %s" % CCACHE_CACHE_DIR, "var/cache/ccache"))
436
47a4cb89
MT
437 return ret
438
d59bde4c
MT
439 @staticmethod
440 def calc_parallelism():
441 """
442 Calculate how many processes to run
443 at the same time.
444
445 We take the log10(number of processors) * factor
446 """
447 num = os.sysconf("SC_NPROCESSORS_CONF")
448 if num == 1:
449 return 2
450 else:
451 return int(round(math.log10(num) * 26))
452
47a4cb89
MT
453 @property
454 def environ(self):
455 env = {
0cf10c2d
MT
456 # Add HOME manually, because it is occasionally not set
457 # and some builds get in trouble then.
458 "HOME" : "/root",
89ebac67
MT
459 "TERM" : os.environ.get("TERM", "dumb"),
460 "PS1" : "\u:\w\$ ",
0cf10c2d 461
47a4cb89 462 "BUILDROOT" : self.buildroot,
d59bde4c 463 "PARALLELISMFLAGS" : "-j%s" % self.calc_parallelism(),
47a4cb89
MT
464 }
465
466 # Inherit environment from distro
467 env.update(self.pakfire.distro.environ)
468
5be98997
MT
469 # Icecream environment settings
470 if self.settings.get("enable_icecream", None):
471 # Set the toolchain path
472 if self.settings.get("icecream_toolchain", None):
473 env["ICECC_VERSION"] = self.settings.get("icecream_toolchain")
474
475 # Set preferred host if configured.
476 if self.settings.get("icecream_preferred_host", None):
477 env["ICECC_PREFERRED_HOST"] = \
478 self.settings.get("icecream_preferred_host")
479
47a4cb89
MT
480 # XXX what do we need else?
481
482 return env
483
484 def do(self, command, shell=True, personality=None, *args, **kwargs):
485 ret = None
486 try:
487 # Environment variables
488 env = self.environ
489
490 if kwargs.has_key("env"):
491 env.update(kwargs.pop("env"))
492
15398910
MT
493 logging.debug("Environment:")
494 for k, v in sorted(env.items()):
495 logging.debug(" %s=%s" % (k, v))
496
e360ea59
MT
497 # Update personality it none was set
498 if not personality:
7c8f2953 499 personality = self.distro.personality
e360ea59 500
5be98997
MT
501 # Make every shell to a login shell because we set a lot of
502 # environment things there.
503 if shell:
504 command = ["bash", "--login", "-c", command]
505
47a4cb89
MT
506 self._mountall()
507
508 if not kwargs.has_key("chrootPath"):
509 kwargs["chrootPath"] = self.chrootPath()
510
c96185e9 511 ret = chroot.do(
47a4cb89
MT
512 command,
513 personality=personality,
5be98997 514 shell=False,
47a4cb89
MT
515 env=env,
516 logger=self.log,
517 *args,
518 **kwargs
519 )
520
521 finally:
522 self._umountall()
523
524 return ret
525
526 def make(self, *args, **kwargs):
4496b160
MT
527 if isinstance(self.pkg, packages.Makefile):
528 filename = os.path.basename(self.pkg.filename)
529 elif isinstance(self.pkg, packages.SourcePackage):
530 filename = "%s.%s" % (self.pkg.name, MAKEFILE_EXTENSION)
531
532 return self.do("make -f /build/%s %s" % (filename, " ".join(args)),
5be98997 533 **kwargs)
47a4cb89
MT
534
535 @property
536 def make_info(self):
537 if not hasattr(self, "_make_info"):
538 info = {}
539
540 output = self.make("buildinfo", returnOutput=True)
541
542 for line in output.splitlines():
543 # XXX temporarily
544 if not line:
545 break
546
547 m = re.match(r"^(\w+)=(.*)$", line)
548 if not m:
549 continue
550
551 info[m.group(1)] = m.group(2).strip("\"")
552
553 self._make_info = info
554
555 return self._make_info
556
557 @property
558 def packages(self):
559 if hasattr(self, "_packages"):
560 return self._packages
561
562 pkgs = []
563 output = self.make("packageinfo", returnOutput=True)
564
565 pkg = {}
566 for line in output.splitlines():
567 if not line:
568 pkgs.append(pkg)
569 pkg = {}
570
571 m = re.match(r"^(\w+)=(.*)$", line)
572 if not m:
573 continue
574
575 k, v = m.groups()
576 pkg[k] = v.strip("\"")
577
3723913b
MT
578 # Create a dummy repository to link the virtual packages to
579 repo = repository.DummyRepository(self.pakfire)
580
47a4cb89
MT
581 self._packages = []
582 for pkg in pkgs:
fa6d335b 583 pkg = packages.VirtualPackage(self.pakfire, pkg) # XXX had to remove repo here?!
47a4cb89
MT
584 self._packages.append(pkg)
585
586 return self._packages
587
588 def make_requires(self):
589 return self.make_info.get("PKG_BUILD_DEPS", "").split()
590
591 def make_sources(self):
592 return self.make_info.get("PKG_FILES", "").split()
593
5be98997
MT
594 def create_icecream_toolchain(self):
595 if not self.settings.get("enable_icecream", None):
596 return
597
598 out = self.do("icecc --build-native", returnOutput=True)
599
600 for line in out.splitlines():
601 m = re.match(r"^creating ([a-z0-9]+\.tar\.gz)", line)
602 if m:
603 self.settings["icecream_toolchain"] = "/%s" % m.group(1)
604
47a4cb89 605 def build(self):
5be98997
MT
606 self.create_icecream_toolchain()
607
e9c20259
MT
608 try:
609 self.make("build")
610
611 except Error:
612 raise BuildError, "The build command failed."
47a4cb89
MT
613
614 for pkg in reversed(self.packages):
d507be4d 615 packager = packages.BinaryPackager(self.pakfire, pkg, self)
47a4cb89
MT
616 packager()
617
618 def dist(self):
619 self.pkg.dist(self)
620
621 def shell(self, args=[]):
e9c20259
MT
622 if not util.cli_is_interactive():
623 logging.warning("Cannot run shell on non-interactive console.")
624 return
625
9c2ad426
MT
626 # Install all packages that are needed to run a shell.
627 self.install(SHELL_PACKAGES)
628
47a4cb89 629 # XXX need to set CFLAGS here
a7596ccf 630 command = "/usr/sbin/chroot %s /usr/bin/chroot-shell %s" % \
47a4cb89
MT
631 (self.chrootPath(), " ".join(args))
632
e360ea59
MT
633 # Add personality if we require one
634 if self.pakfire.distro.personality:
a7596ccf
MT
635 command = "%s %s" % (self.pakfire.distro.personality, command)
636
637 for key, val in self.environ.items():
638 command = "%s=\"%s\" " % (key, val) + command
e360ea59 639
47a4cb89 640 # Empty the environment
a7596ccf 641 command = "env -i - %s" % command
47a4cb89
MT
642
643 logging.debug("Shell command: %s" % command)
644
645 try:
646 self._mountall()
647
648 shell = os.system(command)
649 return os.WEXITSTATUS(shell)
650
651 finally:
652 self._umountall()