~postmarketos/pmbootstrap

3c497d71bba4e386de00ba755d66749272b85c9d — Caleb Connolly 1 year, 5 months ago 8c2e542 proot-hacking
start trying to bootstrap, probably very non-viable...
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"])