get_and_build_mcpp.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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. import argparse
  10. import logging
  11. import os
  12. import pathlib
  13. import platform
  14. import requests
  15. import shutil
  16. import string
  17. import subprocess
  18. import tarfile
  19. import time
  20. SCRIPT_PATH = pathlib.Path(__file__).parent
  21. PATCH_FILE = SCRIPT_PATH / "mcpp_2.7.2_az.patch"
  22. SOURCE_NAME = "mcpp-2.7.2"
  23. SOURCE_TAR_FILE = f"{SOURCE_NAME}.tar.gz"
  24. SOURCEFORGE_URL = "https://sourceforge.net/projects/mcpp/files/mcpp/V.2.7.2"
  25. SOURCEFORGE_DOWNLOAD_URL = f"{SOURCEFORGE_URL}/{SOURCE_TAR_FILE}/download"
  26. PLATFORM_LINUX = 'linux'
  27. PLATFORM_LINUX_ARM64 = 'linux-aarch64'
  28. PLATFORM_MAC = 'mac'
  29. PLATFORM_WINDOWS = 'windows'
  30. if platform.system() == 'Linux':
  31. if platform.processor() == 'aarch64':
  32. platform_name = PLATFORM_LINUX_ARM64
  33. else:
  34. platform_name = PLATFORM_LINUX
  35. shared_lib_name = 'libmcpp.so'
  36. static_lib_name = 'libmcpp.a'
  37. elif platform.system() == 'Darwin':
  38. platform_name = PLATFORM_MAC
  39. shared_lib_name = 'libmcpp.dylib'
  40. static_lib_name = 'libmcpp.a'
  41. elif platform.system() == 'Windows':
  42. platform_name = PLATFORM_WINDOWS
  43. static_lib_name = 'mcpp0.lib'
  44. shared_lib_name = 'mcpp0.dll'
  45. # Note: If we are running from windows, this script must be run from a visual studio command prompt
  46. # We will check the system environment to make sure that 'INCLUDE', 'LIB', 'LIBPATH' are set
  47. for vs_env_key in ('INCLUDE', 'LIB', 'LIBPATH'):
  48. if os.environ.get(vs_env_key) is None:
  49. print("This script must be run from a visual studio command prompt, or the visual studio command line"
  50. " environments must be set")
  51. exit(1)
  52. # Check it's running under x64 build environment.
  53. vs_target_arch = os.environ.get('VSCMD_ARG_TGT_ARCH')
  54. if vs_target_arch is None:
  55. print("Couldn't read the environment variable 'VSCMD_ARG_TGT_ARCH'. This script must be run from a x64 visual studio command prompt, or the visual studio command line"
  56. " environments must be set")
  57. exit(1)
  58. if vs_target_arch != 'x64':
  59. print("This script must be run from a x64 visual studio command prompt, or the visual studio command line"
  60. " environments must be set")
  61. exit(1)
  62. else:
  63. assert False, "Invalid platform"
  64. assert platform_name in (PLATFORM_LINUX, PLATFORM_LINUX_ARM64, PLATFORM_MAC, PLATFORM_WINDOWS), f"Invalid platform_name {platform_name}"
  65. TARGET_3PP_PACKAGE_FOLDER = SCRIPT_PATH.parent / f'mcpp-{platform_name}'
  66. MCPP_DETAIL = f"""
  67. The MCPP package will be patched and built from the following sources
  68. Sourceforge URL : {SOURCEFORGE_URL}
  69. Source name : {SOURCE_NAME}
  70. Patch File : {PATCH_FILE}
  71. Target Pre-package Platform target : {TARGET_3PP_PACKAGE_FOLDER}
  72. Example command:
  73. python get_and_build_linux.sh mcpp-2.7.2_az.1-rev1-{platform_name}
  74. """
  75. def execute_cmd(cmd, cwd=None, shell=False, suppress_std_err=False, env=None):
  76. logging.debug(f"[DEBUG] Calling {subprocess.list2cmdline(cmd)}")
  77. if shell:
  78. cmd = subprocess.list2cmdline(cmd)
  79. return subprocess.call(cmd,
  80. shell=shell,
  81. cwd=cwd,
  82. env=env,
  83. stderr=subprocess.DEVNULL if suppress_std_err else subprocess.STDOUT)
  84. def prepare_temp_folder(temp_folder):
  85. if temp_folder.exists():
  86. shutil.rmtree(str(temp_folder.resolve()), ignore_errors=True)
  87. os.makedirs(temp_folder, exist_ok=True)
  88. return True
  89. def download_from_source_forge(source_forge_download_url, temp_folder):
  90. try:
  91. request = requests.get(source_forge_download_url, allow_redirects=True)
  92. target_file = temp_folder / SOURCE_TAR_FILE
  93. if target_file.is_file():
  94. target_file.unlink()
  95. with open(str(target_file.resolve()), 'wb') as request_stream:
  96. bytes_written = request_stream.write(request.content)
  97. logging.debug(f'[DEBUG] {SOURCE_TAR_FILE} downloaded ({bytes_written} bytes)')
  98. return True
  99. except Exception as e:
  100. logging.fatal(f'[FATAL] Error downloading from {source_forge_download_url} : {e}')
  101. return False
  102. def extract_tarfile(temp_folder):
  103. try:
  104. target_file = temp_folder / SOURCE_TAR_FILE
  105. if not target_file.is_file():
  106. logging.error(f'[ERROR] Missing expected tar file {target_file}')
  107. tar = tarfile.open(str(target_file), 'r:gz')
  108. tar.extractall(path=str(temp_folder.resolve()))
  109. logging.debug(f'[DEBUG] {SOURCE_TAR_FILE} extracted to ({str(temp_folder.resolve())})')
  110. return True
  111. except Exception as e:
  112. logging.fatal(f'[FATAL] extracting tar file {target_file} : {e}')
  113. return False
  114. def init_git_repo(temp_folder):
  115. """
  116. Runs 'git init' on temp_folder.
  117. This is useful to 'git apply' won't fail silently when executing apply_patch(...)
  118. REMARK:
  119. 1- It is very important to set the local git project
  120. to not autoconvert LF to CRLF because it causes the patching
  121. to fail as 'git apply' is very picky about that.
  122. 2- You may notice that there's a .gitattributes file that makes sure
  123. the patch file remains with LF when fetched from the repo.
  124. 3- It was also found that 'git apply' also failed if the patch had CRLF
  125. AND the local git repo also had CRLF.
  126. """
  127. pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
  128. git_cmds = [
  129. ['git', 'init'],
  130. ['git', 'config', '--local', 'core.eol', 'lf'],
  131. ['git', 'config', '--local', 'core.autocrlf', 'false'],
  132. ['git', 'add', '.'],
  133. ['git', 'commit', '--no-verify', '-m', 'Temporary Message'],
  134. ]
  135. try:
  136. # Check for git --version to make sure it's installed.
  137. result = execute_cmd(['git', '--version'], shell=True, suppress_std_err=True)
  138. if result != 0:
  139. raise Exception("'git' command was not found")
  140. for git_cmd in git_cmds:
  141. result = execute_cmd(git_cmd, shell=True, cwd=pristine_source_path)
  142. if result != 0:
  143. cmd_string = " ".join(git_cmd)
  144. raise Exception(f"The command '{cmd_string}' failed to execute")
  145. return True
  146. except Exception as e:
  147. logging.fatal(f'[FATAL] Error initializing git repo : {e}')
  148. return False
  149. def apply_patch(temp_folder, patch_file):
  150. pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
  151. # Git apply for some reason fails on windows, but works on other platforms. We will first try 'git', and if that
  152. # fails, we will try 'patch'
  153. apply_patch_cmds = [
  154. ('git', ['apply', str(patch_file.resolve())]),
  155. ('patch', ['--strip=1', f'--input={str(patch_file.resolve())}'])
  156. ]
  157. try:
  158. patched = False
  159. for apply_patch_cmd in apply_patch_cmds:
  160. patch_cmd = apply_patch_cmd[0]
  161. logging.info(f"Attempt to patch with {patch_cmd}")
  162. result = execute_cmd([patch_cmd, '--version'], shell=True, suppress_std_err=True)
  163. if result != 0:
  164. logging.debug(f"[DEBUG] Unable to locate cmd {patch_cmd} for patching.")
  165. continue
  166. patch_full_cmd = [patch_cmd]
  167. patch_full_cmd.extend(apply_patch_cmd[1])
  168. result = execute_cmd(patch_full_cmd, shell=True, cwd=pristine_source_path)
  169. if result != 0:
  170. logging.debug(f"[DEBUG] cmd {patch_cmd} failed for patching.")
  171. continue
  172. patched = True
  173. break
  174. if not patched:
  175. logging.error(f"[ERROR] Unable to patch. Make sure to 'patch' or 'git' is installed.")
  176. return patched
  177. except Exception as e:
  178. logging.fatal(f'[FATAL] Error applying patch file {patch_file} : {e}')
  179. return False
  180. def configure_build(temp_folder):
  181. try:
  182. pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
  183. if platform_name == PLATFORM_WINDOWS:
  184. # Windows does not have a configure, instead it will use a modified visualc.mak directly
  185. # Copy the modified visualc.mak file to the patched source directory for the subsequent build step
  186. src_visualc_mak = SCRIPT_PATH / 'visualc.mak'
  187. dst_visualc_mak = temp_folder / SOURCE_NAME / 'src' / 'visualc.mak'
  188. shutil.copyfile(str(src_visualc_mak.resolve()), str(dst_visualc_mak.resolve()))
  189. else:
  190. if platform_name == PLATFORM_MAC:
  191. # For mac, we need to disable the 'implicit-function-declaration' or else the build will fail
  192. env_copy = os.environ.copy()
  193. env_copy['CFLAGS'] = '-Wno-implicit-function-declaration'
  194. else:
  195. env_copy = None
  196. # Mac and Linux use the built in ./configure command
  197. if execute_cmd(['./configure',
  198. '--with-pic',
  199. '--enable-mcpplib'],
  200. cwd=pristine_source_path,
  201. suppress_std_err=True,
  202. env=env_copy) != 0:
  203. logging.fatal(f'[ERROR] Error configuring build.')
  204. return False
  205. return True
  206. except Exception as e:
  207. logging.fatal(f'[FATAL] Error configuring build : {e}')
  208. return False
  209. def build_from_source(temp_folder):
  210. try:
  211. if platform_name == PLATFORM_WINDOWS:
  212. # Windows will use a precreated visualc.mak file instead of configure/make.
  213. source_working_path = str((temp_folder / SOURCE_NAME / 'src').resolve())
  214. build_cmds = [
  215. ['nmake', '/f', 'visualc.mak', 'COMPILER=MSC'],
  216. ['nmake', '/f', 'visualc.mak', 'COMPILER=MSC', 'MCPP_LIB=1', 'mcpplib']
  217. ]
  218. for build_cmd in build_cmds:
  219. if execute_cmd(build_cmd, cwd=source_working_path) != 0:
  220. logging.fatal(f'[ERROR] Error building from source.')
  221. return False
  222. else:
  223. # Mac/Linux will use 'make' to build
  224. pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
  225. result = execute_cmd(['make'],
  226. cwd=pristine_source_path,
  227. suppress_std_err=True)
  228. if result != 0:
  229. logging.fatal(f'[ERROR] Error building from source.')
  230. return False
  231. return True
  232. except Exception as e:
  233. logging.fatal(f'[FATAL] Error building from source : {e}')
  234. return False
  235. def copy_build_artifacts(temp_folder):
  236. # Copying LICENSE, headers and libs
  237. source_path = temp_folder / SOURCE_NAME
  238. target_mcpp_root = TARGET_3PP_PACKAGE_FOLDER / 'mcpp'
  239. file_copy_tuples = [
  240. (source_path / 'LICENSE', target_mcpp_root),
  241. (source_path / 'src' / 'mcpp_lib.h', target_mcpp_root / 'include'),
  242. (source_path / 'src' / 'mcpp_out.h', target_mcpp_root / 'include')
  243. ]
  244. if platform_name in ('linux', 'linux-aarch64'):
  245. file_copy_tuples.extend([
  246. (source_path / 'src' / '.libs' / 'libmcpp.a', target_mcpp_root / 'lib'),
  247. (source_path / 'src' / '.libs' / 'libmcpp.so.0.3.0', target_mcpp_root / 'lib'),
  248. (source_path / 'src' / '.libs' / 'mcpp', target_mcpp_root / 'lib')
  249. ])
  250. elif platform_name == 'mac':
  251. file_copy_tuples.extend([
  252. (source_path / 'src' / '.libs' / 'libmcpp.a', target_mcpp_root / 'lib'),
  253. (source_path / 'src' / '.libs' / 'libmcpp.0.3.0.dylib', target_mcpp_root / 'lib'),
  254. (source_path / 'src' / '.libs' / 'mcpp', target_mcpp_root / 'lib')
  255. ])
  256. elif platform_name == 'windows':
  257. file_copy_tuples.extend([
  258. (source_path / 'src' / 'mcpp0.dll', target_mcpp_root / 'lib'),
  259. (source_path / 'src' / 'mcpp0.lib', target_mcpp_root / 'lib'),
  260. (source_path / 'src' / 'mcpp.exe', target_mcpp_root / 'lib')
  261. ])
  262. for file_copy_tuple in file_copy_tuples:
  263. src = file_copy_tuple[0]
  264. dst = file_copy_tuple[1]
  265. if not src.is_file():
  266. logging.error(f'[ERROR] Missing source file {str(src)}')
  267. return False
  268. if not dst.is_dir():
  269. os.makedirs(str(dst.resolve()))
  270. shutil.copy2(str(src), str(dst))
  271. dst_lib_folder = target_mcpp_root / 'lib'
  272. if platform_name in ('linux', 'linux-aarch64'):
  273. base_shared_lib_name = 'libmcpp.so.0.3.0'
  274. symlinks = ['libmcpp.so.0', 'libmcpp.so']
  275. elif platform_name == 'mac':
  276. base_shared_lib_name = 'libmcpp.0.3.0.dylib'
  277. symlinks = ['libmcpp.0.dylib', 'libmcpp.dylib']
  278. else:
  279. base_shared_lib_name = None
  280. symlinks = None
  281. if base_shared_lib_name and symlinks:
  282. for symlink in symlinks:
  283. execute_cmd(['ln', '-s', base_shared_lib_name, symlink], cwd=str(dst_lib_folder))
  284. return True
  285. def create_3PP_package(temp_folder, package_label):
  286. if TARGET_3PP_PACKAGE_FOLDER.is_dir():
  287. shutil.rmtree(str(TARGET_3PP_PACKAGE_FOLDER.resolve()), ignore_errors=True)
  288. os.makedirs(str(TARGET_3PP_PACKAGE_FOLDER.resolve()), exist_ok=True)
  289. # Generate the find cmake file from the template file
  290. find_cmake_template_file = SCRIPT_PATH / f'Findmcpp.cmake.template'
  291. assert find_cmake_template_file.is_file(), f"Missing template file {find_cmake_template_file}"
  292. find_cmake_template_file_content = find_cmake_template_file.read_text("UTF-8", "ignore")
  293. template_env = {
  294. "MCPP_SHARED_LIB": shared_lib_name,
  295. "MCPP_STATIC_LIB": static_lib_name
  296. }
  297. find_cmake_content = string.Template(find_cmake_template_file_content).substitute(template_env)
  298. dst_find_cmake = TARGET_3PP_PACKAGE_FOLDER / 'Findmcpp.cmake'
  299. dst_find_cmake.write_text(find_cmake_content)
  300. # Generate the PackageInfo
  301. package_info_content = f'''
  302. {{
  303. "PackageName" : "{package_label}-{platform_name}",
  304. "URL" : "{SOURCEFORGE_URL}",
  305. "License" : "custom",
  306. "LicenseFile" : "mcpp/LICENSE"
  307. }}
  308. '''
  309. package_info_target = TARGET_3PP_PACKAGE_FOLDER / 'PackageInfo.json'
  310. logging.debug(f'[DEBUG] Generating {package_info_target}')
  311. package_info_target.write_text(package_info_content)
  312. return copy_build_artifacts(temp_folder)
  313. def main():
  314. parser = argparse.ArgumentParser(description="Script to build the O3DE complaint 3rd Party Package version of the mcpp open source project.",
  315. formatter_class=argparse.RawDescriptionHelpFormatter,
  316. epilog=MCPP_DETAIL)
  317. parser.add_argument(
  318. 'package_label',
  319. help="The package name and revision"
  320. )
  321. parser.add_argument(
  322. '--debug',
  323. help="Enable debug messages",
  324. action="store_true"
  325. )
  326. args = parser.parse_args()
  327. logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO)
  328. logging.info("Preparing temp working folder")
  329. temp_folder = SCRIPT_PATH / 'temp'
  330. if not prepare_temp_folder(temp_folder):
  331. return False
  332. logging.info("Downloading from sourceforge")
  333. if not download_from_source_forge(SOURCEFORGE_DOWNLOAD_URL, temp_folder):
  334. return False
  335. logging.info("Extracting source tarball")
  336. if not extract_tarfile(temp_folder):
  337. return False
  338. logging.info("Initializing temporary git repo")
  339. if not init_git_repo(temp_folder):
  340. return False
  341. logging.info("Apply Patch File")
  342. if not apply_patch(temp_folder, PATCH_FILE):
  343. return False
  344. logging.info("Configuring Build")
  345. if not configure_build(temp_folder):
  346. return False
  347. logging.info("Building from source")
  348. if not build_from_source(temp_folder):
  349. return False
  350. logging.info("Creating 3PP Target")
  351. if not create_3PP_package(temp_folder, args.package_label):
  352. return False
  353. # If successful, delete the temp folder
  354. if temp_folder.exists():
  355. shutil.rmtree(str(temp_folder.resolve()), ignore_errors=True)
  356. logging.info("MCPP Package complete")
  357. return True
  358. if __name__ == '__main__':
  359. start = time.time()
  360. result = main()
  361. elapsed = time.time() - start
  362. hour = int(elapsed // 3600)
  363. minute = int((elapsed - 3600*hour) // 60)
  364. seconds = int((elapsed - 3600*hour - 60*minute))
  365. logging.info(f'Total time {hour}:{minute:02d}:{seconds:02d}')
  366. if result:
  367. exit(0)
  368. else:
  369. exit(1)