/ .github / ci-windows.py
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          "test_kernel.exe",
113          "bitcoin-chainstate.exe",
114      }
115      for entry in release_dir.iterdir():
116          if entry.suffix.lower() != ".exe":
117              continue
118          if entry.name in skips:
119              print(f"Skipping {entry.name} (no manifest present)")
120              continue
121          print(f"Checking {entry.name}")
122          cmd_check_manifest = [
123              "mt.exe",
124              "-nologo",
125              f"-inputresource:{entry}",
126              "-validate_manifest",
127          ]
128          run(cmd_check_manifest)
129  
130  
131  def prepare_tests(ci_type):
132      workspace = Path.cwd()
133      if ci_type == "standard":
134          run([sys.executable, "-m", "pip", "install", "pyzmq"])
135          dest = workspace / "unit_test_data"
136          download_script_assets(dest)
137      elif ci_type == "fuzz":
138          repo_dir = str(workspace / "qa-assets")
139          clone_cmd = [
140              "git",
141              "clone",
142              "--depth=1",
143              "https://github.com/bitcoin-core/qa-assets",
144              repo_dir,
145          ]
146          run(clone_cmd)
147          print("Using qa-assets repo from commit ...")
148          run(["git", "-C", repo_dir, "log", "-1"])
149  
150  
151  def run_tests(ci_type):
152      workspace = Path.cwd()
153      build_dir = workspace / "build"
154      num_procs = str(os.process_cpu_count())
155      release_bin = build_dir / "bin" / "Release"
156  
157      if ci_type == "standard":
158          os.environ["DIR_UNIT_TEST_DATA"] = str(workspace / "unit_test_data")
159          test_envs = {
160              "BITCOIN_BIN": "bitcoin.exe",
161              "BITCOIND": "bitcoind.exe",
162              "BITCOINCLI": "bitcoin-cli.exe",
163              "BITCOIN_BENCH": "bench_bitcoin.exe",
164              "BITCOINTX": "bitcoin-tx.exe",
165              "BITCOINUTIL": "bitcoin-util.exe",
166              "BITCOINWALLET": "bitcoin-wallet.exe",
167              "BITCOINCHAINSTATE": "bitcoin-chainstate.exe",
168          }
169          for var, exe in test_envs.items():
170              os.environ[var] = str(release_bin / exe)
171  
172          ctest_cmd = [
173              "ctest",
174              "--test-dir",
175              str(build_dir),
176              "--output-on-failure",
177              "--stop-on-failure",
178              "-j",
179              num_procs,
180              "--build-config",
181              "Release",
182          ]
183          run(ctest_cmd)
184  
185          test_cmd = [
186              sys.executable,
187              str(build_dir / "test" / "functional" / "test_runner.py"),
188              "--jobs",
189              num_procs,
190              "--quiet",
191              f"--tmpdirprefix={workspace}",
192              "--combinedlogslen=99999999",
193              *shlex.split(os.environ.get("TEST_RUNNER_EXTRA", "").strip()),
194          ]
195          run(test_cmd)
196  
197      elif ci_type == "fuzz":
198          os.environ["BITCOINFUZZ"] = str(release_bin / "fuzz.exe")
199          fuzz_cmd = [
200              sys.executable,
201              str(build_dir / "test" / "fuzz" / "test_runner.py"),
202              "--par",
203              num_procs,
204              "--loglevel",
205              "DEBUG",
206              str(workspace / "qa-assets" / "fuzz_corpora"),
207          ]
208          run(fuzz_cmd)
209  
210  
211  def main():
212      parser = argparse.ArgumentParser(description="Utility to run Windows CI steps.")
213      parser.add_argument("ci_type", choices=GENERATE_OPTIONS, help="CI type to run.")
214      steps = list(map(lambda f: f.__name__, [
215          github_import_vs_env,
216          generate,
217          build,
218          check_manifests,
219          prepare_tests,
220          run_tests,
221      ]))
222      parser.add_argument("step", choices=steps, help="CI step to perform.")
223      args = parser.parse_args()
224  
225      exec(f'{args.step}("{args.ci_type}")')
226  
227  
228  if __name__ == "__main__":
229      main()