02_run_container.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  from pathlib import Path
   7  import os
   8  import shlex
   9  import subprocess
  10  import sys
  11  import time
  12  
  13  
  14  def run(cmd, **kwargs):
  15      print("+ " + shlex.join(cmd), flush=True)
  16      kwargs.setdefault("check", True)
  17      try:
  18          return subprocess.run(cmd, **kwargs)
  19      except Exception as e:
  20          sys.exit(str(e))
  21  
  22  
  23  def main():
  24      print("Export only allowed settings:")
  25      settings = run(
  26          ["bash", "-c", "grep export ./ci/test/00_setup_env*.sh"],
  27          stdout=subprocess.PIPE,
  28          text=True,
  29      ).stdout.splitlines()
  30      settings = set(l.split("=")[0].split("export ")[1] for l in settings)
  31      # Add "hidden" settings, which are never exported, manually. Otherwise,
  32      # they will not be passed on.
  33      settings.update([
  34          "BASE_BUILD_DIR",
  35          "CI_FAILFAST_TEST_LEAVE_DANGLING",
  36      ])
  37  
  38      # Append $USER to /tmp/env to support multi-user systems and $CONTAINER_NAME
  39      # to allow support starting multiple runs simultaneously by the same user.
  40      env_file = "/tmp/env-{u}-{c}".format(
  41          u=os.environ["USER"],
  42          c=os.environ["CONTAINER_NAME"],
  43      )
  44      with open(env_file, "w") as file:
  45          for k, v in os.environ.items():
  46              if k in settings:
  47                  file.write(f"{k}={v}\n")
  48      run(["cat", env_file])
  49  
  50      if os.getenv("DANGER_RUN_CI_ON_HOST"):
  51          print("Running on host system without docker wrapper")
  52          print("Create missing folders")
  53          for create_dir in [
  54                  os.environ["CCACHE_DIR"],
  55                  os.environ["PREVIOUS_RELEASES_DIR"],
  56          ]:
  57              Path(create_dir).mkdir(parents=True, exist_ok=True)
  58  
  59          # Modify PATH to prepend the retry script, needed for CI_RETRY_EXE
  60          os.environ["PATH"] = f"{os.environ['BASE_ROOT_DIR']}/ci/retry:{os.environ['PATH']}"
  61      else:
  62          CI_IMAGE_LABEL = "bitcoin-ci-test"
  63  
  64          # Use buildx unconditionally
  65          # Using buildx is required to properly load the correct driver, for use with registry caching. Neither build, nor BUILDKIT=1 currently do this properly
  66          cmd_build = ["docker", "buildx", "build"]
  67          cmd_build += [
  68              f"--file={os.environ['BASE_READ_ONLY_DIR']}/ci/test_imagefile",
  69              f"--build-arg=CI_IMAGE_NAME_TAG={os.environ['CI_IMAGE_NAME_TAG']}",
  70              f"--build-arg=FILE_ENV={os.environ['FILE_ENV']}",
  71              f"--build-arg=BASE_ROOT_DIR={os.environ['BASE_ROOT_DIR']}",
  72              f"--platform={os.environ['CI_IMAGE_PLATFORM']}",
  73              f"--label={CI_IMAGE_LABEL}",
  74              f"--tag={os.environ['CONTAINER_NAME']}",
  75          ]
  76          cmd_build += shlex.split(os.getenv("DOCKER_BUILD_CACHE_ARG", ""))
  77          cmd_build += [os.environ["BASE_READ_ONLY_DIR"]]
  78  
  79          print(f"Building {os.environ['CONTAINER_NAME']} image tag to run in")
  80          if run(cmd_build, check=False).returncode != 0:
  81              print(f"Retry building {os.environ['CONTAINER_NAME']} image tag after failure")
  82              time.sleep(3)
  83              run(cmd_build)
  84  
  85          for suffix in ["ccache", "depends", "depends_sources", "previous_releases"]:
  86              run(["docker", "volume", "create", f"{os.environ['CONTAINER_NAME']}_{suffix}"], check=False)
  87  
  88          CI_CCACHE_MOUNT = f"type=volume,src={os.environ['CONTAINER_NAME']}_ccache,dst={os.environ['CCACHE_DIR']}"
  89          CI_DEPENDS_MOUNT = f"type=volume,src={os.environ['CONTAINER_NAME']}_depends,dst={os.environ['DEPENDS_DIR']}/built"
  90          CI_DEPENDS_SOURCES_MOUNT = f"type=volume,src={os.environ['CONTAINER_NAME']}_depends_sources,dst={os.environ['DEPENDS_DIR']}/sources"
  91          CI_PREVIOUS_RELEASES_MOUNT = f"type=volume,src={os.environ['CONTAINER_NAME']}_previous_releases,dst={os.environ['PREVIOUS_RELEASES_DIR']}"
  92          CI_BUILD_MOUNT = []
  93  
  94          if os.getenv("DANGER_CI_ON_HOST_FOLDERS"):
  95              # ensure the directories exist
  96              for create_dir in [
  97                      os.environ["CCACHE_DIR"],
  98                      f"{os.environ['DEPENDS_DIR']}/built",
  99                      f"{os.environ['DEPENDS_DIR']}/sources",
 100                      os.environ["PREVIOUS_RELEASES_DIR"],
 101                      os.environ["BASE_BUILD_DIR"],  # Unset by default, must be defined externally
 102              ]:
 103                  Path(create_dir).mkdir(parents=True, exist_ok=True)
 104  
 105              CI_CCACHE_MOUNT = f"type=bind,src={os.environ['CCACHE_DIR']},dst={os.environ['CCACHE_DIR']}"
 106              CI_DEPENDS_MOUNT = f"type=bind,src={os.environ['DEPENDS_DIR']}/built,dst={os.environ['DEPENDS_DIR']}/built"
 107              CI_DEPENDS_SOURCES_MOUNT = f"type=bind,src={os.environ['DEPENDS_DIR']}/sources,dst={os.environ['DEPENDS_DIR']}/sources"
 108              CI_PREVIOUS_RELEASES_MOUNT = f"type=bind,src={os.environ['PREVIOUS_RELEASES_DIR']},dst={os.environ['PREVIOUS_RELEASES_DIR']}"
 109              CI_BUILD_MOUNT = [f"--mount=type=bind,src={os.environ['BASE_BUILD_DIR']},dst={os.environ['BASE_BUILD_DIR']}"]
 110  
 111          if os.getenv("DANGER_CI_ON_HOST_CCACHE_FOLDER"):
 112              if not os.path.isdir(os.environ["CCACHE_DIR"]):
 113                  print(f"Error: Directory '{os.environ['CCACHE_DIR']}' must be created in advance.")
 114                  sys.exit(1)
 115              CI_CCACHE_MOUNT = f"type=bind,src={os.environ['CCACHE_DIR']},dst={os.environ['CCACHE_DIR']}"
 116  
 117          run(["docker", "network", "create", "--ipv6", "--subnet", "1111:1111::/112", "ci-ip6net"], check=False)
 118          run(["docker", "network", "create", "--subnet", "1.1.1.0/24", "ci-ip4net"], check=False)
 119  
 120          if os.getenv("RESTART_CI_DOCKER_BEFORE_RUN"):
 121              print("Restart docker before run to stop and clear all containers started with --rm")
 122              run(["podman", "container", "rm", "--force", "--all"])  # Similar to "systemctl restart docker"
 123  
 124              # Still prune everything in case the filtered pruning doesn't work, or if labels were not set
 125              # on a previous run. Belt and suspenders approach, should be fine to remove in the future.
 126              # Prune images used by --external containers (e.g. build containers) when
 127              # using podman.
 128              print("Prune all dangling images")
 129              run(["podman", "image", "prune", "--force", "--external"])
 130  
 131          print(f"Prune all dangling {CI_IMAGE_LABEL} images")
 132          # When detecting podman-docker, `--external` should be added.
 133          run(["docker", "image", "prune", "--force", "--filter", f"label={CI_IMAGE_LABEL}"])
 134  
 135          cmd_run = ["docker", "run", "--rm", "--interactive", "--detach", "--tty"]
 136          cmd_run += [
 137              "--cap-add=LINUX_IMMUTABLE",
 138              *shlex.split(os.getenv("CI_CONTAINER_CAP", "")),
 139              f"--mount=type=bind,src={os.environ['BASE_READ_ONLY_DIR']},dst={os.environ['BASE_READ_ONLY_DIR']},readonly",
 140              f"--mount={CI_CCACHE_MOUNT}",
 141              f"--mount={CI_DEPENDS_MOUNT}",
 142              f"--mount={CI_DEPENDS_SOURCES_MOUNT}",
 143              f"--mount={CI_PREVIOUS_RELEASES_MOUNT}",
 144              *CI_BUILD_MOUNT,
 145              f"--env-file={env_file}",
 146              f"--name={os.environ['CONTAINER_NAME']}",
 147              "--network=ci-ip6net",
 148              "--ip6=1111:1111::5", # Used by some of the tests, don't change it just here (keep them in sync).
 149              f"--platform={os.environ['CI_IMAGE_PLATFORM']}",
 150              os.environ["CONTAINER_NAME"],
 151          ]
 152  
 153          container_id = run(
 154              cmd_run,
 155              stdout=subprocess.PIPE,
 156              text=True,
 157          ).stdout.strip()
 158  
 159          run(["docker", "network", "connect", "--ip=1.1.1.5", "ci-ip4net", container_id]) # The IP address is used by some of the tests, don't change it just here (keep them in sync).
 160  
 161      def ci_exec(cmd_inner, **kwargs):
 162          if os.getenv("DANGER_RUN_CI_ON_HOST"):
 163              prefix = []
 164          else:
 165              prefix = [
 166                  "docker",
 167                  "exec",
 168                  "--env",
 169                  "DANGER_RUN_CI_ON_HOST=1",  # Safe to set *inside* the container
 170                  container_id,
 171              ]
 172  
 173          return run([*prefix, *cmd_inner], **kwargs)
 174  
 175      # Normalize all folders to BASE_ROOT_DIR
 176      ci_exec([
 177          "rsync",
 178          "--recursive",
 179          "--perms",
 180          "--stats",
 181          "--human-readable",
 182          f"{os.environ['BASE_READ_ONLY_DIR']}/",
 183          f"{os.environ['BASE_ROOT_DIR']}",
 184      ])
 185      ci_exec([f"{os.environ['BASE_ROOT_DIR']}/ci/test/01_base_install.sh"])
 186      ci_exec([f"{os.environ['BASE_ROOT_DIR']}/ci/test/03_test_script.sh"])
 187  
 188      if not os.getenv("DANGER_RUN_CI_ON_HOST"):
 189          print("Stop and remove CI container by ID")
 190          run(["docker", "container", "kill", container_id])
 191  
 192  
 193  if __name__ == "__main__":
 194      main()
 195