writers.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. """
  12. Utilities to create data structures for embedding Python modules and additional files into the executable.
  13. """
  14. import marshal
  15. import os
  16. import shutil
  17. import struct
  18. import sys
  19. import zlib
  20. from PyInstaller.building.utils import get_code_object, replace_filename_in_code_object
  21. from PyInstaller.compat import BYTECODE_MAGIC, is_win, strict_collect_mode
  22. from PyInstaller.loader.pyimod01_archive import PYZ_ITEM_MODULE, PYZ_ITEM_NSPKG, PYZ_ITEM_PKG
  23. class ZlibArchiveWriter:
  24. """
  25. Writer for PyInstaller's PYZ (ZlibArchive) archive. The archive is used to store collected byte-compiled Python
  26. modules, as individually-compressed entries.
  27. """
  28. _PYZ_MAGIC_PATTERN = b'PYZ\0'
  29. _HEADER_LENGTH = 12 + 5
  30. _COMPRESSION_LEVEL = 6 # zlib compression level
  31. def __init__(self, filename, entries, code_dict=None):
  32. """
  33. filename
  34. Target filename of the archive.
  35. entries
  36. An iterable containing entries in the form of tuples: (name, src_path, typecode), where `name` is the name
  37. under which the resource is stored (e.g., python module name, without suffix), `src_path` is name of the
  38. file from which the resource is read, and `typecode` is the Analysis-level TOC typecode (`PYMODULE`).
  39. code_dict
  40. Optional code dictionary containing code objects for analyzed/collected python modules.
  41. """
  42. code_dict = code_dict or {}
  43. with open(filename, "wb") as fp:
  44. # Reserve space for the header.
  45. fp.write(b'\0' * self._HEADER_LENGTH)
  46. # Write entries' data and collect TOC entries
  47. toc = []
  48. for entry in entries:
  49. toc_entry = self._write_entry(fp, entry, code_dict)
  50. toc.append(toc_entry)
  51. # Write TOC
  52. toc_offset = fp.tell()
  53. toc_data = marshal.dumps(toc)
  54. fp.write(toc_data)
  55. # Write header:
  56. # - PYZ magic pattern (4 bytes)
  57. # - python bytecode magic pattern (4 bytes)
  58. # - TOC offset (32-bit int, 4 bytes)
  59. # - 4 unused bytes
  60. fp.seek(0, os.SEEK_SET)
  61. fp.write(self._PYZ_MAGIC_PATTERN)
  62. fp.write(BYTECODE_MAGIC)
  63. fp.write(struct.pack('!i', toc_offset))
  64. @classmethod
  65. def _write_entry(cls, fp, entry, code_dict):
  66. name, src_path, typecode = entry
  67. assert typecode in {'PYMODULE', 'PYMODULE-1', 'PYMODULE-2'}
  68. if src_path in {'-', None}:
  69. # PEP-420 namespace package; these do not have code objects, but we still need an entry in PYZ to inform our
  70. # run-time module finder/loader of the package's existence. So create a TOC entry for 0-byte data blob,
  71. # and write no data.
  72. return (name, (PYZ_ITEM_NSPKG, fp.tell(), 0))
  73. code_object = code_dict[name]
  74. src_basename, _ = os.path.splitext(os.path.basename(src_path))
  75. if src_basename == '__init__':
  76. typecode = PYZ_ITEM_PKG
  77. co_filename = os.path.join(*name.split('.'), '__init__.py')
  78. else:
  79. typecode = PYZ_ITEM_MODULE
  80. co_filename = os.path.join(*name.split('.')) + '.py'
  81. # Replace co_filename on code object with anonymized version without absolute path to the module.
  82. code_object = replace_filename_in_code_object(code_object, co_filename)
  83. # Serialize
  84. data = marshal.dumps(code_object)
  85. # First compress, then encrypt.
  86. obj = zlib.compress(data, cls._COMPRESSION_LEVEL)
  87. # Create TOC entry
  88. toc_entry = (name, (typecode, fp.tell(), len(obj)))
  89. # Write data blob
  90. fp.write(obj)
  91. return toc_entry
  92. class CArchiveWriter:
  93. """
  94. Writer for PyInstaller's CArchive (PKG) archive.
  95. This archive contains all files that are bundled within an executable; a PYZ (ZlibArchive), DLLs, Python C
  96. extensions, and other data files that are bundled in onefile mode.
  97. The archive can be read from either C (bootloader code at application's run-time) or Python (for debug purposes).
  98. """
  99. _COOKIE_MAGIC_PATTERN = b'MEI\014\013\012\013\016'
  100. # For cookie and TOC entry structure, see `PyInstaller.archive.readers.CArchiveReader`.
  101. _COOKIE_FORMAT = '!8sIIII64s'
  102. _COOKIE_LENGTH = struct.calcsize(_COOKIE_FORMAT)
  103. _TOC_ENTRY_FORMAT = '!IIIIBc'
  104. _TOC_ENTRY_LENGTH = struct.calcsize(_TOC_ENTRY_FORMAT)
  105. _COMPRESSION_LEVEL = 9 # zlib compression level
  106. def __init__(self, filename, entries, pylib_name):
  107. """
  108. filename
  109. Target filename of the archive.
  110. entries
  111. An iterable containing entries in the form of tuples: (dest_name, src_name, compress, typecode), where
  112. `dest_name` is the name under which the resource is stored in the archive (and name under which it is
  113. extracted at runtime), `src_name` is name of the file from which the resouce is read, `compress` is a
  114. boolean compression flag, and `typecode` is the Analysis-level TOC typecode.
  115. pylib_name
  116. Name of the python shared library.
  117. """
  118. self._collected_names = set() # Track collected names for strict package mode.
  119. with open(filename, "wb") as fp:
  120. # Write entries' data and collect TOC entries
  121. toc = []
  122. for entry in entries:
  123. toc_entry = self._write_entry(fp, entry)
  124. toc.append(toc_entry)
  125. # Write TOC
  126. toc_offset = fp.tell()
  127. toc_data = self._serialize_toc(toc)
  128. toc_length = len(toc_data)
  129. fp.write(toc_data)
  130. # Write cookie
  131. archive_length = toc_offset + toc_length + self._COOKIE_LENGTH
  132. pyvers = sys.version_info[0] * 100 + sys.version_info[1]
  133. cookie_data = struct.pack(
  134. self._COOKIE_FORMAT,
  135. self._COOKIE_MAGIC_PATTERN,
  136. archive_length,
  137. toc_offset,
  138. toc_length,
  139. pyvers,
  140. pylib_name.encode('ascii'),
  141. )
  142. fp.write(cookie_data)
  143. def _write_entry(self, fp, entry):
  144. dest_name, src_name, compress, typecode = entry
  145. # Write OPTION entries as-is, without normalizing them. This also exempts them from duplication check,
  146. # allowing them to be specified multiple times.
  147. if typecode == 'o':
  148. return self._write_blob(fp, b"", dest_name, typecode)
  149. # Ensure forward slashes in paths are on Windows converted to back slashes '\\', as on Windows the bootloader
  150. # works only with back slashes.
  151. dest_name = os.path.normpath(dest_name)
  152. if is_win and os.path.sep == '/':
  153. # When building under MSYS, the above path normalization uses Unix-style separators, so replace them
  154. # manually.
  155. dest_name = dest_name.replace(os.path.sep, '\\')
  156. # For symbolic link entries, also ensure that the symlink target path (stored in src_name) is using
  157. # Windows-style back slash separators.
  158. if typecode == 'n':
  159. src_name = src_name.replace(os.path.sep, '\\')
  160. # Strict pack/collect mode: keep track of the destination names, and raise an error if we try to add a duplicate
  161. # (a file with same destination name, subject to OS case normalization rules).
  162. if strict_collect_mode:
  163. normalized_dest = None
  164. if typecode in {'s', 's1', 's2', 'm', 'M'}:
  165. # Exempt python source scripts and modules from the check.
  166. pass
  167. else:
  168. # Everything else; normalize the case
  169. normalized_dest = os.path.normcase(dest_name)
  170. # Check for existing entry, if applicable
  171. if normalized_dest:
  172. if normalized_dest in self._collected_names:
  173. raise ValueError(
  174. f"Attempting to collect a duplicated file into CArchive: {normalized_dest} (type: {typecode})"
  175. )
  176. self._collected_names.add(normalized_dest)
  177. if typecode == 'd':
  178. # Dependency; merge src_name (= reference path prefix) and dest_name (= name) into single-string format that
  179. # is parsed by bootloader.
  180. return self._write_blob(fp, b"", f"{src_name}:{dest_name}", typecode)
  181. elif typecode in {'s', 's1', 's2'}:
  182. # If it is a source code file, compile it to a code object and marshal the object, so it can be unmarshalled
  183. # by the bootloader. For that, we need to know target optimization level, which is stored in typecode.
  184. optim_level = {'s': 0, 's1': 1, 's2': 2}[typecode]
  185. code = get_code_object(dest_name, src_name, optimize=optim_level)
  186. # Construct new `co_filename` by taking destination name, and replace its suffix with the one from the code
  187. # object's co_filename; this should cover all of the following cases:
  188. # - run-time hook script: the source name has a suffix (that is also present in `co_filename` produced by
  189. # `get_code_object`), destination name has no suffix.
  190. # - entry-point script with a suffix: both source name and destination name have the same suffix (and the
  191. # same suffix is also in `co_filename` produced by `get_code_object`)
  192. # - entry-point script without a suffix: neither source name nor destination name have a suffix, but
  193. # `get_code_object` adds a .py suffix to `co_filename` to mitigate potential issues with POSIX
  194. # executables and `traceback` module; we want to preserve this behavior.
  195. co_filename = os.path.splitext(dest_name)[0] + os.path.splitext(code.co_filename)[1]
  196. code = replace_filename_in_code_object(code, co_filename)
  197. return self._write_blob(fp, marshal.dumps(code), dest_name, 's', compress=compress)
  198. elif typecode in ('m', 'M'):
  199. # Read the PYC file. We do not perform compilation here (in contrast to script files in the above branch),
  200. # so typecode does not contain optimization level information.
  201. with open(src_name, "rb") as in_fp:
  202. data = in_fp.read()
  203. assert data[:4] == BYTECODE_MAGIC
  204. # Skip the PYC header, load the code object.
  205. code = marshal.loads(data[16:])
  206. co_filename = dest_name + '.py' # Use dest name with added .py suffix.
  207. code = replace_filename_in_code_object(code, co_filename)
  208. # These module entries are loaded and executed within the bootloader, which requires only the code
  209. # object, without the PYC header.
  210. return self._write_blob(fp, marshal.dumps(code), dest_name, typecode, compress=compress)
  211. elif typecode == 'n':
  212. # Symbolic link; store target name (as NULL-terminated string)
  213. data = src_name.encode('utf-8') + b'\x00'
  214. return self._write_blob(fp, data, dest_name, typecode, compress=compress)
  215. else:
  216. return self._write_file(fp, src_name, dest_name, typecode, compress=compress)
  217. def _write_blob(self, out_fp, blob: bytes, dest_name, typecode, compress=False):
  218. """
  219. Write the binary contents (**blob**) of a small file to the archive and return the corresponding CArchive TOC
  220. entry.
  221. """
  222. data_offset = out_fp.tell()
  223. data_length = len(blob)
  224. if compress:
  225. blob = zlib.compress(blob, level=self._COMPRESSION_LEVEL)
  226. out_fp.write(blob)
  227. return (data_offset, len(blob), data_length, int(compress), typecode, dest_name)
  228. def _write_file(self, out_fp, src_name, dest_name, typecode, compress=False):
  229. """
  230. Stream copy a large file into the archive and return the corresponding CArchive TOC entry.
  231. """
  232. data_offset = out_fp.tell()
  233. data_length = os.stat(src_name).st_size
  234. with open(src_name, 'rb') as in_fp:
  235. if compress:
  236. tmp_buffer = bytearray(16 * 1024)
  237. compressor = zlib.compressobj(self._COMPRESSION_LEVEL)
  238. while True:
  239. num_read = in_fp.readinto(tmp_buffer)
  240. if not num_read:
  241. break
  242. out_fp.write(compressor.compress(tmp_buffer[:num_read]))
  243. out_fp.write(compressor.flush())
  244. else:
  245. shutil.copyfileobj(in_fp, out_fp)
  246. return (data_offset, out_fp.tell() - data_offset, data_length, int(compress), typecode, dest_name)
  247. @classmethod
  248. def _serialize_toc(cls, toc):
  249. serialized_toc = []
  250. for toc_entry in toc:
  251. data_offset, compressed_length, data_length, compress, typecode, name = toc_entry
  252. # Encode names as UTF-8. This should be safe as standard python modules only contain ASCII-characters (and
  253. # standard shared libraries should have the same), and thus the C-code still can handle this correctly.
  254. name = name.encode('utf-8')
  255. name_length = len(name) + 1 # Add 1 for string-terminating zero byte.
  256. # Ensure TOC entries are aligned on 16-byte boundary, so they can be read by bootloader (C code) on
  257. # platforms with strict data alignment requirements (for example linux on `armhf`/`armv7`, such as 32-bit
  258. # Debian Buster on Raspberry Pi).
  259. entry_length = cls._TOC_ENTRY_LENGTH + name_length
  260. if entry_length % 16 != 0:
  261. padding_length = 16 - (entry_length % 16)
  262. name_length += padding_length
  263. # Serialize
  264. serialized_entry = struct.pack(
  265. cls._TOC_ENTRY_FORMAT + f"{name_length}s", # "Ns" format automatically pads the string with zero bytes.
  266. cls._TOC_ENTRY_LENGTH + name_length,
  267. data_offset,
  268. compressed_length,
  269. data_length,
  270. compress,
  271. typecode.encode('ascii'),
  272. name,
  273. )
  274. serialized_toc.append(serialized_entry)
  275. return b''.join(serialized_toc)
  276. class SplashWriter:
  277. """
  278. Writer for the splash screen resources archive.
  279. The resulting archive is added as an entry into the CArchive with the typecode PKG_ITEM_SPLASH.
  280. """
  281. # This struct describes the splash resources as it will be in an buffer inside the bootloader. All necessary parts
  282. # are bundled, the *_len and *_offset fields describe the data beyond this header definition.
  283. # Whereas script and image fields are binary data, the requirements fields describe an array of strings. Each string
  284. # is null-terminated in order to easily iterate over this list from within C.
  285. #
  286. # typedef struct _splash_data_header
  287. # {
  288. # char tcl_shared_library_name[32];
  289. # char tk_shared_library_name[32];
  290. # char tcl_module_directory_name[16];
  291. # char tk_module_directory_name[16];
  292. #
  293. # uint32_t script_len;
  294. # uint32_t script_offset;
  295. #
  296. # uint32_t image_len;
  297. # uint32_t image_offset;
  298. #
  299. # uint32_t requirements_len;
  300. # uint32_t requirements_offset;
  301. #
  302. # uint32_t centering_mode;
  303. # } SPLASH_DATA_HEADER;
  304. #
  305. _HEADER_FORMAT = '!32s 32s 16s 16s II II II I'
  306. _HEADER_LENGTH = struct.calcsize(_HEADER_FORMAT)
  307. # Centering mode values - keep in sync with values defined in `bootloader/src/pyi_splash.h`!
  308. _SPLASH_CENTER_DEFAULT = 0
  309. _SPLASH_CENTER_VIRTUAL_SCREEN = 1
  310. _SPLASH_CENTER_PRIMARY_SCREEN = 2
  311. _SPLASH_CENTER_ACTIVE_SCREEN = 3
  312. # The created archive is compressed by the CArchive, so no need to compress the data here.
  313. def __init__(
  314. self,
  315. filename,
  316. requirements_list,
  317. tcl_shared_library_name,
  318. tk_shared_library_name,
  319. tcl_module_directory_name,
  320. tk_module_directory_name,
  321. image,
  322. script,
  323. center_mode,
  324. ):
  325. """
  326. Writer for splash screen resources that are bundled into the CArchive as a single archive/entry.
  327. :param filename: The filename of the archive to create
  328. :param requirements_list: List of filenames for the requirements array
  329. :param str tcl_shared_library_name: Basename of the Tcl shared library
  330. :param str tk_shared_library_name: Basename of the Tk shared library
  331. :param str tcl_module_directory_name: Basename of the Tcl module directory (e.g., tcl/)
  332. :param str tk_module_directory_name: Basename of the Tk module directory (e.g., tk/)
  333. :param Union[str, bytes] image: Image like object
  334. :param str script: The tcl/tk script to execute to create the screen.
  335. :param str center_mode: Splash screen centering mode (integer value that matches enum used by bootloader)
  336. """
  337. # Ensure forward slashes in dependency names are on Windows converted to back slashes '\\', as on Windows the
  338. # bootloader works only with back slashes.
  339. def _normalize_filename(filename):
  340. filename = os.path.normpath(filename)
  341. if is_win and os.path.sep == '/':
  342. # When building under MSYS, the above path normalization uses Unix-style separators, so replace them
  343. # manually.
  344. filename = filename.replace(os.path.sep, '\\')
  345. return filename
  346. requirements_list = [_normalize_filename(name) for name in requirements_list]
  347. with open(filename, "wb") as fp:
  348. # Reserve space for the header.
  349. fp.write(b'\0' * self._HEADER_LENGTH)
  350. # Serialize the requirements list. This list (more an array) contains the names of all files the bootloader
  351. # needs to extract before the splash screen can be started. The implementation terminates every name with a
  352. # null-byte, that keeps the list short memory wise and makes it iterable from C.
  353. requirements_len = 0
  354. requirements_offset = fp.tell()
  355. for name in requirements_list:
  356. name = name.encode('utf-8') + b'\0'
  357. fp.write(name)
  358. requirements_len += len(name)
  359. # Write splash script
  360. script_offset = fp.tell()
  361. script_len = len(script)
  362. fp.write(script.encode("utf-8"))
  363. # Write splash image. If image is a bytes buffer, it is written directly into the archive. Otherwise, it
  364. # is assumed to be a path and the file is copied into the archive.
  365. image_offset = fp.tell()
  366. if isinstance(image, bytes):
  367. # Image was converted by PIL/Pillow and is already in buffer
  368. image_len = len(image)
  369. fp.write(image)
  370. else:
  371. # Read image into buffer
  372. with open(image, 'rb') as image_fp:
  373. image_data = image_fp.read()
  374. image_len = len(image_data)
  375. fp.write(image_data)
  376. del image_data
  377. # The following strings are written to 16-character fields with zero-padding, which means that we need to
  378. # ensure that their length is strictly below 16 characters (if it were exactly 16, the field would have no
  379. # terminating NULL character!).
  380. def _encode_str(value, field_name, limit):
  381. enc_value = value.encode("utf-8")
  382. if len(enc_value) >= limit:
  383. raise ValueError(
  384. f"Length of the encoded field {field_name!r} ({len(enc_value)}) is greater or equal to the "
  385. f"limit of {limit} characters!"
  386. )
  387. return enc_value
  388. # Write header
  389. header_data = struct.pack(
  390. self._HEADER_FORMAT,
  391. _encode_str(tcl_shared_library_name, 'tcl_shared_library_name', 32),
  392. _encode_str(tk_shared_library_name, 'tk_shared_library_name', 32),
  393. _encode_str(tcl_module_directory_name, 'tcl_module_directory_name', 16),
  394. _encode_str(tk_module_directory_name, 'tk_module_directory_name', 16),
  395. script_len,
  396. script_offset,
  397. image_len,
  398. image_offset,
  399. requirements_len,
  400. requirements_offset,
  401. center_mode,
  402. )
  403. fp.seek(0, os.SEEK_SET)
  404. fp.write(header_data)