vcpkgbuilder.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (c) Contributors to the Open 3D Engine Project.
  4. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  5. #
  6. # SPDX-License-Identifier: Apache-2.0 OR MIT
  7. #
  8. #
  9. """
  10. Package builder for vcpkg-based packages
  11. """
  12. from pathlib import Path
  13. import errno
  14. import json
  15. import os
  16. import pathlib
  17. import shutil
  18. import string
  19. import subprocess
  20. import platform
  21. class VcpkgBuilder(object):
  22. def __init__(self, packageName: str, portName: str, vcpkgDir: pathlib.Path, targetPlatform: str, static: bool):
  23. self._packageName = packageName
  24. self._portName = portName
  25. self._vcpkgDir = vcpkgDir
  26. self._triplet = VcpkgBuilder.tripletForPlatform(targetPlatform, static)
  27. self._customTripletsDir = Path(__file__).resolve().parents[1] / 'vcpkg/triplets'
  28. self._customEnviron = os.environ.copy()
  29. if targetPlatform == 'android':
  30. if 'ANDROID_NDK_HOME' not in os.environ:
  31. raise RuntimeError('Unable to find the Android NDK. '
  32. 'Please set the ANDROID_NDK_HOME environment variable to the root of the Android NDK')
  33. ANDROID_NDK_HOME = os.environ["ANDROID_NDK_HOME"]
  34. print(f"ANDROID_NDK_HOME ='{ANDROID_NDK_HOME}'")
  35. if targetPlatform == 'mac':
  36. # set default env vars that control minos version. Change this if you change the toolchain files.
  37. # ideally we'd put this in triplet files, but it turns out VCPKG triplets like the minos version
  38. # don't acutally work in the case where the target is built using existing makefiles instead of
  39. # CMake (ie, packages like OpenSSL1.1.1x)
  40. self._customEnviron["MACOSX_DEPLOYMENT_TARGET"] = "11.0"
  41. elif targetPlatform == 'ios':
  42. self._customEnviron["IPHONEOS_DEPLOYMENT_TARGET"] = "14.0"
  43. @staticmethod
  44. def tripletForPlatform(platformName: str, static: bool):
  45. platformMap = {
  46. 'mac': {
  47. True: 'x64-osx',
  48. False: 'x64-osx-dynamic',
  49. },
  50. 'windows': {
  51. True: 'x64-windows-static',
  52. False: 'x64-windows',
  53. },
  54. 'linux': {
  55. True: 'x64-linux',
  56. False: 'x64-linux-shared',
  57. },
  58. 'android': {
  59. True: 'arm64-android-static', # arm64-v8a
  60. False: 'arm64-android', # arm64-v8a
  61. },
  62. 'ios': {
  63. True: 'arm64-ios',
  64. False: 'arm64-ios-dynamic',
  65. }
  66. }
  67. try:
  68. useStaticLibsMap = platformMap[platformName]
  69. except KeyError:
  70. raise RuntimeError(f'Platform {platformName} not supported')
  71. try:
  72. return useStaticLibsMap[static]
  73. except KeyError:
  74. raise RuntimeError('Platform {platformName} does not support building {linkageType} libraries'.format(
  75. platformName=platformName,
  76. linkageType='static' if static else 'dynamic',
  77. ))
  78. @staticmethod
  79. def defaultPackagePlatformName():
  80. platformMap = {
  81. 'Darwin': 'mac',
  82. 'Windows': 'windows',
  83. 'Linux': 'linux',
  84. }
  85. return platformMap[platform.system()]
  86. @staticmethod
  87. def deleteFolder(folder):
  88. """
  89. Use the system's remove folder command instead of os.rmdir().
  90. This function does various checks before trying, to avoid having to do those
  91. checks over and over in code.
  92. """
  93. # wrap it up in a Path so that if a string is passed in, this still works.
  94. path_folder = pathlib.Path(folder).resolve(strict=False)
  95. if path_folder.is_file():
  96. raise Exception(f"deleteFolder: Expected a folder, but found a file: {path_folder}. Continuing may be unsafe.")
  97. if not path_folder.is_dir():
  98. print(f'deleteFolder: Folder is already not present: {path_folder}')
  99. return
  100. if platform.system() == 'Windows':
  101. call_result = subprocess.run(' '.join(['rmdir', '/Q', '/S', str(path_folder)]),
  102. shell=True,
  103. capture_output=True,
  104. cwd=str(path_folder.parent.resolve()))
  105. else:
  106. call_result = subprocess.run(' '.join(['rm', '-rf', str(path_folder)]),
  107. shell=True,
  108. capture_output=True,
  109. cwd=str(path_folder.parent.resolve()))
  110. if call_result.returncode != 0:
  111. raise Exception(f"deleteFolder: Unable to delete folder {str(path_folder)}: {str(call_result.stderr.decode())}")
  112. @property
  113. def customTripletsDir(self):
  114. return self._customTripletsDir
  115. @property
  116. def packageName(self):
  117. """The name of the package that this builder will build"""
  118. return self._packageName
  119. @property
  120. def portName(self):
  121. """The name of the vcpkg port that this builder will build"""
  122. return self._portName
  123. @property
  124. def vcpkgDir(self):
  125. """The directory where vcpkg will be cloned to"""
  126. return self._vcpkgDir
  127. @property
  128. def triplet(self):
  129. """The vcpkg triplet to build"""
  130. return self._triplet
  131. def cloneVcpkg(self, lockToCommit: str):
  132. if not (self.vcpkgDir / '.git').exists():
  133. subprocess.check_call(
  134. ['git', 'init',],
  135. cwd=self.vcpkgDir,
  136. )
  137. subprocess.check_call(
  138. ['git', 'remote', 'add', 'origin', 'https://github.com/microsoft/vcpkg.git',],
  139. cwd=self.vcpkgDir,
  140. )
  141. subprocess.check_call(
  142. ['git', 'fetch', 'origin', '--depth=1', lockToCommit,],
  143. cwd=self.vcpkgDir,
  144. )
  145. subprocess.check_call(
  146. ['git', 'checkout', lockToCommit,],
  147. cwd=self.vcpkgDir,
  148. )
  149. def bootstrap(self):
  150. if platform.system() == 'Windows':
  151. subprocess.check_call(
  152. ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', 'scripts/bootstrap.ps1', '-disableMetrics'],
  153. cwd=self.vcpkgDir,
  154. )
  155. else:
  156. subprocess.check_call(
  157. [self.vcpkgDir / 'bootstrap-vcpkg.sh', '-disableMetrics'],
  158. cwd=self.vcpkgDir,
  159. )
  160. def patch(self, patchFile: pathlib.Path):
  161. subprocess.check_output(
  162. ['git', 'apply', '--whitespace=fix', '--verbose', str(patchFile)],
  163. cwd=self.vcpkgDir,
  164. )
  165. def build(self, allow_unsupported=False):
  166. self.remove()
  167. command = [
  168. str(self.vcpkgDir / 'vcpkg'),
  169. 'install',
  170. f'{self.portName}:{self.triplet}',
  171. '--no-binarycaching',
  172. f'--overlay-triplets={self.customTripletsDir}'
  173. ]
  174. if allow_unsupported:
  175. command.append('--allow-unsupported')
  176. subprocess.check_call(
  177. command,
  178. cwd=self.vcpkgDir,
  179. env=self._customEnviron
  180. )
  181. def remove(self):
  182. subprocess.check_call(
  183. [str(self.vcpkgDir / 'vcpkg'), 'remove', f'{self.portName}:{self.triplet}', f'--overlay-triplets={self.customTripletsDir}'],
  184. cwd=self.vcpkgDir,
  185. )
  186. def copyBuildOutputTo(self, packageDir: pathlib.Path, extraFiles: dict, subdir:pathlib.Path=None):
  187. destdir = packageDir / self.packageName
  188. if subdir is not None:
  189. destdir /= subdir
  190. if destdir.exists():
  191. shutil.rmtree(destdir)
  192. shutil.copytree(
  193. src=self.vcpkgDir / 'packages' / f'{self.portName}_{self.triplet}',
  194. dst=destdir,
  195. symlinks=True,
  196. )
  197. for (src, dst) in extraFiles.items():
  198. try:
  199. shutil.copy2(src, dst)
  200. except IOError as e:
  201. # ENOENT(2): file does not exist, raised also on missing dest parent dir
  202. if e.errno != errno.ENOENT:
  203. raise
  204. # try creating parent directories
  205. Path(dst).parent.mkdir(parents=True, exist_ok=True)
  206. shutil.copy2(src, dst)
  207. def writePackageInfoFile(self, packageDir: pathlib.Path, settings: dict):
  208. with (packageDir / 'PackageInfo.json').open('w') as fh:
  209. json.dump(settings, fh, indent=4)
  210. def writeCMakeFindFile(self, packageDir: pathlib.Path, template, templateEnv:dict, overwrite_find_file:str or None):
  211. cmakeFindFile = packageDir / f'Find{overwrite_find_file or self.packageName}.cmake'
  212. cmakeFindFile.write_text(string.Template(template).substitute(templateEnv))