]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Rework bwrap run functions
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 21 Apr 2023 20:19:47 +0000 (22:19 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 24 Apr 2023 10:44:50 +0000 (12:44 +0200)
- To split up our dependencies more, we need to make run.py
  independent of MkosiState, so let's do that.
- To get rid of the shell hacks in both functions to chmod /tmp,
  /var/tmp and /dev/shm, let's just mount the relevant files from
  the host which have the right permissions.
- Fixing the above exposed a bug in the logic to set up rpm based
  systems, which all ship a filesystem package that includes
  directories such as /tmp, /proc, /sys, ... which we overmount
  with apivfs or tmpfs filesystems when running rpm, causing errors
  when the filesystem package tries to set up these directories. To
  ensure these directories are created with the permissions from the
  filesystem package, the run_with_apivfs() function is renamed to
  bwrap() and gains an apivfs argument, which takes a path to set
  up apivfs directories in. If not provided, no apivfs is set up.
  This is then used to install the filesystem package without apivfs
  so that the directories can be created with the right permissions.
- The various rpm distributions now install the filesystem package
  instead of the setup package by default, so we can disable apivfs
  properly while filesystem is being installed. system-user-root
  was removed for opensuse because the filesystem package depends on
  it.

mkosi/__init__.py
mkosi/distributions/arch.py
mkosi/distributions/centos.py
mkosi/distributions/debian.py
mkosi/distributions/fedora.py
mkosi/distributions/gentoo.py
mkosi/distributions/mageia.py
mkosi/distributions/openmandriva.py
mkosi/distributions/opensuse.py
mkosi/run.py

index a5283e3a180040992a933ce3cedb676c69822417..0941390a8b975a3a74efd06bb0135fe824fe5c4b 100644 (file)
@@ -426,21 +426,21 @@ def run_prepare_script(state: MkosiState, cached: bool, build: bool) -> None:
     if build:
         with complete_step("Running prepare script in build overlay…"), mount_build_overlay(state):
             run_workspace_command(
-                state,
+                state.root,
                 ["/root/prepare", "build"],
                 network=True,
                 bwrap_params=bwrap,
-                env=dict(SRCDIR="/root/src"),
+                env=dict(SRCDIR="/root/src") | state.environment,
             )
             clean()
     else:
         with complete_step("Running prepare script…"):
             run_workspace_command(
-                state,
+                state.root,
                 ["/root/prepare", "final"],
                 network=True,
                 bwrap_params=bwrap,
-                env=dict(SRCDIR="/root/src"),
+                env=dict(SRCDIR="/root/src") | state.environment,
             )
             clean()
 
@@ -456,8 +456,8 @@ def run_postinst_script(state: MkosiState) -> None:
             "--bind", state.config.postinst_script, "/root/postinst",
         ]
 
-        run_workspace_command(state, ["/root/postinst", "final"], bwrap_params=bwrap,
-                              network=state.config.with_network)
+        run_workspace_command(state.root, ["/root/postinst", "final"], bwrap_params=bwrap,
+                              network=state.config.with_network, env=state.environment)
 
         state.root.joinpath("root/postinst").unlink()
 
@@ -1606,7 +1606,8 @@ def run_kernel_install(state: MkosiState, cached: bool) -> None:
         not state.root.joinpath("usr/lib/kernel/install.d/50-dracut.install").exists() and
         not state.root.joinpath("etc/kernel/install.d/50-dracut.install").exists()):
         with complete_step("Running dpkg-reconfigure dracut…"):
-            run_workspace_command(state, ["dpkg-reconfigure", "dracut"], env=dict(hostonly_l="no"))
+            run_workspace_command(state.root, ["dpkg-reconfigure", "dracut"],
+                                  env=dict(hostonly_l="no") | state.environment)
             return
 
     with complete_step("Running kernel-install…"):
@@ -1617,7 +1618,7 @@ def run_kernel_install(state: MkosiState, cached: bool) -> None:
                 cmd.insert(1, "--verbose")
 
             # Make dracut think --no-host-only was passed via the CLI.
