Skip to content

Respect SOURCE_DATE_EPOCH when taring sdist. #2136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions setuptools/archive_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import shutil
import posixpath
import contextlib
import distutils.archive_util
from distutils.errors import DistutilsError
from distutils.dir_util import mkpath
from distutils import log

from pkg_resources import ensure_directory

Expand Down Expand Up @@ -173,3 +176,155 @@ def unpack_tarfile(filename, extract_dir, progress_filter=default_filter):


extraction_drivers = unpack_directory, unpack_zipfile, unpack_tarfile


# Modified version fo distutils' to support SOURCE_DATE_EPOCH
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), "bzip2", "xz", "compress", or
None. ("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", ".xz" or ".Z").

Returns the output filename.
"""
tar_compression = {'gzip': 'gz', 'bzip2': 'bz2', 'xz': 'xz', None: '',
'compress': ''}
compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz',
'compress': '.Z'}

# flags for compression program, each element of list will be an argument
if compress is not None and compress not in compress_ext.keys():
raise ValueError(
"bad value for 'compress': must be None, 'gzip', 'bzip2', "
"'xz' or 'compress'")

archive_name = base_name + '.tar'
if compress != 'compress':
archive_name += compress_ext.get(compress, '')

mkpath(os.path.dirname(archive_name), dry_run=dry_run)

# creating the tarball
import tarfile # late import so Python build itself doesn't break

log.info('Creating tar archive')

uid = distutils.archive_util._get_uid(owner)
gid = distutils.archive_util._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

_filter = _set_uid_gid

# SOURCE_DATE EPOCH is defined there
# https://reproducible-builds.org/specs/source-date-epoch/
# we are at least sure that when it is set no timestamp can be later than
# this.
timestamp = None
sde = os.environ.get('SOURCE_DATE_EPOCH')
if sde:
timestamp = int(sde)

def _filter(tarinfo):
tarinfo = _set_uid_gid(tarinfo)
tarinfo.mtime = min(tarinfo.mtime, timestamp)
return tarinfo

if not dry_run:
tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress])
try:
tar.add(base_dir, filter=_filter)
finally:
tar.close()

# compression using `compress`
if compress == 'compress':
warn("'compress' will be deprecated.", PendingDeprecationWarning)
# the option varies depending on the platform
compressed_name = archive_name + compress_ext[compress]
if sys.platform == 'win32':
cmd = [compress, archive_name, compressed_name]
else:
cmd = [compress, '-f', archive_name]
spawn(cmd, dry_run=dry_run)
return compressed_name

return archive_name


ARCHIVE_FORMATS = {
'gztar': (make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"),
'bztar': (make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"),
'xztar': (make_tarball, [('compress', 'xz')], "xz'ed tar-file"),
'ztar': (make_tarball, [('compress', 'compress')], "compressed tar file"),
'tar': (make_tarball, [('compress', None)], "uncompressed tar file"),
'zip': (distutils.archive_util.make_zipfile, [], "ZIP file")
}


def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=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
extension; 'format' is the archive format: one of "zip", "tar", "gztar",
"bztar", "xztar", or "ztar".

'root_dir' is a directory that will be the root directory of the
archive; ie. we typically chdir into 'root_dir' before creating the
archive. 'base_dir' is the directory where we start archiving from;
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:
log.debug("changing into '%s'", root_dir)
base_name = os.path.abspath(base_name)
if not dry_run:
os.chdir(root_dir)

if base_dir is None:
base_dir = os.curdir

kwargs = {'dry_run': dry_run}

try:
format_info = ARCHIVE_FORMATS[format]
except KeyError:
raise ValueError("unknown archive format '%s'" % format)

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:
if root_dir is not None:
log.debug("changing back to '%s'", save_cwd)
os.chdir(save_cwd)
return filename
7 changes: 7 additions & 0 deletions setuptools/command/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from setuptools.extern import six, ordered_set

from .py36compat import sdist_add_defaults
from .. import archive_util

import pkg_resources

Expand Down Expand Up @@ -77,6 +78,12 @@ def make_distribution(self):
with self._remove_os_link():
orig.sdist.make_distribution(self)

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,
owner=owner, group=group)

@staticmethod
@contextlib.contextmanager
def _remove_os_link():
Expand Down