164 строки
5.4 KiB
Python
164 строки
5.4 KiB
Python
"""Module/script to clamp the mtimes of all .py files to $SOURCE_DATE_EPOCH
|
|
|
|
When called as a script with arguments, this compiles the directories
|
|
given as arguments recursively.
|
|
|
|
If upstream is interested, this can be later integrated to the compileall module
|
|
as an additional option (e.g. --clamp-source-mtime).
|
|
|
|
License:
|
|
This has been derived from the Python's compileall module
|
|
and it follows Python licensing. For more info see: https://www.python.org/psf/license/
|
|
"""
|
|
from __future__ import print_function
|
|
import os
|
|
import sys
|
|
|
|
# Python 3.6 and higher
|
|
PY36 = sys.version_info[0:2] >= (3, 6)
|
|
|
|
__all__ = ["clamp_dir", "clamp_file"]
|
|
|
|
|
|
def _walk_dir(dir, maxlevels, quiet=0):
|
|
if PY36 and quiet < 2 and isinstance(dir, os.PathLike):
|
|
dir = os.fspath(dir)
|
|
else:
|
|
dir = str(dir)
|
|
if not quiet:
|
|
print('Listing {!r}...'.format(dir))
|
|
try:
|
|
names = os.listdir(dir)
|
|
except OSError:
|
|
if quiet < 2:
|
|
print("Can't list {!r}".format(dir))
|
|
names = []
|
|
names.sort()
|
|
for name in names:
|
|
if name == '__pycache__':
|
|
continue
|
|
fullname = os.path.join(dir, name)
|
|
if not os.path.isdir(fullname):
|
|
yield fullname
|
|
elif (maxlevels > 0 and name != os.curdir and name != os.pardir and
|
|
os.path.isdir(fullname) and not os.path.islink(fullname)):
|
|
for result in _walk_dir(fullname, maxlevels=maxlevels - 1,
|
|
quiet=quiet):
|
|
yield result
|
|
|
|
|
|
def clamp_dir(dir, source_date_epoch, quiet=0):
|
|
"""Clamp the mtime of all modules in the given directory tree.
|
|
|
|
Arguments:
|
|
|
|
dir: the directory to byte-compile
|
|
source_date_epoch: integer parsed from $SOURCE_DATE_EPOCH
|
|
quiet: full output with False or 0, errors only with 1,
|
|
no output with 2
|
|
"""
|
|
maxlevels = sys.getrecursionlimit()
|
|
files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels)
|
|
success = True
|
|
for file in files:
|
|
if not clamp_file(file, source_date_epoch, quiet=quiet):
|
|
success = False
|
|
return success
|
|
|
|
|
|
def clamp_file(fullname, source_date_epoch, quiet=0):
|
|
"""Clamp the mtime of one file.
|
|
|
|
Arguments:
|
|
|
|
fullname: the file to byte-compile
|
|
source_date_epoch: integer parsed from $SOURCE_DATE_EPOCH
|
|
quiet: full output with False or 0, errors only with 1,
|
|
no output with 2
|
|
"""
|
|
if PY36 and quiet < 2 and isinstance(fullname, os.PathLike):
|
|
fullname = os.fspath(fullname)
|
|
else:
|
|
fullname = str(fullname)
|
|
name = os.path.basename(fullname)
|
|
|
|
if os.path.isfile(fullname) and not os.path.islink(fullname):
|
|
if name[-3:] == '.py':
|
|
try:
|
|
mtime = int(os.stat(fullname).st_mtime)
|
|
atime = int(os.stat(fullname).st_atime)
|
|
except OSError as e:
|
|
if quiet >= 2:
|
|
return False
|
|
elif quiet:
|
|
print('*** Error checking mtime of {!r}...'.format(fullname))
|
|
else:
|
|
print('*** ', end='')
|
|
print(e.__class__.__name__ + ':', e)
|
|
return False
|
|
if mtime > source_date_epoch:
|
|
if not quiet:
|
|
print('Clamping mtime of {!r}'.format(fullname))
|
|
try:
|
|
os.utime(fullname, (atime, source_date_epoch))
|
|
except OSError as e:
|
|
if quiet >= 2:
|
|
return False
|
|
elif quiet:
|
|
print('*** Error clamping mtime of {!r}...'.format(fullname))
|
|
else:
|
|
print('*** ', end='')
|
|
print(e.__class__.__name__ + ':', e)
|
|
return False
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Script main program."""
|
|
import argparse
|
|
|
|
source_date_epoch = os.getenv('SOURCE_DATE_EPOCH')
|
|
if not source_date_epoch:
|
|
print("Not clamping source mtimes, $SOURCE_DATE_EPOCH not set")
|
|
return True # This is a success, no action needed
|
|
try:
|
|
source_date_epoch = int(source_date_epoch)
|
|
except ValueError:
|
|
print("$SOURCE_DATE_EPOCH must be an integer")
|
|
return False
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='Clamp .py source mtime to $SOURCE_DATE_EPOCH.')
|
|
parser.add_argument('-q', action='count', dest='quiet', default=0,
|
|
help='output only error messages; -qq will suppress '
|
|
'the error messages as well.')
|
|
parser.add_argument('clamp_dest', metavar='FILE|DIR', nargs='+',
|
|
help=('zero or more file and directory paths '
|
|
'to clamp'))
|
|
|
|
args = parser.parse_args()
|
|
clamp_dests = args.clamp_dest
|
|
|
|
success = True
|
|
try:
|
|
for dest in clamp_dests:
|
|
if os.path.isfile(dest):
|
|
if not clamp_file(dest, quiet=args.quiet,
|
|
source_date_epoch=source_date_epoch):
|
|
success = False
|
|
else:
|
|
if not clamp_dir(dest, quiet=args.quiet,
|
|
source_date_epoch=source_date_epoch):
|
|
success = False
|
|
return success
|
|
except KeyboardInterrupt:
|
|
if args.quiet < 2:
|
|
print("\n[interrupted]")
|
|
return False
|
|
return True
|
|
|
|
|
|
if __name__ == '__main__':
|
|
exit_status = int(not main())
|
|
sys.exit(exit_status)
|