Skip to content

Commit 36f0667

Browse files
committed
Support new Apple Container
* No more need to have closed source Docker on Apple platforms * Uses the Virtualization framework features of macOS Tahoe, in particular this should avoid having an always running VM
1 parent ae24aa8 commit 36f0667

7 files changed

Lines changed: 157 additions & 43 deletions

File tree

alibuild_helpers/args.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,13 @@ def finaliseArgs(args, parser):
430430
if args.action in ("build", "doctor", "deps"):
431431
if args.dockerImage or args.docker_extra_args:
432432
args.docker = True
433+
# In case we build with docker / containers, we add a special
434+
# devel prefix (if not already present) so that we do not pollute
435+
# the namespace of the current architecture
436+
if args.docker and hasattr(args, "develPrefix"):
437+
args.develPrefix = args.architecture
433438

434439
args.docker_extra_args = shlex.split(args.docker_extra_args)
435-
args.docker_extra_args.append("--network=host")
436440

437441
if args.docker and args.architecture.startswith("osx"):
438442
parser.error("cannot use `-a %s` and --docker" % args.architecture)

alibuild_helpers/build.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from alibuild_helpers.analytics import report_event
66
from alibuild_helpers.log import debug, info, banner, warning
77
from alibuild_helpers.log import dieOnError
8-
from alibuild_helpers.cmd import execute, DockerRunner, BASH, install_wrapper_script, getstatusoutput
8+
from alibuild_helpers.cmd import execute, ContainerRunner, container_run_string, BASH, install_wrapper_script, getstatusoutput
99
from alibuild_helpers.utilities import prunePaths, symlink, call_ignoring_oserrors, topological_sort, detectArch
1010
from alibuild_helpers.utilities import resolve_store_path
1111
from alibuild_helpers.utilities import parseDefaults, readDefaults
@@ -512,10 +512,10 @@ def doBuild(args, parser):
512512
}
513513
extra_env.update(dict([e.partition('=')[::2] for e in args.environment]))
514514

