From 8b2a97da2be1f6c47634239d0ddbccaa5a35e5d3 Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.dos.anjos@gmail.com>
Date: Wed, 16 Jan 2019 15:36:16 +0100
Subject: [PATCH] DRY; Move boot/build from ci dir into the package

---
 .gitlab-ci.yml                  |   4 +-
 MANIFEST.in                     |   2 +-
 bob/devtools/bootstrap.py       |  83 ++++++---
 bob/devtools/build.py           | 294 +++++++++++++++++++++++++++++++-
 bob/devtools/conda.py           |  76 ---------
 bob/devtools/constants.py       |  39 +----
 bob/devtools/create.py          | 128 --------------
 bob/devtools/data/build-condarc |  14 --
 bob/devtools/scripts/bdt.py     |   3 +-
 bob/devtools/scripts/build.py   |  76 +++++----
 bob/devtools/scripts/create.py  |  52 ++++--
 ci/build.sh                     |  95 -----------
 doc/api.rst                     |   3 -
 13 files changed, 434 insertions(+), 435 deletions(-)
 delete mode 100644 bob/devtools/create.py
 delete mode 100644 bob/devtools/data/build-condarc
 delete mode 100755 ci/build.sh

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5bbc8a40..019127a8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,7 +16,9 @@ stages:
   before_script:
     - python3 ./bob/devtools/bootstrap.py build
   script:
-    - ./ci/build.sh
+    - source ${CONDA_ROOT}/etc/profile.d/conda.sh
+    - conda activate base
+    - python3 ./bob/devtools/build.py
   cache: &build_caches
     paths:
       - miniconda.sh
diff --git a/MANIFEST.in b/MANIFEST.in
index 1a582cbe..86a6e92e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,3 @@
 include LICENSE README.rst buildout.cfg version.txt
 recursive-include doc conf.py *.rst *.sh
-recursive-include bob/devtools/data *.md *.yaml *condarc *.pem matplotlibrc
+recursive-include bob/devtools/data *.md *.yaml *.pem matplotlibrc
diff --git a/bob/devtools/bootstrap.py b/bob/devtools/bootstrap.py
index 930ffafe..52ea6b14 100644
--- a/bob/devtools/bootstrap.py
+++ b/bob/devtools/bootstrap.py
@@ -24,7 +24,7 @@ Arguments:
 '''
 
 
-BASE_CONDARC = '''\
+_BASE_CONDARC = '''\
 default_channels:
   - https://repo.anaconda.com/pkgs/main
   - https://repo.anaconda.com/pkgs/free
@@ -39,6 +39,17 @@ anaconda_upload: false #!final
 ssl_verify: false #!final
 '''
 
+_SERVER = 'http://www.idiap.ch'
+
+_INTERVALS = (
+    ('weeks', 604800),  # 60 * 60 * 24 * 7
+    ('days', 86400),    # 60 * 60 * 24
+    ('hours', 3600),    # 60 * 60
+    ('minutes', 60),
+    ('seconds', 1),
+    )
+'''Time intervals that make up human readable time slots'''
+
 
 import os
 import sys
@@ -49,16 +60,25 @@ import platform
 import subprocess
 
 import logging
-logger = logging.getLogger('bootstrap')
+logger = logging.getLogger(__name__)
 
 
-_INTERVALS = (
-    ('weeks', 604800),  # 60 * 60 * 24 * 7
-    ('days', 86400),    # 60 * 60 * 24
-    ('hours', 3600),    # 60 * 60
-    ('minutes', 60),
-    ('seconds', 1),
-    )
+def set_environment(name, value, env=os.environ):
+    '''Function to setup the environment variable and print debug message
+
+    Args:
+
+      name: The name of the environment variable to set
+      value: The value to set the environment variable to
+      env: Optional environment (dictionary) where to set the variable at
+    '''
+
+    if name in env:
+      logger.warn('Overriding existing environment variable ${%s} (was: "%s")',
+          name, env[name])
+    env[name] = value
+    logger.debug('$ export %s="%s"', name, value)
+
 
 def human_time(seconds, granularity=2):
   '''Returns a human readable time string like "1 day, 2 hours"'''
@@ -228,7 +248,7 @@ def install_miniconda(prefix):
     shutil.rmtree(cached)
 
 
-def get_channels(public, stable):
+def get_channels(public, stable, server, intranet):
   '''Returns the relevant conda channels to consider if building project
 
   The subset of channels to be returned depends on the visibility and stability
@@ -249,22 +269,31 @@ def get_channels(public, stable):
       channels
     stable: Boolean indicating if we're supposed to include only stable
       channels
+    server: The base address of the server containing our conda channels
+    intranet: Boolean indicating if we should add "private"/"public" prefixes
+      on the conda paths
 
 
   Returns: a list of channels that need to be considered.
 
   '''
 
-  server = "http://www.idiap.ch"
   channels = []
 
+  if (not public) and (not intranet):
+    raise RuntimeError('You cannot request for private channels and set' \
+        ' intranet=False (server=%s) - these are conflicting options' % server)
+
   if not public:
+    prefix = '/private' if intranet else ''
     if not stable:  #allowed private channels
-      channels += [server + '/private/conda/label/beta']  #allowed betas
-    channels += [server + '/private/conda']
+      channels += [server + prefix + '/conda/label/beta']  #allowed betas
+    channels += [server + prefix + '/conda']
+
+  prefix = '/public' if intranet else ''
   if not stable:
-    channels += [server + '/public/conda/label/beta']  #allowed betas
-  channels += [server + '/public/conda']
+    channels += [server + prefix + '/conda/label/beta']  #allowed betas
+  channels += [server + prefix + '/conda']
 
   return channels
 
@@ -278,10 +307,10 @@ def add_channels_condarc(channels, condarc):
       f.write('  - %s\n' % k)
 
   with open(condarc, 'rt') as f:
-    logger.info('Contents of $CONDARC:\n%s', f.read())
+    logger.info('Contents of installed CONDARC:\n%s', f.read())
 
 
-def setup_logger():
+def setup_logger(logger):
   '''Sets-up the logging for this command at level ``INFO``'''
 
   warn_err = logging.StreamHandler(sys.stderr)
@@ -313,14 +342,14 @@ if __name__ == '__main__':
     print(__doc__ % sys.argv[0])
     sys.exit(1)
 
-  setup_logger()
+  setup_logger(logger)
 
   if sys.argv[1] == 'test':
     # sets up local variables for testing
-    os.environ['CI_PROJECT_DIR'] = os.path.realpath(os.curdir)
-    os.environ['CI_PROJECT_NAME'] = 'bob.devtools'
-    os.environ['CONDA_ROOT'] = os.path.join(os.environ['CI_PROJECT_DIR'],
-        'miniconda')
+    set_environment('CI_PROJECT_DIR', os.path.realpath(os.curdir))
+    set_environment('CI_PROJECT_NAME', 'bob.devtools')
+    set_environment('CONDA_ROOT', os.path.join(os.environ['CI_PROJECT_DIR'],
+        'miniconda'))
 
   prefix = os.environ['CONDA_ROOT']
   logger.info('os.environ["%s"] = %s', 'CONDA_ROOT', prefix)
@@ -335,9 +364,7 @@ if __name__ == '__main__':
   condarc = os.path.join(prefix, 'condarc')
   logger.info('(create) %s', condarc)
   with open(condarc, 'wt') as f:
-    f.write(BASE_CONDARC)
-  os.environ['CONDARC'] = condarc
-  logger.info('os.environ["%s"] = %s', 'CONDARC', condarc)
+    f.write(_BASE_CONDARC)
 
   conda_version = '4'
   conda_build_version = '3'
@@ -363,7 +390,8 @@ if __name__ == '__main__':
     conda_bld_path = os.path.join(prefix, 'conda-bld')
     run_cmdline([conda_bin, 'index', conda_bld_path])
     # add the locally build directory before defaults, boot from there
-    channels = get_channels(public=True, stable=True)
+    channels = get_channels(public=True, stable=True, server=_SERVER,
+        intranet=True)
     add_channels_condarc(channels + [conda_bld_path, 'defaults'], condarc)
     run_cmdline([conda_bin, 'create', '-n', sys.argv[2], 'bob.devtools'])
 
@@ -372,7 +400,8 @@ if __name__ == '__main__':
     # installs from channel
     channels = get_channels(
         public=os.environ['CI_PROJECT_VISIBILITY'] == 'public',
-        stable=os.environ.get('CI_COMMIT_TAG') is not None)
+        stable=os.environ.get('CI_COMMIT_TAG') is not None,
+        server=_SERVER, intranet=True)
     add_channels_condarc(channels + ['defaults'], condarc)
     run_cmdline([conda_bin, 'create', '-n', sys.argv[2], 'bob.devtools'])
 
diff --git a/bob/devtools/build.py b/bob/devtools/build.py
index 8559ec4e..bbcbd9fd 100644
--- a/bob/devtools/build.py
+++ b/bob/devtools/build.py
@@ -1,26 +1,88 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-'''This is a copy of bob/devtools/conda:next_build_number with a CLI'''
+'''Tools for self-building and other utilities
+
+This script, if called in standalone format, can be used to build the current
+package.  It contains various functions and utilities that can be used by
+modules inside the package itself.  It assumes a base installation for the
+build is operational (i.e., the python package for ``conda-build`` is
+installed).
+'''
 
 import os
 import re
 import sys
+import json
+import shutil
+import platform
+import subprocess
 
-from conda.exports import get_index
+import logging
+logger = logging.getLogger(__name__)
 
+import yaml
+import packaging
+import conda_build.api
 
-if __name__ == '__main__':
 
-  channel_url = sys.argv[1]
-  name = sys.argv[2]
-  version = sys.argv[3]
-  python = sys.argv[4]
+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)
+
+
+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
@@ -32,9 +94,225 @@ if __name__ == '__main__':
       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]
 
-  print(build_number)
+  return build_number, urls
+
+
+def make_conda_config(config, python, append_file, condarc_options):
+
+  from conda_build.api import get_or_merge_config
+  from conda_build.conda_interface import url_path
+
+  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'''
+
+  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'''
+
+  from conda_build.api import output_yaml
+  output = output_yaml(metadata[0][0])
+  return yaml.load(output)
+
+
+def remove_pins(deps):
+  return [l.split()[0] for l in deps]
+
+
+def parse_dependencies(recipe_dir, 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', []) + \
+      recipe.get('test', {}).get('requires', []) + \
+      ['bob.buildout', 'mr.developer', 'ipdb']
+      # by last, packages required for local dev
+
+
+def get_env_directory(conda, name):
+
+  cmd = [conda, 'env', 'list', '--json']
+  output = subprocess.check_output(cmd)
+  data = json.loads(output)
+  retval = [k for k in data.get('envs', []) if k.endswith(os.sep + name)]
+  if retval:
+    return retval[0]
+  return None
+
+
+def conda_create(conda, name, overwrite, condarc, packages, dry_run, use_local):
+  '''Creates a new conda environment following package specifications
+
+  This command can create a new conda environment following the list of input
+  packages.  It will overwrite an existing environment if indicated.
+
+  Args:
+    conda: path to the main conda executable of the installation
+    name: the name of the environment to create or overwrite
+    overwrite: if set to ```True``, overwrite potentially existing environments
+      with the same name
+    condarc: a dictionary of options for conda, including channel urls
+    packages: the package list specification
+    dry_run: if set, then don't execute anything, just print stuff
+    use_local: include the local conda-bld directory as a possible installation
+      channel (useful for testing multiple interdependent recipes that are
+      built locally)
+  '''
+
+  from .bootstrap import run_cmdline
+
+  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(' ', '='))
+
+  # if the current environment exists, delete it first
+  envdir = get_env_directory(conda, name)
+  if envdir is not None:
+    if overwrite:
+      cmd = [conda, 'env', 'remove', '--yes', '--name', name]
+      logger.debug('$ ' + ' '.join(cmd))
+      if not dry_run:
+        run_cmdline(cmd)
+    else:
+      raise RuntimeError('environment `%s\' exists in `%s\' - use '
+                         '--overwrite to overwrite' % (name, envdir))
+
+  cmdline_channels = ['--channel=%s' % k for k in condarc['channels']]
+  cmd = [conda, 'create', '--yes', '--name', name, '--override-channels'] + \
+      cmdline_channels
+  if dry_run:
+    cmd.append('--dry-run')
+  if use_local:
+     cmd.append('--use-local')
+  cmd.extend(sorted(specs))
+  run_cmdline(cmd)
+
+  # creates a .condarc file to sediment the just created environment
+  if not dry_run:
+    # get envdir again - it may just be created!
+    envdir = get_env_directory(conda, name)
+    destrc = os.path.join(envdir, '.condarc')
+    logger.info('Creating %s...', destrc)
+    with open(destrc, 'w') as f:
+      yaml.dump(condarc, f, indent=2)
+
+
+if __name__ == '__main__':
+
+  # loads the "adjacent" bootstrap module
+  import importlib.util
+  mydir = os.path.dirname(os.path.realpath(sys.argv[0]))
+  bootstrap_file = os.path.join(mydir, 'bootstrap.py')
+  spec = importlib.util.spec_from_file_location("bootstrap", bootstrap_file)
+  bootstrap = importlib.util.module_from_spec(spec)
+  spec.loader.exec_module(bootstrap)
+
+  bootstrap.setup_logger(logger)
+
+  prefix = os.environ['CONDA_ROOT']
+  logger.info('os.environ["%s"] = %s', 'CONDA_ROOT', prefix)
+
+  workdir = os.environ['CI_PROJECT_DIR']
+  logger.info('os.environ["%s"] = %s', 'CI_PROJECT_DIR', workdir)
+
+  name = os.environ['CI_PROJECT_NAME']
+  logger.info('os.environ["%s"] = %s', 'CI_PROJECT_NAME', name)
+
+  pyver = os.environ['PYTHON_VERSION']
+  logger.info('os.environ["%s"] = %s', 'PYTHON_VERSION', pyver)
+
+  set_environment('DOCSERVER', bootstrap._SERVER, os.environ)
+  set_environment('LANG', 'en_US.UTF-8', os.environ)
+  set_environment('LC_ALL', os.environ['LANG'], os.environ)
+
+  # create the build configuration
+  conda_build_config = os.path.join(mydir, 'data', 'conda_build_config.yaml')
+  recipe_append = os.path.join(mydir, 'data', 'recipe_append.yaml')
+  logger.info('Merging conda configuration files...')
+
+  condarc = os.path.join(prefix, 'condarc')
+  logger.info('Loading (this build\'s) CONDARC file from %s...', condarc)
+  with open(condarc, 'rb') as f:
+    condarc_options = yaml.load(f)
+
+  conda_config = make_conda_config(conda_build_config, pyver, recipe_append,
+      condarc_options)
+
+  version = open("version.txt").read().rstrip()
+  os.environ['BOB_PACKAGE_VERSION'] = version
+  logger.info('os.environ["%s"] = %s', 'BOB_PACKAGE_VERSION', version)
+
+  # if we're build a stable release, ensure a tag is set
+  parsed_version = packaging.version.Version(version)
+  if parsed_version.is_prerelease:
+    if os.environ.get('CI_COMMIT_TAG') is not None:
+      raise EnvironmentError('"version.txt" indicates version is a ' \
+          'pre-release (v%s) - but os.environ["CI_COMMIT_TAG"]="%s", ' \
+          'which indicates this is a **stable** build. ' \
+          'Have you created the tag using ``bdt release``?', version,
+          os.environ['CI_COMMIT_TAG'])
+  else:  #it is a stable build
+    if os.environ.get('CI_COMMIT_TAG') is None:
+      raise EnvironmentError('"version.txt" indicates version is a ' \
+          'stable build (v%s) - but there is no os.environ["CI_COMMIT_TAG"] ' \
+          'variable defined, which indicates this is **not** ' \
+          'a tagged build. Use ``bdt release`` to create stable releases',
+          version)
+
+  build_number = next_build_number(channels[0], name, version, python)
+  os.environ['BOB_BUILD_NUMBER'] = build_number
+  logger.info('os.environ["%s"] = %s', 'BOB_BUILD_NUMBER', build_number)
+
+  # runs the build using the conda-build API
+  arch = osname()
+  logger.info('Building %s-%s-py%s (build: %d) for %s',
+      rendered_recipe['package']['name'],
+      rendered_recipe['package']['version'], pyver.replace('.',''),
+      build_number, arch)
+  conda_build.api.build(d, config=conda_config)
+
+  # runs git clean to clean everything that is not needed. This helps to keep
+  # the disk usage on CI machines to a minimum.
+  exclude_from_cleanup = [
+      "miniconda.sh",   #the installer, cached
+      "miniconda/pkgs/*.tar.bz2",  #downloaded packages, cached
+      "miniconda/pkgs/urls.txt",  #download index, cached
+      "miniconda/conda-bld/${_os}-64/*.tar.bz2",  #build artifact -- conda
+      "dist/*.zip",  #build artifact -- pypi package
+      "sphinx",  #build artifact -- documentation
+      ]
+  bootstrap.run_cmdline(['git', 'clean', '-ffdx'] + \
+      ['--exclude=%s' % k for k in exclude_from_cleanup])
diff --git a/bob/devtools/conda.py b/bob/devtools/conda.py
index 70c04d49..1cba0f30 100644
--- a/bob/devtools/conda.py
+++ b/bob/devtools/conda.py
@@ -9,79 +9,3 @@ 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)
-
-
-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/constants.py b/bob/devtools/constants.py
index b186d73e..facc0b7e 100644
--- a/bob/devtools/constants.py
+++ b/bob/devtools/constants.py
@@ -9,10 +9,11 @@ import pkg_resources
 import logging
 logger = logging.getLogger(__name__)
 
+from . import bootstrap
 
-CONDARC = pkg_resources.resource_filename(__name__,
-    os.path.join('data', 'build-condarc'))
-'''The .condarc to use for building and creating new environments'''
+
+BASE_CONDARC = bootstrap._BASE_CONDARC
+'''Default setup for conda builds'''
 
 
 CONDA_BUILD_CONFIG = pkg_resources.resource_filename(__name__,
@@ -25,23 +26,10 @@ CONDA_RECIPE_APPEND = pkg_resources.resource_filename(__name__,
 '''Extra information to be appended to every recipe upon building'''
 
 
-SERVER = 'http://www.idiap.ch'
+SERVER = bootstrap._SERVER
 '''This is the default server use use to store data and build artifacts'''
 
 
-CONDA_CHANNELS = {
-    True: {  #stable?
-      False: '/private/conda',  #visible outside?
-      True: '/public/conda',
-      },
-    False: {
-      False: '/private/conda/label/beta',  #visible outside?
-      True: '/public/conda/label/beta',
-      },
-    }
-'''Default locations of our stable, beta, public and private conda channels'''
-
-
 WEBDAV_PATHS = {
     True: {  #stable?
       False: {  #visible?
@@ -136,20 +124,3 @@ MATPLOTLIB_RCDIR = pkg_resources.resource_filename(__name__, 'data')
 
 It is required for certain builds that use matplotlib functionality.
 '''
-
-
-def set_environment(name, value, env=os.environ):
-    '''Function to setup the environment variable and print debug message
-
-    Args:
-
-      name: The name of the environment variable to set
-      value: The value to set the environment variable to
-      env: Optional environment (dictionary) where to set the variable at
-    '''
-
-    if name in env:
-      logger.warn('Overriding existing environment variable ${%s} (was: "%s")',
-          name, env[name])
-    env[name] = value
-    logger.debug('$ export %s="%s"', name, value)
diff --git a/bob/devtools/create.py b/bob/devtools/create.py
deleted file mode 100644
index ed3077eb..00000000
--- a/bob/devtools/create.py
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/env python
-# vim: set fileencoding=utf-8 :
-
-'''Methods to create working environments based on conda-packages'''
-
-import os
-import json
-import shutil
-import subprocess
-
-import logging
-logger = logging.getLogger(__name__)
-
-import yaml
-
-
-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'''
-
-  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'''
-
-  from conda_build.api import output_yaml
-  output = output_yaml(metadata[0][0])
-  return yaml.load(output)
-
-
-def remove_pins(deps):
-  return [l.split()[0] for l in deps]
-
-
-def parse_dependencies(recipe_dir, 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', []) + \
-      recipe.get('test', {}).get('requires', []) + \
-      ['bob.buildout', 'mr.developer', 'ipdb']
-      # by last, packages required for local dev
-
-
-def get_env_directory(conda, name):
-
-  cmd = [conda, 'env', 'list', '--json']
-  output = subprocess.check_output(cmd)
-  data = json.loads(output)
-  retval = [k for k in data.get('envs', []) if k.endswith(os.sep + name)]
-  if retval:
-    return retval[0]
-  return None
-
-
-def conda_create(conda, name, overwrite, condarc, packages, dry_run, use_local):
-
-  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(' ', '='))
-
-  # if the current environment exists, delete it first
-  envdir = get_env_directory(conda, name)
-  if envdir is not None:
-    if overwrite:
-      cmd = [conda, 'env', 'remove', '--yes', '--name', name]
-      logger.debug('$ ' + ' '.join(cmd))
-      if not dry_run:
-        status = subprocess.call(cmd)
-        if status != 0:
-          return status
-    else:
-      raise RuntimeError('environment `%s\' exists in `%s\' - use '
-                         '--overwrite to overwrite' % (name, envdir))
-
-  cmd = [conda, 'create', '--yes', '--name', name]
-  if dry_run:
-    cmd.append('--dry-run')
-  if use_local:
-     cmd.append('--use-local')
-  cmd.extend(sorted(specs))
-  logger.debug('$ ' + ' '.join(cmd))
-  status = subprocess.call(cmd)
-  if status != 0:
-    return status
-
-  # copy the used condarc file to the just created environment
-  if not dry_run:
-    # get envdir again - it may just be created!
-    envdir = get_env_directory(conda, name)
-    destrc = os.path.join(envdir, '.condarc')
-    logger.debug('$ cp %s -> %s' % (condarc, destrc))
-    shutil.copy2(condarc, destrc)
-
-  return status
diff --git a/bob/devtools/data/build-condarc b/bob/devtools/data/build-condarc
deleted file mode 100644
index aacfae4c..00000000
--- a/bob/devtools/data/build-condarc
+++ /dev/null
@@ -1,14 +0,0 @@
-default_channels:
-  - https://repo.anaconda.com/pkgs/main
-  - https://repo.anaconda.com/pkgs/free
-  - https://repo.anaconda.com/pkgs/r
-  - https://repo.anaconda.com/pkgs/pro
-add_pip_as_python_dependency: false
-show_channel_urls: true
-anaconda_upload: false
-ssl_verify: false
-quiet: true
-channels:
-  - https://www.idiap.ch/software/bob/conda/label/beta
-  - https://www.idiap.ch/software/bob/conda
-  - defaults
diff --git a/bob/devtools/scripts/bdt.py b/bob/devtools/scripts/bdt.py
index 7f19a7ed..14784527 100644
--- a/bob/devtools/scripts/bdt.py
+++ b/bob/devtools/scripts/bdt.py
@@ -55,7 +55,8 @@ def main():
     """Bob Development Tools - see available commands below"""
 
     #sets up basic environment variables required everywhere
-    from ..constants import CACERT, set_environment
+    from ..constants import CACERT
+    from ..bootstrap import set_environment
 
     set_environment('SSL_CERT_FILE', CACERT, os.environ)
     set_environment('REQUESTS_CA_BUNDLE', CACERT, os.environ)
diff --git a/bob/devtools/scripts/build.py b/bob/devtools/scripts/build.py
index 8aabd915..c60ce7f1 100644
--- a/bob/devtools/scripts/build.py
+++ b/bob/devtools/scripts/build.py
@@ -8,14 +8,15 @@ logger = logging.getLogger(__name__)
 
 import pkg_resources
 import click
+import yaml
 
 from . import bdt
 from ..log import verbosity_option
-from ..conda import next_build_number, osname
-from ..create import get_rendered_metadata, get_parsed_recipe, \
-    make_conda_config
-from ..constants import CONDARC, CONDA_BUILD_CONFIG, CONDA_RECIPE_APPEND, \
-    SERVER, MATPLOTLIB_RCDIR, set_environment
+from ..build import next_build_number, osname, should_skip_build, \
+    get_rendered_metadata, get_parsed_recipe, make_conda_config
+from ..constants import CONDA_BUILD_CONFIG, CONDA_RECIPE_APPEND, \
+    SERVER, MATPLOTLIB_RCDIR, BASE_CONDARC
+from ..bootstrap import set_environment, get_channels
 
 
 @click.command(epilog='''
@@ -23,55 +24,59 @@ Examples:
 
   1. Builds recipe from one of our build dependencies (inside bob.conda):
 
+\b
      $ 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):
+  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
+     $ bdt build --python=3.6 -vv path/to/recipe-dir1 path/to/recipe-dir2
 ''')
 @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=CONDARC, show_default=True,
-    help='overwrites the path leading to the condarc file to use',)
+@click.option('-r', '--condarc',
+    help='Use custom conda configuration file instead of our own',)
 @click.option('-m', '--config', '--variant-config-files', show_default=True,
-      default=CONDA_BUILD_CONFIG, 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')
+    default=CONDA_BUILD_CONFIG, help='overwrites the path leading to ' \
+        'variant configuration file to use')
 @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=CONDA_RECIPE_APPEND, help='overwrites the path leading to ' \
-          'appended configuration file to use')
-@click.option('-D', '--docserver', show_default=True,
-      default=SERVER, help='Server used for uploading artifacts ' \
-          'and other goodies')
+    default=CONDA_RECIPE_APPEND, help='overwrites the path leading to ' \
+        'appended configuration file to use')
+@click.option('-S', '--server', show_default=True,
+    default='https://www.idiap.ch/software/bob', help='Server used for ' \
+    'downloading conda packages and documentation indexes of required packages')
+@click.option('-P', '--private/--no-private', default=False,
+    help='Set this to **include** private channels on your build - ' \
+        'you **must** be at Idiap to execute this build in this case - ' \
+        'you **must** also use the correct server name through --server - ' \
+        'notice this option has no effect if you also pass --condarc')
+@click.option('-X', '--stable/--no-stable', default=False,
+    help='Set this to **exclude** beta channels from your build - ' \
+        'notice this option has no effect if you also pass --condarc')
 @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):
+def build(recipe_dir, python, condarc, config, no_test, append_file,
+    server, private, stable, dry_run):
   """Builds package through conda-build with stock configuration
 
   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``.
+  conda configuration 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.
@@ -84,13 +89,25 @@ def build(recipe_dir, python, condarc, config, channel, no_test, append_file,
 
   recipe_dir = recipe_dir or [os.path.join(os.path.realpath('.'), 'conda')]
 
-  logger.debug("CONDARC=%s", condarc)
+  # get potential channel upload and other auxiliary channels
+  channels = get_channels(public=(not private), stable=stable, server=server,
+      intranet=private)
+  channel = channels[0]  # where we would upload this package
 
-  conda_config = make_conda_config(config, python, append_file, condarc)
+  if condarc is not None:
+    logger.info('Loading CONDARC file from %s...', condarc)
+    with open(condarc, 'rb') as f:
+      condarc_options = yaml.load(f)
+  else:
+    # use default and add channels
+    condarc_options = yaml.load(BASE_CONDARC)  #n.b.: no channels
+    condarc_options['channels'] = channels + ['defaults']
+
+  conda_config = make_conda_config(config, python, append_file, condarc_options)
 
   set_environment('LANG', 'en_US.UTF-8', os.environ)
   set_environment('LC_ALL', os.environ['LANG'], os.environ)
-  set_environment('DOCSERVER', docserver, os.environ)
+  set_environment('DOCSERVER', server, os.environ)
   set_environment('MATPLOTLIBRC', MATPLOTLIB_RCDIR, os.environ)
 
   for d in recipe_dir:
@@ -107,7 +124,6 @@ def build(recipe_dir, python, condarc, config, channel, no_test, append_file,
     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())
@@ -126,8 +142,6 @@ def build(recipe_dir, python, condarc, config, channel, no_test, append_file,
 
     set_environment('BOB_BUILD_NUMBER', str(build_number), os.environ)
 
-    # 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('.',''),
diff --git a/bob/devtools/scripts/create.py b/bob/devtools/scripts/create.py
index e09bca53..57abe640 100644
--- a/bob/devtools/scripts/create.py
+++ b/bob/devtools/scripts/create.py
@@ -12,9 +12,10 @@ import yaml
 
 from . import bdt
 from ..log import verbosity_option
-from ..create import parse_dependencies, conda_create, make_conda_config
-from ..constants import CONDARC, CONDA_BUILD_CONFIG, CONDA_RECIPE_APPEND, \
-    SERVER
+from ..build import parse_dependencies, conda_create, make_conda_config
+from ..constants import BASE_CONDARC, CONDA_BUILD_CONFIG, \
+    CONDA_RECIPE_APPEND, SERVER
+from ..bootstrap import set_environment, get_channels
 
 
 @click.command(epilog='''
@@ -61,27 +62,35 @@ Examples:
       help='If set and an environment with the same name exists, ' \
           'deletes it first before creating the new environment',
           show_default=True)
-@click.option('-r', '--condarc', default=CONDARC, show_default=True,
-    help='overwrites the path leading to the condarc file to use',)
+@click.option('-r', '--condarc',
+    help='Use custom conda configuration file instead of our own',)
+@click.option('-l', '--use-local', default=False,
+    help='Allow the use of local channels for package retrieval')
 @click.option('-m', '--config', '--variant-config-files', show_default=True,
       default=CONDA_BUILD_CONFIG, help='overwrites the path leading to ' \
           'variant configuration file to use')
 @click.option('-a', '--append-file', show_default=True,
       default=CONDA_RECIPE_APPEND, help='overwrites the path leading to ' \
           'appended configuration file to use')
-@click.option('-D', '--docserver', show_default=True,
-      default=SERVER, help='Server used for uploading artifacts ' \
-          'and other goodies')
+@click.option('-S', '--server', show_default=True,
+    default='https://www.idiap.ch/software/bob', help='Server used for ' \
+    'downloading conda packages and documentation indexes of required packages')
+@click.option('-P', '--private/--no-private', default=False,
+    help='Set this to **include** private channels on your build - ' \
+        'you **must** be at Idiap to execute this build in this case - ' \
+        'you **must** also use the correct server name through --server - ' \
+        'notice this option has no effect if you also pass --condarc')
+@click.option('-X', '--stable/--no-stable', default=False,
+    help='Set this to **exclude** beta channels from your build - ' \
+        'notice this option has no effect if you also pass --condarc')
 @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')
-@click.option('--use-local', default=False,
-    help='Allow the use of local channels for package retrieval')
 @verbosity_option()
 @bdt.raise_on_error
-def create(name, recipe_dir, python, overwrite, condarc, config,
-    append_file, docserver, dry_run, use_local):
+def create(name, recipe_dir, python, overwrite, condarc, use_local, config,
+    append_file, server, private, stable, dry_run):
   """Creates a development environment for a recipe
 
   It uses the conda render API to render a recipe and install an environment
@@ -112,12 +121,23 @@ def create(name, recipe_dir, python, overwrite, condarc, config,
         "properly?")
 
   # set some environment variables before continuing
-  set_environment('CONDARC', condarc, os.environ)
-  set_environment('SERVER', docserver, os.environ)
+  set_environment('DOCSERVER', server, os.environ)
   set_environment('LANG', 'en_US.UTF-8', os.environ)
   set_environment('LC_ALL', os.environ['LANG'], os.environ)
 
-  conda_config = make_conda_config(config, python, append_file, condarc)
+  if condarc is not None:
+    logger.info('Loading CONDARC file from %s...', condarc)
+    with open(condarc, 'rb') as f:
+      condarc_options = yaml.load(f)
+  else:
+    # use default and add channels
+    condarc_options = yaml.load(BASE_CONDARC)  #n.b.: no channels
+    channels = get_channels(public=(not private), stable=stable, server=server,
+        intranet=private)
+    condarc_options['channels'] = channels + ['defaults']
+
+  conda_config = make_conda_config(config, python, append_file, condarc_options)
   deps = parse_dependencies(recipe_dir, conda_config)
-  status = conda_create(conda, name, overwrite, condarc, deps, dry_run, use_local)
+  status = conda_create(conda, name, overwrite, condarc_options, deps,
+      dry_run, use_local)
   click.echo('Execute on your shell: "conda activate %s"' % name)
diff --git a/ci/build.sh b/ci/build.sh
deleted file mode 100755
index 15db005e..00000000
--- a/ci/build.sh
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env bash
-
-# datetime prefix for logging
-log_datetime() {
-	echo "($(date +%T.%3N))"
-}
-
-# Functions for coloring echo commands
-log_info() {
-  echo -e "$(log_datetime) \033[1;34m${@}\033[0m"
-}
-
-
-log_error() {
-  echo -e "$(log_datetime) \033[1;31mError: ${@}\033[0m" >&2
-}
-
-# Function for running command and echoing results
-run_cmd() {
-  log_info "$ ${@}"
-  ${@}
-  local status=$?
-  if [ ${status} != 0 ]; then
-    log_error "Command Failed \"${@}\""
-    exit ${status}
-  fi
-}
-
-# Checks just if the variable is defined and has non-zero length
-check_defined() {
-  if [ -z "${!1+abc}" ]; then
-    log_error "Variable ${1} is undefined - aborting...";
-    exit 1
-  elif [ -z "${!1}" ]; then
-    log_error "Variable ${1} is zero-length - aborting...";
-    exit 1
-  fi
-  log_info "${1}=${!1}"
-}
-
-
-check_defined CONDA_ROOT
-check_defined CI_PROJECT_DIR
-check_defined CI_PROJECT_NAME
-check_defined PYTHON_VERSION
-
-export DOCSERVER=http://www.idiap.ch
-check_defined DOCSERVER
-
-export CONDARC="${CONDA_ROOT}/condarc"
-check_defined CONDARC
-
-export BOB_PACKAGE_VERSION=`cat version.txt | tr -d '\n'`;
-check_defined BOB_PACKAGE_VERSION
-
-# Makes sure we activate the base environment if available
-run_cmd source ${CONDA_ROOT}/etc/profile.d/conda.sh
-run_cmd conda activate base
-export PATH
-check_defined PATH
-
-CONDA_CHANNEL_ROOT="${DOCSERVER}/public/conda"
-check_defined CONDA_CHANNEL_ROOT
-if [ -z "${CI_COMMIT_TAG}" ]; then #building beta
-  UPLOAD_CHANNEL="${CONDA_CHANNEL_ROOT}/label/beta"
-else
-  UPLOAD_CHANNEL="${CONDA_CHANNEL_ROOT}"
-fi
-check_defined UPLOAD_CHANNEL
-
-log_info "$ ${CONDA_ROOT}/bin/python ${CI_PROJECT_DIR}/ci/nextbuild.py ${UPLOAD_CHANNEL} ${CI_PROJECT_NAME} ${BOB_PACKAGE_VERSION} ${PYTHON_VERSION}"
-export BOB_BUILD_NUMBER=$(${CONDA_ROOT}/bin/python ${CI_PROJECT_DIR}/ci/nextbuild.py ${UPLOAD_CHANNEL} ${CI_PROJECT_NAME} ${BOB_PACKAGE_VERSION} ${PYTHON_VERSION})
-check_defined BOB_BUILD_NUMBER
-
-# copy the recipe_append.yaml over before build
-run_cmd cp ${CI_PROJECT_DIR}/bob/devtools/data/recipe_append.yaml conda/
-run_cmd cp ${CI_PROJECT_DIR}/bob/devtools/data/conda_build_config.yaml conda/
-
-# to build, we only rely on the stable channel and defaults
-run_cmd ${CONDA_ROOT}/bin/conda build --override-channels -c "${CONDA_CHANNEL_ROOT} -c defaults --python=${PYTHON_VERSION} --no-anaconda-upload" conda
-
-# run git clean to clean everything that is not needed. This helps to keep the
-# disk usage on CI machines to minimum.
-if [ "$(uname -s)" == "Linux" ]; then
-  _os="linux"
-else
-  _os="osx"
-fi
-run_cmd git clean -ffdx \
-    --exclude="miniconda.sh" \
-    --exclude="miniconda/pkgs/*.tar.bz2" \
-    --exclude="miniconda/pkgs/urls.txt" \
-    --exclude="miniconda/conda-bld/${_os}-64/*.tar.bz2" \
-    --exclude="dist/*.zip" \
-    --exclude="sphinx"
diff --git a/doc/api.rst b/doc/api.rst
index cf688d17..0d5e1250 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -12,7 +12,6 @@
    bob.devtools.constants
    bob.devtools.release
    bob.devtools.changelog
-   bob.devtools.create
    bob.devtools.bootstrap
    bob.devtools.build
    bob.devtools.webdav3.client
@@ -33,8 +32,6 @@ Detailed Information
 
 .. automodule:: bob.devtools.changelog
 
-.. automodule:: bob.devtools.create
-
 .. automodule:: bob.devtools.bootstrap
 
 .. automodule:: bob.devtools.build
-- 
GitLab