2 ###############################################################################
4 # Pakfire - The IPFire package management system #
5 # Copyright (C) 2011 Pakfire development team #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
20 ###############################################################################
33 from . import _pakfire
37 from . import downloaders
39 from . import packages
40 from . import repository
45 log
= logging
.getLogger("pakfire.builder")
47 from .system
import system
48 from .constants
import *
50 from .errors
import BuildError
, BuildRootLocked
, Error
53 BUILD_LOG_HEADER
= """
55 | _ \ __ _| | __/ _(_)_ __ ___ | |__ _ _(_) | __| | ___ _ __
56 | |_) / _` | |/ / |_| | '__/ _ \ | '_ \| | | | | |/ _` |/ _ \ '__|
57 | __/ (_| | <| _| | | | __/ | |_) | |_| | | | (_| | __/ |
58 |_| \__,_|_|\_\_| |_|_| \___| |_.__/ \__,_|_|_|\__,_|\___|_|
61 Host : %(hostname)s (%(host_arch)s)
66 class Builder(object):
67 def __init__(self
, arch
=None, build_id
=None, logfile
=None, **kwargs
):
68 self
.config
= config
.Config("general.conf", "builder.conf")
70 distro_name
= self
.config
.get("builder", "distro", None)
72 self
.config
.read("distros/%s.conf" % distro_name
)
76 "enable_loop_devices" : self
.config
.get_bool("builder", "use_loop_devices", True),
77 "enable_ccache" : self
.config
.get_bool("builder", "use_ccache", True),
78 "buildroot_tmpfs" : self
.config
.get_bool("builder", "use_tmpfs", False),
79 "private_network" : self
.config
.get_bool("builder", "private_network", False),
82 # Get ccache settings.
83 if self
.settings
.get("enable_ccache", False):
84 self
.settings
.update({
85 "ccache_compress" : self
.config
.get_bool("ccache", "compress", True),
88 # Add settings from keyword arguments
89 self
.settings
.update(kwargs
)
92 self
.setup_logging(logfile
)
95 self
.build_id
= build_id
or "%s" % uuid
.uuid4()
98 self
.path
= os
.path
.join(BUILD_ROOT
, self
.build_id
)
101 # Architecture to build for
102 self
.arch
= arch
or _pakfire
.native_arch()
104 # Check if this host can build the requested architecture.
105 if not _pakfire
.arch_supported_by_host(self
.arch
):
106 raise BuildError(_("Cannot build for %s on this host") % self
.arch
)
109 self
.cgroup
= self
._make
_cgroup
()
112 # If this fails because the kernel has no support for CLONE_NEWIPC or CLONE_NEWUTS,
113 # we try to fall back to just set CLONE_NEWNS.
115 _pakfire
.unshare(_pakfire
.SCHED_CLONE_NEWNS|_pakfire
.SCHED_CLONE_NEWIPC|_pakfire
.SCHED_CLONE_NEWUTS
)
116 except RuntimeError as e
:
117 _pakfire
.unshare(_pakfire
.SCHED_CLONE_NEWNS
)
119 # Optionally enable private networking.
120 if self
.settings
.get("private_network", None):
121 _pakfire
.unshare(_pakfire
.SCHED_CLONE_NEWNET
)
124 self
.log
.debug("Entering %s" % self
.path
)
126 # Mount the directories
130 if e
.errno
== 30: # Read-only FS
131 raise BuildError("Buildroot is read-only: %s" % self
.path
)
133 # Raise all other errors
136 # Lock the build environment
142 # Setup domain name resolution in chroot
145 return BuilderContext(self
)
147 def __exit__(self
, type, value
, traceback
):
148 self
.log
.debug("Leaving %s" % self
.path
)
150 # Kill all remaining processes in the build environment
151 self
.cgroup
.killall()
154 self
.cgroup
.destroy()
157 # Umount the build environment
160 # Unlock build environment
166 def setup_logging(self
, logfile
):
168 self
.log
= log
.getChild(self
.build_id
)
169 # Propage everything to the root logger that we will see something
171 self
.log
.propagate
= 1
172 self
.log
.setLevel(logging
.INFO
)
174 # Add the given logfile to the logger.
175 h
= logging
.FileHandler(logfile
)
176 self
.log
.addHandler(h
)
178 # Format the log output for the file.
179 f
= logger
.BuildFormatter()
182 # If no logile was given, we use the root logger.
183 self
.log
= logging
.getLogger("pakfire")
185 def _make_cgroup(self
):
187 Initialises a cgroup so that we can enforce resource limits
188 and can identify processes belonging to this build environment.
190 # Find our current group
191 parent
= cgroups
.get_own_group()
194 cgroup
= parent
.create_subgroup("pakfire-%s" % self
.build_id
)
196 # Make this process join the new group
202 filename
= os
.path
.join(self
.path
, ".lock")
205 self
._lock
= open(filename
, "a+")
210 fcntl
.lockf(self
._lock
.fileno(), fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
212 raise BuildRootLocked("Buildroot is locked")
222 self
.log
.debug("Destroying environment %s" % self
.path
)
224 if os
.path
.exists(self
.path
):
228 def mountpoints(self
):
231 # Make root as a tmpfs if enabled.
232 if self
.settings
.get("buildroot_tmpfs"):
234 ("pakfire_root", "/", "tmpfs", "defaults"),
238 # src, dest, fs, options
239 ("pakfire_proc", "/proc", "proc", "nosuid,noexec,nodev"),
240 ("/proc/sys", "/proc/sys", "bind", "bind"),
241 ("/proc/sys", "/proc/sys", "bind", "bind,ro,remount"),
242 ("/sys", "/sys", "bind", "bind"),
243 ("/sys", "/sys", "bind", "bind,ro,remount"),
244 ("pakfire_tmpfs", "/dev", "tmpfs", "mode=755,nosuid"),
245 ("/dev/pts", "/dev/pts", "bind", "bind"),
246 ("pakfire_tmpfs", "/run", "tmpfs", "mode=755,nosuid,nodev"),
247 ("pakfire_tmpfs", "/tmp", "tmpfs", "mode=755,nosuid,nodev"),
250 # If selinux is enabled.
251 if os
.path
.exists("/sys/fs/selinux"):
253 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind"),
254 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind,ro,remount"),
257 # If ccache support is requested, we bind mount the cache.
258 if self
.settings
.get("enable_ccache"):
259 # Create ccache cache directory if it does not exist.
260 if not os
.path
.exists(CCACHE_CACHE_DIR
):
261 os
.makedirs(CCACHE_CACHE_DIR
)
264 (CCACHE_CACHE_DIR
, "/var/cache/ccache", "bind", "bind"),
270 self
.log
.debug("Mounting environment")
272 for src
, dest
, fs
, options
in self
.mountpoints
:
273 mountpoint
= self
.chrootPath(dest
)
275 options
= "-o %s" % options
277 # Eventually create mountpoint directory
278 if not os
.path
.exists(mountpoint
):
279 os
.makedirs(mountpoint
)
281 self
.execute_root("mount -n -t %s %s %s %s" % (fs
, options
, src
, mountpoint
), shell
=True)
283 def _umountall(self
):
284 self
.log
.debug("Umounting environment")
287 for src
, dest
, fs
, options
in reversed(self
.mountpoints
):
288 dest
= self
.chrootPath(dest
)
290 if not dest
in mountpoints
:
291 mountpoints
.append(dest
)
294 for mp
in mountpoints
:
296 self
.execute_root("umount -n %s" % mp
, shell
=True)
297 except ShellEnvironmentError
:
300 if not os
.path
.ismount(mp
):
301 mountpoints
.remove(mp
)
303 def copyin(self
, file_out
, file_in
):
304 if file_in
.startswith("/"):
305 file_in
= file_in
[1:]
307 file_in
= self
.chrootPath(file_in
)
309 #if not os.path.exists(file_out):
312 dir_in
= os
.path
.dirname(file_in
)
313 if not os
.path
.exists(dir_in
):
316 self
.log
.debug("%s --> %s" % (file_out
, file_in
))
318 shutil
.copy2(file_out
, file_in
)
320 def copyout(self
, file_in
, file_out
):
321 if file_in
.startswith("/"):
322 file_in
= file_in
[1:]
324 file_in
= self
.chrootPath(file_in
)
326 #if not os.path.exists(file_in):
329 dir_out
= os
.path
.dirname(file_out
)
330 if not os
.path
.exists(dir_out
):
333 self
.log
.debug("%s --> %s" % (file_in
, file_out
))
335 shutil
.copy2(file_in
, file_out
)
337 def populate_dev(self
):
351 # If we need loop devices (which are optional) we create them here.
352 if self
.settings
["enable_loop_devices"]:
353 for i
in range(0, 7):
354 nodes
.append("/dev/loop%d" % i
)
357 # Stat the original node of the host system and copy it to
360 node_stat
= os
.stat(node
)
362 # If it cannot be found, just go on.
366 self
._create
_node
(node
, node_stat
.st_mode
, node_stat
.st_rdev
)
368 os
.symlink("/proc/self/fd/0", self
.chrootPath("dev", "stdin"))
369 os
.symlink("/proc/self/fd/1", self
.chrootPath("dev", "stdout"))
370 os
.symlink("/proc/self/fd/2", self
.chrootPath("dev", "stderr"))
371 os
.symlink("/proc/self/fd", self
.chrootPath("dev", "fd"))
373 def chrootPath(self
, *args
):
374 # Remove all leading slashes
377 if arg
.startswith("/"):
382 ret
= os
.path
.join(self
.path
, *args
)
383 ret
= ret
.replace("//", "/")
385 assert ret
.startswith(self
.path
)
391 Add DNS resolution facility to chroot environment by copying
392 /etc/resolv.conf and /etc/hosts.
394 for i
in ("/etc/resolv.conf", "/etc/hosts"):
397 def _create_node(self
, filename
, mode
, device
):
398 self
.log
.debug("Create node: %s (%s)" % (filename
, mode
))
400 filename
= self
.chrootPath(filename
)
402 # Create parent directory if it is missing.
403 dirname
= os
.path
.dirname(filename
)
404 if not os
.path
.exists(dirname
):
407 os
.mknod(filename
, mode
, device
)
409 def execute_root(self
, command
, **kwargs
):
411 Executes the given command outside the build chroot.
413 shellenv
= shell
.ShellExecuteEnvironment(command
, logger
=self
.log
, **kwargs
)
419 class BuilderContext(object):
420 def __init__(self
, builder
):
421 self
.builder
= builder
423 # Get a reference to the logger
424 self
.log
= self
.builder
.log
426 # Initialise Pakfire instance
427 self
.pakfire
= base
.Pakfire(
428 path
=self
.builder
.path
,
429 config
=self
.builder
.config
,
430 distro
=self
.builder
.config
.distro
,
431 arch
=self
.builder
.arch
,
436 # Build a minimal environment for executing, but try to inherit TERM and LANG
439 "PATH" : "/usr/bin:/bin:/usr/sbin:/sbin",
440 "PS1" : "pakfire-chroot \w> ",
441 "TERM" : os
.environ
.get("TERM", "vt100"),
442 "LANG" : os
.environ
.get("LANG", "en_US.UTF-8"),
445 # Inherit environment from distro
446 env
.update(self
.pakfire
.distro
.environ
)
448 # ccache environment settings
449 if self
.builder
.settings
.get("enable_ccache", False):
450 compress
= self
.builder
.settings
.get("ccache_compress", False)
452 env
["CCACHE_COMPRESS"] = "1"
454 # Let ccache create its temporary files in /tmp.
455 env
["CCACHE_TEMPDIR"] = "/tmp"
457 # Fake UTS_MACHINE, when we cannot use the personality syscall and
458 # if the host architecture is not equal to the target architecture.
459 if not _pakfire
.native_arch() == self
.pakfire
.arch
:
461 "LD_PRELOAD" : "/usr/lib/libpakfire_preload.so",
462 "UTS_MACHINE" : self
.pakfire
.arch
,
467 def _install(self
, packages
):
468 self
.log
.debug(_("Installing packages in build environment:"))
469 for package
in packages
:
470 self
.log
.debug(" %s" % package
)
473 with self
.pakfire
as p
:
474 # Install all required packages
475 transaction
= p
.install(packages
)
477 # Dump transaction to log
478 t
= transaction
.dump()
481 # Download transaction
482 d
= downloaders
.TransactionDownloader(self
.pakfire
, transaction
)
485 # Run the transaction
488 def build(self
, package
, private_network
=True, shell
=True):
489 # Install build environment
494 # If we have ccache enabled, we need to install it, too
495 if self
.builder
.settings
.get("enable_ccache"):
496 packages
.append("ccache")
498 # Open the package archive
499 archive
= _pakfire
.Archive(self
.pakfire
, package
)
501 requires
= archive
.get("dependencies.requires")
502 packages
+= requires
.splitlines()
504 # Setup the environment including any build dependencies
505 self
._install
(packages
)
509 def shell(self
, install
=None):
510 if not util
.cli_is_interactive():
511 self
.log
.warning("Cannot run shell on non-interactive console.")
514 # Collect packages to install
517 # Install our standard shell packages
518 packages
+= SHELL_PACKAGES
520 # Install any packages the user requested
524 # Install all required packages
525 self
._install
(packages
)
528 self
.pakfire
.execute(["/usr/bin/bash", "--login"],
529 environ
=self
.environ
, enable_network
=True)