Skip to content

Commit 8f989b3

Browse files
gctuckernathanchance
authored andcommitted
scripts: add tool to run containerized builds
Add a 'scripts/container' tool written in Python to run any command in the source tree from within a container. This can typically be used to call 'make' with a compiler toolchain image to run reproducible builds but any arbitrary command can be run too. Only Docker and Podman are supported in this initial version. Add a new entry to MAINTAINERS accordingly. Link: https://lore.kernel.org/all/affb7aff-dc9b-4263-bbd4-a7965c19ac4e@gtucker.io/ Signed-off-by: Guillaume Tucker <gtucker@gtucker.io> Tested-by: Nicolas Schier <nsc@kernel.org> Acked-by: Nicolas Schier <nsc@kernel.org> Link: https://patch.msgid.link/9b8da20157e409e8fa3134d2101678779e157256.1769090419.git.gtucker@gtucker.io Signed-off-by: Nathan Chancellor <nathan@kernel.org>
1 parent 502678b commit 8f989b3

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

MAINTAINERS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6380,6 +6380,11 @@ S: Supported
63806380
F: drivers/video/console/
63816381
F: include/linux/console*
63826382

6383+
CONTAINER BUILD SCRIPT
6384+
M: Guillaume Tucker <gtucker@gtucker.io>
6385+
S: Maintained
6386+
F: scripts/container
6387+
63836388
CONTEXT TRACKING
63846389
M: Frederic Weisbecker <frederic@kernel.org>
63856390
M: "Paul E. McKenney" <paulmck@kernel.org>
@@ -13668,6 +13673,7 @@ F: scripts/Makefile*
1366813673
F: scripts/bash-completion/
1366913674
F: scripts/basic/
1367013675
F: scripts/clang-tools/
13676+
F: scripts/container
1367113677
F: scripts/dummy-tools/
1367213678
F: scripts/include/
1367313679
F: scripts/mk*

