pmbootstrap-meow/pmb/helpers/repo.py
Oliver Smith 933c4d0f0d new action: 'pmbootstrap repo_missing'
Add a new action that lists all aports, for which no binary packages
exist. Only list packages that can be built for the relevant arch
(specified with --arch). This works recursively: when a package can be
built for a certain arch, but one of its dependencies
(or their depends) can not be built for that arch, then don't list it.

This action will be used for the new sr.ht based build infrastructure,
to figure out which packages need to be built ahead of time (so we can
trigger each of them as single build job). Determining the order of the
packages to be built is not determined with pmbootstrap, the serverside
code of build.postmarketos.org takes care of that.

For testing purposes, a single package can also be specified and the
action will list if it can be built for that arch with its
dependencies, and what needs to be built exactly.

Add pmb/helpers/package.py to hold functions that work on both pmaports
and (binary package) repos - in contrary to the existing
pmb/helpers/pmaports.py (see previous commit) and pmb/helpers/repo.py,
which only work with one of those.

Refactoring:
* pmb/helpers/pmaports.py: add a get_list() function, which lists all
  aports and use it instead of writing the same glob loop over and over
* add pmb.helpers.pmaports.get(), which finds an APKBUILD and parses it
  in one step.
* rename pmb.build._package.check_arch to ...check_arch_abort to
  distinguish it from the other check_arch function
2018-12-01 21:30:59 +00:00

209 lines
7.1 KiB
Python

