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