-            run_workspace_command(state, cmd, env=dict(hostonly_l="no"))
+            run_workspace_command(state.root, cmd, env=dict(hostonly_l="no") | state.environment)
 
             if machine_id and (p := state.root / "boot" / machine_id / kver / "initrd").exists():
                 shutil.move(p, state.root / state.installer.initrd_path(kver))
@@ -1661,7 +1662,7 @@ def run_selinux_relabel(state: MkosiState) -> None:
     cmd = f"mkdir /tmp/relabel && mount --bind / /tmp/relabel && exec setfiles -m -r /tmp/relabel -F {fc} /tmp/relabel || exit $?"
 
     with complete_step(f"Relabeling files using {policy} policy"):
-        run_workspace_command(state, ["sh", "-c", cmd])
+        run_workspace_command(state.root, ["sh", "-c", cmd], env=state.environment)
 
 
 def reuse_cache_tree(state: MkosiState) -> bool:
@@ -1866,8 +1867,8 @@ 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.
-        run_workspace_command(state, cmd, network=state.config.with_network, bwrap_params=bwrap,
-                              stdout=sys.stdout, env=env)
+        run_workspace_command(state.root, cmd, network=state.config.with_network, bwrap_params=bwrap,
+                              stdout=sys.stdout, env=env | state.environment)
 
 
 def need_cache_tree(state: MkosiState) -> bool:
index 5a6ddb56b6d12f230bb1a9e8434ed8cb12a1b377..f9bfe4323857661eda53d2dd84fdf01fa96bc969 100644 (file)
@@ -4,7 +4,7 @@ from collections.abc import Sequence
 from textwrap import dedent
 
 from mkosi.distributions import DistributionInstaller
-from mkosi.run import run_with_apivfs
+from mkosi.run import bwrap
 from mkosi.types import PathString
 from mkosi.util import MkosiState, sort_packages
 
@@ -96,4 +96,4 @@ def invoke_pacman(state: MkosiState, packages: Sequence[str]) -> None:
     if state.config.initrds:
         cmdline += ["--assume-installed", "initramfs"]
 
-    run_with_apivfs(state, cmdline, env=dict(KERNEL_INSTALL_BYPASS="1"))
+    bwrap(cmdline, apivfs=state.root, env=dict(KERNEL_INSTALL_BYPASS="1") | state.environment)
index 2c784a19403d444df4643db01d7f01e5aa8ca08b..4962965cb7cdec763ed2f8018bdceabdea39cc41 100644 (file)
@@ -76,14 +76,14 @@ class CentosInstaller(DistributionInstaller):
 
     @classmethod
     def install(cls, state: MkosiState) -> None:
-        cls.install_packages(state, ["setup"])
+        cls.install_packages(state, ["filesystem"], apivfs=False)
 
         # On Fedora, the default rpmdb has moved to /usr/lib/sysimage/rpm so if that's the case we need to
         # move it back to /var/lib/rpm on CentOS.
         move_rpm_db(state.root)
 
     @classmethod
-    def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
+    def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None:
         release = int(state.config.release)
 
         if release <= 7:
@@ -100,7 +100,7 @@ class CentosInstaller(DistributionInstaller):
         else:
             env = {}
 
-        invoke_dnf(state, "install", packages, env)
+        invoke_dnf(state, "install", packages, env, apivfs=apivfs)
 
     @classmethod
     def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
index 133ae18ecaf445ef123610a0b4c4aab7fdc5bea0..62d3a53b83c85307d18a1a2b37cedfe942374de1 100644 (file)
@@ -7,7 +7,7 @@ from pathlib import Path
 from textwrap import dedent
 
 from mkosi.distributions import DistributionInstaller
-from mkosi.run import run, run_with_apivfs
+from mkosi.run import bwrap, run
 from mkosi.types import CompletedProcess, PathString
 from mkosi.util import MkosiState
 
