1#-----------------------------------------------------------------------------
2# Copyright (c) 2005-2019, PyInstaller Development Team.
3#
4# Distributed under the terms of the GNU General Public License with exception
5# for distributing bootloader.
6#
7# The full license is in the file COPYING.txt, distributed with this software.
8#-----------------------------------------------------------------------------
9
10
11
12#--- functions for checking guts ---
13# NOTE: By GUTS it is meant intermediate files and data structures that
14# PyInstaller creates for bundling files and creating final executable.
15import glob
16import hashlib
17import os
18import os.path
19import pkgutil
20import platform
21import shutil
22import sys
23
24import struct
25
26from PyInstaller.config import CONF
27from .. import compat
28from ..compat import is_darwin, is_win, EXTENSION_SUFFIXES, \
29    open_file, is_py3, is_py37, is_cygwin
30from ..depend import dylib
31from ..depend.bindepend import match_binding_redirect
32from ..utils import misc
33from ..utils.misc import load_py_data_struct, save_py_data_struct
34from .. import log as logging
35
36if is_win:
37    from ..utils.win32 import winmanifest, winresource
38
39logger = logging.getLogger(__name__)
40
41
42#-- Helpers for checking guts.
43#
44# NOTE: By _GUTS it is meant intermediate files and data structures that
45# PyInstaller creates for bundling files and creating final executable.
46
47def _check_guts_eq(attr, old, new, last_build):
48    """
49    rebuild is required if values differ
50    """
51    if old != new:
52        logger.info("Building because %s changed", attr)
53        return True
54    return False
55
56
57def _check_guts_toc_mtime(attr, old, toc, last_build, pyc=0):
58    """
59    rebuild is required if mtimes of files listed in old toc are newer
60    than last_build
61
62    if pyc=1, check for .py files, too
63
64    Use this for calculated/analysed values read from cache.
65    """
66    for (nm, fnm, typ) in old:
67        if misc.mtime(fnm) > last_build:
68            logger.info("Building because %s changed", fnm)
69            return True
70        elif pyc and misc.mtime(fnm[:-1]) > last_build:
71            logger.info("Building because %s changed", fnm[:-1])
72            return True
73    return False
74
75
76def _check_guts_toc(attr, old, toc, last_build, pyc=0):
77    """
78    rebuild is required if either toc content changed or mtimes of
79    files listed in old toc are newer than last_build
80
81    if pyc=1, check for .py files, too
82
83    Use this for input parameters.
84    """
85    return (_check_guts_eq(attr, old, toc, last_build)
86            or _check_guts_toc_mtime(attr, old, toc, last_build, pyc=pyc))
87
88
89#---
90
91def add_suffix_to_extensions(toc):
92    """
93    Returns a new TOC with proper library suffix for EXTENSION items.
94    """
95    # TODO: Fix this recursive import
96    from .datastruct import TOC
97    new_toc = TOC()
98    for inm, fnm, typ in toc:
99        if typ == 'EXTENSION':
100            if is_py3:
101                # Change the dotted name into a relative path. This places C
102                # extensions in the Python-standard location. This only works
103                # in Python 3; see comments above
104                # ``sys.meta_path.append(CExtensionImporter())`` in
105                # ``pyimod03_importers``.
106                inm = inm.replace('.', os.sep)
107            # In some rare cases extension might already contain a suffix.
108            # Skip it in this case.
109            if os.path.splitext(inm)[1] not in EXTENSION_SUFFIXES:
110                # Determine the base name of the file.
111                if is_py3:
112                    base_name = os.path.basename(inm)
113                else:
114                    base_name = inm.rsplit('.')[-1]
115                assert '.' not in base_name
116                # Use this file's existing extension. For extensions such as
117                # ``libzmq.cp36-win_amd64.pyd``, we can't use
118                # ``os.path.splitext``, which would give only the ```.pyd`` part
119                # of the extension.
120                inm = inm + os.path.basename(fnm)[len(base_name):]
121
122        elif typ == 'DEPENDENCY':
123            # Use the suffix from the filename.
124            # TODO Verify what extensions are by DEPENDENCIES.
125            binext = os.path.splitext(fnm)[1]
126            if not os.path.splitext(inm)[1] == binext:
127                inm = inm + binext
128        new_toc.append((inm, fnm, typ))
129    return new_toc
130
131def applyRedirects(manifest, redirects):
132    """
133    Apply the binding redirects specified by 'redirects' to the dependent assemblies
134    of 'manifest'.
135
136    :param manifest:
137    :type manifest:
138    :param redirects:
139    :type redirects:
140    :return:
141    :rtype:
142    """
143    redirecting = False
144    for binding in redirects:
145        for dep in manifest.dependentAssemblies:
146            if match_binding_redirect(dep, binding):
147                logger.info("Redirecting %s version %s -> %s",
148                            binding.name, dep.version, binding.newVersion)
149                dep.version = binding.newVersion
150                redirecting = True
151    return redirecting
152
153
154def checkCache(fnm, strip=False, upx=False, upx_exclude=None, dist_nm=None):
155    """
156    Cache prevents preprocessing binary files again and again.
157
158    'dist_nm'  Filename relative to dist directory. We need it on Mac
159               to determine level of paths for @loader_path like
160               '@loader_path/../../' for qt4 plugins.
161    """
162    from ..config import CONF
163    # On darwin a cache is required anyway to keep the libaries
164    # with relative install names. Caching on darwin does not work
165    # since we need to modify binary headers to use relative paths
166    # to dll depencies and starting with '@loader_path'.
167    if not strip and not upx and not is_darwin and not is_win:
168        return fnm
169
170    if dist_nm is not None and ":" in dist_nm:
171        # A file embedded in another pyinstaller build via multipackage
172        # No actual file exists to process
173        return fnm
174
175    if strip:
176        strip = True
177    else:
178        strip = False
179    upx_exclude = upx_exclude or []
180    upx = (upx and (is_win or is_cygwin) and
181           os.path.normcase(os.path.basename(fnm)) not in upx_exclude)
182
183    # Load cache index
184    # Make cachedir per Python major/minor version.
185    # This allows parallel building of executables with different
186    # Python versions as one user.
187    pyver = ('py%d%s') % (sys.version_info[0], sys.version_info[1])
188    arch = platform.architecture()[0]
189    cachedir = os.path.join(CONF['cachedir'], 'bincache%d%d_%s_%s' % (strip, upx, pyver, arch))
190    if not os.path.exists(cachedir):
191        os.makedirs(cachedir)
192    cacheindexfn = os.path.join(cachedir, "index.dat")
193    if os.path.exists(cacheindexfn):
194        try:
195            cache_index = load_py_data_struct(cacheindexfn)
196        except Exception as e:
197            # tell the user they may want to fix their cache
198            # .. however, don't delete it for them; if it keeps getting
199            #    corrupted, we'll never find out
200            logger.warn("pyinstaller bincache may be corrupted; "
201                        "use pyinstaller --clean to fix")
202            raise
203    else:
204        cache_index = {}
205
206    # Verify if the file we're looking for is present in the cache.
207    # Use the dist_mn if given to avoid different extension modules
208    # sharing the same basename get corrupted.
209    if dist_nm:
210        basenm = os.path.normcase(dist_nm)
211    else:
212        basenm = os.path.normcase(os.path.basename(fnm))
213
214    # Binding redirects should be taken into account to see if the file
215    # needs to be reprocessed. The redirects may change if the versions of dependent
216    # manifests change due to system updates.
217    redirects = CONF.get('binding_redirects', [])
218    digest = cacheDigest(fnm, redirects)
219    cachedfile = os.path.join(cachedir, basenm)
220    cmd = None
221    if basenm in cache_index:
222        if digest != cache_index[basenm]:
223            os.remove(cachedfile)
224        else:
225            # On Mac OS X we need relative paths to dll dependencies
226            # starting with @executable_path
227            if is_darwin:
228                dylib.mac_set_relative_dylib_deps(cachedfile, dist_nm)
229            return cachedfile
230
231
232    # Optionally change manifest and its deps to private assemblies
233    if fnm.lower().endswith(".manifest"):
234        manifest = winmanifest.Manifest()
235        manifest.filename = fnm
236        with open(fnm, "rb") as f:
237            manifest.parse_string(f.read())
238        if CONF.get('win_private_assemblies', False):
239            if manifest.publicKeyToken:
240                logger.info("Changing %s into private assembly", os.path.basename(fnm))
241            manifest.publicKeyToken = None
242            for dep in manifest.dependentAssemblies:
243                # Exclude common-controls which is not bundled
244                if dep.name != "Microsoft.Windows.Common-Controls":
245                    dep.publicKeyToken = None
246
247        applyRedirects(manifest, redirects)
248
249        manifest.writeprettyxml(cachedfile)
250        return cachedfile
251
252    if upx:
253        if strip:
254            fnm = checkCache(fnm, strip=True, upx=False)
255        bestopt = "--best"
256        # FIXME: Linux builds of UPX do not seem to contain LZMA (they assert out)
257        # A better configure-time check is due.
258        if CONF["hasUPX"] >= (3,) and os.name == "nt":
259            bestopt = "--lzma"
260
261        upx_executable = "upx"
262        if CONF.get('upx_dir'):
263            upx_executable = os.path.join(CONF['upx_dir'], upx_executable)
264        cmd = [upx_executable, bestopt, "-q", cachedfile]
265    else:
266        if strip:
267            strip_options = []
268            if is_darwin:
269                # The default strip behaviour breaks some shared libraries
270                # under Mac OSX.
271                # -S = strip only debug symbols.
272                strip_options = ["-S"]
273            cmd = ["strip"] + strip_options + [cachedfile]
274
275    if not os.path.exists(os.path.dirname(cachedfile)):
276        os.makedirs(os.path.dirname(cachedfile))
277    # There are known some issues with 'shutil.copy2' on Mac OS X 10.11
278    # with copying st_flags. Issue #1650.
279    # 'shutil.copy' copies also permission bits and it should be sufficient for
280    # PyInstalle purposes.
281    shutil.copy(fnm, cachedfile)
282    # TODO find out if this is still necessary when no longer using shutil.copy2()
283    if hasattr(os, 'chflags'):
284        # Some libraries on FreeBSD have immunable flag (libthr.so.3, for example)
285        # If flags still remains, os.chmod will failed with:
286        # OSError: [Errno 1] Operation not permitted.
287        try:
288            os.chflags(cachedfile, 0)
289        except OSError:
290            pass
291    os.chmod(cachedfile, 0o755)
292
293    if os.path.splitext(fnm.lower())[1] in (".pyd", ".dll"):
294        # When shared assemblies are bundled into the app, they may optionally be
295        # changed into private assemblies.
296        try:
297            res = winmanifest.GetManifestResources(os.path.abspath(cachedfile))
298        except winresource.pywintypes.error as e:
299            if e.args[0] == winresource.ERROR_BAD_EXE_FORMAT:
300                # Not a win32 PE file
301                pass
302            else:
303                logger.error(os.path.abspath(cachedfile))
304                raise
305        else:
306            if winmanifest.RT_MANIFEST in res and len(res[winmanifest.RT_MANIFEST]):
307                for name in res[winmanifest.RT_MANIFEST]:
308                    for language in res[winmanifest.RT_MANIFEST][name]:
309                        try:
310                            manifest = winmanifest.Manifest()
311                            manifest.filename = ":".join([cachedfile,
312                                                          str(winmanifest.RT_MANIFEST),
313                                                          str(name),
314                                                          str(language)])
315                            manifest.parse_string(res[winmanifest.RT_MANIFEST][name][language],
316                                                  False)
317                        except Exception as exc:
318                            logger.error("Cannot parse manifest resource %s, "
319                                         "%s", name, language)
320                            logger.error("From file %s", cachedfile, exc_info=1)
321                        else:
322                            # optionally change manifest to private assembly
323                            private = CONF.get('win_private_assemblies', False)
324                            if private:
325                                if manifest.publicKeyToken:
326                                    logger.info("Changing %s into a private assembly",
327                                                os.path.basename(fnm))
328                                manifest.publicKeyToken = None
329
330                                # Change dep to private assembly
331                                for dep in manifest.dependentAssemblies:
332                                    # Exclude common-controls which is not bundled
333                                    if dep.name != "Microsoft.Windows.Common-Controls":
334                                        dep.publicKeyToken = None
335                            redirecting = applyRedirects(manifest, redirects)
336                            if redirecting or private:
337                                try:
338                                    manifest.update_resources(os.path.abspath(cachedfile),
339                                                              [name],
340                                                              [language])
341                                except Exception as e:
342                                    logger.error(os.path.abspath(cachedfile))
343                                    raise
344
345    if cmd:
346        logger.info("Executing - " + ' '.join(cmd))
347        # terminates if execution fails
348        compat.exec_command(*cmd)
349
350    # update cache index
351    cache_index[basenm] = digest
352    save_py_data_struct(cacheindexfn, cache_index)
353
354    # On Mac OS X we need relative paths to dll dependencies
355    # starting with @executable_path
356    if is_darwin:
357        dylib.mac_set_relative_dylib_deps(cachedfile, dist_nm)
358    return cachedfile
359
360
361def cacheDigest(fnm, redirects):
362    hasher = hashlib.md5()
363    with open(fnm, "rb") as f:
364        for chunk in iter(lambda: f.read(16 * 1024), b""):
365            hasher.update(chunk)
366    if redirects:
367        redirects = str(redirects)
368        if is_py3:
369            redirects = redirects.encode('utf-8')
370        hasher.update(redirects)
371    digest = bytearray(hasher.digest())
372    return digest
373
374
375def _check_path_overlap(path):
376    """
377    Check that path does not overlap with WORKPATH or SPECPATH (i.e.
378    WORKPATH and SPECPATH may not start with path, which could be
379    caused by a faulty hand-edited specfile)
380
381    Raise SystemExit if there is overlap, return True otherwise
382    """
383    from ..config import CONF
384    specerr = 0
385    if CONF['workpath'].startswith(path):
386        logger.error('Specfile error: The output path "%s" contains '
387                     'WORKPATH (%s)', path, CONF['workpath'])
388        specerr += 1
389    if CONF['specpath'].startswith(path):
390        logger.error('Specfile error: The output path "%s" contains '
391                     'SPECPATH (%s)', path, CONF['specpath'])
392        specerr += 1
393    if specerr:
394        raise SystemExit('Error: Please edit/recreate the specfile (%s) '
395                         'and set a different output name (e.g. "dist").'
396                         % CONF['spec'])
397    return True
398
399
400def _make_clean_directory(path):
401    """
402    Create a clean directory from the given directory name
403    """
404    if _check_path_overlap(path):
405        if os.path.isdir(path) or os.path.isfile(path):
406            try:
407                os.remove(path)
408            except OSError:
409                _rmtree(path)
410
411        os.makedirs(path)
412
413
414def _rmtree(path):
415    """
416    Remove directory and all its contents, but only after user confirmation,
417    or if the -y option is set
418    """
419    from ..config import CONF
420    if CONF['noconfirm']:
421        choice = 'y'
422    elif sys.stdout.isatty():
423        choice = compat.stdin_input('WARNING: The output directory "%s" and ALL ITS '
424                           'CONTENTS will be REMOVED! Continue? (y/N)' % path)
425    else:
426        raise SystemExit('Error: The output directory "%s" is not empty. '
427                         'Please remove all its contents or use the '
428                         '-y option (remove output directory without '
429                         'confirmation).' % path)
430    if choice.strip().lower() == 'y':
431        logger.info('Removing dir %s', path)
432        shutil.rmtree(path)
433    else:
434        raise SystemExit('User aborted')
435
436
437# TODO Refactor to prohibit empty target directories. As the docstring
438#below documents, this function currently permits the second item of each
439#2-tuple in "hook.datas" to be the empty string, in which case the target
440#directory defaults to the source directory's basename. However, this
441#functionality is very fragile and hence bad. Instead:
442#
443#* An exception should be raised if such item is empty.
444#* All hooks currently passing the empty string for such item (e.g.,
445#  "hooks/hook-babel.py", "hooks/hook-matplotlib.py") should be refactored
446#  to instead pass such basename.
447def format_binaries_and_datas(binaries_or_datas, workingdir=None):
448    """
449    Convert the passed list of hook-style 2-tuples into a returned set of
450    `TOC`-style 2-tuples.
451
452    Elements of the passed list are 2-tuples `(source_dir_or_glob, target_dir)`.
453    Elements of the returned set are 2-tuples `(target_file, source_file)`.
454    For backwards compatibility, the order of elements in the former tuples are
455    the reverse of the order of elements in the latter tuples!
456
457    Parameters
458    ----------
459    binaries_or_datas : list
460        List of hook-style 2-tuples (e.g., the top-level `binaries` and `datas`
461        attributes defined by hooks) whose:
462        * The first element is either:
463          * A glob matching only the absolute or relative paths of source
464            non-Python data files.
465          * The absolute or relative path of a source directory containing only
466            source non-Python data files.
467        * The second element ist he relative path of the target directory
468          into which these source files will be recursively copied.
469
470        If the optional `workingdir` parameter is passed, source paths may be
471        either absolute or relative; else, source paths _must_ be absolute.
472    workingdir : str
473        Optional absolute path of the directory to which all relative source
474        paths in the `binaries_or_datas` parameter will be prepended by (and
475        hence converted into absolute paths) _or_ `None` if these paths are to
476        be preserved as relative. Defaults to `None`.
477
478    Returns
479    ----------
480    set
481        Set of `TOC`-style 2-tuples whose:
482        * First element is the absolute or relative path of a target file.
483        * Second element is the absolute or relative path of the corresponding
484          source file to be copied to this target file.
485    """
486    toc_datas = set()
487
488    for src_root_path_or_glob, trg_root_dir in binaries_or_datas:
489        if not trg_root_dir:
490            raise SystemExit("Empty DEST not allowed when adding binary "
491                             "and data files. "
492                             "Maybe you want to used %r.\nCaused by %r." %
493                             (os.curdir, src_root_path_or_glob))
494        # Convert relative to absolute paths if required.
495        if workingdir and not os.path.isabs(src_root_path_or_glob):
496            src_root_path_or_glob = os.path.join(
497                workingdir, src_root_path_or_glob)
498
499        # Normalize paths.
500        src_root_path_or_glob = os.path.normpath(src_root_path_or_glob)
501        if os.path.isfile(src_root_path_or_glob):
502            src_root_paths = [src_root_path_or_glob]
503        else:
504            # List of the absolute paths of all source paths matching the
505            # current glob.
506            src_root_paths = glob.glob(src_root_path_or_glob)
507
508        if not src_root_paths:
509            msg = 'Unable to find "%s" when adding binary and data files.' % (
510                src_root_path_or_glob)
511            # on Debian/Ubuntu, missing pyconfig.h files can be fixed with
512            # installing python-dev
513            if src_root_path_or_glob.endswith("pyconfig.h"):
514                msg += """This would mean your Python installation doesn't
515come with proper library files. This usually happens by missing development
516package, or unsuitable build parameters of Python installation.
517* On Debian/Ubuntu, you would need to install Python development packages
518  * apt-get install python3-dev
519  * apt-get install python-dev
520* If you're building Python by yourself, please rebuild your Python with
521`--enable-shared` (or, `--enable-framework` on Darwin)
522"""
523            raise SystemExit(msg)
524
525        for src_root_path in src_root_paths:
526            if os.path.isfile(src_root_path):
527                # Normalizing the result to remove redundant relative
528                # paths (e.g., removing "./" from "trg/./file").
529                toc_datas.add((
530                    os.path.normpath(os.path.join(
531                        trg_root_dir, os.path.basename(src_root_path))),
532                    os.path.normpath(src_root_path)))
533            elif os.path.isdir(src_root_path):
534                for src_dir, src_subdir_basenames, src_file_basenames in \
535                    os.walk(src_root_path):
536                    # Ensure the current source directory is a subdirectory
537                    # of the passed top-level source directory. Since
538                    # os.walk() does *NOT* follow symlinks by default, this
539                    # should be the case. (But let's make sure.)
540                    assert src_dir.startswith(src_root_path)
541
542                    # Relative path of the current target directory,
543                    # obtained by:
544                    #
545                    # * Stripping the top-level source directory from the
546                    #   current source directory (e.g., removing "/top" from
547                    #   "/top/dir").
548                    # * Normalizing the result to remove redundant relative
549                    #   paths (e.g., removing "./" from "trg/./file").
550                    trg_dir = os.path.normpath(os.path.join(
551                        trg_root_dir,
552                        os.path.relpath(src_dir, src_root_path)))
553
554                    for src_file_basename in src_file_basenames:
555                        src_file = os.path.join(src_dir, src_file_basename)
556                        if os.path.isfile(src_file):
557                            # Normalize the result to remove redundant relative
558                            # paths (e.g., removing "./" from "trg/./file").
559                            toc_datas.add((
560                                os.path.normpath(
561                                    os.path.join(trg_dir, src_file_basename)),
562                                os.path.normpath(src_file)))
563
564    return toc_datas
565
566
567def _load_code(modname, filename):
568    path_item = os.path.dirname(filename)
569    if os.path.basename(filename).startswith('__init__.py'):
570        # this is a package
571        path_item = os.path.dirname(path_item)
572    if os.path.basename(path_item) == '__pycache__':
573        path_item = os.path.dirname(path_item)
574    importer = pkgutil.get_importer(path_item)
575    package, _, modname = modname.rpartition('.')
576
577    if sys.version_info >= (3, 3) and hasattr(importer, 'find_loader'):
578        loader, portions = importer.find_loader(modname)
579    else:
580        loader = importer.find_module(modname)
581
582    logger.debug('Compiling %s', filename)
583    if loader and hasattr(loader, 'get_code'):
584        return loader.get_code(modname)
585    else:
586        # Just as ``python foo.bar`` will read and execute statements in
587        # ``foo.bar``,  even though it lacks the ``.py`` extension, so
588        # ``pyinstaller foo.bar``  should also work. However, Python's import
589        # machinery doesn't load files without a ``.py`` extension. So, use
590        # ``compile`` instead.
591        #
592        # On a side note, neither the Python 2 nor Python 3 calls to
593        # ``pkgutil`` and ``find_module`` above handle modules ending in
594        # ``.pyw``, even though ``imp.find_module`` and ``import <name>`` both
595        # work. This code supports ``.pyw`` files.
596
597        # Open the source file in binary mode and allow the `compile()` call to
598        # detect the source encoding.
599        with open_file(filename, 'rb') as f:
600            source = f.read()
601        return compile(source, filename, 'exec')
602
603def get_code_object(modname, filename):
604    """
605    Get the code-object for a module.
606
607    This is a extra-simple version for compiling a module. It's
608    not worth spending more effort here, as it is only used in the
609    rare case if outXX-Analysis.toc exists, but outXX-PYZ.toc does
610    not.
611    """
612
613    try:
614        if filename in ('-', None):
615            # This is a NamespacePackage, modulegraph marks them
616            # by using the filename '-'. (But wants to use None,
617            # so check for None, too, to be forward-compatible.)
618            logger.debug('Compiling namespace package %s', modname)
619            txt = '#\n'
620            return compile(txt, filename, 'exec')
621        else:
622            logger.debug('Compiling %s', filename)
623            co = _load_code(modname, filename)
624            if not co:
625                raise ValueError("Module file %s is missing" % filename)
626            return co
627    except SyntaxError as e:
628        print("Syntax error in ", filename)
629        print(e.args)
630        raise
631
632
633def strip_paths_in_code(co, new_filename=None):
634
635    # Paths to remove from filenames embedded in code objects
636    replace_paths = sys.path + CONF['pathex']
637    # Make sure paths end with os.sep
638    replace_paths = [os.path.join(f, '') for f in replace_paths]
639
640    if new_filename is None:
641        original_filename = os.path.normpath(co.co_filename)
642        for f in replace_paths:
643            if original_filename.startswith(f):
644                new_filename = original_filename[len(f):]
645                break
646
647        else:
648            return co
649
650    code_func = type(co)
651
652    consts = tuple(
653        strip_paths_in_code(const_co, new_filename)
654        if isinstance(const_co, code_func) else const_co
655        for const_co in co.co_consts
656    )
657
658    # co_kwonlyargcount added in some version of Python 3
659    if hasattr(co, 'co_kwonlyargcount'):
660        return code_func(co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize,
661                     co.co_flags, co.co_code, consts, co.co_names,
662                     co.co_varnames, new_filename, co.co_name,
663                     co.co_firstlineno, co.co_lnotab,
664                     co.co_freevars, co.co_cellvars)
665    else:
666        return code_func(co.co_argcount, co.co_nlocals, co.co_stacksize,
667                     co.co_flags, co.co_code, consts, co.co_names,
668                     co.co_varnames, new_filename, co.co_name,
669                     co.co_firstlineno, co.co_lnotab,
670                     co.co_freevars, co.co_cellvars)
671
672
673def fake_pyc_timestamp(buf):
674    """
675    Reset the timestamp from a .pyc-file header to a fixed value.
676
677    This enables deterministic builds without having to set pyinstaller
678    source metadata (mtime) since that changes the pyc-file contents.
679
680    _buf_ must at least contain the full pyc-file header.
681    """
682    assert buf[:4] == compat.BYTECODE_MAGIC, \
683        "Expected pyc magic {}, got {}".format(compat.BYTECODE_MAGIC, buf[:4])
684    start, end = 4, 8
685    if is_py37:
686        # see https://www.python.org/dev/peps/pep-0552/
687        (flags,) = struct.unpack_from(">I", buf, 4)
688        if flags & 1:
689            # We are in the future and hash-based pyc-files are used, so
690            # clear "check_source" flag, since there is no source
691            buf[4:8] = struct.pack(">I", flags ^ 2)
692            return buf
693        else:
694            # no hash-based pyc-file, timestamp is the next field
695            start, end = 8, 12
696
697    ts = b'pyi0'  # So people know where this comes from
698    return buf[:start] + ts + buf[end:]
699