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