pmbootstrap-meow/pmb/sideload/__init__.py
Frieder Hannenheim 6465a6aa87
sideload: get channel from package not from global pmaports (MR 2569)
Currently packages that are not in the systemd channel cannot be
sideloaded when using the systemd channel. This is because apk sideload
uses the global aports channel and not the channel of the package. Fix
it with this patch.
2025-03-10 22:22:50 +01: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)