ci-windows.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or https://opensource.org/license/mit/.
5
6 import argparse
7 import os
8 import shlex
9 import subprocess
10 import sys
11 import time
12 from pathlib import Path
13
14 sys.path.append(str(Path(__file__).resolve().parent.parent / "test"))
15 from download_utils import download_script_assets
16
17
18 def run(cmd, **kwargs):
19 print("+ " + shlex.join(cmd), flush=True)
20 kwargs.setdefault("check", True)
21 try:
22 return subprocess.run(cmd, **kwargs)
23 except Exception as e:
24 sys.exit(str(e))
25
26
27 GENERATE_OPTIONS = {
28 "standard": [
29 "-DBUILD_BENCH=ON",
30 "-DBUILD_KERNEL_LIB=ON",
31 "-DBUILD_UTIL_CHAINSTATE=ON",
32 "-DCMAKE_COMPILE_WARNING_AS_ERROR=ON",
33 ],
34 "fuzz": [
35 "-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON",
36 "-DVCPKG_MANIFEST_FEATURES=wallet",
37 "-DBUILD_FOR_FUZZING=ON",
38 "-DCMAKE_COMPILE_WARNING_AS_ERROR=ON",
39 ],
40 }
41
42
43 def github_import_vs_env(_ci_type):
44 vswhere_path = Path(os.environ["ProgramFiles(x86)"]) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe"
45 installation_path = run(
46 [str(vswhere_path), "-latest", "-property", "installationPath"],
47 capture_output=True,
48 text=True,
49 ).stdout.strip()
50 vsdevcmd = Path(installation_path) / "Common7" / "Tools" / "vsdevcmd.bat"
51 comspec = os.environ["COMSPEC"]
52 output = run(
53 f'"{comspec}" /s /c ""{vsdevcmd}" -arch=x64 -no_logo && set"',
54 capture_output=True,
55 text=True,
56 ).stdout
57 github_env = os.environ["GITHUB_ENV"]
58 with open(github_env, "a") as env_file:
59 for line in output.splitlines():
60 if "=" not in line:
61 continue
62 name, value = line.split("=", 1)
63 env_file.write(f"{name}={value}\n")
64
65
66 def generate(ci_type):
67 command = [
68 "cmake",
69 "-B",
70 "build",
71 "-Werror=dev",
72 "--preset=vs2026",
73 # Using x64-windows-release for both host and target triplets
74 # to ensure vcpkg builds only release packages, thereby optimizing
75 # build time.
76 # See https://github.com/microsoft/vcpkg/issues/50927.
77 "-DVCPKG_HOST_TRIPLET=x64-windows-release",
78 "-DVCPKG_TARGET_TRIPLET=x64-windows-release",
79 ] + GENERATE_OPTIONS[ci_type]
80 if run(command, check=False).returncode != 0:
81 print("=== ⚠️ ===")
82 print("Generate failure! Network issue? Retry once ...")
83 time.sleep(12)
84 print("=== ⚠️ ===")
85 run(command)
86
87
88 def build(_ci_type):
89 command = [
90 "cmake",
91 "--build",
92 "build",
93 "--config",
94 "Release",
95 ]
96 if run(command + ["-j", str(os.process_cpu_count())], check=False).returncode != 0:
97 print("Build failure. Verbose build follows.")
98 run(command + ["-j1", "--verbose"])
99
100
101 def check_manifests(ci_type):
102 if ci_type != "standard":
103 print(f"Skipping manifest validation for '{ci_type}' ci type.")
104 return
105
106 release_dir = Path.cwd() / "build" / "bin" / "Release"
107 manifest_path = release_dir / "bitcoind.manifest"
108 cmd_bitcoind_manifest = [
109 "mt.exe",
110 "-nologo",
111 f"-inputresource:{release_dir / 'bitcoind.exe'}",
112 f"-out:{manifest_path}",
113 ]
114 run(cmd_bitcoind_manifest)
115 print(manifest_path.read_text())
116
117 skips = { # Skip as they currently do not have manifests
118 "fuzz.exe",
119 "bench_bitcoin.exe",
120 "test_bitcoin-qt.exe",
121 "bitcoin-chainstate.exe",
122 }
123 for entry in release_dir.iterdir():
124 if entry.suffix.lower() != ".exe":
125 continue
126 if entry.name in skips:
127 print(f"Skipping {entry.name} (no manifest present)")
128 continue
129 print(f"Checking {entry.name}")
130 cmd_check_manifest = [
131 "mt.exe",
132 "-nologo",
133 f"-inputresource:{entry}",
134 "-validate_manifest",
135 ]
136 run(cmd_check_manifest)
137
138
139 def prepare_tests(ci_type):
140 workspace = Path.cwd()
141 if ci_type == "standard":
142 run([sys.executable, "-m", "pip", "install", "pyzmq"])
143 dest = workspace / "unit_test_data"
144 download_script_assets(dest)
145 elif ci_type == "fuzz":
146 repo_dir = str(workspace / "qa-assets")
147 clone_cmd = [
148 "git",
149 "clone",
150 "--depth=1",
151 "https://github.com/bitcoin-core/qa-assets",
152 repo_dir,
153 ]
154 run(clone_cmd)
155 print("Using qa-assets repo from commit ...")
156 run(["git", "-C", repo_dir, "log", "-1"])
157
158
159 def run_tests(ci_type):
160 workspace = Path.cwd()
161 build_dir = workspace / "build"
162 num_procs = str(os.process_cpu_count())
163 release_bin = build_dir / "bin" / "Release"
164
165 if ci_type == "standard":
166 os.environ["DIR_UNIT_TEST_DATA"] = str(workspace / "unit_test_data")
167 test_envs = {
168 "BITCOIN_BIN": "bitcoin.exe",
169 "BITCOIND": "bitcoind.exe",
170 "BITCOINCLI": "bitcoin-cli.exe",
171 "BITCOIN_BENCH": "bench_bitcoin.exe",
172 "BITCOINTX": "bitcoin-tx.exe",
173 "BITCOINUTIL": "bitcoin-util.exe",
174 "BITCOINWALLET": "bitcoin-wallet.exe",
175 "BITCOINCHAINSTATE": "bitcoin-chainstate.exe",
176 }
177 for var, exe in test_envs.items():
178 os.environ[var] = str(release_bin / exe)
179
180 ctest_cmd = [
181 "ctest",
182 "--test-dir",
183 str(build_dir),
184 "--output-on-failure",
185 "--stop-on-failure",
186 "-j",
187 num_procs,
188 "--build-config",
189 "Release",
190 ]
191 run(ctest_cmd)
192
193 test_cmd = [
194 sys.executable,
195 str(build_dir / "test" / "functional" / "test_runner.py"),
196 "--jobs",
197 num_procs,
198 "--quiet",
199 f"--tmpdirprefix={workspace / '_ _'}",
200 "--combinedlogslen=99999999",
201 *shlex.split(os.environ.get("TEST_RUNNER_EXTRA", "").strip()),
202 ]
203 run(test_cmd)
204
205 elif ci_type == "fuzz":
206 os.environ["BITCOINFUZZ"] = str(release_bin / "fuzz.exe")
207 fuzz_cmd = [
208 sys.executable,
209 str(build_dir / "test" / "fuzz" / "test_runner.py"),
210 "--par",
211 num_procs,
212 "--loglevel",
213 "DEBUG",
214 str(workspace / "qa-assets" / "fuzz_corpora"),
215 ]
216 run(fuzz_cmd)
217
218
219 def main():
220 parser = argparse.ArgumentParser(description="Utility to run Windows CI steps.")
221 parser.add_argument("ci_type", choices=GENERATE_OPTIONS, help="CI type to run.")
222 steps = list(map(lambda f: f.__name__, [
223 github_import_vs_env,
224 generate,
225 build,
226 check_manifests,
227 prepare_tests,
228 run_tests,
229 ]))
230 parser.add_argument("step", choices=steps, help="CI step to perform.")
231 args = parser.parse_args()
232
233 exec(f'{args.step}("{args.ci_type}")')
234
235
236 if __name__ == "__main__":
237 main()
238