summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Taylor <alex@vector35.com>2026-06-06 13:24:49 -0400
committerAlexander Taylor <alex@vector35.com>2026-06-06 14:03:58 -0400
commit61b068347672bf2dbea4ee31d7080d32a3b919fe (patch)
tree047824bf34cfb9475565f2ada3cd2bbf8536b4bd
parent6bc36d9f834900d8b9b15ecc6956aaa3d39bf153 (diff)
Split internal/external test build scripts.
Also make them a little more configurable.
-rw-r--r--scripts/test_build_extern.py105
-rw-r--r--scripts/test_build_external.py215
-rw-r--r--scripts/test_build_internal.py135
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)