]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add scripts support to bwrap() and use it for chrooting
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 16 Jul 2023 20:24:38 +0000 (22:24 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 17 Jul 2023 11:05:52 +0000 (13:05 +0200)
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.

mkosi/__init__.py
mkosi/distributions/gentoo.py
mkosi/install.py
mkosi/run.py
mkosi/util.py

index 1e46c985c447ead45f9abc16c5c55ba1474f087b..0738d7b7601b461bbec63fddb060a5b104fa9bcb 100644 (file)
@@ -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,
         )
index 7b7fc8389b586e690f2186ccd505c94aa5cd93dc..e5a7c93d36f71bd78270551122bbb6e772b32337 100644 (file)
@@ -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,
+                ),
             ),
         )
 
index 57a0bad17474779103f81c673f6ea86bf7d35341..d0a60c31711c71bbf385604c0c0eb12f2f846f65 100644 (file)
@@ -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(
index d9654e3aaeccbf3c651e7744c13c337e0cbe35e7..a1e5291c605a2eb8690469cf63fbb12495f81fe8 100644 (file)
@@ -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,
index c15ac9908a5fac8db36aa3b4044300d06862950e..55274f9396af0a12aca05e952839ed79e1cd9a2a 100644 (file)
@@ -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)