signatureCloudCompare.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import argparse
  4. import logging
  5. import os
  6. import subprocess
  7. import sys
  8. from pathlib import Path
  9. import multiprocessing
  10. logger = logging.getLogger(__name__)
  11. # Be sure to use system codesign and not one embedded into the conda env
  12. CODESIGN_FULL_PATH = "/usr/bin/codesign"
  13. # Entitlements declaration
  14. PYAPP_ENTITLEMENTS = Path(__file__).parent / "pythonapp.entitlements"
  15. CCAPP_ENTITLEMENTS = Path(__file__).parent / "ccapp.entitlements"
  16. HARDENED_CCAPP_ENTITLEMENTS = Path(__file__).parent / "hdapp.entitlements"
  17. class CCSignBundleConfig:
  18. install_path: Path
  19. bundle_abs_path: Path
  20. cc_bin_path: Path
  21. signature: str
  22. identifier: str
  23. embed_python: bool
  24. def __init__(self, install_path: Path, signature: str, identifier: str, embed_python: bool) -> None:
  25. """Construct a configuration.
  26. Args:
  27. ----
  28. install_path (Path): Path where CC is "installed".
  29. signature (str): Signature to use to sign binaries in the bundle (`codesign -s` option).
  30. identifier (str): Identifier to use to sign binaries in the bundle (`codesign -i` option).
  31. embed_python (bool): Whether or not Python is embedded in the package.
  32. """
  33. self.install_path = install_path
  34. self.bundle_abs_path = install_path / "CloudCompare" / "CloudCompare.app"
  35. self.cc_bin_path = self.bundle_abs_path / "Contents" / "MacOS" / "CloudCompare"
  36. self.embed_python = embed_python
  37. self.signature = signature
  38. self.identifier = identifier
  39. if self.embed_python:
  40. self.embedded_python_rootpath = self.bundle_abs_path / "Contents" / "Resources" / "python"
  41. self.embedded_python_path = self.embedded_python_rootpath / "bin"
  42. self.embedded_python_binary = self.embedded_python_path / "python"
  43. self.embedded_python_libpath = self.embedded_python_rootpath / "lib"
  44. class CCSignBundle:
  45. config: CCSignBundleConfig
  46. def __init__(self, config: CCSignBundleConfig) -> None:
  47. """Construct a CCSingBundle.
  48. Args:
  49. ----
  50. config (CCSignBundleConfig): The configuration.
  51. """
  52. self.config = config
  53. @staticmethod
  54. def _remove_signature(path: Path) -> None:
  55. """Remove signature of a binary file.
  56. Call `codesign` utility via a subprocess
  57. Args:
  58. ----
  59. path (Path): The path to the file to sign.
  60. """
  61. subprocess.run([CODESIGN_FULL_PATH, "--remove-signature", str(path)], stdout=subprocess.PIPE, check=True)
  62. def _add_signature(self, path: Path) -> None:
  63. """Sign a binary file.
  64. Call `codesign` utility via a subprocess. The signature stored
  65. into the `config` object is used to sign the binary.
  66. Args:
  67. ----
  68. path (Path): The path to the file to sign.
  69. """
  70. subprocess.run(
  71. [CODESIGN_FULL_PATH, "-s", self.config.signature, "-f", "--timestamp", str(path)],
  72. stdout=subprocess.PIPE,
  73. check=True,
  74. )
  75. def _add_entitlements(self, path: Path, entitlements: Path) -> None:
  76. """Sign a binary file with some specific entitlements.
  77. Call `codesign` utility via a subprocess. The signature and the
  78. identifier stored into the `config` object are used to sign the binary.
  79. Args:
  80. ----
  81. path (Path): The path to the file to sign.
  82. entitlements (Path): Path to a file that contains entitlements
  83. """
  84. subprocess.run(
  85. [
  86. CODESIGN_FULL_PATH,
  87. "-s",
  88. self.config.signature,
  89. "-f",
  90. "--timestamp",
  91. "-i",
  92. self.config.identifier,
  93. "--entitlements",
  94. str(entitlements),
  95. str(path),
  96. ],
  97. stdout=subprocess.PIPE,
  98. check=True,
  99. )
  100. def sign(self) -> int:
  101. """Process and sign the bundle.
  102. Returns
  103. -------
  104. (int) : Error code at the end of the process.
  105. """
  106. # collect all libs in the bundle
  107. # split set to have 100% Python lib in one set and other libs in another set
  108. # TODO: Not sure the split is really usefull
  109. logger.info("Collect libs in the bundle")
  110. so_generator = self.config.bundle_abs_path.rglob("*.so")
  111. dylib_generator = self.config.bundle_abs_path.rglob("*.dylib")
  112. all_libs = set(list(so_generator) + list(dylib_generator))
  113. if self.config.embed_python:
  114. python_libs = set(filter(lambda p: p.is_relative_to(self.config.embedded_python_rootpath), all_libs))
  115. cc_app_libs = all_libs - python_libs
  116. logger.info("--- Total # lib in the bundle %i", len(all_libs))
  117. if self.config.embed_python:
  118. logger.info("--- Total # lib in the python sub system %i", len(python_libs))
  119. logger.info("--- Total # lib in the CC sub system (framework and plugins) %i", len(cc_app_libs))
  120. logger.info("Remove old signatures")
  121. # create the process pool
  122. process_pool = multiprocessing.Pool()
  123. # Remove signature in all embedded libs
  124. process_pool.map(CCSignBundle._remove_signature, all_libs)
  125. # Remove CC signature as well
  126. CCSignBundle._remove_signature(self.config.cc_bin_path)
  127. if self.config.embed_python:
  128. CCSignBundle._remove_signature(self.config.embedded_python_binary)
  129. logger.info("Sign Python dynamic libraries")
  130. process_pool.map(self._add_signature, python_libs)
  131. logger.info("Add entitlements to Python binary")
  132. self._add_entitlements(self.config.embedded_python_binary, PYAPP_ENTITLEMENTS)
  133. logger.info("Sign CC dynamic libraries")
  134. process_pool.map(self._add_signature, cc_app_libs)
  135. logger.info("Add entitlements to CC binary")
  136. self._add_entitlements(self.config.cc_bin_path, CCAPP_ENTITLEMENTS)
  137. logger.warning("Add entitlements to CC bundle (CloudCompare.app)")
  138. self._add_entitlements(self.config.bundle_abs_path, CCAPP_ENTITLEMENTS)
  139. else:
  140. logger.info("Sign CC dynamic libraries")
  141. process_pool.map(self._add_signature, cc_app_libs)
  142. logger.info("Add entitlements to CC binary")
  143. # TODO: the original shell script sign the binary AND add entitlements...
  144. # is it necessary?
  145. self._add_entitlements(self.config.cc_bin_path, HARDENED_CCAPP_ENTITLEMENTS)
  146. logger.warning("Add entitlements to CC bundle (CloudCompare.app)")
  147. self._add_entitlements(self.config.bundle_abs_path, HARDENED_CCAPP_ENTITLEMENTS)
  148. process_pool.close()
  149. return 0
  150. if __name__ == "__main__":
  151. # configure logger
  152. formatter = "CCSignBundle::%(levelname)-6s:: %(message)s"
  153. logging.basicConfig(level=logging.INFO, format=formatter)
  154. std_handler = logging.StreamHandler()
  155. # CLI parser
  156. parser = argparse.ArgumentParser("CCSignBundle")
  157. parser.add_argument(
  158. "install_path",
  159. help="Path where the CC application is installed (CMake install prefix)",
  160. type=Path,
  161. )
  162. parser.add_argument(
  163. "--signature",
  164. help="Signature to use for code signing (or will use CC_BUNDLE_SIGN var)",
  165. type=str,
  166. )
  167. parser.add_argument(
  168. "--identifier",
  169. help="Identifier to use for code signing",
  170. type=str,
  171. default="fr.openfields.CloudCompare",
  172. )
  173. parser.add_argument(
  174. "--embed_python",
  175. help="Whether embedding python or not",
  176. action="store_true",
  177. )
  178. arguments = parser.parse_args()
  179. if arguments.signature is not None:
  180. signature = arguments.signature
  181. else:
  182. signature = os.environ.get("CC_BUNDLE_SIGN")
  183. if signature is None:
  184. logger.error(
  185. "CC_BUNDLE_SIGN variable is undefined. Please define it or use the `signature` argument in the CLI",
  186. )
  187. sys.exit(1)
  188. logger.info("Identifier: %s", arguments.identifier)
  189. logger.debug("Signature: %s", signature) # Could be dangerous to display this
  190. try:
  191. Path(CODESIGN_FULL_PATH).exists()
  192. except Exception:
  193. logger.exception("Unable to find codesign binary on this computer")
  194. sys.exit(1)
  195. config = CCSignBundleConfig(
  196. arguments.install_path,
  197. signature,
  198. arguments.identifier,
  199. arguments.embed_python,
  200. )
  201. sign_bundle = CCSignBundle(config)
  202. sys.exit(sign_bundle.sign())