scripts/container

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0-only
3+
# Copyright (C) 2025 Guillaume Tucker
4+
5+
"""Containerized builds"""
6+
7+
import abc
8+
import argparse
9+
import logging
10+
import os
11+
import pathlib
12+
import shutil
13+
import subprocess
14+
import sys
15+
import uuid
16+
17+
18+
class ContainerRuntime(abc.ABC):
19+
"""Base class for a container runtime implementation"""
20+
21+
name = None # Property defined in each implementation class
22+
23+
def __init__(self, args, logger):
24+
self._uid = args.uid or os.getuid()
25+
self._gid = args.gid or args.uid or os.getgid()
26+
self._env_file = args.env_file
27+
self._shell = args.shell
28+
self._logger = logger
29+
30+
@classmethod
31+
def is_present(cls):
32+
"""Determine whether the runtime is present on the system"""
33+
return shutil.which(cls.name) is not None
34+
35+
@abc.abstractmethod
36+
def _do_run(self, image, cmd, container_name):
37+
"""Runtime-specific handler to run a command in a container"""
38+
39+
@abc.abstractmethod
40+
def _do_abort(self, container_name):
41+
"""Runtime-specific handler to abort a running container"""
42+
43+
def run(self, image, cmd):
44+
"""Run a command in a runtime container"""
45+
container_name = str(uuid.uuid4())
46+
self._logger.debug("container: %s", container_name)
47+
try:
48+
return self._do_run(image, cmd, container_name)
49+
except KeyboardInterrupt:
50+
self._logger.error("user aborted")
51+
self._do_abort(container_name)
52+
return 1
53+
54+
55+
class CommonRuntime(ContainerRuntime):
56+
"""Common logic for Docker and Podman"""
57+
58+
def _do_run(self, image, cmd, container_name):
59+
cmdline = [self.name, 'run']
60+
cmdline += self._get_opts(container_name)
61+
cmdline.append(image)
62+
cmdline += cmd
63+
self._logger.debug('command: %s', ' '.join(cmdline))
64+
return subprocess.call(cmdline)
65+
66+
def _get_opts(self, container_name):
67+
opts = [
68+
'--name', container_name,
69+
'--rm',
70+
'--volume', f'{pathlib.Path.cwd()}:/src',
71+
'--workdir', '/src',
72+
]
73+
if self._env_file:
74+
opts += ['--env-file', self._env_file]
75+
if self._shell:
76+
opts += ['--interactive', '--tty']
77+
return opts
78+
79+
def _do_abort(self, container_name):
80+
subprocess.call([self.name, 'kill', container_name])
81+
82+
83+
class DockerRuntime(CommonRuntime):
84+
"""Run a command in a Docker container"""
85+
86+
name = 'docker'
87+
88+
def _get_opts(self, container_name):
89+
return super()._get_opts(container_name) + [
90+
'--user', f'{self._uid}:{self._gid}'
91+
]
92+
93+
94+
class PodmanRuntime(CommonRuntime):
95+
"""Run a command in a Podman container"""
96+
97+
name = 'podman'
98+
99+
def _get_opts(self, container_name):
100+
return super()._get_opts(container_name) + [
101+
'--userns', f'keep-id:uid={self._uid},gid={self._gid}',
102+
]
103+
104+
105+
class Runtimes:
106+
"""List of all supported runtimes"""
107+
108+
runtimes = [PodmanRuntime, DockerRuntime]
109+
110+
@classmethod
111+
def get_names(cls):
112+
"""Get a list of all the runtime names"""
113+
return list(runtime.name for runtime in cls.runtimes)
114+
115+
@classmethod
116+
def get(cls, name):
117+
"""Get a single runtime class matching the given name"""
118+
for runtime in cls.runtimes:
119+
if runtime.name == name:
120+
if not runtime.is_present():
121+
raise ValueError(f"runtime not found: {name}")
122+
return runtime
123+
raise ValueError(f"unknown runtime: {name}")
124+
125+
@classmethod
126+
def find(cls):
127+
"""Find the first runtime present on the system"""
128+
for runtime in cls.runtimes:
129+
if runtime.is_present():
130+
return runtime
131+
raise ValueError("no runtime found")
132+
133+
134+
def _get_logger(verbose):
135+
"""Set up a logger with the appropriate level"""
136+
logger = logging.getLogger('container')
137+
handler = logging.StreamHandler()
138+
handler.setFormatter(logging.Formatter(
139+
fmt='[container {levelname}] {message}', style='{'
140+
))
141+
logger.addHandler(handler)
142+
logger.setLevel(logging.DEBUG if verbose is True else logging.INFO)
143+
return logger
144+
145+
146+
def main(args):
147+
"""Main entry point for the container tool"""
148+
logger = _get_logger(args.verbose)
149+
try:
150+
cls = Runtimes.get(args.runtime) if args.runtime else Runtimes.find()
151+
except ValueError as ex:
152+
logger.error(ex)
153+
return 1
154+
logger.debug("runtime: %s", cls.name)
155+
logger.debug("image: %s", args.image)
156+
return cls(args, logger).run(args.image, args.cmd)
157+
158+
159+
if __name__ == '__main__':
160+
parser = argparse.ArgumentParser(
161+
'container',
162+
description="See the documentation for more details: "
163+
"https://docs.kernel.org/dev-tools/container.html"
164+
)
165+
parser.add_argument(
166+
'-e', '--env-file',
167+
help="Path to an environment file to load in the container."
168+
)
169+
parser.add_argument(
170+
'-g', '--gid',
171+
help="Group ID to use inside the container."
172+
)
173+
parser.add_argument(
174+
'-i', '--image', required=True,
175+
help="Container image name."
176+
)
177+
parser.add_argument(
178+
'-r', '--runtime', choices=Runtimes.get_names(),
179+
help="Container runtime name. If not specified, the first one found "
180+
"on the system will be used i.e. Podman if present, otherwise Docker."
181+
)
182+
parser.add_argument(
183+
'-s', '--shell', action='store_true',
184+
help="Run the container in an interactive shell."
185+
)
186+
parser.add_argument(
187+
'-u', '--uid',
188+
help="User ID to use inside the container. If the -g option is not "
189+
"specified, the user ID will also be set as the group ID."
190+
)
191+
parser.add_argument(
192+
'-v', '--verbose', action='store_true',
193+
help="Enable verbose output."
194+
)
195+
parser.add_argument(
196+
'cmd', nargs='+',
197+
help="Command to run in the container"
198+
)
199+
sys.exit(main(parser.parse_args(sys.argv[1:])))

0 commit comments

Comments
 (0)