diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1ca218d3bf46f1798f288de17bafc93850a5ead8..2b6dd69b2effa39f1b00ac143fd6f10cab65a90d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,11 +14,6 @@ stages:
 # Build targets
 .build_template:
   stage: build
-  script:
-    - python3 ./bob/devtools/bootstrap.py -vv build
-    - source ${CONDA_ROOT}/etc/profile.d/conda.sh
-    - conda activate base
-    - python3 ./bob/devtools/build.py -vv
   artifacts:
     expire_in: 1 week
   cache:
@@ -26,16 +21,25 @@ stages:
       - miniconda.sh
 
 
-.build_linux_template:
+build_linux:
   extends: .build_template
+  variables:
+    BUILD_EGG: "true"
   tags:
     - docker
   image: continuumio/conda-concourse-ci
   before_script:
     - rm -f /root/.condarc
     - rm -rf /root/.conda
+  script:
+    - python3 ./bob/devtools/bootstrap.py -vv build
+    - source ${CONDA_ROOT}/etc/profile.d/conda.sh
+    - conda activate base
+    - python3 ./bob/devtools/build.py -vv --twine-check
   artifacts:
     paths:
+      - dist/*.zip
+      - sphinx
       - ${CONDA_ROOT}/conda-bld/linux-64/*.conda
       - ${CONDA_ROOT}/conda-bld/noarch/*.conda
       - ${CONDA_ROOT}/conda-bld/linux-64/*.tar.bz2
@@ -44,64 +48,23 @@ stages:
     key: "linux-cache"
 
 
-.build_macosx_template:
+build_macosx:
   extends: .build_template
   tags:
     - macosx
-  artifacts:
-    paths:
-      - ${CONDA_ROOT}/conda-bld/osx-64/*.conda
-      - ${CONDA_ROOT}/conda-bld/noarch/*.conda
-      - ${CONDA_ROOT}/conda-bld/osx-64/*.tar.bz2
-      - ${CONDA_ROOT}/conda-bld/noarch/*.tar.bz2
-  cache:
-    key: "macosx-cache"
-
-
-build_linux_36:
-  extends: .build_linux_template
-  variables:
-    PYTHON_VERSION: "3.6"
-
-build_linux_37:
-  extends: .build_linux_template
-  variables:
-    PYTHON_VERSION: "3.7"
-
-build_linux_38:
-  extends: .build_linux_template
-  variables:
-    PYTHON_VERSION: "3.8"
-    BUILD_EGG: "true"
   script:
     - python3 ./bob/devtools/bootstrap.py -vv build
     - source ${CONDA_ROOT}/etc/profile.d/conda.sh
     - conda activate base
-    - python3 ./bob/devtools/build.py -vv --twine-check
+    - python3 ./bob/devtools/build.py -vv
   artifacts:
     paths:
-      - dist/*.zip
-      - sphinx
-      - ${CONDA_ROOT}/conda-bld/linux-64/*.conda
+      - ${CONDA_ROOT}/conda-bld/osx-64/*.conda
       - ${CONDA_ROOT}/conda-bld/noarch/*.conda
-      - ${CONDA_ROOT}/conda-bld/linux-64/*.tar.bz2
+      - ${CONDA_ROOT}/conda-bld/osx-64/*.tar.bz2
       - ${CONDA_ROOT}/conda-bld/noarch/*.tar.bz2
-
-
-build_macosx_36:
-  extends: .build_macosx_template
-  variables:
-    PYTHON_VERSION: "3.6"
-
-build_macosx_37:
-  extends: .build_macosx_template
-  variables:
-    PYTHON_VERSION: "3.7"
-
-build_macosx_38:
-  extends: .build_macosx_template
-  variables:
-    PYTHON_VERSION: "3.8"
+  cache:
+    key: "macosx-cache"
 
 
 # Deploy targets
@@ -118,12 +81,8 @@ build_macosx_38:
     - bdt ci deploy -vv
     - bdt ci clean -vv
   dependencies:
-    - build_linux_36
-    - build_linux_37
-    - build_linux_38
-    - build_macosx_36
-    - build_macosx_37
-    - build_macosx_38
+    - build_linux
+    - build_macosx
   tags:
     - docker
   cache:
@@ -164,12 +123,8 @@ pypi:
     - bdt ci pypi -vv dist/*.zip
     - bdt ci clean -vv
   dependencies:
-    - build_linux_36
-    - build_linux_37
-    - build_linux_38
-    - build_macosx_36
-    - build_macosx_37
-    - build_macosx_38
+    - build_linux
+    - build_macosx
   tags:
     - docker
   cache:
diff --git a/bob/devtools/build.py b/bob/devtools/build.py
index 93ca35625668ee9f20bbf71f7693096878cdcd49..1b6492034ef329e190f22d6f83f92147c78aaef1 100644
--- a/bob/devtools/build.py
+++ b/bob/devtools/build.py
@@ -102,7 +102,9 @@ def next_build_number(channel_url, basename):
     remove_conda_loggers()
 
     # get the channel index
-    channel_urls = calculate_channel_urls([channel_url], prepend=False, use_local=False)
+    channel_urls = calculate_channel_urls(
+        [channel_url], prepend=False, use_local=False
+    )
     logger.debug("Downloading channel index from %s", channel_urls)
     index = fetch_index(channel_urls=channel_urls)
 
@@ -215,7 +217,7 @@ def make_conda_config(config, python, append_file, condarc_options):
 def get_output_path(metadata, config):
     """Renders the recipe and returns the name of the output file."""
 
-    return conda_build.api.get_output_file_paths(metadata, config=config)[0]
+    return conda_build.api.get_output_file_paths(metadata, config=config)
 
 
 def get_rendered_metadata(recipe_dir, config):
@@ -565,7 +567,6 @@ def base_build(
     group,
     recipe_dir,
     conda_build_config,
-    python_version,
     condarc_options,
 ):
     """Builds a non-beat/non-bob software dependence that doesn't exist on
@@ -590,11 +591,6 @@ def base_build(
         our internal webserver.  Currently, only "bob" or "beat" will work.
       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).  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
 
@@ -607,8 +603,7 @@ def base_build(
 
     # if you get to this point, tries to build the package
     channels = bootstrap.get_channels(
-        public=True, stable=True, server=server, intranet=intranet,
-        group=group
+        public=True, stable=True, server=server, intranet=intranet, group=group
     )
 
     if "channels" not in condarc_options:
@@ -619,54 +614,40 @@ def base_build(
         "\n  - ".join(condarc_options["channels"]),
     )
     logger.info("Merging conda configuration files...")
-    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
-        )
+    conda_config = make_conda_config(
+        conda_build_config, None, None, condarc_options
+    )
 
     metadata = get_rendered_metadata(recipe_dir, conda_config)
-
-    # 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()
 
     # checks we should actually build this recipe
     if should_skip_build(metadata):
-        if py_ver is None:
-            logger.warn(
-                'Skipping UNSUPPORTED build of "%s" on %s', recipe_dir, arch
-            )
-        elif not py_ver:
-            logger.warn(
-                'Skipping UNSUPPORTED build of "%s" for (noarch) python '
-                "on %s",
-                recipe_dir,
-                arch,
-            )
-        else:
-            logger.warn(
-                'Skipping UNSUPPORTED build of "%s" for python-%s ' "on %s",
-                recipe_dir,
-                python_version,
-                arch,
-            )
+        logger.warn(
+            'Skipping UNSUPPORTED build of "%s" on %s', recipe_dir, arch
+        )
         return
 
-    path = get_output_path(metadata, conda_config)
+    paths = get_output_path(metadata, conda_config)
+    urls = [exists_on_channel(channels[0], os.path.basename(k)) for k in paths]
 
-    url = exists_on_channel(channels[0], os.path.basename(path))
-    if url is not None:
-        logger.info("Skipping build for %s as it exists (at %s)", path, url)
+    if all(urls):
+        logger.info(
+            "Skipping build(s) for recipe at '%s' as packages with matching "
+            "characteristics exist (%s)",
+            recipe_dir,
+            ", ".join(urls),
+        )
         return
 
-    # if you get to this point, just builds the package
+    if any(urls):
+        raise RuntimeError(
+            "One or more packages for recipe at '%s' already exist (%s). "
+            "Change the package build number to trigger a build." % \
+            (recipe_dir, ", ".join(urls)),
+        )
+
+    # if you get to this point, just builds the package(s)
     logger.info("Building %s", path)
     return conda_build.api.build(recipe_dir, config=conda_config)
 
@@ -718,14 +699,6 @@ if __name__ == "__main__":
         default=os.environ.get("CI_PROJECT_DIR", os.path.realpath(os.curdir)),
         help="The directory where the repo was cloned [default: %(default)s]",
     )
-    parser.add_argument(
-        "-p",
-        "--python-version",
-        default=os.environ.get(
-            "PYTHON_VERSION", "%d.%d" % sys.version_info[:2]
-        ),
-        help="The version of python to build for [default: %(default)s]",
-    )
     parser.add_argument(
         "-T",
         "--twine-check",
@@ -783,8 +756,9 @@ if __name__ == "__main__":
     bootstrap.set_environment("BOB_PACKAGE_VERSION", version)
 
     # 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")
+    conda_build_config = os.path.join(args.work_dir, "conda",
+            "conda_build_config.yaml")
+    recipe_append = os.path.join(args.work_dir, "data", "recipe_append.yaml")
 
     condarc = os.path.join(args.conda_root, "condarc")
     logger.info("Loading (this build's) CONDARC file from %s...", condarc)
@@ -812,11 +786,10 @@ if __name__ == "__main__":
             args.group,
             recipe,
             conda_build_config,
-            args.python_version,
             condarc_options,
         )
 
-    public = (args.visibility == "public")
+    public = args.visibility == "public"
     channels = bootstrap.get_channels(
         public=public,
         stable=(not is_prerelease),
@@ -834,36 +807,34 @@ if __name__ == "__main__":
     )
     logger.info("Merging conda configuration files...")
     conda_config = make_conda_config(
-        conda_build_config, args.python_version, recipe_append, condarc_options
+        conda_build_config, None, recipe_append, condarc_options
     )
 
     recipe_dir = os.path.join(args.work_dir, "conda")
     metadata = get_rendered_metadata(recipe_dir, conda_config)
-    path = get_output_path(metadata, conda_config)
+    paths = get_output_path(metadata, conda_config)
 
     # asserts we're building at the right location
-    assert path.startswith(os.path.join(args.conda_root, "conda-bld")), (
-        'Output path for build (%s) does not start with "%s" - this '
-        "typically means this build is running on a shared builder and "
-        "the file ~/.conda/environments.txt is polluted with other "
-        "environment paths.  To fix, empty that file and set its mode "
-        "to read-only for all."
-        % (path, os.path.join(args.conda_root, "conda-bld"))
-    )
+    for path in paths:
+        assert path.startswith(os.path.join(args.conda_root, "conda-bld")), (
+            'Output path for build (%s) does not start with "%s" - this '
+            "typically means this build is running on a shared builder and "
+            "the file ~/.conda/environments.txt is polluted with other "
+            "environment paths.  To fix, empty that file and set its mode "
+            "to read-only for all."
+            % (path, os.path.join(args.conda_root, "conda-bld"))
+        )
+
+    # retrieve the current build number(s) for this build
+    build_numbers = [
+        next_build_number(channels[0], os.path.basename(k))[0] for k in paths
+    ]
 
-    # retrieve the current build number for this build
-    build_number, _ = next_build_number(channels[0], os.path.basename(path))
+    # homogenize to the largest build number
+    build_number = max([int(k) for k in build_numbers])
 
     # runs the build using the conda-build API
     arch = conda_arch()
-    logger.info(
-        "Building %s-%s-py%s (build: %d) for %s",
-        args.name,
-        version,
-        args.python_version.replace(".", ""),
-        build_number,
-        arch,
-    )
 
     # notice we cannot build from the pre-parsed metadata because it has already
     # resolved the "wrong" build number.  We'll have to reparse after setting the
diff --git a/bob/devtools/graph.py b/bob/devtools/graph.py
index c7fe00912ed1ce94c78e7663d316c1248e11be47..a6b7296e627f2f3003973e516e84aedbaa7f3ea5 100644
--- a/bob/devtools/graph.py
+++ b/bob/devtools/graph.py
@@ -124,7 +124,7 @@ def compute_adjencence_matrix(
         # pre-renders the recipe - figures out the destination
         metadata = get_rendered_metadata(recipe_dir, conda_config)
         rendered_recipe = get_parsed_recipe(metadata)
-        path = get_output_path(metadata, conda_config)
+        path = get_output_path(metadata, conda_config)[0]
 
         # gets the next build number
         build_number, _ = next_build_number(
diff --git a/bob/devtools/scripts/build.py b/bob/devtools/scripts/build.py
index 9bc98d4566b7959b82e77ff72e7732547bfbee76..4601c46b7650e0f0f1320ff51ad276d4d4017b76 100644
--- a/bob/devtools/scripts/build.py
+++ b/bob/devtools/scripts/build.py
@@ -266,7 +266,7 @@ def build(
             continue
 
         rendered_recipe = get_parsed_recipe(metadata)
-        path = get_output_path(metadata, conda_config)
+        path = get_output_path(metadata, conda_config)[0]
 
         # gets the next build number
         build_number, _ = next_build_number(channels[0], os.path.basename(path))
diff --git a/bob/devtools/scripts/local.py b/bob/devtools/scripts/local.py
index d7bacd6a9598e0bcf080832852dcaff69a1469ce..a008757e12b7e8935f21d534476b5e4ec2271570 100644
--- a/bob/devtools/scripts/local.py
+++ b/bob/devtools/scripts/local.py
@@ -176,13 +176,6 @@ Examples:
     "(combine with the verbosity flags - e.g. ``-vvv``) to enable "
     "printing to help you understand what will be done",
 )
-@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(
     "-g",
     "--group",
@@ -196,7 +189,4 @@ Examples:
 def base_build(ctx, order, dry_run, python, group):
     """Run the CI build step locally."""
     set_up_environment_variables(python=python, name_space=group)
-
-    ctx.invoke(
-        ci.base_build, order=order, dry_run=dry_run, group=group, python=python
-    )
+    ctx.invoke(ci.base_build, order=order, dry_run=dry_run, group=group)
diff --git a/bob/devtools/scripts/rebuild.py b/bob/devtools/scripts/rebuild.py
index c7bb655ceda3175d3fd6d6c4650452ee0ceff68e..8b5a33710b04795ddbe99c8f4ec59e493ce7c169 100644
--- a/bob/devtools/scripts/rebuild.py
+++ b/bob/devtools/scripts/rebuild.py
@@ -253,7 +253,7 @@ def rebuild(
             continue
 
         rendered_recipe = get_parsed_recipe(metadata)
-        path = get_output_path(metadata, conda_config)
+        path = get_output_path(metadata, conda_config)[0]
 
         # Get the latest build number
         build_number, existing = next_build_number(
diff --git a/conda/conda_build_config.yaml b/conda/conda_build_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a4c56ab1cd521d843cba736f4eb0ee212f14943d
--- /dev/null
+++ b/conda/conda_build_config.yaml
@@ -0,0 +1,24 @@
+macos_min_version:
+  - 10.9
+macos_machine:
+  - x86_64-apple-darwin13.4.0
+MACOSX_DEPLOYMENT_TARGET:
+  - 10.9
+CONDA_BUILD_SYSROOT:            # [osx]
+  - /opt/MacOSX10.9.sdk         # [osx]
+# This helps CMAKE find the sysroot. See
+# https://cmake.org/cmake/help/v3.11/variable/CMAKE_OSX_SYSROOT.html
+SDKROOT:                        # [osx]
+  - /opt/MacOSX10.9.sdk         # [osx]
+# makes autotools verbose
+VERBOSE_AT:
+  - V=1
+# makes cmake verbose
+VERBOSE_CM:
+  - VERBOSE=1
+
+## the dependencies that we build against multiple versions
+python:
+  - 3.6
+  - 3.7
+  - 3.8
diff --git a/conda/meta.yaml b/conda/meta.yaml
index a41ac8893c86bacf1701ce5386d6b4bae861dfb2..3baf01d7f17936bd7a15ab1d29190c81b795fcf2 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -12,10 +12,10 @@ build:
     - {{ pin_subpackage(name) }}
   script:
     - cd {{ environ.get('RECIPE_DIR') + '/..' }}
-    {% if environ.get('BUILD_EGG') %}
+    {% if environ.get('BUILD_EGG') and not os.path.exists('dist') %}
     - python setup.py sdist --formats=zip
     {% endif %}
-    - python setup.py install --single-version-externally-managed --record record.txt
+    - {{ PYTHON }} -m pip install --no-deps --ignore-installed .
     # installs the documentation source, readme to share/doc so it is available
     # during test time
     - install -d "${PREFIX}/share/doc/{{ name }}"
@@ -24,7 +24,7 @@ build:
 requirements:
   host:
     - python {{ python }}
-    - setuptools {{ setuptools }}
+    - pip
   run:
     - python
     - setuptools
@@ -108,7 +108,9 @@ test:
     - bdt gitlab graph --help
     - bdt gitlab badges --help
     - sphinx-build -aEW ${PREFIX}/share/doc/{{ name }}/doc sphinx
+    {% if not os.path.exists('sphinx') %}
     - if [ -n "${CI_PROJECT_DIR}" ]; then mv sphinx "${CI_PROJECT_DIR}/"; fi
+    {% endif %}
 
 about:
   home: https://www.idiap.ch/software/bob/
diff --git a/conda/recipe_append.yaml b/conda/recipe_append.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c0481152b266e4b8d412ba2750661527a880c518
--- /dev/null
+++ b/conda/recipe_append.yaml
@@ -0,0 +1,4 @@
+build:
+  script_env:
+    - DOCSERVER
+    - NOSE_EVAL_ATTR