Meson download, verify and extract compressed files

Meson does not have built-in the ability to download any file. While this could also be done via a custom_target(), we do it via run_command() in meson.build. This technique uses only Python stdlib modules; no extra pip install is needed.

meson.build

run_command('python', 'meson_file_download.py', url, zipfn, '-hash', 'md5', md5hash, check: true)

run_command('python', 'meson_file_extract.py', zipfn, outpath, check: true)

meson_file_download.py

#!/usr/bin/env python3
"""
We use SystemExit as this will not blast the whole traceback to Meson.
Usually just a terse stderr will suffice and not overwhelm the Meson user.
"""
from pathlib import Path
import urllib.request
import urllib.error
import hashlib
import argparse
import typing
import socket


def url_retrieve(
    url: str,
    outfile: Path,
    filehash: typing.Sequence[str] = None,
    overwrite: bool = False,
):
    """
    Parameters
    ----------
    url: str
        URL to download from
    outfile: pathlib.Path
        output filepath (including name)
    filehash: tuple of str, str
        hash type (md5, sha1, etc.) and hash
    overwrite: bool
        overwrite if file exists
    """
    outfile = Path(outfile).expanduser().resolve()
    if outfile.is_dir():
        raise ValueError("Please specify full filepath, including filename")
    # need .resolve() in case intermediate relative dir doesn't exist
    if overwrite or not outfile.is_file():
        outfile.parent.mkdir(parents=True, exist_ok=True)
        try:
            urllib.request.urlretrieve(url, str(outfile))
        except (socket.gaierror, urllib.error.URLError) as err:
            raise SystemExit(
                "ConnectionError: could not download {} due to {}".format(url, err)
            )

    if filehash:
        if not file_checksum(outfile, filehash[0], filehash[1]):
            raise SystemExit("HashError: {}".format(outfile))


def file_checksum(fn: Path, mode: str, filehash: str) -> bool:
    h = hashlib.new(mode)
    h.update(fn.read_bytes())
    return h.hexdigest() == filehash


if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("url", help="URL to file download")
    p.add_argument("outfile", help="filename to download to")
    p.add_argument("-hash", help="expected hash", nargs=2)
    P = p.parse_args()

    url_retrieve(P.url, P.outfile, P.hash)

meson_file_extract.py

#!/usr/bin/env python3
from pathlib import Path
import argparse
import zipfile
import tarfile

def extract_zip(fn: Path, outpath: Path, overwrite: bool = False):
    outpath = Path(outpath).expanduser().resolve()
    # need .resolve() in case intermediate relative dir doesn't exist
    if outpath.is_dir() and not overwrite:
        return

    fn = Path(fn).expanduser().resolve()
    with zipfile.ZipFile(fn) as z:
        z.extractall(str(outpath.parent))


def extract_tar(fn: Path, outpath: Path, overwrite: bool = False):
    outpath = Path(outpath).expanduser().resolve()
    # need .resolve() in case intermediate relative dir doesn't exist
    if outpath.is_dir() and not overwrite:
        return

    fn = Path(fn).expanduser().resolve()
    if not fn.is_file():
        raise FileNotFoundError(fn)  # keep this, tarfile gives confusing error
    with tarfile.open(fn) as z:
        z.extractall(str(outpath.parent))


if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("infile", help="compressed file to extract")
    p.add_argument("outpath", help="path to extract into")
    P = p.parse_args()

    infile = Path(P.infile)
    if infile.suffix.lower() == ".zip":
        extract_zip(infile, P.outpath)
    elif infile.suffix.lower() in (".tar", ".gz", ".bz2", ".xz"):
        extract_tar(infile, P.outpath)
    else:
        raise ValueError("Not sure how to decompress {}".format(infile))

CMake download file