become_root,
bwrap,
bwrap_cmd,
+ chroot_cmd,
fork_and_wait,
run,
- run_workspace_command,
spawn,
)
from mkosi.state import MkosiState
if build and state.config.build_script is None:
return
- bwrap: list[PathString] = [
+ options: list[PathString] = [
"--bind", state.config.prepare_script, "/work/prepare",
"--chdir", "/work/src",
]
for src, target in finalize_sources(state.config):
- bwrap += ["--bind", src, Path("/") / target]
+ options += ["--bind", src, Path("/") / target]
if build:
with complete_step("Running prepare script in build overlay…"), mount_build_overlay(state):
- run_workspace_command(
- state.root,
+ bwrap(
["/work/prepare", "build"],
- network=True,
- bwrap_params=bwrap,
+ tools=state.config.tools_tree,
+ apivfs=state.root,
+ extra=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…"):
- run_workspace_command(
- state.root,
+ bwrap(
["/work/prepare", "final"],
- network=True,
- bwrap_params=bwrap,
+ tools=state.config.tools_tree,
+ apivfs=state.root,
+ extra=chroot_cmd(state.root, options=options, network=True),
env=dict(SRCDIR="/work/src") | state.environment,
)
shutil.rmtree(state.root / "work")
return
with complete_step("Running postinstall script…"):
- bwrap: list[PathString] = [
- "--bind", state.config.postinst_script, "/work/postinst",
- ]
-
- run_workspace_command(state.root, ["/work/postinst", "final"], bwrap_params=bwrap,
- network=state.config.with_network, env=state.environment)
+ bwrap(
+ ["/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,
+ ),
+ env=state.environment,
+ )
shutil.rmtree(state.root / "work")
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.root, ["sh", "-c", cmd], env=state.environment)
+ bwrap(
+ cmd=["sh", "-c", cmd],
+ tools=state.config.tools_tree,
+ apivfs=state.root,
+ extra=chroot_cmd(state.root),
+ env=state.environment,
+ )
def need_build_packages(config: MkosiConfig) -> bool:
state.root.joinpath(target).mkdir(mode=0o755, exist_ok=True, parents=True)
with complete_step("Running build script…"), mount_build_overlay(state, read_only=True):
- bwrap: list[PathString] = [
+ options: list[PathString] = [
"--bind", state.config.build_script, "/work/build-script",
"--bind", state.install_dir, "/work/dest",
"--bind", state.staging, "/work/out",
]
for src, target in finalize_sources(state.config):
- bwrap += ["--bind", src, Path("/") / target]
+ options += ["--bind", src, Path("/") / target]
env = dict(
WITH_DOCS=one_zero(state.config.with_docs),
)
if state.config.build_dir is not None:
- bwrap += ["--bind", state.config.build_dir, "/work/build"]
+ options += ["--bind", state.config.build_dir, "/work/build"]
env |= dict(BUILDDIR="/work/build")
# 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.root, ["/work/build-script"], network=state.config.with_network,
- bwrap_params=bwrap, stdout=sys.stdout, env=env | state.environment)
+ bwrap(
+ ["/work/build-script"],
+ tools=state.config.tools_tree,
+ apivfs=state.root,
+ extra=chroot_cmd(state.root, options=options, network=state.config.with_network),
+ env=env | state.environment,
+ stdout=sys.stdout,
+ )
def setfacl(config: MkosiConfig, root: Path, uid: int, allow: bool) -> None:
from mkosi.install import copy_path
from mkosi.log import ARG_DEBUG, complete_step, die
from mkosi.remove import unlink_try_hard
-from mkosi.run import bwrap, run_workspace_command
+from mkosi.run import bwrap, chroot_cmd
from mkosi.state import MkosiState
from mkosi.types import PathString
options: Sequence[str] = (),
env: Mapping[str, str] = {},
) -> None:
- run_workspace_command(
- state.cache_dir / "stage3",
+ bwrap(
cmd=[
"emerge",
*packages,
*(["--verbose", "--quiet=n", "--quiet-fail=n"] if ARG_DEBUG.get() else ["--quiet-build", "--quiet"]),
*options,
],
- bwrap_params=[
- "--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,
+ 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,
+ ),
env=dict(
FEATURES=" ".join([
"getbinpkg",
copy_path(state.pkgmngr, stage3, preserve_owner=False, tools=state.config.tools_tree)
- run_workspace_command(
- stage3,
+ bwrap(
cmd=["emerge-webrsync"],
- bwrap_params=["--bind", state.cache_dir / "repos", "/var/db/repos"],
- network=True,
+ tools=state.config.tools_tree,
+ apivfs=stage3,
+ extra=chroot_cmd(
+ stage3,
+ options=["--bind", state.cache_dir / "repos", "/var/db/repos"],
+ network=True,
+ ),
)
invoke_emerge(state, packages=["sys-apps/baselayout"], env={"USE": "build"})
import os
import pwd
import queue
-import shutil
import signal
import subprocess
import sys
stdin: _FILE = None,
stdout: _FILE = None,
stderr: _FILE = None,
+ input: Optional[str] = None,
env: Mapping[str, PathString] = {},
log: bool = True,
**kwargs: Any,
if ARG_DEBUG.get():
env["SYSTEMD_LOG_LEVEL"] = "debug"
- if "input" in kwargs:
+ if input is not None:
assert stdin is None # stdin and input cannot be specified together
elif stdin is None:
stdin = subprocess.DEVNULL
stdin=stdin,
stdout=stdout,
stderr=stderr,
+ input=input,
env=env,
**kwargs,
preexec_fn=foreground)
logging.error(f"\"{' '.join(str(s) for s in cmdline)}\" returned non-zero exit code {e.returncode}.")
raise e
+
@contextlib.contextmanager
def bwrap_cmd(
*,
# Clean up some stuff that might get written by package manager post install scripts.
if apivfs:
for f in ("var/lib/systemd/random-seed", "var/lib/systemd/credential.secret", "etc/machine-info"):
- apivfs.joinpath(f).unlink(missing_ok=True)
+ # Using missing_ok=True still causes an OSError if the mount is read-only even if the
+ # file doesn't exist so do an explicit exists() check first.
+ if (apivfs / f).exists():
+ (apivfs / f).unlink()
def bwrap(
tools: Optional[Path] = None,
apivfs: Optional[Path] = None,
log: bool = True,
+ extra: Sequence[PathString] = (),
# The following arguments are passed directly to run().
stdin: _FILE = None,
stdout: _FILE = None,
try:
result = run(
- [*bwrap, *cmd],
+ [*bwrap, *extra, *cmd],
text=True,
env=env,
log=False,
logging.error(f"\"{' '.join(str(s) for s in cmd)}\" returned non-zero exit code {e.returncode}.")
if ARG_DEBUG_SHELL.get():
run(
- [*bwrap, "sh"],
+ [*bwrap, *extra, "sh"],
stdin=sys.stdin,
check=False,
env=env,
return result
-def run_workspace_command(
- root: Path,
- cmd: Sequence[PathString],
- bwrap_params: Sequence[PathString] = (),
- network: bool = False,
- stdout: _FILE = None,
- env: Mapping[str, PathString] = {},
-) -> CompletedProcess:
+def chroot_cmd(root: Path, *, options: Sequence[PathString] = (), network: bool = False) -> Sequence[PathString]:
cmdline: list[PathString] = [
"bwrap",
"--unshare-ipc",
"--unshare-pid",
"--unshare-cgroup",
- "--bind", root, "/",
- "--tmpfs", "/run",
- "--tmpfs", "/tmp",
- "--dev", "/dev",
- "--proc", "/proc",
- "--ro-bind", "/sys", "/sys",
+ "--dev-bind", root, "/",
"--die-with-parent",
- *bwrap_params,
+ "--setenv", "container", "mkosi",
+ "--setenv", "SYSTEMD_OFFLINE", str(int(network)),
+ "--setenv", "HOME", "/",
+ "--setenv", "PATH", "/usr/bin:/usr/sbin",
+ *options,
]
- resolve = root.joinpath("etc/resolv.conf")
-
- tmp = Path(tempfile.NamedTemporaryFile(delete=False).name)
- tmp.unlink()
-
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, tmp)
-
- # If we're using the host network namespace, use the same resolver
- cmdline += ["--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf"]
+ resolve = Path("etc/resolv.conf")
+ if (root / resolve).is_symlink():
+ # For each component in the target path, bubblewrap will try to create it if it doesn't exist
+ # yet. If a component in the path is a dangling symlink, bubblewrap will end up calling
+ # mkdir(symlink) which obviously fails if multiple components of the dangling symlink path don't
+ # exist yet. As a workaround, we resolve the symlink ourselves so that bubblewrap will correctly
+ # create all missing components in the target path.
+ resolve = (root / resolve).readlink()
+
+ # If we're using the host network namespace, use the same resolver.
+ cmdline += ["--ro-bind", "/etc/resolv.conf", Path("/") / resolve]
else:
cmdline += ["--unshare-net"]
- env = dict(
- container="mkosi",
- SYSTEMD_OFFLINE=str(int(network)),
- HOME="/",
- PATH="/usr/bin:/usr/sbin",
- ) | env
-
- with tempfile.TemporaryDirectory(dir="/var/tmp", prefix="mkosi-var-tmp") as var_tmp:
- cmdline += [
- "--bind", var_tmp, "/var/tmp",
- "sh", "-c", "chmod 1777 /tmp /var/tmp /dev/shm && exec $0 \"$@\" || exit $?"
- ]
-
- try:
- return run([*cmdline, *cmd], text=True, stdout=stdout, env=env, log=False)
- except subprocess.CalledProcessError as e:
- logging.error(f"\"{' '.join(str(s) for s in cmd)}\" 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
- finally:
- if tmp.is_symlink():
- resolve.unlink(missing_ok=True)
- shutil.move(tmp, resolve)
+ return cmdline
class MkosiAsyncioThread(threading.Thread):