Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0-only
3# Copyright (C) 2025 Guillaume Tucker
4
5"""Containerized builds"""
6
7import abc
8import argparse
9import logging
10import os
11import pathlib
12import shutil
13import subprocess
14import sys
15import uuid
16
17
18class 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
55class 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
83class 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
94class 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
105class 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
134def _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
146def 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
159if __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:])))