diff --git a/External/PyAnalysis/CMakeLists.txt b/External/PyAnalysis/CMakeLists.txt
index 1b4acdd83259648741c9555be6705c2e6973abd9..1fc5ceac77f8ebc7d0a2816b4b951d1cfa26e502 100644
--- a/External/PyAnalysis/CMakeLists.txt
+++ b/External/PyAnalysis/CMakeLists.txt
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2018 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration
 #
 # Configuration for building/installing python "analysis" modules.
 #
@@ -70,7 +70,7 @@ ExternalProject_Add( setuptools
    INSTALL_COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${_sitePkgsDir}
    ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/atlas_build_run.sh
    ${PYTHON_EXECUTABLE} <SOURCE_DIR>/setup.py install --prefix=${_buildDir}
-   COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/cmake/sanitizeScripts.sh
+   COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/sanitizePythonScripts.sh
    "${_buildDir}/bin/easy_install*"
    COMMAND ${CMAKE_COMMAND} -E copy_directory ${_buildDir}/ <INSTALL_DIR> )
 add_dependencies( Package_PyAnalysis setuptools )
@@ -97,7 +97,7 @@ if( NOT NUMPY_FOUND OR ATLAS_BUILD_PYTHON )
       PYTHONPATH=${_sitePkgsDir}
       ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/atlas_build_run.sh
       ${PYTHON_EXECUTABLE} <SOURCE_DIR>/setup.py install --prefix=${_buildDir}
-      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/cmake/sanitizeScripts.sh
+      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/sanitizePythonScripts.sh
       "${_buildDir}/bin/f2py*"
       COMMAND ${CMAKE_COMMAND} -E copy_directory ${_buildDir}/ <INSTALL_DIR> )
    add_dependencies( Package_PyAnalysis numpy )
@@ -122,7 +122,7 @@ if( NOT PIP_FOUND OR ATLAS_BUILD_PYTHON )
       INSTALL_COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${_sitePkgsDir}
       ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/atlas_build_run.sh
       ${PYTHON_EXECUTABLE} <SOURCE_DIR>/setup.py install --prefix=${_buildDir}
-      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/cmake/sanitizeScripts.sh
+      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/sanitizePythonScripts.sh
       "${_buildDir}/bin/wheel*"
       COMMAND ${CMAKE_COMMAND} -E copy_directory ${_buildDir}/ <INSTALL_DIR> )
    add_dependencies( Package_PyAnalysis wheel )
@@ -148,7 +148,7 @@ if( NOT PIP_FOUND OR ATLAS_BUILD_PYTHON )
       INSTALL_COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${_sitePkgsDir}
       ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/atlas_build_run.sh
       ${PYTHON_EXECUTABLE} <SOURCE_DIR>/setup.py install --prefix=${_buildDir}
-      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/cmake/sanitizeScripts.sh
+      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/sanitizePythonScripts.sh
       "${_buildDir}/bin/pip*"
       COMMAND ${CMAKE_COMMAND} -E copy_directory ${_buildDir}/ <INSTALL_DIR> )
    add_dependencies( Package_PyAnalysis pip )