515-
with DockerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{os.path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
515+
with ContainerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{os.path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_container:
516516
def performPreferCheckWithTempDir(pkg, cmd):
517517
with tempfile.TemporaryDirectory(prefix=f"alibuild_prefer_check_{pkg['package']}_") as temp_dir:
518-
return getstatusoutput_docker(cmd, cwd=temp_dir)
518+
return getstatusoutput_container(cmd, cwd=temp_dir)
519519

520520
systemPackages, ownPackages, failed, validDefaults = \
521521
getPackageList(packages = packages,
@@ -1091,28 +1091,9 @@ def performPreferCheckWithTempDir(pkg, cmd):
10911091
# In case the --docker options is passed, we setup a docker container which
10921092
# will perform the actual build. Otherwise build as usual using bash.
10931093
if args.docker:
1094-
build_command = (
1095-
"docker run --rm --entrypoint= --user $(id -u):$(id -g) "
1096-
"-v {workdir}:/sw -v{configDir}:/alidist:ro -v {scriptDir}/build.sh:/build.sh:ro "
1097-
"{mirrorVolume} {develVolumes} {additionalEnv} {additionalVolumes} "
1098-
"-e WORK_DIR_OVERRIDE=/sw -e ALIBUILD_CONFIG_DIR_OVERRIDE=/alidist {extraArgs} {image} bash -ex /build.sh"
1099-
).format(
1100-
image=quote(args.dockerImage),
1101-
workdir=quote(abspath(args.workDir)),
1102-
configDir=quote(abspath(args.configDir)),
1103-
scriptDir=quote(scriptDir),
1104-
extraArgs=" ".join(map(quote, args.docker_extra_args)),
1105-
additionalEnv=" ".join(
1106-
f"-e {var}={quote(value)}" for var, value in buildEnvironment),
1107-
# Used e.g. by O2DPG-sim-tests to find the O2DPG repository.
1108-
develVolumes=" ".join(
1109-
'-v "$PWD/$(readlink {pkg} || echo {pkg})":/{pkg}:rw'.format(pkg=quote(spec["package"]))
1110-
for spec in specs.values() if spec["is_devel_pkg"]),
1111-
additionalVolumes=" ".join(
1112-
"-v %s" % quote(volume) for volume in args.volumes),
1113-
mirrorVolume=("-v %s:/mirror" % quote(dirname(spec["reference"]))
1114-
if "reference" in spec else ""),
1115-
)
1094+
build_command = container_run_string(args.dockerImage, args.workDir, args.configDir, scriptDir,
1095+
args.docker_extra_args, spec, specs, args.volumes, buildEnvironment)
1096+
11161097
else:
11171098
os.environ.update(buildEnvironment)
11181099
build_command = f"{BASH} -e -x {quote(scriptDir)}/build.sh 2>&1"

alibuild_helpers/build_template.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ mkdir -p "${WORK_DIR}/TARS/$HASH_PATH" \
293293
PACKAGE_WITH_REV=$PKGNAME-$PKGVERSION-$PKGREVISION.$ARCHITECTURE.tar.gz
294294
# Copy and tar/compress (if applicable) in parallel.
295295
# Use -H to match tar's behaviour of preserving hardlinks.
296-
rsync -aH "$WORK_DIR/INSTALLROOT/$PKGHASH/" "$WORK_DIR" & rsync_pid=$!
296+
rsync -DgloprH "$WORK_DIR/INSTALLROOT/$PKGHASH/" "$WORK_DIR" & rsync_pid=$!
297297
if [ "$CAN_DELETE" = 1 ]; then
298298
# We're deleting the tarball anyway, so no point in creating a new one.
299299
# There might be an old existing tarball, and we should delete it.

alibuild_helpers/cmd.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from textwrap import dedent
66
from subprocess import TimeoutExpired
77
from shlex import quote
8+
import platform
89

910
from alibuild_helpers.log import debug, error, dieOnError
1011

@@ -70,6 +71,52 @@ def execute(command, printer=debug, timeout=None):
7071

7172
BASH = "bash" if getstatusoutput("/bin/bash --version")[0] else "/bin/bash"
7273

74+
class AppleContainerRunner:
75+
"""A context manager for running commands inside a Apple Container (see https://github.com/apple/container)
76+
If the image given is None or empty, the commands are run on the host instead.
77+
"""
78+
def __init__(self, container_image, container_run_args, extra_env, extra_volumes) -> None:
79+
self._container_image = container_image
80+
self._container_run_args = container_run_args
81+
self._container = None
82+
self._extra_env = extra_env
83+
self._extra_volumes = extra_volumes
84+
85+
def __enter__(self):
86+
def getstatusoutput_host(cmd, cwd=None):
87+
command_prefix=""
88+
if self._extra_env:
89+
command_prefix="env " + " ".join("{}={}".format(k, quote(v)) for (k,v) in self._extra_env.items()) + " "
90+
return getstatusoutput("{}{} -c {}".format(command_prefix, BASH, quote(cmd))
91+
, cwd=cwd)
92+
93+
if not self._container_image:
94+
return getstatusoutput_host
95+
96+
envOpts = [opt for k, v in self._extra_env.items() for opt in ("-e", f"{k}={v}")]
97+
volumes = [opt for v in self._extra_volumes for opt in ("-v", v)]
98+
# Apple Container is more picky about missing entrypoints, so we always override it
99+
# with /bin/sleep.
100+
cmd = ["container", "run", "--detach"] + envOpts + volumes + ["--rm", "--entrypoint=/bin/sleep"]
101+
cmd += self._container_run_args
102+
cmd += [self._container_image, "inf"]
103+
self._container = getoutput(cmd).strip()
104+
105+
def getstatusoutput_container(cmd, cwd=None):
106+
if self._container is None:
107+
return getstatusoutput_host(cmd, cwd=cwd)
108+
envOpts = [opt for k, v in self._extra_env.items() for opt in ("-e", f"{k}={v}")]
109+
exec_cmd = ["container", "exec"] + envOpts + [self._container, "bash", "-c", cmd]
110+
return getstatusoutput(exec_cmd, cwd=cwd)
111+
112+
return getstatusoutput_container
113+
114+
def __exit__(self, exc_type, exc_value, traceback):
115+
if self._container is not None:
116+
getstatusoutput("container kill " + quote(self._container))
117+
self._container = None
118+
return False # propagate any exception that may have occurred
119+
73120

74121
class DockerRunner:
75122
"""A context manager for running commands inside a Docker container.
@@ -146,3 +193,85 @@ def install_wrapper_script(name, work_dir):
146193
"rerunning this command inside a login shell (e.g. `bash -l`). "
147194
"If that doesn't work, run `export PATH` manually.")
148195
os.environ["PATH"] = script_dir + ":" + os.environ["PATH"]
196+
197+
def to_int(s: str) -> int:
198+
try:
199+
return int(float(s))
200+
except (ValueError, TypeError):
201+
return 0
202+
203+
204+
def _apple_run_string(dockerImage, workDir, configDir, scriptDir, docker_extra_args, spec, specs, volumes, buildEnvironment):
205+
build_command = (
206+
"container run --rm --entrypoint=/bin/bash --user $(id -u):$(id -g) "
207+
"--mount type=bind,source={workdir},target=/sw "
208+
"--mount type=bind,source={configDir},target=/alidist,readonly "
209+
"--mount type=bind,source={scriptDir},target=/scripts,readonly "
210+
"{mirrorVolume} {develVolumes} {additionalEnv} {additionalVolumes} "
211+
"-e WORK_DIR_OVERRIDE=/sw -e ALIBUILD_CONFIG_DIR_OVERRIDE=/alidist {extraArgs} {image} -ex /scripts/build.sh"
212+
).format(
213+
image=quote(dockerImage),
214+
workdir=quote(os.path.abspath(workDir)),
215+
configDir=quote(os.path.abspath(configDir)),
216+
scriptDir=quote(scriptDir),
217+
extraArgs=" ".join(map(quote, docker_extra_args)),
218+
additionalEnv=" ".join(
219+
"-e {}={}".format(var, quote(value)) for var, value in buildEnvironment
220+
),
221+
# Used e.g. by O2DPG-sim-tests to find the O2DPG repository.
222+
develVolumes=" ".join(
223+
'--mount type=bind,source="$PWD/$(readlink {pkg} || echo {pkg})",target=/{pkg},readonly'.format(
224+
pkg=quote(s["package"])
225+
)
226+
for s in specs.values()
227+
if s["is_devel_pkg"]
228+
),
229+
additionalVolumes=" ".join("--mount type=bind,source=%s" % quote(volume) for volume in volumes),
230+
mirrorVolume=(
231+
"--mount source=%s,target=/mirror" % quote(os.path.dirname(spec["reference"]))
232+
if "reference" in spec
233+
else ""
234+
),
235+
)
236+
print(build_command)
237+
return build_command
238+
239+
def _docker_run_string( dockerImage, workDir, configDir, scriptDir, docker_extra_args, spec, specs, volumes, buildEnvironment):
240+
build_command = (
241+
"docker run --rm --entrypoint=/bin/bash --user $(id -u):$(id -g) "
242+
"-v {workdir}:/sw -v{configDir}:/alidist:ro -v {scriptDir}/build.sh:/build.sh:ro "
243+
"{mirrorVolume} {develVolumes} {additionalEnv} {additionalVolumes} "
244+
"-e WORK_DIR_OVERRIDE=/sw -e ALIBUILD_CONFIG_DIR_OVERRIDE=/alidist {extraArgs} --network=host {image} bash -ex /build.sh"
245+
).format(
246+
image=quote(dockerImage),
247+
workdir=quote(os.path.abspath(workDir)),
248+
configDir=quote(os.path.abspath(configDir)),
249+
scriptDir=quote(scriptDir),
250+
extraArgs=" ".join(map(quote, docker_extra_args)),
251+
additionalEnv=" ".join(
252+
"-e {}={}".format(var, quote(value)) for var, value in buildEnvironment
253+
),
254+
# Used e.g. by O2DPG-sim-tests to find the O2DPG repository.
255+
develVolumes=" ".join(
256+
'-v "$PWD/$(readlink {pkg} || echo {pkg})":/{pkg}:rw'.format(
257+
pkg=quote(spec["package"])
258+
)
259+
for spec in specs.values()
260+
if spec["is_devel_pkg"]
261+
),
262+
additionalVolumes=" ".join("-v %s" % quote(volume) for volume in volumes),
263+
mirrorVolume=(
264+
"-v %s:/mirror" % quote(os.path.dirname(spec["reference"]))
265+
if "reference" in spec
266+
else ""
267+
),
268+
)
269+
return build_command
270+
271+
272+
if to_int(platform.mac_ver()[0].split(".")[0]) >= 26:
273+
ContainerRunner = AppleContainerRunner
274+
container_run_string = _apple_run_string
275+
else:
276+
ContainerRunner = DockerRunner
277+
container_run_string = _docker_run_string

alibuild_helpers/deps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from alibuild_helpers.log import debug, error, info, dieOnError
44
from alibuild_helpers.utilities import parseDefaults, readDefaults, getPackageList, validateDefaults
5-
from alibuild_helpers.cmd import DockerRunner, execute
5+
from alibuild_helpers.cmd import ContainerRunner, execute
66
from tempfile import NamedTemporaryFile
77
from os import remove, path
88

@@ -20,7 +20,7 @@ def doDeps(args, parser):
2020
extra_env = {"ALIBUILD_CONFIG_DIR": "/alidist" if args.docker else path.abspath(args.configDir)}
2121
extra_env.update(dict([e.partition('=')[::2] for e in args.environment]))
2222

23-
with DockerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
23+
with ContainerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
2424
def performCheck(pkg, cmd):
2525
return getstatusoutput_docker(cmd)
2626

alibuild_helpers/doctor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from alibuild_helpers.log import debug, error, banner, info, success, warning
88
from alibuild_helpers.log import logger
99
from alibuild_helpers.utilities import getPackageList, parseDefaults, readDefaults, validateDefaults
10-
from alibuild_helpers.cmd import getstatusoutput, DockerRunner
10+
from alibuild_helpers.cmd import getstatusoutput, ContainerRunner
1111
import tempfile
1212

1313
def prunePaths(workDir) -> None:
@@ -89,7 +89,7 @@ def doDoctor(args, parser):
8989
extra_env = {"ALIBUILD_CONFIG_DIR": "/alidist" if args.docker else os.path.abspath(args.configDir)}
9090
extra_env.update(dict([e.partition('=')[::2] for e in args.environment]))
9191

92-
with DockerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{os.path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
92+
with ContainerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{os.path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
9393
err, output = getstatusoutput_docker("type c++")
9494
if err:
9595
warning("Unable to find system compiler.\n"
@@ -149,7 +149,7 @@ def performValidateDefaults(spec):
149149
extra_env = {"ALIBUILD_CONFIG_DIR": "/alidist" if args.docker else os.path.abspath(args.configDir)}
150150
extra_env.update(dict([e.partition('=')[::2] for e in args.environment]))
151151

152-
with DockerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{os.path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
152+
with ContainerRunner(args.dockerImage, args.docker_extra_args, extra_env=extra_env, extra_volumes=[f"{os.path.abspath(args.configDir)}:/alidist:ro"] if args.docker else []) as getstatusoutput_docker:
153153
fromSystem, own, failed, validDefaults = \
154154
getPackageList(packages = packages,
155155
specs = specs,

tests/test_cmd.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Assuming you are using the mock library to ... mock things
22
from unittest import mock
33

4-
from alibuild_helpers.cmd import execute, DockerRunner
4+
from alibuild_helpers.cmd import execute, ContainerRunner
55

66
import unittest
77

@@ -20,9 +20,9 @@ def test_execute(self, mock_debug):
2020

2121
@mock.patch("alibuild_helpers.cmd.getoutput")
2222
@mock.patch("alibuild_helpers.cmd.getstatusoutput")
23-
def test_DockerRunner(self, mock_getstatusoutput, mock_getoutput):
23+
def test_ContainerRunner(self, mock_getstatusoutput, mock_getoutput):
2424
mock_getoutput.side_effect = lambda cmd: "container-id\n"
25-
with DockerRunner("image", ["extra arg"]) as getstatusoutput_docker:
25+
with ContainerRunner("image", ["extra arg"]) as getstatusoutput_docker:
2626
mock_getoutput.assert_called_with(["docker", "run", "--detach", "--rm", "--entrypoint=",
2727
"extra arg", "image", "sleep", "inf"])
2828
getstatusoutput_docker("echo foo")
@@ -31,7 +31,7 @@ def test_DockerRunner(self, mock_getstatusoutput, mock_getoutput):
3131

3232
mock_getoutput.reset_mock()
3333
mock_getstatusoutput.reset_mock()
34-
with DockerRunner("") as getstatusoutput_docker:
34+
with ContainerRunner("") as getstatusoutput_docker:
3535
mock_getoutput.assert_not_called()
3636
getstatusoutput_docker("echo foo")
3737
mock_getstatusoutput.assert_called_with("/bin/bash -c 'echo foo'", cwd=None)
@@ -40,13 +40,13 @@ def test_DockerRunner(self, mock_getstatusoutput, mock_getoutput):
4040

4141
@mock.patch("alibuild_helpers.cmd.getoutput")
4242
@mock.patch("alibuild_helpers.cmd.getstatusoutput")
43-
def test_DockerRunner_with_env_vars(self, mock_getstatusoutput, mock_getoutput):
43+
def test_ContainerRunner_with_env_vars(self, mock_getstatusoutput, mock_getoutput):
4444
# Test that environment variables are properly injected into docker exec commands.
4545
mock_getoutput.side_effect = lambda cmd: "container-id\n"
4646

4747
# Test with environment variables
4848
extra_env = {"TEST_VAR": "test_value", "ANOTHER_VAR": "another_value"}
49-
with DockerRunner("image", extra_env=extra_env) as getstatusoutput_docker:
49+
with ContainerRunner("image", extra_env=extra_env) as getstatusoutput_docker:
5050
# Verify container creation includes environment variables
5151
mock_getoutput.assert_called_with(["docker", "run", "--detach",
5252
"-e", "TEST_VAR=test_value",
@@ -63,29 +63,29 @@ def test_DockerRunner_with_env_vars(self, mock_getstatusoutput, mock_getoutput):
6363
# Test host execution with environment variables
6464
mock_getoutput.reset_mock()
6565
mock_getstatusoutput.reset_mock()
66-
with DockerRunner("", extra_env=extra_env) as getstatusoutput_docker:
66+
with ContainerRunner("", extra_env=extra_env) as getstatusoutput_docker:
6767
mock_getoutput.assert_not_called()
6868
getstatusoutput_docker("echo test")
6969
mock_getstatusoutput.assert_called_with("env TEST_VAR=test_value ANOTHER_VAR=another_value /bin/bash -c 'echo test'", cwd=None)
7070

7171
@mock.patch("alibuild_helpers.cmd.getoutput")
7272
@mock.patch("alibuild_helpers.cmd.getstatusoutput")
73-
def test_DockerRunner_multiline_env_var(self, mock_getstatusoutput, mock_getoutput):
73+
def test_ContainerRunner_multiline_env_var(self, mock_getstatusoutput, mock_getoutput):
7474
multiline_value = "line1\nline2\nline3"
7575
extra_env = {"MULTILINE_VAR": multiline_value}
7676

77-
with DockerRunner("", extra_env=extra_env) as getstatusoutput_docker:
77+
with ContainerRunner("", extra_env=extra_env) as getstatusoutput_docker:
7878
mock_getoutput.assert_not_called()
7979
getstatusoutput_docker("echo test")
8080
mock_getstatusoutput.assert_called_with("env MULTILINE_VAR='line1\nline2\nline3' /bin/bash -c 'echo test'", cwd=None)
8181

8282
@mock.patch("alibuild_helpers.cmd.getoutput")
8383
@mock.patch("alibuild_helpers.cmd.getstatusoutput")
84-
def test_DockerRunner_env_var_with_semicolon(self, mock_getstatusoutput, mock_getoutput):
84+
def test_ContainerRunner_env_var_with_semicolon(self, mock_getstatusoutput, mock_getoutput):
8585
semicolon_value = "value1;value2;value3"
8686
extra_env = {"SEMICOLON_VAR": semicolon_value}
8787

88-
with DockerRunner("", extra_env=extra_env) as getstatusoutput_docker:
88+
with ContainerRunner("", extra_env=extra_env) as getstatusoutput_docker:
8989
mock_getoutput.assert_not_called()
9090
getstatusoutput_docker("echo test")
9191
mock_getstatusoutput.assert_called_with("env SEMICOLON_VAR='value1;value2;value3' /bin/bash -c 'echo test'", cwd=None)

0 commit comments

Comments
 (0)