From 5e2d45672c8f84f6b1877e68ab92b4b50e2d124d Mon Sep 17 00:00:00 2001 From: Andrew Kuchling Date: Fri, 15 Nov 2013 13:01:52 -0500 Subject: [PATCH] Issue #19544 and Issue #6516: Restore support for --user and --group parameters to sdist command as found in Python 2.7 and originally slated for Python 3.2 but accidentally rolled back as part of the distutils2 rollback. Closes Issue #6516. --- Doc/distutils/sourcedist.rst | 18 ++++-- Lib/distutils/archive_util.py | 71 ++++++++++++++++++++++-- Lib/distutils/cmd.py | 6 +- Lib/distutils/command/bdist.py | 13 +++++ Lib/distutils/command/bdist_dumb.py | 11 +++- Lib/distutils/command/sdist.py | 9 ++- Lib/distutils/tests/test_archive_util.py | 61 +++++++++++++++++++- Lib/distutils/tests/test_sdist.py | 53 ++++++++++++++++++ Misc/NEWS | 3 + 9 files changed, 230 insertions(+), 15 deletions(-) diff --git a/Doc/distutils/sourcedist.rst b/Doc/distutils/sourcedist.rst index 1666436be0b..9f7a38eda66 100644 --- a/Doc/distutils/sourcedist.rst +++ b/Doc/distutils/sourcedist.rst @@ -26,16 +26,16 @@ to create a gzipped tarball and a zip file. The available formats are: +===========+=========================+=========+ | ``zip`` | zip file (:file:`.zip`) | (1),(3) | +-----------+-------------------------+---------+ -| ``gztar`` | gzip'ed tar file | (2),(4) | +| ``gztar`` | gzip'ed tar file | \(2) | | | (:file:`.tar.gz`) | | +-----------+-------------------------+---------+ -| ``bztar`` | bzip2'ed tar file | \(4) | +| ``bztar`` | bzip2'ed tar file | | | | (:file:`.tar.bz2`) | | +-----------+-------------------------+---------+ | ``ztar`` | compressed tar file | \(4) | | | (:file:`.tar.Z`) | | +-----------+-------------------------+---------+ -| ``tar`` | tar file (:file:`.tar`) | \(4) | +| ``tar`` | tar file (:file:`.tar`) | | +-----------+-------------------------+---------+ Notes: @@ -51,8 +51,16 @@ Notes: of the standard Python library since Python 1.6) (4) - requires external utilities: :program:`tar` and possibly one of :program:`gzip`, - :program:`bzip2`, or :program:`compress` + requires the :program:`compress` program. Notice that this format is now + pending for deprecation and will be removed in the future versions of Python. + +When using any ``tar`` format (``gztar``, ``bztar``, ``ztar`` or +``tar``), under Unix you can specify the ``owner`` and ``group`` names +that will be set for each member of the archive. + +For example, if you want all files of the archive to be owned by root:: + + python setup.py sdist --owner=root --group=root .. _manifest: diff --git a/Lib/distutils/archive_util.py b/Lib/distutils/archive_util.py index fcda08e20a2..306ef6a838d 100644 --- a/Lib/distutils/archive_util.py +++ b/Lib/distutils/archive_util.py @@ -18,15 +18,55 @@ from distutils.spawn import spawn from distutils.dir_util import mkpath from distutils import log -def make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0): +try: + from pwd import getpwnam +except AttributeError: + getpwnam = None + +try: + from grp import getgrnam +except AttributeError: + getgrnam = None + +def _get_gid(name): + """Returns a gid, given a group name.""" + if getgrnam is None or name is None: + return None + try: + result = getgrnam(name) + except KeyError: + result = None + if result is not None: + return result[2] + return None + +def _get_uid(name): + """Returns an uid, given a user name.""" + if getpwnam is None or name is None: + return None + try: + result = getpwnam(name) + except KeyError: + result = None + if result is not None: + return result[2] + return None + +def make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0, + owner=None, group=None): """Create a (possibly compressed) tar file from all the files under 'base_dir'. 'compress' must be "gzip" (the default), "compress", "bzip2", or None. - Both "tar" and the compression utility named by 'compress' must be on - the default program search path, so this is probably Unix-specific. + (compress will be deprecated in Python 3.2) + + 'owner' and 'group' can be used to define an owner and a group for the + archive that is being built. If not provided, the current owner and group + will be used. + The output tar file will be named 'base_dir' + ".tar", possibly plus the appropriate compression extension (".gz", ".bz2" or ".Z"). + Returns the output filename. """ tar_compression = {'gzip': 'gz', 'bzip2': 'bz2', None: '', 'compress': ''} @@ -48,10 +88,23 @@ def make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0): import tarfile # late import so Python build itself doesn't break log.info('Creating tar archive') + + uid = _get_uid(owner) + gid = _get_gid(group) + + def _set_uid_gid(tarinfo): + if gid is not None: + tarinfo.gid = gid + tarinfo.gname = group + if uid is not None: + tarinfo.uid = uid + tarinfo.uname = owner + return tarinfo + if not dry_run: tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress]) try: - tar.add(base_dir) + tar.add(base_dir, filter=_set_uid_gid) finally: tar.close() @@ -140,7 +193,7 @@ def check_archive_formats(formats): return None def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, - dry_run=0): + dry_run=0, owner=None, group=None): """Create an archive file (eg. zip or tar). 'base_name' is the name of the file to create, minus any format-specific @@ -153,6 +206,9 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, ie. 'base_dir' will be the common prefix of all files and directories in the archive. 'root_dir' and 'base_dir' both default to the current directory. Returns the name of the archive file. + + 'owner' and 'group' are used when creating a tar archive. By default, + uses the current owner and group. """ save_cwd = os.getcwd() if root_dir is not None: @@ -174,6 +230,11 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, func = format_info[0] for arg, val in format_info[1]: kwargs[arg] = val + + if format != 'zip': + kwargs['owner'] = owner + kwargs['group'] = group + try: filename = func(base_name, base_dir, **kwargs) finally: diff --git a/Lib/distutils/cmd.py b/Lib/distutils/cmd.py index 3ea08101acb..c89d5efc45e 100644 --- a/Lib/distutils/cmd.py +++ b/Lib/distutils/cmd.py @@ -365,9 +365,11 @@ class Command: from distutils.spawn import spawn spawn(cmd, search_path, dry_run=self.dry_run) - def make_archive(self, base_name, format, root_dir=None, base_dir=None): + def make_archive(self, base_name, format, root_dir=None, base_dir=None, + owner=None, group=None): return archive_util.make_archive(base_name, format, root_dir, base_dir, - dry_run=self.dry_run) + dry_run=self.dry_run, + owner=owner, group=group) def make_file(self, infiles, outfile, func, args, exec_msg=None, skip_msg=None, level=1): diff --git a/Lib/distutils/command/bdist.py b/Lib/distutils/command/bdist.py index 38b169afd10..6814a1c3820 100644 --- a/Lib/distutils/command/bdist.py +++ b/Lib/distutils/command/bdist.py @@ -37,6 +37,12 @@ class bdist(Command): "[default: dist]"), ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), + ('owner=', 'u', + "Owner name used when creating a tar file" + " [default: current user]"), + ('group=', 'g', + "Group name used when creating a tar file" + " [default: current group]"), ] boolean_options = ['skip-build'] @@ -77,6 +83,8 @@ class bdist(Command): self.formats = None self.dist_dir = None self.skip_build = 0 + self.group = None + self.owner = None def finalize_options(self): # have to finalize 'plat_name' before 'bdist_base' @@ -122,6 +130,11 @@ class bdist(Command): if cmd_name not in self.no_format_option: sub_cmd.format = self.formats[i] + # passing the owner and group names for tar archiving + if cmd_name == 'bdist_dumb': + sub_cmd.owner = self.owner + sub_cmd.group = self.group + # If we're going to need to run this command again, tell it to # keep its temporary files around so subsequent runs go faster. if cmd_name in commands[i+1:]: diff --git a/Lib/distutils/command/bdist_dumb.py b/Lib/distutils/command/bdist_dumb.py index eefdfea5ad3..4405d12c058 100644 --- a/Lib/distutils/command/bdist_dumb.py +++ b/Lib/distutils/command/bdist_dumb.py @@ -33,6 +33,12 @@ class bdist_dumb(Command): ('relative', None, "build the archive using relative paths" "(default: false)"), + ('owner=', 'u', + "Owner name used when creating a tar file" + " [default: current user]"), + ('group=', 'g', + "Group name used when creating a tar file" + " [default: current group]"), ] boolean_options = ['keep-temp', 'skip-build', 'relative'] @@ -48,6 +54,8 @@ class bdist_dumb(Command): self.dist_dir = None self.skip_build = None self.relative = 0 + self.owner = None + self.group = None def finalize_options(self): if self.bdist_dir is None: @@ -101,7 +109,8 @@ class bdist_dumb(Command): # Make the archive filename = self.make_archive(pseudoinstall_root, - self.format, root_dir=archive_root) + self.format, root_dir=archive_root, + owner=self.owner, group=self.group) if self.distribution.has_ext_modules(): pyversion = get_python_version() else: diff --git a/Lib/distutils/command/sdist.py b/Lib/distutils/command/sdist.py index 116f67ef9be..7ea3d5fa27e 100644 --- a/Lib/distutils/command/sdist.py +++ b/Lib/distutils/command/sdist.py @@ -74,6 +74,10 @@ class sdist(Command): ('metadata-check', None, "Ensure that all required elements of meta-data " "are supplied. Warn if any missing. [default]"), + ('owner=', 'u', + "Owner name used when creating a tar file [default: current user]"), + ('group=', 'g', + "Group name used when creating a tar file [default: current group]"), ] boolean_options = ['use-defaults', 'prune', @@ -113,6 +117,8 @@ class sdist(Command): self.archive_files = None self.metadata_check = 1 + self.owner = None + self.group = None def finalize_options(self): if self.manifest is None: @@ -444,7 +450,8 @@ class sdist(Command): self.formats.append(self.formats.pop(self.formats.index('tar'))) for fmt in self.formats: - file = self.make_archive(base_name, fmt, base_dir=base_dir) + file = self.make_archive(base_name, fmt, base_dir=base_dir, + owner=self.owner, group=self.group) archive_files.append(file) self.distribution.dist_files.append(('sdist', '', file)) diff --git a/Lib/distutils/tests/test_archive_util.py b/Lib/distutils/tests/test_archive_util.py index 1afdd462254..581c0cc841c 100644 --- a/Lib/distutils/tests/test_archive_util.py +++ b/Lib/distutils/tests/test_archive_util.py @@ -15,6 +15,13 @@ from distutils.spawn import find_executable, spawn from distutils.tests import support from test.support import check_warnings, run_unittest, patch +try: + import grp + import pwd + UID_GID_SUPPORT = True +except ImportError: + UID_GID_SUPPORT = False + try: import zipfile ZIP_SUPPORT = True @@ -77,7 +84,7 @@ class ArchiveUtilTestCase(support.TempdirManager, tmpdir2 = self.mkdtemp() unittest.skipUnless(splitdrive(tmpdir)[0] == splitdrive(tmpdir2)[0], - "Source and target should be on same drive") + "source and target should be on same drive") base_name = os.path.join(tmpdir2, target_name) @@ -275,6 +282,58 @@ class ArchiveUtilTestCase(support.TempdirManager, finally: del ARCHIVE_FORMATS['xxx'] + def test_make_archive_owner_group(self): + # testing make_archive with owner and group, with various combinations + # this works even if there's not gid/uid support + if UID_GID_SUPPORT: + group = grp.getgrgid(0)[0] + owner = pwd.getpwuid(0)[0] + else: + group = owner = 'root' + + base_dir, root_dir, base_name = self._create_files() + base_name = os.path.join(self.mkdtemp() , 'archive') + res = make_archive(base_name, 'zip', root_dir, base_dir, owner=owner, + group=group) + self.assertTrue(os.path.exists(res)) + + res = make_archive(base_name, 'zip', root_dir, base_dir) + self.assertTrue(os.path.exists(res)) + + res = make_archive(base_name, 'tar', root_dir, base_dir, + owner=owner, group=group) + self.assertTrue(os.path.exists(res)) + + res = make_archive(base_name, 'tar', root_dir, base_dir, + owner='kjhkjhkjg', group='oihohoh') + self.assertTrue(os.path.exists(res)) + + @unittest.skipUnless(zlib, "Requires zlib") + @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") + def test_tarfile_root_owner(self): + tmpdir, tmpdir2, base_name = self._create_files() + old_dir = os.getcwd() + os.chdir(tmpdir) + group = grp.getgrgid(0)[0] + owner = pwd.getpwuid(0)[0] + try: + archive_name = make_tarball(base_name, 'dist', compress=None, + owner=owner, group=group) + finally: + os.chdir(old_dir) + + # check if the compressed tarball was created + self.assertTrue(os.path.exists(archive_name)) + + # now checks the rights + archive = tarfile.open(archive_name) + try: + for member in archive.getmembers(): + self.assertEquals(member.uid, 0) + self.assertEquals(member.gid, 0) + finally: + archive.close() + def test_suite(): return unittest.makeSuite(ArchiveUtilTestCase) diff --git a/Lib/distutils/tests/test_sdist.py b/Lib/distutils/tests/test_sdist.py index e6359d6a8af..6170a48fea0 100644 --- a/Lib/distutils/tests/test_sdist.py +++ b/Lib/distutils/tests/test_sdist.py @@ -14,6 +14,12 @@ try: except ImportError: ZLIB_SUPPORT = False +try: + import grp + import pwd + UID_GID_SUPPORT = True +except ImportError: + UID_GID_SUPPORT = False from distutils.command.sdist import sdist, show_formats from distutils.core import Distribution @@ -425,6 +431,53 @@ class SDistTestCase(PyPIRCCommandTestCase): self.assertEqual(sorted(filenames), ['fake-1.0', 'fake-1.0/PKG-INFO', 'fake-1.0/README.manual']) + @unittest.skipUnless(zlib, "requires zlib") + @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") + def test_make_distribution_owner_group(self): + + # check if tar and gzip are installed + if (find_executable('tar') is None or + find_executable('gzip') is None): + return + + # now building a sdist + dist, cmd = self.get_cmd() + + # creating a gztar and specifying the owner+group + cmd.formats = ['gztar'] + cmd.owner = pwd.getpwuid(0)[0] + cmd.group = grp.getgrgid(0)[0] + cmd.ensure_finalized() + cmd.run() + + # making sure we have the good rights + archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + archive = tarfile.open(archive_name) + try: + for member in archive.getmembers(): + self.assertEquals(member.uid, 0) + self.assertEquals(member.gid, 0) + finally: + archive.close() + + # building a sdist again + dist, cmd = self.get_cmd() + + # creating a gztar + cmd.formats = ['gztar'] + cmd.ensure_finalized() + cmd.run() + + # making sure we have the good rights + archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + archive = tarfile.open(archive_name) + try: + for member in archive.getmembers(): + self.assertEquals(member.uid, os.getuid()) + self.assertEquals(member.gid, os.getgid()) + finally: + archive.close() + def test_suite(): return unittest.makeSuite(SDistTestCase) diff --git a/Misc/NEWS b/Misc/NEWS index 0083fe05b1d..70988fee54d 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -47,6 +47,9 @@ Core and Builtins Library ------- +- Issue #19544 and #6516: Restore support for --user and --group parameters to + sdist command accidentally rolled back as part of the distutils2 rollback. + - Issue #13674: Prevented time.strftime from crashing on Windows when given a year before 1900 and a format of %y.