1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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')
|