]> git.ipfire.org Git - pakfire.git/blobdiff - pakfire/builder.py
Misc. fixes on extraction and packaging.
[pakfire.git] / pakfire / builder.py
index 23efb12679f860e7b986f9dd0edae29acf704c80..1cc433caa1282cd33a940a0e4428c6f4efe34f09 100644 (file)
 #!/usr/bin/python
+###############################################################################
+#                                                                             #
+# Pakfire - The IPFire package management system                              #
+# Copyright (C) 2011 Pakfire development team                                 #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
 
 import fcntl
 import grp
 import logging
+import math
 import os
 import re
 import shutil
+import socket
 import stat
 import time
+import uuid
 
-import depsolve
+import base
+import chroot
+import logger
 import packages
-import transaction
+import packages.packager
+import repository
 import util
 
 from constants import *
-from errors import BuildRootLocked
+from i18n import _
+from errors import BuildError, BuildRootLocked, Error
 
 
-class Builder(object):
+BUILD_LOG_HEADER = """
+ ____       _     __ _            _           _ _     _
+|  _ \ __ _| | __/ _(_)_ __ ___  | |__  _   _(_) | __| | ___ _ __
+| |_) / _` | |/ / |_| | '__/ _ \ | '_ \| | | | | |/ _` |/ _ \ '__|
+|  __/ (_| |   <|  _| | | |  __/ | |_) | |_| | | | (_| |  __/ |
+|_|   \__,_|_|\_\_| |_|_|  \___| |_.__/ \__,_|_|_|\__,_|\___|_|
+
+       Time    : %(time)s
+       Host    : %(host)s
+       Version : %(version)s
+
+"""
+
+class BuildEnviron(object):
        # The version of the kernel this machine is running.
        kernel_version = os.uname()[2]
 
-       def __init__(self, pakfire, pkg):
-               self.pakfire = pakfire
-               self.pkg = pkg
-
+       def __init__(self, pkg=None, distro_config=None, build_id=None, logfile=None,
+                       builder_mode="release", **pakfire_args):
+               # Set mode.
+               assert builder_mode in ("development", "release",)
+               self.mode = builder_mode
+
+               # Disable the build repository in release mode.
+               if self.mode == "release":
+                       if pakfire_args.has_key("disable_repos") and pakfire_args["disable_repos"]:
+                               pakfire_args["disable_repos"] += ["build",]
+                       else:
+                               pakfire_args["disable_repos"] = ["build",]
+
+               # Save the build id and generate one if no build id was provided.
+               if not build_id:
+                       build_id = "%s" % uuid.uuid4()
+
+               self.build_id = build_id
+
+               # Setup the logging.
+               if logfile:
+                       self.log = logging.getLogger(self.build_id)
+                       # Propage everything to the root logger that we will see something
+                       # on the terminal.
+                       self.log.propagate = 1
+                       self.log.setLevel(logging.INFO)
+
+                       # Add the given logfile to the logger.
+                       h = logging.FileHandler(logfile)
+                       self.log.addHandler(h)
+
+                       # Format the log output for the file.
+                       f = logger.BuildFormatter()
+                       h.setFormatter(f)
+               else:
+                       # If no logile was given, we use the root logger.
+                       self.log = logging.getLogger()
+
+               # Log information about pakfire and some more information, when we
+               # are running in release mode.
+               if self.mode == "release":
+                       logdata = {
+                               "host"    : socket.gethostname(),
+                               "time"    : time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()),
+                               "version" : "Pakfire %s" % PAKFIRE_VERSION,
+                       }
+
+                       for line in BUILD_LOG_HEADER.splitlines():
+                               self.log.info(line % logdata)
+
+               # Create pakfire instance.
+               if pakfire_args.has_key("mode"):
+                       del pakfire_args["mode"]
+               self.pakfire = base.Pakfire(mode="builder", distro_config=distro_config, **pakfire_args)
+               self.distro = self.pakfire.distro
+               self.path = self.pakfire.path
+
+               # Log the package information.
+               self.pkg = packages.Makefile(self.pakfire, pkg)
+               self.log.info(_("Package information:"))
+               for line in self.pkg.dump(long=True).splitlines():
+                       self.log.info("  %s" % line)
+               self.log.info("")
+
+               # XXX need to make this configureable
                self.settings = {
                        "enable_loop_devices" : True,
+                       "enable_ccache"   : True,
+                       "enable_icecream" : False,
                }
