aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHidehiko Abe <hidehiko@chromium.org>2017-06-14 02:28:21 +0900
committerZac Medico <zmedico@gentoo.org>2017-12-02 13:17:10 -0800
commitd9522ba661b5ae1e55ad595af2e913db755f184e (patch)
tree054e1cfe5f7c1af735ad86716d85b56233f59d48
parentCOPYING: add header paragraph (diff)
downloadportage-d9522ba661b5ae1e55ad595af2e913db755f184e.tar.gz
portage-d9522ba661b5ae1e55ad595af2e913db755f184e.tar.bz2
portage-d9522ba661b5ae1e55ad595af2e913db755f184e.zip
Rewrite doins in python (bug 624526)
doins is written in bash. However, specifically in case that too many files are installed, it is very slow. This CL rewrites the script in python for performance. BUG=chromium:712659 TEST=time (./setup_board --forace && \ ./build_package --withdev && \ ./build_image --noenable_rootfs_verification test) ===Before=== real 21m35.445s user 93m40.588s sys 21m31.224s ===After=== real 17m30.106s user 94m1.812s sys 20m13.468s Change-Id: Ib10f623961ba316753d58397cff5e72fbc343339 Reviewed-on: https://chromium-review.googlesource.com/559225 X-Chromium-Bug: 712659 X-Chromium-Bug-url: https://bugs.chromium.org/p/chromium/issues/detail?id=712659 Reviewed-by: Manuel RĂ¼ger <mrueg@gentoo.org> Bug: https://bugs.gentoo.org/624526
-rw-r--r--COPYING29
-rw-r--r--bin/doins.py580
-rwxr-xr-xbin/ebuild-helpers/doins156
-rw-r--r--pym/portage/tests/bin/test_doins.py352
4 files changed, 1004 insertions, 113 deletions
diff --git a/COPYING b/COPYING
index e7f811f09..cca0dc58b 100644
--- a/COPYING
+++ b/COPYING
@@ -4,3 +4,32 @@ Upstream-Contact: Zac Medico <zmedico@gentoo.org>
Source: https://gitweb.gentoo.org/proj/portage.git/
Copyright: Copyright 1999-2017 Gentoo Foundation
License: GPL-2+ (See LICENSE)
+
+Files: bin/doins.py
+Copyright: Copyright 2017 The Chromium OS Authors. All rights reserved.
+License: BSD-3-clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/bin/doins.py b/bin/doins.py
new file mode 100644
index 000000000..13b9c5fc0
--- /dev/null
+++ b/bin/doins.py
@@ -0,0 +1,580 @@
+#!/usr/bin/python -b
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Core implementation of doins ebuild helper command.
+
+This script is designed to be executed by ebuild-helpers/doins.
+"""
+
+
+from __future__ import print_function
+
+import argparse
+import errno
+import grp
+import logging
+import os
+import pwd
+import re
+import shlex
+import shutil
+import stat
+import subprocess
+import sys
+
+from portage.util import movefile
+
+
+def _warn(helper, msg):
+ """Output warning message to stderr.
+
+ Args:
+ helper: helper executable name.
+ msg: Message to be output.
+ """
+ print('!!! %s: %s\n' % (helper, msg), file=sys.stderr)
+
+
+def _parse_group(group):
+ """Parses gid.
+
+ Args:
+ group: string representation of the group. Maybe name or gid.
+ Returns:
+ Parsed gid.
+ """
+ try:
+ return grp.getgrnam(group).gr_gid
+ except KeyError:
+ pass
+ return int(group)
+
+
+def _parse_user(user):
+ """Parses uid.
+
+ Args:
+ user: string representation of the user. Maybe name or uid.
+ Returns:
+ Parsed uid.
+ """
+ try:
+ return pwd.getpwnam(user).pw_uid
+ except KeyError:
+ pass
+ return int(user)
+
+
+def _parse_mode(mode):
+ """Parses mode.
+
+ Args:
+ mode: string representation of the permission.
+ Returns:
+ Parsed mode.
+ """
+ # `install`'s --mode option is complicated. So here is partially
+ # supported.
+ try:
+ return int(mode, 8)
+ except ValueError:
+ # In case of fail, returns None, so that caller can check
+ # if unknown '-m' is set or not.
+ return None
+
+
+def _parse_install_options(
+ options, is_strict, helper, inprocess_runner_class,
+ subprocess_runner_class):
+ """Parses command line arguments for `install` command.
+
+ Args:
+ options: string representation of `install` options.
+ is_strict: bool. If True, this exits the program in case of
+ that an unknown option is found.
+ helper: helper executable name.
+ inprocess_runner_class: Constructor to run procedure which
+ `install` command will do.
+ subprocess_runner_class: Constructor to run `install` command.
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-g', '--group', default=-1, type=_parse_group)
+ parser.add_argument('-o', '--owner', default=-1, type=_parse_user)
+ parser.add_argument('-m', '--mode', default=0o755, type=_parse_mode)
+ split_options = shlex.split(options)
+ namespace, remaining = parser.parse_known_args(split_options)
+ # Because parsing '--mode' option is partially supported. If unknown
+ # arg for --mode is passed, namespace.mode is set to None.
+ if remaining or namespace.mode is None:
+ _warn(helper, 'Unknown install options: %s, %r' % (
+ options, remaining))
+ if is_strict:
+ sys.exit(1)
+ _warn(helper, 'Continue with falling back to `install` '
+ 'command execution, which can be slower.')
+ return subprocess_runner_class(split_options)
+ return inprocess_runner_class(namespace)
+
+
+def _set_attributes(options, path):
+ """Sets attributes the file/dir at given |path|.
+
+ Args:
+ options: object which has |owner|, |group| and |mode| fields.
+ |owner| is int value representing uid. Similary |group|
+ represents gid.
+ If -1 is set, just unchanged.
+ |mode| is the bits of permissions.
+ path: File/directory path.
+ """
+ if options.owner != -1 or options.group != -1:
+ os.lchown(path, options.owner, options.group)
+ if options.mode is not None:
+ os.chmod(path, options.mode)
+
+
+class _InsInProcessInstallRunner(object):
+ """Implements `install` command behavior running in a process."""
+
+ def __init__(self, opts, parsed_options):
+ """Initializes the instance.
+
+ Args:
+ opts: namespace object containing the parsed
+ arguments for this program.
+ parsed_options: namespace object contaning the parsed
+ options for `install`.
+ """
+ self._parsed_options = parsed_options
+ self._helper = opts.helper
+ self._copy_xattr = opts.enable_copy_xattr
+ if self._copy_xattr:
+ self._xattr_exclude = opts.xattr_exclude
+
+ def run(self, source, dest_dir):
+ """Installs a file at |source| into |dest_dir| in process.
+
+ Args:
+ source: Path to the file to be installed.
+ dest_dir: Path to the directory which |source| will be
+ installed into.
+ Returns:
+ True on success, otherwise False.
+ """
+ dest = os.path.join(dest_dir, os.path.basename(source))
+ if not self._is_install_allowed(source, dest):
+ return False
+
+ # To emulate the `install` command, remove the dest file in
+ # advance.
+ try:
+ os.unlink(dest)
+ except OSError as e:
+ # Removing a non-existing entry should be handled as a
+ # regular case.
+ if e.errno != errno.ENOENT:
+ raise
+ try:
+ # TODO: Consider to use portage.util.file_copy.copyfile
+ # introduced by
+ # https://gitweb.gentoo.org/proj/portage.git/commit/
+ # ?id=8ab5c8835931fd9ec098dbf4c5f416eb32e4a3a4
+ # after uprev.
+ shutil.copyfile(source, dest)
+ _set_attributes(self._parsed_options, dest)
+ if self._copy_xattr:
+ movefile._copyxattr(
+ source, dest,
+ exclude=self._xattr_exclude)
+ except Exception:
+ logging.exception(
+ 'Failed to copy file: '
+ '_parsed_options=%r, source=%r, dest_dir=%r',
+ self._parsed_options, source, dest_dir)
+ return False
+ return True
+
+ def _is_install_allowed(self, source, dest):
+ """Returns if installing source into dest should work.
+
+ This is to keep compatibility with the `install` command.
+
+ Args:
+ source: path to the source file.
+ dest: path to the dest file.
+
+ Returns:
+ True if it should succeed.
+ """
+ # To match `install` command, use stat() for source, while
+ # lstat() for dest. Raise an exception if stat(source) fails,
+ # intentionally.
+ source_stat = os.stat(source)
+ try:
+ dest_lstat = os.lstat(dest)
+ except OSError as e:
+ # It is common to install a file into a new path,
+ # so if the destination doesn't exist, ignore it.
+ if e.errno == errno.ENOENT:
+ return True
+ raise
+
+ # Allowing install, if the target is a symlink.
+ if stat.S_ISLNK(dest_lstat.st_mode):
+ return True
+
+ # Allowing install, if source file and dest file are different.
+ # Note that, later, dest will be unlinked.
+ if not os.path.samestat(source_stat, dest_lstat):
+ return True
+
+ # Allowing install, in hardlink case, if the actual path are
+ # different, because source can be preserved even after dest is
+ # unlinked.
+ if (dest_lstat.st_nlink > 1 and
+ os.path.realpath(source) != os.path.realpath(dest)):
+ return True
+
+ _warn(self._helper, '%s and %s are same file.' % (
+ source, dest))
+ return False
+
+
+class _InsSubprocessInstallRunner(object):
+ """Runs `install` command in a subprocess to install a file."""
+
+ def __init__(self, split_options):
+ """Initializes the instance.
+
+ Args:
+ split_options: Command line options to be passed to
+ `install` command. List of str.
+ """
+ self._split_options = split_options
+
+ def run(self, source, dest_dir):
+ """Installs a file at |source| into |dest_dir| by `install`.
+
+ Args:
+ source: Path to the file to be installed.
+ dest_dir: Path to the directory which |source| will be
+ installed into.
+ Returns:
+ True on success, otherwise False.
+ """
+ command = ['install'] + self._split_options + [source, dest_dir]
+ return subprocess.call(command) == 0
+
+
+class _DirInProcessInstallRunner(object):
+ """Implements `install` command behavior running in a process."""
+
+ def __init__(self, parsed_options):
+ """Initializes the instance.
+
+ Args:
+ parsed_options: namespace object contaning the parsed
+ options for `install`.
+ """
+ self._parsed_options = parsed_options
+
+ def run(self, dest):
+ """Installs a dir into |dest| in process.
+
+ Args:
+ dest: Path where a directory should be created.
+ """
+ try:
+ os.makedirs(dest)
+ except OSError as e:
+ if e.errno != errno.EEXIST or not os.path.isdir(dest):
+ raise
+ _set_attributes(self._parsed_options, dest)
+
+
+class _DirSubprocessInstallRunner(object):
+ """Runs `install` command to create a directory."""
+
+ def __init__(self, split_options):
+ """Initializes the instance.
+
+ Args:
+ split_options: Command line options to be passed to
+ `install` command. List of str.
+ """
+ self._split_options = split_options
+
+ def run(self, dest):
+ """Installs a dir into |dest| by `install` command.
+
+ Args:
+ dest: Path where a directory should be created.
+ """
+ command = ['install', '-d'] + self._split_options + [dest]
+ subprocess.check_call(command)
+
+
+class _InstallRunner(object):
+ """Handles `install` command operation.
+
+ Runs operations which `install` command should work. If possible,
+ this may just call in-process functions, instead of executing `install`
+ in a subprocess for performance.
+ """
+
+ def __init__(self, opts):
+ """Initializes the instance.
+
+ Args:
+ opts: namespace object containing the parsed
+ arguments for this program.
+ """
+ self._ins_runner = _parse_install_options(
+ opts.insoptions,
+ opts.strict_option,
+ opts.helper,
+ lambda options: _InsInProcessInstallRunner(
+ opts, options),
+ _InsSubprocessInstallRunner)
+ self._dir_runner = _parse_install_options(
+ opts.diroptions,
+ opts.strict_option,
+ opts.helper,
+ _DirInProcessInstallRunner,
+ _DirSubprocessInstallRunner)
+ self._helpers_can_die = opts.helpers_can_die
+
+ def install_file(self, source, dest_dir):
+ """Installs a file at |source| into |dest_dir| directory.
+
+ Args:
+ source: Path to the file to be installed.
+ dest_dir: Path to the directory which |source| will be
+ installed into.
+ Returns:
+ True on success, otherwise False.
+ """
+ return self._ins_runner.run(source, dest_dir)
+
+ def install_dir(self, dest):
+ """Creates a directory at |dest|.
+
+ Args:
+ dest: Path where a directory should be created.
+ """
+ try:
+ self._dir_runner.run(dest)
+ except Exception:
+ if self._helpers_can_die:
+ raise
+ logging.exception('install_dir failed.')
+
+
+def _doins(opts, install_runner, relpath, source_root):
+ """Installs a file as if `install` command runs.
+
+ Installs a file at |source_root|/|relpath| into
+ |opts.dest|/|relpath|.
+ If |args.preserve_symlinks| is set, creates symlink if the source is a
+ symlink.
+
+ Args:
+ opts: parsed arguments. It should have following fields.
+ - preserve_symlinks: bool representing whether symlinks
+ needs to be preserved.
+ - dest: Destination root directory.
+ - distdir: location where Portage stores the downloaded
+ source code archives.
+ install_runner: _InstallRunner instance for file install.
+ relpath: Relative path of the file being installed.
+ source_root: Source root directory.
+
+ Returns: True on success.
+ """
+ source = os.path.join(source_root, relpath)
+ dest = os.path.join(opts.dest, relpath)
+ if os.path.islink(source):
+ # Our fake $DISTDIR contains symlinks that should not be
+ # reproduced inside $D. In order to ensure that things like
+ # dodoc "$DISTDIR"/foo.pdf work as expected, we dereference
+ # symlinked files that refer to absolute paths inside
+ # $PORTAGE_ACTUAL_DISTDIR/.
+ try:
+ if (opts.preserve_symlinks and
+ not os.readlink(source).startswith(
+ opts.distdir)):
+ linkto = os.readlink(source)
+ shutil.rmtree(dest, ignore_errors=True)
+ os.symlink(linkto, dest)
+ return True
+ except Exception:
+ logging.exception(
+ 'Failed to create symlink: '
+ 'opts=%r, relpath=%r, source_root=%r',
+ opts, relpath, source_root)
+ return False
+
+ return install_runner.install_file(source, os.path.dirname(dest))
+
+
+def _create_arg_parser():
+ """Returns the parser for the command line arguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--recursive', action='store_true',
+ help='If set, installs files recursively. Otherwise, '
+ 'just skips directories.')
+ parser.add_argument(
+ '--preserve_symlinks', action='store_true',
+ help='If set, a symlink will be installed as symlink.')
+ parser.add_argument(
+ '--helpers_can_die', action='store_true',
+ help='If set, die in isolated-functions.sh is enabled. '
+ 'Specifically this is used to keep compatible dodir\'s '
+ 'behavior.')
+ parser.add_argument(
+ '--distdir', default='', help='Path to the actual distdir.')
+ parser.add_argument(
+ '--insoptions', default='',
+ help='Options passed to `install` command for installing a '
+ 'file.')
+ parser.add_argument(
+ '--diroptions', default='',
+ help='Options passed to `install` command for installing a '
+ 'dir.')
+ parser.add_argument(
+ '--strict_option', action='store_true',
+ help='If set True, abort if insoptions/diroptions contains an '
+ 'option which cannot be interpreted by this script, instead of '
+ 'fallback to execute `install` command.')
+ parser.add_argument(
+ '--enable_copy_xattr', action='store_true',
+ help='Copies xattrs, if set True')
+ parser.add_argument(
+ '--xattr_exclude', default='',
+ help='White space delimited glob pattern to exclude xattr copy.'
+ 'Used only if --enable_xattr_copy is set.')
+
+ # If helper is dodoc, it changes the behavior for the directory
+ # install without --recursive.
+ parser.add_argument('--helper', help='Name of helper.')
+ parser.add_argument(
+ '--dest',
+ help='Destination where the files are installed.')
+ parser.add_argument(
+ 'sources', nargs='*',
+ help='Source file/directory paths to be installed.')
+
+ return parser
+
+
+def _parse_args(argv):
+ """Parses the command line arguments.
+
+ Args:
+ argv: command line arguments to be parsed.
+ Returns:
+ namespace instance containing the parsed argument data.
+ """
+ parser = _create_arg_parser()
+ opts = parser.parse_args(argv)
+
+ # Encode back to the original byte stream. Please see
+ # http://bugs.python.org/issue8776.
+ if sys.version_info.major >= 3:
+ opts.distdir = os.fsencode(opts.distdir) + b'/'
+ opts.dest = os.fsencode(opts.dest)
+ opts.sources = [os.fsencode(source) for source in opts.sources]
+
+ return opts
+
+
+def _install_dir(opts, install_runner, source):
+ """Installs directory at |source|.
+
+ Args:
+ opts: namespace instance containing parsed command line
+ argument data.
+ install_runner: _InstallRunner instance for dir install.
+ source: Path to the source directory.
+ Returns:
+ True on success, False on failure, or None on skipped.
+ """
+ if not opts.recursive:
+ if opts.helper == 'dodoc':
+ _warn(opts.helper, '%s is a directory' % (source,))
+ return False
+ # Neither success nor fail. Return None to indicate skipped.
+ return None
+
+ # Strip trailing '/'s.
+ source = source.rstrip(b'/')
+ source_root = os.path.dirname(source)
+ dest_dir = os.path.join(opts.dest, os.path.basename(source))
+ install_runner.install_dir(dest_dir)
+
+ relpath_list = []
+ for dirpath, dirnames, filenames in os.walk(source):
+ for dirname in dirnames:
+ source_dir = os.path.join(dirpath, dirname)
+ relpath = os.path.relpath(source_dir, source_root)
+ if os.path.islink(source_dir):
+ # If this is a symlink, it will be processed
+ # in _doins() called later.
+ relpath_list.append(relpath)
+ else:
+ dest = os.path.join(opts.dest, relpath)
+ install_runner.install_dir(dest)
+ relpath_list.extend(
+ os.path.relpath(
+ os.path.join(dirpath, filename), source_root)
+ for filename in filenames)
+
+ if not relpath_list:
+ # NOTE: Even if only an empty directory is installed here, it
+ # still counts as success, since an empty directory given as
+ # an argument to doins -r should not trigger failure.
+ return True
+ success = True
+ for relpath in relpath_list:
+ if not _doins(opts, install_runner, relpath, source_root):
+ success = False
+ return success
+
+
+def main(argv):
+ opts = _parse_args(argv)
+ install_runner = _InstallRunner(opts)
+
+ if not os.path.isdir(opts.dest):
+ install_runner.install_dir(opts.dest)
+
+ any_success = False
+ any_failure = False
+ for source in opts.sources:
+ if (os.path.isdir(source) and
+ (not opts.preserve_symlinks or
+ not os.path.islink(source))):
+ ret = _install_dir(opts, install_runner, source)
+ if ret is None:
+ continue
+ if ret:
+ any_success = True
+ else:
+ any_failure = True
+ else:
+ if _doins(
+ opts, install_runner,
+ os.path.basename(source),
+ os.path.dirname(source)):
+ any_success = True
+ else:
+ any_failure = True
+
+ return 0 if not any_failure and any_success else 1
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/bin/ebuild-helpers/doins b/bin/ebuild-helpers/doins
index 93052c2ea..c3a57890a 100755
--- a/bin/ebuild-helpers/doins
+++ b/bin/ebuild-helpers/doins
@@ -23,11 +23,10 @@ if [ $# -lt 1 ] ; then
exit 1
fi
+DOINS_ARGS=()
if [[ "$1" == "-r" ]] ; then
- DOINSRECUR=y
+ DOINS_ARGS+=( --recursive )
shift
-else
- DOINSRECUR=n
fi
if ! ___eapi_has_prefix_variables; then
@@ -44,116 +43,47 @@ if [[ ${INSDESTTREE#${ED}} != "${INSDESTTREE}" ]]; then
fi
if ___eapi_doins_and_newins_preserve_symlinks; then
- PRESERVE_SYMLINKS=y
-else
- PRESERVE_SYMLINKS=n
+ DOINS_ARGS+=( --preserve_symlinks )
fi
-export TMP=$(mktemp -d "${T}/.doins_tmp_XXXXXX")
-# Use separate directories to avoid potential name collisions.
-mkdir -p "$TMP"/{1,2}
-
-[[ ! -d ${ED}${INSDESTTREE} ]] && dodir "${INSDESTTREE}"
-
-_doins() {
- local mysrc="$1" mydir="$2" cleanup="" rval
-
- if [ -L "$mysrc" ] ; then
- # Our fake $DISTDIR contains symlinks that should
- # not be reproduced inside $D. In order to ensure
- # that things like dodoc "$DISTDIR"/foo.pdf work
- # as expected, we dereference symlinked files that
- # refer to absolute paths inside
- # $PORTAGE_ACTUAL_DISTDIR/.
- if [ $PRESERVE_SYMLINKS = y ] && \
- ! [[ $(readlink "$mysrc") == "$PORTAGE_ACTUAL_DISTDIR"/* ]] ; then
- rm -rf "${ED}$INSDESTTREE/$mydir/${mysrc##*/}" || return $?
- cp -P "$mysrc" "${ED}$INSDESTTREE/$mydir/${mysrc##*/}"
- return $?
- else
- cp "$mysrc" "$TMP/2/${mysrc##*/}" || return $?
- mysrc="$TMP/2/${mysrc##*/}"
- cleanup=$mysrc
- fi
- fi
+if ___eapi_helpers_can_die; then
+ DOINS_ARGS+=( --helpers_can_die )
+fi
- install ${INSOPTIONS} "${mysrc}" "${ED}${INSDESTTREE}/${mydir}"
- rval=$?
- [[ -n ${cleanup} ]] && rm -f "${cleanup}"
- [ $rval -ne 0 ] && echo "!!! ${helper}: $mysrc does not exist" 1>&2
- return $rval
-}
-
-_xdoins() {
- local -i failed=0
- while read -r -d $'\0' x ; do
- _doins "$x" "${x%/*}"
- ((failed|=$?))
- done
- return $failed
-}
-
-success=0
-failed=0
-
-for x in "$@" ; do
- if [[ $PRESERVE_SYMLINKS = n && -d $x ]] || \
- [[ $PRESERVE_SYMLINKS = y && -d $x && ! -L $x ]] ; then
- if [ "${DOINSRECUR}" == "n" ] ; then
- if [[ ${helper} == dodoc ]] ; then
- echo "!!! ${helper}: $x is a directory" 1>&2
- ((failed|=1))
- fi
- continue
- fi
-
- while [ "$x" != "${x%/}" ] ; do
- x=${x%/}
- done
- if [ "$x" = "${x%/*}" ] ; then
- pushd "$PWD" >/dev/null
- else
- pushd "${x%/*}" >/dev/null
- fi
- x=${x##*/}
- x_orig=$x
- # Follow any symlinks recursively until we've got
- # a normal directory for 'find' to traverse. The
- # name of the symlink will be used for the name
- # of the installed directory, as discussed in
- # bug #239529.
- while [ -L "$x" ] ; do
- pushd "$(readlink "$x")" >/dev/null
- x=${PWD##*/}
- pushd "${PWD%/*}" >/dev/null
- done
- if [[ $x != $x_orig ]] ; then
- mv "$x" "$TMP/1/$x_orig"
- pushd "$TMP/1" >/dev/null
- fi
- find "$x_orig" -type d -exec dodir "${INSDESTTREE}/{}" \;
- find "$x_orig" \( -type f -or -type l \) -print0 | _xdoins
- if [[ ${PIPESTATUS[1]} -eq 0 ]] ; then
- # NOTE: Even if only an empty directory is installed here, it
- # still counts as success, since an empty directory given as
- # an argument to doins -r should not trigger failure.
- ((success|=1))
- else
- ((failed|=1))
- fi
- if [[ $x != $x_orig ]] ; then
- popd >/dev/null
- mv "$TMP/1/$x_orig" "$x"
- fi
- while popd >/dev/null 2>&1 ; do true ; done
- else
- _doins "${x}"
- if [[ $? -eq 0 ]] ; then
- ((success|=1))
- else
- ((failed|=1))
- fi
- fi
-done
-rm -rf "$TMP"
-[[ $failed -ne 0 || $success -eq 0 ]] && { __helpers_die "${helper} failed"; exit 1; } || exit 0
+if [[ -n "${INSOPTIONS}" ]]; then
+ DOINS_ARGS+=( "--insoptions=${INSOPTIONS}" )
+fi
+
+if [[ -n "${DIROPTIONS}" ]]; then
+ DOINS_ARGS+=( "--diroptions=${DIROPTIONS}" )
+fi
+
+if [[ -n "${PORTAGE_ACTUAL_DISTDIR}" ]]; then
+ DOINS_ARGS+=( "--distdir=${PORTAGE_ACTUAL_DISTDIR}" )
+fi
+
+if [[ "${DOINSSTRICTOPTION}" == 1 ]]; then
+ DOINS_ARGS+=( --strict_option )
+fi
+
+if has xattr ${FEATURES}; then
+ DOINS_ARGS+=(
+ --enable_copy_xattr
+ "--xattr_exclude=${PORTAGE_XATTR_EXCLUDE}"
+ )
+fi
+
+DOINS_ARGS+=(
+ "--helper=${helper}"
+ "--dest=${ED}${INSDESTTREE}"
+)
+
+# Explicitly set PYTHONPATH to non empty.
+# If PYTHONPATH is empty (not unset), it means "add current working directory
+# to the import path" if the Python is prior to 3.4, which would cause
+# unexpected import. See also #469338.
+PYTHONPATH="${PORTAGE_PYM_PATH:-/usr/lib/portage/pym}${PYTHONPATH:+:}${PYTHONPATH}" \
+ "${PORTAGE_PYTHON:-/usr/bin/python}" \
+ "${PORTAGE_BIN_PATH:-/usr/lib/portage/bin}"/doins.py \
+ "${DOINS_ARGS[@]}" -- "$@" || \
+{ __helpers_die "${helper} failed"; exit 1; }
diff --git a/pym/portage/tests/bin/test_doins.py b/pym/portage/tests/bin/test_doins.py
new file mode 100644
index 000000000..14d7adfa6
--- /dev/null
+++ b/pym/portage/tests/bin/test_doins.py
@@ -0,0 +1,352 @@
+# test_doins.py -- Portage Unit Testing Functionality
+# Copyright 2007-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+import grp
+import os
+import pwd
+import stat
+
+from portage.tests.bin import setup_env
+from portage import tests
+
+doins = setup_env.doins
+exists_in_D = setup_env.exists_in_D
+
+
+class DoIns(setup_env.BinTestCase):
+ def testDoIns(self):
+ """Tests the most basic senario."""
+ self.init()
+ try:
+ env = setup_env.env
+ # Create a file to be installed.
+ test_path = os.path.join(env['S'], 'test')
+ with open(test_path, 'w'):
+ pass
+ doins('test')
+ exists_in_D('/test')
+ st = os.lstat(env['D'] + '/test')
+ # By default, `install`'s permission is 755.
+ if stat.S_IMODE(st.st_mode) != 0o755:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsOption(self):
+ """Tests with INSOPTIONS doins.py understands."""
+ self.init()
+ try:
+ env = setup_env.env
+ env['INSOPTIONS'] = '-m0644'
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ doins('test')
+ st = os.lstat(env['D'] + '/test')
+ if stat.S_IMODE(st.st_mode) != 0o644:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsOptionUnsupportedMode(self):
+ """Tests with INSOPTIONS in the format doins.py doesn't know."""
+ self.init()
+ try:
+ env = setup_env.env
+ # Parse test for -m with unsupported format.
+ # It should fall back to `install` command.
+ env['INSOPTIONS'] = '-m u+r'
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ doins('test')
+ st = os.lstat(env['D'] + '/test')
+ if stat.S_IMODE(st.st_mode) != 0o400:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsOptionUid(self):
+ """Tests setting owner by uid works."""
+ self.init()
+ try:
+ env = setup_env.env
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ uid = os.lstat(os.path.join(env['S'], 'test')).st_uid
+ # Set owner option with uid. No guarantee that this
+ # runs with capability, we set the current UID so that
+ # chown should success, although it is difficult to
+ # check if chown actually runs or not.
+ env['INSOPTIONS'] = '-o %d' % uid
+ doins('test')
+ st = os.lstat(env['D'] + '/test')
+ if st.st_uid != uid:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsOptionUserName(self):
+ """Tests setting owner by name works."""
+ self.init()
+ try:
+ env = setup_env.env
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ uid = os.lstat(os.path.join(env['S'], 'test')).st_uid
+ pw = pwd.getpwuid(uid)
+ # Similary to testDoInsOptionUid, use user name.
+ env['INSOPTIONS'] = '-o %s' % pw.pw_name
+ doins('test')
+ st = os.lstat(env['D'] + '/test')
+ if st.st_uid != uid:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsOptionGid(self):
+ """Tests setting group by gid works."""
+ self.init()
+ try:
+ env = setup_env.env
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ gid = os.lstat(os.path.join(env['S'], 'test')).st_gid
+ # Similary to testDoInsOptionUid, use gid.
+ env['INSOPTIONS'] = '-g %d' % gid
+ doins('test')
+ st = os.lstat(env['D'] + '/test')
+ if st.st_gid != gid:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsOptionGroupName(self):
+ """Tests setting group by name works."""
+ self.init()
+ try:
+ env = setup_env.env
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ gid = os.lstat(os.path.join(env['S'], 'test')).st_gid
+ gr = grp.getgrgid(gid)
+ # Similary to testDoInsOptionUid, use group name.
+ env['INSOPTIONS'] = '-g %s' % gr.gr_name
+ doins('test')
+ st = os.lstat(env['D'] + '/test')
+ if st.st_gid != gid:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoInsFallback(self):
+ """Tests if falling back to the `install` command works."""
+ self.init()
+ try:
+ env = setup_env.env
+ # Use an option which doins.py does not know.
+ # Then, fallback to `install` command is expected.
+ env['INSOPTIONS'] = '-p'
+ with open(os.path.join(env['S'], 'test'), 'w'):
+ pass
+ doins('test')
+ # So `install` should still work.
+ exists_in_D('/test')
+ finally:
+ self.cleanup()
+
+ def testDoInsRecursive(self):
+ """Tests installing a directory recursively."""
+ self.init()
+ try:
+ env = setup_env.env
+ os.mkdir(os.path.join(env['S'], 'testdir'))
+ with open(os.path.join(env['S'], 'testdir/test'), 'w'):
+ pass
+ doins('-r testdir')
+ exists_in_D('/testdir/test')
+ finally:
+ self.cleanup()
+
+ def testDoDirOption(self):
+ """Tests with DIROPTIONS."""
+ self.init()
+ try:
+ env = setup_env.env
+ # Use an option which doins.py knows.
+ env['DIROPTIONS'] = '-m0755'
+ os.mkdir(os.path.join(env['S'], 'testdir'))
+ with open(os.path.join(env['S'], 'testdir/test'), 'w'):
+ pass
+ doins('-r testdir')
+ st = os.lstat(env['D'] + '/testdir')
+ if stat.S_IMODE(st.st_mode) != 0o755:
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testDoDirFallback(self):
+ """Tests with DIROPTIONS which doins.py doesn't understand."""
+ self.init()
+ try:
+ env = setup_env.env
+ # Use an option which doins.py does not know.
+ # Then, fallback to `install` command is expected.
+ env['DIROPTIONS'] = '-p'
+ os.mkdir(os.path.join(env['S'], 'testdir'))
+ with open(os.path.join(env['S'], 'testdir/test'), 'w'):
+ pass
+ doins('-r testdir')
+ # So, `install` should still work.
+ exists_in_D('/testdir/test')
+ finally:
+ self.cleanup()
+
+ def testSymlinkFile(self):
+ """Tests if installing a symlink works.
+
+ In EAPI=4 and later, installing a symlink should creates a
+ symlink at destination.
+ """
+ self.init()
+ try:
+ env = setup_env.env
+ env['EAPI'] = '4' # Enable symlink.
+ env['ED'] = env['D']
+ env['PORTAGE_ACTUAL_DISTDIR'] = '/foo'
+ # Create a file to be installed.
+ test_path = os.path.join(env['S'], 'test')
+ with open(test_path, 'w'):
+ pass
+ symlink_path = os.path.join(env['S'], 'symlink')
+ os.symlink('test', symlink_path)
+ doins('test symlink')
+ exists_in_D('/symlink')
+ # Make sure installed symlink is actually a symbolic
+ # link pointing to test.
+ if not os.path.islink(env['D'] + '/symlink'):
+ raise tests.TestCase.failureException
+ if os.readlink(env['D'] + '/symlink') != 'test':
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testSymlinkFileRecursive(self):
+ """Tests if installing a symlink in a directory works."""
+ self.init()
+ try:
+ env = setup_env.env
+ env['EAPI'] = '4' # Enable symlink.
+ env['ED'] = env['D']
+ env['PORTAGE_ACTUAL_DISTDIR'] = '/foo'
+ # Create a file to be installed.
+ parent_path = os.path.join(env['S'], 'test')
+ os.mkdir(parent_path)
+ with open(os.path.join(parent_path, 'test'), 'w'):
+ pass
+ symlink_path = os.path.join(
+ env['S'], 'test', 'symlink')
+ os.symlink('test', symlink_path)
+ doins('-r test')
+ exists_in_D('/test/symlink')
+ # Make sure installed symlink is actually a symbolic
+ # link pointing to test.
+ if not os.path.islink(env['D'] + '/test/symlink'):
+ raise tests.TestCase.failureException
+ if os.readlink(env['D'] + '/test/symlink') != 'test':
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testSymlinkDir(self):
+ """Tests installing a symlink to a directory."""
+ self.init()
+ try:
+ env = setup_env.env
+ env['EAPI'] = '4' # Enable symlink.
+ env['ED'] = env['D']
+ env['PORTAGE_ACTUAL_DISTDIR'] = '/foo'
+ # Create a dir to be installed.
+ os.mkdir(os.path.join(env['S'], 'test'))
+ symlink_path = os.path.join(env['S'], 'symlink')
+ os.symlink('test', symlink_path)
+ doins('test symlink')
+ # Make sure installed symlink is actually a symbolic
+ # link pointing to test.
+ if not os.path.islink(env['D'] + '/symlink'):
+ raise tests.TestCase.failureException
+ if os.readlink(env['D'] + '/symlink') != 'test':
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testSymlinkDirRecursive(self):
+ """Tests installing a symlink to a directory under a directory.
+ """
+ self.init()
+ try:
+ env = setup_env.env
+ env['EAPI'] = '4' # Enable symlink.
+ env['ED'] = env['D']
+ env['PORTAGE_ACTUAL_DISTDIR'] = '/foo'
+ # Create a file to be installed.
+ parent_path = os.path.join(env['S'], 'test')
+ os.mkdir(parent_path)
+ os.mkdir(os.path.join(parent_path, 'test'))
+ symlink_path = os.path.join(
+ env['S'], 'test', 'symlink')
+ os.symlink('test', symlink_path)
+ doins('-r test')
+ # Make sure installed symlink is actually a symbolic
+ # link pointing to test.
+ if not os.path.islink(env['D'] + '/test/symlink'):
+ raise tests.TestCase.failureException
+ if not os.path.isdir(env['D'] + '/test/symlink'):
+ raise tests.TestCase.failureException
+ if os.readlink(env['D'] + '/test/symlink') != 'test':
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testSymlinkOverwrite(self):
+ """Tests installing a file to overwrite the existing file.
+
+ Specifically, if the existing file is a symlink, it should be
+ removed once, and the file content should be copied.
+ """
+ self.init()
+ try:
+ env = setup_env.env
+ test_path = os.path.join(env['S'], 'test')
+ with open(test_path, 'w'):
+ pass
+ # Create a dangling symlink. If removal does not work,
+ # this would easily cause ENOENT error.
+ os.symlink('foo/bar', env['D'] + '/test')
+ doins('test')
+ # Actual file should be installed.
+ if os.path.islink(env['D'] + '/test'):
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()
+
+ def testHardlinkOverwrite(self):
+ """Tests installing a file to overwrite the hardlink.
+
+ If the existing file is a hardlink, it should be removed once,
+ and the file content should be copied.
+ """
+ self.init()
+ try:
+ env = setup_env.env
+ test_path = os.path.join(env['S'], 'test')
+ with open(test_path, 'w'):
+ pass
+ # Create hardlink at the dest.
+ os.link(test_path, env['D'] + '/test')
+ doins('test')
+ # The hardlink should be unlinked, and then a copy
+ # should be created.
+ if os.path.samefile(test_path, env['D'] + '/test'):
+ raise tests.TestCase.failureException
+ finally:
+ self.cleanup()