123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- #!/usr/bin/env python3
- #
- # Copyright (c) Contributors to the Open 3D Engine Project.
- # For complete copyright and license terms please see the LICENSE at the root of this distribution.
- #
- # SPDX-License-Identifier: Apache-2.0 OR MIT
- #
- #
- import argparse
- import logging
- import os
- import pathlib
- import platform
- import requests
- import shutil
- import string
- import subprocess
- import tarfile
- import time
- SCRIPT_PATH = pathlib.Path(__file__).parent
- PATCH_FILE = SCRIPT_PATH / "mcpp_2.7.2_az.patch"
- SOURCE_NAME = "mcpp-2.7.2"
- SOURCE_TAR_FILE = f"{SOURCE_NAME}.tar.gz"
- SOURCEFORGE_URL = "https://sourceforge.net/projects/mcpp/files/mcpp/V.2.7.2"
- SOURCEFORGE_DOWNLOAD_URL = f"{SOURCEFORGE_URL}/{SOURCE_TAR_FILE}/download"
- PLATFORM_LINUX = 'linux'
- PLATFORM_LINUX_ARM64 = 'linux-aarch64'
- PLATFORM_MAC = 'mac'
- PLATFORM_WINDOWS = 'windows'
- if platform.system() == 'Linux':
- if platform.processor() == 'aarch64':
- platform_name = PLATFORM_LINUX_ARM64
- else:
- platform_name = PLATFORM_LINUX
- shared_lib_name = 'libmcpp.so'
- static_lib_name = 'libmcpp.a'
- elif platform.system() == 'Darwin':
- platform_name = PLATFORM_MAC
- shared_lib_name = 'libmcpp.dylib'
- static_lib_name = 'libmcpp.a'
- elif platform.system() == 'Windows':
- platform_name = PLATFORM_WINDOWS
- static_lib_name = 'mcpp0.lib'
- shared_lib_name = 'mcpp0.dll'
- # Note: If we are running from windows, this script must be run from a visual studio command prompt
- # We will check the system environment to make sure that 'INCLUDE', 'LIB', 'LIBPATH' are set
- for vs_env_key in ('INCLUDE', 'LIB', 'LIBPATH'):
- if os.environ.get(vs_env_key) is None:
- print("This script must be run from a visual studio command prompt, or the visual studio command line"
- " environments must be set")
- exit(1)
- # Check it's running under x64 build environment.
- vs_target_arch = os.environ.get('VSCMD_ARG_TGT_ARCH')
- if vs_target_arch is None:
- 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"
- " environments must be set")
- exit(1)
- if vs_target_arch != 'x64':
- print("This script must be run from a x64 visual studio command prompt, or the visual studio command line"
- " environments must be set")
- exit(1)
- else:
- assert False, "Invalid platform"
- assert platform_name in (PLATFORM_LINUX, PLATFORM_LINUX_ARM64, PLATFORM_MAC, PLATFORM_WINDOWS), f"Invalid platform_name {platform_name}"
- TARGET_3PP_PACKAGE_FOLDER = SCRIPT_PATH.parent / f'mcpp-{platform_name}'
- MCPP_DETAIL = f"""
- The MCPP package will be patched and built from the following sources
- Sourceforge URL : {SOURCEFORGE_URL}
- Source name : {SOURCE_NAME}
- Patch File : {PATCH_FILE}
- Target Pre-package Platform target : {TARGET_3PP_PACKAGE_FOLDER}
- Example command:
- python get_and_build_linux.sh mcpp-2.7.2_az.1-rev1-{platform_name}
- """
- def execute_cmd(cmd, cwd=None, shell=False, suppress_std_err=False, env=None):
- logging.debug(f"[DEBUG] Calling {subprocess.list2cmdline(cmd)}")
- if shell:
- cmd = subprocess.list2cmdline(cmd)
- return subprocess.call(cmd,
- shell=shell,
- cwd=cwd,
- env=env,
- stderr=subprocess.DEVNULL if suppress_std_err else subprocess.STDOUT)
- def prepare_temp_folder(temp_folder):
- if temp_folder.exists():
- shutil.rmtree(str(temp_folder.resolve()), ignore_errors=True)
- os.makedirs(temp_folder, exist_ok=True)
- return True
- def download_from_source_forge(source_forge_download_url, temp_folder):
- try:
- request = requests.get(source_forge_download_url, allow_redirects=True)
- target_file = temp_folder / SOURCE_TAR_FILE
- if target_file.is_file():
- target_file.unlink()
- with open(str(target_file.resolve()), 'wb') as request_stream:
- bytes_written = request_stream.write(request.content)
- logging.debug(f'[DEBUG] {SOURCE_TAR_FILE} downloaded ({bytes_written} bytes)')
- return True
- except Exception as e:
- logging.fatal(f'[FATAL] Error downloading from {source_forge_download_url} : {e}')
- return False
- def extract_tarfile(temp_folder):
- try:
- target_file = temp_folder / SOURCE_TAR_FILE
- if not target_file.is_file():
- logging.error(f'[ERROR] Missing expected tar file {target_file}')
- tar = tarfile.open(str(target_file), 'r:gz')
- tar.extractall(path=str(temp_folder.resolve()))
- logging.debug(f'[DEBUG] {SOURCE_TAR_FILE} extracted to ({str(temp_folder.resolve())})')
- return True
- except Exception as e:
- logging.fatal(f'[FATAL] extracting tar file {target_file} : {e}')
- return False
- def init_git_repo(temp_folder):
- """
- Runs 'git init' on temp_folder.
- This is useful to 'git apply' won't fail silently when executing apply_patch(...)
- REMARK:
- 1- It is very important to set the local git project
- to not autoconvert LF to CRLF because it causes the patching
- to fail as 'git apply' is very picky about that.
- 2- You may notice that there's a .gitattributes file that makes sure
- the patch file remains with LF when fetched from the repo.
- 3- It was also found that 'git apply' also failed if the patch had CRLF
- AND the local git repo also had CRLF.
- """
- pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
- git_cmds = [
- ['git', 'init'],
- ['git', 'config', '--local', 'core.eol', 'lf'],
- ['git', 'config', '--local', 'core.autocrlf', 'false'],
- ['git', 'add', '.'],
- ['git', 'commit', '--no-verify', '-m', 'Temporary Message'],
- ]
- try:
- # Check for git --version to make sure it's installed.
- result = execute_cmd(['git', '--version'], shell=True, suppress_std_err=True)
- if result != 0:
- raise Exception("'git' command was not found")
- for git_cmd in git_cmds:
- result = execute_cmd(git_cmd, shell=True, cwd=pristine_source_path)
- if result != 0:
- cmd_string = " ".join(git_cmd)
- raise Exception(f"The command '{cmd_string}' failed to execute")
- return True
- except Exception as e:
- logging.fatal(f'[FATAL] Error initializing git repo : {e}')
- return False
- def apply_patch(temp_folder, patch_file):
- pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
- # Git apply for some reason fails on windows, but works on other platforms. We will first try 'git', and if that
- # fails, we will try 'patch'
- apply_patch_cmds = [
- ('git', ['apply', str(patch_file.resolve())]),
- ('patch', ['--strip=1', f'--input={str(patch_file.resolve())}'])
- ]
- try:
- patched = False
- for apply_patch_cmd in apply_patch_cmds:
- patch_cmd = apply_patch_cmd[0]
- logging.info(f"Attempt to patch with {patch_cmd}")
- result = execute_cmd([patch_cmd, '--version'], shell=True, suppress_std_err=True)
- if result != 0:
- logging.debug(f"[DEBUG] Unable to locate cmd {patch_cmd} for patching.")
- continue
- patch_full_cmd = [patch_cmd]
- patch_full_cmd.extend(apply_patch_cmd[1])
- result = execute_cmd(patch_full_cmd, shell=True, cwd=pristine_source_path)
- if result != 0:
- logging.debug(f"[DEBUG] cmd {patch_cmd} failed for patching.")
- continue
- patched = True
- break
- if not patched:
- logging.error(f"[ERROR] Unable to patch. Make sure to 'patch' or 'git' is installed.")
- return patched
- except Exception as e:
- logging.fatal(f'[FATAL] Error applying patch file {patch_file} : {e}')
- return False
- def configure_build(temp_folder):
- try:
- pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
- if platform_name == PLATFORM_WINDOWS:
- # Windows does not have a configure, instead it will use a modified visualc.mak directly
- # Copy the modified visualc.mak file to the patched source directory for the subsequent build step
- src_visualc_mak = SCRIPT_PATH / 'visualc.mak'
- dst_visualc_mak = temp_folder / SOURCE_NAME / 'src' / 'visualc.mak'
- shutil.copyfile(str(src_visualc_mak.resolve()), str(dst_visualc_mak.resolve()))
- else:
- if platform_name == PLATFORM_MAC:
- # For mac, we need to disable the 'implicit-function-declaration' or else the build will fail
- env_copy = os.environ.copy()
- env_copy['CFLAGS'] = '-Wno-implicit-function-declaration'
- else:
- env_copy = None
- # Mac and Linux use the built in ./configure command
- if execute_cmd(['./configure',
- '--with-pic',
- '--enable-mcpplib'],
- cwd=pristine_source_path,
- suppress_std_err=True,
- env=env_copy) != 0:
- logging.fatal(f'[ERROR] Error configuring build.')
- return False
- return True
- except Exception as e:
- logging.fatal(f'[FATAL] Error configuring build : {e}')
- return False
- def build_from_source(temp_folder):
- try:
- if platform_name == PLATFORM_WINDOWS:
- # Windows will use a precreated visualc.mak file instead of configure/make.
- source_working_path = str((temp_folder / SOURCE_NAME / 'src').resolve())
- build_cmds = [
- ['nmake', '/f', 'visualc.mak', 'COMPILER=MSC'],
- ['nmake', '/f', 'visualc.mak', 'COMPILER=MSC', 'MCPP_LIB=1', 'mcpplib']
- ]
- for build_cmd in build_cmds:
- if execute_cmd(build_cmd, cwd=source_working_path) != 0:
- logging.fatal(f'[ERROR] Error building from source.')
- return False
- else:
- # Mac/Linux will use 'make' to build
- pristine_source_path = str((temp_folder / SOURCE_NAME).resolve())
- result = execute_cmd(['make'],
- cwd=pristine_source_path,
- suppress_std_err=True)
- if result != 0:
- logging.fatal(f'[ERROR] Error building from source.')
- return False
- return True
- except Exception as e:
- logging.fatal(f'[FATAL] Error building from source : {e}')
- return False
- def copy_build_artifacts(temp_folder):
- # Copying LICENSE, headers and libs
- source_path = temp_folder / SOURCE_NAME
- target_mcpp_root = TARGET_3PP_PACKAGE_FOLDER / 'mcpp'
- file_copy_tuples = [
- (source_path / 'LICENSE', target_mcpp_root),
- (source_path / 'src' / 'mcpp_lib.h', target_mcpp_root / 'include'),
- (source_path / 'src' / 'mcpp_out.h', target_mcpp_root / 'include')
- ]
- if platform_name in ('linux', 'linux-aarch64'):
- file_copy_tuples.extend([
- (source_path / 'src' / '.libs' / 'libmcpp.a', target_mcpp_root / 'lib'),
- (source_path / 'src' / '.libs' / 'libmcpp.so.0.3.0', target_mcpp_root / 'lib'),
- (source_path / 'src' / '.libs' / 'mcpp', target_mcpp_root / 'lib')
- ])
- elif platform_name == 'mac':
- file_copy_tuples.extend([
- (source_path / 'src' / '.libs' / 'libmcpp.a', target_mcpp_root / 'lib'),
- (source_path / 'src' / '.libs' / 'libmcpp.0.3.0.dylib', target_mcpp_root / 'lib'),
- (source_path / 'src' / '.libs' / 'mcpp', target_mcpp_root / 'lib')
- ])
- elif platform_name == 'windows':
- file_copy_tuples.extend([
- (source_path / 'src' / 'mcpp0.dll', target_mcpp_root / 'lib'),
- (source_path / 'src' / 'mcpp0.lib', target_mcpp_root / 'lib'),
- (source_path / 'src' / 'mcpp.exe', target_mcpp_root / 'lib')
- ])
- for file_copy_tuple in file_copy_tuples:
- src = file_copy_tuple[0]
- dst = file_copy_tuple[1]
- if not src.is_file():
- logging.error(f'[ERROR] Missing source file {str(src)}')
- return False
- if not dst.is_dir():
- os.makedirs(str(dst.resolve()))
- shutil.copy2(str(src), str(dst))
- dst_lib_folder = target_mcpp_root / 'lib'
- if platform_name in ('linux', 'linux-aarch64'):
- base_shared_lib_name = 'libmcpp.so.0.3.0'
- symlinks = ['libmcpp.so.0', 'libmcpp.so']
- elif platform_name == 'mac':
- base_shared_lib_name = 'libmcpp.0.3.0.dylib'
- symlinks = ['libmcpp.0.dylib', 'libmcpp.dylib']
- else:
- base_shared_lib_name = None
- symlinks = None
- if base_shared_lib_name and symlinks:
- for symlink in symlinks:
- execute_cmd(['ln', '-s', base_shared_lib_name, symlink], cwd=str(dst_lib_folder))
- return True
- def create_3PP_package(temp_folder, package_label):
- if TARGET_3PP_PACKAGE_FOLDER.is_dir():
- shutil.rmtree(str(TARGET_3PP_PACKAGE_FOLDER.resolve()), ignore_errors=True)
- os.makedirs(str(TARGET_3PP_PACKAGE_FOLDER.resolve()), exist_ok=True)
- # Generate the find cmake file from the template file
- find_cmake_template_file = SCRIPT_PATH / f'Findmcpp.cmake.template'
- assert find_cmake_template_file.is_file(), f"Missing template file {find_cmake_template_file}"
- find_cmake_template_file_content = find_cmake_template_file.read_text("UTF-8", "ignore")
- template_env = {
- "MCPP_SHARED_LIB": shared_lib_name,
- "MCPP_STATIC_LIB": static_lib_name
- }
- find_cmake_content = string.Template(find_cmake_template_file_content).substitute(template_env)
- dst_find_cmake = TARGET_3PP_PACKAGE_FOLDER / 'Findmcpp.cmake'
- dst_find_cmake.write_text(find_cmake_content)
- # Generate the PackageInfo
- package_info_content = f'''
- {{
- "PackageName" : "{package_label}-{platform_name}",
- "URL" : "{SOURCEFORGE_URL}",
- "License" : "custom",
- "LicenseFile" : "mcpp/LICENSE"
- }}
- '''
- package_info_target = TARGET_3PP_PACKAGE_FOLDER / 'PackageInfo.json'
- logging.debug(f'[DEBUG] Generating {package_info_target}')
- package_info_target.write_text(package_info_content)
- return copy_build_artifacts(temp_folder)
- def main():
- parser = argparse.ArgumentParser(description="Script to build the O3DE complaint 3rd Party Package version of the mcpp open source project.",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog=MCPP_DETAIL)
- parser.add_argument(
- 'package_label',
- help="The package name and revision"
- )
- parser.add_argument(
- '--debug',
- help="Enable debug messages",
- action="store_true"
- )
- args = parser.parse_args()
- logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO)
- logging.info("Preparing temp working folder")
- temp_folder = SCRIPT_PATH / 'temp'
- if not prepare_temp_folder(temp_folder):
- return False
- logging.info("Downloading from sourceforge")
- if not download_from_source_forge(SOURCEFORGE_DOWNLOAD_URL, temp_folder):
- return False
- logging.info("Extracting source tarball")
- if not extract_tarfile(temp_folder):
- return False
- logging.info("Initializing temporary git repo")
- if not init_git_repo(temp_folder):
- return False
- logging.info("Apply Patch File")
- if not apply_patch(temp_folder, PATCH_FILE):
- return False
- logging.info("Configuring Build")
- if not configure_build(temp_folder):
- return False
- logging.info("Building from source")
- if not build_from_source(temp_folder):
- return False
- logging.info("Creating 3PP Target")
- if not create_3PP_package(temp_folder, args.package_label):
- return False
- # If successful, delete the temp folder
- if temp_folder.exists():
- shutil.rmtree(str(temp_folder.resolve()), ignore_errors=True)
- logging.info("MCPP Package complete")
- return True
- if __name__ == '__main__':
- start = time.time()
- result = main()
- elapsed = time.time() - start
- hour = int(elapsed // 3600)
- minute = int((elapsed - 3600*hour) // 60)
- seconds = int((elapsed - 3600*hour - 60*minute))
- logging.info(f'Total time {hour}:{minute:02d}:{seconds:02d}')
- if result:
- exit(0)
- else:
- exit(1)
|