+               #self.settings.update(settings)
 
                self.buildroot = "/buildroot"
 
@@ -36,12 +139,24 @@ class Builder(object):
                self._lock = None
                self.lock()
 
-               # Initialize the environment
-               self.prepare()
+               # Save the build time.
+               self.build_time = int(time.time())
 
        @property
-       def path(self):
-               return self.pakfire.path
+       def arch(self):
+               """
+                       Inherit architecture from distribution configuration.
+               """
+               return self.distro.arch
+
+       @property
+       def info(self):
+               return {
+                       "build_date" : time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(self.build_time)),
+                       "build_host" : socket.gethostname(),
+                       "build_id"   : self.build_id,
+                       "build_time" : self.build_time,
+               }
 
        def lock(self):
                filename = os.path.join(self.path, ".lock")
@@ -114,49 +229,102 @@ class Builder(object):
 
                                self.copyout(file_in, file_out)
 
-       def extract(self, requires=[], build_deps=True):
+       def get_pwuid(self, uid):
+               users = {}
+
+               f = open(self.chrootPath("/etc/passwd"))
+               for line in f.readlines():
+                       m = re.match(r"^([a-z][a-z0-9_\-]{,30}):x:(\d+):(\d+):(.*):(.*)$", line)
+                       if not m:
+                               continue
+
+                       item = {
+                               "name"  : m.group(1),
+                               "uid"   : int(m.group(2)),
+                               "gid"   : int(m.group(3)),
+                               "home"  : m.group(4),
+                               "shell" : m.group(5),
+                       }
+
+                       assert not users.has_key(item["uid"])
+                       users[item["uid"]] = item
+
+               f.close()
+
+               return users.get(uid, None)
+
+       def get_grgid(self, gid):
+               groups = {}
+
+               f = open(self.chrootPath("/etc/group"))
+               for line in f.readlines():
+                       m = re.match(r"^([a-z][a-z0-9_\-]{,30}):x:(\d+):(.*)$", line)
+                       if not m:
+                               continue
+
+                       item = {
+                               "name"  : m.group(1),
+                               "gid"   : int(m.group(2)),
+                       }
+
+                       # XXX re-enable later
+                       #assert not groups.has_key(item["gid"])
+                       groups[item["gid"]] = item
+
+               f.close()
+
+               return groups.get(gid, None)
+
+       def extract(self, requires=None, build_deps=True):
                """
                        Gets a dependency set and extracts all packages
                        to the environment.
                """
-               ds = depsolve.DependencySet(self.pakfire)
-               for p in BUILD_PACKAGES + requires:
-                       ds.add_requires(p)
-               ds.resolve()
+               if not requires:
+                       requires = []
+
+               # Add neccessary build dependencies.
+               requires += BUILD_PACKAGES
+
+               # If we have ccache enabled, we need to extract it
+               # to the build chroot.
+               if self.settings.get("enable_ccache"):
+                       requires.append("ccache")
+
+               # If we have icecream enabled, we need to extract it
+               # to the build chroot.
+               if self.settings.get("enable_icecream"):
+                       requires.append("icecream")
 
                # Get build dependencies from source package.
-               if isinstance(self.pkg, packages.SourcePackage):
-                       for req in self.pkg.requires:
-                               ds.add_requires(req)
+               for req in self.pkg.requires:
+                       requires.append(req)
 
-               ts = transaction.TransactionSet(self.pakfire, ds)
-               ts.dump()
-               ts.run()
+               # Install all packages.
+               self.install(requires)
 
                # Copy the makefile and load source tarballs.
