mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-02-28 19:06:51 +01:00
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>
200 lines
5.9 KiB
Python
Executable File
200 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-only
|
|
# Copyright (C) 2025 Guillaume Tucker
|
|
|
|
"""Containerized builds"""
|
|
|
|
import abc
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
|
|
|
|
class ContainerRuntime(abc.ABC):
|
|
"""Base class for a container runtime implementation"""
|
|
|
|
name = None # Property defined in each implementation class
|
|
|
|
def __init__(self, args, logger):
|
|
self._uid = args.uid or os.getuid()
|
|
self._gid = args.gid or args.uid or os.getgid()
|
|
self._env_file = args.env_file
|
|
self._shell = args.shell
|
|
self._logger = logger
|
|
|
|
@classmethod
|
|
def is_present(cls):
|
|
"""Determine whether the runtime is present on the system"""
|
|
return shutil.which(cls.name) is not None
|
|
|
|
@abc.abstractmethod
|
|
def _do_run(self, image, cmd, container_name):
|
|
"""Runtime-specific handler to run a command in a container"""
|
|
|
|
@abc.abstractmethod
|
|
def _do_abort(self, container_name):
|
|
"""Runtime-specific handler to abort a running container"""
|
|
|
|
def run(self, image, cmd):
|
|
"""Run a command in a runtime container"""
|
|
container_name = str(uuid.uuid4())
|
|
self._logger.debug("container: %s", container_name)
|
|
try:
|
|
return self._do_run(image, cmd, container_name)
|
|
except KeyboardInterrupt:
|
|
self._logger.error("user aborted")
|
|
self._do_abort(container_name)
|
|
return 1
|
|
|
|
|
|
class CommonRuntime(ContainerRuntime):
|
|
"""Common logic for Docker and Podman"""
|
|
|
|
def _do_run(self, image, cmd, container_name):
|
|
cmdline = [self.name, 'run']
|
|
cmdline += self._get_opts(container_name)
|
|
cmdline.append(image)
|
|
cmdline += cmd
|
|
self._logger.debug('command: %s', ' '.join(cmdline))
|
|
return subprocess.call(cmdline)
|
|
|
|
def _get_opts(self, container_name):
|
|
opts = [
|
|
'--name', container_name,
|
|
'--rm',
|
|
'--volume', f'{pathlib.Path.cwd()}:/src',
|
|
'--workdir', '/src',
|
|
]
|
|
if self._env_file:
|
|
opts += ['--env-file', self._env_file]
|
|
if self._shell:
|
|
opts += ['--interactive', '--tty']
|
|
return opts
|
|
|
|
def _do_abort(self, container_name):
|
|
subprocess.call([self.name, 'kill', container_name])
|
|
|
|
|
|
class DockerRuntime(CommonRuntime):
|
|
"""Run a command in a Docker container"""
|
|
|
|
name = 'docker'
|
|
|
|
def _get_opts(self, container_name):
|
|
return super()._get_opts(container_name) + [
|
|
'--user', f'{self._uid}:{self._gid}'
|
|
]
|
|
|
|
|
|
class PodmanRuntime(CommonRuntime):
|
|
"""Run a command in a Podman container"""
|
|
|
|
name = 'podman'
|
|
|
|
def _get_opts(self, container_name):
|
|
return super()._get_opts(container_name) + [
|
|
'--userns', f'keep-id:uid={self._uid},gid={self._gid}',
|
|
]
|
|
|
|
|
|
class Runtimes:
|
|
"""List of all supported runtimes"""
|
|
|
|
runtimes = [PodmanRuntime, DockerRuntime]
|
|
|
|
@classmethod
|
|
def get_names(cls):
|
|
"""Get a list of all the runtime names"""
|
|
return list(runtime.name for runtime in cls.runtimes)
|
|
|
|
@classmethod
|
|
def get(cls, name):
|
|
"""Get a single runtime class matching the given name"""
|
|
for runtime in cls.runtimes:
|
|
if runtime.name == name:
|
|
if not runtime.is_present():
|
|
raise ValueError(f"runtime not found: {name}")
|
|
return runtime
|
|
raise ValueError(f"unknown runtime: {name}")
|
|
|
|
@classmethod
|
|
def find(cls):
|
|
"""Find the first runtime present on the system"""
|
|
for runtime in cls.runtimes:
|
|
if runtime.is_present():
|
|
return runtime
|
|
raise ValueError("no runtime found")
|
|
|
|
|
|
def _get_logger(verbose):
|
|
"""Set up a logger with the appropriate level"""
|
|
logger = logging.getLogger('container')
|
|
handler = logging.StreamHandler()
|
|
handler.setFormatter(logging.Formatter(
|
|
fmt='[container {levelname}] {message}', style='{'
|
|
))
|
|
logger.addHandler(handler)
|
|
logger.setLevel(logging.DEBUG if verbose is True else logging.INFO)
|
|
return logger
|
|
|
|
|
|
def main(args):
|
|
"""Main entry point for the container tool"""
|
|
logger = _get_logger(args.verbose)
|
|
try:
|
|
cls = Runtimes.get(args.runtime) if args.runtime else Runtimes.find()
|
|
except ValueError as ex:
|
|
logger.error(ex)
|
|
return 1
|
|
logger.debug("runtime: %s", cls.name)
|
|
logger.debug("image: %s", args.image)
|
|
return cls(args, logger).run(args.image, args.cmd)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(
|
|
'container',
|
|
description="See the documentation for more details: "
|
|
"https://docs.kernel.org/dev-tools/container.html"
|
|
)
|
|
parser.add_argument(
|
|
'-e', '--env-file',
|
|
help="Path to an environment file to load in the container."
|
|
)
|
|
parser.add_argument(
|
|
'-g', '--gid',
|
|
help="Group ID to use inside the container."
|
|
)
|
|
parser.add_argument(
|
|
'-i', '--image', required=True,
|
|
help="Container image name."
|
|
)
|
|
parser.add_argument(
|
|
'-r', '--runtime', choices=Runtimes.get_names(),
|
|
help="Container runtime name. If not specified, the first one found "
|
|
"on the system will be used i.e. Podman if present, otherwise Docker."
|
|
)
|
|
parser.add_argument(
|
|
'-s', '--shell', action='store_true',
|
|
help="Run the container in an interactive shell."
|
|
)
|
|
parser.add_argument(
|
|
'-u', '--uid',
|
|
help="User ID to use inside the container. If the -g option is not "
|
|
"specified, the user ID will also be set as the group ID."
|
|
)
|
|
parser.add_argument(
|
|
'-v', '--verbose', action='store_true',
|
|
help="Enable verbose output."
|
|
)
|
|
parser.add_argument(
|
|
'cmd', nargs='+',
|
|
help="Command to run in the container"
|
|
)
|
|
sys.exit(main(parser.parse_args(sys.argv[1:])))
|