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