]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add git script to set safe.directory=* whenever git is executed
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 19 Oct 2023 11:57:11 +0000 (13:57 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 19 Oct 2023 13:13:59 +0000 (15:13 +0200)
Instead of requiring every git command to be executed as the user
invoking mkosi, let's add a git script that appends
"-c safe.directory=*" to disable the git safe directory check. This
also reworks the script machinery to support scripts both inside and
outside of the chroot since the git script needs to be available in both.

mkosi/__init__.py
mkosi/run.py

index 5b2d7677879190e745b3b9e5f90ac9685ab12838..dc5e29a0ce001868e906c3cc34c8d14316c2e197 100644 (file)
@@ -10,15 +10,16 @@ import json
 import logging
 import os
 import resource
+import shlex
 import shutil
 import subprocess
 import sys
 import tempfile
 import textwrap
 import uuid
-from collections.abc import Iterator, Sequence
+from collections.abc import Iterator, Mapping, Sequence
 from pathlib import Path
-from typing import Optional, TextIO, Union
+from typing import ContextManager, Optional, TextIO, Union
 
 from mkosi.archive import extract_tar, make_cpio, make_tar
 from mkosi.config import (
@@ -63,6 +64,7 @@ from mkosi.util import (
     chdir,
     flatten,
     format_rlimit,
+    make_executable,
     one_zero,
     scopedenv,
     try_import,
@@ -325,6 +327,39 @@ def script_maybe_chroot(script: Path, mountpoint: str) -> list[str]:
     return ["mkosi-chroot", mountpoint] if script.suffix == ".chroot" else [os.fspath(script)]
 
 
+@contextlib.contextmanager
+def finalize_scripts(scripts: Mapping[str, Sequence[PathString]] = {}) -> Iterator[Path]:
+    with tempfile.TemporaryDirectory(prefix="mkosi-scripts") as d:
+        for name, script 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
+                    DIR="$(cd "$(dirname "$0")" && pwd)"
+                    PATH="$(echo "$PATH" | tr ':' '\\n' | grep -v "$DIR" | tr '\\n' ':')"
+                    export PATH
+                    exec {shlex.join(str(s) for s in script)} "$@"
+                    """
+                )
+            )
+
+            make_executable(Path(d) / name)
+
+        yield Path(d)
+
+
+def finalize_host_scripts(state: MkosiState, chroot: Sequence[PathString]) -> ContextManager[Path]:
+    git = {"git": ("git", "-c", "safe.directory=*")} if find_binary("git") else {}
+    return finalize_scripts(git | {"mkosi-chroot": chroot} | package_manager_scripts(state))
+
+
+def finalize_chroot_scripts(state: MkosiState) -> ContextManager[Path]:
+    git = {"git": ("git", "-c", "safe.directory=*")} if find_binary("git", state.root) else {}
+    return finalize_scripts(git)
+
+
 def run_prepare_scripts(state: MkosiState, build: bool) -> None:
     if not state.config.prepare_scripts:
         return
@@ -353,25 +388,30 @@ def run_prepare_scripts(state: MkosiState, build: bool) -> None:
             step_msg = "Running prepare script {}…"
             arg = "final"
 
+        d = stack.enter_context(finalize_chroot_scripts(state))
+
         for script in state.config.prepare_scripts:
             chroot: list[PathString] = chroot_cmd(
                 state.root,
                 options=[
                     "--bind", script, "/work/prepare",
                     "--bind", Path.cwd(), "/work/src",
+                    "--bind", d, "/work/scripts",
                     "--chdir", "/work/src",
                     "--setenv", "SRCDIR", "/work/src",
                     "--setenv", "BUILDROOT", "/",
                 ],
             )
 
+            d = stack.enter_context(finalize_host_scripts(state, chroot))
+
             with complete_step(step_msg.format(script)):
                 bwrap(
                     script_maybe_chroot(script, "/work/prepare") + [arg],
                     network=True,
                     readonly=True,
                     options=finalize_mounts(state.config),
-                    scripts={"mkosi-chroot": chroot} | package_manager_scripts(state),
+                    scripts=d,
                     env=env | state.config.environment,
                     stdin=sys.stdin,
                 )
@@ -407,7 +447,8 @@ def run_build_scripts(state: MkosiState) -> None:
     with (
         mount_build_overlay(state),\
         mount_passwd(state.root),\
-        mount_volatile_overlay(state)\
+        mount_volatile_overlay(state),\
+        finalize_chroot_scripts(state) as d\
     ):
         for script in state.config.build_scripts:
             chroot = chroot_cmd(
@@ -417,6 +458,7 @@ def run_build_scripts(state: MkosiState) -> None:
                     "--bind", state.install_dir, "/work/dest",
                     "--bind", state.staging, "/work/out",
                     "--bind", Path.cwd(), "/work/src",
+                    "--bind", d, "/work/scripts",
                     *(["--bind", str(state.config.build_dir), "/work/build"] if state.config.build_dir else []),
                     "--chdir", "/work/src",
                     "--setenv", "SRCDIR", "/work/src",
@@ -429,13 +471,16 @@ def run_build_scripts(state: MkosiState) -> None:
 
             cmdline = state.args.cmdline if state.args.verb == Verb.build else []
 
-            with complete_step(f"Running build script {script}…"):
+            with (
+                finalize_host_scripts(state, chroot) as d,\
+                complete_step(f"Running build script {script}…")\
+            ):
                 bwrap(
                     script_maybe_chroot(script, "/work/build-script") + cmdline,
                     network=state.config.with_network,
                     readonly=True,
                     options=finalize_mounts(state.config),
-                    scripts={"mkosi-chroot": chroot} | package_manager_scripts(state),
+                    scripts=d,
                     env=env | state.config.environment,
                     stdin=sys.stdin,
                 )
@@ -458,30 +503,35 @@ def run_postinst_scripts(state: MkosiState) -> None:
     )
 
     for script in state.config.postinst_scripts:
-        chroot = chroot_cmd(
-            state.root,
-            options=[
-                "--bind", script, "/work/postinst",
-                "--bind", state.staging, "/work/out",
-                "--bind", Path.cwd(), "/work/src",
-                "--chdir", "/work/src",
-                "--setenv", "SRCDIR", "/work/src",
-                "--setenv", "OUTPUTDIR", "/work/out",
-                "--setenv", "BUILDROOT", "/",
-            ],
-        )
-
-        with complete_step(f"Running postinstall script {script}…"):
-            bwrap(
-                script_maybe_chroot(script, "/work/postinst") + ["final"],
-                network=state.config.with_network,
-                readonly=True,
-                options=finalize_mounts(state.config),
-                scripts={"mkosi-chroot": chroot} | package_manager_scripts(state),
-                env=env | state.config.environment,
-                stdin=sys.stdin,
+        with finalize_chroot_scripts(state) as d:
+            chroot = chroot_cmd(
+                state.root,
+                options=[
+                    "--bind", script, "/work/postinst",
+                    "--bind", state.staging, "/work/out",
+                    "--bind", Path.cwd(), "/work/src",
+                    "--bind", d, "/work/scripts",
+                    "--chdir", "/work/src",
+                    "--setenv", "SRCDIR", "/work/src",
+                    "--setenv", "OUTPUTDIR", "/work/out",
+                    "--setenv", "BUILDROOT", "/",
+                ],
             )
 
+            with (
+                finalize_host_scripts(state, chroot) as d,\
+                complete_step(f"Running postinstall script {script}…")\
+            ):
+                bwrap(
+                    script_maybe_chroot(script, "/work/postinst") + ["final"],
+                    network=state.config.with_network,
+                    readonly=True,
+                    options=finalize_mounts(state.config),
+                    scripts=d,
+                    env=env | state.config.environment,
+                    stdin=sys.stdin,
+                )
+
 
 def run_finalize_scripts(state: MkosiState) -> None:
     if not state.config.finalize_scripts:
@@ -500,30 +550,35 @@ def run_finalize_scripts(state: MkosiState) -> None:
     )
 
     for script in state.config.finalize_scripts:
-        chroot = chroot_cmd(
-            state.root,
-            options=[
-                "--bind", script, "/work/finalize",
-                "--bind", state.staging, "/work/out",
-                "--bind", Path.cwd(), "/work/src",
-                "--chdir", "/work/src",
-                "--setenv", "SRCDIR", "/work/src",
-                "--setenv", "OUTPUTDIR", "/work/out",
-                "--setenv", "BUILDROOT", "/",
-            ],
-        )
-
-        with complete_step(f"Running finalize script {script}…"):
-            bwrap(
-                script_maybe_chroot(script, "/work/finalize"),
-                network=state.config.with_network,
-                readonly=True,
-                options=finalize_mounts(state.config),
-                scripts={"mkosi-chroot": chroot} | package_manager_scripts(state),
-                env=env | state.config.environment,
-                stdin=sys.stdin,
+        with finalize_chroot_scripts(state) as d:
+            chroot = chroot_cmd(
+                state.root,
+                options=[
+                    "--bind", script, "/work/finalize",
+                    "--bind", state.staging, "/work/out",
+                    "--bind", Path.cwd(), "/work/src",
+                    "--bind", d, "/work/scripts",
+                    "--chdir", "/work/src",
+                    "--setenv", "SRCDIR", "/work/src",
+                    "--setenv", "OUTPUTDIR", "/work/out",
+                    "--setenv", "BUILDROOT", "/",
+                ],
             )
 
+            with (
+                finalize_host_scripts(state, chroot) as d,\
+                complete_step(f"Running finalize script {script}…")\
+            ):
+                bwrap(
+                    script_maybe_chroot(script, "/work/finalize"),
+                    network=state.config.with_network,
+                    readonly=True,
+                    options=finalize_mounts(state.config),
+                    scripts=d,
+                    env=env | state.config.environment,
+                    stdin=sys.stdin,
+                )
+
 
 def run_openssl(args: Sequence[PathString], stdout: _FILE = None) -> CompletedProcess:
     with tempfile.NamedTemporaryFile(prefix="mkosi-openssl.cnf") as config:
index 16f51f00d4e070193108c2a17a87fee23412c808..cc408d38cd28078c48582a42524f98864c94421b 100644 (file)
@@ -16,8 +16,6 @@ import shutil
 import signal
 import subprocess
 import sys
-import tempfile
-import textwrap
 import threading
 from collections.abc import Awaitable, Collection, Iterator, Mapping, Sequence
 from pathlib import Path
@@ -26,7 +24,7 @@ from typing import Any, Optional
 
 from mkosi.log import ARG_DEBUG, ARG_DEBUG_SHELL, die
 from mkosi.types import _FILE, CompletedProcess, PathString, Popen
-from mkosi.util import INVOKING_USER, flock, make_executable, one_zero
+from mkosi.util import INVOKING_USER, flock, one_zero
 
 CLONE_NEWNS = 0x00020000
 CLONE_NEWUSER = 0x10000000
@@ -303,7 +301,7 @@ def bwrap(
     readonly: bool = False,
     options: Sequence[PathString] = (),
     log: bool = True,
-    scripts: Mapping[str, Sequence[PathString]] = {},
+    scripts: Optional[Path] = None,
     env: Mapping[str, str] = {},
     stdin: _FILE = None,
     stdout: _FILE = None,
@@ -339,44 +337,26 @@ def bwrap(
         "--setenv", "SYSTEMD_OFFLINE", one_zero(network),
     ]
 
-    with tempfile.TemporaryDirectory(prefix="mkosi-scripts") as d:
-
-        for name, script 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 script)} "$@"
-                    """
-                )
-            )
-
-            make_executable(Path(d) / name)
-
-        cmdline += [
-            "--setenv", "PATH", f"{d}:{os.environ['PATH']}",
-            *options,
-            "sh", "-c", "chmod 1777 /dev/shm && exec $0 \"$@\"",
-        ]
+    cmdline += [
+        "--setenv", "PATH", f"{scripts or ''}:{os.environ['PATH']}",
+        *options,
+        "sh", "-c", "chmod 1777 /dev/shm && exec $0 \"$@\"",
+    ]
 
-        if setpgid := find_binary("setpgid"):
-            cmdline += [setpgid, "--foreground", "--"]
+    if setpgid := find_binary("setpgid"):
+        cmdline += [setpgid, "--foreground", "--"]
 
-        try:
-            result = run([*cmdline, *cmd], env=env, log=False, stdin=stdin, stdout=stdout, input=input)
-        except subprocess.CalledProcessError as e:
-            if log:
-                c = shlex.join(os.fspath(s) for s in cmd)
-                logging.error(f"\"{c}\" returned non-zero exit code {e.returncode}.")
-            if ARG_DEBUG_SHELL.get():
-                run([*cmdline, "sh"], stdin=sys.stdin, check=False, env=env, log=False)
-            raise e
+    try:
+        result = run([*cmdline, *cmd], env=env, log=False, stdin=stdin, stdout=stdout, input=input)
+    except subprocess.CalledProcessError as e:
+        if log:
+            c = shlex.join(os.fspath(s) for s in cmd)
+            logging.error(f"\"{c}\" returned non-zero exit code {e.returncode}.")
+        if ARG_DEBUG_SHELL.get():
+            run([*cmdline, "sh"], stdin=sys.stdin, check=False, env=env, log=False)
+        raise e
 
-        return result
+    return result
 
 
 def finalize_passwd_mounts(root: Path) -> list[PathString]:
@@ -442,7 +422,7 @@ def chroot_cmd(root: Path, *, options: Sequence[PathString] = ()) -> list[PathSt
         "--dev-bind", root, "/",
         "--setenv", "container", "mkosi",
         "--setenv", "HOME", "/",
-        "--setenv", "PATH", "/usr/bin:/usr/sbin",
+        "--setenv", "PATH", "/work/scripts:/usr/bin:/usr/sbin",
     ]
 
     resolve = Path("etc/resolv.conf")