@@ -220,4 +220,4 @@ def invoke_apt(
         INITRD="No",
     )
 
-    return run_with_apivfs(state, ["apt-get", operation, *extra], env=env)
+    return bwrap(["apt-get", operation, *extra], apivfs=state.root, env=env | state.environment)
index 2b1543b3d90c7181496a03a2dcffc017e506943b..ebb13ec7f59e1a36262f65f9d359b28a6d89f387 100644 (file)
@@ -12,7 +12,7 @@ from typing import Any, NamedTuple, Optional
 
 from mkosi.distributions import DistributionInstaller
 from mkosi.remove import unlink_try_hard
-from mkosi.run import run_with_apivfs
+from mkosi.run import bwrap
 from mkosi.util import Distribution, MkosiState, detect_distribution, sort_packages
 
 
@@ -23,10 +23,10 @@ class FedoraInstaller(DistributionInstaller):
 
     @classmethod
     def install(cls, state: MkosiState) -> None:
-        cls.install_packages(state, ["setup"])
+        cls.install_packages(state, ["filesystem"], apivfs=False)
 
     @classmethod
-    def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
+    def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None:
         release, releasever = parse_fedora_release(state.config.release)
 
         if state.config.local_mirror:
@@ -60,7 +60,7 @@ class FedoraInstaller(DistributionInstaller):
             repos += [Repo("updates", updates_url, gpgpath, gpgurl)]
 
         setup_dnf(state, repos)
-        invoke_dnf(state, "install", packages)
+        invoke_dnf(state, "install", packages, apivfs=apivfs)
 
     @classmethod
     def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
@@ -123,7 +123,13 @@ def setup_dnf(state: MkosiState, repos: Sequence[Repo] = ()) -> None:
             )
 
 
-def invoke_dnf(state: MkosiState, command: str, packages: Iterable[str], env: Mapping[str, Any] = {}) -> None:
+def invoke_dnf(
+    state: MkosiState,
+    command: str,
+    packages: Iterable[str],
+    env: Mapping[str, Any] = {},
+    apivfs: bool = True
+) -> None:
     if state.config.distribution == Distribution.fedora:
         release, _ = parse_fedora_release(state.config.release)
     else:
