/ .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  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()