osx.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2014-2021, 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. Utils for Mac OS X platform.
  13. """
  14. import os
  15. import math
  16. import shutil
  17. from PyInstaller.compat import base_prefix, exec_command_all
  18. from macholib.MachO import MachO
  19. from macholib.mach_o import LC_BUILD_VERSION, LC_VERSION_MIN_MACOSX, \
  20. LC_SEGMENT_64, LC_SYMTAB, LC_CODE_SIGNATURE
  21. import PyInstaller.log as logging
  22. logger = logging.getLogger(__name__)
  23. def is_homebrew_env():
  24. """
  25. Check if Python interpreter was installed via Homebrew command 'brew'.
  26. :return: True if Homebrew else otherwise.
  27. """
  28. # Python path prefix should start with Homebrew prefix.
  29. env_prefix = get_homebrew_prefix()
  30. if env_prefix and base_prefix.startswith(env_prefix):
  31. return True
  32. return False
  33. def is_macports_env():
  34. """
  35. Check if Python interpreter was installed via Macports command 'port'.
  36. :return: True if Macports else otherwise.
  37. """
  38. # Python path prefix should start with Macports prefix.
  39. env_prefix = get_macports_prefix()
  40. if env_prefix and base_prefix.startswith(env_prefix):
  41. return True
  42. return False
  43. def get_homebrew_prefix():
  44. """
  45. :return: Root path of the Homebrew environment.
  46. """
  47. prefix = shutil.which('brew')
  48. # Conversion: /usr/local/bin/brew -> /usr/local
  49. prefix = os.path.dirname(os.path.dirname(prefix))
  50. return prefix
  51. def get_macports_prefix():
  52. """
  53. :return: Root path of the Macports environment.
  54. """
  55. prefix = shutil.which('port')
  56. # Conversion: /usr/local/bin/port -> /usr/local
  57. prefix = os.path.dirname(os.path.dirname(prefix))
  58. return prefix
  59. def _find_version_cmd(header):
  60. """
  61. Helper that finds the version command in the given MachO header.
  62. """
  63. # The SDK version is stored in LC_BUILD_VERSION command (used when
  64. # targeting the latest versions of macOS) or in older LC_VERSION_MIN_MACOSX
  65. # command. Check for presence of either.
  66. version_cmd = [cmd for cmd in header.commands
  67. if cmd[0].cmd in {LC_BUILD_VERSION, LC_VERSION_MIN_MACOSX}]
  68. assert len(version_cmd) == 1, \
  69. "Expected exactly one LC_BUILD_VERSION or " \
  70. "LC_VERSION_MIN_MACOSX command!"
  71. return version_cmd[0]
  72. def get_macos_sdk_version(filename):
  73. """
  74. Obtain the version of macOS SDK against which the given binary
  75. was built.
  76. NOTE: currently, version is retrieved only from the first arch
  77. slice in the binary.
  78. :return: (major, minor, revision) tuple
  79. """
  80. binary = MachO(filename)
  81. header = binary.headers[0]
  82. # Find version command using helper
  83. version_cmd = _find_version_cmd(header)
  84. return _hex_triplet(version_cmd[1].sdk)
  85. def _hex_triplet(version):
  86. # Parse SDK version number
  87. major = (version & 0xFF0000) >> 16
  88. minor = (version & 0xFF00) >> 8
  89. revision = (version & 0xFF)
  90. return major, minor, revision
  91. def macosx_version_min(filename: str) -> tuple:
  92. """Get the -macosx-version-min used to compile a macOS binary.
  93. For fat binaries, the minimum version is selected.
  94. """
  95. versions = []
  96. for header in MachO(filename).headers:
  97. cmd = _find_version_cmd(header)
  98. if cmd[0].cmd == LC_VERSION_MIN_MACOSX:
  99. versions.append(cmd[1].version)
  100. else:
  101. # macOS >= 10.14 uses LC_BUILD_VERSION instead.
  102. versions.append(cmd[1].minos)
  103. return min(map(_hex_triplet, versions))
  104. def set_macos_sdk_version(filename, major, minor, revision):
  105. """
  106. Overwrite the macOS SDK version declared in the given binary with
  107. the specified version.
  108. NOTE: currently, only version in the first arch slice is modified.
  109. """
  110. # Validate values
  111. assert major >= 0 and major <= 255, "Invalid major version value!"
  112. assert minor >= 0 and minor <= 255, "Invalid minor version value!"
  113. assert revision >= 0 and revision <= 255, "Invalid revision value!"
  114. # Open binary
  115. binary = MachO(filename)
  116. header = binary.headers[0]
  117. # Find version command using helper
  118. version_cmd = _find_version_cmd(header)
  119. # Write new SDK version number
  120. version_cmd[1].sdk = major << 16 | minor << 8 | revision
  121. # Write changes back.
  122. with open(binary.filename, 'rb+') as fp:
  123. binary.write(fp)
  124. def fix_exe_for_code_signing(filename):
  125. """
  126. Fixes the Mach-O headers to make code signing possible.
  127. Code signing on OS X does not work out of the box with embedding
  128. .pkg archive into the executable.
  129. The fix is done this way:
  130. - Make the embedded .pkg archive part of the Mach-O 'String Table'.
  131. 'String Table' is at end of the OS X exe file so just change the size
  132. of the table to cover the end of the file.
  133. - Fix the size of the __LINKEDIT segment.
  134. Note: the above fix works only if the single-arch thin executable or
  135. the last arch slice in a multi-arch fat executable is not signed,
  136. because LC_CODE_SIGNATURE comes after LC_SYMTAB, and because modification
  137. of headers invalidates the code signature. On modern arm64 macOS, code
  138. signature is mandatory, and therefore compilers create a dummy
  139. signature when executable is built. In such cases, that signature
  140. needs to be removed before this function is called.
  141. Mach-O format specification:
  142. http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/Mach-O.5.html
  143. """
  144. # Estimate the file size after data was appended
  145. file_size = os.path.getsize(filename)
  146. # Take the last available header. A single-arch thin binary contains a
  147. # single slice, while a multi-arch fat binary contains multiple, and we
  148. # need to modify the last one, which is adjacent to the appended data.
  149. executable = MachO(filename)
  150. header = executable.headers[-1]
  151. # Sanity check: ensure the executable slice is not signed (otherwise
  152. # signature's section comes last in the __LINKEDIT segment).
  153. sign_sec = [cmd for cmd in header.commands
  154. if cmd[0].cmd == LC_CODE_SIGNATURE]
  155. assert len(sign_sec) == 0, "Executable contains code signature!"
  156. # Find __LINKEDIT segment by name (16-byte zero padded string)
  157. __LINKEDIT_NAME = b'__LINKEDIT\x00\x00\x00\x00\x00\x00'
  158. linkedit_seg = [cmd for cmd in header.commands
  159. if cmd[0].cmd == LC_SEGMENT_64
  160. and cmd[1].segname == __LINKEDIT_NAME]
  161. assert len(linkedit_seg) == 1, "Expected exactly one __LINKEDIT segment!"
  162. linkedit_seg = linkedit_seg[0][1] # Take the segment command entry
  163. # Find SYMTAB section
  164. symtab_sec = [cmd for cmd in header.commands
  165. if cmd[0].cmd == LC_SYMTAB]
  166. assert len(symtab_sec) == 1, "Expected exactly one SYMTAB section!"
  167. symtab_sec = symtab_sec[0][1] # Take the symtab command entry
  168. # Sanity check; the string table is located at the end of the SYMTAB
  169. # section, which in turn is the last section in the __LINKEDIT segment
  170. assert linkedit_seg.fileoff + linkedit_seg.filesize == \
  171. symtab_sec.stroff + symtab_sec.strsize, "Sanity check failed!"
  172. # Compute the old/declared file size (header.offset is zero for
  173. # single-arch thin binaries)
  174. old_file_size = \
  175. header.offset + linkedit_seg.fileoff + linkedit_seg.filesize
  176. delta = file_size - old_file_size
  177. # Expand the string table in SYMTAB section...
  178. symtab_sec.strsize += delta
  179. # .. as well as its parent __LINEDIT segment
  180. linkedit_seg.filesize += delta
  181. # Compute new vmsize by rounding filesize up to full page size
  182. page_size = (0x4000 if _get_arch_string(header.header).startswith('arm64')
  183. else 0x1000)
  184. linkedit_seg.vmsize = \
  185. math.ceil(linkedit_seg.filesize / page_size) * page_size
  186. # NOTE: according to spec, segments need to be aligned to page
  187. # boundaries: 0x4000 (16 kB) for arm64, 0x1000 (4 kB) for other arches.
  188. # But it seems we can get away without rounding and padding the segment
  189. # file size - perhaps because it is the last one?
  190. # Write changes
  191. with open(filename, 'rb+') as fp:
  192. executable.write(fp)
  193. # In fat binaries, we also need to adjust the fat header. macholib as
  194. # of version 1.14 does not support this, so we need to do it ourselves...
  195. if executable.fat:
  196. from macholib.mach_o import FAT_MAGIC, FAT_MAGIC_64
  197. from macholib.mach_o import fat_header, fat_arch, fat_arch64
  198. with open(filename, 'rb+') as fp:
  199. # Taken from MachO.load_fat() implementation. The fat
  200. # header's signature has already been validated when we
  201. # loaded the file for the first time.
  202. fat = fat_header.from_fileobj(fp)
  203. if fat.magic == FAT_MAGIC:
  204. archs = [fat_arch.from_fileobj(fp)
  205. for i in range(fat.nfat_arch)]
  206. elif fat.magic == FAT_MAGIC_64:
  207. archs = [fat_arch64.from_fileobj(fp)
  208. for i in range(fat.nfat_arch)]
  209. # Adjust the size in the fat header for the last slice
  210. arch = archs[-1]
  211. arch.size = file_size - arch.offset
  212. # Now write the fat headers back to the file
  213. fp.seek(0)
  214. fat.to_fileobj(fp)
  215. for arch in archs:
  216. arch.to_fileobj(fp)
  217. def _get_arch_string(header):
  218. """
  219. Converts cputype and cpusubtype from mach_o.mach_header_64 into
  220. arch string comparible with lipo/codesign. The list of supported
  221. architectures can be found in man(1) arch.
  222. """
  223. # NOTE: the constants below are taken from macholib.mach_o
  224. cputype = header.cputype
  225. cpusubtype = header.cpusubtype & 0x0FFFFFFF
  226. if cputype == 0x01000000 | 7:
  227. if cpusubtype == 8:
  228. return 'x86_64h' # 64-bit intel (haswell)
  229. else:
  230. return 'x86_64' # 64-bit intel
  231. elif cputype == 0x01000000 | 12:
  232. if cpusubtype == 2:
  233. return 'arm64e'
  234. else:
  235. return 'arm64'
  236. elif cputype == 7:
  237. return 'i386' # 32-bit intel
  238. assert False, 'Unhandled architecture!'
  239. def get_binary_architectures(filename):
  240. """
  241. Inspects the given binary and returns tuple (is_fat, archs),
  242. where is_fat is boolean indicating fat/thin binary, and arch is
  243. list of architectures with lipo/codesign compatible names.
  244. """
  245. executable = MachO(filename)
  246. return bool(executable.fat), [_get_arch_string(hdr.header)
  247. for hdr in executable.headers]
  248. def convert_binary_to_thin_arch(filename, thin_arch):
  249. """
  250. Convert the given fat binary into thin one with the specified
  251. target architecture.
  252. """
  253. cmd_args = ['lipo', '-thin', thin_arch, filename, '-output', filename]
  254. retcode, stdout, stderr = exec_command_all(*cmd_args)
  255. if retcode != 0:
  256. logger.warning("lipo command (%r) failed with error code %d!\n"
  257. "stdout: %r\n"
  258. "stderr: %r",
  259. cmd_args, retcode, stdout, stderr)
  260. raise SystemError("lipo failure!")
  261. def binary_to_target_arch(filename, target_arch, display_name=None):
  262. """
  263. Check that the given binary contains required architecture slice(s)
  264. and convert the fat binary into thin one, if necessary.
  265. """
  266. if not display_name:
  267. display_name = filename # Same as input file
  268. # Check the binary
  269. is_fat, archs = get_binary_architectures(filename)
  270. if is_fat:
  271. if target_arch == 'universal2':
  272. return # Assume fat binary is universal2; nothing to do
  273. else:
  274. assert target_arch in archs, \
  275. f"{display_name} does not contain slice for {target_arch}!"
  276. # Convert to thin arch
  277. logger.debug("Converting fat binary %s (%s) to thin binary (%s)",
  278. filename, display_name, target_arch)
  279. convert_binary_to_thin_arch(filename, target_arch)
  280. else:
  281. assert target_arch != 'universal2', \
  282. f"{display_name} is not a fat binary!"
  283. assert target_arch in archs, \
  284. f"{display_name} is incompatible with target arch " \
  285. f"{target_arch} (has arch: {archs[0]})!"
  286. return # Nothing to do
  287. def remove_signature_from_binary(filename):
  288. """
  289. Remove the signature from all architecture slices of the given
  290. binary file using the codesign utility.
  291. """
  292. logger.debug("Removing signature from file %r", filename)
  293. cmd_args = ['codesign', '--remove', '--all-architectures', filename]
  294. retcode, stdout, stderr = exec_command_all(*cmd_args)
  295. if retcode != 0:
  296. logger.warning("codesign command (%r) failed with error code %d!\n"
  297. "stdout: %r\n"
  298. "stderr: %r",
  299. cmd_args, retcode, stdout, stderr)
  300. raise SystemError("codesign failure!")
  301. def sign_binary(filename, identity=None, entitlements_file=None, deep=False):
  302. """
  303. Sign the binary using codesign utility. If no identity is provided,
  304. ad-hoc signing is performed.
  305. """
  306. extra_args = []
  307. if not identity:
  308. identity = '-' # ad-hoc signing
  309. else:
  310. extra_args.append('--options=runtime') # hardened runtime
  311. if entitlements_file:
  312. extra_args.append('--entitlements')
  313. extra_args.append(entitlements_file)
  314. if deep:
  315. extra_args.append('--deep')
  316. logger.debug("Signing file %r", filename)
  317. cmd_args = ['codesign', '-s', identity, '--force', '--all-architectures',
  318. '--timestamp', *extra_args, filename]
  319. retcode, stdout, stderr = exec_command_all(*cmd_args)
  320. if retcode != 0:
  321. logger.warning("codesign command (%r) failed with error code %d!\n"
  322. "stdout: %r\n"
  323. "stderr: %r",
  324. cmd_args, retcode, stdout, stderr)
  325. raise SystemError("codesign failure!")