@@ -167,7 +173,8 @@ def invoke_dnf(state: MkosiState, command: str, packages: Iterable[str], env: Ma
 
     cmdline += sort_packages(packages)
 
-    run_with_apivfs(state, cmdline, env=dict(KERNEL_INSTALL_BYPASS="1") | env)
+    bwrap(cmdline, apivfs=state.root if apivfs else None,
+          env=dict(KERNEL_INSTALL_BYPASS="1") | env | state.environment)
 
     distribution, _ = detect_distribution()
     if distribution not in (Distribution.debian, Distribution.ubuntu):
index e3261cad66d004f8e50e657e137bfc9370e003af..eb9d20db692859bf4aa1e7dd2fb65d5866d69df1 100644 (file)
@@ -46,7 +46,7 @@ def invoke_emerge(
     else:
         emerge_default_opts += ["--quiet-build", "--quiet"]
     cmd = ["emerge", *pkgs, *emerge_default_opts, *opts, *actions]
-    run_workspace_command(state, cmd, network=True)
+    run_workspace_command(state.root, cmd, network=True, env=state.environment)
 
 
 class Gentoo:
@@ -298,7 +298,8 @@ class Gentoo:
         )
 
     def get_snapshot_of_portage_tree(self) -> None:
-        run_workspace_command(self.state, ["/usr/bin/emerge-webrsync"], network=True)
+        run_workspace_command(self.state.root, ["/usr/bin/emerge-webrsync"], network=True,
+                              env=self.state.environment)
 
     def update_stage3(self) -> None:
         invoke_emerge(self.state, opts=self.EMERGE_UPDATE_OPTS, pkgs=self.pkgs['boot'])
index cc8f13317224b27bd715621814e35f31a4953cc5..140ca97c4dfad86997fc17cf5cd0485322aa35ca 100644 (file)
@@ -15,10 +15,10 @@ class MageiaInstaller(DistributionInstaller):
 
     @classmethod
     def install(cls, state: MkosiState) -> None:
-        cls.install_packages(state, ["setup"])
+        cls.install_packages(state, ["filesystem"], apivfs=False)
 
     @classmethod
-    def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
+    def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None:
         release = state.config.release.strip("'")
 
         if state.config.local_mirror:
@@ -46,7 +46,7 @@ class MageiaInstaller(DistributionInstaller):
             repos += [Repo(f"mageia-{release}-updates", updates_url, gpgpath)]
 
         setup_dnf(state, repos)
-        invoke_dnf(state, "install", packages)
+        invoke_dnf(state, "install", packages, apivfs=apivfs)
 
     @classmethod
     def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
index 01e08d4f0ded8796b07c94af8f198eb42e826ac1..8355b8771e696410ffe9d355e640681f976d046a 100644 (file)
@@ -15,10 +15,10 @@ class OpenmandrivaInstaller(DistributionInstaller):
 
     @classmethod
     def install(cls, state: MkosiState) -> None:
-        cls.install_packages(state, ["setup"])
+        cls.install_packages(state, ["filesystem"])
 
     @classmethod
-    def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
+    def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None:
         release = state.config.release.strip("'")
 
         if release[0].isdigit():
@@ -47,7 +47,7 @@ class OpenmandrivaInstaller(DistributionInstaller):
             repos += [Repo("updates", updates_url, gpgpath)]
 
         setup_dnf(state, repos)
-        invoke_dnf(state, "install", packages)
+        invoke_dnf(state, "install", packages, apivfs=apivfs)
 
     @classmethod
     def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
index 96f914963f2edd7ff8cccbb7ab68ab7769fb6902..dd56d3ac8028a03232deeccc0a46f4cde3240cff 100644 (file)
@@ -5,7 +5,7 @@ from pathlib import Path
 from textwrap import dedent
 
 from mkosi.distributions import DistributionInstaller
-from mkosi.run import run_with_apivfs
+from mkosi.run import bwrap
 from mkosi.types import PathString
 from mkosi.util import MkosiState
 
@@ -17,10 +17,10 @@ class OpensuseInstaller(DistributionInstaller):
 
     @classmethod
     def install(cls, state: MkosiState) -> None:
-        cls.install_packages(state, ["filesystem", "system-user-root"])
+        cls.install_packages(state, ["filesystem"], apivfs=False)
 
     @classmethod
-    def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
+    def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None:
         release = state.config.release
         if release == "leap":
             release = "stable"
@@ -45,7 +45,7 @@ class OpensuseInstaller(DistributionInstaller):
             repos += [("repo-update", updates_url)]
 
         setup_zypper(state, repos)
-        invoke_zypper(state, "install", ["-y", "--download-in-advance", "--no-recommends"], packages)
+        invoke_zypper(state, "install", ["-y", "--download-in-advance", "--no-recommends"], packages, apivfs=apivfs)
 
     @classmethod
     def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
@@ -85,7 +85,13 @@ def setup_zypper(state: MkosiState, repos: Sequence[tuple[str, str]] = ()) -> No
             )
 
 
