M pmb/chroot/__init__.py => pmb/chroot/__init__.py +1 -1
@@ 2,7 2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from pmb.chroot.init import init, init_keys
from pmb.chroot.mount import mount, mount_native_into_foreign
-from pmb.chroot.root import root
+from pmb.chroot.root import root, make_proot_cmd
from pmb.chroot.user import user
from pmb.chroot.user import exists as user_exists
from pmb.chroot.shutdown import shutdown
M pmb/chroot/apk_static.py => pmb/chroot/apk_static.py +11 -3
@@ 9,6 9,7 @@ import stat
import pmb.helpers.apk
import pmb.helpers.run
+import pmb.helpers.mount
import pmb.config
import pmb.config.load
import pmb.parse.apkindex
@@ 165,8 166,15 @@ def init(args):
extract(args, version, apk_static)
-def run(args, parameters):
+# Wrap apk.static with proot to init the rootfs
+# cache is already bind-mounted
+def run(args, suffix, parameters, chroot=True):
if args.offline:
parameters = ["--no-network"] + parameters
- pmb.helpers.apk.apk_with_progress(
- args, [f"{args.work}/apk.static"] + parameters, chroot=False)
+ target_full = f"{args.work}/chroot_{suffix}/bin/apk.static"
+ pmb.helpers.mount.bind(args, f"{args.work}/apk.static", target_full)
+ #pmb.chroot.root(args, ["/host-rootfs/usr/bin/uname", "-a"], suffix=suffix, disable_timeout=True, exists_check=False)
+ return pmb.chroot.root(args, ["/bin/apk.static"] + parameters, suffix=suffix,
+ disable_timeout=True, exists_check=False)
+ # pmb.helpers.apk.apk_with_progress(
+ # args, [f"{args.work}/apk.static"] + parameters, chroot=chroot, suffix=suffix)
M pmb/chroot/init.py => pmb/chroot/init.py +12 -6
@@ 38,7 38,7 @@ def mark_in_chroot(args, suffix="native"):
"""
in_chroot_file = f"{args.work}/chroot_{suffix}/in-pmbootstrap"
if not os.path.exists(in_chroot_file):
- pmb.helpers.run.root(args, ["touch", in_chroot_file])
+ pmb.helpers.run.user(args, ["touch", in_chroot_file])
def setup_qemu_emulation(args, suffix):
@@ 79,8 79,11 @@ def init(args, suffix="native"):
chroot = f"{args.work}/chroot_{suffix}"
arch = pmb.parse.arch.from_chroot_suffix(args, suffix)
+ pmb.helpers.run.user(args, ["mkdir", "-p", f"{chroot}/tmp"])
+
pmb.chroot.mount(args, suffix)
- setup_qemu_emulation(args, suffix)
+ # Handled by proot
+ # setup_qemu_emulation(args, suffix)
mark_in_chroot(args, suffix)
if os.path.islink(f"{chroot}/bin/sh"):
pmb.config.workdir.chroot_check_channel(args, suffix)
@@ 95,6 98,7 @@ def init(args, suffix="native"):
# Initialize cache
apk_cache = f"{args.work}/cache_apk_{arch}"
+ pmb.helpers.run.root(args, ["mkdir", "-p", f"{chroot}/etc/apk/cache"])
pmb.helpers.run.root(args, ["ln", "-s", "-f", "/var/cache/apk",
f"{chroot}/etc/apk/cache"])
@@ 106,11 110,13 @@ def init(args, suffix="native"):
pmb.config.workdir.chroot_save_init(args, suffix)
# Install alpine-base
+ # /home/cas/.local/var/pmbootstrap/apk.static
+ # --root /home/cas/.local/var/pmbootstrap/chroot_rootfs_oneplus-fajita
+ # --cache-dir /home/cas/.local/var/pmbootstrap/cache_apk_aarch64 --initdb
+ # --arch aarch64 add alpine-base
pmb.helpers.repo.update(args, arch)
- pmb.chroot.apk_static.run(args, ["--root", chroot,
- "--cache-dir", apk_cache,
- "--initdb", "--arch", arch,
- "add", "alpine-base"])
+ # wrap apk.static in proot!
+ pmb.chroot.apk_static.run(args, suffix, ["--initdb", "--arch", arch, "add", "alpine-base"])
# Building chroots: create "pmos" user, add symlinks to /home/pmos
if not suffix.startswith("rootfs_"):
M pmb/chroot/mount.py => pmb/chroot/mount.py +8 -2
@@ 6,7 6,7 @@ import os
import pmb.config
import pmb.parse
import pmb.helpers.mount
-
+from pmb.helpers.run import which
def create_device_nodes(args, suffix):
"""
@@ 56,7 56,7 @@ def mount_dev_tmpfs(args, suffix="native"):
"""
# Do nothing when it is already mounted
dev = args.work + "/chroot_" + suffix + "/dev"
- if pmb.helpers.mount.ismount(dev):
+ if pmb.helpers.mount.ismount(dev) or pmb.config.rootless:
return
# Create the $chroot/dev folder and mount tmpfs there
@@ 83,6 83,9 @@ def mount(args, suffix="native"):
# Mount tmpfs as the chroot's /dev
mount_dev_tmpfs(args, suffix)
+ if pmb.config.rootless and os.path.exists(f"{args.work}/config_proot/proot_{suffix}.cfg"):
+ return
+
# Get all mountpoints
arch = pmb.parse.arch.from_chroot_suffix(args, suffix)
channel = pmb.config.pmaports.read_config(args)["channel"]
@@ 93,6 96,9 @@ def mount(args, suffix="native"):
source = source.replace("$CHANNEL", channel)
mountpoints[source] = target
+ mountpoints[which("sh")] = "/bin/sh_host"
+ #mountpoints[which("cat")] = "/bin/cat_host"
+
# Mount if necessary
for source, target in mountpoints.items():
target_full = args.work + "/chroot_" + suffix + target
M pmb/chroot/root.py => pmb/chroot/root.py +32 -26
@@ 8,26 8,24 @@ import pmb.chroot
import pmb.chroot.binfmt
import pmb.helpers.run
import pmb.helpers.run_core
+from pmb.helpers.run import which
+def make_proot_cmd(args, suffix="native", working_dir="/"):
+ arch = pmb.parse.arch.from_chroot_suffix(args, suffix)
+ arch_qemu = pmb.parse.arch.alpine_to_qemu(arch)
+ cmd_chroot = [which("proot"), "-q", f"qemu-{arch_qemu}-static", "-w", working_dir]
+ bindmounts = pmb.helpers.mount.proot_listmounts(args, suffix)
+ # FIXME: SECURITY!!!! proot will make the host /dev /sys /proc /tmp and /run
+ # available to the chroot with -S
+ for bindmount in bindmounts:
+ cmd_chroot += ["-b", bindmount]
+ cmd_chroot += ["-S", f"{args.work}/chroot_{suffix}"]
-def executables_absolute_path():
- """
- Get the absolute paths to the sh and chroot executables.
- """
- ret = {}
- for binary in ["sh", "chroot"]:
- path = shutil.which(binary, path=pmb.config.chroot_host_path)
- if not path:
- raise RuntimeError(f"Could not find the '{binary}'"
- " executable. Make sure that it is in"
- " your current user's PATH.")
- ret[binary] = path
- return ret
-
+ return cmd_chroot
def root(args, cmd, suffix="native", working_dir="/", output="log",
output_return=False, check=None, env={}, auto_init=True,
- disable_timeout=False):
+ disable_timeout=False, exists_check=True):
"""
Run a command inside a chroot as root.
@@ 39,11 37,12 @@ def root(args, cmd, suffix="native", working_dir="/", output="log",
arguments and the return value.
"""
# Initialize chroot
- chroot = f"{args.work}/chroot_{suffix}"
- if not auto_init and not os.path.islink(f"{chroot}/bin/sh"):
- raise RuntimeError(f"Chroot does not exist: {chroot}")
- if auto_init:
- pmb.chroot.init(args, suffix)
+ chroot_path = f"{args.work}/chroot_{suffix}"
+ if exists_check:
+ if not auto_init and not os.path.islink(f"{chroot_path}/bin/sh"):
+ raise RuntimeError(f"Chroot does not exist: {chroot_path}")
+ if auto_init:
+ pmb.chroot.init(args, suffix)
# Readable log message (without all the escaping)
msg = f"({suffix}) % "
@@ 75,11 74,18 @@ def root(args, cmd, suffix="native", working_dir="/", output="log",
# cmd: ["echo", "test"]
# cmd_chroot: ["/sbin/chroot", "/..._native", "/bin/sh", "-c", "echo test"]
# cmd_sudo: ["sudo", "env", "-i", "sh", "-c", "PATH=... /sbin/chroot ..."]
- executables = executables_absolute_path()
- cmd_chroot = [executables["chroot"], chroot, "/bin/sh", "-c",
- pmb.helpers.run.flat_cmd(cmd, working_dir)]
- cmd_sudo = [pmb.config.sudo, "env", "-i", executables["sh"], "-c",
- pmb.helpers.run.flat_cmd(cmd_chroot, env=env_all)]
- return pmb.helpers.run_core.core(args, msg, cmd_sudo, None, output,
+ cmd_chroot = []
+ if pmb.config.rootless:
+ cmd_chroot = make_proot_cmd(args, suffix, working_dir) + cmd# + ["/bin/sh_host", "-c",
+ #pmb.helpers.run.flat_cmd(cmd)]
+ cmd_chroot = ["env", "-i", which("sh"), "-c",
+ pmb.helpers.run.flat_cmd(cmd_chroot, env=env_all)]
+ else:
+ cmd_chroot += [which("chroot"), chroot_path, "/bin/sh", "-c",
+ pmb.helpers.run.flat_cmd(cmd)]
+ cmd_chroot = [pmb.config.sudo, "env", "-i", which("sh"), "-c",
+ pmb.helpers.run.flat_cmd(cmd_chroot, env=env_all)]
+
+ return pmb.helpers.run_core.core(args, msg, cmd_chroot, None, output,
output_return, check, True,
disable_timeout)
M pmb/chroot/shutdown.py => pmb/chroot/shutdown.py +4 -0
@@ 81,6 81,10 @@ def shutdown(args, only_install_related=False):
for marker in glob.glob(f"{args.work}/chroot_*/in-pmbootstrap"):
pmb.helpers.run.root(args, ["rm", marker])
+ # Remove proot mount configs so they don't go stale
+ for marker in glob.glob(f"{args.work}/config_proot/proot_*.cfg"):
+ pmb.helpers.run.root(args, ["rm", marker])
+
if not only_install_related:
# Umount all folders inside args.work
# The folders are explicitly iterated over, so folders symlinked inside
M pmb/config/__init__.py => pmb/config/__init__.py +3 -0
@@ 60,6 60,9 @@ required_programs = [
]
sudo = which_sudo()
+# Should pmbootstrap run without root permissions?
+rootless = True
+
# Keys saved in the config file (mostly what we ask in 'pmbootstrap init')
config_keys = ["aports",
"ccache_size",
M pmb/helpers/apk.py => pmb/helpers/apk.py +3 -2
@@ 2,7 2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import os
-import pmb.chroot.root
+import pmb.chroot
import pmb.config.pmaports
import pmb.helpers.cli
import pmb.helpers.run
@@ 22,8 22,9 @@ def _run(args, command, chroot=False, suffix="native", output="log"):
arguments and the return value.
"""
if chroot:
+ # exists_check=False: avoid infinite recursion
return pmb.chroot.root(args, command, output=output, suffix=suffix,
- disable_timeout=True)
+ disable_timeout=True, exists_check=False)
return pmb.helpers.run.root(args, command, output=output)
M pmb/helpers/mount.py => pmb/helpers/mount.py +68 -0
@@ 1,7 1,23 @@
# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
import os
+import logging
import pmb.helpers.run
+import pmb.config
+
+def dest_to_suffix(args, destination):
+ """
+ Convert a destination path to a chroot suffix.
+ """
+ target_relative = destination.replace(args.work, "")
+
+ if target_relative[0] == "/":
+ target_relative = target_relative[1:]
+ if not target_relative.startswith("chroot_"):
+ raise RuntimeError(f"Unknown proot target: {destination}")
+
+ target_relative = target_relative.split("/")
+ return "/" + "/".join(target_relative[1:]), target_relative[0].replace("chroot_", "")
def ismount(folder):
@@ 19,12 35,64 @@ def ismount(folder):
return True
return False
+def proot_listmounts(args, suffix):
+ """
+ List all bindmounts for a proot call.
+ """
+ cfg = f"{args.work}/config_proot/proot_{suffix}.cfg"
+ if not os.path.exists(cfg):
+ return []
+ ret = []
+ with open(cfg, "r") as handle:
+ for line in handle:
+ ret.append(line.strip())
+ return ret
+
+def proot_bindmount(args, source, destination):
+ """
+ Store the bindmount in the proot config file, so that it is applied
+ to every proot call.
+ $WORK/config_proot/proot_<suffix>.cfg
+
+ We do this trickery to avoid manually fixing every usage of pmb.helpers.mount.bind()
+ ideally we fix it there....
+ """
+
+ target_relative, suffix = dest_to_suffix(args, destination)
+ cfg = f"{args.work}/config_proot/proot_{suffix}.cfg"
+
+ logging.verbose(f"{suffix}: proot_bindmount add {source}:{target_relative}")
+ if not os.path.exists(os.path.dirname(cfg)):
+ pmb.helpers.run.user(args, ["mkdir", "-p", os.path.dirname(cfg)])
+ elif target_relative in proot_listmounts(args, suffix):
+ return
+ with open(cfg, "a") as f:
+ f.write(f"{source}:{target_relative}\n")
+
+def proot_umount(args, destination):
+ """
+ Remove the bindmount from the proot config file.
+ """
+
+ target_relative, suffix = dest_to_suffix(args, destination)
+ cfg = f"{args.work}/config_proot/proot_{suffix}.cfg"
+
+ logging.verbose(f"{suffix}: proot_bindmount del {target_relative}")
+ pmb.helpers.run.user(args, ["sed", "-i", f"/{target_relative}/d", cfg])
def bind(args, source, destination, create_folders=True, umount=False):
"""
Mount --bind a folder and create necessary directory structure.
:param umount: when destination is already a mount point, umount it first.
"""
+
+ if pmb.config.rootless:
+ if umount:
+ proot_umount(args, destination)
+ else:
+ proot_bindmount(args, source, destination)
+ return
+
# Check/umount destination
if ismount(destination):
if umount:
M pmb/helpers/run.py => pmb/helpers/run.py +21 -1
@@ 1,7 1,24 @@
# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
import shlex
+import shutil
import pmb.helpers.run_core
+import pmb.config
+from functools import lru_cache
+
+@lru_cache(maxsize=32)
+def which(binary):
+ """
+ Get the absolute paths and default args of an executable
+ :param binary: the name of the executable
+ """
+ path = shutil.which(binary, path=pmb.config.chroot_host_path)
+ if not path:
+ raise RuntimeError(f"Could not find the '{binary}'"
+ " executable. Make sure that it is in"
+ " your current user's PATH.")
+
+ return path
def flat_cmd(cmd, working_dir=None, env={}):
@@ 72,7 89,10 @@ def root(args, cmd, working_dir=None, output="log", output_return=False,
"""
if env:
cmd = ["sh", "-c", flat_cmd(cmd, env=env)]
- cmd = [pmb.config.sudo] + cmd
+ if pmb.config.rootless:
+ cmd = [which("fakeroot"), "--unknown-is-real", "--"] + cmd
+ else:
+ cmd = [pmb.config.sudo] + cmd
return user(args, cmd, working_dir, output, output_return, check, env,
True)
M test/test_qemu_running_processes.py => test/test_qemu_running_processes.py +8 -0
@@ 88,6 88,9 @@ class QEMU(object):
# Create and run rootfs
pmbootstrap_yes(args, config, ["install", "--password", "y"])
+ if ui == "phosh":
+ pmb.chroot.root(args, ["echo", "\"export LIBGL_ALWAYS_SOFTWARE=true;export LIBGL_DRI2_DISABLE=true\"", ">>", "/etc/tinydm.d/env-wayland.d/50-libgl-virt.sh"], suffix="rootfs_qemu-amd64")
+ pmb.chroot.root(args, ["chmod", "+x", "/etc/tinydm.d/env-wayland.d/50-libgl-virt.sh"], suffix="rootfs_qemu-amd64")
self.process = pmbootstrap_run(args, config, ["qemu", "--display",
"none"], "background")
@@ 186,3 189,8 @@ def test_plasma_mobile(args, tmpdir, qemu):
# check for more processes
qemu.run(args, tmpdir, "plasma-mobile")
assert is_running(args, ["polkitd"])
+
+@pytest.mark.skip_ci
+def test_phosh(args, tmpdir, qemu):
+ qemu.run(args, tmpdir, "phosh")
+ assert is_running(args, ["gnome-session-binary", "phoc", "phosh"])