diff options
| -rw-r--r-- | scripts/test_build_extern.py | 105 | ||||
| -rw-r--r-- | scripts/test_build_external.py | 215 | ||||
| -rw-r--r-- | scripts/test_build_internal.py | 135 |
3 files changed, 350 insertions, 105 deletions
diff --git a/scripts/test_build_extern.py b/scripts/test_build_extern.py deleted file mode 100644 index e2c2cd87..00000000 --- a/scripts/test_build_extern.py +++ /dev/null @@ -1,105 +0,0 @@ -import argparse -import glob -import os -from pathlib import Path -import platform -import shutil -import subprocess -import sys -import tempfile -from datetime import datetime - -import argparse - -parser = argparse.ArgumentParser( - description='Test building the API repo and plugins out-of-tree (for CI checking of end-user workflow)' -) -parser.add_argument('--headless', default=False, action='store_true', help='Only include headless plugins') -parser.add_argument('-j', '--parallel', default=4, help='Number of parallel jobs to tell cmake to run.') -args = parser.parse_args() - -try: - # Make sure we have clang! - subprocess.check_call(['clang', '-v']) -except subprocess.SubprocessError as e: - print("Clang not found! Please install clang and add it to PATH") - sys.exit(1) - -if not args.headless: - try: - # Also Qt - subprocess.check_call(['qmake', '--version']) - except subprocess.SubprocessError as e: - print("qmake not found! Please install Qt and add it to PATH") - sys.exit(1) - -api_base = Path(__file__).parent.parent.absolute() - -configure_args = [] -build_args = [] -configure_env = os.environ.copy() - -if platform.system() == "Windows": - configure_env['CXXFLAGS'] = f'/MP{args.parallel}' - configure_env['CFLAGS'] = f'/MP{args.parallel}' -else: - build_args.extend(['-j', str(args.parallel)]) - -if args.headless: - configure_args.extend(['-DHEADLESS=1']) - -# Copy api out of the source tree and build it externally -with tempfile.TemporaryDirectory() as tempdir: - temp_api_base = Path(tempdir) / 'binaryninjaapi' - print(f'Copy {api_base} => {temp_api_base}') - shutil.copytree(api_base, temp_api_base) - - # Clean up dirty repo - if (temp_api_base / 'build').exists(): - shutil.rmtree(temp_api_base / 'build') - - # Now try to build - try: - subprocess.check_call(['cmake', '-B', 'build'] + configure_args, cwd=temp_api_base, env=configure_env) - subprocess.check_call(['cmake', '--build', 'build'] + build_args, cwd=temp_api_base) - finally: - if (temp_api_base / 'build').exists(): - shutil.rmtree(temp_api_base / 'build') - - # Now try to build examples (in-tree) - try: - subprocess.check_call(['cmake', '-B', 'build', '-DBN_API_BUILD_EXAMPLES=1'] + configure_args, cwd=temp_api_base, - env=configure_env) - subprocess.check_call(['cmake', '--build', 'build'] + build_args, cwd=temp_api_base) - finally: - if (temp_api_base / 'build').exists(): - shutil.rmtree(temp_api_base / 'build') - - # Now try to build examples (out-of-tree) - for f in glob.glob(str(api_base / 'examples' / '**' / 'CMakeLists.txt'), recursive=True): - example_base = Path(f).parent - if example_base == api_base / 'examples': - continue - - # Check for headless - if args.headless: - with open(f, 'r') as cmake_file: - # Assume any ui plugin has 'binaryninjaui' in its contents - if 'binaryninjaui' in cmake_file.read(): - continue - - with tempfile.TemporaryDirectory() as tempexdir: - temp_example_base = Path(tempexdir) / example_base.name - print(f'Copy {example_base} => {temp_example_base} at {datetime.now()}') - shutil.copytree(example_base, temp_example_base) - - if (temp_example_base / 'build').exists(): - shutil.rmtree(temp_example_base / 'build') - - try: - subprocess.check_call(['cmake', '-B', 'build', f'-DBN_API_PATH={temp_api_base}'] + configure_args, - cwd=temp_example_base, env=configure_env) - subprocess.check_call(['cmake', '--build', 'build'] + build_args, cwd=temp_example_base) - finally: - if (temp_example_base / 'build').exists(): - shutil.rmtree(temp_example_base / 'build') diff --git a/scripts/test_build_external.py b/scripts/test_build_external.py new file mode 100644 index 00000000..e103c2b1 --- /dev/null +++ b/scripts/test_build_external.py @@ -0,0 +1,215 @@ +import argparse +import glob +import os +from pathlib import Path +import platform +import shutil +import subprocess +import sys +import tempfile +from datetime import datetime + +parser = argparse.ArgumentParser( + description='Test building the API repo and plugins out-of-tree (for CI checking of end-user workflow)' +) +parser.add_argument('--headless', default=False, action='store_true', help='Only include headless plugins') +parser.add_argument('-j', '--parallel', default=4, help='Number of parallel jobs to tell cmake to run.') +parser.add_argument('--bn-install-dir', default=os.environ.get('BN_INSTALL_DIR'), + help='Path to a Binary Ninja installation. Defaults to BN_INSTALL_DIR.') +parser.add_argument('--qt-install-dir', default=os.environ.get('QT_INSTALL_DIR'), + help='Path to a Qt installation. Defaults to QT_INSTALL_DIR.') +parser.add_argument('--qmake', default=os.environ.get('QMAKE'), help='Path to qmake. Defaults to QMAKE or PATH lookup.') +parser.add_argument('--cmake', default=os.environ.get('CMAKE'), help='Path to cmake. Defaults to CMAKE or PATH lookup.') +parser.add_argument('--cc', default=os.environ.get('CC'), help='C compiler to use. Defaults to CC or CMake platform default.') +parser.add_argument('--cxx', default=os.environ.get('CXX'), help='C++ compiler to use. Defaults to CXX or CMake platform default.') +parser.add_argument('--extra-project', action='append', default=[], + help='Additional out-of-tree CMake project directory to build. May be specified multiple times.') +args = parser.parse_args() + +api_base = Path(__file__).parent.parent.absolute() + + +def fail(message): + print(f'ERROR: {message}') + sys.exit(1) + + +def prepend_path(env, path): + env['PATH'] = f'{path}{os.pathsep}{env.get("PATH", "")}' + + +def find_executable(name, explicit_path=None, env=None): + if explicit_path: + path = Path(explicit_path).expanduser() + if path.is_file(): + return str(path) + fail(f'{name} was specified as {path}, but that file does not exist.') + + return shutil.which(name, path=(env or os.environ).get('PATH')) + + +def run_tool(tool, args_for_tool, env=None): + try: + result = subprocess.run([tool] + args_for_tool, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, env=env) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + output = e.stdout.strip() if e.stdout else '' + fail(f'{tool} failed with exit code {e.returncode}.\n{output}') + + +def print_tool(tool_name, tool_path, version_args, env=None): + print(f'{tool_name}: {tool_path}') + version = run_tool(tool_path, version_args, env=env) + print(version) + + +def configure_qt_env(env): + qt_dir = Path(args.qt_install_dir).expanduser().absolute() if args.qt_install_dir else None + if qt_dir: + if not qt_dir.exists(): + fail(f'Qt directory was specified as {qt_dir}, but it does not exist.') + qt_bin_dir = qt_dir / 'bin' + if not qt_bin_dir.exists(): + fail(f'Qt directory {qt_dir} does not contain a bin directory.') + prepend_path(env, qt_bin_dir) + env.setdefault('CMAKE_PREFIX_PATH', str(qt_dir / 'lib' / 'cmake')) + print(f'Qt directory: {qt_dir}') + print(f'CMAKE_PREFIX_PATH: {env["CMAKE_PREFIX_PATH"]}') + + if args.headless: + return None + + qmake = find_executable('qmake', args.qmake, env=env) + if not qmake: + fail('qmake not found. Install Qt and add its bin directory to PATH, set QT_INSTALL_DIR, or pass --qt-install-dir/--qmake.') + + print_tool('qmake', qmake, ['--version'], env=env) + qt_lib_path = run_tool(qmake, ['-query', 'QT_INSTALL_LIBS'], env=env) + qt_cmake_path = str(Path(qt_lib_path) / 'cmake') + env.setdefault('CMAKE_PREFIX_PATH', qt_cmake_path) + print(f'Qt libraries: {qt_lib_path}') + print(f'CMAKE_PREFIX_PATH: {env["CMAKE_PREFIX_PATH"]}') + return qmake + + +def check_binary_ninja_install(env): + bn_install_dir = Path(args.bn_install_dir).expanduser().absolute() if args.bn_install_dir else None + if not bn_install_dir: + fail('Binary Ninja install directory was not specified. Set BN_INSTALL_DIR or pass --bn-install-dir.') + if not bn_install_dir.exists(): + fail(f'Binary Ninja install directory {bn_install_dir} does not exist.') + + env['BN_INSTALL_DIR'] = str(bn_install_dir) + if platform.system() == 'Darwin': + core_candidates = [bn_install_dir / 'Contents' / 'MacOS' / 'libbinaryninjacore.dylib'] + ui_candidates = [bn_install_dir / 'Contents' / 'MacOS' / 'libbinaryninjaui.dylib'] + elif platform.system() == 'Windows': + core_candidates = [bn_install_dir / 'binaryninjacore.dll'] + ui_candidates = [bn_install_dir / 'binaryninjaui.dll'] + else: + core_candidates = [bn_install_dir / 'libbinaryninjacore.so.1', bn_install_dir / 'libbinaryninjacore.so'] + ui_candidates = [bn_install_dir / 'libbinaryninjaui.so.1', bn_install_dir / 'libbinaryninjaui.so'] + + core = next((p for p in core_candidates if p.exists()), None) + if not core: + fail(f'Binary Ninja Core was not found in {bn_install_dir}. Checked: {", ".join(str(p) for p in core_candidates)}') + print(f'Binary Ninja install: {bn_install_dir}') + print(f'Binary Ninja Core: {core}') + + ui = next((p for p in ui_candidates if p.exists()), None) + if args.headless: + print('Binary Ninja UI: skipped because --headless was specified') + elif ui: + print(f'Binary Ninja UI: {ui}') + else: + fail(f'Binary Ninja UI was not found in {bn_install_dir}. Checked: {", ".join(str(p) for p in ui_candidates)}') + + +def contains_ui_dependency(cmake_lists): + with open(cmake_lists, 'r', encoding='utf-8') as cmake_file: + contents = cmake_file.read() + return 'binaryninjaui' in contents or 'Qt6::' in contents or 'find_package(Qt6' in contents + + +def external_project_cmake_files(): + cmake_files = [] + + # The examples are the supported customer-facing out-of-tree CMake projects + for f in glob.glob(str(api_base / 'examples' / '**' / 'CMakeLists.txt'), recursive=True): + cmake_path = Path(f) + if cmake_path.parent == api_base / 'examples': + continue + cmake_files.append(cmake_path) + + for project in args.extra_project: + project_path = Path(project).expanduser() + if not project_path.is_absolute(): + project_path = api_base / project_path + cmake_path = project_path / 'CMakeLists.txt' + if not cmake_path.exists(): + fail(f'Extra project {project_path} does not contain a CMakeLists.txt file.') + cmake_files.append(cmake_path) + + return sorted(cmake_files) + + +configure_env = os.environ.copy() +configure_qt_env(configure_env) +check_binary_ninja_install(configure_env) + +if args.cc: + configure_env['CC'] = args.cc +if args.cxx: + configure_env['CXX'] = args.cxx + +cmake = find_executable('cmake', args.cmake, env=configure_env) +if not cmake: + fail('cmake not found. Install CMake and add it to PATH, set CMAKE, or pass --cmake.') +print_tool('cmake', cmake, ['--version'], env=configure_env) + +print(f'C compiler: {configure_env.get("CC", "CMake platform default")}') +print(f'C++ compiler: {configure_env.get("CXX", "CMake platform default")}') + +configure_args = [] +build_args = [] + +if platform.system() == "Windows": + configure_env['CXXFLAGS'] = f'/MP{args.parallel}' + configure_env['CFLAGS'] = f'/MP{args.parallel}' +else: + build_args.extend(['-j', str(args.parallel)]) + +if args.headless: + configure_args.extend(['-DHEADLESS=1']) + +# Copy api out of the source tree and build it externally +with tempfile.TemporaryDirectory() as tempdir: + temp_api_base = Path(tempdir) / 'binaryninjaapi' + print(f'Copy {api_base} => {temp_api_base}') + shutil.copytree(api_base, temp_api_base) + + # Now try to build bundled out-of-tree projects the way customers would. + for f in external_project_cmake_files(): + example_base = Path(f).parent + + # Check for headless + if args.headless and contains_ui_dependency(f): + print(f'Skip {example_base} because it requires UI/Qt and --headless was specified') + continue + + with tempfile.TemporaryDirectory() as tempexdir: + temp_example_base = Path(tempexdir) / example_base.name + print(f'Copy {example_base} => {temp_example_base} at {datetime.now()}') + shutil.copytree(example_base, temp_example_base) + + if (temp_example_base / 'build').exists(): + shutil.rmtree(temp_example_base / 'build') + + try: + subprocess.check_call([cmake, '-B', 'build', f'-DBN_API_PATH={temp_api_base}'] + configure_args, + cwd=temp_example_base, env=configure_env) + subprocess.check_call([cmake, '--build', 'build'] + build_args, cwd=temp_example_base) + finally: + if (temp_example_base / 'build').exists(): + shutil.rmtree(temp_example_base / 'build') diff --git a/scripts/test_build_internal.py b/scripts/test_build_internal.py new file mode 100644 index 00000000..b31f63d5 --- /dev/null +++ b/scripts/test_build_internal.py @@ -0,0 +1,135 @@ +import argparse +import os +from pathlib import Path +import platform +import shutil +import subprocess +import sys + + +parser = argparse.ArgumentParser( + description='Test building the API repository and examples in-tree' +) +parser.add_argument('--headless', default=False, action='store_true', help='Only include headless plugins') +parser.add_argument('-j', '--parallel', default=4, help='Number of parallel jobs to tell cmake to run.') +parser.add_argument('--bn-install-dir', default=os.environ.get('BN_INSTALL_DIR'), + help='Path to a Binary Ninja installation. Defaults to BN_INSTALL_DIR.') +parser.add_argument('--qt-install-dir', default=os.environ.get('QT_INSTALL_DIR'), + help='Path to a Qt installation. Defaults to QT_INSTALL_DIR.') +parser.add_argument('--qmake', default=os.environ.get('QMAKE'), help='Path to qmake. Defaults to QMAKE or PATH lookup.') +parser.add_argument('--cmake', default=os.environ.get('CMAKE'), help='Path to cmake. Defaults to CMAKE or PATH lookup.') +parser.add_argument('--cc', default=os.environ.get('CC'), help='C compiler to use. Defaults to CC or CMake platform default.') +parser.add_argument('--cxx', default=os.environ.get('CXX'), help='C++ compiler to use. Defaults to CXX or CMake platform default.') +args = parser.parse_args() + +api_base = Path(__file__).parent.parent.absolute() + + +def fail(message): + print(f'ERROR: {message}') + sys.exit(1) + + +def prepend_path(env, path): + env['PATH'] = f'{path}{os.pathsep}{env.get("PATH", "")}' + + +def find_executable(name, explicit_path=None, env=None): + if explicit_path: + path = Path(explicit_path).expanduser() + if path.is_file(): + return str(path) + fail(f'{name} was specified as {path}, but that file does not exist.') + + return shutil.which(name, path=(env or os.environ).get('PATH')) + + +def run_tool(tool, args_for_tool, env=None): + try: + result = subprocess.run([tool] + args_for_tool, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, env=env) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + output = e.stdout.strip() if e.stdout else '' + fail(f'{tool} failed with exit code {e.returncode}.\n{output}') + + +def print_tool(tool_name, tool_path, version_args, env=None): + print(f'{tool_name}: {tool_path}') + version = run_tool(tool_path, version_args, env=env) + print(version) + + +def configure_qt_env(env): + qt_dir = Path(args.qt_install_dir).expanduser().absolute() if args.qt_install_dir else None + if qt_dir: + if not qt_dir.exists(): + fail(f'Qt directory was specified as {qt_dir}, but it does not exist.') + qt_bin_dir = qt_dir / 'bin' + if not qt_bin_dir.exists(): + fail(f'Qt directory {qt_dir} does not contain a bin directory.') + prepend_path(env, qt_bin_dir) + env.setdefault('CMAKE_PREFIX_PATH', str(qt_dir / 'lib' / 'cmake')) + print(f'Qt directory: {qt_dir}') + print(f'CMAKE_PREFIX_PATH: {env["CMAKE_PREFIX_PATH"]}') + + if args.headless: + return + + qmake = find_executable('qmake', args.qmake, env=env) + if not qmake: + fail('qmake not found. Install Qt and add its bin directory to PATH, set QT_INSTALL_DIR, or pass --qt-install-dir/--qmake.') + + print_tool('qmake', qmake, ['--version'], env=env) + qt_lib_path = run_tool(qmake, ['-query', 'QT_INSTALL_LIBS'], env=env) + env.setdefault('CMAKE_PREFIX_PATH', str(Path(qt_lib_path) / 'cmake')) + print(f'Qt libraries: {qt_lib_path}') + print(f'CMAKE_PREFIX_PATH: {env["CMAKE_PREFIX_PATH"]}') + + +def configure_binary_ninja_env(env): + bn_install_dir = Path(args.bn_install_dir).expanduser().absolute() if args.bn_install_dir else None + if not bn_install_dir: + fail('Binary Ninja install directory was not specified. Set BN_INSTALL_DIR or pass --bn-install-dir.') + if not bn_install_dir.exists(): + fail(f'Binary Ninja install directory {bn_install_dir} does not exist.') + env['BN_INSTALL_DIR'] = str(bn_install_dir) + print(f'Binary Ninja install: {bn_install_dir}') + + +env = os.environ.copy() +configure_qt_env(env) +configure_binary_ninja_env(env) + +if args.cc: + env['CC'] = args.cc +if args.cxx: + env['CXX'] = args.cxx + +cmake = find_executable('cmake', args.cmake, env=env) +if not cmake: + fail('cmake not found. Install CMake and add it to PATH, set CMAKE, or pass --cmake.') +print_tool('cmake', cmake, ['--version'], env=env) + +print(f'C compiler: {env.get("CC", "CMake platform default")}') +print(f'C++ compiler: {env.get("CXX", "CMake platform default")}') + +configure_args = ['-DBN_API_BUILD_EXAMPLES=1'] +build_args = [] + +if platform.system() == 'Windows': + env['CXXFLAGS'] = f'/MP{args.parallel}' + env['CFLAGS'] = f'/MP{args.parallel}' +else: + build_args.extend(['-j', str(args.parallel)]) + +if args.headless: + configure_args.extend(['-DHEADLESS=1']) + +build_dir = api_base / 'build' +try: + subprocess.check_call([cmake, '-B', str(build_dir)] + configure_args, cwd=api_base, env=env) + subprocess.check_call([cmake, '--build', str(build_dir)] + build_args, cwd=api_base) +finally: + if build_dir.exists(): + shutil.rmtree(build_dir) |
