2017-05-26 20:08:45 +00:00
|
|
|
"""
|
|
|
|
Copyright 2017 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/>.
|
|
|
|
"""
|
|
|
|
import distutils.version
|
|
|
|
import glob
|
|
|
|
import os
|
|
|
|
import tarfile
|
|
|
|
|
|
|
|
|
|
|
|
def compare_version(a_str, b_str):
|
|
|
|
"""
|
|
|
|
http://stackoverflow.com/a/11887885
|
|
|
|
LooseVersion behaves just like apk's version check, at least
|
|
|
|
for all package versions, that have "-r".
|
|
|
|
|
|
|
|
:returns:
|
|
|
|
(a < b): -1
|
|
|
|
(a == b): 0
|
|
|
|
(a > b): 1
|
|
|
|
"""
|
|
|
|
if a_str is None:
|
|
|
|
a_str = "0"
|
|
|
|
if b_str is None:
|
|
|
|
b_str = "0"
|
|
|
|
a = distutils.version.LooseVersion(a_str)
|
|
|
|
b = distutils.version.LooseVersion(b_str)
|
|
|
|
if a < b:
|
|
|
|
return -1
|
|
|
|
if a == b:
|
|
|
|
return 0
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
2017-06-17 22:46:14 +00:00
|
|
|
def parse_next_block(args, path, lines, start):
|
|
|
|
"""
|
|
|
|
Parse the next block in an APKINDEX.
|
|
|
|
|
|
|
|
:param path: to the APKINDEX.tar.gz
|
|
|
|
:param start: current index in lines, gets increased in this
|
|
|
|
function. Wrapped into a list, so it can be modified
|
|
|
|
"by reference". Example: [5]
|
|
|
|
:param lines: all lines from the "APKINDEX" file inside the archive
|
|
|
|
:returns: a dictionary with the following structure:
|
|
|
|
{ "pkgname": "postmarketos-mkinitfs",
|
|
|
|
"version": "0.0.4-r10",
|
|
|
|
"depends": ["busybox-extras", "lddtree", ... ],
|
|
|
|
"provides": ["mkinitfs=0.0.1"],
|
|
|
|
}
|
|
|
|
:returns: None, when there are no more blocks
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Parse until we hit an empty line or end of file
|
|
|
|
ret = {}
|
|
|
|
mapping = {
|
|
|
|
"P": "pkgname",
|
|
|
|
"V": "version",
|
|
|
|
"D": "depends",
|
|
|
|
"p": "provides"
|
|
|
|
}
|
|
|
|
end_of_block_found = False
|
|
|
|
for i in range(start[0], len(lines)):
|
|
|
|
# Check for empty line
|
|
|
|
start[0] = i + 1
|
|
|
|
line = lines[i].decode()
|
|
|
|
if line == "\n":
|
|
|
|
end_of_block_found = True
|
|
|
|
break
|
|
|
|
|
|
|
|
# Parse keys from the mapping
|
|
|
|
for letter, key in mapping.items():
|
|
|
|
if line.startswith(letter + ":"):
|
|
|
|
if key in ret:
|
|
|
|
raise RuntimeError(
|
|
|
|
"Key " + key + " (" + letter + ":) specified twice"
|
|
|
|
" in block: " + str(ret) + ", file: " + path)
|
|
|
|
ret[key] = line[2:-1]
|
|
|
|
|
|
|
|
# Format and return the block
|
|
|
|
if end_of_block_found:
|
|
|
|
# Check for required keys
|
|
|
|
for key in ["pkgname", "version"]:
|
|
|
|
if key not in ret:
|
|
|
|
raise RuntimeError("Missing required key '" + key +
|
|
|
|
"' in block " + str(ret) + ", file: " + path)
|
|
|
|
|
|
|
|
# Format optional lists
|
|
|
|
for key in ["provides", "depends"]:
|
|
|
|
if key in ret and ret[key] != "":
|
|
|
|
ret[key] = ret[key].split(" ")
|
|
|
|
else:
|
|
|
|
ret[key] = []
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
# No more blocks
|
|
|
|
elif ret != {}:
|
|
|
|
raise RuntimeError("Last block in " + path + " does not end"
|
|
|
|
" with a new line! Delete the file and"
|
|
|
|
" try again. Last block: " + str(ret))
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def parse_add_block(path, strict, ret, block, pkgname=None,
|
|
|
|
version=None):
|
|
|
|
"""
|
|
|
|
Add one block to the return dictionary of parse().
|
|
|
|
|
|
|
|
:param path: to the APKINDEX.tar.gz
|
|
|
|
:param strict: When set to True, only allow one entry per pkgname.
|
|
|
|
In case there are two, raise an exception.
|
|
|
|
When set to False, and there are multiple entries
|
|
|
|
for one pkgname, it uses the latest one.
|
|
|
|
:param ret: dictionary of all packages in the APKINDEX, that is
|
|
|
|
getting built right now. This function will extend it.
|
|
|
|
:param block: return value from parse_next_block().
|
|
|
|
:param pkgname: defaults to the real pkgname, could be an alias
|
|
|
|
from the "provides" list.
|
|
|
|
:param version: defaults to the real version, could be a value
|
|
|
|
from the "provides" list.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Defaults
|
|
|
|
if not pkgname:
|
|
|
|
pkgname = block["pkgname"]
|
|
|
|
if not version:
|
|
|
|
version = block["version"]
|
|
|
|
|
|
|
|
# Handle duplicate entries
|
|
|
|
if pkgname in ret:
|
|
|
|
if strict:
|
|
|
|
raise RuntimeError("Multiple blocks for " +
|
|
|
|
pkgname + " in " + path)
|
|
|
|
if compare_version(ret[pkgname]["version"], version) < 1:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Add it to the result set
|
|
|
|
ret[pkgname] = block
|
|
|
|
|
|
|
|
|
|
|
|
def parse(args, path, strict=False):
|
|
|
|
"""
|
|
|
|
Parse an APKINDEX.tar.gz file, and return its content as dictionary.
|
|
|
|
|
|
|
|
:param strict: When set to True, only allow one entry per pkgname.
|
|
|
|
In case there are two, raise an exception.
|
|
|
|
When set to False, and there are multiple entries
|
|
|
|
for one pkgname, it uses the latest one.
|
|
|
|
:returns: a dictionary with the following structure:
|
|
|
|
{ "postmarketos-mkinitfs":
|
|
|
|
{
|
|
|
|
"pkgname": "postmarketos-mkinitfs"
|
|
|
|
"version": "0.0.4-r10",
|
|
|
|
"depends": ["busybox-extras", "lddtree", ...],
|
|
|
|
"provides": ["mkinitfs=0.0.1"]
|
|
|
|
}, ...
|
|
|
|
}
|
|
|
|
"""
|
2017-06-17 23:09:21 +00:00
|
|
|
|
|
|
|
# Try to get a cached result first
|
|
|
|
lastmod = os.path.getmtime(path)
|
|
|
|
if path in args.cache["apkindex"]:
|
|
|
|
cache = args.cache["apkindex"][path]
|
|
|
|
if cache["lastmod"] == lastmod:
|
|
|
|
return cache["ret"]
|
|
|
|
|
|
|
|
# Parse the whole APKINDEX.tar.gz file
|
2017-06-17 22:46:14 +00:00
|
|
|
ret = {}
|
|
|
|
start = [0]
|
|
|
|
with tarfile.open(path, "r:gz") as tar:
|
|
|
|
with tar.extractfile(tar.getmember("APKINDEX")) as handle:
|
|
|
|
lines = handle.readlines()
|
|
|
|
while True:
|
|
|
|
block = parse_next_block(args, path, lines, start)
|
|
|
|
if not block:
|
|
|
|
break
|
|
|
|
|
|
|
|
# Add the next package and all aliases
|
|
|
|
parse_add_block(path, strict, ret, block)
|
|
|
|
if "provides" in block:
|
|
|
|
for alias in block["provides"]:
|
|
|
|
split = alias.split("=")
|
|
|
|
if len(split) == 2:
|
|
|
|
parse_add_block(path, strict, ret, block,
|
|
|
|
split[0], split[1])
|
2017-06-17 23:09:21 +00:00
|
|
|
# Update the cache
|
|
|
|
args.cache["apkindex"][path] = {"lastmod": lastmod, "ret": ret}
|
|
|
|
|
2017-06-17 22:46:14 +00:00
|
|
|
return ret
|
|
|
|
|
|
|
|
|
2017-05-26 20:08:45 +00:00
|
|
|
def read(args, package, path, must_exist=True):
|
|
|
|
"""
|
2017-06-17 22:46:14 +00:00
|
|
|
Get information about a single package from an APKINDEX.tar.gz file.
|
|
|
|
|
2017-05-26 20:08:45 +00:00
|
|
|
:param path: Path to APKINDEX.tar.gz, defaults to $WORK/APKINDEX.tar.gz
|
|
|
|
:param package: The package of which you want to read the properties.
|
|
|
|
:param must_exist: When set to true, raise an exception when the package is
|
|
|
|
missing in the index, or the index file was not found.
|
|
|
|
:returns: {"pkgname": ..., "version": ..., "depends": [...]}
|
|
|
|
When the package appears multiple times in the APKINDEX, this
|
|
|
|
function returns the attributes of the latest version.
|
|
|
|
"""
|
|
|
|
# Verify APKINDEX path
|
|
|
|
if not os.path.exists(path):
|
|
|
|
if not must_exist:
|
|
|
|
return None
|
|
|
|
raise RuntimeError("File not found: " + path)
|
|
|
|
|
2017-06-17 22:46:14 +00:00
|
|
|
# Parse the APKINDEX
|
|
|
|
apkindex = parse(args, path)
|
|
|
|
if package not in apkindex:
|
|
|
|
if must_exist:
|
|
|
|
raise RuntimeError("Package '" + package +
|
|
|
|
"' not found in " + path)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
return apkindex[package]
|
2017-05-26 20:08:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def read_any_index(args, package, arch=None):
|
|
|
|
"""
|
2017-06-17 22:46:14 +00:00
|
|
|
Get information about a single package from any APKINDEX.tar.gz.
|
2017-05-26 20:08:45 +00:00
|
|
|
|
|
|
|
:param arch: defaults to native architecture
|
2017-06-17 22:46:14 +00:00
|
|
|
:returns: the same format as read()
|
2017-05-26 20:08:45 +00:00
|
|
|
"""
|
|
|
|
if not arch:
|
|
|
|
arch = args.arch_native
|
|
|
|
indexes = [args.work + "/packages/" + arch + "/APKINDEX.tar.gz"]
|
|
|
|
pattern = args.work + "/cache_apk_" + arch + "/APKINDEX.*.tar.gz"
|
|
|
|
indexes += glob.glob(pattern)
|
|
|
|
|
|
|
|
for index in indexes:
|
|
|
|
index_data = read(args, package, index, False)
|
|
|
|
if index_data:
|
|
|
|
return index_data
|
|
|
|
return None
|