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