update_recipes.py 3.77 KB
Newer Older
Stefan Scherfke's avatar
Stefan Scherfke committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
"""
Update one or more external recipes.

The script uses the update checker to finde the lastest release for a recipe.
It then replaces the current with the latest versions, downloads the package
and updates its sha256 hash.

.. code-block:: console

   $ ownconda update-recipes python python-numpy

Use the `--dry-run` option to perform a dry-run and don't actually write the
updated recipes.

"""
from urllib.request import urlopen
import difflib
import hashlib
import re
import sys

import click
import requests
import trio

from .. import click_util, recipes, update_check, util


def get_sha256(url: str) -> bytes:
    """Download *url* and return the responses sha256 hex digest as bytes.

    *url* must start with ``ftp://``, ``http://`` or ``https://``.

    """
    if url.startswith('ftp://'):
        con = urlopen(url)
        data = con.read()
        con.close()
    else:
        assert url.startswith(('https://', 'http://'))
        response = requests.get(url, stream=True)
        data = response.raw.read()

    return hashlib.sha256(data).hexdigest()


def update_recipe(meta, filename, dry_run=False, allow_prerelease=False):
    """Update the recipe *meta* stored at *filename*."""
    mod_update_check = update_check.load_update_check(filename)
    recipe_name, latest, current = trio.run(update_check.run_check, meta, filename)

    click_util.info(recipe_name)

    if latest is None:
        click_util.error('Skipping (cannot determine latest version)')

    elif latest == current:
        click_util.ok('Skipping (already latest version)')

    elif 'url' not in meta['source'] and 'sha256' not in meta['source']:
        click_util.warning(f'Skipping (contains no download URL)')

    elif mod_update_check.checker == 'pypi':
        cmd = [
            sys.executable,
            '-m', 'own_conda_tools',
            'pypi-recipe',
            getattr(mod_update_check, 'package_name', meta['package']['name']),
            '-u',
        ]
        if dry_run:
            cmd.append('--dry-run')
        if allow_prerelease:
            cmd.append('--pre-release')
        if meta['source']['url'].endswith('.whl'):
            cmd.append('-w')
        cwd = filename.resolve().parent.parent
        util.run(cmd, cwd=cwd)

    else:
        _update_recipe(meta, filename, current, latest, dry_run)


def _update_recipe(meta, filename, current, latest, dry_run):
    old_meta_yaml = open(filename).read()
    old_sha256 = meta['source']['sha256']

    new_meta_yaml = old_meta_yaml.replace(str(current), str(latest))
    meta_new = recipes.load_yaml(new_meta_yaml)
    new_sha256 = get_sha256(meta_new['source']['url'])
    new_meta_yaml = new_meta_yaml.replace(old_sha256, new_sha256)
    new_meta_yaml = re.sub(r'^  number: \d+$', '  number: 0', new_meta_yaml,
                           flags=re.MULTILINE)

    fg = {
        ' ': None,
        '@': 'cyan',
        '+': 'green',
        '-': 'red',
    }
    for line in difflib.unified_diff(old_meta_yaml.splitlines(keepends=True),
                                     new_meta_yaml.splitlines(keepends=True)):
        if line.startswith(('+++', '---')):
            continue
        click_util.echo(line, fg=fg[line[0]], nl=False)

    if not dry_run:
        with open(filename, 'w') as f:
            f.write(new_meta_yaml)


@click.command()
@click_util.argument.recipe_root()
@click_util.option.dry_run()
@click.option(
    '--pre-release',
    '-p',
    is_flag=True,
    default=False,
    help='Allow pre-release of PyPI packages.',
)
def cli(recipe_root, dry_run, pre_release):
    """Update Conda recipes in RECIPE_ROOT.

    You can specify multiple recipe roots which are all searched recursively
    for recipes.

    """
    for meta, path in recipes.load_recipes(recipe_root):
        update_recipe(meta, path, dry_run=dry_run, allow_prerelease=pre_release)