From 6e6b148e2ea3c8a0b287c3588c2d78c4a5810954 Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.dos.anjos@gmail.com>
Date: Thu, 14 Feb 2019 10:39:22 +0100
Subject: [PATCH] [build] Support multi-python base builds

---
 bob/devtools/build.py                       | 51 +++++++++++++++------
 bob/devtools/data/gitlab-ci/base-build.yaml | 12 ++---
 bob/devtools/scripts/ci.py                  | 26 ++++++++---
 3 files changed, 62 insertions(+), 27 deletions(-)

diff --git a/bob/devtools/build.py b/bob/devtools/build.py
index 709529a4..2045b8a1 100644
--- a/bob/devtools/build.py
+++ b/bob/devtools/build.py
@@ -86,7 +86,10 @@ def next_build_number(channel_url, name, version, python):
   for dist in index:
 
     if dist.name == name and dist.version == version:
-      match = re.match('py[2-9][0-9]+', dist.build_string)
+      if py_ver:
+        match = re.match('py[2-9][0-9]+', dist.build_string)
+      else:
+        match = re.match('py', 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,
@@ -165,7 +168,9 @@ def exists_on_channel(channel_url, name, version, build_number,
     name: The name of the package
     version: The version of the package
     build_number: The build number of the package
-    python_version: The current version of python we're building for
+    python_version: The current version of python we're building for.  May be
+      ``noarch``, to check for "noarch" packages or ``None``, in which case we
+      don't check for the python version
 
   Returns: A complete package name, version and build string, if the package
   already exists in the channel or ``None`` otherwise.
@@ -174,8 +179,10 @@ def exists_on_channel(channel_url, name, version, build_number,
 
   from conda.exports import get_index
 
-  # no dot in py_ver
-  py_ver = python_version.replace('.', '')
+  # handles different cases as explained on the description of
+  # ``python_version``
+  py_ver = python_version.replace('.', '') if python_version else None
+  if py_ver == 'noarch': py_ver = ''
 
   # get the channel index
   logger.debug('Downloading channel index from %s', channel_url)
@@ -204,8 +211,12 @@ def exists_on_channel(channel_url, name, version, build_number,
             dist.version, dist.build_string)
         return (dist.name, dist.version, dist.build_string)
 
-  logger.info('No matches for %s-%s-(py%s_?)%s found among %d packages',
-      name, version, py_ver, build_number, len(index))
+  if py_ver is None:
+    logger.info('No matches for %s-%s-%s found among %d packages',
+        name, version, build_number, len(index))
+  else:
+    logger.info('No matches for %s-%s-py%s_%s found among %d packages',
+        name, version, py_ver, build_number, len(index))
   return
 
 
@@ -483,7 +494,10 @@ def base_build(bootstrap, server, intranet, recipe_dir, conda_build_config,
     recipe_dir: The directory containing the recipe's ``meta.yaml`` file
     conda_build_config: Path to the ``conda_build_config.yaml`` file to use
     python_version: String with the python version to build for, in the format
-      ``x.y`` (should be passed even if not building a python package)
+      ``x.y`` (should be passed even if not building a python package).  It
+      can also be set to ``noarch``, or ``None``.  If set to ``None``, then we
+      don't assume there is a python-specific version being built.  If set to
+      ``noarch``, then it is a python package without a specific build.
     condarc_options: Pre-parsed condarc options loaded from the respective YAML
       file
 
@@ -498,8 +512,12 @@ def base_build(bootstrap, server, intranet, recipe_dir, conda_build_config,
   condarc_options['channels'] = public_channels + ['defaults']
 
   logger.info('Merging conda configuration files...')
-  conda_config = make_conda_config(conda_build_config, python_version,
-      None, condarc_options)
+  if python_version not in ('noarch', None):
+    conda_config = make_conda_config(conda_build_config, python_version,
+        None, condarc_options)
+  else:
+    conda_config = make_conda_config(conda_build_config, None, None,
+        condarc_options)
 
   metadata = get_rendered_metadata(recipe_dir, conda_config)
   recipe = get_parsed_recipe(metadata)
@@ -508,8 +526,10 @@ def base_build(bootstrap, server, intranet, recipe_dir, conda_build_config,
     logger.info('Skipping build for %s - rendering returned None', recipe_dir)
     return
 
-  # no dot in py_ver
-  py_ver = python_version.replace('.', '')
+  # handles different cases as explained on the description of
+  # ``python_version``
+  py_ver = python_version.replace('.', '') if python_version else None
+  if py_ver == 'noarch': py_ver = ''
   arch = conda_arch()
 
   candidate = exists_on_channel(public_channels[0], recipe['package']['name'],
@@ -521,9 +541,14 @@ def base_build(bootstrap, server, intranet, recipe_dir, conda_build_config,
     return
 
   # if you get to this point, just builds the package
-    logger.info('Building %s-%s-(py%s_?)%s for %s',
+  if py_ver is None:
+    logger.info('Building %s-%s-%s for %s',
       recipe['package']['name'], recipe['package']['version'],
-      recipe['build']['number'], py_ver, arch)
+      recipe['build']['number'], arch)
+  else:
+    logger.info('Building %s-%s-py%s_%s for %s',
+      recipe['package']['name'], recipe['package']['version'], py_ver,
+      recipe['build']['number'], arch)
   conda_build.api.build(recipe_dir, config=conda_config)
 
 
diff --git a/bob/devtools/data/gitlab-ci/base-build.yaml b/bob/devtools/data/gitlab-ci/base-build.yaml
index 8fdc9374..a9f616d9 100644
--- a/bob/devtools/data/gitlab-ci/base-build.yaml
+++ b/bob/devtools/data/gitlab-ci/base-build.yaml
@@ -21,7 +21,8 @@ stages:
     - python3 bootstrap.py -vv channel base
     - source ${CONDA_ROOT}/etc/profile.d/conda.sh
     - conda activate base
-    - bdt ci base-build -vv order.txt
+    - bdt ci base-build -vv nopython.txt
+    - bdt ci base-build -vv --python=3.6 python.txt
     - '[ "${CI_COMMIT_REF_NAME}" = "master" ] && bdt ci base-deploy -vv'
     - bdt ci clean -vv
   cache: &build_caches
@@ -31,10 +32,8 @@ stages:
       - ${CONDA_ROOT}/pkgs/urls.txt
 
 
-build_linux_36:
+build_linux:
   <<: *build_job
-  variables:
-    PYTHON_VERSION: "3.6"
   tags:
     - docker
   image: continuumio/conda-concourse-ci
@@ -42,10 +41,9 @@ build_linux_36:
     <<: *build_caches
     key: "linux-cache"
 
-build_macosx_36:
+
+build_macosx:
   <<: *build_job
-  variables:
-    PYTHON_VERSION: "3.6"
   tags:
     - macosx
   cache:
diff --git a/bob/devtools/scripts/ci.py b/bob/devtools/scripts/ci.py
index de5aea19..af9aff04 100644
--- a/bob/devtools/scripts/ci.py
+++ b/bob/devtools/scripts/ci.py
@@ -310,13 +310,16 @@ Examples:
 ''')
 @click.argument('order', required=True, type=click.Path(file_okay=True,
   dir_okay=False, exists=True), nargs=1)
+@click.option('-p', '--python', multiple=True,
+    help='Versions of python in the format "x.y" we should build for.  Pass ' \
+        'various times this option to build for multiple python versions')
 @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 base_build(order, dry_run):
+def base_build(order, python, dry_run):
   """Builds base (dependence) packages
 
   This command builds dependence packages (packages that are not Bob/BEAT
@@ -343,17 +346,26 @@ def base_build(order, dry_run):
       line = line.partition('#')[0].strip()
       if line: recipes.append(line)
 
+  import itertools
   from .. import bootstrap
 
-  for order, recipe in enumerate(recipes):
-    click.echo('\n' + (60*'='))
-    click.echo('Building "%s" (%d/%d)' % (recipe, order+1, len(recipes)))
-    click.echo((60*'=') + '\n')
+  # combine all versions of python with recipes
+  if python:
+    recipes = list(itertools.product(python, recipes))
+  else:
+    recipes = list(itertools.product([None], recipes))
+
+  for order, (pyver, recipe) in enumerate(recipes):
+    click.echo('\n' + (80*'='))
+    pytext = 'for python-%s' % pyver if pyver is not None else ''
+    click.echo('Building "%s" %s(%d/%d)' % \
+        (recipe, pytext, order+1, total_recipes))
+    click.echo((80*'=') + '\n')
     if not os.path.exists(os.path.join(recipe, 'meta.yaml')):
       logger.info('Ignoring directory "%s" - no meta.yaml found' % recipe)
       continue
-    _build(bootstrap, SERVER, True, recipe, CONDA_BUILD_CONFIG,
-        os.environ['PYTHON_VERSION'], condarc_options)
+    _build(bootstrap, SERVER, True, recipe, CONDA_BUILD_CONFIG, pyver,
+        condarc_options)
 
 
 @ci.command(epilog='''
-- 
GitLab