diff --git a/External/PyModules/CMakeLists.txt b/External/PyModules/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7d5d87cffa575c192767a7bfd6ed7cac5d8ecb2f
--- /dev/null
+++ b/External/PyModules/CMakeLists.txt
@@ -0,0 +1,106 @@
+# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration
+#
+# CMake configuration for additional Python modules to be built
+# as part of ATLAS externals.
+#
+
+# The name of the package:
+atlas_subdir( PyModules )
+
+# In "release mode" return right away:
+if( ATLAS_RELEASE_MODE )
+   return()
+endif()
+
+# Figure out where to take Python from:
+if( ATLAS_BUILD_PYTHON )
+   set( PYTHON_EXECUTABLE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/python )
+else()
+   find_package( PythonInterp 2.7 REQUIRED )
+endif()
+
+# A common installation directory for all python externals of the package:
+set( _buildDir    ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/PyModulesBuild )
+set( _sitePkgsDir ${_buildDir}/lib/python2.7/site-packages )
+
+# Helper macro for building and installing python packages.
+# Usage: _setup_python_package( name tarball sha256_hash
+#                               [DEPENDS dep...] )
+#
+function( _setup_python_package name file hash )
+   # Parse the optional argument(s):
+   cmake_parse_arguments( ARG "" "" "DEPENDS" ${ARGN} )
+
+   # Build the package with the help of python's distutils:
+   ExternalProject_Add( ${name}
+      PREFIX ${CMAKE_BINARY_DIR}
+      INSTALL_DIR ${CMAKE_BINARY_DIR}/${ATLAS_PLATFORM}
+      URL "${file}"
+      URL_HASH SHA256=${hash}
+      BUILD_IN_SOURCE 1
+      CONFIGURE_COMMAND ${CMAKE_COMMAND} -E make_directory ${_sitePkgsDir}
+      BUILD_COMMAND ${CMAKE_COMMAND} -E env --unset=SHELL
+      ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/atlas_build_run.sh
+      ${PYTHON_EXECUTABLE} <SOURCE_DIR>/setup.py build
+      INSTALL_COMMAND ${CMAKE_COMMAND} -E env --unset=SHELL
+      PYTHONPATH=${_sitePkgsDir}
+      ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/atlas_build_run.sh
+      ${PYTHON_EXECUTABLE} <SOURCE_DIR>/setup.py install --prefix=${_buildDir}
+      COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/sanitizePythonScripts.sh "${_buildDir}/bin/*"
+      COMMAND ${CMAKE_COMMAND} -E copy_directory ${_sitePkgsDir}/ <INSTALL_DIR>/${CMAKE_INSTALL_PYTHONDIR}
+      COMMAND ${CMAKE_COMMAND} -E copy_directory ${_buildDir}/bin <INSTALL_DIR>/${CMAKE_INSTALL_BINDIR} )
+
+   # Make the package target depend on this one:
+   add_dependencies( Package_PyModules ${name} )
+
+   # Add possible extra dependencies:
+   if( ARG_DEPENDS )
+      add_dependencies( ${name} ${ARG_DEPENDS} )
+   endif()
+
+   # Get the package directory:
+   atlas_get_package_dir( pkgDir )
+
+   # Add some metadata to the target:
+   set_property( TARGET ${name} PROPERTY LABEL PyModules )
+   set_property( TARGET ${name} PROPERTY FOLDER ${pkgDir} )
+
+endfunction( _setup_python_package )
+
+
+# Location of the source tarballs:
+set( _sourceURL "http://cern.ch/atlas-software-dist-eos/externals/PyModules" )
+
+# Install extensions:
+_setup_python_package( extensions
+   ${_sourceURL}/extensions-0.4.tar.gz
+   02a6c12973f527fd09adad71792a8d70dfc94cb49fca67ceb201a4dfa5b18099 )
+
+# Install flake8 and dependencies:
+_setup_python_package( pyflakes
+   ${_sourceURL}/pyflakes-2.1.1.tar.gz
+   d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2 )
+
+_setup_python_package( mccabe
+   ${_sourceURL}/mccabe-0.6.1.tar.gz
+   dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f )
+
+_setup_python_package( pycodestyle
+   ${_sourceURL}/pycodestyle-2.5.0.tar.gz
+   e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c )
+
+_setup_python_package( flake8
+   ${_sourceURL}/flake8-3.7.7.tar.gz
+   859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661
+   DEPENDS pyflakes pycodestyle mccabe )
+
+
+# Install all built modules at the same time:
+install( DIRECTORY ${_buildDir}/
+   DESTINATION . USE_SOURCE_PERMISSIONS OPTIONAL )
+
+# Clean up:
+unset( _sourceURL )
+unset( _setup_python_package )
+unset( _buildDir )
+unset( _sitePkgsDir )
diff --git a/External/scripts/README.md b/External/scripts/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5f0d618ed95ac0d015494899ff4dcbebc6239060
--- /dev/null
+++ b/External/scripts/README.md
@@ -0,0 +1 @@
+Directory containing scripts used during the build of the External packages.
diff --git a/External/PyAnalysis/cmake/sanitizeScripts.sh b/External/scripts/sanitizePythonScripts.sh
similarity index 63%
rename from External/PyAnalysis/cmake/sanitizeScripts.sh
rename to External/scripts/sanitizePythonScripts.sh
index b1d111265e6662043a37d8cfd8984366aacccd21..6e78e58cd1adb0f1a821a4d6bef124425908fab8 100755
--- a/External/PyAnalysis/cmake/sanitizeScripts.sh
+++ b/External/scripts/sanitizePythonScripts.sh
@@ -3,16 +3,18 @@
 # Script used to sanitize the executable scripts after their installation,
 # to make them relocatable.
 #
+# Workaround for https://github.com/colcon/colcon-bundle/issues/6
+#
 
 # Fail on errors:
 set -e
+# globs matching no files evaluate to empty string:
+shopt -s nullglob
 
 # Loop over all scripts provided on the command line:
 for script in $*; do
 
     # Create a sanitized version of the script, by just replacing
     # its first line with a relocatable expression:
-    sed -i.bak -e '1s:.*:#!/usr/bin/env python:' ${script}
-    # Remove the old version:
-    rm ${script}.bak
+    sed -i -e '1s:#!.*:#!/usr/bin/env python:' ${script}
 done
diff --git a/Projects/AthenaExternals/package_filters.txt b/Projects/AthenaExternals/package_filters.txt
index 085c42f13d1ba12daa109aa44035927fcaf72ead..c2eba261a690cde01038416dbc3b41d154f8636d 100644
--- a/Projects/AthenaExternals/package_filters.txt
+++ b/Projects/AthenaExternals/package_filters.txt
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration
 #
 # List of packages to build as part of AthenaExternals.
 #
@@ -19,6 +19,7 @@
 + External/MKL
 + External/nlohmann_json
 + External/prmon
++ External/PyModules
 + External/Simage
 + External/SoQt
 + External/dSFMT