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