]>
git.ipfire.org Git - people/stevee/pakfire.git/blob - python/pakfire/builder.py
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 ###############################################################################
37 import packages
.packager
43 log
= logging
.getLogger("pakfire")
45 from system
import system
46 from constants
import *
48 from errors
import BuildError
, BuildRootLocked
, Error
51 BUILD_LOG_HEADER
= """
53 | _ \ __ _| | __/ _(_)_ __ ___ | |__ _ _(_) | __| | ___ _ __
54 | |_) / _` | |/ / |_| | '__/ _ \ | '_ \| | | | | |/ _` |/ _ \ '__|
55 | __/ (_| | <| _| | | | __/ | |_) | |_| | | | (_| | __/ |
56 |_| \__,_|_|\_\_| |_|_| \___| |_.__/ \__,_|_|_|\__,_|\___|_|
59 Host : %(hostname)s (%(host_arch)s)
64 class BuildEnviron(object):
65 # The version of the kernel this machine is running.
66 kernel_version
= os
.uname()[2]
68 def __init__(self
, filename
=None, distro_config
=None, build_id
=None, logfile
=None,
69 builder_mode
="release", use_cache
=None, **pakfire_args
):
71 assert builder_mode
in ("development", "release",)
72 self
.mode
= builder_mode
74 # Disable the build repository in release mode.
75 if self
.mode
== "release":
76 if pakfire_args
.has_key("disable_repos") and pakfire_args
["disable_repos"]:
77 pakfire_args
["disable_repos"] += ["build",]
79 pakfire_args
["disable_repos"] = ["build",]
81 # Save the build id and generate one if no build id was provided.
83 build_id
= "%s" % uuid
.uuid4()
85 self
.build_id
= build_id
89 self
.log
= log
.getChild(self
.build_id
)
90 # Propage everything to the root logger that we will see something
92 self
.log
.propagate
= 1
93 self
.log
.setLevel(logging
.INFO
)
95 # Add the given logfile to the logger.
96 h
= logging
.FileHandler(logfile
)
97 self
.log
.addHandler(h
)
99 # Format the log output for the file.
100 f
= logger
.BuildFormatter()
103 # If no logile was given, we use the root logger.
104 self
.log
= logging
.getLogger("pakfire")
106 # Log information about pakfire and some more information, when we
107 # are running in release mode.
108 if self
.mode
== "release":
110 "host_arch" : system
.arch
,
111 "hostname" : system
.hostname
,
112 "time" : time
.strftime("%a, %d %b %Y %H:%M:%S +0000", time
.gmtime()),
113 "version" : "Pakfire %s" % PAKFIRE_VERSION
,
116 for line
in BUILD_LOG_HEADER
.splitlines():
117 self
.log
.info(line
% logdata
)
119 # Create pakfire instance.
120 if pakfire_args
.has_key("mode"):
121 del pakfire_args
["mode"]
122 self
.pakfire
= base
.Pakfire(mode
="builder", distro_config
=distro_config
, **pakfire_args
)
123 self
.distro
= self
.pakfire
.distro
124 self
.path
= self
.pakfire
.path
126 # Check if this host can build the requested architecture.
127 if not self
.arch
in self
.pakfire
.config
.supported_arches
:
128 raise BuildError
, _("Cannot build for %s on this host.") % self
.arch
130 # Where do we put the result?
131 self
.resultdir
= os
.path
.join(self
.path
, "result")
133 # Check weather to use or not use the cache.
134 if use_cache
is None:
135 # If use_cache is None, the user did not provide anything and
137 if self
.mode
== "development":
142 self
.use_cache
= use_cache
145 # If we have a plain makefile, we first build a source package and go with that.
147 if filename
.endswith(".%s" % MAKEFILE_EXTENSION
):
148 pkg
= packages
.Makefile(self
.pakfire
, filename
)
149 filename
= pkg
.dist(os
.path
.join(self
.resultdir
, "src"))
151 assert os
.path
.exists(filename
), filename
153 # Open source package.
154 self
.pkg
= packages
.SourcePackage(self
.pakfire
, None, filename
)
155 assert self
.pkg
, filename
157 # Log the package information.
158 self
.log
.info(_("Package information:"))
159 for line
in self
.pkg
.dump(long=True).splitlines():
160 self
.log
.info(" %s" % line
)
163 # Path where we extract the package and put all the source files.
164 self
.build_dir
= os
.path
.join(self
.path
, "usr/src/packages", self
.pkg
.friendly_name
)
169 # XXX need to make this configureable
171 "enable_loop_devices" : True,
172 "enable_ccache" : True,
173 "enable_icecream" : False,
175 #self.settings.update(settings)
181 # Save the build time.
182 self
.build_time
= int(time
.time())
185 # Mount the directories.
191 # Setup domain name resolution in chroot.
194 # Extract all needed packages.
198 # Kill all still running processes.
199 util
.orphans_kill(self
.path
)
201 # Close pakfire instance.
204 # Umount the build environment.
213 Inherit architecture from distribution configuration.
215 return self
.distro
.arch
220 "build_date" : time
.strftime("%a, %d %b %Y %H:%M:%S +0000", time
.gmtime(self
.build_time
)),
221 "build_host" : socket
.gethostname(),
222 "build_id" : self
.build_id
,
223 "build_time" : self
.build_time
,
227 filename
= os
.path
.join(self
.path
, ".lock")
230 self
._lock
= open(filename
, "a+")
235 fcntl
.lockf(self
._lock
.fileno(), fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
237 raise BuildRootLocked
, "Buildroot is locked"
246 def copyin(self
, file_out
, file_in
):
247 if file_in
.startswith("/"):
248 file_in
= file_in
[1:]
250 file_in
= self
.chrootPath(file_in
)
252 #if not os.path.exists(file_out):
255 dir_in
= os
.path
.dirname(file_in
)
256 if not os
.path
.exists(dir_in
):
259 self
.log
.debug("%s --> %s" % (file_out
, file_in
))
261 shutil
.copy2(file_out
, file_in
)
263 def copyout(self
, file_in
, file_out
):
264 if file_in
.startswith("/"):
265 file_in
= file_in
[1:]
267 file_in
= self
.chrootPath(file_in
)
269 #if not os.path.exists(file_in):
272 dir_out
= os
.path
.dirname(file_out
)
273 if not os
.path
.exists(dir_out
):
276 self
.log
.debug("%s --> %s" % (file_in
, file_out
))
278 shutil
.copy2(file_in
, file_out
)
280 def copy_result(self
, resultdir
):
281 dir_in
= self
.chrootPath("result")
283 for dir, subdirs
, files
in os
.walk(dir_in
):
284 basename
= os
.path
.basename(dir)
285 dir = dir[len(self
.chrootPath()):]
287 file_in
= os
.path
.join(dir, file)
289 file_out
= os
.path
.join(
295 self
.copyout(file_in
, file_out
)
297 def extract(self
, requires
=None, build_deps
=True):
299 Gets a dependency set and extracts all packages
305 if self
.use_cache
and os
.path
.exists(self
.cache_file
):
306 # If we are told to use the cache, we just import the
310 # Add neccessary build dependencies.
311 requires
+= BUILD_PACKAGES
313 # If we have ccache enabled, we need to extract it
314 # to the build chroot.
315 if self
.settings
.get("enable_ccache"):
316 requires
.append("ccache")
318 # If we have icecream enabled, we need to extract it
319 # to the build chroot.
320 if self
.settings
.get("enable_icecream"):
321 requires
.append("icecream")
323 # Get build dependencies from source package.
325 for req
in self
.pkg
.requires
:
328 # Install all packages.
329 self
.log
.info(_("Install packages needed for build..."))
330 self
.install(requires
)
332 # Copy the makefile and load source tarballs.
334 self
.pkg
.extract(_("Extracting"), prefix
=self
.build_dir
)
336 def install(self
, requires
):
338 Install everything that is required in requires.
340 # If we got nothing to do, we quit immediately.
345 self
.pakfire
.install(requires
, interactive
=False,
346 allow_downgrade
=True, logger
=self
.log
)
348 # Catch dependency errors and log it.
349 except DependencyError
, e
:
352 def install_test(self
):
354 for dir, subdirs
, files
in os
.walk(self
.chrootPath("result")):
356 pkgs
.append(os
.path
.join(dir, file))
358 self
.pakfire
.localinstall(pkgs
, yes
=True, allow_uninstall
=True)
360 def chrootPath(self
, *args
):
361 # Remove all leading slashes
364 if arg
.startswith("/"):
369 ret
= os
.path
.join(self
.path
, *args
)
370 ret
= ret
.replace("//", "/")
372 assert ret
.startswith(self
.path
)
376 def populate_dev(self
):
390 # If we need loop devices (which are optional) we create them here.
391 if self
.settings
["enable_loop_devices"]:
392 for i
in range(0, 7):
393 nodes
.append("/dev/loop%d" % i
)
396 # Stat the original node of the host system and copy it to
398 node_stat
= os
.stat(node
)
400 self
._create
_node
(node
, node_stat
.st_mode
, node_stat
.st_rdev
)
402 os
.symlink("/proc/self/fd/0", self
.chrootPath("dev", "stdin"))
403 os
.symlink("/proc/self/fd/1", self
.chrootPath("dev", "stdout"))
404 os
.symlink("/proc/self/fd/2", self
.chrootPath("dev", "stderr"))
405 os
.symlink("/proc/self/fd", self
.chrootPath("dev", "fd"))
409 Add DNS resolution facility to chroot environment by copying
410 /etc/resolv.conf and /etc/hosts.
412 for i
in ("/etc/resolv.conf", "/etc/hosts"):
415 def _create_node(self
, filename
, mode
, device
):
416 self
.log
.debug("Create node: %s (%s)" % (filename
, mode
))
418 filename
= self
.chrootPath(filename
)
420 # Create parent directory if it is missing.
421 dirname
= os
.path
.dirname(filename
)
422 if not os
.path
.exists(dirname
):
425 os
.mknod(filename
, mode
, device
)
428 self
.log
.debug("Destroying environment %s" % self
.path
)
430 if os
.path
.exists(self
.path
):
434 self
.log
.debug("Cleaning environemnt.")
436 # Remove the build directory and buildroot.
437 dirs
= (self
.build_dir
, self
.chrootPath("result"),)
440 if not os
.path
.exists(d
):
447 self
.log
.debug("Mounting environment")
448 for src
, dest
, fs
, options
in self
.mountpoints
:
449 mountpoint
= self
.chrootPath(dest
)
451 options
= "-o %s" % options
453 # Eventually create mountpoint directory
454 if not os
.path
.exists(mountpoint
):
455 os
.makedirs(mountpoint
)
457 cmd
= "mount -n -t %s %s %s %s" % \
458 (fs
, options
, src
, mountpoint
)
459 chroot
.do(cmd
, shell
=True)
461 def _umountall(self
):
462 self
.log
.debug("Umounting environment")
465 for src
, dest
, fs
, options
in reversed(self
.mountpoints
):
466 if not dest
in mountpoints
:
467 mountpoints
.append(dest
)
469 for dest
in mountpoints
:
470 mountpoint
= self
.chrootPath(dest
)
472 chroot
.do("umount -n %s" % mountpoint
, raiseExc
=0, shell
=True)
475 def mountpoints(self
):
478 # Make root as a tmpfs.
480 # ("pakfire_root", "/", "tmpfs", "defaults"),
484 # src, dest, fs, options
485 ("pakfire_proc", "/proc", "proc", "nosuid,noexec,nodev"),
486 ("/proc/sys", "/proc/sys", "bind", "bind"),
487 ("/proc/sys", "/proc/sys", "bind", "bind,ro,remount"),
488 ("/sys", "/sys", "bind", "bind"),
489 ("/sys", "/sys", "bind", "bind,ro,remount"),
490 ("pakfire_tmpfs", "/dev", "tmpfs", "mode=755,nosuid"),
491 ("/dev/pts", "/dev/pts", "bind", "bind"),
492 ("pakfire_tmpfs", "/run", "tmpfs", "mode=755,nosuid,nodev"),
495 # If selinux is enabled.
496 if os
.path
.exists("/sys/fs/selinux"):
498 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind"),
499 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind,ro,remount"),
502 # If ccache support is requested, we bind mount the cache.
503 if self
.settings
.get("enable_ccache"):
504 # Create ccache cache directory if it does not exist.
505 if not os
.path
.exists(CCACHE_CACHE_DIR
):
506 os
.makedirs(CCACHE_CACHE_DIR
)
509 (CCACHE_CACHE_DIR
, "/var/cache/ccache", "bind", "bind"),
517 # Add HOME manually, because it is occasionally not set
518 # and some builds get in trouble then.
520 "TERM" : os
.environ
.get("TERM", "dumb"),
523 # Set the container that we can detect, if we are inside a
525 "container" : "pakfire-builder",
528 # Inherit environment from distro
529 env
.update(self
.pakfire
.distro
.environ
)
531 # Icecream environment settings
532 if self
.settings
.get("enable_icecream", False):
533 # Set the toolchain path
534 if self
.settings
.get("icecream_toolchain", None):
535 env
["ICECC_VERSION"] = self
.settings
.get("icecream_toolchain")
537 # Set preferred host if configured.
538 if self
.settings
.get("icecream_preferred_host", None):
539 env
["ICECC_PREFERRED_HOST"] = \
540 self
.settings
.get("icecream_preferred_host")
542 # Fake UTS_MACHINE, when we cannot use the personality syscall and
543 # if the host architecture is not equal to the target architecture.
544 if not self
.pakfire
.distro
.personality
and \
545 not self
.pakfire
.config
.host_arch
== self
.pakfire
.distro
.arch
:
547 "LD_PRELOAD" : "/usr/lib/libpakfire_preload.so",
548 "UTS_MACHINE" : self
.pakfire
.distro
.arch
,
554 def installed_packages(self
):
556 Returns an iterator over all installed packages in this build environment.
558 # Get the repository of all installed packages.
559 repo
= self
.pakfire
.repos
.get_repo("@system")
561 # Return an iterator over the packages.
564 def do(self
, command
, shell
=True, personality
=None, logger
=None, *args
, **kwargs
):
567 # Environment variables
570 if kwargs
.has_key("env"):
571 env
.update(kwargs
.pop("env"))
573 self
.log
.debug("Environment:")
574 for k
, v
in sorted(env
.items()):
575 self
.log
.debug(" %s=%s" % (k
, v
))
577 # Update personality it none was set
579 personality
= self
.distro
.personality
581 # Make every shell to a login shell because we set a lot of
582 # environment things there.
584 command
= ["bash", "--login", "-c", command
]
586 if not kwargs
.has_key("chrootPath"):
587 kwargs
["chrootPath"] = self
.chrootPath()
591 personality
=personality
,
601 def build(self
, install_test
=True):
603 raise BuildError
, _("You cannot run a build when no package was given.")
605 # Search for the package file in build_dir and raise BuildError if it is not present.
606 pkgfile
= os
.path
.join(self
.build_dir
, "%s.%s" % (self
.pkg
.name
, MAKEFILE_EXTENSION
))
607 if not os
.path
.exists(pkgfile
):
608 raise BuildError
, _("Could not find makefile in build root: %s") % pkgfile
609 pkgfile
= "/%s" % os
.path
.relpath(pkgfile
, self
.chrootPath())
611 resultdir
= self
.chrootPath("/result")
613 # Create the build command, that is executed in the chroot.
614 build_command
= ["/usr/lib/pakfire/builder", "--offline",
615 "build", pkgfile
, "--arch", self
.arch
, "--nodeps",
616 "--resultdir=/result",]
619 self
.do(" ".join(build_command
), logger
=self
.log
)
622 raise BuildError
, _("The build command failed. See logfile for details.")
624 # Perform install test.
628 # Copy the final packages and stuff.
631 def shell(self
, args
=[]):
632 if not util
.cli_is_interactive():
633 self
.log
.warning("Cannot run shell on non-interactive console.")
636 # Install all packages that are needed to run a shell.
637 self
.install(SHELL_PACKAGES
)
639 # XXX need to set CFLAGS here
640 command
= "/usr/sbin/chroot %s %s %s" % \
641 (self
.chrootPath(), SHELL_SCRIPT
, " ".join(args
))
643 # Add personality if we require one
644 if self
.pakfire
.distro
.personality
:
645 command
= "%s %s" % (self
.pakfire
.distro
.personality
, command
)
647 for key
, val
in self
.environ
.items():
648 command
= "%s=\"%s\" " % (key
, val
) + command
650 # Empty the environment
651 command
= "env -i - %s" % command
653 self
.log
.debug("Shell command: %s" % command
)
655 shell
= os
.system(command
)
656 return os
.WEXITSTATUS(shell
)
659 def cache_file(self
):
661 self
.pakfire
.distro
.sname
, # name of the distribution
662 self
.pakfire
.distro
.release
, # release version
663 self
.pakfire
.distro
.arch
, # architecture
666 return os
.path
.join(CACHE_ENVIRON_DIR
, "%s.cache" %"-".join(comps
))
668 def cache_export(self
, filename
):
669 # Sync all disk caches.
672 # A list to store all mountpoints, so we don't package them.
675 # A list containing all files we want to package.
678 # Walk through the whole tree and collect all files
679 # that are on the same disk (not crossing mountpoints).
680 log
.info(_("Creating filelist..."))
681 root
= self
.chrootPath()
682 for dir, subdirs
, files
in os
.walk(root
):
683 # Search for mountpoints and skip them.
684 if not dir == root
and os
.path
.ismount(dir):
685 mountpoints
.append(dir)
688 # Skip all directories under mountpoints.
689 if any([dir.startswith(m
) for m
in mountpoints
]):
692 # Add all other files.
695 file = os
.path
.join(dir, file)
696 filelist
.append(file)
698 # Create a nice progressbar.
699 p
= util
.make_progress(_("Compressing files..."), len(filelist
))
702 # Create tar file and add all files to it.
703 f
= packages
.file.InnerTarFile
.open(filename
, "w:gz")
704 for file in filelist
:
709 f
.add(file, os
.path
.relpath(file, root
), recursive
=False)
712 # Finish progressbar.
716 filesize
= os
.path
.getsize(filename
)
718 log
.info(_("Cache file was successfully created at %s.") % filename
)
719 log
.info(_(" Containing %(files)s files, it has a size of %(size)s.") % \
720 { "files" : len(filelist
), "size" : util
.format_size(filesize
), })
722 def cache_extract(self
):
723 root
= self
.chrootPath()
724 filename
= self
.cache_file
726 f
= packages
.file.InnerTarFile
.open(filename
, "r:gz")
727 members
= f
.getmembers()
729 # Make a nice progress bar as always.
730 p
= util
.make_progress(_("Extracting files..."), len(members
))
732 # Extract all files from the cache.
734 for member
in members
:
739 f
.extract(member
, path
=root
)
742 # Finish progressbar.
746 # Re-read local repository.
747 self
.pakfire
.repos
.local
.update(force
=True)
749 # Update all packages.
750 self
.log
.info(_("Updating packages from cache..."))
751 self
.pakfire
.update(interactive
=False, logger
=self
.log
,
752 allow_archchange
=True, allow_vendorchange
=True, allow_downgrade
=True)
755 class Builder(object):
756 def __init__(self
, pakfire
, filename
, resultdir
, **kwargs
):
757 self
.pakfire
= pakfire
759 self
.filename
= filename
761 self
.resultdir
= resultdir
764 self
.pkg
= packages
.Makefile(self
.pakfire
, self
.filename
)
772 Create a temporary file in the build environment.
774 file = "/tmp/pakfire_%s" % util
.random_string()
784 return self
.pkg
.buildroot
788 return self
.pakfire
.distro
794 # Get all definitions from the package.
795 environ
.update(self
.pkg
.exports
)
797 # Overwrite some definitions by default values.
798 environ
.update(self
._environ
)
802 def do(self
, command
, shell
=True, personality
=None, cwd
=None, *args
, **kwargs
):
803 # Environment variables
804 log
.debug("Environment:")
805 for k
, v
in sorted(self
.environ
.items()):
806 log
.debug(" %s=%s" % (k
, v
))
808 # Update personality it none was set
810 personality
= self
.distro
.personality
813 cwd
= "/%s" % LOCAL_TMP_PATH
815 # Make every shell to a login shell because we set a lot of
816 # environment things there.
818 command
= ["bash", "--login", "-c", command
]
822 personality
=personality
,
825 logger
=logging
.getLogger("pakfire"),
831 def create_icecream_toolchain(self
):
833 out
= self
.do("icecc --build-native 2>/dev/null", returnOutput
=True, cwd
="/tmp")
837 for line
in out
.splitlines():
838 m
= re
.match(r
"^creating ([a-z0-9]+\.tar\.gz)", line
)
840 self
._environ
["ICECC_VERSION"] = "/tmp/%s" % m
.group(1)
842 def create_buildscript(self
, stage
):
843 file = "/tmp/build_%s" % util
.random_string()
845 # Get buildscript from the package.
846 script
= self
.pkg
.get_buildscript(stage
)
848 # Write script to an empty file.
850 f
.write("#!/bin/sh\n\n")
853 f
.write("\n%s\n" % script
)
861 # Create buildroot and remove all content if it was existant.
862 util
.rm(self
.buildroot
)
863 os
.makedirs(self
.buildroot
)
865 # Build icecream toolchain if icecream is installed.
866 self
.create_icecream_toolchain()
868 for stage
in ("prepare", "build", "test", "install"):
869 self
.build_stage(stage
)
871 # Run post-build stuff.
872 self
.post_compress_man_pages()
873 self
.post_remove_static_libs()
874 self
.post_extract_debuginfo()
876 # Package the result.
877 # Make all these little package from the build environment.
878 log
.info(_("Creating packages:"))
880 for pkg
in reversed(self
.pkg
.packages
):
881 packager
= packages
.packager
.BinaryPackager(self
.pakfire
, pkg
,
882 self
, self
.buildroot
)
883 pkg
= packager
.run(self
.resultdir
)
887 for pkg
in sorted(pkgs
):
888 for line
in pkg
.dump(long=True).splitlines():
893 def build_stage(self
, stage
):
894 # Get the buildscript for this stage.
895 buildscript
= self
.create_buildscript(stage
)
897 # Execute the buildscript of this stage.
898 log
.info(_("Running stage %s:") % stage
)
901 self
.do(buildscript
, shell
=False)
904 # Remove the buildscript.
905 if os
.path
.exists(buildscript
):
906 os
.unlink(buildscript
)
908 def post_remove_static_libs(self
):
909 keep_libs
= self
.pkg
.lexer
.build
.get_var("keep_libraries")
910 keep_libs
= keep_libs
.split()
913 self
.do("%s/remove-static-libs %s %s" % \
914 (SCRIPT_DIR
, self
.buildroot
, " ".join(keep_libs
)))
916 log
.warning(_("Could not remove static libraries: %s") % e
)
918 def post_compress_man_pages(self
):
920 self
.do("%s/compress-man-pages %s" % (SCRIPT_DIR
, self
.buildroot
))
922 log
.warning(_("Compressing man pages did not complete successfully."))
924 def post_extract_debuginfo(self
):
927 # Check if we need to run with strict build-id.
928 strict_id
= self
.pkg
.lexer
.build
.get_var("debuginfo_strict_build_id", "true")
929 if strict_id
in ("true", "yes", "1"):
930 args
.append("--strict-build-id")
932 args
.append("--buildroot=%s" % self
.pkg
.buildroot
)
933 args
.append("--sourcedir=%s" % self
.pkg
.sourcedir
)
935 # Get additional options to pass to script.
936 options
= self
.pkg
.lexer
.build
.get_var("debuginfo_options", "")
937 args
+= options
.split()
940 self
.do("%s/extract-debuginfo %s %s" % (SCRIPT_DIR
, " ".join(args
), self
.pkg
.buildroot
))
942 log
.error(_("Extracting debuginfo did not complete with success. Aborting build."))
946 if os
.path
.exists(self
.buildroot
):
947 util
.rm(self
.buildroot
)