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.
diff --git a/Doc/distutils/sourcedist.rst b/Doc/distutils/sourcedist.rst
index 1666436..9f7a38e 100644
--- a/Doc/distutils/sourcedist.rst
+++ b/Doc/distutils/sourcedist.rst
@@ -26,16 +26,16 @@
+===========+=========================+=========+
| ``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 @@
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 fcda08e..306ef6a 100644
--- a/Lib/distutils/archive_util.py
+++ b/Lib/distutils/archive_util.py
@@ -18,15 +18,55 @@
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 @@
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 @@
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 @@
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 @@
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 3ea0810..c89d5ef 100644
--- a/Lib/distutils/cmd.py
+++ b/Lib/distutils/cmd.py
@@ -365,9 +365,11 @@
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 38b169a..6814a1c 100644
--- a/Lib/distutils/command/bdist.py
+++ b/Lib/distutils/command/bdist.py
@@ -37,6 +37,12 @@
"[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 @@
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 @@
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 eefdfea..4405d12 100644
--- a/Lib/distutils/command/bdist_dumb.py
+++ b/Lib/distutils/command/bdist_dumb.py
@@ -33,6 +33,12 @@
('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 @@
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 @@
# 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 116f67e..7ea3d5f 100644
--- a/Lib/distutils/command/sdist.py
+++ b/Lib/distutils/command/sdist.py
@@ -74,6 +74,10 @@
('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 @@
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 @@
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 1afdd46..581c0cc 100644
--- a/Lib/distutils/tests/test_archive_util.py
+++ b/Lib/distutils/tests/test_archive_util.py
@@ -16,6 +16,13 @@
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
except ImportError:
@@ -77,7 +84,7 @@
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 @@
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 e6359d6..6170a48 100644
--- a/Lib/distutils/tests/test_sdist.py
+++ b/Lib/distutils/tests/test_sdist.py
@@ -14,6 +14,12 @@
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 @@
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 0083fe0..70988fe 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -47,6 +47,9 @@
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.