-def invoke_zypper(state: MkosiState, verb: str, options: Sequence[str], packages: Sequence[str]) -> None:
+def invoke_zypper(
+    state: MkosiState,
+    verb: str,
+    options: Sequence[str],
+    packages: Sequence[str],
+    apivfs: bool = True
+) -> None:
     cmdline: list[PathString] = [
         "zypper",
         "--root", state.root,
@@ -98,5 +104,7 @@ def invoke_zypper(state: MkosiState, verb: str, options: Sequence[str], packages
         *packages,
     ]
 
-    run_with_apivfs(state, cmdline,
-                    env=dict(ZYPP_CONF=str(state.workspace / "zypp.conf"), KERNEL_INSTALL_BYPASS="1"))
+    env = dict(ZYPP_CONF=str(state.workspace / "zypp.conf"), KERNEL_INSTALL_BYPASS="1") | state.environment
+
+    bwrap(cmdline, apivfs=state.root if apivfs else None, env=env)
+
index 11c464b89c06451bd81c3a308de8fc5dfd1515d4..eca150f85a78add0b0118db547d59f3d240cb149 100644 (file)
@@ -9,6 +9,7 @@ import shutil
 import signal
 import subprocess
 import sys
+import tempfile
 import traceback
 from pathlib import Path
 from types import TracebackType
@@ -16,7 +17,7 @@ from typing import Any, Callable, Mapping, Optional, Sequence, Type, TypeVar
 
 from mkosi.log import ARG_DEBUG, die
 from mkosi.types import _FILE, CompletedProcess, PathString, Popen
-from mkosi.util import MkosiState, current_user
+from mkosi.util import current_user
 
 CLONE_NEWNS = 0x00020000
 CLONE_NEWUSER = 0x10000000
@@ -219,9 +220,6 @@ def run(
         LANG="C.UTF-8",
     ) | env
 
-    if env["PATH"] == "":
-        del env["PATH"]
-
     if "run" in ARG_DEBUG:
         env["SYSTEMD_LOG_LEVEL"] = "debug"
 
@@ -270,10 +268,10 @@ def spawn(
         die(f'"{shlex.join(str(s) for s in cmdline)}" returned non-zero exit code {e.returncode}.', e)
 
 
-def run_with_apivfs(
-    state: MkosiState,
+def bwrap(
     cmd: Sequence[PathString],
-    bwrap_params: Sequence[PathString] = tuple(),
+    *,
+    apivfs: Optional[Path] = None,
     stdout: _FILE = None,
     env: Mapping[str, PathString] = {},
 ) -> CompletedProcess:
@@ -283,45 +281,41 @@ def run_with_apivfs(
         "--unshare-pid",
         "--dev-bind", "/", "/",
         "--chdir", Path.cwd(),
-        "--tmpfs", state.root / "run",
-        "--tmpfs", state.root / "tmp",
-        "--proc", state.root / "proc",
-        "--dev", state.root / "dev",
-        "--ro-bind", "/sys", state.root / "sys",
-        "--bind", state.var_tmp, state.root / "var/tmp",
         "--die-with-parent",
     ]
 
-    # If passwd or a related file exists in the root directory, bind mount it over the host files while we
-    # run the command, to make sure that the command we run uses user/group information from the root instead
-    # of from the host. If the file doesn't exist yet, mount over /dev/null instead.
-    for f in ("passwd", "group", "shadow", "gshadow"):
-        p = state.root / "etc" / f
-        if p.exists():
-            cmdline += ["--bind", p, f"/etc/{f}"]
-        else:
-            cmdline += ["--bind", "/dev/null", f"/etc/{f}"]
-
-    cmdline += [
-        *bwrap_params,
-        "sh", "-c",
-    ]
-
-    env = env | state.environment
+    if apivfs:
+        cmdline += [
+            "--tmpfs", apivfs / "run",
+            "--proc", apivfs / "proc",
+            "--dev", apivfs / "dev",
+            "--ro-bind", "/sys", apivfs / "sys",
+            "--bind", "/tmp", apivfs / "tmp",
+            "--bind", "/var/tmp", apivfs / "var/tmp",
+            "--bind", "/dev/shm", apivfs / "dev/shm",
+        ]
 
-    template = f"chmod 1777 {state.root / 'tmp'} {state.root / 'var/tmp'} {state.root / 'dev/shm'} && exec {{}} || exit $?"
+        # If passwd or a related file exists in the apivfs directory, bind mount it over the host files while
+        # we run the command, to make sure that the command we run uses user/group information from the
+        # apivfs directory instead of from the host. If the file doesn't exist yet, mount over /dev/null
+        # instead.
+        for f in ("passwd", "group", "shadow", "gshadow"):
+            p = apivfs / "etc" / f
+            if p.exists():
+                cmdline += ["--bind", p, f"/etc/{f}"]
+            else:
+                cmdline += ["--bind", "/dev/null", f"/etc/{f}"]
 
     try:
-        return run([*cmdline, template.format(shlex.join(str(s) for s in cmd))],
-                   text=True, stdout=stdout, env=env, log=False)
+        return run([*cmdline, *cmd], text=True, stdout=stdout, env=env, log=False)
     except subprocess.CalledProcessError as e:
         if "run" in ARG_DEBUG:
-            run([*cmdline, template.format("sh")], check=False, env=env, log=False)
+            run([*cmdline, "sh"], stdin=sys.stdin, check=False, env=env, log=False)
         die(f'"{shlex.join(str(s) for s in cmd)}" returned non-zero exit code {e.returncode}.')
 
 
 def run_workspace_command(
-    state: MkosiState,
+    root: Path,
     cmd: Sequence[PathString],
     bwrap_params: Sequence[PathString] = tuple(),
     network: bool = False,
@@ -333,50 +327,47 @@ def run_workspace_command(
         "--unshare-ipc",
         "--unshare-pid",
         "--unshare-cgroup",
-        "--bind", state.root, "/",
+        "--bind", root, "/",
         "--tmpfs", "/run",
-        "--tmpfs", "/tmp",
         "--dev", "/dev",
         "--proc", "/proc",
         "--ro-bind", "/sys", "/sys",
-        "--bind", state.var_tmp, "/var/tmp",
+        "--bind", "/tmp", "/tmp",
+        "--bind", "/var/tmp", "/var/tmp",
+        "--bind", "/dev/shm", "/dev/shm",
         "--die-with-parent",
         *bwrap_params,
     ]
 
-    resolve = state.root.joinpath("etc/resolv.conf")
+    resolve = root.joinpath("etc/resolv.conf")
+
+    tmp = Path(tempfile.NamedTemporaryFile(delete=False).name)
 
     if network:
         # Bubblewrap does not mount over symlinks and /etc/resolv.conf might be a symlink. Deal with this by
         # temporarily moving the file somewhere else.
         if resolve.is_symlink():
-            shutil.move(resolve, state.workspace / "resolv.conf")
+            shutil.move(resolve, tmp)
 
         # If we're using the host network namespace, use the same resolver
         cmdline += ["--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf"]
     else:
         cmdline += ["--unshare-net"]
 
-    cmdline += ["sh", "-c"]
-
     env = dict(
         container="mkosi",
         SYSTEMD_OFFLINE=str(int(network)),
         HOME="/",
-        # Make sure the default PATH of the distro shell is used.
-        PATH="",
-    ) | env | state.environment
-
-    template = "chmod 1777 /tmp /var/tmp /dev/shm && PATH=$PATH:/usr/bin:/usr/sbin exec {} || exit $?"
+        PATH="/usr/bin:/usr/sbin",
+    ) | env
 
     try:
-        return run([*cmdline, template.format(shlex.join(str(s) for s in cmd))],
-                   text=True, stdout=stdout, env=env, log=False)
+        return run([*cmdline, *cmd], text=True, stdout=stdout, env=env, log=False)
     except subprocess.CalledProcessError as e:
         if "run" in ARG_DEBUG:
-            run([*cmdline, template.format("sh")], check=False, env=env, log=False)
+            run([*cmdline, "sh"], stdin=sys.stdin, check=False, env=env, log=False)
         die(f'"{shlex.join(str(s) for s in cmd)}" returned non-zero exit code {e.returncode}.')
     finally:
-        if state.workspace.joinpath("resolv.conf").is_symlink():
+        if tmp.is_symlink():
             resolve.unlink(missing_ok=True)
-            shutil.move(state.workspace.joinpath("resolv.conf"), resolve)
+            shutil.move(tmp, resolve)