From da9d8a52e7b9a54a3a1af0dba00baffaad47e3f3 Mon Sep 17 00:00:00 2001 From: Andre Anjos <andre.dos.anjos@gmail.com> Date: Tue, 8 Jan 2019 22:16:19 +0100 Subject: [PATCH] [scritpts] Implement build support; Remove old cb-output script (incorporate into build) --- bob/devtools/bootstrap.py | 53 +++++++--- bob/devtools/conda.py | 89 ++++++++++++++++- bob/devtools/data/recipe_append.yaml | 3 + bob/devtools/scripts/bootstrap.py | 25 ++++- bob/devtools/scripts/build.py | 139 +++++++++++++++++++++++++++ bob/devtools/scripts/cb_output.py | 52 ---------- conda/meta.yaml | 1 + setup.py | 2 +- 8 files changed, 286 insertions(+), 78 deletions(-) create mode 100644 bob/devtools/data/recipe_append.yaml create mode 100644 bob/devtools/scripts/build.py delete mode 100644 bob/devtools/scripts/cb_output.py diff --git a/bob/devtools/bootstrap.py b/bob/devtools/bootstrap.py index 90700df8..21436df0 100755 --- a/bob/devtools/bootstrap.py +++ b/bob/devtools/bootstrap.py @@ -10,25 +10,45 @@ import logging logger = logging.getLogger(__name__) import yaml -from conda_build.api import get_or_merge_config, render, output_yaml -def get_rendered_recipe(conda, recipe_dir, python, config): +def make_conda_config(config, python, append_file, condarc): + + from conda_build.api import get_or_merge_config + from conda_build.conda_interface import url_path + + with open(condarc, 'rb') as f: + condarc_options = yaml.load(f) + + retval = get_or_merge_config(None, variant_config_files=config, + python=python, append_sections_file=append_file, **condarc_options) + + retval.channel_urls = [] + + for url in condarc_options['channels']: + # allow people to specify relative or absolute paths to local channels + # These channels still must follow conda rules - they must have the + # appropriate platform-specific subdir (e.g. win-64) + if os.path.isdir(url): + if not os.path.isabs(url): + url = os.path.normpath(os.path.abspath(os.path.join(os.getcwd(), url))) + url = url_path(url) + retval.channel_urls.append(url) + + return retval + + +def get_rendered_metadata(recipe_dir, config): '''Renders the recipe and returns the interpreted YAML file''' - # equivalent command execute - in here we use the conda API - cmd = [ - conda, 'render', - '--variant-config-files', config, - '--python', python, - recipe_dir, - ] - logger.debug('$ ' + ' '.join(cmd)) + from conda_build.api import render + return render(recipe_dir, config=config) + + +def get_parsed_recipe(metadata): + '''Renders the recipe and returns the interpreted YAML file''' - # do the real job - config = get_or_merge_config(None, variant_config_files=config, - python=python) - metadata = render(recipe_dir, config=config) + from conda_build.api import output_yaml output = output_yaml(metadata[0][0]) return yaml.load(output) @@ -37,9 +57,10 @@ def remove_pins(deps): return [l.split()[0] for l in deps] -def parse_dependencies(conda, recipe_dir, python, config): +def parse_dependencies(recipe_dir, config): - recipe = get_rendered_recipe(conda, recipe_dir, python, config) + metadata = get_rendered_metadata(recipe_dir, config) + recipe = get_parsed_recipe(metadata) return remove_pins(recipe['requirements'].get('build', [])) + \ remove_pins(recipe['requirements'].get('host', [])) + \ recipe['requirements'].get('run', []) + \ diff --git a/bob/devtools/conda.py b/bob/devtools/conda.py index 558c094e..70c04d49 100644 --- a/bob/devtools/conda.py +++ b/bob/devtools/conda.py @@ -1,6 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +'''Utilities for deadling with conda packages''' + +import re +import logging +import platform +logger = logging.getLogger(__name__) + + +def osname(): + """Returns the current OS name as recognized by conda""" + + r = 'unknown' + if platform.system().lower() == 'linux': + r = 'linux' + elif platform.system().lower() == 'darwin': + r = 'osx' + else: + raise RuntimeError('Unsupported system "%s"' % platform.system()) + + if platform.machine().lower() == 'x86_64': + r += '-64' + else: + raise RuntimeError('Unsupported machine type "%s"' % platform.machine()) + + return r + def should_skip_build(metadata_tuples): - """Takes the output of render_recipe as input and evaluates if this - recipe's build should be skipped. - """ - return all(m[0].skip() for m in metadata_tuples) + """Takes the output of render_recipe as input and evaluates if this + recipe's build should be skipped. + """ + + return all(m[0].skip() for m in metadata_tuples) + + +def next_build_number(channel_url, name, version, python): + """Calculates the next build number of a package given the channel + + This function returns the next build number (integer) for a package given its + recipe, dependencies, name, version and python version. It looks on the + channel URL provided and figures out if any clash would happen and what would + be the highest build number available for that configuration. + + + Args: + + channel_url: The URL where to look for packages clashes (normally a beta + channel) + name: The name of the package + version: The version of the package + python: The version of python as 2 digits (e.g.: "2.7" or "3.6") + + Returns: The next build number with the current configuration. Zero (0) is + returned if no match is found. Also returns the URLs of the packages it + finds with matches on the name, version and python-version. + + """ + + from conda.exports import get_index + + # no dot in py_ver + py_ver = python.replace('.', '') + + # get the channel index + logger.debug('Downloading channel index from %s', channel_url) + index = get_index(channel_urls=[channel_url], prepend=False) + + # search if package with the same version exists + build_number = 0 + urls = [] + for dist in index: + + if dist.name == name and dist.version == version: + match = re.match('py[2-9][0-9]+', dist.build_string) + + if match and match.group() == 'py{}'.format(py_ver): + logger.debug("Found match at %s for %s-%s-py%s", index[dist].url, + name, version, py_ver) + build_number = max(build_number, dist.build_number + 1) + urls.append(index[dist].url) + + urls = [url.replace(channel_url, '') for url in urls] + + return build_number, urls diff --git a/bob/devtools/data/recipe_append.yaml b/bob/devtools/data/recipe_append.yaml new file mode 100644 index 00000000..dcfb3076 --- /dev/null +++ b/bob/devtools/data/recipe_append.yaml @@ -0,0 +1,3 @@ +build: + script_env: + - DOCSERVER diff --git a/bob/devtools/scripts/bootstrap.py b/bob/devtools/scripts/bootstrap.py index f3ba9d3a..d8d7b115 100644 --- a/bob/devtools/scripts/bootstrap.py +++ b/bob/devtools/scripts/bootstrap.py @@ -8,16 +8,20 @@ logger = logging.getLogger(__name__) import pkg_resources import click +import yaml from . import bdt from ..log import verbosity_option -from ..bootstrap import parse_dependencies, conda_create +from ..bootstrap import parse_dependencies, conda_create, make_conda_config DEFAULT_CONDARC = pkg_resources.resource_filename(__name__, os.path.join('..', 'data', 'build-condarc')) DEFAULT_VARIANT = pkg_resources.resource_filename(__name__, os.path.join('..', 'data', 'conda_build_config.yaml')) +DEFAULT_APPEND = pkg_resources.resource_filename(__name__, + os.path.join('..', 'data', 'recipe_append.yaml')) +DEFAULT_DOCSERVER = 'http://www.idiap.ch' @click.command(epilog=''' @@ -69,14 +73,21 @@ Examples: @click.option('-m', '--config', '--variant-config-files', show_default=True, default=DEFAULT_VARIANT, help='overwrites the path leading to ' \ 'variant configuration file to use') +@click.option('-a', '--append-file', show_default=True, + default=DEFAULT_APPEND, help='overwrites the path leading to ' \ + 'appended configuration file to use') +@click.option('-D', '--docserver', show_default=True, + default=DEFAULT_DOCSERVER, help='Server used for uploading artifacts ' \ + 'and other goodies') @click.option('-d', '--dry-run/--no-dry-run', default=False, help='Only goes through the actions, but does not execute them ' \ '(combine with the verbosity flags - e.g. ``-vvv``) to enable ' \ 'printing to help you understand what will be done') @verbosity_option() @bdt.raise_on_error -def bootstrap(name, recipe_dir, python, overwrite, condarc, config, dry_run): - """This program uses conda to build a development environment for a recipe +def bootstrap(name, recipe_dir, python, overwrite, condarc, config, + append_file, docserver, dry_run): + """Creates a development environment for a recipe It uses the conda render API to render a recipe and install an environment containing all build/host, run and test dependencies of a package. It does @@ -106,9 +117,13 @@ def bootstrap(name, recipe_dir, python, overwrite, condarc, config, dry_run): "properly?") # set condarc before continuing - logger.debug('$ export CONDARC=%s', condarc) + logger.debug("[var] CONDARC=%s", condarc) os.environ['CONDARC'] = condarc - deps = parse_dependencies(conda, recipe_dir, python, config) + logger.debug("[var] DOCSERVER=%s", docserver) + os.environ['DOCSERVER'] = docserver + + conda_config = make_conda_config(config, python, append_file, condarc) + deps = parse_dependencies(recipe_dir, conda_config) status = conda_create(conda, name, overwrite, condarc, deps, dry_run) click.echo('Execute on your shell: "conda activate %s"' % name) diff --git a/bob/devtools/scripts/build.py b/bob/devtools/scripts/build.py new file mode 100644 index 00000000..e17335d9 --- /dev/null +++ b/bob/devtools/scripts/build.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import logging +logger = logging.getLogger(__name__) + +import pkg_resources +import click + +from . import bdt +from ..log import verbosity_option +from ..conda import next_build_number, osname +from ..bootstrap import get_rendered_metadata, get_parsed_recipe + + +from .bootstrap import DEFAULT_CONDARC, DEFAULT_VARIANT, DEFAULT_APPEND, \ + DEFAULT_DOCSERVER + + +@click.command(epilog=''' +Examples: + + 1. Builds recipe from one of our build dependencies (inside bob.conda): + + $ cd bob.conda + $ bdt build -vv conda/libblitz + + + 2. Builds recipe from one of our packages, for Python 3.6 (if that is not + already the default for you): + + $ bdt build --python=3.6 -vv path/to/conda/dir + + + 3. To build multiple recipes, just pass the paths to them: + + $ bdt build --python=3.6 -vv path/to/recipe-dir/1 path/to/recipe-dir/2 +''') +@click.argument('recipe-dir', required=False, type=click.Path(file_okay=False, + dir_okay=True, exists=True), nargs=-1) +@click.option('-p', '--python', default=('%d.%d' % sys.version_info[:2]), + show_default=True, help='Version of python to build the ' \ + 'environment for [default: %(default)s]') +@click.option('-r', '--condarc', default=DEFAULT_CONDARC, show_default=True, + help='overwrites the path leading to the condarc file to use',) +@click.option('-m', '--config', '--variant-config-files', show_default=True, + default=DEFAULT_VARIANT, help='overwrites the path leading to ' \ + 'variant configuration file to use') +@click.option('-c', '--channel', show_default=True, + default='https://www.idiap.ch/software/bob/conda/label/beta', + help='Channel URL where this package is meant to be uploaded to, ' \ + 'after a successful build - typically, this is a beta channel') +@click.option('-n', '--no-test', is_flag=True, + help='Do not test the package, only builds it') +@click.option('-a', '--append-file', show_default=True, + default=DEFAULT_APPEND, help='overwrites the path leading to ' \ + 'appended configuration file to use') +@click.option('-D', '--docserver', show_default=True, + default=DEFAULT_DOCSERVER, help='Server used for uploading artifacts ' \ + 'and other goodies') +@click.option('-d', '--dry-run/--no-dry-run', default=False, + help='Only goes through the actions, but does not execute them ' \ + '(combine with the verbosity flags - e.g. ``-vvv``) to enable ' \ + 'printing to help you understand what will be done') +@verbosity_option() +@bdt.raise_on_error +def build(recipe_dir, python, condarc, config, channel, no_test, append_file, + docserver, dry_run): + """Runs conda-build with a standard configuration and environment + + This command wraps the execution of conda-build so that you use the same + ``condarc`` and ``conda_build_config.yaml`` file we use for our CI. It + always set ``--no-anaconda-upload``. + + Note that both files are embedded within bob.devtools - you may need to + update your environment before trying this. + """ + + # if we are in a dry-run mode, let's let it be known + if dry_run: + logger.warn('!!!! DRY RUN MODE !!!!') + logger.warn('Nothing will be really built') + + recipe_dir = recipe_dir or [os.path.join(os.path.realpath('.'), 'conda')] + + logger.debug("[var] CONDARC=%s", condarc) + + from ..bootstrap import make_conda_config + conda_config = make_conda_config(config, python, append_file, condarc) + + logger.debug("[var] DOCSERVER=%s", docserver) + os.environ['DOCSERVER'] = docserver + + for d in recipe_dir: + + if not os.path.exists(d): + raise RuntimeError("The directory %s does not exist" % recipe_dir) + + version_candidate = os.path.join(d, '..', 'version.txt') + if os.path.exists(version_candidate): + version = open(version_candidate).read().rstrip() + logger.debug("[var] BOB_PACKAGE_VERSION=%s", version) + os.environ['BOB_PACKAGE_VERSION'] = version + + # pre-renders the recipe - figures out package name and version + metadata = get_rendered_metadata(d, conda_config) + + # checks we should actually build this recipe + from ..conda import should_skip_build + if should_skip_build(metadata): + logger.warn('Skipping UNSUPPORTED build of "%s" for py%s on %s', + d, python.replace('.',''), osname()) + return 0 + + # converts the metadata output into parsed yaml and continues the process + rendered_recipe = get_parsed_recipe(metadata) + + # if a channel URL was passed, set the build number + if channel: + build_number, _ = next_build_number(channel, + rendered_recipe['package']['name'], + rendered_recipe['package']['version'], python) + else: + build_number = 0 + + logger.debug("[var] BOB_BUILD_NUMBER=%s", build_number) + os.environ['BOB_BUILD_NUMBER'] = str(build_number) + + # we don't execute the following command, it is just here for logging + # purposes. we directly use the conda_build API. + logger.info('Building %s-%s-py%s (build: %d) for %s', + rendered_recipe['package']['name'], + rendered_recipe['package']['version'], python.replace('.',''), + build_number, osname()) + if not dry_run: + from conda_build.api import build + build(d, config=conda_config, notest=no_test) diff --git a/bob/devtools/scripts/cb_output.py b/bob/devtools/scripts/cb_output.py deleted file mode 100644 index cfebdb64..00000000 --- a/bob/devtools/scripts/cb_output.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import logging -logger = logging.getLogger(__name__) - -import click -from click.testing import CliRunner -import conda_build.api as cb - -from . import bdt -from ..log import verbosity_option -from ..conda import should_skip_build - - -@click.command(context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ), - epilog='''\b -Examples: -$ bdt cb-output conda_recipe_dir -$ bdt cb-output ../bob.conda/conda/kaldi -m ../bob.admin/gitlab/conda_build_config.yaml --python 3.6 -''' -) -@click.argument('recipe_path') -@click.option('-m', '--variant-config-files', help='see conda build --help') -@click.option('--python', help='see conda build --help') -@verbosity_option() -@bdt.raise_on_error -def cb_output(recipe_path, variant_config_files, python): - """Outputs name(s) of package(s) that would be generated by conda build. - - This command accepts extra unknown arguments so you can give it the same - arguments that you would give to conda build. - - As of now, it only parses -m/--variant_config_files and --python and other - arguments are ignored. - """ - clirunner = CliRunner() - with clirunner.isolation(): - # render - config = cb.get_or_merge_config( - None, variant_config_files=variant_config_files, python=python) - metadata_tuples = cb.render(recipe_path, config=config) - - # check if build(s) should be skipped - if should_skip_build(metadata_tuples): - return 0 - - paths = cb.get_output_file_paths(metadata_tuples, config=config) - click.echo('\n'.join(sorted(paths))) diff --git a/conda/meta.yaml b/conda/meta.yaml index 21ac1915..566b7319 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -57,6 +57,7 @@ test: - bdt visibility --help - bdt dumpsphinx --help - bdt bootstrap --help + - bdt build --help - sphinx-build -aEW ${PREFIX}/share/doc/{{ name }}/doc {{ project_dir }}/sphinx about: diff --git a/setup.py b/setup.py index d5a8e803..08acb341 100644 --- a/setup.py +++ b/setup.py @@ -39,13 +39,13 @@ setup( 'bdt = bob.devtools.scripts.bdt:main', ], 'bdt.cli': [ - 'cb-output = bob.devtools.scripts.cb_output:cb_output', 'release = bob.devtools.scripts.release:release', 'changelog = bob.devtools.scripts.changelog:changelog', 'lasttag = bob.devtools.scripts.lasttag:lasttag', 'visibility = bob.devtools.scripts.visibility:visibility', 'dumpsphinx = bob.devtools.scripts.dumpsphinx:dumpsphinx', 'bootstrap = bob.devtools.scripts.bootstrap:bootstrap', + 'build = bob.devtools.scripts.build:build', ], }, classifiers=[ -- GitLab