pmbootstrap-meow/pmb/sideload/__init__.py
Oliver Smith 3ea5a3433b
Revert "pmb: Make RunOutputTypeDefault and RunOutputTypePopen enums"
Revert the patch, as it breaks "pmbootstrap chroot".

This reverts commit 7d2f055bcb.
2025-07-10 23:53:54 +02:00

144 lines
5.2 KiB
Python

# Copyright 2023 Martijn Braam
# SPDX-License-Identifier: GPL-3.0-or-later
import os
from pathlib import Path
from pmb.core.arch import Arch
from pmb.helpers import logging
import shlex
from pmb.types import PathString, PmbArgs
import pmb.helpers.run
import pmb.helpers.run_core
import pmb.parse.apkindex
import pmb.config.pmaports
import pmb.build
from pmb.core.context import get_context
su_cmd = "_su=$(command -v sudo >/dev/null && echo sudo || echo doas); $_su"
def scp_abuild_key(args: PmbArgs, user: str, host: str, port: str) -> None:
"""Copy the building key of the local installation to the target device,
so it trusts the apks that were signed here.
:param user: target device ssh username
:param host: target device ssh hostname
:param port: target device ssh port"""
keys = list((get_context().config.work / "config_abuild").glob("*.pub"))
key = keys[0]
key_name = os.path.basename(key)
logging.info(f"Copying signing key ({key_name}) to {user}@{host}")
command: list[PathString] = ["scp", "-P", port, key, f"{user}@{host}:/tmp"]
pmb.helpers.run.user(command, output="interactive")
logging.info(f"Installing signing key at {user}@{host}")
keyname = os.path.join("/tmp", os.path.basename(key))
remote_cmd_l: list[PathString] = [
"mv",
"-n",
keyname,
"/etc/apk/keys/",
]
remote_cmd = pmb.helpers.run_core.flat_cmd([remote_cmd_l])
full_cmd = shlex.quote(f"{su_cmd} {remote_cmd}")
command = ["ssh", "-t", "-p", port, f"{user}@{host}", f"sh -c {full_cmd}"]
pmb.helpers.run.user(command, output="tui")
def ssh_find_arch(args: PmbArgs, user: str, host: str, port: str) -> Arch:
"""Connect to a device via ssh and query the architecture."""
logging.info(f"Querying architecture of {user}@{host}")
# Run command in a subshell in case the foreign device has a weird uname
# implementation, e.g. Nushell.
architecture_cmd = shlex.quote("uname -m")
command = ["ssh", "-p", port, f"{user}@{host}", f"sh -c {architecture_cmd}"]
output = pmb.helpers.run.user_output(command)
# Split by newlines so we can pick out any irrelevant output, e.g. the "permanently
# added to list of known hosts" warnings.
output_lines = output.strip().splitlines()
# Pick out last line which should contain the foreign device's architecture
foreign_machine_type = output_lines[-1]
alpine_architecture = Arch.from_machine_type(foreign_machine_type)
return alpine_architecture
def ssh_install_apks(args: PmbArgs, user: str, host: str, port: str, paths: list[Path]) -> None:
"""Copy binary packages via SCP and install them via SSH.
:param user: target device ssh username
:param host: target device ssh hostname
:param port: target device ssh port
:param paths: list of absolute paths to locally stored apks
"""
remote_paths = []
for path in paths:
remote_paths.append(os.path.join("/tmp", os.path.basename(path)))
logging.info(f"Copying packages to {user}@{host}")
command: list[PathString] = ["scp", "-P", port, *paths, f"{user}@{host}:/tmp"]
pmb.helpers.run.user(command, output="interactive")
logging.info(f"Installing packages at {user}@{host}")
add_cmd_list = ["apk", "--wait", "30", "add", *remote_paths]
add_cmd = pmb.helpers.run_core.flat_cmd([add_cmd_list])
clean_cmd = pmb.helpers.run_core.flat_cmd([["rm", *remote_paths]])
add_cmd_complete = shlex.quote(f"{su_cmd} {add_cmd} rc=$?; {clean_cmd} exit $rc")
# Run apk command in a subshell in case the foreign device has a non-POSIX shell.
command = ["ssh", "-t", "-p", port, f"{user}@{host}", f"sh -c {add_cmd_complete}"]
pmb.helpers.run.user(command, output="tui")
def sideload(
args: PmbArgs,
user: str,
host: str,
port: str,
arch: Arch | None,
copy_key: bool,
pkgnames: list[str],
) -> None:
"""Build packages if necessary and install them via SSH.
:param user: target device ssh username
:param host: target device ssh hostname
:param port: target device ssh port
:param arch: target device architecture
:param copy_key: copy the abuild key too
:param pkgnames: list of pkgnames to be built"""
paths = []
if arch is None:
arch = ssh_find_arch(args, user, host, port)
context = get_context()
to_build = []
for pkgname in pkgnames:
data_repo = pmb.parse.apkindex.package(pkgname, arch, True)
if data_repo is None:
raise RuntimeError(f"Couldn't find APKINDEX data for {pkgname}!")
base_aports, _ = pmb.build.get_apkbuild(pkgname)
channel = pmb.config.pmaports.read_config(base_aports)["channel"]
apk_file = f"{pkgname}-{data_repo.version}.apk"
host_path = context.config.work / "packages" / channel / arch / apk_file
if not host_path.is_file():
to_build.append(pkgname)
paths.append(host_path)
if to_build:
pmb.build.packages(context, to_build, arch, force=True)
# Check all the packages actually got builts
for path in paths:
if not path.is_file():
raise RuntimeError(f"The package '{pkgname}' could not be built")
if copy_key:
scp_abuild_key(args, user, host, port)
ssh_install_apks(args, user, host, port, paths)