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