diff --git a/zapp/compiler/__main__.py b/zapp/compiler/__main__.py index db16393..60dc081 100644 --- a/zapp/compiler/__main__.py +++ b/zapp/compiler/__main__.py @@ -7,6 +7,7 @@ import io import json import os import pathlib +from itertools import chain import stat import sys import zipfile @@ -15,6 +16,7 @@ from shutil import move from tempfile import TemporaryDirectory from zapp.support.unpack import cache_wheel_path +from zapp.support.pep425 import compress_tags, decompress_tag parser = argparse.ArgumentParser(description="The (bootstrap) Zapp compiler") parser.add_argument("-o", "--out", dest="output", help="Output target file") @@ -81,7 +83,14 @@ def load_wheel(opts, manifest, path): """Load a single wheel, returning ...""" def _parse_email(msg): - return dict(Parser().parsestr(msg).items()) + msg = Parser().parsestr(msg) + def _get(k): + v = msg.get_all(k) + if len(v) == 1: + return v[0] + else: + return v + return {k: _get(k) for k in msg.keys()} # RECORD seems to just record file reference checksums for validation # with open(os.path.join(path, "RECORD")) as recordf: @@ -105,14 +114,16 @@ def load_wheel(opts, manifest, path): } + def wheel_name(wheel): """Construct the "canonical" filename of the wheel.""" + # https://www.python.org/dev/peps/pep-0425/ tags = wheel["wheel"].get("Tag") if isinstance(tags, list): - tags = "-" + ".".join(sorted(wheel["wheel"]["Tag"])) + tags = "-" + compress_tags(chain(*[decompress_tag(t) for t in tags])) elif isinstance(tags, str): - tags = "-" + wheel["wheel"]["Tag"] + tags = "-" + tags else: tags = "" @@ -155,17 +166,23 @@ def rezip_wheels(opts, manifest): # Zip up the wheels and insert wheel records to the manifest for w in wheels: # Try to cheat and hit in the local cache first rather than building wheels every time - wf = cache_wheel_path(wheel_name(w)) - if wf.exists(): - try: - wf.touch() - except OSError: - pass - else: - wf = zip_wheel(opts.tmpdir, w) + wn = wheel_name(w) - # Insert a new wheel source - manifest["wheels"][wheel_name(w)] = {"hashes": [], "source": wf} + # We may have a double-path dependency. + # If we DON'T, we have to zip + if wn not in manifest["wheels"]: + wf = cache_wheel_path(wn) + if wf.exists(): + try: + wf.touch() + except OSError: + pass + wf = str(wf) + else: + wf = zip_wheel(opts.tmpdir, w) + + # Insert a new wheel source + manifest["wheels"][wn] = {"hashes": [], "source": wf} # Expunge sources available in the wheel manifest["sources"] = dsub(manifest["sources"], w["sources"]) diff --git a/zapp/support/pep425.py b/zapp/support/pep425.py new file mode 100644 index 0000000..51ab577 --- /dev/null +++ b/zapp/support/pep425.py @@ -0,0 +1,36 @@ +"""An implementation of PEP-425 tag parsing, expansion and compression.""" + +import typing as t + + +class Tag(t.NamedTuple): + python: str + abi: str + arch: str # 'Platform' in the PEP + + +def decompress_tag(tag: str) -> t.Iterable[Tag]: + """Decompress tag string into a sequence of compatible tuples.""" + + pytags, abitags, archtags = tag.split("-", 2) + for x in pytags.split('.'): + for y in abitags.split('.'): + for z in archtags.split('.'): + yield Tag(x, y, z) + + +def compress_tags(tags: t.Iterable[Tag]) -> str: + """Compress a tag sequence into a string encoding compatible tuples.""" + + tags = set(tags) + pytags = set(t.python for t in tags) + abitags = set(t.abi for t in tags) + archtags = set(t.arch for t in tags) + + tag = "-".join([ + ".".join(sorted(pytags)), + ".".join(sorted(abitags)), + ".".join(sorted(archtags)), + ]) + assert set(decompress_tag(tag)) == tags + return tag