diff --git a/conda/conda-bootstrap.py b/conda/conda-bootstrap.py index 695453ec418a35f0f645f1e6465b870d72c5a92a..dd205f1391db8f3b6548797f9a26795037d83be8 100755 --- a/conda/conda-bootstrap.py +++ b/conda/conda-bootstrap.py @@ -1,13 +1,68 @@ #!/usr/bin/env python # vim: set fileencoding=utf-8 : -# Uses the conda render API to render a recipe and install an environment -# containing all build/host, run and test dependencies of a package. +"""This program uses conda to build 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 +**not** build the package itself, just install dependencies so you can build +the package by hand, possibly using buildout or similar. If you'd like to +conda-build your package, just use `conda build` instead. + +The easiest way to setup is to ensure the `conda' program is available on your +path and that the packages `conda-build' and `pyyaml' are installed in the +base/root environment of your installation. + +Once the environment is created, a copy of the used `condarc' file is placed on +the root of the environment. Installing or updating packages on the newly +created environment should be possible without further configuration. Notice +that beta packages quickly get outdated and upgrading may no longer be possible +for aging development environments. You're advised to always re-use this +app and use the flag `--overwrite` to re-create from scratch the development +environment. + +examples: + + 1. Creates an environment called `myenv' for developing the currently + checked-out package: + + $ cd bob.package.foo + $ %(prog)s myenv + + The above command assumes the directory `conda' exists on the current + directory and that it contains a file called `meta.yaml' containing the + recipe for the package you want to create a development environment for. + + If you get things right by default, the above form is the typical usage + scenario of this app. Read-on to tweak special flags and settings. + + 2. By default, we use the native python version of your conda installation as + the default python version to use for the newly created environment. You + may select a different one with `--python=X.Y': + + $ %(prog)s --python=3.6 myenv + + 3. By default, we use bob.admin's condarc and `conda_build_config.yaml` files + that are used in creating packages for our CI/CD system. If you wish to + use your own, specify them on the command line: + + $ %(prog)s --config=config.yaml --condarc=~/.condarc myenv + + Notice the condarc file **must** end in `condarc', or conda will complain. + + 4. You may also specify the location of your conda installation, if it is not + available on $PATH: + + $ %(prog)s --conda=/path/to/conda myenv + +""" + import os import re import sys import copy +import json import shutil import tempfile import argparse @@ -20,58 +75,53 @@ except ImportError: sys.exit(1) -if not hasattr(shutil, 'which'): - print('This program only runs with python v3.3 or above') - sys.exit(1) - - -CONDA=shutil.which('conda') -if CONDA is None: - print('You must have the "conda" command available on your shell') - print(' -- also, make sure to have installed the conda-build package') - sys.exit(1) - - -BASEDIR=os.path.realpath(os.path.dirname(sys.argv[0])) +def which(env, program): + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) -def get_condarc(): - rcpath = os.path.join(BASEDIR, 'build-condarc') - return os.environ.get('CONDARC', rcpath) + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): return exe_file + return None -def get_buildopt(python): - return [ - '--variant-config-files', os.path.join(os.path.dirname(BASEDIR), - 'gitlab', 'conda_build_config.yaml'), - '--python', python, - ] +def get_rendered_recipe(args): -def get_rendered_recipe(python, path): - if path.endswith('meta.yaml'): - path = os.path.dirname(path) - - with tempfile.TemporaryDirectory(dir=path, suffix='.tmp') as d: + with tempfile.TemporaryDirectory(dir=args.recipe, suffix='.tmp') as d: + print('$ mkdir -p %s' % (d,)) # writes a temporary recipe without bob-devel - orig = os.path.join(path, 'meta.yaml') + orig = os.path.join(args.recipe, 'meta.yaml') contents = open(orig).read() result = re.search(r'requirements:\s*\n.+host:\s*\n', contents, re.DOTALL) contents = contents[:result.end()] + \ ' - bob-devel {{ bob_devel }}.*\n' + \ contents[result.end():] - with open(os.path.join(d, 'meta.yaml'), 'wt') as f: f.write(contents) - cmd = [CONDA, 'render'] + get_buildopt(python) + [d] - print('$ ' + ' '.join(cmd)) - env = copy.copy(os.environ) - env['CONDARC'] = get_condarc() - output = subprocess.check_output(cmd, env=env) + destfile = os.path.join(d, 'meta.yaml') + print('$ cp %s -> %s' % (orig, destfile)) + print('$ edit %s # add bob-devel to host section' % (destfile,)) + with open(destfile, 'wt') as f: f.write(contents) + + cmd = [ + args.conda, 'render', + '--variant-config-files', args.config, + '--python', args.python, d + ] + print(('$ CONDARC=%s ' % args.condarc) + ' '.join(cmd)) + output = subprocess.check_output(cmd, env=args.env) + print('$ rm -rf %s' % (d,)) return yaml.load(output) -def parse_dependencies(python, path): - recipe = get_rendered_recipe(python, path) +def parse_dependencies(args): + + recipe = get_rendered_recipe(args) return recipe['requirements'].get('host', []) + \ recipe['requirements'].get('build', []) + \ recipe['requirements'].get('run', []) + \ @@ -79,31 +129,111 @@ def parse_dependencies(python, path): ['bob.buildout', 'ipdb'] #extra packages required for local dev -def conda_install(subcmd, env, packages): +def get_env_directory(args): + + cmd = [args.conda, 'env', 'list', '--json'] + output = subprocess.check_output(cmd, env=args.env) + data = json.loads(output) + retval = [k for k in data.get('envs', []) if k.endswith(os.sep + args.name)] + if retval: return retval[0] + return None + + +def conda_create(args, packages): + specs = [] for k in packages: k = ' '.join(k.split()[:2]) #remove eventual build string if any(elem in k for elem in '><|'): specs.append(k.replace(' ', '')) else: specs.append(k.replace(' ', '=')) - cmd = [CONDA, subcmd, '--yes', '--name', env] + specs - print('$ ' + ' '.join(cmd)) - env = copy.copy(os.environ) - env['CONDARC'] = get_condarc() - status = subprocess.call(cmd, env=env) + + #if the current environment exists, delete it first + envdir = get_env_directory(args) + if envdir is not None: + if args.overwrite: + cmd = [args.conda, 'env', 'remove', '--yes', '--name', args.name] + print(('$ CONDARC=%s ' % args.condarc) + ' '.join(cmd)) + status = subprocess.call(cmd, env=args.env) + if status != 0: return status + else: + raise RuntimeError('environment `%s\' exists in `%s\' - use ' \ + '--overwrite to overwrite' % (args.name, envdir)) + + cmd = [args.conda, 'create', '--yes', '--name', args.name] + specs + print(('$ CONDARC=%s ' % args.condarc) + ' '.join(cmd)) + status = subprocess.call(cmd, env=args.env) + + if status != 0: return status + + #copy the used condarc file to the just created environment + envdir = get_env_directory(args) + destrc = os.path.join(envdir, '.condarc') + print('$ cp %s -> %s' % (args.condarc, destrc)) + shutil.copy2(args.condarc, destrc) return status def main(): - parser = argparse.ArgumentParser(description='Creates a new conda environment with the build/run/test dependencies of a package') - parser.add_argument('name', help='name of the target environment to create') - parser.add_argument('python', help='version of python to build the environment for [default: %(default)s]', nargs='?', default='3.6') - parser.add_argument('path', help='path to the directory containing the conda recipe [default: %(default)s]', nargs='?', default='conda') + subst = {'prog': os.path.basename(sys.argv[0])} + parser = argparse.ArgumentParser(description='Creates a conda ' \ + 'environment with the build/run/test dependencies of a package', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ % subst) + parser.add_argument('name', help='name of the target environment to create') + parser.add_argument('recipe', help='path to the **directory** ' \ + 'containing the conda recipe [default: %(default)s]', nargs='?', + default=os.path.join(os.path.realpath('.'), 'conda')) + + + parser.add_argument('-p', '--python', help='version of python to build the ' \ + 'environment for [default: %(default)s]', nargs='?', + default=('%d.%d' % sys.version_info[:2])) + parser.add_argument('-c', '--conda', default=which(os.environ, 'conda'), + help='path leading to the conda executable to use if not available on ' \ + '$PATH [default: %(default)s]') + parser.add_argument('-o', '--overwrite', action='store_true', default=False, + help='if set and an environment with the same name exists, ' \ + 'deletes it first before creating the new environment') + + BASEDIR=os.path.realpath(os.path.dirname(sys.argv[0])) + default_condarc = os.path.join(BASEDIR, 'build-condarc') + default_condarc = os.environ.get('CONDARC', default_condarc) + + parser.add_argument('-r', '--condarc', default=default_condarc, + help='overwrites the path leading to the condarc file to use ' \ + '[default: %(default)s]') + + default_variant = os.path.join(os.path.dirname(BASEDIR), 'gitlab', + 'conda_build_config.yaml') + + parser.add_argument('-m', '--config', '--variant-config-files', + default=default_variant, + help='overwrites the path leading to variant configuration file to ' \ + 'use [default: %(default)s]') + + # no surprises, no globals - all configuration comes from the cmdline args = parser.parse_args() - deps = parse_dependencies(args.python, args.path) - status = conda_install('create', args.name, deps) + if args.conda is None: + print('Use --conda=/path/to/conda to set a path to a conda executable') + print(' -- also, make sure to have installed the conda-build package') + sys.exit(1) + + if args.recipe.endswith('meta.yaml'): + raise RuntimeError("You should provide the path leading to `meta.yaml'") + + if not os.path.exists(args.recipe): + raise RuntimeError("The directory %s does not exist" % args.recipe) + + # use this environment for all conda-related commands + args.env = env = copy.copy(os.environ) + args.env['CONDARC'] = args.condarc + + deps = parse_dependencies(args) + status = conda_create(args, deps) + print('$ #execute on your shell: source activate %s' % args.name) sys.exit(status)