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