pmbootstrap/pmb/helpers/pmaports.py

290 lines
10 KiB
Python
Raw Normal View History

2023-01-22 18:11:10 +00:00
# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
2018-11-15 07:36:39 +00:00
"""
Functions that work with pmaports. See also:
- pmb/helpers/repo.py (work with binary package repos)
- pmb/helpers/package.py (work with both)
2018-11-15 07:36:39 +00:00
"""
import glob
import logging
import os
import pmb.parse
def _find_apkbuilds(args):
# Try to get a cached result first (we assume that the aports don't change
# in one pmbootstrap call)
apkbuilds = pmb.helpers.other.cache.get("pmb.helpers.pmaports.apkbuilds")
if apkbuilds is not None:
return apkbuilds
apkbuilds = {}
for apkbuild in glob.iglob(f"{args.aports}/**/*/APKBUILD", recursive=True):
package = os.path.basename(os.path.dirname(apkbuild))
if package in apkbuilds:
raise RuntimeError(f"Package {package} found in multiple aports "
"subfolders. Please put it only in one folder.")
apkbuilds[package] = apkbuild
2021-05-19 18:36:24 +00:00
# Sort dictionary so we don't need to do it over and over again in
# get_list()
apkbuilds = dict(sorted(apkbuilds.items()))
# Save result in cache
pmb.helpers.other.cache["pmb.helpers.pmaports.apkbuilds"] = apkbuilds
return apkbuilds
def get_list(args):
2018-11-15 07:36:39 +00:00
""" :returns: list of all pmaport pkgnames (["hello-world", ...]) """
return list(_find_apkbuilds(args).keys())
2018-11-15 07:36:39 +00:00
def guess_main_dev(args, subpkgname):
"""
Check if a package without "-dev" at the end exists in pmaports or not, and
log the appropriate message. Don't call this function directly, use
guess_main() instead.
:param subpkgname: subpackage name, must end in "-dev"
:returns: full path to the pmaport or None
"""
pkgname = subpkgname[:-4]
path = _find_apkbuilds(args).get(pkgname)
if path:
logging.verbose(subpkgname + ": guessed to be a subpackage of " +
pkgname + " (just removed '-dev')")
return os.path.dirname(path)
logging.verbose(subpkgname + ": guessed to be a subpackage of " + pkgname +
", which we can't find in pmaports, so it's probably in"
" Alpine")
return None
def guess_main(args, subpkgname):
"""
Find the main package by assuming it is a prefix of the subpkgname.
We do that, because in some APKBUILDs the subpkgname="" variable gets
filled with a shell loop and the APKBUILD parser in pmbootstrap can't
parse this right. (Intentionally, we don't want to implement a full shell
parser.)
:param subpkgname: subpackage name (e.g. "u-boot-some-device")
:returns: * full path to the aport, e.g.:
"/home/user/code/pmbootstrap/aports/main/u-boot"
* None when we couldn't find a main package
"""
# Packages ending in -dev: just assume that the originating aport has the
# same pkgname, except for the -dev at the end. If we use the other method
# below on subpackages, we may end up with the wrong package. For example,
# if something depends on plasma-framework-dev, and plasma-framework is in
# Alpine, but plasma is in pmaports, then the cutting algorithm below would
# pick plasma instead of plasma-framework.
if subpkgname.endswith("-dev"):
return guess_main_dev(args, subpkgname)
# Iterate until the cut up subpkgname is gone
words = subpkgname.split("-")
while len(words) > 1:
# Remove one dash-separated word at a time ("a-b-c" -> "a-b")
words.pop()
pkgname = "-".join(words)
# Look in pmaports
path = _find_apkbuilds(args).get(pkgname)
if path:
logging.verbose(subpkgname + ": guessed to be a subpackage of " +
pkgname)
return os.path.dirname(path)
2021-11-09 11:54:07 +00:00
def _find_package_in_apkbuild(package, path):
"""
Look through subpackages and all provides to see if the APKBUILD at the
specified path contains (or provides) the specified package.
:param package: The package to search for
:param path: The path to the apkbuild
:return: True if the APKBUILD contains or provides the package
"""
apkbuild = pmb.parse.apkbuild(path)
# Subpackages
if package in apkbuild["subpackages"]:
return True
# Search for provides in both package and subpackages
apkbuild_pkgs = [apkbuild, *apkbuild["subpackages"].values()]
for apkbuild_pkg in apkbuild_pkgs:
if not apkbuild_pkg:
continue
# Provides (cut off before equals sign for entries like
# "mkbootimg=0.0.1")
for provides_i in apkbuild_pkg["provides"]:
# Ignore provides without version, they shall never be
# automatically selected
if "=" not in provides_i:
continue
if package == provides_i.split("=", 1)[0]:
return True
return False
def find(args, package, must_exist=True):
"""
Find the aport path that provides a certain subpackage.
2018-11-15 07:36:39 +00:00
If you want the parsed APKBUILD instead, use pmb.helpers.pmaports.get().
:param must_exist: Raise an exception, when not found
:returns: the full path to the aport folder
"""
# Try to get a cached result first (we assume that the aports don't change
# in one pmbootstrap call)
ret = None
if package in pmb.helpers.other.cache["find_aport"]:
ret = pmb.helpers.other.cache["find_aport"][package]
else:
# Sanity check
if "*" in package:
raise RuntimeError("Invalid pkgname: " + package)
# Search in packages
path = _find_apkbuilds(args).get(package)
if path:
ret = os.path.dirname(path)
# Try to guess based on the subpackage name
guess = guess_main(args, package)
if guess:
# ... but see if we were right
2021-11-09 11:54:07 +00:00
if _find_package_in_apkbuild(package, f'{guess}/APKBUILD'):
ret = guess
# Search in subpackages and provides
if not ret:
for path_current in _find_apkbuilds(args).values():
2021-11-09 11:54:07 +00:00
if _find_package_in_apkbuild(package, path_current):
ret = os.path.dirname(path_current)
break
# Use the guess otherwise
if not ret:
ret = guess
# Crash when necessary
if ret is None and must_exist:
raise RuntimeError("Could not find aport for package: " +
package)
# Save result in cache
pmb.helpers.other.cache["find_aport"][package] = ret
return ret
2018-11-15 07:36:39 +00:00
def get(args, pkgname, must_exist=True, subpackages=True):
2018-11-15 07:36:39 +00:00
""" Find and parse an APKBUILD file.
Run 'pmbootstrap apkbuild_parse hello-world' for a full output example.
Relevant variables are defined in pmb.config.apkbuild_attributes.
:param pkgname: the package name to find
:param must_exist: raise an exception when it can't be found
2021-05-19 18:36:24 +00:00
:param subpackages: also search for subpackages with the specified
names (slow! might need to parse all APKBUILDs to
find it)
2018-11-15 07:36:39 +00:00
:returns: relevant variables from the APKBUILD as dictionary, e.g.:
{ "pkgname": "hello-world",
"arch": ["all"],
"pkgrel": "4",
"pkgrel": "1",
"options": [],
... }
"""
if subpackages:
aport = find(args, pkgname, must_exist)
if aport:
return pmb.parse.apkbuild(f"{aport}/APKBUILD")
else:
path = _find_apkbuilds(args).get(pkgname)
if path:
return pmb.parse.apkbuild(path)
if must_exist:
2021-05-19 18:36:24 +00:00
raise RuntimeError("Could not find APKBUILD for package:"
f" {pkgname}")
2018-11-15 07:36:39 +00:00
return None
pmb.config/install: add flexible provider selection for "pmbootstrap init" (MR 2132) The provider selection for "pmbootstrap init" added in this commit is a flexible way to offer UI/device-specific configuration options in "pmbootstrap init", without hardcoding them in pmbootstrap. Instead, the options are defined entirely in pmaports using APK's virtual package provider mechanism. The code in pmbootstrap searches for available providers and displays them together with their pkgdesc. There are many possible use cases for this but I have tested two so far: 1. Selecting root provider (sudo vs doas). This can be defined entirely in postmarketos-base, without having to handle this specifically in pmbootstrap. $ pmbootstrap init [...] Available providers for postmarketos-root (2): * sudo: Use sudo to run root commands (**default**) * doas: Use doas (minimal replacement for sudo) to run root commands (Note: Does not support all functionality of sudo) Provider [default]: doas 2. Device-specific options. My main motivation for working on this feature is a new configuration option for the MSM8916-based devices. It allows more control about which firmware to enable: $ pmbootstrap init [...] Available providers for soc-qcom-msm8916-rproc (3): * all: Enable all remote processors (audio goes through modem) (default) * no-modem: Disable only modem (audio bypasses modem, ~80 MiB more RAM) * none: Disable all remote processors (no WiFi/BT/modem, ~90 MiB more RAM) Provider [default]: no-modem The configuration prompts show up dynamically by defining _pmb_select="<virtual packages>" in postmarketos-base, a UI PKGBUILD or the device APKBUILD. Selecting "default" (just pressing enter) means that no provider is selected. This allows APK to choose it automatically based on the "provider_priority". It also provides compatibility with existing installation; APK will just choose the default provider when upgrading. The selection can still be changed after installation by installing another provider using "apk". Note that at the end this is just a more convenient interface for the already existing "extra packages" prompt. When using pmbootstrap in automated scripts the providers (e.g. "postmarketos-root-doas") can be simply selected through the existing "extra_packages" option.
2021-10-22 10:57:01 +00:00
def find_providers(args, provide):
"""
Search for providers of the specified (virtual) package in pmaports.
Note: Currently only providers from a single APKBUILD are returned.
:param provide: the (virtual) package to search providers for
:returns: tuple list (pkgname, apkbuild_pkg) with providers, sorted by
provider_priority. The provider with the highest priority
(which would be selected by default) comes first.
"""
providers = {}
apkbuild = get(args, provide)
for subpkgname, subpkg in apkbuild["subpackages"].items():
for provides in subpkg["provides"]:
# Strip provides version (=$pkgver-r$pkgrel)
if provides.split("=", 1)[0] == provide:
providers[subpkgname] = subpkg
return sorted(providers.items(), reverse=True,
key=lambda p: p[1].get('provider_priority', 0))
2018-11-15 07:36:39 +00:00
def get_repo(args, pkgname, must_exist=True):
""" Get the repository folder of an aport.
:pkgname: package name
:must_exist: raise an exception when it can't be found
:returns: a string like "main", "device", "cross", ...
or None when the aport could not be found """
aport = find(args, pkgname, must_exist)
if not aport:
return None
return os.path.basename(os.path.dirname(aport))
def check_arches(arches, arch):
""" Check if building for a certain arch is allowed.
:param arches: list of all supported arches, as it can be found in the
arch="" line of APKBUILDS (including all, noarch,
!arch, ...). For example: ["x86_64", "x86", "!armhf"]
:param arch: the architecture to check for
:returns: True when building is allowed, False otherwise
"""
if "!" + arch in arches:
return False
for value in [arch, "all", "noarch"]:
if value in arches:
return True
return False
def get_channel_new(channel):
""" Translate legacy channel names to the new ones. Legacy names are still
supported for compatibility with old branches (pmb#2015).
:param channel: name as read from pmaports.cfg or channels.cfg, like
"edge", "v21.03" etc., or potentially a legacy name
like "stable".
:returns: name in the new format, e.g. "edge" or "v21.03"
"""
legacy_cfg = pmb.config.pmaports_channels_legacy
if channel in legacy_cfg:
ret = legacy_cfg[channel]
logging.verbose(f"Legacy channel '{channel}' translated to '{ret}'")
return ret
return channel