]>
git.ipfire.org Git - 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 constants
import *
47 from errors
import BuildError
, BuildRootLocked
, Error
50 BUILD_LOG_HEADER
= """
52 | _ \ __ _| | __/ _(_)_ __ ___ | |__ _ _(_) | __| | ___ _ __
53 | |_) / _` | |/ / |_| | '__/ _ \ | '_ \| | | | | |/ _` |/ _ \ '__|
54 | __/ (_| | <| _| | | | __/ | |_) | |_| | | | (_| | __/ |
55 |_| \__,_|_|\_\_| |_|_| \___| |_.__/ \__,_|_|_|\__,_|\___|_|
63 class BuildEnviron(object):
64 # The version of the kernel this machine is running.
65 kernel_version
= os
.uname()[2]
67 def __init__(self
, filename
=None, distro_config
=None, build_id
=None, logfile
=None,
68 builder_mode
="release", use_cache
=None, **pakfire_args
):
70 assert builder_mode
in ("development", "release",)
71 self
.mode
= builder_mode
73 # Disable the build repository in release mode.
74 if self
.mode
== "release":
75 if pakfire_args
.has_key("disable_repos") and pakfire_args
["disable_repos"]:
76 pakfire_args
["disable_repos"] += ["build",]
78 pakfire_args
["disable_repos"] = ["build",]
80 # Save the build id and generate one if no build id was provided.
82 build_id
= "%s" % uuid
.uuid4()
84 self
.build_id
= build_id
88 self
.log
= logging
.getLogger(self
.build_id
)
89 # Propage everything to the root logger that we will see something
91 self
.log
.propagate
= 1
92 self
.log
.setLevel(logging
.INFO
)
94 # Add the given logfile to the logger.
95 h
= logging
.FileHandler(logfile
)
96 self
.log
.addHandler(h
)
98 # Format the log output for the file.
99 f
= logger
.BuildFormatter()
102 # If no logile was given, we use the root logger.
103 self
.log
= logging
.getLogger("pakfire")
105 # Log information about pakfire and some more information, when we
106 # are running in release mode.
107 if self
.mode
== "release":
109 "host" : socket
.gethostname(),
110 "time" : time
.strftime("%a, %d %b %Y %H:%M:%S +0000", time
.gmtime()),
111 "version" : "Pakfire %s" % PAKFIRE_VERSION
,
114 for line
in BUILD_LOG_HEADER
.splitlines():
115 self
.log
.info(line
% logdata
)
117 # Create pakfire instance.
118 if pakfire_args
.has_key("mode"):
119 del pakfire_args
["mode"]
120 self
.pakfire
= base
.Pakfire(mode
="builder", distro_config
=distro_config
, **pakfire_args
)
121 self
.distro
= self
.pakfire
.distro
122 self
.path
= self
.pakfire
.path
124 # Check if this host can build the requested architecture.
125 if not self
.arch
in self
.pakfire
.config
.supported_arches
:
126 raise BuildError
, _("Cannot build for %s on this host.") % self
.arch
128 # Where do we put the result?
129 self
.resultdir
= os
.path
.join(self
.path
, "result")
131 # Check weather to use or not use the cache.
132 if use_cache
is None:
133 # If use_cache is None, the user did not provide anything and
135 if self
.mode
== "development":
140 self
.use_cache
= use_cache
143 # If we have a plain makefile, we first build a source package and go with that.
145 if filename
.endswith(".%s" % MAKEFILE_EXTENSION
):
146 pkg
= packages
.Makefile(self
.pakfire
, filename
)
147 pkg
.dist([self
.resultdir
,])
149 filename
= os
.path
.join(self
.resultdir
, "src", pkg
.package_filename
)
150 assert os
.path
.exists(filename
), filename
152 # Open source package.
153 self
.pkg
= packages
.SourcePackage(self
.pakfire
, None, filename
)
154 assert self
.pkg
, filename
156 # Log the package information.
157 self
.log
.info(_("Package information:"))
158 for line
in self
.pkg
.dump(long=True).splitlines():
159 self
.log
.info(" %s" % line
)
162 # Path where we extract the package and put all the source files.
163 self
.build_dir
= os
.path
.join(self
.path
, "usr/src/packages", self
.pkg
.friendly_name
)
168 # XXX need to make this configureable
170 "enable_loop_devices" : True,
171 "enable_ccache" : True,
172 "enable_icecream" : False,
174 #self.settings.update(settings)
180 # Save the build time.
181 self
.build_time
= int(time
.time())
184 # Mount the directories.
190 # Setup domain name resolution in chroot.
193 # Extract all needed packages.
197 # Kill all still running processes.
198 util
.orphans_kill(self
.path
)
200 # Close pakfire instance.
203 # Umount the build environment.
212 Inherit architecture from distribution configuration.
214 return self
.distro
.arch
219 "build_date" : time
.strftime("%a, %d %b %Y %H:%M:%S +0000", time
.gmtime(self
.build_time
)),
220 "build_host" : socket
.gethostname(),
221 "build_id" : self
.build_id
,
222 "build_time" : self
.build_time
,
226 filename
= os
.path
.join(self
.path
, ".lock")
229 self
._lock
= open(filename
, "a+")
234 fcntl
.lockf(self
._lock
.fileno(), fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
236 raise BuildRootLocked
, "Buildroot is locked"
245 def copyin(self
, file_out
, file_in
):
246 if file_in
.startswith("/"):
247 file_in
= file_in
[1:]
249 file_in
= self
.chrootPath(file_in
)
251 #if not os.path.exists(file_out):
254 dir_in
= os
.path
.dirname(file_in
)
255 if not os
.path
.exists(dir_in
):
258 self
.log
.debug("%s --> %s" % (file_out
, file_in
))
260 shutil
.copy2(file_out
, file_in
)
262 def copyout(self
, file_in
, file_out
):
263 if file_in
.startswith("/"):
264 file_in
= file_in
[1:]
266 file_in
= self
.chrootPath(file_in
)
268 #if not os.path.exists(file_in):
271 dir_out
= os
.path
.dirname(file_out
)
272 if not os
.path
.exists(dir_out
):
275 self
.log
.debug("%s --> %s" % (file_in
, file_out
))
277 shutil
.copy2(file_in
, file_out
)
279 def copy_result(self
, resultdir
):
280 dir_in
= self
.chrootPath("result")
282 for dir, subdirs
, files
in os
.walk(dir_in
):
283 basename
= os
.path
.basename(dir)
284 dir = dir[len(self
.chrootPath()):]
286 file_in
= os
.path
.join(dir, file)
288 file_out
= os
.path
.join(
294 self
.copyout(file_in
, file_out
)
296 def extract(self
, requires
=None, build_deps
=True):
298 Gets a dependency set and extracts all packages
304 if self
.use_cache
and os
.path
.exists(self
.cache_file
):
305 # If we are told to use the cache, we just import the
309 # Add neccessary build dependencies.
310 requires
+= BUILD_PACKAGES
312 # If we have ccache enabled, we need to extract it
313 # to the build chroot.
314 if self
.settings
.get("enable_ccache"):
315 requires
.append("ccache")
317 # If we have icecream enabled, we need to extract it
318 # to the build chroot.
319 if self
.settings
.get("enable_icecream"):
320 requires
.append("icecream")
322 # Get build dependencies from source package.
324 for req
in self
.pkg
.requires
:
327 # Install all packages.
328 self
.log
.info(_("Install packages needed for build..."))
329 self
.install(requires
)
331 # Copy the makefile and load source tarballs.
333 self
.pkg
.extract(_("Extracting"), prefix
=self
.build_dir
)
335 def install(self
, requires
):
337 Install everything that is required in requires.
339 # If we got nothing to do, we quit immediately.
343 self
.pakfire
.install(requires
, interactive
=False,
344 allow_downgrade
=True, logger
=self
.log
)
346 def install_test(self
):
348 for dir, subdirs
, files
in os
.walk(self
.chrootPath("result")):
350 pkgs
.append(os
.path
.join(dir, file))
352 self
.pakfire
.localinstall(pkgs
, yes
=True, allow_uninstall
=True)
354 def chrootPath(self
, *args
):
355 # Remove all leading slashes
358 if arg
.startswith("/"):
363 ret
= os
.path
.join(self
.path
, *args
)
364 ret
= ret
.replace("//", "/")
366 assert ret
.startswith(self
.path
)
370 def populate_dev(self
):
384 # If we need loop devices (which are optional) we create them here.
385 if self
.settings
["enable_loop_devices"]:
386 for i
in range(0, 7):
387 nodes
.append("/dev/loop%d" % i
)
390 # Stat the original node of the host system and copy it to
392 node_stat
= os
.stat(node
)
394 self
._create
_node
(node
, node_stat
.st_mode
, node_stat
.st_rdev
)
396 os
.symlink("/proc/self/fd/0", self
.chrootPath("dev", "stdin"))
397 os
.symlink("/proc/self/fd/1", self
.chrootPath("dev", "stdout"))
398 os
.symlink("/proc/self/fd/2", self
.chrootPath("dev", "stderr"))
399 os
.symlink("/proc/self/fd", self
.chrootPath("dev", "fd"))
403 Add DNS resolution facility to chroot environment by copying
404 /etc/resolv.conf and /etc/hosts.
406 for i
in ("/etc/resolv.conf", "/etc/hosts"):
409 def _create_node(self
, filename
, mode
, device
):
410 self
.log
.debug("Create node: %s (%s)" % (filename
, mode
))
412 filename
= self
.chrootPath(filename
)
414 # Create parent directory if it is missing.
415 dirname
= os
.path
.dirname(filename
)
416 if not os
.path
.exists(dirname
):
419 os
.mknod(filename
, mode
, device
)
422 self
.log
.debug("Destroying environment %s" % self
.path
)
424 if os
.path
.exists(self
.path
):
428 self
.log
.debug("Cleaning environemnt.")
430 # Remove the build directory and buildroot.
431 dirs
= (self
.build_dir
, self
.chrootPath("result"),)
434 if not os
.path
.exists(d
):
441 self
.log
.debug("Mounting environment")
442 for src
, dest
, fs
, options
in self
.mountpoints
:
443 mountpoint
= self
.chrootPath(dest
)
445 options
= "-o %s" % options
447 # Eventually create mountpoint directory
448 if not os
.path
.exists(mountpoint
):
449 os
.makedirs(mountpoint
)
451 cmd
= "mount -n -t %s %s %s %s" % \
452 (fs
, options
, src
, mountpoint
)
453 chroot
.do(cmd
, shell
=True)
455 def _umountall(self
):
456 self
.log
.debug("Umounting environment")
459 for src
, dest
, fs
, options
in reversed(self
.mountpoints
):
460 if not dest
in mountpoints
:
461 mountpoints
.append(dest
)
463 for dest
in mountpoints
:
464 mountpoint
= self
.chrootPath(dest
)
466 chroot
.do("umount -n %s" % mountpoint
, raiseExc
=0, shell
=True)
469 def mountpoints(self
):
472 # Make root as a tmpfs.
474 # ("pakfire_root", "/", "tmpfs", "defaults"),
478 # src, dest, fs, options
479 ("pakfire_proc", "/proc", "proc", "nosuid,noexec,nodev"),
480 ("/proc/sys", "/proc/sys", "bind", "bind"),
481 ("/proc/sys", "/proc/sys", "bind", "bind,ro,remount"),
482 ("/sys", "/sys", "bind", "bind"),
483 ("/sys", "/sys", "bind", "bind,ro,remount"),
484 ("pakfire_tmpfs", "/dev", "tmpfs", "mode=755,nosuid"),
485 ("/dev/pts", "/dev/pts", "bind", "bind"),
486 ("pakfire_tmpfs", "/run", "tmpfs", "mode=755,nosuid,nodev"),
489 # If selinux is enabled.
490 if os
.path
.exists("/sys/fs/selinux"):
492 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind"),
493 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind,ro,remount"),
496 # If ccache support is requested, we bind mount the cache.
497 if self
.settings
.get("enable_ccache"):
498 # Create ccache cache directory if it does not exist.
499 if not os
.path
.exists(CCACHE_CACHE_DIR
):
500 os
.makedirs(CCACHE_CACHE_DIR
)
503 (CCACHE_CACHE_DIR
, "/var/cache/ccache", "bind", "bind"),
511 # Add HOME manually, because it is occasionally not set
512 # and some builds get in trouble then.
514 "TERM" : os
.environ
.get("TERM", "dumb"),
517 # Set the container that we can detect, if we are inside a
519 "container" : "pakfire-builder",
522 # Inherit environment from distro
523 env
.update(self
.pakfire
.distro
.environ
)
525 # Icecream environment settings
526 if self
.settings
.get("enable_icecream", False):
527 # Set the toolchain path
528 if self
.settings
.get("icecream_toolchain", None):
529 env
["ICECC_VERSION"] = self
.settings
.get("icecream_toolchain")
531 # Set preferred host if configured.
532 if self
.settings
.get("icecream_preferred_host", None):
533 env
["ICECC_PREFERRED_HOST"] = \
534 self
.settings
.get("icecream_preferred_host")
536 # Fake UTS_MACHINE, when we cannot use the personality syscall and
537 # if the host architecture is not equal to the target architecture.
538 if not self
.pakfire
.distro
.personality
and \
539 not self
.pakfire
.config
.host_arch
== self
.pakfire
.distro
.arch
:
541 "LD_PRELOAD" : "/usr/lib/libpakfire_preload.so",
542 "UTS_MACHINE" : self
.pakfire
.distro
.arch
,
547 def do(self
, command
, shell
=True, personality
=None, logger
=None, *args
, **kwargs
):
550 # Environment variables
553 if kwargs
.has_key("env"):
554 env
.update(kwargs
.pop("env"))
556 self
.log
.debug("Environment:")
557 for k
, v
in sorted(env
.items()):
558 self
.log
.debug(" %s=%s" % (k
, v
))
560 # Update personality it none was set
562 personality
= self
.distro
.personality
564 # Make every shell to a login shell because we set a lot of
565 # environment things there.
567 command
= ["bash", "--login", "-c", command
]
569 if not kwargs
.has_key("chrootPath"):
570 kwargs
["chrootPath"] = self
.chrootPath()
574 personality
=personality
,
584 def build(self
, install_test
=True):
586 raise BuildError
, _("You cannot run a build when no package was given.")
588 # Search for the package file in build_dir and raise BuildError if it is not present.
589 pkgfile
= os
.path
.join(self
.build_dir
, "%s.%s" % (self
.pkg
.name
, MAKEFILE_EXTENSION
))
590 if not os
.path
.exists(pkgfile
):
591 raise BuildError
, _("Could not find makefile in build root: %s") % pkgfile
592 pkgfile
= "/%s" % os
.path
.relpath(pkgfile
, self
.chrootPath())
594 resultdir
= self
.chrootPath("/result")
596 # Create the build command, that is executed in the chroot.
597 build_command
= ["/usr/lib/pakfire/builder", "--offline",
598 "build", pkgfile
, "--arch", self
.arch
, "--nodeps",
599 "--resultdir=/result",]
602 self
.do(" ".join(build_command
), logger
=self
.log
)
605 raise BuildError
, _("The build command failed. See logfile for details.")
607 # Perform install test.
611 # Copy the final packages and stuff.
614 def shell(self
, args
=[]):
615 if not util
.cli_is_interactive():
616 self
.log
.warning("Cannot run shell on non-interactive console.")
619 # Install all packages that are needed to run a shell.
620 self
.install(SHELL_PACKAGES
)
622 # XXX need to set CFLAGS here
623 command
= "/usr/sbin/chroot %s %s %s" % \
624 (self
.chrootPath(), SHELL_SCRIPT
, " ".join(args
))
626 # Add personality if we require one
627 if self
.pakfire
.distro
.personality
:
628 command
= "%s %s" % (self
.pakfire
.distro
.personality
, command
)
630 for key
, val
in self
.environ
.items():
631 command
= "%s=\"%s\" " % (key
, val
) + command
633 # Empty the environment
634 command
= "env -i - %s" % command
636 self
.log
.debug("Shell command: %s" % command
)
638 shell
= os
.system(command
)
639 return os
.WEXITSTATUS(shell
)
642 def cache_file(self
):
644 self
.pakfire
.distro
.sname
, # name of the distribution
645 self
.pakfire
.distro
.release
, # release version
646 self
.pakfire
.distro
.arch
, # architecture
649 return os
.path
.join(CACHE_ENVIRON_DIR
, "%s.cache" %"-".join(comps
))
651 def cache_export(self
, filename
):
652 # Sync all disk caches.
655 # A list to store all mountpoints, so we don't package them.
658 # A list containing all files we want to package.
661 # Walk through the whole tree and collect all files
662 # that are on the same disk (not crossing mountpoints).
663 log
.info(_("Creating filelist..."))
664 root
= self
.chrootPath()
665 for dir, subdirs
, files
in os
.walk(root
):
666 # Search for mountpoints and skip them.
667 if not dir == root
and os
.path
.ismount(dir):
668 mountpoints
.append(dir)
671 # Skip all directories under mountpoints.
672 if any([dir.startswith(m
) for m
in mountpoints
]):
675 # Add all other files.
678 file = os
.path
.join(dir, file)
679 filelist
.append(file)
681 # Create a nice progressbar.
682 p
= util
.make_progress(_("Compressing files..."), len(filelist
))
685 # Create tar file and add all files to it.
686 f
= packages
.file.InnerTarFile
.open(filename
, "w:gz")
687 for file in filelist
:
692 f
.add(file, os
.path
.relpath(file, root
), recursive
=False)
695 # Finish progressbar.
699 filesize
= os
.path
.getsize(filename
)
701 log
.info(_("Cache file was successfully created at %s.") % filename
)
702 log
.info(_(" Containing %(files)s files, it has a size of %(size)s.") % \
703 { "files" : len(filelist
), "size" : util
.format_size(filesize
), })
705 def cache_extract(self
):
706 root
= self
.chrootPath()
707 filename
= self
.cache_file
709 f
= packages
.file.InnerTarFile
.open(filename
, "r:gz")
710 members
= f
.getmembers()
712 # Make a nice progress bar as always.
713 p
= util
.make_progress(_("Extracting files..."), len(members
))
715 # Extract all files from the cache.
717 for member
in members
:
722 f
.extract(member
, path
=root
)
725 # Finish progressbar.
729 # Re-read local repository.
730 self
.pakfire
.repos
.local
.update(force
=True)
732 # Update all packages.
733 self
.log
.info(_("Updating packages from cache..."))
734 self
.pakfire
.update(interactive
=False, logger
=self
.log
,
735 allow_archchange
=True, allow_vendorchange
=True, allow_downgrade
=True)
738 class Builder(object):
739 def __init__(self
, pakfire
, filename
, resultdir
, **kwargs
):
740 self
.pakfire
= pakfire
742 self
.filename
= filename
744 self
.resultdir
= resultdir
747 self
.pkg
= packages
.Makefile(self
.pakfire
, self
.filename
)
755 Create a temporary file in the build environment.
757 file = "/tmp/pakfire_%s" % util
.random_string()
767 return self
.pkg
.buildroot
771 return self
.pakfire
.distro
777 # Get all definitions from the package.
778 environ
.update(self
.pkg
.exports
)
780 # Overwrite some definitions by default values.
781 environ
.update(self
._environ
)
785 def do(self
, command
, shell
=True, personality
=None, cwd
=None, *args
, **kwargs
):
786 # Environment variables
787 log
.debug("Environment:")
788 for k
, v
in sorted(self
.environ
.items()):
789 log
.debug(" %s=%s" % (k
, v
))
791 # Update personality it none was set
793 personality
= self
.distro
.personality
796 cwd
= "/%s" % LOCAL_TMP_PATH
798 # Make every shell to a login shell because we set a lot of
799 # environment things there.
801 command
= ["bash", "--login", "-c", command
]
805 personality
=personality
,
808 logger
=logging
.getLogger("pakfire"),
814 def create_icecream_toolchain(self
):
816 out
= self
.do("icecc --build-native 2>/dev/null", returnOutput
=True, cwd
="/tmp")
820 for line
in out
.splitlines():
821 m
= re
.match(r
"^creating ([a-z0-9]+\.tar\.gz)", line
)
823 self
._environ
["ICECC_VERSION"] = "/tmp/%s" % m
.group(1)
825 def create_buildscript(self
, stage
):
826 file = "/tmp/build_%s" % util
.random_string()
828 # Get buildscript from the package.
829 script
= self
.pkg
.get_buildscript(stage
)
831 # Write script to an empty file.
833 f
.write("#!/bin/sh\n\n")
836 f
.write("\n%s\n" % script
)
844 # Create buildroot and remove all content if it was existant.
845 util
.rm(self
.buildroot
)
846 os
.makedirs(self
.buildroot
)
848 # Build icecream toolchain if icecream is installed.
849 self
.create_icecream_toolchain()
851 for stage
in ("prepare", "build", "test", "install"):
852 self
.build_stage(stage
)
854 # Run post-build stuff.
855 self
.post_compress_man_pages()
856 self
.post_remove_static_libs()
857 self
.post_extract_debuginfo()
859 # Package the result.
860 # Make all these little package from the build environment.
861 log
.info(_("Creating packages:"))
863 for pkg
in reversed(self
.pkg
.packages
):
864 packager
= packages
.packager
.BinaryPackager(self
.pakfire
, pkg
,
865 self
, self
.buildroot
)
866 pkg
= packager
.run(self
.resultdir
)
870 for pkg
in sorted(pkgs
):
871 for line
in pkg
.dump(long=True).splitlines():
876 def build_stage(self
, stage
):
877 # Get the buildscript for this stage.
878 buildscript
= self
.create_buildscript(stage
)
880 # Execute the buildscript of this stage.
881 log
.info(_("Running stage %s:") % stage
)
884 self
.do(buildscript
, shell
=False)
887 # Remove the buildscript.
888 if os
.path
.exists(buildscript
):
889 os
.unlink(buildscript
)
891 def post_remove_static_libs(self
):
892 keep_libs
= self
.pkg
.lexer
.build
.get_var("keep_libraries")
893 keep_libs
= keep_libs
.split()
896 self
.do("%s/remove-static-libs %s %s" % \
897 (SCRIPT_DIR
, self
.buildroot
, " ".join(keep_libs
)))
899 log
.warning(_("Could not remove static libraries: %s") % e
)
901 def post_compress_man_pages(self
):
903 self
.do("%s/compress-man-pages %s" % (SCRIPT_DIR
, self
.buildroot
))
905 log
.warning(_("Compressing man pages did not complete successfully."))
907 def post_extract_debuginfo(self
):
910 # Check if we need to run with strict build-id.
911 strict_id
= self
.pkg
.lexer
.build
.get_var("debuginfo_strict_build_id", "true")
912 if strict_id
in ("true", "yes", "1"):
913 args
.append("--strict-build-id")
915 args
.append("--buildroot=%s" % self
.pkg
.buildroot
)
916 args
.append("--sourcedir=%s" % self
.pkg
.sourcedir
)
918 # Get additional options to pass to script.
919 options
= self
.pkg
.lexer
.build
.get_var("debuginfo_options", "")
920 args
+= options
.split()
923 self
.do("%s/extract-debuginfo %s %s" % (SCRIPT_DIR
, " ".join(args
), self
.pkg
.buildroot
))
925 log
.error(_("Extracting debuginfo did not complete with success. Aborting build."))
929 if os
.path
.exists(self
.buildroot
):
930 util
.rm(self
.buildroot
)