-               if isinstance(self.pkg, packages.Makefile):
-                       self.pkg.extract(self)
+               self.pkg.extract(_("Extracting"),
+                       prefix=os.path.join(self.path, "build"))
 
-               # If we have a makefile, we can only get the build dependencies
-               # after we have extracted all the rest.
-               if build_deps and isinstance(self.pkg, packages.Makefile):
-                       requires = self.make_requires()
-                       if not requires:
-                               return
+       def install(self, requires):
+               """
+                       Install everything that is required in requires.
+               """
+               # If we got nothing to do, we quit immediately.
+               if not requires:
+                       return
 
-                       ds = depsolve.DependencySet(self.pakfire)
-                       for r in requires:
-                               ds.add_requires(r)
-                       ds.resolve()
+               self.pakfire.install(requires, interactive=False,
+                       allow_downgrade=True, logger=self.log)
 
-                       ts = transaction.TransactionSet(self.pakfire, ds)
-                       ts.dump()
-                       ts.run()
+       def install_test(self):
+               pkgs = []
+               for dir, subdirs, files in os.walk(self.chrootPath("result")):
+                       for file in files:
+                               pkgs.append(os.path.join(dir, file))
 
-       @property
-       def log(self):
-               # XXX for now, return the root logger
-               return logging.getLogger()
+               self.pakfire.localinstall(pkgs, yes=True)
 
        def chrootPath(self, *args):
                # Remove all leading slashes
@@ -175,6 +343,11 @@ class Builder(object):
                return ret
 
        def prepare(self):
+               prepared_tag = ".prepared"
+
+               if os.path.exists(self.chrootPath(prepared_tag)):
+                       return
+
                # Create directory.
                if not os.path.exists(self.path):
                        os.makedirs(self.path)
@@ -193,13 +366,35 @@ class Builder(object):
                        "tmp",
                        "usr/src",
                ]
+
+               # Create cache dir if ccache is enabled.
+               if self.settings.get("enable_ccache"):
+                       dirs.append("var/cache/ccache")
+
+                       if not os.path.exists(CCACHE_CACHE_DIR):
+                               os.makedirs(CCACHE_CACHE_DIR)
+
                for dir in dirs:
                        dir = self.chrootPath(dir)
                        if not os.path.exists(dir):
                                os.makedirs(dir)
 
+               # Create neccessary files like /etc/fstab and /etc/mtab.
+               files = (
+                       "etc/fstab",
+                       "etc/mtab",
+                       prepared_tag,
+               )
+
+               for file in files:
+                       file = self.chrootPath(file)
+                       dir = os.path.dirname(file)
+                       if not os.path.exists(dir):
+                               os.makedirs(dir)
+                       f = open(file, "w")
+                       f.close()
+
                self._prepare_dev()
-               self._prepare_users()
                self._prepare_dns()
 
        def _prepare_dev(self):
@@ -237,17 +432,6 @@ class Builder(object):
 
                os.umask(prevMask)
 