"""
Copyright 2018 Oliver Smith
This file is part of pmbootstrap.
pmbootstrap is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pmbootstrap is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pmbootstrap. If not, see <http://www.gnu.org/licenses/>.
"""
"""
Functions that work on both (binary package) repos. See also:
- pmb/helpers/pmaports.py (work on pmaports)
- pmb/helpers/package.py (work on both)
"""
import os
import hashlib
import logging
import pmb.helpers.http
import pmb.helpers.run
def hash(url, length=8):
"""
Generate the hash, that APK adds to the APKINDEX and apk packages
in its apk cache folder. It is the "12345678" part in this example:
"APKINDEX.12345678.tar.gz".
:param length: The length of the hash in the output file.
See also: official implementation in apk-tools:
<https://git.alpinelinux.org/cgit/apk-tools/>
blob.c: apk_blob_push_hexdump(), "const char *xd"
apk_defines.h: APK_CACHE_CSUM_BYTES
database.c: apk_repo_format_cache_index()
"""
binary = hashlib.sha1(url.encode("utf-8")).digest()
xd = "0123456789abcdefghijklmnopqrstuvwxyz"
csum_bytes = int(length / 2)
ret = ""
for i in range(csum_bytes):
ret += xd[(binary[i] >> 4) & 0xf]
ret += xd[binary[i] & 0xf]
return ret
def urls(args, user_repository=True, postmarketos_mirror=True):
"""
Get a list of repository URLs, as they are in /etc/apk/repositories.
"""
ret = []
# Local user repository (for packages compiled with pmbootstrap)
if user_repository:
ret.append("/mnt/pmbootstrap-packages")
# Upstream postmarketOS binary repository
if postmarketos_mirror and args.mirror_postmarketos:
if os.path.exists(args.mirror_postmarketos):
ret.append("/mnt/postmarketos-mirror")
else:
ret.append(args.mirror_postmarketos)
# Upstream Alpine Linux repositories
directories = ["main", "community"]
if args.alpine_version == "edge":
directories.append("testing")
for dir in directories:
ret.append(args.mirror_alpine + args.alpine_version + "/" + dir)
return ret
def apkindex_files(args, arch=None):
"""
Get a list of outside paths to all resolved APKINDEX.tar.gz files for a
specific arch.
:param arch: defaults to native
"""
if not arch:
arch = args.arch_native
# Local user repository (for packages compiled with pmbootstrap)
ret = [args.work + "/packages/" + arch + "/APKINDEX.tar.gz"]
# Upstream postmarketOS binary repository
urls_todo = []
mirror = args.mirror_postmarketos
if mirror:
if os.path.exists(mirror):
ret.append(mirror + "/" + arch + "/APKINDEX.tar.gz")
else:
# Non-local path: treat it like other URLs
urls_todo.append(mirror)
# Resolve the APKINDEX.$HASH.tar.gz files
urls_todo += urls(args, False, False)
for url in urls_todo:
ret.append(args.work + "/cache_apk_" + arch + "/APKINDEX." +
hash(url) + ".tar.gz")
return ret
def update(args, arch=None, force=False, existing_only=False):
"""
Download the APKINDEX files for all URLs depending on the architectures.
:param arch: * one Alpine architecture name ("x86_64", "armhf", ...)
* None for all architectures
:param force: even update when the APKINDEX file is fairly recent
:param existing_only: only update the APKBUILD files that already exist,
this is used by "pmbootstrap update"
:returns: True when files have been downloaded, False otherwise
"""
# Skip in offline mode, only show once
if args.offline:
if not args.cache["offline_msg_shown"]:
logging.info("NOTE: skipping package index update (offline mode)")
args.cache["offline_msg_shown"] = True
return False
# Architectures and retention time
architectures = [arch] if arch else pmb.config.build_device_architectures
retention_hours = pmb.config.apkindex_retention_time
retention_seconds = retention_hours * 3600
# Find outdated APKINDEX files. Formats:
# outdated: {URL: apkindex_path, ... }
# outdated_arches: ["armhf", "x86_64", ... ]
outdated = {}
outdated_arches = []
for url in urls(args, False):
for arch in architectures:
# APKINDEX file name from the URL
url_full = url + "/" + arch + "/APKINDEX.tar.gz"
cache_apk_outside = args.work + "/cache_apk_" + arch
apkindex = cache_apk_outside + "/APKINDEX." + hash(url) + ".tar.gz"
# Find update reason, possibly skip non-existing files
reason = None
if not os.path.exists(apkindex):
if existing_only:
continue
reason = "file does not exist yet"
elif force:
reason = "forced update"
elif pmb.helpers.file.is_older_than(apkindex, retention_seconds):
reason = "older than " + str(retention_hours) + "h"
if not reason:
continue
# Update outdated and outdated_arches
logging.debug("APKINDEX outdated (" + reason + "): " + url_full)
outdated[url_full] = apkindex
if arch not in outdated_arches:
outdated_arches.append(arch)
# Bail out or show log message
if not len(outdated):
return False
logging.info("Update package index for " + ", ".join(outdated_arches) +
" (" + str(len(outdated)) + " file(s))")
# Download and move to right location
for url, target in outdated.items():
temp = pmb.helpers.http.download(args, url, "APKINDEX", False,
logging.DEBUG)
target_folder = os.path.dirname(target)
if not os.path.exists(target_folder):
pmb.helpers.run.root(args, ["mkdir", "-p", target_folder])
pmb.helpers.run.root(args, ["cp", temp, target])
return True
def alpine_apkindex_path(args, repo="main", arch=None):
"""
Get the path to a specific Alpine APKINDEX file on disk and download it if
necessary.
:param repo: Alpine repository name (e.g. "main")
:param arch: Alpine architecture (e.g. "armhf"), defaults to native arch.
:returns: full path to the APKINDEX file
"""
# Repo sanity check
if repo not in ["main", "community", "testing", "non-free"]:
raise RuntimeError("Invalid Alpine repository: " + repo)
# Download the file
update(args, arch)
# Find it on disk
arch = arch or args.arch_native
repo_link = args.mirror_alpine + args.alpine_version + "/" + repo
cache_folder = args.work + "/cache_apk_" + arch
return cache_folder + "/APKINDEX." + hash(repo_link) + ".tar.gz"