Enable BytesWarnings.
[portage.git] / bin / quickpkg
index 1231da9ffa6df465201c46b061e83f59ca0f2865..a3e2f5b20e394cdd69cf9cbe15417283890b96ff 100755 (executable)
-#!/bin/bash
-# Copyright 1999-2006 Gentoo Foundation
+#!/usr/bin/python -bb
+# Copyright 1999-2014 Gentoo Foundation
 # Distributed under the terms of the GNU General Public License v2
-# $Id$
-
-# This script tries to quickly create a Gentoo binary package using the
-# VDB_PATH/category/pkg/*  files
-#
-# Resulting tbz2 file will be created in ${PKGDIR} ...
-# default is /usr/portage/packages/All/
-
-if [ "${UID}" != "0" ] ; then
-       echo "You must run this as root"
-       exit 1
-fi
-
-export PORTAGE_DB=$(portageq vdb_path)
-export ROOT=$(portageq envvar ROOT)
-export ROOT=${ROOT%/}/
-
-if [ -z "$1" ] || [ $1 == "-h" ] || [ $1 == "--help" ] ; then
-       echo "QUICKPKG ver 1.2"
-       echo "USAGE: quickpkg <list of pkgs>"
-       echo "    a pkg can be of the form:"
-       echo "        - ${PORTAGE_DB}/<CATEGORY>/<PKG-VERSION>/"
-       echo "        - single depend-type atom ..."
-       echo "              if portage can emerge it, quickpkg can make a package"
-       echo "              for exact definitions of depend atoms, see ebuild(5)"
-       echo
-       echo "EXAMPLE:"
-       echo "    quickpkg ${PORTAGE_DB}/net-www/apache-1.3.27-r1"
-       echo "        package up apache, just version 1.3.27-r1"
-       echo "    quickpkg apache"
-       echo "        package up apache, all versions of apache installed"
-       echo "    quickpkg =apache-1.3.27-r1"
-       echo "        package up apache, just version 1.3.27-r1"
-       exit 1
-fi
-
-export PKGDIR=$(portageq envvar PKGDIR)
-export PORTAGE_TMPDIR=$(portageq envvar PORTAGE_TMPDIR)
-
-source /sbin/functions.sh
-
-# here we make a package given a little info
-# $1 = package-name w/version
-# $2 = category
-do_pkg() {
-       mkdir -p "${PORTAGE_TMPDIR}/binpkgs" || exit 1
-       chmod 0750 "${PORTAGE_TMPDIR}/binpkgs"
-       MYDIR="${PORTAGE_TMPDIR}/binpkgs/$1"
-       SRCDIR="${PORTAGE_DB}/$2/$1"
-       LOG="${PORTAGE_TMPDIR}/binpkgs/$1-quickpkglog"
-
-       ebegin "Building package for $1"
-       (
-               # clean up temp directory
-               rm -rf "${MYDIR}"
-
-               # get pkg info files
-               mkdir -p "${MYDIR}"/temp
-               cp "${SRCDIR}"/* "${MYDIR}"/temp/
-
-               # create filelist and a basic tbz2
-               gawk '{
-                       if ($1 != "dir") {
-                               if ($1 == "obj")
-                                       NF=NF-2
-                               else if ($1 == "sym")
-                                       NF=NF-3
-                               print
-                       }
-               }' "${SRCDIR}"/CONTENTS | cut -f2- -d" " - | sed -e 's:^/:./:' > "${MYDIR}"/filelist
-               tar vjcf "${MYDIR}"/bin.tar.bz2 -C "${ROOT}" --files-from="${MYDIR}"/filelist --no-recursion
-
-               # join together the basic tbz2 and the pkg info files
-               xpak "${MYDIR}"/temp "${MYDIR}"/inf.xpak
-               tbz2tool join "${MYDIR}"/bin.tar.bz2 "${MYDIR}"/inf.xpak "${MYDIR}"/$1.tbz2
-
-               # move the final binary package to PKGDIR
-               [ -d "${PKGDIR}"/All ] ||  mkdir -p "${PKGDIR}"/All
-               [ -d "${PKGDIR}/$2" ] || mkdir -p "${PKGDIR}/$2"
-               mv "${MYDIR}"/$1.tbz2 "${PKGDIR}"/All
-               ( cd "${PKGDIR}/$2" && ln -s ../All/$1.tbz2 )
-
-               # cleanup again
-               rm -rf "${MYDIR}"
-       ) >& "${LOG}"
-
-       if [ -e "${PKGDIR}/All/$1.tbz2" ] ; then
-               rm -f "${LOG}"
-               PKGSTATS="${PKGSTATS}"$'\n'"$(einfo $1: $(du -h "${PKGDIR}/All/$1.tbz2" | gawk '{print $1}'))"
-               eend 0
-       else
-               cat ${LOG}
-               PKGSTATS="${PKGSTATS}"$'\n'"$(ewarn $1: not created)"
-               eend 1
-       fi
-}
-
-# here we parse the parameters given to use on the cmdline
-export PKGERROR=""
-export PKGSTATS=""
-for x in "$@" ; do
-
-       # they gave us full path
-       if [ -e "${x}"/CONTENTS ] ; then
-               x=$(readlink -f $x)
-               pkg=$(echo ${x} | cut -d/ -f6)
-               cat=$(echo ${x} | cut -d/ -f5)
-               do_pkg "${pkg}" "${cat}"
-
-       # lets figure out what they want
-       else
-               DIRLIST=$(portageq match "${ROOT}" "${x}")
-               if [ -z "${DIRLIST}" ] ; then
-                       eerror "Could not find anything to match '${x}'; skipping"
-                       export PKGERROR="${PKGERROR} ${x}"
+
+from __future__ import print_function
+
+import errno
+import math
+import signal
+import sys
+import tarfile
+
+from os import path as osp
+pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")
+sys.path.insert(0, pym_path)
+import portage
+portage._internal_caller = True
+from portage import os
+from portage import xpak
+from portage.dbapi.dep_expand import dep_expand
+from portage.dep import Atom, use_reduce
+from portage.exception import (AmbiguousPackageName, InvalidAtom, InvalidData,
+       InvalidDependString, PackageSetNotFound, PermissionDenied)
+from portage.util import ConfigProtect, ensure_dirs, shlex_split
+from portage.dbapi.vartree import dblink, tar_contents
+from portage.checksum import perform_md5
+from portage._sets import load_default_config, SETPREFIX
+from portage.util._argparse import ArgumentParser
+
+def quickpkg_atom(options, infos, arg, eout):
+       settings = portage.settings
+       root = portage.settings['ROOT']
+       eroot = portage.settings['EROOT']
+       trees = portage.db[eroot]
+       vartree = trees["vartree"]
+       vardb = vartree.dbapi
+       bintree = trees["bintree"]
+
+       include_config = options.include_config == "y"
+       include_unmodified_config = options.include_unmodified_config == "y"
+       fix_metadata_keys = ["PF", "CATEGORY"]
+
+       try:
+               atom = dep_expand(arg, mydb=vardb, settings=vartree.settings)
+       except AmbiguousPackageName as e:
+               # Multiple matches thrown from cpv_expand
+               eout.eerror("Please use a more specific atom: %s" % \
+                       " ".join(e.args[0]))
+               del e
+               infos["missing"].append(arg)
+               return
+       except (InvalidAtom, InvalidData):
+               eout.eerror("Invalid atom: %s" % (arg,))
+               infos["missing"].append(arg)
+               return
+       if atom[:1] == '=' and arg[:1] != '=':
+               # dep_expand() allows missing '=' but it's really invalid
+               eout.eerror("Invalid atom: %s" % (arg,))
+               infos["missing"].append(arg)
+               return
+
+       matches = vardb.match(atom)
+       pkgs_for_arg = 0
+       for cpv in matches:
+               excluded_config_files = []
+               bintree.prevent_collision(cpv)
+               dblnk = vardb._dblink(cpv)
+               have_lock = False
+
+               if "__PORTAGE_INHERIT_VARDB_LOCK" not in settings:
+                       try:
+                               dblnk.lockdb()
+                               have_lock = True
+                       except PermissionDenied:
+                               pass
+
+               try:
+                       if not dblnk.exists():
+                               # unmerged by a concurrent process
+                               continue
+                       iuse, use, restrict = vardb.aux_get(cpv,
+                               ["IUSE","USE","RESTRICT"])
+                       iuse = [ x.lstrip("+-") for x in iuse.split() ]
+                       use = use.split()
+                       try:
+                               restrict = use_reduce(restrict, uselist=use, flat=True)
+                       except InvalidDependString as e:
+                               eout.eerror("Invalid RESTRICT metadata " + \
+                                       "for '%s': %s; skipping" % (cpv, str(e)))
+                               del e
+                               continue
+                       if "bindist" in iuse and "bindist" not in use:
+                               eout.ewarn("%s: package was emerged with USE=-bindist!" % cpv)
+                               eout.ewarn("%s: it might not be legal to redistribute this." % cpv)
+                       elif "bindist" in restrict:
+                               eout.ewarn("%s: package has RESTRICT=bindist!" % cpv)
+                               eout.ewarn("%s: it might not be legal to redistribute this." % cpv)
+                       eout.ebegin("Building package for %s" % cpv)
+                       pkgs_for_arg += 1
+                       contents = dblnk.getcontents()
+                       protect = None
+                       if not include_config:
+                               confprot = ConfigProtect(eroot,
+                                       shlex_split(settings.get("CONFIG_PROTECT", "")),
+                                       shlex_split(settings.get("CONFIG_PROTECT_MASK", "")))
+                               def protect(filename):
+                                       if not confprot.isprotected(filename):
+                                               return False
+                                       if include_unmodified_config:
+                                               file_data = contents[filename]
+                                               if file_data[0] == "obj":
+                                                       orig_md5 = file_data[2].lower()
+                                                       cur_md5 = perform_md5(filename, calc_prelink=1)
+                                                       if orig_md5 == cur_md5:
+                                                               return False
+                                       excluded_config_files.append(filename)
+                                       return True
+                       existing_metadata = dict(zip(fix_metadata_keys,
+                               vardb.aux_get(cpv, fix_metadata_keys)))
+                       category, pf = portage.catsplit(cpv)
+                       required_metadata = {}
+                       required_metadata["CATEGORY"] = category
+                       required_metadata["PF"] = pf
+                       update_metadata = {}
+                       for k, v in required_metadata.items():
+                               if v != existing_metadata[k]:
+                                       update_metadata[k] = v
+                       if update_metadata:
+                               vardb.aux_update(cpv, update_metadata)
+                       xpdata = xpak.xpak(dblnk.dbdir)
+                       binpkg_tmpfile = os.path.join(bintree.pkgdir,
+                               cpv + ".tbz2." + str(os.getpid()))
+                       ensure_dirs(os.path.dirname(binpkg_tmpfile))
+                       tar = tarfile.open(binpkg_tmpfile, "w:bz2")
+                       tar_contents(contents, root, tar, protect=protect)
+                       tar.close()
+                       xpak.tbz2(binpkg_tmpfile).recompose_mem(xpdata)
+               finally:
+                       if have_lock:
+                               dblnk.unlockdb()
+               bintree.inject(cpv, filename=binpkg_tmpfile)
+               binpkg_path = bintree.getname(cpv)
+               try:
+                       s = os.stat(binpkg_path)
+               except OSError as e:
+                       # Sanity check, shouldn't happen normally.
+                       eout.eend(1)
+                       eout.eerror(str(e))
+                       del e
+                       eout.eerror("Failed to create package: '%s'" % binpkg_path)
+               else:
+                       eout.eend(0)
+                       infos["successes"].append((cpv, s.st_size))
+                       infos["config_files_excluded"] += len(excluded_config_files)
+                       for filename in excluded_config_files:
+                               eout.ewarn("Excluded config: '%s'" % filename)
+       if not pkgs_for_arg:
+               eout.eerror("Could not find anything " + \
+                       "to match '%s'; skipping" % arg)
+               infos["missing"].append(arg)
+
+def quickpkg_set(options, infos, arg, eout):
+       eroot = portage.settings['EROOT']
+       trees = portage.db[eroot]
+       vartree = trees["vartree"]
+
+       settings = vartree.settings
+       settings._init_dirs()
+       setconfig = load_default_config(settings, trees)
+       sets = setconfig.getSets()
+
+       set = arg[1:]
+       if not set in sets:
+               eout.eerror("Package set not found: '%s'; skipping" % (arg,))
+               infos["missing"].append(arg)
+               return
+
+       try:
+               atoms = setconfig.getSetAtoms(set)
+       except PackageSetNotFound as e:
+               eout.eerror("Failed to process package set '%s' because " % set +
+                       "it contains the non-existent package set '%s'; skipping" % e)
+               infos["missing"].append(arg)
+               return
+
+       for atom in atoms:
+               quickpkg_atom(options, infos, atom, eout)
+
+
+def quickpkg_extended_atom(options, infos, atom, eout):
+       eroot = portage.settings['EROOT']
+       trees = portage.db[eroot]
+       vartree = trees["vartree"]
+       vardb = vartree.dbapi
+
+       require_metadata = atom.slot or atom.repo
+       atoms = []
+       for cpv in vardb.cpv_all():
+               cpv_atom = Atom("=%s" % cpv)
+
+               if atom == "*/*":
+                       atoms.append(cpv_atom)
+                       continue
+
+               if not portage.match_from_list(atom, [cpv]):
                        continue
-               fi
-
-               for d in ${DIRLIST} ; do
-                       pkg=$(echo ${d} | cut -d/ -f2)
-                       cat=$(echo ${d} | cut -d/ -f1)
-                       if [ -f "${PORTAGE_DB}/${cat}/${pkg}/CONTENTS" ] ; then
-                               do_pkg ${pkg} ${cat}
-                       elif [ -d "${PORTAGE_DB}/${cat}/${pkg}" ] ; then
-                               ewarn "Package '${cat}/${pkg}' was injected; skipping"
-                       else
-                               eerror "Unhandled case (${cat}/${pkg}) !"
-                               eerror "Please file a bug at http://bugs.gentoo.org/"
-                               exit 10
-                       fi
-               done
-       fi
-
-done
-
-if [ -z "${PKGSTATS}" ] ; then
-       eerror "No packages found"
-       exit 1
-else
-       echo $'\n'"$(einfo Packages now in ${PKGDIR}:)${PKGSTATS}"
-fi
-if [ ! -z "${PKGERROR}" ] ; then
-       ewarn "The following packages could not be found:"
-       ewarn "${PKGERROR}"
-       exit 2
-fi
-
-exit 0
+
+               if require_metadata:
+                       try:
+                               cpv = vardb._pkg_str(cpv, atom.repo)
+                       except (KeyError, InvalidData):
+                               continue
+                       if not portage.match_from_list(atom, [cpv]):
+                               continue
+
+               atoms.append(cpv_atom)
+
+       for atom in atoms:
+               quickpkg_atom(options, infos, atom, eout)
+
+
+def quickpkg_main(options, args, eout):
+       eroot = portage.settings['EROOT']
+       trees = portage.db[eroot]
+       bintree = trees["bintree"]
+
+       try:
+               ensure_dirs(bintree.pkgdir)
+       except portage.exception.PortageException:
+               pass
+       if not os.access(bintree.pkgdir, os.W_OK):
+               eout.eerror("No write access to '%s'" % bintree.pkgdir)
+               return errno.EACCES
+
+       infos = {}
+       infos["successes"] = []
+       infos["missing"] = []
+       infos["config_files_excluded"] = 0
+       for arg in args:
+               if arg[0] == SETPREFIX:
+                       quickpkg_set(options, infos, arg, eout)
+                       continue
+               try:
+                       atom = Atom(arg, allow_wildcard=True, allow_repo=True)
+               except (InvalidAtom, InvalidData):
+                       # maybe it's valid but missing category (requires dep_expand)
+                       quickpkg_atom(options, infos, arg, eout)
+               else:
+                       if atom.extended_syntax:
+                               quickpkg_extended_atom(options, infos, atom, eout)
+                       else:
+                               quickpkg_atom(options, infos, atom, eout)
+
+       if not infos["successes"]:
+               eout.eerror("No packages found")
+               return 1
+       print()
+       eout.einfo("Packages now in '%s':" % bintree.pkgdir)
+       units = {10:'K', 20:'M', 30:'G', 40:'T',
+               50:'P', 60:'E', 70:'Z', 80:'Y'}
+       for cpv, size in infos["successes"]:
+               if not size:
+                       # avoid OverflowError in math.log()
+                       size_str = "0"
+               else:
+                       power_of_2 = math.log(size, 2)
+                       power_of_2 = 10*int(power_of_2/10)
+                       unit = units.get(power_of_2)
+                       if unit:
+                               size = float(size)/(2**power_of_2)
+                               size_str = "%.1f" % size
+                               if len(size_str) > 4:
+                                       # emulate `du -h`, don't show too many sig figs
+                                       size_str = str(int(size))
+                               size_str += unit
+                       else:
+                               size_str = str(size)
+               eout.einfo("%s: %s" % (cpv, size_str))
+       if infos["config_files_excluded"]:
+               print()
+               eout.ewarn("Excluded config files: %d" % infos["config_files_excluded"])
+               eout.ewarn("See --help if you would like to include config files.")
+       if infos["missing"]:
+               print()
+               eout.ewarn("The following packages could not be found:")
+               eout.ewarn(" ".join(infos["missing"]))
+               return 2
+       return os.EX_OK
+
+if __name__ == "__main__":
+       usage = "quickpkg [options] <list of package atoms or package sets>"
+       parser = ArgumentParser(usage=usage)
+       parser.add_argument("--umask",
+               default="0077",
+               help="umask used during package creation (default is 0077)")
+       parser.add_argument("--ignore-default-opts",
+               action="store_true",
+               help="do not use the QUICKPKG_DEFAULT_OPTS environment variable")
+       parser.add_argument("--include-config",
+               choices=["y","n"],
+               default="n",
+               metavar="<y|n>",
+               help="include all files protected by CONFIG_PROTECT (as a security precaution, default is 'n')")
+       parser.add_argument("--include-unmodified-config",
+               choices=["y","n"],
+               default="n",
+               metavar="<y|n>",
+               help="include files protected by CONFIG_PROTECT that have not been modified since installation (as a security precaution, default is 'n')")
+       options, args = parser.parse_known_args(sys.argv[1:])
+       if not options.ignore_default_opts:
+               default_opts = shlex_split(
+                       portage.settings.get("QUICKPKG_DEFAULT_OPTS", ""))
+               options, args = parser.parse_known_args(default_opts + sys.argv[1:])
+       if not args:
+               parser.error("no packages atoms given")
+       try:
+               umask = int(options.umask, 8)
+       except ValueError:
+               parser.error("invalid umask: %s" % options.umask)
+       # We need to ensure a sane umask for the packages that will be created.
+       old_umask = os.umask(umask)
+       eout = portage.output.EOutput()
+       def sigwinch_handler(signum, frame):
+               lines, eout.term_columns =  portage.output.get_term_size()
+       signal.signal(signal.SIGWINCH, sigwinch_handler)
+       try:
+               retval = quickpkg_main(options, args, eout)
+       finally:
+               os.umask(old_umask)
+               signal.signal(signal.SIGWINCH, signal.SIG_DFL)
+       sys.exit(retval)