Repository: pyinstaller/pyinstaller

Issue:
Build not reproducible when different python environement used
I'm not sure whether this is a bug but I found that a build on Windows is not reproducible when a different virtualenv is used even when both environements have exactly the same packages.

Im using 
Python 3.8.3
pyinstaller 4.1
Windows 10

What I did:
open cmd
mkdir folder_1
cd folder_1
echo print('hello world') >> test.py
set PYTHONHASHSEED=1
C:\path_to_python_38\python.exe -m virtualenv venv
venv\Scripts\activate
python -m pip install pyinstaller
python -m PyInstaller --clean test.py

:: md5 checksum data of: dist\test				       16C651921E1AF9A6CC5B539E4AE772C0
:: md5 checksum data and names of: dist\test	 	4521166FB1B3B049CB7FEEF23FAD181E
:: md5 checksum data of: dist\test\test.exe	 	       3A2E7CDB2FF328172FEF5ECC4805E99B

open cmd
mkdir folder_2
cd folder_2
echo print('hello world') >> test.py
set PYTHONHASHSEED=1
C:\path_to_python_38\python.exe -m virtualenv venv
venv\Scripts\activate
python -m pip install pyinstaller
python -m PyInstaller --clean test.py

:: md5 checksum data of: dist\test				        7FB0108BEB9E484B99D935A1D8121934
:: md5 checksum data and names of: dist\test	 	C8A76D3B688AD89536ABC1A0A5841482
:: md5 checksum data of: dist\test\test.exe	 	        A3183BD4FC7778BBFB6C41CFD6308F0F

This implies that a third person is probably not able to reproduce a given binary and even on the same computer when a folder is changed, the binary will differ. I'm not sure wether this is even a desired feature and maybe I understand this wrong, but what is the point of reproducibility if you cannot reproduce the binary file on a third machine in order to check wether a given binary actually is the one corresponding to a given code.


Discussion:

Resolving patch:
diff --git a/PyInstaller/archive/writers.py b/PyInstaller/archive/writers.py
--- a/PyInstaller/archive/writers.py
+++ b/PyInstaller/archive/writers.py
@@ -29,12 +29,13 @@
 from types import CodeType
 import marshal
 import zlib
+import io
 
 from PyInstaller.building.utils import get_code_object, strip_paths_in_code,\
     fake_pyc_timestamp
 from PyInstaller.loader.pyimod02_archive import PYZ_TYPE_MODULE, PYZ_TYPE_PKG, \
     PYZ_TYPE_DATA
-from ..compat import BYTECODE_MAGIC
+from ..compat import BYTECODE_MAGIC, is_py37
 
 
 class ArchiveWriter(object):
@@ -375,6 +376,37 @@ def add(self, entry):
 
                 code_data = marshal.dumps(code)
                 ulen = len(code_data)
+            elif typcd == 'm':
+                fh = open(pathnm, 'rb')
+                ulen = os.fstat(fh.fileno()).st_size
+                # Check if it is a PYC file
+                header = fh.read(4)
+                fh.seek(0)
+                if header == BYTECODE_MAGIC:
+                    # Read whole header and load code.
+                    # According to PEP-552, in python versions prior to
+                    # 3.7, the PYC header consists of three 32-bit words
+                    # (magic, timestamp, and source file size).
+                    # From python 3.7 on, the PYC header was extended to
+                    # four 32-bit words (magic, flags, and, depending on
+                    # the flags, either timestamp and source file size,
+                    # or a 64-bit hash).
+                    if is_py37:
+                        header = fh.read(16)
+                    else:
+                        header = fh.read(12)
+                    code = marshal.load(fh)
+                    # Strip paths from code, marshal back into module form.
+                    # The header fields (timestamp, size, hash, etc.) are
+                    # all referring to the source file, so our modification
+                    # of the code object does not affect them, and we can
+                    # re-use the original header.
+                    code = strip_paths_in_code(code)
+                    data = header + marshal.dumps(code)
+                    # Create file-like object for timestamp re-write
+                    # in the subsequent steps
+                    fh = io.BytesIO(data)
+                    ulen = len(data)
             else:
                 fh = open(pathnm, 'rb')
                 ulen = os.fstat(fh.fileno()).st_size


Test patch: