Meson download, verify and extract compressed files

Related: CMake download file


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))

```