From: Daan De Meyer Date: Sun, 16 Jul 2023 20:24:38 +0000 (+0200) Subject: Add scripts support to bwrap() and use it for chrooting X-Git-Tag: v15~76^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6733f5d208030d56abc63d9d14c8b1f8438ff8cc;p=thirdparty%2Fmkosi.git Add scripts support to bwrap() and use it for chrooting The new scripts argument to bwrap() allows providing a mapping of script names to command lines. Each of these becomes a script in a directory that's prepended to PATH before executing the command given to bwrap(). This allows us to make extra commands available to scripts we execute with bwrap(). The first usage of this is to replace the extra argument which is used to provide the extra arguments to chroot into the root directory. Instead, we provide a chroot script and use that in the command line we pass to bwrap() to perform the chroot. --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 1e46c985c..0738d7b76 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -321,20 +321,20 @@ def run_prepare_script(state: MkosiState, build: bool) -> None: if build: with complete_step("Running prepare script in build overlay…"), mount_build_overlay(state): bwrap( - ["/work/prepare", "build"], + ["chroot", "/work/prepare", "build"], tools=state.config.tools_tree, apivfs=state.root, - extra=chroot_cmd(state.root, options=options, network=True), + scripts=dict(chroot=chroot_cmd(state.root, options=options, network=True)), env=dict(SRCDIR="/work/src") | state.environment, ) shutil.rmtree(state.root / "work") else: with complete_step("Running prepare script…"): bwrap( - ["/work/prepare", "final"], + ["chroot", "/work/prepare", "final"], tools=state.config.tools_tree, apivfs=state.root, - extra=chroot_cmd(state.root, options=options, network=True), + scripts=dict(chroot=chroot_cmd(state.root, options=options, network=True)), env=dict(SRCDIR="/work/src") | state.environment, ) shutil.rmtree(state.root / "work") @@ -346,13 +346,15 @@ def run_postinst_script(state: MkosiState) -> None: with complete_step("Running postinstall script…"): bwrap( - ["/work/postinst", "final"], + ["chroot", "/work/postinst", "final"], tools=state.config.tools_tree, apivfs=state.root, - extra=chroot_cmd( - state.root, - options=["--bind", state.config.postinst_script, "/work/postinst"], - network=state.config.with_network, + scripts=dict( + chroot=chroot_cmd( + state.root, + options=["--bind", state.config.postinst_script, "/work/postinst"], + network=state.config.with_network, + ), ), env=state.environment, ) @@ -1598,10 +1600,10 @@ def run_selinux_relabel(state: MkosiState) -> None: with complete_step(f"Relabeling files using {policy} policy"): bwrap( - cmd=["sh", "-c", cmd], + cmd=["chroot", "sh", "-c", cmd], tools=state.config.tools_tree, apivfs=state.root, - extra=chroot_cmd(state.root), + scripts=dict(chroot=chroot_cmd(state.root)), env=state.environment, ) @@ -1901,10 +1903,10 @@ def run_build_script(state: MkosiState) -> None: # build-script output goes to stdout so we can run language servers from within mkosi # build-scripts. See https://github.com/systemd/mkosi/pull/566 for more information. bwrap( - ["/work/build-script"], + ["chroot", "/work/build-script"], tools=state.config.tools_tree, apivfs=state.root, - extra=chroot_cmd(state.root, options=options, network=state.config.with_network), + scripts=dict(chroot=chroot_cmd(state.root, options=options, network=state.config.with_network)), env=env | state.environment, stdout=sys.stdout, ) diff --git a/mkosi/distributions/gentoo.py b/mkosi/distributions/gentoo.py index 7b7fc8389..e5a7c93d3 100644 --- a/mkosi/distributions/gentoo.py +++ b/mkosi/distributions/gentoo.py @@ -25,6 +25,7 @@ def invoke_emerge( ) -> None: bwrap( cmd=[ + "chroot", "emerge", *packages, "--update", @@ -48,15 +49,17 @@ def invoke_emerge( ], tools=state.config.tools_tree, apivfs=state.cache_dir / "stage3", - extra=chroot_cmd( - root=state.cache_dir / "stage3", - options=[ - "--bind", state.root, "/tmp/mkosi-root", - "--bind", state.cache_dir / "binpkgs", "/var/cache/binpkgs", - "--bind", state.cache_dir / "distfiles", "/var/cache/distfiles", - "--bind", state.cache_dir / "repos", "/var/db/repos", - ], - network=True, + scripts=dict( + chroot=chroot_cmd( + root=state.cache_dir / "stage3", + options=[ + "--bind", state.root, "/tmp/mkosi-root", + "--bind", state.cache_dir / "binpkgs", "/var/cache/binpkgs", + "--bind", state.cache_dir / "distfiles", "/var/cache/distfiles", + "--bind", state.cache_dir / "repos", "/var/db/repos", + ], + network=True, + ), ), env=dict( FEATURES=" ".join([ @@ -146,13 +149,15 @@ class GentooInstaller(DistributionInstaller): copy_path(state.pkgmngr, stage3, preserve_owner=False, tools=state.config.tools_tree) bwrap( - cmd=["emerge-webrsync"], + cmd=["chroot", "emerge-webrsync"], tools=state.config.tools_tree, apivfs=stage3, - extra=chroot_cmd( - stage3, - options=["--bind", state.cache_dir / "repos", "/var/db/repos"], - network=True, + scripts=dict( + chroot=chroot_cmd( + stage3, + options=["--bind", state.cache_dir / "repos", "/var/db/repos"], + network=True, + ), ), ) diff --git a/mkosi/install.py b/mkosi/install.py index 57a0bad17..d0a60c317 100644 --- a/mkosi/install.py +++ b/mkosi/install.py @@ -4,17 +4,12 @@ import contextlib import fcntl import importlib.resources import os -import stat from collections.abc import Iterator from pathlib import Path from typing import Optional from mkosi.run import bwrap - - -def make_executable(path: Path) -> None: - st = path.stat() - os.chmod(path, st.st_mode | stat.S_IEXEC) +from mkosi.util import make_executable def write_resource( diff --git a/mkosi/run.py b/mkosi/run.py index d9654e3aa..a1e5291c6 100644 --- a/mkosi/run.py +++ b/mkosi/run.py @@ -10,10 +10,12 @@ import multiprocessing import os import pwd import queue +import shlex import signal import subprocess import sys import tempfile +import textwrap import threading import traceback from pathlib import Path @@ -33,7 +35,7 @@ from typing import ( from mkosi.log import ARG_DEBUG, ARG_DEBUG_SHELL, die from mkosi.types import _FILE, CompletedProcess, PathString, Popen -from mkosi.util import InvokingUser +from mkosi.util import InvokingUser, make_executable CLONE_NEWNS = 0x00020000 CLONE_NEWUSER = 0x10000000 @@ -312,6 +314,7 @@ def bwrap_cmd( *, tools: Optional[Path] = None, apivfs: Optional[Path] = None, + scripts: Mapping[str, Sequence[PathString]] = {}, ) -> Iterator[list[PathString]]: cmdline: list[PathString] = [ "bwrap", @@ -355,7 +358,36 @@ def bwrap_cmd( else: chmod = ":" - with tempfile.TemporaryDirectory(dir="/var/tmp", prefix="mkosi-var-tmp") as var_tmp: + with tempfile.TemporaryDirectory(dir="/var/tmp", prefix="mkosi-var-tmp") as var_tmp,\ + tempfile.TemporaryDirectory(dir="/tmp", prefix="mkosi-scripts") as d: + + for name, cmd in scripts.items(): + # Make sure we don't end up in a recursive loop when we name a script after the binary it execs + # by removing the scripts directory from the PATH when we execute a script. + (Path(d) / name).write_text( + textwrap.dedent( + f"""\ + #!/bin/sh + PATH="$(echo $PATH | tr ':' '\n' | grep -v {Path(d)} | tr '\n' ':')" + export PATH + exec {shlex.join(str(s) for s in cmd)} "$@" + """ + ) + ) + + make_executable(Path(d) / name) + + # We modify the PATH via --setenv so that bwrap itself is looked up in PATH before we change it. + if tools: + # If a tools tree is specified, we should ignore any local modifications made to PATH as any of + # those binaries might not work anymore when /usr is replaced wholesale. We also make sure that + # both /usr/bin and /usr/sbin/ are searched so that e.g. if the host is Arch and the root is + # Debian we don't ignore the binaries from /usr/sbin in the Debian root. We also keep the scripts + # directory in PATH as all of them are interpreted and can't be messed up by replacing /usr. + cmdline += ["--setenv", "PATH", f"{d}:/usr/bin:/usr/sbin"] + else: + cmdline += ["--setenv", "PATH", f"{d}:{os.environ['PATH']}"] + if apivfs: cmdline += [ "--bind", var_tmp, apivfs / "var/tmp", @@ -383,7 +415,7 @@ def bwrap( tools: Optional[Path] = None, apivfs: Optional[Path] = None, log: bool = True, - extra: Sequence[PathString] = (), + scripts: Mapping[str, Sequence[PathString]] = {}, # The following arguments are passed directly to run(). stdin: _FILE = None, stdout: _FILE = None, @@ -392,17 +424,10 @@ def bwrap( check: bool = True, env: Mapping[str, PathString] = {}, ) -> CompletedProcess: - with bwrap_cmd(tools=tools, apivfs=apivfs) as bwrap: - if tools: - # If a tools tree is specified, we should ignore any local modifications made to PATH as any of - # those binaries might not work anymore when /usr is replaced wholesale. We also make sure that - # both /usr/bin and /usr/sbin/ are searched so that e.g. if the host is Arch and the root is - # Debian we don't ignore the binaries from /usr/sbin in the Debian root. - env = dict(PATH="/usr/bin:/usr/sbin") | env - + with bwrap_cmd(tools=tools, apivfs=apivfs, scripts=scripts) as bwrap: try: result = run( - [*bwrap, *extra, *cmd], + [*bwrap, *cmd], text=True, env=env, log=False, @@ -417,7 +442,7 @@ def bwrap( logging.error(f"\"{' '.join(str(s) for s in cmd)}\" returned non-zero exit code {e.returncode}.") if ARG_DEBUG_SHELL.get(): run( - [*bwrap, *extra, "sh"], + [*bwrap, "sh"], stdin=sys.stdin, check=False, env=env, diff --git a/mkosi/util.py b/mkosi/util.py index c15ac9908..55274f939 100644 --- a/mkosi/util.py +++ b/mkosi/util.py @@ -11,6 +11,7 @@ import os import pwd import re import resource +import stat import sys import tempfile from collections.abc import Iterable, Iterator, Sequence @@ -320,3 +321,8 @@ def format_bytes(num_bytes: int) -> str: return f"{num_bytes/1024 :0.1f}K" return f"{num_bytes}B" + + +def make_executable(path: Path) -> None: + st = path.stat() + os.chmod(path, st.st_mode | stat.S_IEXEC)