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