-       def _prepare_users(self):
-               f = open(self.chrootPath("etc", "passwd"), "w")
-               f.write("root:x:0:0:root:/root:/bin/bash\n")
-               f.write("nobody:x:99:99:Nobody:/:/sbin/nologin\n")
-               f.close()
-
-               f = open(self.chrootPath("etc", "group"), "w")
-               f.write("root:x:0:root\n")
-               f.write("nobody:x:99:\n")
-               f.close()
-
        def _prepare_dns(self):
                """
                        Add DNS resolution facility to chroot environment by copying
@@ -268,23 +452,39 @@ class Builder(object):
 
                os.mknod(filename, mode, device)
 
-       def cleanup(self):
-               logging.debug("Cleanup environment %s" % self.path)
+       def destroy(self):
+               util.orphans_kill(self.path)
+
+               logging.debug("Destroying environment %s" % self.path)
 
                if os.path.exists(self.path):
                        util.rm(self.path)
 
+       def cleanup(self):
+               logging.debug("Cleaning environemnt.")
+
+               # Remove the build directory and buildroot.
+               dirs = ("build", self.buildroot, "result")
+
+               for d in dirs:
+                       d = self.chrootPath(d)
+                       if not os.path.exists(d):
+                               continue
+
+                       util.rm(d)
+                       os.makedirs(d)
+
        def _mountall(self):
                self.log.debug("Mounting environment")
                for cmd, mountpoint in self.mountpoints:
                        cmd = "%s %s" % (cmd, self.chrootPath(mountpoint))
-                       util.do(cmd, shell=True)
+                       chroot.do(cmd, shell=True)
 
        def _umountall(self):
                self.log.debug("Umounting environment")
                for cmd, mountpoint in self.mountpoints:
                        cmd = "umount -n %s" % self.chrootPath(mountpoint)
-                       util.do(cmd, raiseExc=0, shell=True)
+                       chroot.do(cmd, raiseExc=0, shell=True)
 
        @property
        def mountpoints(self):
@@ -302,22 +502,43 @@ class Builder(object):
                        ("mount -n -t tmpfs pakfire_chroot_shmfs", "dev/shm"),
                ])
 
+               if self.settings.get("enable_ccache"):
+                       ret.append(("mount -n --bind %s" % CCACHE_CACHE_DIR, "var/cache/ccache"))
+
                return ret
 
        @property
        def environ(self):
                env = {
+                       # Add HOME manually, because it is occasionally not set
+                       # and some builds get in trouble then.
+                       "HOME" : "/root",
+                       "TERM" : os.environ.get("TERM", "dumb"),
+                       "PS1"  : "\u:\w\$ ",
+
                        "BUILDROOT" : self.buildroot,
+                       "PARALLELISMFLAGS" : "-j%s" % util.calc_parallelism(),
                }
 
                # Inherit environment from distro
                env.update(self.pakfire.distro.environ)
 
+               # Icecream environment settings
+               if self.settings.get("enable_icecream", False):
+                       # Set the toolchain path
+                       if self.settings.get("icecream_toolchain", None):
+                               env["ICECC_VERSION"] = self.settings.get("icecream_toolchain")
+
+                       # Set preferred host if configured.
+                       if self.settings.get("icecream_preferred_host", None):
+                               env["ICECC_PREFERRED_HOST"] = \
+                                       self.settings.get("icecream_preferred_host")
+
                # XXX what do we need else?
 
                return env
 
-       def do(self, command, shell=True, personality=None, *args, **kwargs):
+       def do(self, command, shell=True, personality=None, logger=None, *args, **kwargs):
                ret = None
                try:
                        # Environment variables
@@ -326,21 +547,30 @@ class Builder(object):
                        if kwargs.has_key("env"):
                                env.update(kwargs.pop("env"))
 
+                       logging.debug("Environment:")
+                       for k, v in sorted(env.items()):
+                               logging.debug("  %s=%s" % (k, v))
+
                        # Update personality it none was set
                        if not personality:
-                               personality = self.pakfire.distro.personality
+                               personality = self.distro.personality
+
+                       # Make every shell to a login shell because we set a lot of
+                       # environment things there.
+                       if shell:
+                               command = ["bash", "--login", "-c", command]
 
                        self._mountall()
 
                        if not kwargs.has_key("chrootPath"):
                                kwargs["chrootPath"] = self.chrootPath()
 
-                       ret = util.do(
+                       ret = chroot.do(
                                command,
                                personality=personality,
-                               shell=shell,
+                               shell=False,
                                env=env,
-                               logger=self.log,
+                               logger=logger,
                                *args,
                                **kwargs
                        )
@@ -350,102 +580,187 @@ class Builder(object):
 
                return ret
 
-       def make(self, *args, **kwargs):
-               command = ["bash", "--login", "-c",]
-               command.append("make -f /build/%s %s" % \
-                       (os.path.basename(self.pkg.filename), " ".join(args)))
+       def build(self, install_test=True):
+               assert self.pkg
 
-               return self.do(command, shell=False, **kwargs)
+               pkgfile = os.path.join("/build", os.path.basename(self.pkg.filename))
+               resultdir = self.chrootPath("/result")
 
-       @property
-       def make_info(self):
-               if not hasattr(self, "_make_info"):
-                       info = {}
+               # Create the build command, that is executed in the chroot.
+               build_command = ["pakfire-build2", "--offline", "build", pkgfile,
+                       "--nodeps", "--resultdir=/result",]
 
-                       output = self.make("buildinfo", returnOutput=True)
+               try:
+                       self.do(" ".join(build_command), logger=self.log)
 
-                       for line in output.splitlines():
-                               # XXX temporarily
-                               if not line:
-                                       break
+               except Error:
+                       raise BuildError, _("The build command failed. See logfile for details.")
 
-                               m = re.match(r"^(\w+)=(.*)$", line)
-                               if not m:
-                                       continue
+               # Perform install test.
+               if install_test:
+                       self.install_test()
 
-                               info[m.group(1)] = m.group(2).strip("\"")
+               # Copy the final packages and stuff.
+               # XXX TODO resultdir
 
-                       self._make_info = info
+       def shell(self, args=[]):
+               if not util.cli_is_interactive():
+                       logging.warning("Cannot run shell on non-interactive console.")
+                       return
 
-               return self._make_info
+               # Install all packages that are needed to run a shell.
+               self.install(SHELL_PACKAGES)
 
-       @property
-       def packages(self):
-               if hasattr(self, "_packages"):
-                       return self._packages
+               # XXX need to set CFLAGS here
+               command = "/usr/sbin/chroot %s /usr/bin/chroot-shell %s" % \
+                       (self.chrootPath(), " ".join(args))
 
-               pkgs = []
-               output = self.make("packageinfo", returnOutput=True)
+               # Add personality if we require one
+               if self.pakfire.distro.personality:
+                       command = "%s %s" % (self.pakfire.distro.personality, command)
 
-               pkg = {}
-               for line in output.splitlines():
-                       if not line:
-                               pkgs.append(pkg)
-                               pkg = {}
+               for key, val in self.environ.items():
+                       command = "%s=\"%s\" " % (key, val) + command
 
-                       m = re.match(r"^(\w+)=(.*)$", line)
-                       if not m:
-                               continue
+               # Empty the environment
+               command = "env -i - %s" % command
 
-                       k, v = m.groups()
-                       pkg[k] = v.strip("\"")
+               logging.debug("Shell command: %s" % command)
 
-               self._packages = []
-               for pkg in pkgs:
-                       pkg = packages.VirtualPackage(pkg)
-                       self._packages.append(pkg)
+               try:
+                       self._mountall()
 
-               return self._packages
+                       shell = os.system(command)
+                       return os.WEXITSTATUS(shell)
 
-       def make_requires(self):
-               return self.make_info.get("PKG_BUILD_DEPS", "").split()
+               finally:
+                       self._umountall()
 
-       def make_sources(self):
-               return self.make_info.get("PKG_FILES", "").split()
+# XXX backwards compatibilty
+Builder = BuildEnviron
 
-       def build(self):
-               self.make("build")
+class Builder2(object):
+       def __init__(self, pakfire, filename, resultdir, **kwargs):
+               self.pakfire = pakfire
 
-               for pkg in reversed(self.packages):
-                       packager = packages.Packager(self.pakfire, pkg, self)
-                       packager()
+               self.filename = filename
 
-       def dist(self):
-               self.pkg.dist(self)
+               self.resultdir = resultdir
 
-       def shell(self, args=[]):
-               # XXX need to add linux32 or linux64 to the command line
-               # XXX need to set CFLAGS here
-               command = "chroot %s /usr/bin/chroot-shell %s" % \
-                       (self.chrootPath(), " ".join(args))
+               # Open package file.
+               self.pkg = packages.Makefile(self.pakfire, self.filename)
 
-               for key, val in self.environ.items():
-                       command = "%s=\"%s\" " % (key, val) + command
-
-               # Add personality if we require one
-               if self.pakfire.distro.personality:
-                       command = "%s %s" % (self.pakfire.disto.personality, command)
+               #self.buildroot = "/tmp/pakfire_buildroot/%s" % util.random_string(20)
+               self.buildroot = "/buildroot"
 
-               # Empty the environment
-               #command = "env -i - %s" % command
+               self._environ = {
+                       "BUILDROOT" : self.buildroot,
+                       "LANG"      : "C",
+               }
 
-               logging.debug("Shell command: %s" % command)
+       @property
+       def distro(self):
+               return self.pakfire.distro
 
+       @property
+       def environ(self):
+               environ = os.environ
+               environ.update(self._environ)
+
+               return environ
+
+       def do(self, command, shell=True, personality=None, cwd=None, *args, **kwargs):
+               # Environment variables
+               logging.debug("Environment:")
+               for k, v in sorted(self.environ.items()):
+                       logging.debug("  %s=%s" % (k, v))
+
+               # Update personality it none was set
+               if not personality:
+                       personality = self.distro.personality
+
+               if not cwd:
+                       cwd = "/%s" % LOCAL_TMP_PATH
+
+               # Make every shell to a login shell because we set a lot of
+               # environment things there.
+               if shell:
+                       command = ["bash", "--login", "-c", command]
+
+               return chroot.do(
+                       command,
+                       personality=personality,
+                       shell=False,
+                       env=self.environ,
+                       logger=logging.getLogger(),
+                       cwd=cwd,
+                       *args,
+                       **kwargs
+               )
+
+       def create_icecream_toolchain(self):
                try:
-                       self._mountall()
+                       out = self.do("icecc --build-native", returnOutput=True)
+               except Error:
+                       return
+
+               for line in out.splitlines():
+                       m = re.match(r"^creating ([a-z0-9]+\.tar\.gz)", line)
+                       if m:
+                               self._environ["icecream_toolchain"] = "/%s" % m.group(1)
+
+       def create_buildscript(self, stage):
+               file = "/tmp/build_%s" % util.random_string()
+
+               # Get buildscript from the package.
+               script = self.pkg.get_buildscript(stage)
+
+               # Write script to an empty file.
+               f = open(file, "w")
+               f.write("#!/bin/sh\n\n")
+               f.write("set -e\n")
+               f.write("set -x\n")
+               f.write("\n%s\n" % script)
+               f.write("exit 0\n")
+               f.close()
+               os.chmod(file, 700)
 
-                       shell = os.system(command)
-                       return os.WEXITSTATUS(shell)
+               return file
+
+       def build(self):
+               # Create buildroot.
+               if not os.path.exists(self.buildroot):
+                       os.makedirs(self.buildroot)
+
+               # Build icecream toolchain if icecream is installed.
+               self.create_icecream_toolchain()
+
+               for stage in ("prepare", "build", "test", "install"):
+                       self.build_stage(stage)
+
+               # Package the result.
+               # Make all these little package from the build environment.
+               logging.info(_("Creating packages:"))
+               for pkg in reversed(self.pkg.packages):
+                       packager = packages.packager.BinaryPackager(self.pakfire, pkg, self.buildroot)
+                       packager.run(self.resultdir)
+               logging.info("")
+
+       def build_stage(self, stage):
+               # Get the buildscript for this stage.
+               buildscript = self.create_buildscript(stage)
+
+               # Execute the buildscript of this stage.
+               logging.info(_("Running stage %s:") % stage)
+
+               try:
+                       self.do(buildscript, shell=False)
 
                finally:
-                       self._umountall()
+                       # Remove the buildscript.
+                       if os.path.exists(buildscript):
+                               os.unlink(buildscript)
+
+       def cleanup(self):
+               if os.path.exists(self.buildroot):
+                       util.rm(self.buildroot)