Repository: cropsinsilico/yggdrasil (language: python)

Issue:
Migrate CI to Github Actions
Update CI to run on Github actions while maintaining backwards compat with Travis/Appveyor.
Fix bug when using external threading in model
There is a bug where initializing (or possibly just using) yggdrasil communication channels inside of a threaded loop causes a segfault in C/C++ (so far only tested w/ OpenMP threading). This is related to race conditions on the initialization of the ZeroMQ context (which are must be created on the main thread) and will probably cause other issues due to simultaneous modification of global variables in the C interface that are used for tracking and cleaning up connections.


Discussion / hints:

Fix patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index dc92561bd..1fa477ddd 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -14,9 +14,6 @@ os: Visual Studio 2015
 
 environment:
 
-  PYPI_PASSWORD:
-    secure: vixzjLjh1xT6my9x65rOcw==
-
   global:
     PYTHON: "C:\\Miniconda3-x64"
     APPVEYOR_SAVE_CACHE_ON_ERROR: true
@@ -111,9 +108,7 @@ install:
   # Setup environment
   # Currently conda is used reguardless of the installation method as
   # that is how python is installed by appveyor
-  - "python utils\\ci_setup.py pip env %PYTHON_VERSION%"
-  # - if "%YGG_CONDA%" == "1" "python utils\\ci_setup.py conda env %PYTHON_VERSION%"
-  # - if "%YGG_CONDA%" == "0" "python utils\\ci_setup.py pip env %PYTHON_VERSION%"
+  - "python utils\\setup_test_env.py env conda %PYTHON_VERSION% --env-name=test-environment"
 
   # Activate environment (should be conda reguardless)
   - "activate test-environment"
@@ -122,9 +117,8 @@ install:
   - if "%INSTALLC%" == "1" call "ci\\install-compilers.bat"
 
   # Build and install
-  - "python utils\\ci_setup.py pip deploy"
-  # - if "%YGG_CONDA%" == "1" "python utils\\ci_setup.py conda deploy"
-  # - if "%YGG_CONDA%" == "0" "python utils\\ci_setup.py pip deploy"
+  - "python utils\\setup_test_env.py install pip"
+  - "python utils\\setup_test_env.py verify"
   - "echo %PATH%"
 
 cache:
@@ -141,29 +135,9 @@ build: false
 
 test_script:
   - "yggclean"
+  - "yggcompile"
   - "yggtest --ci %YGG_TEST_FLAGS%"
   - "%YGG_SECOND_TEST%"
 
-after_test:
-  - "codecov"
-
-# Deploy to PyPI
-# on_success:
-#   - echo "Starting Artifact Deployment"
-#   - "pip install twine"
-#   - "python setup.py bdist_wheel"
-
-#   # Specify account details for PyPI
-#   - echo [distutils]                                  > %USERPROFILE%\\.pypirc
-#   - echo index-servers =                             >> %USERPROFILE%\\.pypirc
-#   - echo     pypi                                    >> %USERPROFILE%\\.pypirc
-#   - echo [pypi]                                      >> %USERPROFILE%\\.pypirc
-#   - echo username=langmm                             >> %USERPROFILE%\\.pypirc
-#   - echo password=%PYPI_PASSWORD%                    >> %USERPROFILE%\\.pypirc
-
-#   # Workaround required to ensure setup.py finds the .pypirc under Windows
-#   - set HOME=%USERPROFILE%
-
-#   # Deploy wheel on tags
-#   - ps: If ($env:APPVEYOR_REPO_TAG -eq "true" -And $env:APPVEYOR_REPO_BRANCH -eq "master") { Invoke-Expression "twine upload --skip-existing dist/*" 2>$null } Else { write-output "Not on a tag on master, won't deploy to pypi"}
-#   # - if "%APPVEYOR_REPO_TAG%"=="true" ( python setup.py bdist_wheel upload ) else ( echo "Not deploying because not a tagged commit." )
+# after_test:
+#   - "codecov"
diff --git a/.travis.yml b/.travis.yml
index 633e16d94..d28f3c371 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -43,9 +43,7 @@ env:
   matrix:
     - YGG_TEST_FLAGS="--test-suite=examples_part1 --test-suite=demos" INSTALLTRIMESH=1
     - YGG_TEST_FLAGS="--test-suite=examples_part2"
-    - YGG_CONDA=yes YGG_TEST_FLAGS="--long-running" CC=gcc CFLAGS="-Wall -g" INSTALLRMQ=1 INSTALLAPY=1 INSTALLTRIMESH=1 INSTALLPYGMENTS=1 YGG_SECOND_TEST="yggtest --ci --default-comm=IPCComm --long-running"
-    # - YGG_CONDA=yes YGG_TEST_FLAGS="--test-suite=types --languages R c"
-    # - YGG_TEST_FLAGS="--test-suite=types --languages matlab python cpp"
+    - YGG_CONDA=yes YGG_TEST_FLAGS="--long-running" CC=gcc CFLAGS="-Wall -g" INSTALLRMQ=1 INSTALLAPY=1 INSTALLTRIMESH=1 INSTALLPYGMENTS=1 YGG_SECOND_TEST="yggtest --ci --default-comm=IPCComm --test-suite=timing"  # --long-running"
 
 before_install:
   - |
@@ -53,9 +51,6 @@ before_install:
     if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
        export PATH=/usr/lib/ccache:$PATH
     fi
-    # if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
-    #    export PATH=/usr/local/opt/ccache/libexec:$PATH
-    # fi
 
     if [[ -n "$TRAVIS_PYTHON_VERSION" ]]; then
       if [[ -n "$YGG_CONDA" ]]; then
@@ -71,38 +66,15 @@ before_install:
     if [[ -n "$YGG_CONDA" || $INSTALLLPY == '1' ]]; then
       echo Installing Python using conda...;
       if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then MINICONDA_OS=Linux; else MINICONDA_OS=MacOSX; fi
-      # At one point this saved time when testing in a Python 2.7 conda environment, but
-      # recent version of conda build have issues when the base conda is not Python 3
-      # wget https://repo.anaconda.com/miniconda/Miniconda${YGG_CONDA:0:1}-latest-$MINICONDA_OS-x86_64.sh -O miniconda.sh
       wget https://repo.anaconda.com/miniconda/Miniconda3-latest-$MINICONDA_OS-x86_64.sh -O miniconda.sh
       bash miniconda.sh -b -p $HOME/miniconda
       export PATH="$HOME/miniconda/bin:$PATH"
       hash -r
-
-      # Download and point to older SDK on OSX
-      echo "TRAVIS_OS_NAME = ${TRAVIS_OS_NAME}"
-      if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
-        conda info -a
-        # Install older Mac SDK so that conda llvm (7) can be used
-        # https://github.com/conda-forge/mpi-feedstock/issues/4
-        export MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET:-10.9}
-        export CONDA_BUILD_SYSROOT="$(xcode-select -p)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX${MACOSX_DEPLOYMENT_TARGET}.sdk"
-        export SDKROOT=${CONDA_BUILD_SYSROOT}
-        echo "MACOSX_DEPLOYMENT_TARGET = ${MACOSX_DEPLOYMENT_TARGET}"
-        echo "CONDA_BUILD_SYSROOT = ${CONDA_BUILD_SYSROOT}"
-        echo "SDKROOT = ${SDKROOT}"
-
-        if [[ ! -d ${CONDA_BUILD_SYSROOT} || "$OSX_FORCE_SDK_DOWNLOAD" == "1" ]]; then
-          echo "Downloading ${MACOSX_DEPLOYMENT_TARGET} sdk to ${CONDA_BUILD_SYSROOT}"
-          curl -L -O https://github.com/phracker/MacOSX-SDKs/releases/download/10.13/MacOSX${MACOSX_DEPLOYMENT_TARGET}.sdk.tar.xz
-          tar -xf MacOSX${MACOSX_DEPLOYMENT_TARGET}.sdk.tar.xz -C "$(dirname "$CONDA_BUILD_SYSROOT")"
-          ls "$(dirname "$CONDA_BUILD_SYSROOT")"
-          # set minimum sdk version to our target
-          plutil -replace MinimumSDKVersion -string ${MACOSX_DEPLOYMENT_TARGET} $(xcode-select -p)/Platforms/MacOSX.platform/Info.plist
-          plutil -replace DTSDKName -string macosx${MACOSX_DEPLOYMENT_TARGET}internal $(xcode-select -p)/Platforms/MacOSX.platform/Info.plist
-          printf "CONDA_BUILD_SYSROOT:\n  - ${CONDA_BUILD_SYSROOT}  # [osx]\n" > ~/conda_build_config.yaml
-        fi
-      fi
+    fi
+    # Download and point to older SDK on OSX
+    echo "TRAVIS_OS_NAME = ${TRAVIS_OS_NAME}"
+    if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
+      ./utils/setup_old_mac_sdk.sh
     fi
 
     # Install RabbitMQ & Start a server
@@ -114,31 +86,40 @@ before_install:
           else
              sudo apt-get install rabbitmq-server
           fi
-          echo Starting RabbitMQ...;
-          export PATH="${PATH}:/usr/local/sbin";
-          sudo /bin/sh -c "RABBITMQ_PID_FILE=$TRAVIS_BUILD_DIR/rabbitmq.pid rabbitmq-server &"
-          sudo rabbitmqctl wait "$TRAVIS_BUILD_DIR/rabbitmq.pid"
+          ./utils/setup_rabbitmq.sh
+       else
+         # It has been installed via a service
+         echo Checking rabbitmq status...;
+         sudo rabbitmqctl status
        fi
-       echo Checking rabbitmq status...;
-       sudo rabbitmqctl status
     fi
 
     # Setup environment
-    if [[ -n "$YGG_CONDA" ]]; then
-      python utils/ci_setup.py conda env $YGG_CONDA
+    if [[ -n "$YGG_CONDA" || $INSTALLLPY == '1' ]]; then
+      python utils/setup_test_env.py env conda $YGG_CONDA --env-name=test-environment
     else
-      python utils/ci_setup.py pip env $YGG_PYTHON
+      # No environment required on Linux because Travis is managing the version
+      # of Python that is installed
+      if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
+        python utils/setup_test_env.py env virtualenv $YGG_PYTHON --env-name=test-environment
+      fi
+    fi
+
+    # Perform the conda build in the base environment
+    if [[ -n "$YGG_CONDA" ]]; then
+      python utils/setup_test_env.py build conda --python $YGG_CONDA
     fi
 
     # Activate environment
     if [[ -n "$YGG_CONDA" || $INSTALLLPY == '1' ]]; then
       source activate test-environment
+      # TODO: Check if this can be removed
       if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
         export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH
       fi
     fi
     if [[ -n "$YGG_PYTHON" && $TRAVIS_OS_NAME == 'osx' ]]; then
-      source venv/bin/activate
+      source test-environment/bin/activate
     fi
 
 install:
@@ -148,24 +129,27 @@ install:
       ccache -s
     fi
 
-    # Build and install
+    # Install dependencies & package
     if [[ -n "$YGG_CONDA" ]]; then
-      python utils/ci_setup.py conda deploy
+      travis_wait python utils/setup_test_env.py deps conda
+      travis_wait python utils/setup_test_env.py install conda --without-build --without-deps
     else
-      if [[ $INSTALLR == '1' ]]; then
-        export YGG_USE_SUDO_FOR_R=1
-      fi
-      python utils/ci_setup.py pip deploy
+      # if [[ $INSTALLR == '1' ]]; then
+      #   export YGG_USE_SUDO_FOR_R=1
+      # fi
+      travis_wait python utils/setup_test_env.py install pip
     fi
 
 script:
+  - python utils/setup_test_env.py verify
+  - travis_wait yggcompile
   - yggtest --ci $YGG_TEST_FLAGS
   - $YGG_SECOND_TEST
 
 
-after_success:
-  - |
-    codecov
+# after_success:
+#   - |
+#     codecov
 
 
 stages:
@@ -209,7 +193,7 @@ jobs:
           - $HOME/Library/Caches/pip
           - $HOME/Library/Caches/Homebrew
           - $HOME/R/Library
-      env: YGG_PYTHON=3.6 R_LIBS_USER=$HOME/R/Library YGG_TEST_FLAGS="tests/test_tools.py --test-suite=types --languages R c" INSTALLR=0 INSTALLFORTRAN=0
+      env: YGG_PYTHON=3.6 R_LIBS_USER=$HOME/R/Library YGG_TEST_FLAGS="tests/test_tools.py --test-suite=types --languages R c" INSTALLR=0 INSTALLFORTRAN=0 INSTALLZMQ=0
     - os: osx
       python: 3.7
       language: generic  # https://github.com/travis-ci/travis-ci/issues/2312
@@ -218,7 +202,7 @@ jobs:
         directories:
           - $HOME/Library/Caches/pip
           - $HOME/Library/Caches/Homebrew
-      env: YGG_CONDA=3.7 YGG_TEST_FLAGS="serialize --test-suite=examples_part1 communication/tests/test_IPCComm.py communication/tests/test_RMQAsyncComm.py --test-suite=timing" INSTALLZMQ=0
+      env: YGG_CONDA=3.7 YGG_TEST_FLAGS="serialize --test-suite=examples_part1 communication/tests/test_IPCComm.py communication/tests/test_RMQAsyncComm.py --test-suite=timing" # INSTALLZMQ=0
       script:
         - yggconfig --remove-file
         - yggtest --ci tests/test_config.py $YGG_TEST_FLAGS
@@ -239,43 +223,43 @@ jobs:
           update: true
 
     # Docs
-    - stage: deploy
-      name: "Deploy docs"
-      python: 3.6
-      env: BUILDDOCS=1
-      script: |
-              cd docs
-              make autodoc
-              cd "$TRAVIS_BUILD_DIR"
-      after_success: skip
-      if: (tag IS present)
-      deploy:
-        provider: pages
-        skip_cleanup: true
-        local_dir: docs/build/html/
-        keep-history: true
-        github_token: $GITHUB_TOKEN  # Set in travis-ci.org dashboard
-        on:
-          tags: true
-        verbose: true
-      addons:
-        apt:
-          packages:
-            - *native_deps_apt
-            - doxygen
-          update: true
+    # - stage: deploy
+    #   name: "Deploy docs"
+    #   python: 3.6
+    #   env: BUILDDOCS=1
+    #   script: |
+    #           cd docs
+    #           make autodoc
+    #           cd "$TRAVIS_BUILD_DIR"
+    #   after_success: skip
+    #   if: (tag IS present)
+    #   deploy:
+    #     provider: pages
+    #     skip_cleanup: true
+    #     local_dir: docs/build/html/
+    #     keep-history: true
+    #     github_token: $GITHUB_TOKEN  # Set in travis-ci.org dashboard
+    #     on:
+    #       tags: true
+    #     verbose: true
+    #   addons:
+    #     apt:
+    #       packages:
+    #         - *native_deps_apt
+    #         - doxygen
+    #       update: true
 
     # Deploy
-    - stage: deploy
-      name: "Deploy to PyPI"
-      script: skip
-      if: (tag IS present)
-      deploy:
-        provider: pypi
-        skip_existing: true
-        distributions: sdist bdist_wheel --universal
-        username: "__token__"
-        password:
-          secure: "iRsnZ8i1fWoqZVQd7LzX8L+z3aS/c1io/tUHApJE8ujHdNF7owtLyg1JG9eUWdinfApv0WS7uaoUxdM44I3eMpFdsXBXCEcQZ77ZQEJ5kO+QqqSfPX6BO/hKTcckuw3UMWYMxWk7BmreUFBFwV1vx5/o0Wv+vql968+zhHILtWnaW+1jhDwHn2FmUZ9ESXww87FyJhcpvvsCQOXBTKEWN2AOToLaU4NWvzrHi5B7Vr9+4rZ70HyUwCIhb53ktZ+3Xn0zZwbf5or7XbRPrkfiVHqX2ywfa2lQU+59KHwXG0UHt8cSqvD08Tig3QgFO76SAsLXaJ+GI1YPTJ/Q/2xC+5N5tWDi1wL8SXmetjnku3/PGk9tfljVbMVErDLq8Vchra/w8AQ1EAyyhV4xBRIWJr/PlwsjxHB+qNnJ5S/x/0ZeSylUsPlQNTsqRqHdMYNJR1m1/pJqzZ6WURMVW2A341fRHHZNFQDNFePWvEA1qWKmeCWkKeKAu22TbIl0YlIX4m/y87BfjuHq4pditoIMws5K9SH4HVgrm4Kcqo8JlYN8bsTDlKw0ydtHZAYnh2BS6KHaCQqM+03/8yBrnrP0nzei1qkFVfAoAKcPMIsEYcPcme9MgJG5j/pAAzY1VeY52zgc7F9FRVmufKxEfro2Gcid/PW0reaPZgqD98B+ubo="
-        on:
-          tags: true
+    # - stage: deploy
+    #   name: "Deploy to PyPI"
+    #   script: skip
+    #   if: (tag IS present)
+    #   deploy:
+    #     provider: pypi
+    #     skip_existing: true
+    #     distributions: sdist bdist_wheel --universal
+    #     username: "__token__"
+    #     password:
+    #       secure: "iRsnZ8i1fWoqZVQd7LzX8L+z3aS/c1io/tUHApJE8ujHdNF7owtLyg1JG9eUWdinfApv0WS7uaoUxdM44I3eMpFdsXBXCEcQZ77ZQEJ5kO+QqqSfPX6BO/hKTcckuw3UMWYMxWk7BmreUFBFwV1vx5/o0Wv+vql968+zhHILtWnaW+1jhDwHn2FmUZ9ESXww87FyJhcpvvsCQOXBTKEWN2AOToLaU4NWvzrHi5B7Vr9+4rZ70HyUwCIhb53ktZ+3Xn0zZwbf5or7XbRPrkfiVHqX2ywfa2lQU+59KHwXG0UHt8cSqvD08Tig3QgFO76SAsLXaJ+GI1YPTJ/Q/2xC+5N5tWDi1wL8SXmetjnku3/PGk9tfljVbMVErDLq8Vchra/w8AQ1EAyyhV4xBRIWJr/PlwsjxHB+qNnJ5S/x/0ZeSylUsPlQNTsqRqHdMYNJR1m1/pJqzZ6WURMVW2A341fRHHZNFQDNFePWvEA1qWKmeCWkKeKAu22TbIl0YlIX4m/y87BfjuHq4pditoIMws5K9SH4HVgrm4Kcqo8JlYN8bsTDlKw0ydtHZAYnh2BS6KHaCQqM+03/8yBrnrP0nzei1qkFVfAoAKcPMIsEYcPcme9MgJG5j/pAAzY1VeY52zgc7F9FRVmufKxEfro2Gcid/PW0reaPZgqD98B+ubo="
+    #     on:
+    #       tags: true
diff --git a/HISTORY.rst b/HISTORY.rst
new file mode 100644
index 000000000..692541fa9
--- /dev/null
+++ b/HISTORY.rst
@@ -0,0 +1,135 @@
+=======
+History
+=======
+
+1.5.0 (XXXX-XX-XX)
+------------------
+
+* Move continuous integration to Github actions
+* Refactor the command line interface and add the `yggdrasil` CLI with subcommands for other command line actions
+* Fix bug where colons cause environment variables to be invalid for R models run in Conda environments on Ubuntu
+* Pin to conda czmq requirement to 4.2.0 on Mac OS until there is a fix for a bug in the czmq socket initalizaiton
+
+
+1.4.0 (2020-12-09) Support for OpenSimRoot models, wrapped functions as clients/servers, & misc. features/bug fixes
+------------------
+
+General
+~~~~~~~
+
+* Added driver for running OpenSimRoot models
+* Added a new  'demo' directory to contain submodules linking to external materials that can be used in demos, but tested with the repo as part of the CI
+* Added FSPM demo materials as a submodule
+* Added support for “global” comms that can be reused between calls on the same process (and different threads, though there needs to be additional work to make non-client/server comms fully thread safe)
+* Added support for auto-wrapping functions for use as servers/clients and that contain yggdrasil calls
+* Added rpc example demonstrating use of the “global” comms feature to support wrapping of functions for client/server call patterns
+* Created config context for handling runtime options as controlled by combinations of CLI arguments and configuration files
+* Removed use of “last_header” attribute on comms to eliminate ambiguity when messages are received asynchronously in the background
+* Streamlined how RMQ import is tested so that RMQComm is the basis instead of RMQAsyncComm
+* Added interface regex to model drivers for locating & replacing existing yggdrasil imports/calls in wrapped code when ‘global’ version should be used in the case of R
+* Change interface behavior for all Python-based languages (R & Matlab) to no longer assume format_str values of ‘%s’ for client/server comms (this prevents defaulting to arrays)
+* Added support for use of trimesh objects with ply/obj messages
+* Added tools for displaying code w/ syntax highlighting
+* Improved error handling in yaml processing including checking for duplicates
+
+Command Line Interface
+~~~~~~~~~~~~~~~~~~~~~~
+
+* Added CLI utilities for updating after pulling development updates (yggdefup) and compiling the interface libraries (yggcompile)
+* Improved the CLI utilities for getting compilation flags to allow language/os specific options
+
+Testing
+~~~~~~~
+
+* Cleaned up test output to limit log (after reaching log limit on Travis CI)
+* Added test fixtures for demos
+* Created test context for handling configuration and environment variables that control which tests will be skipped
+* Added coverage pragmas for handling specific cases
+* Updated how tests are identified to eliminate unnecessary languages from test discovery (avoid superfluous skips)
+* Removed explicit version of sbml test required by differences in release on different os (this has been resolved)
+* Added additional flags for improving the performance of tests
+* Generalized CI setup script to consolidate dependencies and streamline installation
+
+General bug fixes
+~~~~~~~~~~~~~~~~~
+
+* Stopped duplicate logging output
+* Compile internal dependencies on demand when compilation/linking flags are requested
+* Avoid infinite loop when auto wrapping functions without any inputs
+* Fixed a bug in the WOFOST serializer for null units
+* Fixed bug in the method used to extract units from versions used by other languages (including unicode characters for degree) where calling the method twice resulted in an incomplete unit string
+* Fixed bug in handling of dimensionless quantities when checking for units
+  
+Fortran Interface
+~~~~~~~~~~~~~~~~~
+
+* Added support for passing references to relocatable types in function wrappers
+* Don’t split lines that include macros
+* Added support for wrapping functions in modules
+* Fixed bug following updates to the gfortran compiler on conda-forge that removed support for mapping to character arrays (rather than arrays of characters)
+* Added optional arguments to client/server interfaces (for the format strings)
+* Added versions of client/server interfaces in that allow direct type specification
+
+R Interface
+~~~~~~~~~~~
+
+* Fixed bugs in the handling of conversions for units and null objects
+* Added support for named arguments in the R interface
+
+C/C++ Interface
+~~~~~~~~~~~~~~~
+
+* Fixed a bug where arguments were not being correctly skipped (now they are explicitly skipped based on the expected type)
+* Added support for std::string typed names as input to the C++ interface
+* Fix bug in C++ function regex when reference/pointer operators are included in the types
+* Added versions of client/server interfaces in that allow direct type specification
+
+Matlab Interface
+~~~~~~~~~~~~~~~~
+
+* Fixed a bug in the Matlab to Python object transformation
+* Added support for keyword arguments to the Matlab interface
+
+
+1.3.0 (2020-07-08) Support for Fortran Models
+------------------
+
+* Fortran interface which uses the Fortran 2003 standard (f70, f90 will be added at a later date)
+* Fortran versions of all examples
+* Tests for use of GNU and LLVM compilers on Windows
+
+
+1.2.0 (2020-06-11) Support for WOFOST parameter files, NetCDF files, SBML models, & automated timestep synchronization
+------------------
+
+* Add support for reading/writing WOFOST parameter files.
+* Add support for reading/writing NetCDF files.
+* Update tests for serialization/comms/filters/transforms so that tests are generated automatically.
+* Add support for running SBML models.
+* Add dedicated base class for domain specific languages.
+* Allow connections to be run in processes as well as threads.
+* New submodule for handling threading/multiprocessing uniformly and interchangeably.
+* Add dedicated driver for handling synchronization of scalar variables between time based models at each timestep that can be invoked via a yaml parameter.
+
+
+1.1.1 (2020-03-20) Matlab bug fix
+------------------
+
+* Fixes a bug where on some operating systems, the environment variables in the process used to launch Matlab are not inherited by the Matlab script.
+* Minor changes to CI setup
+
+
+1.1.0 (2020-03-16) Drop Python 2 + Misc.
+------------------
+
+* Droped support for Python 2
+* Added schema for generating model form
+* Move configuration out of model driver classes to speed up and simplify import
+* Various bug fixes for installation (search directory for Matlab, default python include/libraries, etc.)
+* Allow for matlab <r2019a call signature which doesn’t include -batch option
+* Various fixes for pandas compatibility across languages including reading as string vs. bytes.
+* Added option for including other yamls files
+* Fixed bug in CLI for getting C/C++ compiler/linker flags
+* Move doutside_loop to comm (not valid on file)
+* Added tests for transforms and fixed various bugs in transformations
+* Added buffer comm which stores messages in-memory
diff --git a/README.rst b/README.rst
index 39c3e7163..ca2b59d75 100644
--- a/README.rst
+++ b/README.rst
@@ -1,4 +1,4 @@
-|tag version| |PyPI version| |Travis Build Status| |Appveyor Build status| 
+|tag version| |PyPI version| |GHA Build Status|  
 |codecov| |PEP8| |license| |platform|
 
 yggdrasil, originally created as a framework for the Crops in Silico (CiS) 
@@ -44,6 +44,8 @@ If you use yggdrasil in your research, please cite the accompanying paper:
    :target: https://travis-ci.org/cropsinsilico/yggdrasil
 .. |Appveyor Build status| image:: https://img.shields.io/appveyor/ci/langmm/yggdrasil.svg?style=flat-square
    :target: https://ci.appveyor.com/project/langmm/yggdrasil/branch/master
+.. |GHA Build Status| image:: https://img.shields.io/github/workflow/status/cropsinsilico/yggdrasil/test-install?style=flat-square
+   :target: https://github.com/cropsinsilico/yggdrasil/actions?query=workflow%3A%22Test+of+package+installation+%26+execution%22
 ..
    .. |Coverage Status| image:: https://coveralls.io/repos/github/cropsinsilico/yggdrasil/badge.svg?branch=master
       :target: https://coveralls.io/github/cropsinsilico/yggdrasil?branch=master
diff --git a/ci/install-czmq-osx.sh b/ci/install-czmq-osx.sh
old mode 100644
new mode 100755
index ddf3c4b60..4c6c3cde5
--- a/ci/install-czmq-osx.sh
+++ b/ci/install-czmq-osx.sh
@@ -1,11 +1,7 @@
 #!/bin/bash
 brew update
 brew install git libtool pkg-config autoconf automake \
-     cmake ossp-uuid pcre valgrind
-# sudo apt-get install -y \
-#      git-all build-essential libtool \
-#      pkg-config autotools-dev autoconf automake cmake \
-#      uuid-dev libpcre3-dev valgrind
+     cmake ossp-uuid pcre # valgrind
 
 # Install Libsodium
 git clone git://github.com/jedisct1/libsodium.git
@@ -13,7 +9,6 @@ cd libsodium
 ./autogen.sh
 ./configure # && make check
 sudo make install
-# sudo ldconfig  # Don't do for MAC
 cd ..
 
 # Install libzmq
@@ -24,7 +19,6 @@ cd libzmq
 # security implementation (recommended for development)
 ./configure --with-libsodium # && make check
 sudo make install
-# sudo ldconfig  # Don't do for MAC
 cd ..
 
 # Install czmq
@@ -33,5 +27,4 @@ cd czmq
 ./autogen.sh
 ./configure # && make check
 sudo make install
-# sudo ldconfig  # Don't do for MAC
 cd ..
diff --git a/create_coveragerc.py b/create_coveragerc.py
index 508b2f8d0..2852c35eb 100644
--- a/create_coveragerc.py
+++ b/create_coveragerc.py
@@ -2,6 +2,10 @@
 import sys
 PY_MAJOR_VERSION = sys.version_info[0]
 IS_WINDOWS = (sys.platform in ['win32', 'cygwin'])
+_on_gha = bool(os.environ.get('GITHUB_ACTIONS', False))
+_on_travis = bool(os.environ.get('TRAVIS_OS_NAME', False))
+_on_appveyor = bool(os.environ.get('APPVEYOR_BUILD_FOLDER', False))
+# _on_ci = (_on_gha or _on_travis or _on_appveyor)
 # Import config parser
 try:
     from ConfigParser import RawConfigParser as HandyConfigParser
@@ -83,8 +87,10 @@ def create_coveragerc(installed_languages):
                 cp.add_section(sect_cp)
             for opt in cp_cfg.options(x):
                 if cp.has_option(sect_cp, opt):
-                    val_old = [l.strip() for l in cp.get(sect_cp, opt).split('\n')]
-                    val_new = [l.strip() for l in cp_cfg.get(x, opt).split('\n')]
+                    val_old = [line.strip() for line in
+                               cp.get(sect_cp, opt).split('\n')]
+                    val_new = [line.strip() for line in
+                               cp_cfg.get(x, opt).split('\n')]
                     for v in val_new:
                         if v not in val_old:
                             val_old.append(v)
@@ -100,11 +106,24 @@ def create_coveragerc(installed_languages):
         excl_list = excl_str.strip().split('\n')
     else:
         excl_list = []
-    # Platform
+    # Operating system
     if IS_WINDOWS:
         excl_list = rm_excl_rule(excl_list, 'pragma: windows')
     else:
         excl_list = add_excl_rule(excl_list, 'pragma: windows')
+    # CI Platform
+    if _on_gha:
+        excl_list = rm_excl_rule(excl_list, 'pragma: gha')
+    else:
+        excl_list = add_excl_rule(excl_list, 'pragma: gha')
+    if _on_travis:
+        excl_list = rm_excl_rule(excl_list, 'pragma: travis')
+    else:
+        excl_list = add_excl_rule(excl_list, 'pragma: travis')
+    if _on_appveyor:
+        excl_list = rm_excl_rule(excl_list, 'pragma: appveyor')
+    else:
+        excl_list = add_excl_rule(excl_list, 'pragma: appveyor')
     # Python version
     verlist = [2, 3]
     for v in verlist:
@@ -129,7 +148,9 @@ def create_coveragerc(installed_languages):
     return True
 
 
-if __name__ == "__main__":
+def run():
+    r"""Run coverage creation function after getting a list of installed
+    languages."""
     LANG_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)),
                              'yggdrasil', 'languages')
     sys.path.insert(0, LANG_PATH)
@@ -141,3 +162,7 @@ def create_coveragerc(installed_languages):
     flag = create_coveragerc(installed_languages)
     if not flag:
         raise Exception("Failed to create/update converagerc file.")
+
+
+if __name__ == "__main__":
+    run()
diff --git a/docs/source/development/todo.rst b/docs/source/development/todo.rst
index 79cc32262..19eb0490f 100644
--- a/docs/source/development/todo.rst
+++ b/docs/source/development/todo.rst
@@ -61,6 +61,7 @@ New feature/example
 * Allow users to select from list of multiple possiblities when locating libraries to avoid conflict
 * Write C/C++ as extension to rapidjson and wrap in Python
 * Add alias key to schema that is then translated into valid JSON schema
+* Add 'shell' option for executable models on Windows to allow calling different shell types.
 
 Deprecation
 -----------
diff --git a/docs/source/install.rst b/docs/source/install.rst
index 60448a193..b6efed40f 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -9,15 +9,21 @@ Installation
 
    **Windows Users**
 
-   If you will be running C/C++ models on a Windows operatoring system, you will first need to install Microsoft Visual Studio, regardless of the installation method you end up using. Visual Studio Community can be downloaded for free from `here <https://visualstudio.microsoft.com/vs/community/>`_. During installation, we recommend selecting the components below. If you forget to add something during the initial download, you can always modify the installation via the "Visual Studio Installer" program.
+   If you will be running C/C++/Fortran models on a Windows operatoring system, you will first need to install Microsoft Visual Studio, regardless of the installation method you end up using. Visual Studio Community can be downloaded for free from `here <https://visualstudio.microsoft.com/vs/community/>`_. During installation, we recommend selecting the components below. If you forget to add something during the initial download, you can always modify the installation via the "Visual Studio Installer" program.
 
     * "Desktop development with C++" - Workload under "Windows" section
     * "MSVC v140 - VS 2015 C++ build tools (v14.00)" - Individual component under "Compilers, build tools, and runtimes" section.
 
-   If you *do not use conda* to install |yggdrasil|, you will also need to initialize the command line build tools in any prompt you will be calling |yggdrasil| from. This can be done by calling |yggdrasil| from a developer prompt, or by locating the ``vsvarsall.bat`` script that comes with Visual Studio. Information on the developer prompt and how to locate the ``vsvarsall.bat`` script can be found `here <https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=vs-2019>`_. The script used must be the one associated with Visual Studio 2015 build tools, which can be installed from within Visual Studio 2019. On a 64bit Windows machine, the command to initialize these tools within a prompt will probably look something like this::
+   If you *do not use conda* to install |yggdrasil|, you will also need to initialize the command line build tools in any prompt you will be calling |yggdrasil| from. This can be done by calling |yggdrasil| from a "VS2015 x64 Native Tools Developer Command Prompt", or by locating the ``vsvarsall.bat`` script that comes with Visual Studio. Information on the developer prompt and how to locate the ``vsvarsall.bat`` script can be found `here <https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=vs-2019>`_. The prompt/script used must enable the Visual Studio 2015 build tools (the specific year) to be compatible with the Python C library (see the discussion `here <https://wiki.python.org/moin/WindowsCompilers>`_). The VS 2015 tools prompt/script will be installed from within Visual Studio 2019 by selecting the components indicated above. On a 64bit Windows machine (assuming you will be using 64 bit Python), the command to initialize these tools within a regular command prompt will look something like this, but the exact path will vary with your particular installation::
 
      $ call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64
 
+.. note::
+
+   **Mac Users**
+
+   If you will be running C/C++/Fortran models, you will need to install the command line developer tools on Mac which provides several utilities for compilation and code management (e.g. clang, gcc, make, git). `This <https://cloudtechpoint.medium.com/how-to-install-command-line-tools-homebrew-in-macos-without-xcode-1b910742d923>`_ article outlines how to check if they are installed and install them if they are not. It also describes how to install the Homebrew package manager, which will be useful for installing non-Python dependencies if you do not plan on using conda to install |yggdrasil|.
+
 
 Conda Installation (recommended)
 --------------------------------
@@ -74,9 +80,34 @@ Manual Installation
    should manually install the non-Python dependencies, particularly the
    ZeroMQ C and C++ libraries and R packages (see below).
 
-If you do not want to use conda, |yggdrasil| can also be installed 
+.. warning::
+   **Windows Users** Be warned that there is 260 character limit to the size of the ``PATH`` environment variable (see note `here <https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation>`_). If you modify the path (e.g. to add Python or the Python scripts directory), be sure that you do not exceed this limit. If you do, Windows will not report an error, but the characters past the limit will be ignored and so those directories will not be availble on the command prompt.
+
+.. note::
+
+   **Windows Users** If you do not use conda to install dependencies, we highly recommend installing two package managers, Chocolatey and vcpkg, to handle the installation of the non-Python dependencies. Instructions for installing Chocolatey can be found `here <https://chocolatey.org/install>`_ and instructions for installing vcpkg can be found `here <https://github.com/microsoft/vcpkg>`_.
+
+If you do not want to use conda, you can install Python yourself and then install 
+|yggdrasil| via ``pip`` or from the source code (See below). Python can be installed 
+in a number of ways, but be sure that you get Python>=3.5. Installation methods include:
+
+* Executable installer from the `Python Software Foundaction <https://www.python.org/downloads/>`_ (Recommended for Windows)
+* Package manager, e.g.
+  * ``brew install python3`` on Mac (be sure to include the 3)
+  * ``apt-get install python`` on Linux
+  * ``choco install python3`` on Windows
+* `Microsoft Visual Studio <https://visualstudio.microsoft.com/vs/features/python/>`_
+* `Microsoft Store <https://www.microsoft.com/en-us/p/python-38/9mssztt1n39l?activetab=pivot:overviewtab>`_ (Windows only; we do not recommend this method as it can be difficult to get working correctly as it requires tracking down the Scripts direcotry modifying the path yourself, see discussion `here <https://dev.to/naruaika/why-i-didn-t-install-python-from-the-microsoft-store-5cbd>`_)
+
+.. note::
+   **Mac Users** Python 2 is included on Mac as the default Python (as ``python``), but ``python3`` is what you will want to use (Python 2 has been deprecated). If you have Mac OS Catalina, you will already have Python 3, but you may need to enable developer tools to use it. You can check to see if Python 3 is installed (or prompt the developer tool `installation <https://apple.stackexchange.com/questions/376077/is-usr-bin-python3-provided-with-macos-catalina>`_) by running ``python3 --version`` and ``python --version``. If these commands do not return the same version, you will need to be sure to always use ``python3`` and ``pip3`` during installation (as opposed to the versions of the executables without the ``3``), or set Python 3 to be the default version of Python (see `this article <https://opensource.com/article/19/5/python-3-default-mac>`_).
+
+.. note::
+   **Windows Users** If you install Python, but your prompt cannot locate Python (i.e. ``where python`` fails), you may need to add the directory containing the Python executable (and the ``Scripts`` directory inside that as discussed below) to you ``PATH`` environment variable (e.g. ``set path=%path%;C:\path\to\Python\directory`` or ``setx path=%path%;C:\path\to\Python\directory`` to make the change for new prompts). For more information on setting the path, including instruction on seting in via the GUI, see `this article <https://datatofish.com/add-python-to-windows-path/>`_. 
+
+Once Python is installed, |yggdrasil| can be installed from the command line/prompt 
 from either `PyPI <https://pypi.org/project/yggdrasil-framework/>`_ 
-using ``pip``::
+using ``pip`` ::
 
   $ pip install yggdrasil-framework
 
@@ -108,21 +139,24 @@ Python commands::
 
   >>> import os
   >>> from distutils.sysconfig import get_python_lib
-  >>> os.path.realpath(os.path.join(get_python_lib(), '../../../bin/'))
+  >>> os.path.realpath(os.path.join(get_python_lib(), '..', '..', '..', 'bin'))
+
+.. note::
+   *Windows Users* If you used the Windows store to install Python, the above commands will not yield the correct scripts directory. It will be something along the lines of ``%userprofile%\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\Scripts``
 
 The displayed path can then be added either on the command link or in a startup
 script (e.g. ``.bashrc`` or ``.bash_profile``), using one of the following::
 
   $ export PATH=$PATH:<scripts_dir>  # (linux/osx, bash)
   $ setenv PATH $PATH:<scripts_dir>  # (linux/osx, tcsh)
-  $ set PATH "%PATH%:<scripts_dir>   # (windows)
+  $ set PATH=%PATH%:<scripts_dir>   # (windows)
 
 These commands will only add the directory to your path for the current 
 session. For the change to be permanent on Linux/MacOS, the appropriate command 
 from above can be added to your ``.bashrc`` or ``.bash_profile``. On 
 Windows (>=7), the following command will permanently modify your path::
 
-  $ setx PATH "%PATH%:<scripts_dir>
+  $ setx PATH=%PATH%:<scripts_dir>
 
 The changes will take affect the next time you open the terminal.
 
@@ -147,6 +181,44 @@ If you install |yggdrasil| using conda, these will be installed
 automatically as dependencies. If you are not using conda, you will need to 
 install them yourself.
 
+.. note::
+   Although not required, the ZeroMQ libraries are also recommended for message 
+   passing on Linux and MacOS operating systems as the IPC V message queues 
+   have default upper limits of 2048 bytes on some operating systems and will 
+   have to send larger messages piecemeal, adding to the message passing 
+   overhead. We recommend installing zeromq & czmq via apt on Linux 
+   (``apt-get libczmq-dev libzmq3-dev``) or Homebrew 
+   on Mac (``brew install czmq zmq``) if you do not use conda.
+
+Installing via vcpkg
+~~~~~~~~~~~~~~~~~~~~
+
+You can install the ZeroMQ C and C++ libraries via vcpkg (instructions for 
+installing vcpkg found `here <https://github.com/microsoft/vcpkg>`_. To 
+do so run the following from your "VS2015 x64 Native Tools Developer Command Prompt"::
+
+  > vcpkg install czmq zeromq --triplet x64-windows
+
+.. note::
+   The ``--triplet x64-windows`` flag indicates a 64 bit version of Windows 
+   (the most common). If you have a 32 bit Windows installation or are using a 
+   32 bit version of Python, omit the flag.
+
+When you run ``yggconfig`` following installation of yggdrasil
+If you did not set the ``VCPKG_ROOT`` environment variable before installing vcpkg, 
+you will need to add a flag indicating the location of the vcpkg installation when 
+running ``yggconfig`` following installation of yggdrasil. e.g.::
+
+  > yggconfig --vcpkg-dir=C:\path\to\vcpkg\root\directory
+
+If you do not do this, you will need to manually add the paths to the czmq and 
+zeromq libraries/headers to your |yggdrasil| configuration file (See 
+:ref:`Configuration Options <config_rst>`).
+
+
+Building from Source
+~~~~~~~~~~~~~~~~~~~~
+
 Instructions for installing the ZeroMQ C and C++ libraries can be found 
 `here <https://github.com/zeromq/czmq#building-and-installing>`_ 
 At install (and any time ``yggconfig`` is called), |yggdrasil| will attempt 
@@ -158,13 +230,6 @@ If you install these libraries after installing |yggdrasil| you can re-configure
 |yggdrasil| and have it search for the libraries again by calling ``yggconfig``
 from the command line or by setting the appropriate config options manually.
 
-.. note::
-   Although not required, the ZeroMQ libraries are also recommended for message 
-   passing on Linux and MacOS operating systems as the IPC V message queues 
-   have default upper limits of 2048 bytes on some operating systems and will 
-   have to send larger messages piecemeal, adding to the message passing 
-   overhead.
-
 
 Additional Steps for Matlab Models
 ----------------------------------
@@ -214,6 +279,11 @@ R2017b            2.7, 3.3, 3.4, 3.5, 3.6
 Additional Steps for R Models
 -----------------------------
 
+Rtools (Windows only)
+~~~~~~~~~~~~~~~~~~~~~
+
+On Windows, if you do not install the R dependencies via conda, you will also need to install Rtools so that the R dependencies with C/C++/Fortran components can be compiled if they need to be installed from source. The Rtools installer and instructions for installing Rtools for R<4.0.0 can be found `here <https://cran.r-project.org/bin/windows/Rtools/history.html>`_. For R>=4.0.0, you will need to install Rtools40 instead (installer/instructions `here <https://cran.r-project.org/bin/windows/Rtools/>`_).
+
 R Interpreter
 ~~~~~~~~~~~~~
 
@@ -273,6 +343,10 @@ If you install R and/or the R dependencies after installing |yggdrasil|, you can
   $ ygginstall R
 
 from your terminal (Linux/Mac) or Anaconda prompt (Windows).
+
+.. note::
+   If you see errors along the lines of ``'lib = "/usr/local/lib/R/site-library"' is not writable`` when running ``ygginstall R``, it is likely that the directory R is attempting to install in requires staff privileges to edit. On Unix systems (Linux/Mac), if you have ``sudo`` access you can either add yourself to the staff group (e.g. ``sudo usermod -a -G staff your_user_name``; then log off and back on) or add the ``--sudoR`` flag to the installation call (i.e. ``ygginstall R --sudoR``). On windows, you can open R from an administrator prompt and install the dependencies from CRAN directly as shown above. If you do not have administrator access, you can pick a directory that you have write access to and add it to the list of paths checked by R for R libraries in your .Rprofile file (i.e. add ``.libPaths( c( "~/userLibrary" , .libPaths() ) )``). `This article <https://support.rstudio.com/hc/en-us/articles/360047157094-Managing-R-with-Rprofile-Renviron-Rprofile-site-Renviron-site-rsession-conf-and-repos-conf>`_ describes what the .Rprofile file is and how to locate it. `This article <https://www.accelebrate.com/library/how-to-articles/r-rstudio-library>`_ discusses this in the context of Rstudio and how to make these changes via the Rstudio GUI on Windows.
+
    
 Additional Steps for RabbitMQ Message Passing
 ---------------------------------------------
@@ -291,3 +365,12 @@ downloading, installing, and starting a RabbitMQ server can be found
 `here <https://www.rabbitmq.com/download.html>`_. The default values
 for RabbitMQ related properties in the config file are set to the defaults
 for starting a RabbitMQ server.
+
+Additional Steps for OpenSimRoot
+--------------------------------
+
+If you would like to use OpenSimRoot, you will need to install GNU make. If you are on Linux/Mac or used conda to install |yggdrasil| it is likely that it will already be installed (you can run ``make --version`` to check). If it is not installed, and you are using a Mac OS, you can install XCode and the command line developer tools to get make and several other GNU tools (see relavent section of `this article <https://cloudtechpoint.medium.com/how-to-install-command-line-tools-homebrew-in-macos-without-xcode-1b910742d923>`_). If you are not on Mac, we recommend installing it via a package manager, e.g.
+
+* ``brew install make`` on Mac
+* ``apt-get install make`` on Linux
+* ``choco install make`` on Windows
diff --git a/recipe/meta.yaml b/recipe/meta.yaml
index 12c36582a..e5e2af168 100644
--- a/recipe/meta.yaml
+++ b/recipe/meta.yaml
@@ -1,5 +1,5 @@
 {% set name = "yggdrasil" %}
-{% set version = "1.3.0" %}
+{% set version = "1.4.0" %}
 
 package:
   name: {{ name|lower }}
@@ -10,17 +10,20 @@ source:
 
 build:
   number: 0
-  script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed --no-cache-dir -vvv "
+  script: '{{ PYTHON }} -m pip install . --no-deps --ignore-installed --no-cache-dir -vvv '
   skip: true  # [(win and vc<14) or py<35]
   entry_points:
+    - yggdrasil = yggdrasil.command_line:main
     - ygginfo = yggdrasil.command_line:ygginfo
     - yggrun = yggdrasil.command_line:yggrun
     - cisrun = yggdrasil.command_line:yggrun
     - yggcompile = yggdrasil.command_line:yggcompile
     - yggcc = yggdrasil.command_line:yggcc
+    - yggcctool = yggdrasil.command_line:cc_toolname
+    - yggldtool = yggdrasil.command_line:ld_toolname
     - yggccflags = yggdrasil.command_line:cc_flags
     - yggldflags = yggdrasil.command_line:ld_flags
-    - yggtest = yggdrasil:run_tsts
+    - yggtest = yggdrasil.command_line:run_tsts
     - yggmetaschema = yggdrasil.command_line:regen_metaschema
     - yggschema = yggdrasil.command_line:regen_schema
     - yggbuildapi_c = yggdrasil.command_line:rebuild_c_api
@@ -33,23 +36,30 @@ build:
     - yggvalidate = yggdrasil.command_line:validate_yaml
     - ygginstall = yggdrasil.command_line:ygginstall
     - yggdevup = yggdrasil.command_line:yggdevup
+    - ygggha = yggdrasil.command_line:generate_gha_workflow
 
 requirements:
   host:
     - czmq
     - pip
     - python
+    - r-base
+    - r-bit64
+    - r-r6
+    - r-reticulate
+    - r-units
+    - r-zeallot
     - sysv_ipc  # [unix]
     - zeromq
   run:
-    - cmake
+    - cmake  # [not win]
     - czmq
     - flaky
     - git
     - GitPython
     - jsonschema
     - matplotlib-base
-    - numpy
+    - numpy>=1.13.0
     - pandas
     - pyperf
     - pint  # [py2k]
@@ -72,6 +82,8 @@ requirements:
     - zeromq
     - {{ compiler('c') }}  # [win]
     - {{ compiler('cxx') }}  # [win]
+    # - {{ compiler('fortran') }}  # [not win]
+    # - fortran-compiler  # [not win]
     - m2w64-gcc-fortran  # [win]
     - m2w64-toolchain_win-64  # [win]
 
@@ -86,7 +98,7 @@ about:
   license: BSD-3-Clause
   license_family: BSD
   license_file: LICENSE.txt
-  summary: 'A framework for connecting computational models from multiple languages.'
+  summary: A framework for connecting computational models from multiple languages.
 
   description: |
     The Crops in Silico (CiS) framework, yggdrasil, provides support for
diff --git a/recipe/pre-unlink.bat b/recipe/pre-unlink.bat
new file mode 100644
index 000000000..f60eb59db
--- /dev/null
+++ b/recipe/pre-unlink.bat
@@ -0,0 +1,8 @@
+set "rdir=%CONDA_PREFIX%\lib\R\library\yggdrasil\"
+IF exist %rdir% (
+    RD /S /Q %rdir%
+)
+set "config_file=%CONDA_PREFIX%\.yggdrasil.cfg"
+IF exist %config_file% (
+    DEL %config_file%
+)
diff --git a/recipe/pre-unlink.sh b/recipe/pre-unlink.sh
new file mode 100644
index 000000000..0ec9b3346
--- /dev/null
+++ b/recipe/pre-unlink.sh
@@ -0,0 +1,8 @@
+rdir="${CONDA_PREFIX}/lib/R/library/yggdrasil"
+if [ -d "$rdir" ]; then
+    rm -rf $rdir
+fi
+config_file="${CONDA_PREFIX}/.yggdrasil.cfg"
+if [ -f "$config_file" ]; then
+    rm $config_file
+fi
diff --git a/requirements_condaonly.txt b/requirements_condaonly.txt
index 25304be11..2c00d65b4 100644
--- a/requirements_condaonly.txt
+++ b/requirements_condaonly.txt
@@ -1,14 +1,16 @@
-cmake
-czmq
-r-rcpp<=1.0.3; platform_system == 'Darwin'
-r-base
-r-bit64
-r-r6
-r-reticulate
-r-zeallot
-r-units
-zeromq
-c-compiler; platform_system == 'Windows'
-cxx-compiler; platform_system == 'Windows'
-m2w64-gcc-fortran; platform_system == 'Windows'
-m2w64-toolchain_win-64; platform_system == 'Windows'
+cmake; platform_system != 'Windows'  # [conda,c]
+czmq             # [conda,zmq,c]
+matplotlib       # [conda]
+r-rcpp<=1.0.3; platform_system == 'Darwin'  # [conda,R]
+r-base           # [conda,R]
+r-bit64          # [conda,R]
+r-r6             # [conda,R]
+r-reticulate     # [conda,R]
+r-zeallot        # [conda,R]
+r-units          # [conda,R]
+zeromq           # [conda,zmq,c]
+# c-compiler; platform_system == 'Windows'    # [conda,c]
+# cxx-compiler; platform_system == 'Windows'  # [conda,c]
+m2w64-gcc-fortran; platform_system == 'Windows'       # [conda,fortran]
+m2w64-toolchain_win-64; platform_system == 'Windows'  # [conda,fortran]
+fortran-compiler; platform_system != 'Windows'  # [conda,fortran]
diff --git a/requirements_development.txt b/requirements_development.txt
index 44dc48fd1..3a2166050 100644
--- a/requirements_development.txt
+++ b/requirements_development.txt
@@ -1,2 +1,2 @@
 black
-git  # conda
+git  # [conda]
diff --git a/requirements_distribution.txt b/requirements_distribution.txt
index dba36a40c..48c470e12 100644
--- a/requirements_distribution.txt
+++ b/requirements_distribution.txt
@@ -1,3 +1,3 @@
-conda-build
-conda-smithy
+conda-build   # [conda]
+conda-smithy  # [conda]
 twine
diff --git a/requirements_documentation.txt b/requirements_documentation.txt
index 20ac43db3..52910cd94 100644
--- a/requirements_documentation.txt
+++ b/requirements_documentation.txt
@@ -1,4 +1,4 @@
-doxygen  # conda
+doxygen  # [conda]
 breathe
 sphinx
 sphinx_rtd_theme
diff --git a/requirements_optional.txt b/requirements_optional.txt
index 6b889ffaa..5fb6c2c31 100644
--- a/requirements_optional.txt
+++ b/requirements_optional.txt
@@ -1,2 +1,5 @@
-astropy
-pika<1.0.0b1
+astropy       # [astropy]
+pika<1.0.0b1  # [rmq]
+pygments      # [pygments]
+trimesh       # [trimesh]
+libroadrunner  # [pip,sbml]
diff --git a/requirements_piponly.txt b/requirements_piponly.txt
index 0293e896d..b6a244d75 100644
--- a/requirements_piponly.txt
+++ b/requirements_piponly.txt
@@ -1,1 +1,1 @@
-libroadrunner
+libroadrunner  # [pip,sbml]
diff --git a/setup.cfg b/setup.cfg
index 599ac1509..0b2954434 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,8 +6,6 @@ cover-package=yggdrasil
 with-coverage=1
 
 [tool:pytest]
-cov-fail-under = 100
-ignore = yggdrasil/rapidjson/
 norecursedirs = .* build dist CVS _darcs {arch} *.egg rapidjson
 python_files = test_*.py __init__.py
 
@@ -30,6 +28,7 @@ omit =
      */yggdrasil/examples/*/src/*.py
      */yggdrasil/doctools.py
      */yggdrasil/demos/*
+     */yggdrasil/__main__.py
 
 [coverage:report]
 sort = Cover
@@ -43,6 +42,7 @@ exclude_lines =
      pragma: config
      pragma: completion
      pragma: testing
+     pragma: version
      def __repr__
      def description_prefix
      if 0:
diff --git a/setup.py b/setup.py
index c4831aea0..ecc6f6264 100644
--- a/setup.py
+++ b/setup.py
@@ -61,11 +61,11 @@
 if '--user' in sys.argv:
     script_dir = os.path.realpath(os.path.join(get_python_lib(),
                                                '../../../bin/'))
-    warnings.warn("When installing locally, you may need to add the script " +
-                  "directory to your path manually in order to have access " +
-                  "to the command line entry points (e.g. yggrun). " +
-                  "If 'yggrun' is not a recognized command, try adding " +
-                  "'%s' to your PATH." % script_dir)
+    warnings.warn("When installing locally, you may need to add the script "
+                  + "directory to your path manually in order to have access "
+                  + "to the command line entry points (e.g. yggrun). "
+                  + "If 'yggrun' is not a recognized command, try adding "
+                  + "'%s' to your PATH." % script_dir)
     
     
 setup(
@@ -103,14 +103,17 @@
         "Development Status :: 5 - Production/Stable",
     ],
     entry_points={
-        'console_scripts': ['ygginfo=yggdrasil.command_line:ygginfo',
+        'console_scripts': ['yggdrasil=yggdrasil.command_line:main',
+                            'ygginfo=yggdrasil.command_line:ygginfo',
                             'yggrun=yggdrasil.command_line:yggrun',
                             'cisrun=yggdrasil.command_line:yggrun',
                             'yggcompile=yggdrasil.command_line:yggcompile',
                             'yggcc=yggdrasil.command_line:yggcc',
+                            'yggcctool=yggdrasil.command_line:cc_toolname',
+                            'yggldtool=yggdrasil.command_line:ld_toolname',
                             'yggccflags=yggdrasil.command_line:cc_flags',
                             'yggldflags=yggdrasil.command_line:ld_flags',
-                            'yggtest=yggdrasil:run_tsts',
+                            'yggtest=yggdrasil.command_line:run_tsts',
                             'yggmetaschema=yggdrasil.command_line:regen_metaschema',
                             'yggschema=yggdrasil.command_line:regen_schema',
                             'yggbuildapi_c=yggdrasil.command_line:rebuild_c_api',
@@ -124,7 +127,8 @@
                             'ygginstall=yggdrasil.command_line:ygginstall',
                             'yggclean=yggdrasil.command_line:yggclean',
                             'yggmodelform=yggdrasil.command_line:yggmodelform',
-                            'yggdevup=yggdrasil.command_line:yggdevup'],
+                            'yggdevup=yggdrasil.command_line:yggdevup',
+                            'ygggha=yggdrasil.command_line:generate_gha_workflow'],
     },
     license="BSD",
     python_requires='>=3.5',
diff --git a/utils/ci_setup.py b/utils/ci_setup.py
deleted file mode 100644
index 1485635c2..000000000
--- a/utils/ci_setup.py
+++ /dev/null
@@ -1,359 +0,0 @@
-import os
-import sys
-import argparse
-import uuid
-import pprint
-import shutil
-import subprocess
-PYVER = ('%s.%s' % sys.version_info[:2])
-PY2 = (sys.version_info[0] == 2)
-_is_osx = (sys.platform == 'darwin')
-_is_linux = ('linux' in sys.platform)
-_is_win = (sys.platform in ['win32', 'cygwin'])
-INSTALLLPY = (os.environ.get('INSTALLLPY', '0') == '1')
-if _is_win:
-    INSTALLC = (os.environ.get('INSTALLC', '0') == '1')
-else:
-    INSTALLC = True  # c compiler usually installed by default
-INSTALLR = (os.environ.get('INSTALLR', '0') == '1')
-INSTALLFORTRAN = (os.environ.get('INSTALLFORTRAN', '0') == '1')
-if not INSTALLC:
-    INSTALLFORTRAN = False
-INSTALLSBML = (os.environ.get('INSTALLSBML', '0') == '1')
-INSTALLAPY = (os.environ.get('INSTALLAPY', '0') == '1')
-INSTALLZMQ = (os.environ.get('INSTALLZMQ', '0') == '1')
-INSTALLRMQ = (os.environ.get('INSTALLRMQ', '0') == '1')
-INSTALLTRIMESH = (os.environ.get('INSTALLTRIMESH', '0') == '1')
-INSTALLPYGMENTS = (os.environ.get('INSTALLPYGMENTS', '0') == '1')
-BUILDDOCS = (os.environ.get('BUILDDOCS', '0') == '1')
-
-
-def call_conda_command(args, **kwargs):
-    r"""Function for calling conda commands as the conda script is not
-    available on subprocesses for windows unless invoked via the shell.
-
-    Args:
-        args (list): Command arguments.
-        **kwargs: Additional keyword arguments are passed to subprocess.check_output.
-
-    Returns:
-        str: The output from the command.
-
-    """
-    if _is_win:
-        args = ' '.join(args)
-        kwargs['shell'] = True
-    return subprocess.check_output(args, **kwargs).decode("utf-8")
-
-
-def call_script(lines):
-    r"""Write lines to a script and call it.
-
-    Args:
-        lines (list): Lines that should be written to the script.
-
-    """
-    if not lines:
-        return
-    if _is_win:  # pragma: windows
-        script_ext = '.bat'
-        error_check = 'if %errorlevel% neq 0 exit /b %errorlevel%'
-        for i in range(len(lines), 0, -1):
-            lines.insert(i, error_check)
-    else:
-        script_ext = '.sh'
-        if lines[0] != '#!/bin/bash':
-            lines.insert(0, '#!/bin/bash')
-        error_check = 'set -e'
-        if error_check not in lines:
-            lines.insert(1, error_check)
-    fname = 'ci_script_%s%s' % (str(uuid.uuid4()), script_ext)
-    try:
-        pprint.pprint(lines)
-        with open(fname, 'w') as fd:
-            fd.write('\n'.join(lines))
-            
-        call_kws = {}
-        if _is_win:  # pragma: windows
-            call_cmd = [fname]
-        else:
-            call_cmd = ['./%s' % fname]
-            os.chmod(fname, 0o755)
-        subprocess.check_call(call_cmd, **call_kws)
-    finally:
-        if os.path.isfile(fname):
-            os.remove(fname)
-
-
-def setup_package_on_ci(method, python):
-    r"""Setup a test environment on a CI resource.
-
-    Args:
-        method (str): Method that should be used to build and install
-            the package. Valid values include 'conda' and 'pip'.
-        python (str): Version of Python that should be tested.
-
-    Raises:
-        ValueError: If method is not 'conda' or 'pip'.
-
-    """
-    cmds = []
-    major, minor = [int(x) for x in python.split('.')]
-    if os.environ.get('GITHUB_ACTIONS', False):
-        conda_cmd = '$CONDA/bin/conda'
-    elif _is_win:
-        conda_cmd = 'call conda'
-    else:
-        conda_cmd = 'conda'
-    if method == 'conda':
-        cmds += [
-            "echo Installing Python using conda...",
-            # Configure conda
-            "%s config --set always_yes yes --set changeps1 no" % conda_cmd,
-            # "%s config --set channel_priority strict" % conda_cmd,
-            "%s config --add channels conda-forge" % conda_cmd,
-            "%s update -q conda" % conda_cmd,
-            # "%s config --set allow_conda_downgrades true" % conda_cmd,
-            # "%s install -n root conda=4.9" % conda_cmd,
-            "%s create -q -n test-environment python=%s" % (conda_cmd, python)
-        ]
-    elif method == 'pip':
-        if INSTALLLPY or _is_win:
-            setup_package_on_ci('conda', python)
-        elif _is_osx:
-            cmds.append("echo Installing Python using virtualenv...")
-            pip_cmd = 'pip'
-            if sys.version_info[0] != major:
-                pip_cmd = 'pip%d' % major
-                try:
-                    call_script(['python%d --version' % major])
-                except BaseException:
-                    cmds.append('brew install python%d' % major)
-            cmds += [
-                "%s install --upgrade pip virtualenv" % pip_cmd,
-                "virtualenv -p python%d venv" % major
-            ]
-    else:  # pragma: debug
-        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
-                         % method)
-    call_script(cmds)
-    
-
-def deploy_package_on_ci(method, verbose=False):
-    r"""Build and install the package and its dependencies on a CI
-    resource.
-
-    Args:
-        method (str): Method that should be used to build and install
-            the package. Valid values include 'conda' and 'pip'.
-        verbose (bool, optional): If True, setup steps are run with verbosity
-            turned up. Defaults to False.
-
-    Raises:
-        ValueError: If method is not 'conda' or 'pip'.
-
-    """
-    if _is_win:
-        conda_cmd = 'call conda'
-    else:
-        conda_cmd = 'conda'
-    cmds = [
-        # Check that we have the expected version of Python
-        "python --version",
-    ]
-    # Upgrade pip and setuptools and wheel to get clean install
-    upgrade_pkgs = ['wheel', 'setuptools']
-    if not _is_win:
-        upgrade_pkgs.insert(0, 'pip')
-    cmds += ["pip install --upgrade %s" % ' '.join(upgrade_pkgs)]
-    # Uninstall default numpy and matplotlib to allow installation
-    # of specific versions
-    cmds += ["pip uninstall -y numpy matplotlib"]
-    # Get dependencies
-    install_req = os.path.join("utils", "install_from_requirements.py")
-    conda_pkgs = []
-    pip_pkgs = []
-    requirements_files = ['requirements_testing.txt']
-    _in_conda = False
-    if method == 'conda':
-        _in_conda = True
-        default_pkgs = conda_pkgs
-    elif method == 'pip':
-        _in_conda = (_is_win or INSTALLLPY)
-        default_pkgs = pip_pkgs
-    else:  # pragma: debug
-        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
-                         % method)
-    # Installing via pip causes import error on Windows and
-    # a conflict when installing LPy
-    conda_pkgs += ['scipy', os.environ.get('NUMPY', 'numpy')]
-    default_pkgs += [os.environ.get('MATPLOTLIB', 'matplotlib'),
-                     os.environ.get('JSONSCHEMA', 'jsonschema')]
-    if INSTALLSBML:
-        pip_pkgs.append('libroadrunner')
-    if INSTALLAPY:
-        default_pkgs.append('astropy')
-    if INSTALLRMQ:
-        default_pkgs.append("\"pika<1.0.0b1\"")
-    if INSTALLTRIMESH:
-        default_pkgs.append('trimesh')
-    if INSTALLPYGMENTS:
-        default_pkgs.append('pygments')
-    if (method == 'pip') and _in_conda:
-        if INSTALLR:
-            conda_pkgs.append('r-base')
-        if INSTALLFORTRAN:
-            if _is_win:
-                conda_pkgs += ['m2w64-gcc-fortran', 'make',
-                               'm2w64-toolchain_win-64']
-            else:
-                conda_pkgs += ['fortran-compiler']
-        if INSTALLZMQ:
-            conda_pkgs += ['czmq', 'zeromq']
-    if BUILDDOCS:
-        requirements_files.append('requirements_documentation.txt')
-    # Install dependencies
-    if method == 'conda':
-        cmds += [
-            "%s install -q -n base conda-build conda-verify" % conda_cmd,
-        ]
-    elif method == 'pip':
-        if INSTALLR and (not _in_conda):
-            cmds.append("echo Installing R...")
-            if _is_linux:
-                cmds += [("sudo add-apt-repository 'deb https://cloud"
-                          ".r-project.org/bin/linux/ubuntu xenial-cran35/'"),
-                         ("sudo apt-key adv --keyserver keyserver.ubuntu.com "
-                          "--recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9"),
-                         "sudo apt update",
-                         "sudo apt-get install r-base r-base-dev",
-                         "sudo apt-get install libudunits2-dev"]
-            elif _is_osx:
-                cmds += ["brew install r",
-                         "brew install udunits"]
-            else:
-                raise NotImplementedError("Could not determine "
-                                          "R installation method.")
-        if INSTALLFORTRAN and (not _in_conda):
-            cmds.append("echo Installing Fortran...")
-            if _is_linux:
-                cmds += ["sudo apt-get install gfortran"]
-            elif _is_osx:
-                cmds += ["brew install gfortran"]
-            else:
-                raise NotImplementedError("Could not determine "
-                                          "Fortran installation method.")
-        if INSTALLZMQ and (not _in_conda):
-            cmds.append("echo Installing ZeroMQ...")
-            if _is_linux:
-                cmds.append("./ci/install-czmq-linux.sh")
-            elif _is_osx:
-                cmds.append("bash ci/install-czmq-osx.sh")
-            # elif _is_win:
-            #     cmds += ["call ci\\install-czmq-windows.bat",
-            #              "echo \"%PATH%\""]
-            else:
-                raise NotImplementedError("Could not determine "
-                                          "ZeroMQ installation method.")
-    if _in_conda:
-        if conda_pkgs:
-            cmds += ["%s install %s" % (conda_cmd, ' '.join(conda_pkgs))]
-    else:
-        pip_pkgs += conda_pkgs
-    if pip_pkgs:
-        cmds += ["pip install %s" % ' '.join(pip_pkgs)]
-    for x in requirements_files:
-        cmds += ["python %s %s %s" % (install_req, method, x)]
-    if INSTALLLPY:
-        if _in_conda:
-            cmds += ["%s install openalea.lpy boost=1.66.0 -c openalea"
-                     % conda_cmd]
-        else:  # pragma: debug
-            raise RuntimeError("Could not detect conda environment. "
-                               "Cannot proceed with a conda deployment "
-                               "(required for LPy).")
-    # Install yggdrasil
-    if method == 'conda':
-        # Install from conda build
-        # Assumes that an environment is active
-        conda_prefix = os.environ.get('CONDA_PREFIX', None)
-        if not conda_prefix:
-            if os.environ.get('GITHUB_ACTIONS', False):
-                conda_prefix = os.environ['CONDA']
-            else:
-                conda_prefix = shutil.which('conda')
-        prefix_dir = os.path.dirname(os.path.dirname(conda_prefix))
-        index_dir = os.path.join(prefix_dir, "conda-bld")
-        if verbose:
-            build_flags = ''
-            install_flags = '-vvv'
-        else:
-            build_flags = '-q'
-            install_flags = '-q'
-        cmds += [
-            "%s build %s --python %s %s" % (
-                conda_cmd, 'recipe', PYVER, build_flags),
-            "%s index %s" % (conda_cmd, index_dir),
-            "%s install %s --use-local --update-deps yggdrasil" % (
-                conda_cmd, install_flags),
-            # "%s install %s --update-deps -c file:/%s/conda-bld yggdrasil" % (
-            #     conda_cmd, install_flags, prefix_dir),
-        ]
-    elif method == 'pip':
-        if verbose:
-            build_flags = ''
-            install_flags = '--verbose'
-        else:
-            build_flags = '--quiet'
-            install_flags = ''
-        # Install from source dist
-        cmds += ["python setup.py %s sdist" % build_flags]
-        if _is_win:  # pragma: windows
-            cmds += [
-                "for %%a in (\"dist\\*.tar.gz\") do set YGGSDIST=%%a",
-                "echo %YGGSDIST%"
-            ]
-            sdist = "%YGGSDIST%"
-        else:
-            sdist = "dist/*.tar.gz"
-        cmds += [
-            "pip install %s %s" % (install_flags, sdist),
-            "python create_coveragerc.py"
-        ]
-    else:  # pragma: debug
-        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
-                         % method)
-    # Print summary of what was installed
-    cmds.append('pip list')
-    if _in_conda:
-        cmds.append("%s list" % conda_cmd)
-    call_script(cmds)
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        "Perform setup operations to test package build and "
-        "installation on continuous integration services.")
-    parser.add_argument(
-        'method', choices=['conda', 'pip'],
-        help=("Method that should be used to build "
-              "and install the packages and its "
-              "dependencies."))
-    subparsers = parser.add_subparsers(
-        dest='operation',
-        help="CI setup operation that should be performed.")
-    parser_env = subparsers.add_parser(
-        'env', help="Setup an environment for testing.")
-    parser_env.add_argument(
-        'python',
-        help="Version of python that should be tested.")
-    parser_dep = subparsers.add_parser(
-        'deploy', help="Build and install package.")
-    parser_dep.add_argument(
-        '--verbose', action='store_true',
-        help="Turn up verbosity of output from setup step.")
-    args = parser.parse_args()
-    if args.operation in ['env', 'setup']:
-        setup_package_on_ci(args.method, args.python)
-    elif args.operation == 'deploy':
-        deploy_package_on_ci(args.method, verbose=args.verbose)
diff --git a/utils/create_envs.py b/utils/create_envs.py
index 7cc57fc26..83ed3554a 100644
--- a/utils/create_envs.py
+++ b/utils/create_envs.py
@@ -1,55 +1,10 @@
 import os
 import argparse
-from install_from_requirements import locate_conda_exe
-from ci_setup import call_conda_command
+from setup_test_env import create_env, install_pkg
 _utils_dir = os.path.dirname(os.path.abspath(__file__))
 _pkg_dir = os.path.dirname(_utils_dir)
 
 
-def conda_env_exists(name):
-    r"""Determine if a conda environment already exists.
-
-    Args:
-        name (str): Name of the environment to check.
-
-    Returns:
-        bool: True the the environment exits, False otherwise.
-
-    """
-    args = ['conda', 'info', '--envs']
-    out = call_conda_command(args)
-    envs = []
-    for x in out.splitlines():
-        if x.startswith('#') or (not x):
-            continue
-        envs.append(x.split()[0])
-    return (name in envs)
-
-
-def create_env(name, python='3.6', packages=None):
-    r"""Create a new conda environment with the specified parameters.
-
-    Args:
-        name (str): Name of the environment being created.
-        python (str, optional): Version of python that should be used. Defaults
-            to '3.6'.
-        packages (list, optional): Packages that should be installed in the new
-            environment. Defaults to None and is ignored.
-
-    """
-    if conda_env_exists(name):
-        print("Conda env with name '%s' already exists." % name)
-        return
-    if packages is None:
-        packages = []
-    if 'ipython' not in packages:
-        packages.append('ipython')
-    args = ['conda', 'create', '-y', '-n', name, 'python=%s' % python] + packages
-    print('running', args)
-    print(call_conda_command(args))
-    assert(conda_env_exists(name))
-
-
 def create_devenv(env_type, python='3.6', name=None, **kwargs):
     r"""Create a new conda environment with the specified parameters with
     yggdrasil installed in development mode.
@@ -68,60 +23,55 @@ def create_devenv(env_type, python='3.6', name=None, **kwargs):
     assert(env_type in ['pip', 'conda'])
     if name is None:
         name = '%s%s' % (env_type, python.replace('.', ''))
-    create_env(name, python=python, **kwargs)
-    python_cmd = locate_conda_exe(name, 'python')
-    req_suffixes = ['', '_testing', '_development']
-    if env_type == 'conda':
-        req_suffixes.append('_condaonly')
-        # TODO: Find a way to install pip-only packages into conda
-        # environment
-    elif env_type == 'pip':
-        req_suffixes.append('_piponly')
-        print(call_conda_command(['conda', 'install', '-y', '--name', name,
-                                  'czmq', 'zeromq']))
-    req = [os.path.join(_pkg_dir, 'requirements%s.txt' % x)
-           for x in req_suffixes]
-    args = ([python_cmd, os.path.join(_utils_dir,
-                                      'install_from_requirements.py'),
-             '--conda-env', name, env_type] + req)
-    print(call_conda_command(args))
-    # This is not called directly because it needs to be invoked with the
-    # version of Python installed in the target environment
-    # install_from_requirements(env_type, req, conda_env=name)
-    print(call_conda_command([python_cmd, 'setup.py', 'develop'],
-                             cwd=_pkg_dir))
-    print("Created environment %s" % name)
+    create_env('conda', python=python, name=name, **kwargs)
+    install_pkg('%s-dev' % env_type, python=python,
+                conda_env=name, always_yes=True, only_python=True,
+                fallback_to_conda=True)
+    # python_cmd = locate_conda_exe(name, 'python')
+    # req_suffixes = ['', '_testing', '_development']
+    # if env_type == 'conda':
+    #     req_suffixes.append('_condaonly')
+    #     # TODO: Find a way to install pip-only packages into conda
+    #     # environment
+    # elif env_type == 'pip':
+    #     req_suffixes.append('_piponly')
+    #     print(call_conda_command(['conda', 'install', '-y', '--name', name,
+    #                               'czmq', 'zeromq']))
+    # req = [os.path.join(_pkg_dir, 'requirements%s.txt' % x)
+    #        for x in req_suffixes]
+    # args = ([python_cmd, os.path.join(_utils_dir,
+    #                                   'install_from_requirements.py'),
+    #          '--conda-env', name, env_type] + req)
+    # print(call_conda_command(args))
+    # # This is not called directly because it needs to be invoked with the
+    # # version of Python installed in the target environment
+    # # install_from_requirements(env_type, req, conda_env=name)
+    # print(call_conda_command([python_cmd, 'setup.py', 'develop'],
+    #                          cwd=_pkg_dir))
+    # print("Created environment %s" % name)
     
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(
         "Create dev environments for matrix of installation methods "
         "and Python versions.")
-    parser.add_argument('--method', choices=['conda', 'pip', 'both'], default='both',
+    parser.add_argument('--method', '--methods',
+                        nargs='+', default=['conda', 'pip'],
+                        choices=['conda', 'pip'],
                         help=("Method(s) that should be used to install the "
                               "dependencies."))
-    parser.add_argument('--version', '--python', default='2&3',
-                        choices=['2.7', '3.6', '3.7', '3.8', '2&3'],
+    parser.add_argument('--version', '--versions', '--python', '--pythons',
+                        nargs='+', default=['3.6'],
+                        choices=['3.6', '3.7', '3.8', '3.9'],
                         help=("Python version(s) to create environments for."))
     parser.add_argument('--name', default=None,
                         help=("Name of the conda env that should be created or "
                               "updated. If provided, 'method' will default to "
                               "'conda' and 'version' will default to '3.6'."))
     args = parser.parse_args()
-    if args.method == 'both':
-        if args.name:
-            args.method = ['conda']
-        else:
-            args.method = ['pip', 'conda']
-    else:
-        args.method = [args.method]
-    if args.version == '2&3':
-        if args.name:
-            args.version = ['3.6']
-        else:
-            args.version = ['2.7', '3.6']
-    else:
-        args.version = [args.version]
+    if args.name:
+        args.method = ['conda']
+        args.version = ['3.6']
     for env_type in args.method:
         for python in args.version:
             create_devenv(env_type, python=python, name=args.name)
diff --git a/utils/install_from_requirements.py b/utils/install_from_requirements.py
index 36db416b7..15c1d3be4 100644
--- a/utils/install_from_requirements.py
+++ b/utils/install_from_requirements.py
@@ -1,12 +1,79 @@
 # https://www.python.org/dev/peps/pep-0508/
 from pip._vendor.packaging.requirements import Requirement, InvalidRequirement
 import os
-import sys
+import re
+import copy
+import uuid
+import pprint
 import argparse
-from ci_setup import call_conda_command
+from setup_test_env import (
+    call_conda_command, locate_conda_exe, get_install_opts,
+    PYTHON_CMD, CONDA_CMD, _is_win)
 
 
-def prune(fname_in, fname_out=None, excl_suffix=None):
+def isolate_package_name(entry):
+    r"""Get the package name without any constraints or conditions.
+
+    Args:
+        entry (str): Requirements entry.
+
+    Returns:
+        str: Package name.
+
+    """
+    keys = ['<', '>', '=', ';', '#']
+    out = entry
+    for k in keys:
+        out = out.split(k)[0].strip()
+    return out.strip()
+
+
+def get_pip_dependencies(pkg):
+    r"""Get the dependencies required by a package via pip.
+
+    Args:
+        pkg (str): The name of a pip-installable package.
+
+    Returns:
+        list: The package's dependencies.
+
+    """
+    import requests
+    url = 'https://pypi.org/pypi/{}/json'
+    json = requests.get(url.format(pkg)).json()
+    return json['info']['requires_dist']
+
+
+def get_pip_dependency_version(pkg, dep):
+    r"""Get the version of a dependency required by a pip-installable
+    package.
+
+    Args:
+        pkg (str): The name of a pip-installable package.
+        dep (str): The name of a dependency of pkg.
+
+    Returns:
+        str: The version of the dependency required by the package.
+
+    """
+    reqs = get_pip_dependencies(pkg)
+    dep_regex = r'%s(?:\s*\((?P<ver>[^\)]+)\))?' % dep
+    for x in reqs:
+        m = re.fullmatch(dep_regex, x)
+        if m:
+            ver = ''
+            if m.group('ver'):
+                ver = m.group('ver').strip()
+            return dep + ver
+    raise RuntimeError(
+        ("Could not locate the dependency '%s' "
+         "in the list of requirements for package '%s': %s")
+        % (dep, pkg, reqs))
+
+
+def prune(fname_in, fname_out=None, excl_method=None, incl_method=None,
+          install_opts=None, additional_packages=[], skip_packages=[],
+          verbose=False):
     r"""Prune a requirements.txt file to remove/select dependencies that are
     dependent on the current environment.
 
@@ -15,27 +82,88 @@ def prune(fname_in, fname_out=None, excl_suffix=None):
             should be read.
         fname_out (str, optional): Full path to requirements file that should be
             created. Defaults to None and is set to <fname_in[0]>_pruned.txt.
-        excl_suffix (str, optional): Lines ending in this string will be
-            excluded. Defaults to None and is ignored.
+        excl_method (str, optional): Installation method (pip or conda) that
+            should be ignored. Defaults to None and is ignored.
+        incl_method (str, optional): Installation method (pip or conda) that
+            should be installed (requirements with without an installation method
+            or with a different method will be ignored). Defaults to None and is
+            ignored.
+        install_opts (dict, optional): Mapping from language/package to bool
+            specifying whether or not the language/package should be installed.
+            If not provided, get_install_opts is used to create it.
+        additional_packages (list, optional): Additional packages that should
+            be installed. Defaults to empty list. Versions specified here take
+            precedence over versions in the provided files.
+        skip_packages (list, optional): A list of packages that should not
+            be added to the pruned list. Defaults to an empty list.
+        verbose (bool, optional): If True, setup steps are run with verbosity
+            turned up. Defaults to False.
 
     Returns:
         str: Full path to created file.
 
     """
+    regex_constrain = r'(?:(?:pip)|(?:conda)|(?:[a-zA-Z][a-zA-Z0-9]*))'
+    regex_comment = r'\s*\[\s*(?P<vals>%s(?:\s*\,\s*%s)*)\s*\]\s*' % (
+        regex_constrain, regex_constrain)
+    # regex_elem = r'(?P<val>%s)\s*(?:(?:\,)|(?:\]))' % regex_constrain
+    install_opts = get_install_opts(install_opts)
     if not isinstance(fname_in, (list, tuple)):
         fname_in = [fname_in]
+    packages = []
     new_lines = []
+    orig_lines = copy.copy(additional_packages)
+    for line in additional_packages:
+        pkg = isolate_package_name(line)
+        if pkg in packages:
+            continue
+        if pkg in skip_packages:
+            continue
+        new_lines.append(line)
+        packages.append(pkg)
     for ifname_in in fname_in:
         with open(ifname_in, 'r') as fd:
             old_lines = fd.readlines()
+            orig_lines += old_lines
         for line in old_lines:
             line = line.strip()
             if line.startswith('#'):
                 continue
-            if excl_suffix and line.endswith(excl_suffix):
+            pkg = isolate_package_name(line)
+            if pkg in packages:
+                continue
+            if pkg in skip_packages:
+                continue
+            packages.append(pkg)
+            skip_line = False
+            req_name = line
+            if '#' in line:
+                req_name, comment = line.split('#')
+                m = re.fullmatch(regex_comment, comment)
+                if m:
+                    values = [x.strip() for x in m.group('vals').split(',')]
+                    if verbose:
+                        print('line: %s, values = %s, excl = %s, incl = %s'
+                              % (line, values, excl_method, incl_method))
+                    if excl_method and (excl_method in values):
+                        continue
+                    if incl_method and (incl_method not in values):
+                        continue
+                    for v in values:
+                        if v not in ['pip', 'conda']:
+                            if v not in install_opts:
+                                raise RuntimeError("Unsupported install opt: '%s'" % v)
+                            if not install_opts[v]:
+                                skip_line = True
+                                break
+                elif incl_method:
+                    skip_line = True
+            elif incl_method:
+                skip_line = True
+            if skip_line:
                 continue
             try:
-                req = Requirement(line.split('#')[0].strip())
+                req = Requirement(req_name.strip())
                 if req.marker and (not req.marker.evaluate()):
                     continue
                 new_lines.append(req.name + str(req.specifier))
@@ -44,51 +172,25 @@ def prune(fname_in, fname_out=None, excl_suffix=None):
                 continue
     # Write file
     if fname_out is None:
-        fname_out = '_pruned'.join(os.path.splitext(fname_in[0]))
-    with open(fname_out, 'w') as fd:
-        fd.write('\n'.join(new_lines))
+        fname_out = ('_pruned%s' % str(uuid.uuid4())).join(os.path.splitext(fname_in[0]))
+    if new_lines:
+        with open(fname_out, 'w') as fd:
+            fd.write('\n'.join(new_lines))
+    if verbose:
+        print('INSTALL OPTS:\n%s' % pprint.pformat(install_opts))
+        print('ORIGINAL DEP LIST:\n\t%s\nPRUNED DEP LIST:\n\t%s'
+              % ('\n\t'.join([x.strip() for x in orig_lines]),
+                 '\n\t'.join(new_lines)))
     return fname_out
 
 
-def locate_conda_exe(conda_env, name):
-    r"""Determine the full path to an executable in a specific conda environment.
-
-    Args:
-        conda_env (str): Name of conda environment that executable should be
-            returned for.
-        name (str): Name of the executable to locate.
-
-    Returns:
-        str: Full path to the executable.
-
-    """
-    conda_prefix = os.environ.get('CONDA_PREFIX', None)
-    assert(conda_prefix)
-    if os.path.dirname(conda_prefix).endswith('envs'):
-        conda_prefix = os.path.dirname(conda_prefix)
-    else:
-        conda_prefix = os.path.join(conda_prefix, 'envs')
-    if (sys.platform in ['win32', 'cygwin']):
-        if not name.endswith('.exe'):
-            name += '.exe'
-        if name.startswith('python'):
-            out = os.path.join(conda_prefix, conda_env, name)
-        else:
-            out = os.path.join(conda_prefix, conda_env,
-                               'Scripts', name)
-    else:
-        out = os.path.join(conda_prefix, conda_env, 'bin', name)
-    try:
-        assert(os.path.isfile(out))
-    except AssertionError:
-        out = os.path.expanduser(os.path.join('~', '.conda', 'envs', name))
-        if not os.path.isfile(out):
-            raise
-    return out
-
-
 def install_from_requirements(method, fname_in, conda_env=None,
-                              user=False):
+                              user=False, unique_to_method=False,
+                              python_cmd=None, install_opts=None,
+                              verbose=False, verbose_prune=False,
+                              additional_packages=[], skip_packages=[],
+                              return_cmds=False, append_cmds=None,
+                              temp_file=None):
     r"""Install packages via pip or conda from one or more pip-style
     requirements file(s).
 
@@ -100,43 +202,113 @@ def install_from_requirements(method, fname_in, conda_env=None,
             should be installed into. Defaults to None and is ignored.
         user (bool, optional): If True, install in user mode. Defaults to
             False.
+        unique_to_method (bool, optional): If True, only those packages that
+            can only be installed via the specified method will be installed.
+        install_opts (dict, optional): Mapping from language/package to bool
+            specifying whether or not the language/package should be installed.
+            If not provided, get_install_opts is used to create it.
+        python_cmd (str, optional): Python executable that should be used to
+            call pip. Defaults to None and will be determined from conda_env if
+            provided. Otherwise the current executable will be used.
+        verbose (bool, optional): If True, setup steps are run with verbosity
+            turned up. Defaults to False.
+        verbose_prune (bool, optional): If True, additional information will
+            be printed when determine the list of requirements that should be
+            installed. Defaults to False. If verbose is True, verbose_prune
+            will be set to True as well.
+        additional_packages (list, optional): Additional packages that should
+            be installed. Defaults to empty list.
+        skip_packages (list, optional): A list of packages that should not
+            be added to the pruned list. Defaults to an empty list.
+        return_cmds (bool, optional): If True, the necessary commands will be
+            returned. Defaults to False.
+        append_cmds (list, optional): List that commands should be appended to.
+            Defaults to None and is ignored. If provided, the temporary file
+            is returned. This keyword will be ignored if return_cmds is True.
+        temp_file (str, optional): File where pruned requirements list should
+            be stored. Defaults to None and one will be created.
 
     """
-    if method not in ['pip', 'conda']:
-        raise ValueError("Invalid method: '%s'" % method)
-    elif method == 'pip':
-        excl_suffix = '# conda'
+    if verbose:
+        verbose_prune = True
+    return_temp = (return_cmds or isinstance(append_cmds, list))
+    install_opts = get_install_opts(install_opts)
+    if python_cmd is None:
+        python_cmd = PYTHON_CMD
+    if conda_env:
+        python_cmd = locate_conda_exe(conda_env, 'python')
+    if method == 'pip':
+        excl_method = 'conda'
     elif method == 'conda':
-        excl_suffix = '# pip'
-    temp_file = prune(fname_in, excl_suffix=excl_suffix)
+        excl_method = 'pip'
+    else:
+        raise ValueError("Invalid method: '%s'" % method)
+    if unique_to_method:
+        incl_method = method
+    else:
+        incl_method = None
+    temp_file = prune(fname_in, fname_out=temp_file,
+                      excl_method=excl_method, incl_method=incl_method,
+                      install_opts=install_opts, verbose=verbose_prune,
+                      additional_packages=additional_packages,
+                      skip_packages=skip_packages)
     try:
         if method == 'conda':
-            args = ['conda', 'install', '-y']
+            assert(CONDA_CMD)
+            args = [CONDA_CMD, 'install', '-y']
+            if verbose:
+                args.append('-vvv')
+            else:
+                args.append('-q')
             if conda_env:
                 args += ['--name', conda_env]
             args += ['--file', temp_file]
             if user:
                 args.append('--user')
+            args.append('--update-all')
         elif method == 'pip':
-            if conda_env:
-                pip_cmd = locate_conda_exe(conda_env, 'pip')
-            else:
-                pip_cmd = 'pip'
-            args = [pip_cmd, 'install', '-r', temp_file]
+            assert(python_cmd)
+            args = [python_cmd, '-m', 'pip', 'install']
+            if verbose:
+                args.append('--verbose')
+            args += ['-r', temp_file]
             if user:
                 args.append('--user')
-        print(call_conda_command(args))
+        if return_temp:
+            if return_cmds:
+                cmd_list = []
+            else:
+                cmd_list = append_cmds
+            if os.path.isfile(temp_file):
+                cmd_list += [' '.join(args)]
+                if _is_win:
+                    cmd_list.append(
+                        ('%s -c \'exec(\"import os;if os.path.isfile'
+                         '(\\"%s\\"): os.remove(\\"%s\\")\")\'')
+                        % (python_cmd, temp_file, temp_file))
+                else:
+                    cmd_list.append(
+                        ('%s -c \'import os\nif os.path.isfile'
+                         '(\"%s\"): os.remove(\"%s\")\'')
+                        % (python_cmd, temp_file, temp_file))
+        if return_cmds:
+            return cmd_list
+        if isinstance(append_cmds, list):
+            return temp_file
+        if os.path.isfile(temp_file):
+            print(call_conda_command(args))
     except BaseException:
         if os.path.isfile(temp_file):
             with open(temp_file, 'r') as fd:
                 print(fd.read())
         raise
     finally:
-        if os.path.isfile(temp_file):
+        if os.path.isfile(temp_file) and (not return_temp):
             os.remove(temp_file)
 
 
 if __name__ == "__main__":
+    install_opts = get_install_opts()
     parser = argparse.ArgumentParser(
         "Install dependencies via pip or conda from one or more "
         "pip-style requirements files.")
@@ -150,6 +322,31 @@ def install_from_requirements(method, fname_in, conda_env=None,
                               'installed into.'))
     parser.add_argument('--user', action='store_true',
                         help=('Install in user mode.'))
+    parser.add_argument('--unique-to-method', action='store_true',
+                        help=('Only install packages that are unique to the specified '
+                              'installation method.'))
+    parser.add_argument('--verbose', action='store_true',
+                        help="Turn up verbosity of output.")
+    parser.add_argument('--additional-packages', nargs='+',
+                        help="Additional packages that should be installed.")
+    for k, v in install_opts.items():
+        if v:
+            parser.add_argument(
+                '--dont-install-%s' % k, action='store_true',
+                help=("Don't install %s" % k))
+        else:
+            parser.add_argument(
+                '--install-%s' % k, action='store_true',
+                help=("Install %s" % k))
     args = parser.parse_args()
+    new_opts = {}
+    for k, v in install_opts.items():
+        if v and getattr(args, 'dont_install_%s' % k, False):
+            new_opts[k] = False
+        elif (not v) and getattr(args, 'install_%s' % k, False):
+            new_opts[k] = True
+    install_opts.update(new_opts)
     install_from_requirements(args.method, args.files, conda_env=args.conda_env,
-                              user=args.user)
+                              user=args.user, unique_to_method=args.unique_to_method,
+                              install_opts=install_opts, verbose=args.verbose,
+                              additional_packages=args.additional_packages)
diff --git a/utils/setup_old_mac_sdk.sh b/utils/setup_old_mac_sdk.sh
new file mode 100755
index 000000000..83c067c30
--- /dev/null
+++ b/utils/setup_old_mac_sdk.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+conda info -a
+# Install older Mac SDK so that conda llvm (7) can be used
+# https://github.com/conda-forge/mpi-feedstock/issues/4
+export MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET:-10.9}
+export CONDA_BUILD_SYSROOT="$(xcode-select -p)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX${MACOSX_DEPLOYMENT_TARGET}.sdk"
+export SDKROOT=${CONDA_BUILD_SYSROOT}
+echo "MACOSX_DEPLOYMENT_TARGET = ${MACOSX_DEPLOYMENT_TARGET}"
+echo "CONDA_BUILD_SYSROOT = ${CONDA_BUILD_SYSROOT}"
+echo "SDKROOT = ${SDKROOT}"
+	
+if [[ ! -d ${CONDA_BUILD_SYSROOT} || "$OSX_FORCE_SDK_DOWNLOAD" == "1" ]]; then
+    echo "Downloading ${MACOSX_DEPLOYMENT_TARGET} sdk to ${CONDA_BUILD_SYSROOT}"
+    curl -L -O https://github.com/phracker/MacOSX-SDKs/releases/download/10.13/MacOSX${MACOSX_DEPLOYMENT_TARGET}.sdk.tar.xz
+    tar -xf MacOSX${MACOSX_DEPLOYMENT_TARGET}.sdk.tar.xz -C "$(dirname "$CONDA_BUILD_SYSROOT")"
+    ls "$(dirname "$CONDA_BUILD_SYSROOT")"
+    # set minimum sdk version to our target
+    plutil -replace MinimumSDKVersion -string ${MACOSX_DEPLOYMENT_TARGET} $(xcode-select -p)/Platforms/MacOSX.platform/Info.plist
+    plutil -replace DTSDKName -string macosx${MACOSX_DEPLOYMENT_TARGET}internal $(xcode-select -p)/Platforms/MacOSX.platform/Info.plist
+    printf "CONDA_BUILD_SYSROOT:\n  - ${CONDA_BUILD_SYSROOT}  # [osx]\n" > ~/conda_build_config.yaml
+fi
+
+# Propagate environment variables to subsequent steps
+if [[ -n $GITHUB_ACTIONS ]]; then
+    echo -n "MACOSX_DEPLOYMENT_TARGET=" >> $GITHUB_ENV
+    echo $MACOSX_DEPLOYMENT_TARGET >> $GITHUB_ENV
+    echo -n "CONDA_BUILD_SYSROOT=" >> $GITHUB_ENV
+    echo $CONDA_BUILD_SYSROOT >> $GITHUB_ENV
+    echo -n "SDKROOT=" >> $GITHUB_ENV
+    echo $SDKROOT >> $GITHUB_ENV
+fi
diff --git a/utils/setup_rabbitmq.sh b/utils/setup_rabbitmq.sh
new file mode 100755
index 000000000..ad096caef
--- /dev/null
+++ b/utils/setup_rabbitmq.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+echo "Starting RabbitMQ..."
+export PATH="${PATH}:/usr/local/sbin"
+sudo /bin/sh -c "RABBITMQ_PID_FILE=$TRAVIS_BUILD_DIR/rabbitmq.pid rabbitmq-server &"
+sudo rabbitmqctl wait "$TRAVIS_BUILD_DIR/rabbitmq.pid"
+echo "Checking rabbitmq status..."
+sudo rabbitmqctl status
diff --git a/yggdrasil/.ygg_schema.yml b/yggdrasil/.ygg_schema.yml
index a0ad60383..b01031a2d 100644
--- a/yggdrasil/.ygg_schema.yml
+++ b/yggdrasil/.ygg_schema.yml
@@ -125,8 +125,8 @@ definitions:
             type: string
           type: array
       required:
-      - datatype
       - name
+      - datatype
       - commtype
       title: comm_base
       type: object
@@ -629,9 +629,9 @@ definitions:
             is used.
           type: string
       required:
-      - filetype
-      - name
       - working_dir
+      - name
+      - filetype
       title: file_base
       type: object
     - anyOf:
@@ -1249,6 +1249,13 @@ definitions:
         driver:
           description: '[DEPRECATED] Name of driver class that should be used.'
           type: string
+        env:
+          additional_properties:
+            type: string
+          default: {}
+          description: Dictionary of environment variables that should be set when
+            the driver starts. Defaults to {}.
+          type: object
         env_compiler:
           default: CC
           description: Environment variable where the compiler executable should be
@@ -1278,8 +1285,7 @@ definitions:
             model must contain it's own calls to the |yggdrasil| interface.
           type: string
         inputs:
-          default:
-          - name: default
+          default: []
           description: A mapping object containing the entry for a model input channel
             or a list of input channel entries. If the model does not get input from
             another model, this may be ommitted. A full description of channel entries
@@ -1367,6 +1373,7 @@ definitions:
           - c++
           - cmake
           - cpp
+          - cxx
           - executable
           - fortran
           - lpy
@@ -1417,8 +1424,7 @@ definitions:
           description: If True, only the final timestep is output. Defaults to False.
           type: boolean
         outputs:
-          default:
-          - name: default
+          default: []
           description: A mapping object containing the entry for a model output channel
             or a list of output channel entries. If the model does not output to another
             model, this may be ommitted. A full description of channel entries and
@@ -1671,10 +1677,10 @@ definitions:
             is used.
           type: string
       required:
-      - working_dir
+      - language
       - name
       - args
-      - language
+      - working_dir
       title: model_base
       type: object
     - anyOf:
@@ -1846,85 +1852,6 @@ definitions:
             type: string
         title: ExecutableModelDriver
         type: object
-      - additionalProperties: true
-        description: Schema for model component ['osr'] subtype.
-        properties:
-          copy_xml_to_osr:
-            default: false
-            description: If True, the XML file(s) will be copied to the OSR repository
-              InputFiles direcitory before running. This is necessary if the XML file(s)
-              use any of the files located there since OSR always assumes the included
-              file paths are relative. Defaults to False.
-            type: boolean
-          language:
-            default: executable
-            description: Model is an OSR model.
-            enum:
-            - osr
-            type: string
-          sync_vars_in:
-            default: []
-            description: Variables that should be synchronized from other models.
-              Defaults to [].
-            items:
-              type: string
-            type: array
-          sync_vars_out:
-            default: []
-            description: Variables that should be synchronized to other models. Defaults
-              to [].
-            items:
-              type: string
-            type: array
-          update_interval:
-            additionalProperties:
-              type: float
-            default:
-              timesync: 1.0
-            description: Max simulation interval at which synchronization should occur
-              (in days). Defaults to 1.0 if not provided. If the XML input file loads
-              additional export modules that output at a shorter rate, the existing
-              table of values will be extrapolated.
-            type: object
-        title: OSRModelDriver
-        type: object
-      - additionalProperties: true
-        description: Schema for model component ['matlab'] subtype.
-        properties:
-          interpreter:
-            description: Name or path of interpreter executable that should be used
-              to run the model. If not provided, the interpreter will be determined
-              based on configuration options for the language (if present) and the
-              default_interpreter class attribute.
-            type: string
-          interpreter_flags:
-            default: []
-            description: Flags that should be passed to the interpreter when running
-              the model. If not provided, the flags are determined based on configuration
-              options for the language (if present) and the default_interpreter_flags
-              class attribute.
-            items:
-              type: string
-            type: array
-          language:
-            default: executable
-            description: Model is written in Matlab.
-            enum:
-            - matlab
-            type: string
-          skip_interpreter:
-            default: false
-            description: If True, no interpreter will be added to the arguments. This
-              should only be used for subclasses that will not be invoking the model
-              via the command line. Defaults to False.
-            type: boolean
-          use_symunit:
-            default: false
-            description: If True, input/output variables with units will be represented
-              in Matlab using symunit. Defaults to False.
-            type: boolean
-        title: MatlabModelDriver
-        type: object
       - additionalProperties: true
         description: Schema for model component ['c'] subtype.
         properties:
@@ -1975,7 +1902,7 @@ definitions:
         title: CModelDriver
         type: object
       - additionalProperties: true
-        description: Schema for model component ['c++', 'cpp'] subtype.
+        description: Schema for model component ['c++', 'cpp', 'cxx'] subtype.
         properties:
           compiler:
             description: Command or path to executable that should be used to compile
@@ -1998,6 +1925,7 @@ definitions:
             enum:
             - c++
             - cpp
+            - cxx
             type: string
           linker:
             description: Command or path to executable that should be used to link
@@ -2024,6 +1952,85 @@ definitions:
             type: array
         title: CPPModelDriver
         type: object
+      - additionalProperties: true
+        description: Schema for model component ['osr'] subtype.
+        properties:
+          copy_xml_to_osr:
+            default: false
+            description: If True, the XML file(s) will be copied to the OSR repository
+              InputFiles direcitory before running. This is necessary if the XML file(s)
+              use any of the files located there since OSR always assumes the included
+              file paths are relative. Defaults to False.
+            type: boolean
+          language:
+            default: executable
+            description: Model is an OSR model.
+            enum:
+            - osr
+            type: string
+          sync_vars_in:
+            default: []
+            description: Variables that should be synchronized from other models.
+              Defaults to [].
+            items:
+              type: string
+            type: array
+          sync_vars_out:
+            default: []
+            description: Variables that should be synchronized to other models. Defaults
+              to [].
+            items:
+              type: string
+            type: array
+          update_interval:
+            additionalProperties:
+              type: float
+            default:
+              timesync: 1.0
+            description: Max simulation interval at which synchronization should occur
+              (in days). Defaults to 1.0 if not provided. If the XML input file loads
+              additional export modules that output at a shorter rate, the existing
+              table of values will be extrapolated.
+            type: object
+        title: OSRModelDriver
+        type: object
+      - additionalProperties: true
+        description: Schema for model component ['matlab'] subtype.
+        properties:
+          interpreter:
+            description: Name or path of interpreter executable that should be used
+              to run the model. If not provided, the interpreter will be determined
+              based on configuration options for the language (if present) and the
+              default_interpreter class attribute.
+            type: string
+          interpreter_flags:
+            default: []
+            description: Flags that should be passed to the interpreter when running
+              the model. If not provided, the flags are determined based on configuration
+              options for the language (if present) and the default_interpreter_flags
+              class attribute.
+            items:
+              type: string
+            type: array
+          language:
+            default: executable
+            description: Model is written in Matlab.
+            enum:
+            - matlab
+            type: string
+          skip_interpreter:
+            default: false
+            description: If True, no interpreter will be added to the arguments. This
+              should only be used for subclasses that will not be invoking the model
+              via the command line. Defaults to False.
+            type: boolean
+          use_symunit:
+            default: false
+            description: If True, input/output variables with units will be represented
+              in Matlab using symunit. Defaults to False.
+            type: boolean
+        title: MatlabModelDriver
+        type: object
       - additionalProperties: true
         description: Schema for model component ['R', 'r'] subtype.
         properties:
diff --git a/yggdrasil/__init__.py b/yggdrasil/__init__.py
index 3fe651ab0..a9fd8100b 100644
--- a/yggdrasil/__init__.py
+++ b/yggdrasil/__init__.py
@@ -1,31 +1,13 @@
 r"""This package provides a framework for integrating models across languages
 such that they can be run simultaneously, passing input back and forth."""
-from yggdrasil import platform
 import os
 import sys
-import glob
 import shutil
 import logging
-import argparse
-import subprocess
-import importlib
 from ._version import get_versions
-from yggdrasil import config
-_test_package_name = None
-_test_package = None
+from yggdrasil import platform, config
 logging.basicConfig()
 logger = logging.getLogger(__name__)
-order = ['pytest', 'nose']
-try:
-    _test_package_name = order[0]
-    _test_package = importlib.import_module(_test_package_name)
-except ImportError:  # pragma: debug
-    try:
-        _test_package_name = order[1]
-        _test_package = importlib.import_module(_test_package_name)
-    except ImportError:
-        _test_package_name = None
-        _test_package = None
     
 
 if platform._is_win:  # pragma: windows
@@ -34,287 +16,14 @@
     os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = 'T'
 
 
-if not os.path.isfile(config.usr_config_file):  # pragma: no cover
+if not os.path.isfile(config.usr_config_file):  # pragma: config
     from yggdrasil.languages import install_languages
     shutil.copy(config.def_config_file, config.usr_config_file)
     install_languages.install_all_languages(from_setup=True)
-    config.update_language_config()
-    with open(config.usr_config_file, 'r') as fd:
-        print(fd.read())
-
-
-def expand_and_add(path, path_list, dir_list):  # pragma: no cover
-    r"""Expand the specified path and add it's expanded forms to the provided
-    list.
-
-    Args:
-        path (str): Absolute/relative path with or without regex wildcard
-            expressions to expand.
-        path_list (list): Existing list that expansions should be added to.
-        dir_list (list): Directories that should be tried for relative paths.
-
-    Returns:
-        int: The number of expansions added to path_list.
-
-    """
-    if os.path.isabs(path):
-        matches = sorted(glob.glob(path))
-        path_list += matches
-        return len(matches)
-    # Try checking for function
-    for func_sep in ['::', ':']:
-        if (func_sep in path):
-            prepath, mod = path.rsplit(func_sep, 1)
-            nadded = expand_and_add(prepath, path_list, dir_list)
-            if nadded:
-                for i in range(-nadded, 0):
-                    path_list[i] += '%s%s' % (func_sep, mod)
-                return nadded
-    # Try directory prefixes
-    for path_prefix in dir_list:
-        nadded = expand_and_add(os.path.join(path_prefix, path),
-                                path_list, dir_list)
-        if nadded:
-            return nadded
-    return 0
-
-
-def run_tsts(**kwargs):  # pragma: no cover
-    r"""Run tests for the package. Relative paths are interpreted to be
-    relative to the package root directory.
-
-    Args:
-        verbose (bool, optional): If True, set option '-v' which
-            increases the verbosity. Defaults to True.
-        nocapture (bool, optional): If True, set option '--nocapture'
-            ('--capture=no' with pytest) which allows messages to be printed to
-            stdout. Defaults to True.
-        stop (bool, optional): If True, set option '--stop' ('--exitfirst' for
-            pytest) which stops tests at the first failure. Defaults to True.
-        nologcapture (bool, optional): If True, set option '--nologcapture'
-            which allows logged messages to be printed. Defaults to True.
-        withcoverage (bool, optional): If True, set option '--with-coverage'
-            which invokes coverage. Defaults to True.
-
-    """
-    if '-h' not in sys.argv:
-        if _test_package is None:
-            raise RuntimeError("Could not locate test runner pytest or nose.")
-        elif _test_package_name == 'pytest':
-            test_cmd = 'pytest'
-        elif _test_package_name == 'nose':
-            test_cmd = 'nosetests'
-        else:
-            raise RuntimeError("Unsupported test package: '%s'"
-                               % _test_package_name)
-    parser = argparse.ArgumentParser(description='Run yggdrasil tests.')
-    arguments = [
-        (['withcoverage', 'with-coverage'], ['nocover', 'no-cover'],
-         True, {'help': 'Record coverage during tests.'}),
-        (['verbose', 'v'], ['quiet'],
-         True, {'help': ('Increase verbosity of output from '
-                         'the test runner.')}),
-        (['nocapture', 's'], ['capture'],
-         True, {'help': 'Don\'t capture output from tests.'}),
-        (['stop', 'x'], ['dontstop', 'dont-stop'],
-         True, {'help': 'Stop after first test failure.'}),
-        (['nologcapture'], ['logcapture'],
-         True, {'help': ('Don\'t capture output from log '
-                         'messages generated during tests.')}),
-        (['noflaky', 'no-flaky'], ['flaky'],
-         False, {'help': 'Don\'t re-run flaky tests.'}),
-    ]
-    for pos_dest, neg_dest, default, kws in arguments:
-        dest = pos_dest[0]
-        for x in [pos_dest, neg_dest]:
-            for i, y in enumerate(x):
-                if len(y) == 1:
-                    x[i] = '-' + y
-                else:
-                    x[i] = '--' + y
-        if kwargs.get(dest, default):
-            if kws['help'].startswith('Don\'t'):
-                kws['help'].split('Don\'t', 1)[-1]
-                kws['help'] = kws['help'].replace(
-                    kws['help'][0], kws['help'][0].upper(), 1)
-            else:
-                kws['help'] = kws['help'].replace(
-                    kws['help'][0], kws['help'][0].lower(), 1)
-                kws['help'] = 'Don\'t ' + kws['help']
-            parser.add_argument(*neg_dest, action='store_false',
-                                dest=dest, **kws)
-        else:
-            parser.add_argument(*pos_dest, action='store_true',
-                                dest=dest, **kws)
-    parser = config.get_config_parser(parser)
-    parser.add_argument('--ci', action='store_true',
-                        help=('Perform addition operations required '
-                              'for testing on continuous integration '
-                              'services.'))
-    suite_args = ('--test-suite', '--test-suites')
-    suite_kws = dict(nargs='+', action="extend", type=str,
-                     choices=['examples', 'examples_part1',
-                              'examples_part2', 'demos', 'types', 'timing'],
-                     help='Test suite(s) that should be run.',
-                     dest='test_suites')
-    try:
-        parser.add_argument(*suite_args, **suite_kws)
-    except ValueError:
-        # 'extend' introduced in 3.8
-        suite_kws['action'] = 'append'
-        suite_kws.pop('nargs')
-        parser.add_argument(*suite_args, **suite_kws)
-    args, extra_argv = parser.parse_known_args()
-    initial_dir = os.getcwd()
-    package_dir = os.path.dirname(os.path.abspath(__file__))
-    error_code = 0
-    # Peform ci tests/operations
-    # Call bash script?
-    if args.ci:
-        extra_argv += ['-c', 'setup.cfg', '--cov-config=.coveragerc']
-    # Separate out paths from options
-    argv = [test_cmd]
-    test_paths = []
-    opt_val = 0
-    for x in extra_argv:
-        if opt_val > 0:
-            argv.append(x)
-            opt_val -= 1
-        elif x.endswith('yggtest'):
-            pass
-        elif x.startswith('-'):
-            argv.append(x)
-            if (_test_package_name == 'pytest') and (x in ['-c']):
-                opt_val = 1
-        else:
-            test_paths.append(x)
-    if args.test_suites:
-        for x in args.test_suites:
-            if x == 'examples':
-                args.enable_examples = True
-                test_paths.append('examples')
-            elif x == 'examples_part1':
-                args.enable_examples = True
-                if platform._is_win:  # pragma: windows
-                    pattern = 'test_[a-g]*.py'
-                else:
-                    pattern = 'test_[A-Za-g]*.py'
-                test_paths.append(os.path.join(
-                    'examples', 'tests', pattern))
-            elif x == 'examples_part2':
-                args.enable_examples = True
-                pattern = 'test_[h-z]*.py'
-                # if platform._is_win:  # pragma: windows
-                #     pattern = 'test_[g-z]*.py'
-                test_paths.append(os.path.join(
-                    'examples', 'tests', pattern))
-            # elif x.startswith('examples_'):
-            #     args.enable_examples = True
-            #     test_paths.append(os.path.join(
-            #         'examples', 'tests',
-            #         'test_%s*.py'.format(x.split('examples_')[-1])))
-            elif x == 'demos':
-                args.enable_demos = True
-                test_paths.append('demos')
-            elif x == 'types':
-                args.enable_examples = True
-                args.long_running = True
-                test_paths.append(os.path.join('examples', 'tests',
-                                               'test_types.py'))
-            elif x == 'timing':
-                args.long_running = True
-                args.enable_production_runs = True
-                test_paths.append(os.path.join('tests', 'test_timing.py'))
-    args = config.resolve_config_parser(args)
-    if _test_package_name == 'nose':
-        argv += ['--detailed-errors', '--exe']
-    if args.verbose:
-        argv.append('-v')
-    if args.nocapture:
-        argv.append('-s')
-    if args.stop:
-        argv.append('-x')
-    if args.nologcapture and (_test_package_name == 'nose'):
-        argv.append('--nologcapture')
-    if args.withcoverage:
-        if _test_package_name == 'nose':
-            argv.append('--with-coverage')
-            argv.append('--cover-package=yggdrasil')
-        elif _test_package_name == 'pytest':
-            # See information about getting coverage of test fixtures
-            # https://pytest-cov.readthedocs.io/en/stable/plugins.html
-            argv.append('--cov=%s' % package_dir)
-            # argv.append('--cov-append')
-    if args.noflaky:
-        if _test_package_name == 'pytest':
-            argv += ['-p', 'no:flaky']
-    else:
-        if _test_package_name == 'nose':
-            argv.append('--with-flaky')
-    # Get expanded tests to allow for paths that are relative to either the
-    # yggdrasil root directory or the current working directory
-    expanded_test_paths = []
-    if not test_paths:
-        expanded_test_paths.append(package_dir)
-    else:
-        for x in test_paths:
-            if not expand_and_add(x, expanded_test_paths,
-                                  [package_dir, os.getcwd()]):
-                expanded_test_paths.append(x)
-    argv += expanded_test_paths
-    # Run test command and perform cleanup before logging any errors
-    logger.info("Running %s from %s", argv, os.getcwd())
-    new_config = {}
-    pth_file = 'ygg_coverage.pth'
-    assert(not os.path.isfile(pth_file))
-    # Get arguments for temporary test environment
-    if args.withcoverage:
-        new_config['COVERAGE_PROCESS_START'] = 'True'
-        # if _test_package_name == 'pytest':
-        #     # See information about getting coverage of test fixtures
-        #     # https://pytest-cov.readthedocs.io/en/stable/plugins.html
-        #     new_config['COV_CORE_SOURCE'] = package_dir
-        #     new_config['COV_CORE_CONFIG'] = '.coveragerc'
-        #     new_config['COV_CORE_DATAFILE'] = '.coverage.eager'
-        with open(pth_file, 'w') as fd:
-            fd.write("import coverage; coverage.process_startup()")
-    with config.parser_config(args, **new_config):
-        try:
-            # Perform CI specific pretest operations
-            if args.ci:
-                top_dir = os.path.dirname(os.getcwd())
-                src_cmd = ('python -c \"import versioneer; '
-                           'print(versioneer.get_version())\"')
-                dst_cmd = ('python -c \"import yggdrasil; '
-                           'print(yggdrasil.__version__)\"')
-                src_ver = subprocess.check_output(src_cmd, shell=True)
-                dst_ver = subprocess.check_output(dst_cmd, shell=True,
-                                                  cwd=top_dir)
-                if src_ver != dst_ver:  # pragma: debug
-                    raise RuntimeError(("Versions do not match:\n"
-                                        "\tSource version: %s\n"
-                                        "\tBuild  version: %s\n")
-                                       % (src_ver, dst_ver))
-                if os.environ.get("INSTALLR", None) == "1":
-                    print(shutil.which("R"))
-                    print(shutil.which("Rscript"))
-                subprocess.check_call(["flake8", "yggdrasil"])
-                if os.environ.get("YGG_CONDA", None):
-                    subprocess.check_call(["python", "create_coveragerc.py"])
-                if not os.path.isfile(".coveragerc"):
-                    raise RuntimeError(".coveragerc file dosn't exist.")
-                with open(".coveragerc", "r") as fd:
-                    print(fd.read())
-                subprocess.check_call(["ygginfo", "--verbose"])
-            error_code = subprocess.call(argv)
-        except BaseException:
-            logger.exception('Error in running test.')
-            error_code = -1
-        finally:
-            os.chdir(initial_dir)
-            if os.path.isfile(pth_file):
-                os.remove(pth_file)
-    return error_code
+    if not any([x.endswith(('yggconfig', 'yggconfig.exe', 'config'))
+                for x in sys.argv]):
+        # Don't configure if that is what is going to happen anyway
+        config.update_language_config(verbose=True)
 
 
 __all__ = []
diff --git a/yggdrasil/__main__.py b/yggdrasil/__main__.py
new file mode 100644
index 000000000..c3ad8093c
--- /dev/null
+++ b/yggdrasil/__main__.py
@@ -0,0 +1,3 @@
+from yggdrasil.command_line import main
+
+main()
diff --git a/yggdrasil/command_line.py b/yggdrasil/command_line.py
index 9d4dff20e..0f4df356a 100755
--- a/yggdrasil/command_line.py
+++ b/yggdrasil/command_line.py
@@ -7,484 +7,1429 @@
 import argparse
 import pprint
 import shutil
+from yggdrasil.constants import LANGUAGES, LANGUAGES_WITH_ALIASES
 
 
 logger = logging.getLogger(__name__)
+package_dir = os.path.dirname(os.path.abspath(__file__))
 
 
-def ygginfo():
-    r"""Print information about yggdrasil installation."""
-    from yggdrasil import __version__, tools, config, platform
-    from yggdrasil.components import import_component
-    lang_list = tools.get_installed_lang()
-    prefix = '    '
-    curr_prefix = ''
-    vardict = [
-        ('Location', os.path.dirname(__file__)),
-        ('Version', __version__),
-        ('Languages', ', '.join(lang_list)),
-        ('Communication Mechanisms', ', '.join(tools.get_installed_comm())),
-        ('Default Comm Mechanism', tools.get_default_comm()),
-        ('Config File', config.usr_config_file)]
-    parser = argparse.ArgumentParser(
-        description='Display information about the current yggdrasil installation.')
-    parser.add_argument('--no-languages', action='store_true',
-                        dest='no_languages',
-                        help='Don\'t print information about individual languages.')
-    parser.add_argument('--verbose', action='store_true',
-                        help='Increase the verbosity of the printed information.')
-    args = parser.parse_args()
+def githook():
+    r"""Git hook to determine if the Github workflow need to be
+    re-generated."""
     try:
-        # Add language information
-        if not args.no_languages:
-            # Install languages
-            vardict.append(('Installed Languages:', ''))
-            curr_prefix += prefix
-            for lang in sorted(lang_list):
-                drv = import_component('model', lang)
-                vardict.append((curr_prefix + '%s:' % lang.upper(), ''))
-                curr_prefix += prefix
-                if lang == 'executable':
-                    vardict.append((curr_prefix + 'Location', ''))
+        files = subprocess.check_output(
+            ["git", "diff-index", "--cached", "--name-only",
+             "--diff-filter=ACMRTUXB", "HEAD"],
+            stderr=subprocess.PIPE).decode('utf-8').splitlines()
+    except subprocess.CalledProcessError:
+        return 1
+    regen = (os.path.join('utils', 'test-install-base.yml') in files)
+    if regen:
+        try:
+            gitdir = subprocess.check_output(
+                ["git", "rev-parse", "--git-dir"],
+                stderr=subprocess.PIPE).decode('utf-8').strip()
+        except subprocess.CalledProcessError:
+            return 1
+        workflow_dir = os.path.join(gitdir, '..', '.github', 'workflows')
+        generate_gha_workflow(args=[], gitdir=gitdir)
+        try:
+            subprocess.run(
+                ['git', 'add',
+                 os.path.join(workflow_dir, 'test-install.yml')],
+                check=True,
+                stderr=subprocess.STDOUT,
+                stdout=subprocess.PIPE)
+        except subprocess.CalledProcessError:
+            return 1
+    return 0
+
+
+class ArgumentTuple(tuple):
+
+    def __new__(self, args, kwargs):
+        return tuple.__new__(ArgumentTuple, (args, kwargs))
+
+
+class ConditionalArgumentTuple(ArgumentTuple):
+
+    def __new__(cls, args, kwargs, conditions=None):
+        out = ArgumentTuple.__new__(ConditionalArgumentTuple,
+                                    args, kwargs)
+        out.conditions = conditions
+        if out.conditions is None:
+            out.conditions = {}
+        return out
+            
+
+class ArgumentBase(object):
+    
+    def __init__(self, arguments=None, conditions=None, **kwargs):
+        self.arguments = arguments
+        if self.arguments is None:
+            self.arguments = []
+        self.conditions = conditions
+        if self.conditions is None:
+            self.conditions = {}
+        self.kwargs = kwargs
+
+
+class ArgumentParser(ArgumentBase):
+    pass
+
+
+class ArgumentGroup(ArgumentBase):
+
+    def __init__(self, exclusive=False, **kwargs):
+        self.exclusive = exclusive
+        super(ArgumentGroup, self).__init__(**kwargs)
+
+
+class ArgumentSubparser(ArgumentBase):
+
+    def __init__(self, parsers=None, **kwargs):
+        self.parsers = parsers
+        if self.parsers is None:
+            self.parsers = []
+        super(ArgumentSubparser, self).__init__(**kwargs)
+
+
+class SubCommandMeta(type):
+    r"""Meta class for subcommands."""
+
+    def __call__(cls, *args, **kwargs):
+        return cls.call(*args, **kwargs)
+
+
+def ReplacementWarning(old, new):
+    import warnings
+    warnings.warn(("'%s' will soon be removed. Use '%s' instead.")
+                  % (old, new), FutureWarning)
+
+
+class SubCommand(metaclass=SubCommandMeta):
+    r"""Class for handling subcommands so that they can be run
+    as subcommands or individually."""
+
+    name = None
+    help = None
+    arguments = []
+    allow_unknown = False
+
+    @classmethod
+    def parse_args(cls, parser, args=None, allow_unknown=False):
+        # TODO: Check choices for positional arguments that can
+        # have more than one element
+        if isinstance(args, argparse.Namespace):
+            return args
+        if cls.allow_unknown or allow_unknown:
+            args, extra = parser.parse_known_args(args=args)
+            args._extra_commands = extra
+        else:
+            args = parser.parse_args(args=args)
+        for k in ['language', 'languages']:
+            v = getattr(args, k, None)
+            if isinstance(v, list):
+                v_flag = getattr(args, k + '_flag', None)
+                if isinstance(v_flag, list):
+                    v.extend(v_flag)
+                if (len(v) == 0) or ('all' in v):
+                    setattr(args, k, LANGUAGES['all'])
+                    args.all_languages = True
+        return args
+
+    @classmethod
+    def func(cls, args):  # pragma: debug
+        raise NotImplementedError
+
+    @classmethod
+    def call(cls, args=None, **kwargs):
+        parser = cls.get_parser(args=args)
+        args = cls.parse_args(parser, args=args)
+        return cls.func(args, **kwargs)
+
+    @classmethod
+    def get_parser(cls, args=None):
+        parser = argparse.ArgumentParser(cls.help)
+        cls.add_arguments(parser, args=args)
+        return parser
+
+    @classmethod
+    def add_argument_to_parser(cls, parser, x):
+        if hasattr(x, 'conditions') and x.conditions:
+            for k, v in x.conditions.items():
+                if k == 'os':
+                    from yggdrasil import platform
+                    if platform._platform not in v:
+                        return
+                else:  # pragma: debug
+                    raise NotImplementedError(k)
+        if isinstance(x, list):
+            for xx in x:
+                cls.add_argument_to_parser(parser, xx)
+        elif isinstance(x, (tuple, ArgumentTuple,
+                            ConditionalArgumentTuple)):
+            assert(len(x) == 2)
+            args, kwargs = x[:]
+            try:
+                parser.add_argument(*args, **kwargs)
+            except ValueError:
+                if kwargs.get('action', None) == 'extend':
+                    kwargs['action'] = 'append'
+                    kwargs.pop('nargs', None)
+                    parser.add_argument(*args, **kwargs)
                 else:
-                    exec_name = drv.language_executable()
-                    if not os.path.isabs(exec_name):
-                        exec_name = shutil.which(exec_name)
-                    vardict.append((curr_prefix + 'Location', exec_name))
-                vardict.append((curr_prefix + 'Version',
-                                drv.language_version()))
+                    raise
+        elif isinstance(x, ArgumentGroup):
+            if x.exclusive:
+                group = parser.add_mutually_exclusive_group(**x.kwargs)
+            else:
+                group = parser.add_argument_group(**x.kwargs)
+            cls.add_argument_to_parser(group, x.arguments)
+        elif isinstance(x, ArgumentSubparser):
+            subparsers = parser.add_subparsers(**x.kwargs)
+            for xx in x.parsers:
+                isubparser = subparsers.add_parser(**xx.kwargs)
+                cls.add_argument_to_parser(isubparser, x.arguments)
+                cls.add_argument_to_parser(isubparser, xx.arguments)
+        else:
+            raise NotImplementedError("type(x) = %s" % type(x))
+
+    @classmethod
+    def add_arguments(cls, parser, args=None):
+        cls.add_argument_to_parser(parser, cls.arguments)
+
+    @classmethod
+    def add_subparser(cls, subparsers, args=None):
+        parser = subparsers.add_parser(cls.name, help=cls.help)
+        cls.add_arguments(parser, args=args)
+        parser.set_defaults(func=cls.func)
+
+
+class main(SubCommand):
+    r"""Runner for yggdrasil CLI."""
+
+    name = "yggdrasil"
+    help = (
+        "Command line interface for the yggdrasil package.")
+    arguments = []
+
+    @classmethod
+    def get_parser(cls, **kwargs):
+        from yggdrasil import __version__ as ver
+        parser = super(main, cls).get_parser(**kwargs)
+        parser.add_argument('--version', action='version',
+                            version=('yggdrasil %s' % ver))
+        subparsers = parser.add_subparsers(title='subcommands',
+                                           dest='subcommand')
+        parser._ygg_subparsers = {}
+        for x in [yggrun, ygginfo, validate_yaml,
+                  yggcc, yggcompile, yggclean,
+                  ygginstall, update_config,
+                  regen_metaschema, regen_schema,
+                  yggmodelform, yggdevup, run_tsts,
+                  timing_plots, generate_gha_workflow]:
+            x.add_subparser(subparsers, args=kwargs.get('args', None))
+            parser._ygg_subparsers[x.name] = x
+        return parser
+
+    @classmethod
+    def parse_args(cls, parser, args=None, **kwargs):
+        if args is None:
+            args = sys.argv[1:]
+        if isinstance(args, list) and ('test' in args):
+            kwargs['allow_unknown'] = True
+        args = super(main, cls).parse_args(parser, args=args, **kwargs)
+        if args.subcommand:
+            args = parser._ygg_subparsers[args.subcommand].parse_args(
+                parser, args=args, **kwargs)
+        return args
+
+    @classmethod
+    def func(cls, args):
+        args.func(args)
+
+
+class yggrun(SubCommand):
+    r"""Start a run."""
+
+    name = "run"
+    help = "Run an integration."
+    arguments = [
+        (('yamlfile', ),
+         {'nargs': '+',
+          'help': "One or more yaml specification files."})]
+
+    @classmethod
+    def add_arguments(cls, parser, **kwargs):
+        from yggdrasil import config
+        super(yggrun, cls).add_arguments(parser, **kwargs)
+        config.get_config_parser(parser, skip_sections='testing')
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import runner, config
+        prog = sys.argv[0].split(os.path.sep)[-1]
+        with config.parser_config(args):
+            runner.run(args.yamlfile, ygg_debug_prefix=prog,
+                       production_run=args.production_run)
+
+
+class ygginfo(SubCommand):
+    r"""Print information about yggdrasil installation."""
+
+    name = 'info'
+    help = ('Display information about the current yggdrasil '
+            'installation.')
+    arguments = [
+        (('--no-languages', ),
+         {'action': 'store_true', 'dest': 'no_languages',
+          'help': ('Don\'t print information about individual '
+                   'languages.')}),
+        (('--no-comms', ),
+         {'action': 'store_true', 'dest': 'no_comms',
+          'help': ('Don\'t print information about individual '
+                   'comms.')}),
+        (('--verbose', '-v'),
+         {'action': 'store_true',
+          'help': ('Increase the verbosity of the printed '
+                   'information.')}),
+        ArgumentSubparser(
+            title='tool', dest='tool',
+            description='Compilation tool types to get info about.',
+            arguments=[
+                (('language', ),
+                 {'choices': LANGUAGES_WITH_ALIASES['compiled'],
+                  'type': str.lower,
+                  'help': 'Language to get tool information for.'}),
+                (('--toolname', ),
+                 {'default': None,
+                  'help': ('Name of tool to get information for. '
+                           'If not provided, information for the '
+                           'default tool will be returned.')}),
+                (('--flags', ),
+                 {'action': 'store_true',
+                  'help': ('Display the flags that yggdrasil will '
+                           ' pass to the tool when it is called.')})],
+            parsers=[
+                ArgumentParser(
+                    name='compiler',
+                    help='Get information about a compiler.'),
+                ArgumentParser(
+                    name='linker',
+                    help='Get information about a linker.',
+                    arguments=[
+                        (('--library', ),
+                         {'action': 'store_true',
+                          'help': 'Get flags for linking a library.'})]),
+                ArgumentParser(
+                    name='archiver',
+                    help='Get information about a archiver.')])]
+
+    @classmethod
+    def func(cls, args, return_str=False):
+        from yggdrasil import platform
+        from yggdrasil.components import import_component
+        if args.tool:
+            drv = import_component('model', args.language)
+            if args.flags:
+                if args.tool == 'compiler':
+                    flags = drv.get_compiler_flags(
+                        for_model=True, toolname=args.toolname,
+                        dry_run=True)
+                    if flags[-1] == '/link':  # pragma: windows
+                        flags = flags[:-1]
+                else:
+                    if args.tool == 'archiver':
+                        libtype = 'static'
+                    elif getattr(args, 'library', False):
+                        libtype = 'shared'
+                    else:
+                        libtype = 'object'
+                    flags = drv.get_linker_flags(
+                        for_model=True, toolname=args.toolname,
+                        dry_run=True, libtype=libtype)
+                out = ' '.join(flags)
+                if platform._is_win:  # pragma: windows:
+                    out = out.replace('/', '-')
+                    out = out.replace('\\', '/')
+            else:
+                out = drv.get_tool(args.tool, return_prop='name')
+            if return_str:
+                return out
+            print(out)
+            return
+        from yggdrasil import tools, config, __version__
+        lang_list = tools.get_installed_lang()
+        comm_list = tools.get_installed_comm()
+        prefix = '    '
+        curr_prefix = ''
+        vardict = [
+            ('Location', os.path.dirname(__file__)),
+            ('Version', __version__),
+            ('Languages', ', '.join(lang_list)),
+            ('Communication Mechanisms',
+             ', '.join(tools.get_installed_comm())),
+            ('Default Comm Mechanism', tools.get_default_comm()),
+            ('Config File', config.usr_config_file)]
+        try:
+            # Add language information
+            if not args.no_languages:
+                # Install languages
+                vardict.append(('Installed Languages:', ''))
+                curr_prefix += prefix
+                for lang in sorted(lang_list):
+                    drv = import_component('model', lang)
+                    vardict.append(
+                        (curr_prefix + '%s:' % lang.upper(), ''))
+                    curr_prefix += prefix
+                    if lang == 'executable':
+                        vardict.append((curr_prefix + 'Location', ''))
+                    else:
+                        exec_name = drv.language_executable()
+                        if not os.path.isabs(exec_name):
+                            exec_name = shutil.which(exec_name)
+                        vardict.append((curr_prefix + 'Location',
+                                        exec_name))
+                    vardict.append((curr_prefix + 'Version',
+                                    drv.language_version()))
+                    curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
                 curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-            curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-            # Not installed languages
-            vardict.append(("Languages Not Installed:", ''))
-            curr_prefix += prefix
-            for lang in tools.get_supported_lang():
-                if lang in lang_list:
-                    continue
-                drv = import_component('model', lang)
-                vardict.append((curr_prefix + '%s:' % lang.upper(), ''))
+                # Not installed languages
+                vardict.append(("Languages Not Installed:", ''))
                 curr_prefix += prefix
-                vardict.append((curr_prefix + "Language Installed",
-                                drv.is_language_installed()))
-                vardict.append((curr_prefix + "Base Languages Installed",
-                                drv.are_base_languages_installed()))
-                if not drv.are_base_languages_installed():
+                for lang in tools.get_supported_lang():
+                    if lang in lang_list:
+                        continue
+                    drv = import_component('model', lang)
+                    vardict.append(
+                        (curr_prefix + '%s:' % lang.upper(), ''))
+                    curr_prefix += prefix
+                    vardict.append(
+                        (curr_prefix + "Language Installed",
+                         drv.is_language_installed()))
+                    vardict.append(
+                        (curr_prefix + "Base Languages Installed",
+                         drv.are_base_languages_installed()))
+                    missing = []
+                    if not drv.are_base_languages_installed(
+                            missing=missing):
+                        vardict.append(
+                            (curr_prefix
+                             + "Base Languages Not Installed",
+                             missing))
+                    vardict.append(
+                        (curr_prefix + "Dependencies Installed",
+                         drv.are_dependencies_installed()))
+                    if not drv.are_dependencies_installed():
+                        vardict.append(
+                            (curr_prefix
+                             + "Dependencies Not Installed",
+                             [b for b in drv.interface_dependencies if
+                              (not drv.is_library_installed(b))]))
                     vardict.append(
-                        (curr_prefix + "Base Languages Not Installed",
-                         [b for b in drv.base_languages if
-                          (not import_component('model', b).is_installed())]))
-                vardict.append((curr_prefix + "Dependencies Installed",
-                                drv.are_dependencies_installed()))
-                vardict.append((curr_prefix + "Interface Installed",
-                                drv.is_interface_installed()))
-                vardict.append((curr_prefix + "Comm Installed",
-                                drv.is_comm_installed()))
-                vardict.append((curr_prefix + "Configured",
-                                drv.is_configured()))
-                vardict.append((curr_prefix + "Disabled",
-                                drv.is_disabled()))
+                        (curr_prefix + "Interface Installed",
+                         drv.is_interface_installed()))
+                    vardict.append((curr_prefix + "Comm Installed",
+                                    drv.is_comm_installed()))
+                    vardict.append((curr_prefix + "Configured",
+                                    drv.is_configured()))
+                    vardict.append((curr_prefix + "Disabled",
+                                    drv.is_disabled()))
+                    curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
                 curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-            curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-        # Add verbose information
-        if args.verbose:
-            # Conda info
-            if os.environ.get('CONDA_PREFIX', ''):
-                if platform._is_win:  # pragma: windows
-                    out = tools.bytes2str(subprocess.check_output(
-                        'conda info', shell=True)).strip()
-                else:
-                    out = tools.bytes2str(subprocess.check_output(
-                        ['conda', 'info'])).strip()
+            # Add comm information
+            if not args.no_comms:
+                # Fully installed comms
+                vardict.append(
+                    ('Comms Available for All Languages:', ''))
                 curr_prefix += prefix
-                vardict.append((curr_prefix + 'Conda Info:', "\n%s%s"
-                                % (curr_prefix + prefix,
-                                   ("\n" + curr_prefix + prefix).join(
-                                       out.splitlines(False)))))
+                for comm in sorted(comm_list):
+                    cmm = import_component('comm', comm)
+                    vardict.append(
+                        (curr_prefix + '%s' % comm.upper(), ''))
                 curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-            # R and reticulate info
-            Rdrv = import_component("model", "R")
-            if Rdrv.is_installed():
-                env_reticulate = copy.deepcopy(os.environ)
-                env_reticulate['RETICULATE_PYTHON'] = sys.executable
-                # Stack size
-                out = Rdrv.run_executable(["-e", "Cstack_info()"]).strip()
-                vardict.append((curr_prefix + "R Cstack_info:", "\n%s%s"
-                                % (curr_prefix + prefix,
-                                   ("\n" + curr_prefix + prefix).join(
-                                       out.splitlines(False)))))
-                # Compilation tools
-                interp = 'R'.join(Rdrv.get_interpreter().rsplit('Rscript', 1))
-                vardict.append((curr_prefix + "R C Compiler:", ""))
+                # Partially installed comms
+                vardict.append(
+                    ('Comms Available for Some/No Languages:', ''))
                 curr_prefix += prefix
-                for x in ['CC', 'CFLAGS', 'CXX', 'CXXFLAGS']:
-                    out = tools.bytes2str(subprocess.check_output(
-                        [interp, 'CMD', 'config', x])).strip()
-                    vardict.append((curr_prefix + x, "%s"
-                                    % ("\n" + curr_prefix + prefix).join(
-                                        out.splitlines(False))))
+                for comm in tools.get_supported_comm():
+                    if comm in comm_list:
+                        continue
+                    cmm = import_component('comm', comm)
+                    vardict.append(
+                        (curr_prefix + '%s:' % comm.upper(), ''))
+                    curr_prefix += prefix
+                    avail = [cmm.is_installed(language=lang)
+                             for lang in lang_list]
+                    vardict.append(
+                        (curr_prefix + "Available for ",
+                         sorted([lang_list[i].upper()
+                                 for i in range(len(avail))
+                                 if avail[i]])))
+                    vardict.append(
+                        (curr_prefix + "Not Available for ",
+                         sorted([lang_list[i].upper()
+                                 for i in range(len(avail))
+                                 if not avail[i]])))
+                    curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
+                curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
+            # Add verbose information
+            if args.verbose:
+                # Path variables
+                path_vars = ['PATH', 'C_INCLUDE_PATH', 'INCLUDE',
+                             'LIBRARY_PATH', 'LD_LIBRARY_PATH', 'LIB']
+                vardict.append(('Environment paths:', ''))
+                curr_prefix += prefix
+                for k in path_vars:
+                    if os.environ.get(k, ''):
+                        vardict.append(
+                            (curr_prefix + k, '\n%s%s'
+                             % (curr_prefix + prefix,
+                                ("\n" + curr_prefix + prefix).join(
+                                    os.environ[k].split(os.pathsep)))))
+                    else:
+                        vardict.append((curr_prefix + k, ''))
                 curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-                # Session info
-                out = Rdrv.run_executable(["-e", "sessionInfo()"]).strip()
-                vardict.append((curr_prefix + "R sessionInfo:", "\n%s%s"
-                                % (curr_prefix + prefix,
-                                   ("\n" + curr_prefix + prefix).join(
-                                       out.splitlines(False)))))
-                # Reticulate conda_list
+                # Environment variabless
+                env_vars = ['CONDA_PREFIX', 'CONDA', 'SDKROOT', 'CC',
+                            'CXX', 'FC', 'GFORTRAN', 'DISPLAY', 'CL',
+                            '_CL_']
+                if platform._is_win:  # pragma: windows
+                    env_vars += ['VCPKG_ROOT']
+                vardict.append(('Environment variables:', ''))
+                curr_prefix += prefix
+                for k in env_vars:
+                    vardict.append(
+                        (curr_prefix + k, os.environ.get(k, None)))
+                curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
+                # Conda info
                 if os.environ.get('CONDA_PREFIX', ''):
+                    if platform._is_win:  # pragma: windows
+                        out = tools.bytes2str(subprocess.check_output(
+                            'conda info', shell=True)).strip()
+                    else:
+                        out = tools.bytes2str(subprocess.check_output(
+                            ['conda', 'info'])).strip()
+                    curr_prefix += prefix
+                    vardict.append(
+                        (curr_prefix + 'Conda Info:', "\n%s%s"
+                         % (curr_prefix + prefix,
+                            ("\n" + curr_prefix + prefix).join(
+                                out.splitlines(False)))))
+                    curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
+                # Configuration
+                with open(config.usr_config_file, 'r') as fd:
+                    contents = fd.read()
+                vardict.append(
+                    ('Configuration file:', '%s\n\t%s' % (
+                        config.usr_config_file,
+                        '\n\t'.join(contents.splitlines()))))
+                # R and reticulate info
+                Rdrv = import_component("model", "R")
+                if Rdrv.is_installed():
+                    env_reticulate = copy.deepcopy(os.environ)
+                    env_reticulate['RETICULATE_PYTHON'] = sys.executable
+                    # Stack size
                     out = Rdrv.run_executable(
-                        ["-e", ("library(reticulate); "
-                                "reticulate::conda_list()")],
-                        env=env_reticulate).strip()
-                    vardict.append((curr_prefix + "R reticulate::conda_list():",
-                                    "\n%s%s" % (curr_prefix + prefix,
-                                                ("\n" + curr_prefix + prefix).join(
-                                                    out.splitlines(False)))))
-                # Windows python versions
-                if platform._is_win:  # pragma: windows
+                        ["-e", "Cstack_info()"]).strip()
+                    vardict.append(
+                        (curr_prefix + "R Cstack_info:", "\n%s%s"
+                         % (curr_prefix + prefix,
+                            ("\n" + curr_prefix + prefix).join(
+                                out.splitlines(False)))))
+                    # Compilation tools
+                    interp = 'R'.join(
+                        Rdrv.get_interpreter().rsplit('Rscript', 1))
+                    vardict.append(
+                        (curr_prefix + "R C Compiler:", ""))
+                    curr_prefix += prefix
+                    for x in ['CC', 'CFLAGS', 'CXX', 'CXXFLAGS']:
+                        try:
+                            out = tools.bytes2str(
+                                subprocess.check_output(
+                                    [interp, 'CMD', 'config', x],
+                                    stderr=subprocess.STDOUT)).strip()
+                        except subprocess.CalledProcessError:
+                            out = 'ERROR (missing Rtools?)'
+                        vardict.append(
+                            (curr_prefix + x, "%s"
+                             % ("\n" + curr_prefix + prefix).join(
+                                 out.splitlines(False))))
+                    curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
+                    # Session info
                     out = Rdrv.run_executable(
-                        ["-e", ("library(reticulate); "
-                                "reticulate::py_versions_windows()")],
-                        env=env_reticulate).strip()
-                    vardict.append((curr_prefix
-                                    + "R reticulate::py_versions_windows():",
-                                    "\n%s%s" % (curr_prefix + prefix,
-                                                ("\n" + curr_prefix + prefix).join(
-                                                    out.splitlines(False)))))
-                # conda_binary
-                if platform._is_win:  # pragma: windows
+                        ["-e", "sessionInfo()"]).strip()
+                    vardict.append(
+                        (curr_prefix + "R sessionInfo:", "\n%s%s"
+                         % (curr_prefix + prefix,
+                            ("\n" + curr_prefix + prefix).join(
+                                out.splitlines(False)))))
+                    # Reticulate conda_list
+                    if os.environ.get('CONDA_PREFIX', ''):
+                        out = Rdrv.run_executable(
+                            ["-e", ("library(reticulate); "
+                                    "reticulate::conda_list()")],
+                            env=env_reticulate).strip()
+                        vardict.append(
+                            (curr_prefix
+                             + "R reticulate::conda_list():",
+                             "\n%s%s" % (
+                                 curr_prefix + prefix,
+                                 ("\n" + curr_prefix + prefix).join(
+                                     out.splitlines(False)))))
+                    # Windows python versions
+                    if platform._is_win:  # pragma: windows
+                        out = Rdrv.run_executable(
+                            ["-e",
+                             ("library(reticulate); "
+                              "reticulate::py_versions_windows()")],
+                            env=env_reticulate).strip()
+                        vardict.append(
+                            (curr_prefix
+                             + "R reticulate::py_versions_windows():",
+                             "\n%s%s" % (
+                                 curr_prefix + prefix,
+                                 ("\n" + curr_prefix + prefix).join(
+                                     out.splitlines(False)))))
+                    # conda_binary
+                    if platform._is_win and shutil.which('conda'):  # pragma: windows
+                        out = Rdrv.run_executable(
+                            ["-e",
+                             ("library(reticulate); "
+                              "conda <- reticulate:::conda_binary(\"auto\"); "
+                              "system(paste(conda, \"info --json\"))")],
+                            env=env_reticulate).strip()
+                        vardict.append(
+                            (curr_prefix
+                             + "R reticulate::py_versions_windows():",
+                             "\n%s%s" % (
+                                 curr_prefix + prefix,
+                                 ("\n" + curr_prefix + prefix).join(
+                                     out.splitlines(False)))))
+                    # Reticulate py_config
                     out = Rdrv.run_executable(
                         ["-e", ("library(reticulate); "
-                                "conda <- reticulate:::conda_binary(\"auto\"); "
-                                "system(paste(conda, \"info --json\"))")],
+                                "reticulate::py_config()")],
                         env=env_reticulate).strip()
-                    vardict.append((curr_prefix
-                                    + "R reticulate::py_versions_windows():",
-                                    "\n%s%s" % (curr_prefix + prefix,
-                                                ("\n" + curr_prefix + prefix).join(
-                                                    out.splitlines(False)))))
-                # Reticulate py_config
-                out = Rdrv.run_executable(["-e", ("library(reticulate); "
-                                                  "reticulate::py_config()")],
-                                          env=env_reticulate).strip()
-                vardict.append((curr_prefix + "R reticulate::py_config():",
-                                "\n%s%s" % (curr_prefix + prefix,
-                                            ("\n" + curr_prefix + prefix).join(
-                                                out.splitlines(False)))))
-    finally:
-        # Print things
-        max_len = max(len(x[0]) for x in vardict)
-        lines = []
-        line_format = '%-' + str(max_len) + 's' + prefix + '%s'
-        for k, v in vardict:
-            lines.append(line_format % (k, v))
-        logger.info("yggdrasil info:\n%s" % '\n'.join(lines))
-
-
-def yggrun():
-    r"""Start a run."""
-    from yggdrasil import runner, config
-    parser = argparse.ArgumentParser(description='Run an integration.')
-    parser.add_argument('yamlfile', nargs='+',
-                        help='One or more yaml specification files.')
-    config.get_config_parser(parser, skip_sections='testing')
-    args = parser.parse_args()
-    prog = sys.argv[0].split(os.path.sep)[-1]
-    with config.parser_config(args):
-        runner.run(args.yamlfile, ygg_debug_prefix=prog,
-                   production_run=args.production_run)
-
-
-def yggclean():
-    r"""Cleanup dependency files."""
-    from yggdrasil.tools import get_supported_lang
-    from yggdrasil.components import import_component
-    parser = argparse.ArgumentParser(
-        description='Remove dependency libraries compiled by yggdrasil.')
-    parser.add_argument('language', nargs='*', default=[],
-                        help=('One or more languages to clean up '
-                              'dependencies for.'))
-    args = parser.parse_args()
-    if (len(args.language) == 0) or ('all' in args.language):
-        args.language = get_supported_lang()
-    for lang in args.language:
-        import_component('model', lang).cleanup_dependencies()
-
-
-def yggcompile():
+                    vardict.append(
+                        (curr_prefix + "R reticulate::py_config():",
+                         "\n%s%s" % (
+                             curr_prefix + prefix,
+                             ("\n" + curr_prefix + prefix).join(
+                                 out.splitlines(False)))))
+        finally:
+            # Print things
+            max_len = max(len(x[0]) for x in vardict)
+            lines = []
+            line_format = '%-' + str(max_len) + 's' + prefix + '%s'
+            for k, v in vardict:
+                lines.append(line_format % (k, v))
+            msg = "yggdrasil info:\n%s" % '\n'.join(lines)
+            if return_str:
+                return msg
+            logger.info(msg)
+
+        
+class validate_yaml(SubCommand):
+    r"""Validate a set of or or more YAMLs defining an integration."""
+
+    name = "validate"
+    help = 'Validate a set of YAML specification files for an integration.'
+    arguments = [
+        (('yamlfile', ),
+         {'nargs': '+',
+          'help': 'One or more YAML specification files.'})]
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import yamlfile
+        yamlfile.parse_yaml(args.yamlfile)
+        logger.info("Validation succesful.")
+
+
+class yggcc(SubCommand):
+    r"""Compile a program."""
+
+    name = "compile"
+    help = ("Compile a program from source files for use in an "
+            "yggdrasil integration.")
+    arguments = [
+        (('source', ),
+         {'nargs': '+',
+          'help': "One or more source files."}),
+        (('--language', ),
+         {'default': None,
+          'choices': [None] + LANGUAGES_WITH_ALIASES['compiled'],
+          'help': ("Language of the source code. If not provided, "
+                   "the language will be determined from the "
+                   "source extension.")})]
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil.components import import_component
+        from yggdrasil.constants import EXT2LANG
+        if args.language is None:
+            args.language = EXT2LANG[os.path.splitext(args.source[0])[-1]]
+        drv = import_component('model', args.language)
+        print("executable: %s" % drv.call_compile(args.source))
+
+
+class yggcompile(SubCommand):
     r"""Compile interface library/libraries."""
-    from yggdrasil.tools import get_supported_lang
-    from yggdrasil.components import import_component
-    yggclean()
-    parser = argparse.ArgumentParser(
-        description='Compile yggdrasil dependency libraries')
-    parser.add_argument('language', nargs='*', default=[],
-                        help=('One or more languages to compile the '
-                              'interface libraries for.'))
-    parser.add_argument('--toolname',
-                        help=('Name of compilation tool that should be '
-                              'used.'))
-    args = parser.parse_args()
-    if (len(args.language) == 0) or ('all' in args.language):
-        args.language = get_supported_lang()
-    for lang in args.language:
-        drv = import_component('model', lang)
-        if ((hasattr(drv, 'compile_dependencies')
-             and (not getattr(drv, 'is_build_tool', False)))):
-            drv.compile_dependencies(toolname=args.toolname)
-
-
-def yggcc():
-    r"""Compile C/C++ program."""
-    from yggdrasil.drivers import CModelDriver
-    parser = argparse.ArgumentParser(description='Compile a C/C++ program.')
-    parser.add_argument('source', nargs='+',
-                        help='One or more source files.')
-    args = parser.parse_args()
-    out = CModelDriver.CModelDriver.call_compile(args.source)
-    print("executable: %s" % out)
-
-
-def cc_flags():
+
+    name = "compile-deps"
+    help = ("Compile yggdrasil dependency libraries. Existing "
+            "libraries are first deleted.")
+    arguments = [
+        (('language', ),
+         {'nargs': '*', 'default': ['all'],
+          # 'choices': ['all'] + LANGUAGES_WITH_ALIASES['compiled'],
+          'help': ("One or more languages to compile dependencies "
+                   "for.")}),
+        (('--toolname', ),
+         {'help': "Name of compilation tool that should be used"})]
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil.components import import_component
+        yggclean.func(args, verbose=False)
+        error_on_missing = (not getattr(args, 'all_languages', False))
+        missing = []
+        for lang in args.language:
+            import_component('model', lang).cleanup_dependencies()
+            drv = import_component('model', lang)
+            if ((hasattr(drv, 'compile_dependencies')
+                 and (not getattr(drv, 'is_build_tool', False)))):
+                if drv.is_installed():
+                    drv.compile_dependencies(toolname=args.toolname)
+                else:
+                    missing.append(lang)
+        if error_on_missing and missing:  # pragma: debug
+            raise Exception(("One or more of the requested languages "
+                             "are not fully installed for use with "
+                             "yggdrasil: %s") % missing)
+
+
+class yggclean(SubCommand):
+    r"""Cleanup dependency files."""
+
+    name = "clean"
+    help = "Remove dependency libraries compiled by yggdrasil."
+    arguments = [
+        (('language', ),
+         {'nargs': '*', 'default': ['all'],
+          # 'choices': ['all'] + LANGUAGES_WITH_ALIASES['all'],
+          'help': ("One or more languages to clean up dependencies "
+                   "for.")})]
+
+    @classmethod
+    def func(cls, args, verbose=True):
+        from yggdrasil.components import import_component
+        for lang in args.language:
+            import_component('model', lang).cleanup_dependencies(
+                verbose=verbose)
+
+
+class cc_toolname(SubCommand):
+    r"""Output the name of the compiler used to compile C or C++ programs."""
+
+    name = "compiler-tool"
+    help = 'Get the compiler used for C/C++ programs.'
+    arguments = [
+        (('--cpp', ),
+         {'action': 'store_true',
+          'help': 'Get the compiler used for C++ programs.'})]
+
+    @classmethod
+    def parse_args(cls, *args, **kwargs):
+        args = super(cc_toolname, cls).parse_args(*args, **kwargs)
+        args.tool = 'compiler'
+        args.toolname = None
+        args.flags = False
+        if args.cpp:
+            args.language = 'cpp'
+        else:
+            args.language = 'c'
+        return args
+
+    @classmethod
+    def func(cls, args, **kwargs):
+        ygginfo.func(args, **kwargs)
+
+
+class ld_toolname(cc_toolname):
+    r"""Output the name of the linker used to compile C or C++ programs."""
+
+    name = "linker-tool"
+    help = 'Get the linker used for C/C++ programs.'
+    arguments = [
+        (('--cpp', ),
+         {'action': 'store_true',
+          'help': 'Get the linker used for C++ programs.'})]
+
+    @classmethod
+    def parse_args(cls, *args, **kwargs):
+        args = super(ld_toolname, cls).parse_args(*args, **kwargs)
+        args.tool = 'linker'
+        return args
+
+
+class cc_flags(cc_toolname):
     r"""Get the compiler flags necessary for including the interface
-    library in a C or C++ program.
-
-    Returns:
-        list: The necessary compiler flags and preprocessor definitions.
-
-    """
-    from yggdrasil import platform
-    parser = argparse.ArgumentParser(
-        description='Get the compilation flags necessary for a C/C++ program.')
-    parser.add_argument('--cpp', action='store_true',
-                        help='Get compilation flags for a C++ program.')
-    parser.add_argument('--toolname', default=None,
-                        help=('Name of the tool that associated flags should '
-                              'be returned for.'))
-    args = parser.parse_args()
-    if args.cpp:
-        from yggdrasil.drivers.CPPModelDriver import CPPModelDriver as driver
-    else:
-        from yggdrasil.drivers.CModelDriver import CModelDriver as driver
-    out = ' '.join(driver.get_compiler_flags(for_model=True,
-                                             toolname=args.toolname,
-                                             dry_run=True))
-    if platform._is_win:  # pragma: windows:
-        if out.endswith(' /link'):
-            out = out[:-(len(' /link'))]
-        out = out.replace('/', '-')
-        out = out.replace('\\', '/')
-        # out = out.encode('unicode_escape').decode('utf-8')
-    print(out)
-
-
-def ld_flags():
-    r"""Get the linker flags necessary for calling functions/classes from
-    the interface library in a C or C++ program.
-
-    Returns:
-        list: The necessary library linking flags.
-
-    """
-    from yggdrasil import platform
-    parser = argparse.ArgumentParser(
-        description='Get the linker flags necessary for a C/C++ program.')
-    parser.add_argument('--cpp', action='store_true',
-                        help='Get linker flags for a C++ program.')
-    parser.add_argument('--toolname', default=None,
-                        help=('Name of the tool that associated flags should '
-                              'be returned for.'))
-    args = parser.parse_args()
-    if args.cpp:
-        from yggdrasil.drivers.CPPModelDriver import CPPModelDriver as driver
-    else:
-        from yggdrasil.drivers.CModelDriver import CModelDriver as driver
-    out = ' '.join(driver.get_linker_flags(for_model=True,
-                                           toolname=args.toolname,
-                                           dry_run=True))
-    if platform._is_win:  # pragma: windows:
-        out = out.replace('/', '-')
-        out = out.replace('\\', '/')
-        # out = out.encode('unicode_escape').decode('utf-8')
-    print(out)
+    library in a C or C++ program."""
 
+    name = "compiler-flags"
+    help = 'Get the compilation flags necessary for a C/C++ program.'
+    arguments = [
+        (('--cpp', ),
+         {'action': 'store_true',
+          'help': 'Get the compilation flags used for C++ programs.'}),
+        (('--toolname', ),
+         {'default': None,
+          'help': 'Name of the tool that associated flags be returned for.'})]
 
-def rebuild_c_api():
-    r"""Rebuild the C/C++ API."""
-    from yggdrasil.drivers import CModelDriver, CPPModelDriver
-    if CModelDriver.CModelDriver.is_installed():
-        CModelDriver.CModelDriver.compile_dependencies(overwrite=True)
-        # TODO: Check that this compiles library correctly
-        CPPModelDriver.CPPModelDriver.compile_dependencies(overwrite=True)
-    else:
-        raise Exception("The libraries necessary for running models written in "
-                        "C/C++ could not be located.")
+    @classmethod
+    def parse_args(cls, *args, **kwargs):
+        args = super(cc_flags, cls).parse_args(*args, **kwargs)
+        args.flags = True
+        return args
 
-    
-def regen_metaschema():
+
+class ld_flags(cc_toolname):
+    r"""Get the linker flags necessary for including the interface
+    library in a C or C++ program."""
+
+    name = "linker-flags"
+    help = 'Get the compilation flags necessary for a C/C++ program.'
+    arguments = [
+        (('--cpp', ),
+         {'action': 'store_true',
+          'help': 'Get the compilation flags used for C++ programs.'}),
+        (('--toolname', ),
+         {'default': None,
+          'help': 'Name of the tool that associated flags be returned for.'})]
+
+    @classmethod
+    def parse_args(cls, *args, **kwargs):
+        args = super(ld_flags, cls).parse_args(*args, **kwargs)
+        args.tool = 'linker'
+        args.flags = True
+        return args
+
+
+class ygginstall(SubCommand):
+    r"""Call installation script."""
+
+    name = "install"
+
+    @classmethod
+    def get_parser(cls, **kwargs):
+        from yggdrasil.languages import install_languages
+        # TODO: Migrate
+        return install_languages.update_argparser()
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil.languages import install_languages
+        for x in args.language:
+            install_languages.install_language(x, args=args)
+
+
+class update_config(SubCommand):
+    r"""Update the user config file for yggdrasil."""
+
+    name = "config"
+    help = 'Update the user config file.'
+    arguments = [
+        (('languages', ),
+         {'nargs': '*',
+          # 'choices': ['all'] + LANGUAGES_WITH_ALIASES['all'],
+          'default': [],
+          'help': 'One or more languages that should be configured.'}),
+        (('--languages', ),
+         {'nargs': '+', 'dest': 'languages_flag',
+          # 'choices': ['all'] + LANGUAGES_WITH_ALIASES['all'],
+          'default': [],
+          'help': 'One or more languages that should be configured.'}),
+        (('--show-file', ),
+         {'action': 'store_true',
+          'help': 'Print the path to the config file without updating it.'}),
+        (('--remove-file', ),
+         {'action': 'store_true',
+          'help': 'Remove the existing config file and return.'}),
+        (('--overwrite', ),
+         {'action': 'store_true',
+          'help': 'Overwrite the existing file.'}),
+        (('--disable-languages', ),
+         {'nargs': '+', 'default': [],
+          'choices': LANGUAGES_WITH_ALIASES['all'],
+          'help': 'One or more languages that should be disabled.'}),
+        (('--enable-languages', ),
+         {'nargs': '+', 'default': [],
+          'choices': LANGUAGES_WITH_ALIASES['all'],
+          'help': 'One or more languages that should be enabled.'}),
+        (('--quiet', '-q'),
+         {'action': 'store_true',
+          'help': 'Suppress output.'})]
+    # TODO: Move these into the language directories?
+    language_arguments = {
+        'c': [
+            ConditionalArgumentTuple(
+                ('--vcpkg-dir', ),
+                {'help': 'Directory containing the vcpkg installation.'},
+                conditions={'os': ['Windows']}),
+            ConditionalArgumentTuple(
+                ('--macos-sdkroot', '--sdkroot'),
+                {'help': ('The full path to the MacOS SDK that '
+                          'should be used.')},
+                conditions={'os': ['MacOS']})],
+        'matlab': [
+            (('--disable-matlab-engine-for-python', ),
+             {'action': 'store_true',
+              'dest': 'disable_engine',
+              'help': 'Disable use of the Matlab engine for Python.'})]}
+        
+    @classmethod
+    def add_arguments(cls, parser, **kwargs):
+        super(update_config, cls).add_arguments(parser, **kwargs)
+        args = kwargs.get('args', None)
+        if args is None:
+            args = sys.argv[1:]
+        if ('-h' in args) or ('--help' in args):
+            args = [x for x in args if x not in ['-h', '--help']]
+        preargs = parser.parse_known_args(args=args)[0]
+        prelang = preargs.languages
+        if preargs.languages_flag:
+            prelang += preargs.languages_flag
+        if (len(prelang) == 0) or ('all' in prelang):
+            prelang = LANGUAGES['all']
+        # TODO: The languages could be subparsers
+        for k, v in cls.language_arguments.items():
+            if k in prelang:
+                cls.add_argument_to_parser(parser, v)
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import config
+        if args.show_file:
+            print('Config file located here: %s'
+                  % config.usr_config_file)
+        if args.remove_file and os.path.isfile(config.usr_config_file):
+            os.remove(config.usr_config_file)
+        if args.show_file or args.remove_file:
+            return
+        lang_kwargs = {}
+        for k, v in cls.language_arguments.items():
+            for v_args in v:
+                name = v_args[1].get(
+                    'dest',
+                    v_args[0][0].lstrip('-').replace('-', '_'))
+                if hasattr(args, name):
+                    lang_kwargs.setdefault(k, {})
+                    lang_kwargs[k][name] = getattr(args, name)
+        config.update_language_config(
+            args.languages, overwrite=args.overwrite,
+            verbose=(not args.quiet),
+            disable_languages=args.disable_languages,
+            enable_languages=args.enable_languages,
+            lang_kwargs=lang_kwargs)
+
+
+class regen_metaschema(SubCommand):
     r"""Regenerate the yggdrasil metaschema."""
-    from yggdrasil import metaschema
-    if os.path.isfile(metaschema._metaschema_fname):
-        os.remove(metaschema._metaschema_fname)
-    metaschema._metaschema = None
-    metaschema._validator = None
-    metaschema.get_metaschema()
-    
 
-def regen_schema():
+    name = "metaschema"
+    help = "Regenerate the yggdrasil metaschema."
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import metaschema
+        if os.path.isfile(metaschema._metaschema_fname):
+            os.remove(metaschema._metaschema_fname)
+        metaschema._metaschema = None
+        metaschema._validator = None
+        metaschema.get_metaschema()
+
+
+class regen_schema(SubCommand):
     r"""Regenerate the yggdrasil schema."""
-    from yggdrasil import schema
-    if os.path.isfile(schema._schema_fname):
-        os.remove(schema._schema_fname)
-    schema.clear_schema()
-    schema.init_schema()
 
+    name = "schema"
+    help = "Regenerate the yggdrasil schema."
 
-def validate_yaml():
-    r"""Validate a set of or or more YAMLs defining an integration."""
-    from yggdrasil import yamlfile
-    parser = argparse.ArgumentParser(
-        description='Validate a set of YAML specification files for an integration.')
-    parser.add_argument('yamlfile', nargs='+',
-                        help='One or more YAML specification files.')
-    args = parser.parse_args()
-    yamlfile.parse_yaml(args.yamlfile)
-    logger.info("Validation succesful.")
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import schema
+        if os.path.isfile(schema._schema_fname):
+            os.remove(schema._schema_fname)
+        schema.clear_schema()
+        schema.init_schema()
 
 
-def update_config():
-    r"""Update the user config file for yggdrasil."""
-    from yggdrasil import config, tools, platform
-    parser = argparse.ArgumentParser(
-        description='Update the user config file.')
-    parser.add_argument('--show-file', action='store_true',
-                        help='Print the path to the config file without updating it.')
-    parser.add_argument('--remove-file', action='store_true',
-                        help='Remove the existing config file and return.')
-    parser.add_argument('--overwrite', action='store_true',
-                        help='Overwrite the existing file.')
-    parser.add_argument('--languages', nargs='+',
-                        default=tools.get_supported_lang(),
-                        help=('One or more languages that should be'
-                              'configured.'))
-    parser.add_argument('--disable-languages', nargs='+',
-                        default=[], dest='disable_languages',
-                        help='One or more languages that should be disabled.')
-    parser.add_argument('--enable-languages', nargs='+', default=[],
-                        help='One or more languages that should be enabled.')
-    if ('-h' in sys.argv) or ('--help' in sys.argv):
-        prelang = tools.get_supported_lang()
-    else:
-        prelang = parser.parse_known_args()[0].languages
-    lang_args = {}
-    lang_args2kwargs = {}
-    if platform._is_mac:
-        lang_args.setdefault('c', [])
-        lang_args['c'].append(
-            (('--macos-sdkroot', '--sdkroot'),
-             {'help': (
-                 'The full path to the MacOS SDK '
-                 'that should be used.')}))
-    for lang in prelang:
-        if lang in lang_args:
-            lang_args2kwargs[lang] = []
-            for args, kwargs in lang_args.get(lang, []):
-                parser.add_argument(*args, **kwargs)
-                lang_args2kwargs[lang].append(parser._actions[-1].dest)
-    args = parser.parse_args()
-    lang_kwargs = {lang: {k: getattr(args, k) for k in alist}
-                   for lang, alist in lang_args2kwargs.items()}
-    if args.show_file:
-        print('Config file located here: %s' % config.usr_config_file)
-    if args.remove_file and os.path.isfile(config.usr_config_file):
-        os.remove(config.usr_config_file)
-    if args.show_file or args.remove_file:
-        return
-    config.update_language_config(args.languages, overwrite=args.overwrite,
-                                  verbose=True,
-                                  disable_languages=args.disable_languages,
-                                  enable_languages=args.enable_languages,
-                                  lang_kwargs=lang_kwargs)
+class yggmodelform(SubCommand):
+    r"""Save/print a JSON schema that can be used for generating a
+    form for composing a model specification files."""
+
+    name = "model-form-schema"
+    help = ('Save/print the JSON schema for generating the model '
+            'specification form.')
+    arguments = [
+        (('--file', ),
+         {'help': 'Path to file where the schema should be saved.'})]
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil.schema import get_model_form_schema
+        out = get_model_form_schema(fname_dst=args.file)
+        if not args.file:
+            pprint.pprint(out)
+
+
+class yggdevup(SubCommand):
+    r"""Cleanup old libraries, re-install languages, and re-compile
+    interface libraries following an update to the code (when doing
+    development)."""
+
+    name = "dev-update"
+    help = ('Perform cleanup and reinitialization following an '
+            'update to the code during development.')
+
+    @classmethod
+    def func(cls, args):
+        yggclean(args=['all'])
+        ygginstall(args=['all'])
+        yggcompile(args=['all'])
+        ygginfo(args=[])
+
+
+class run_tsts(SubCommand):
+    r"""Run yggdrasil test suite."""
+    # TODO: Move this into a pytest extension?
+
+    name = "test"
+    help = 'Run yggdrasil tests.'
+    arguments = [
+        (('--withcoverage', '--with-coverage'),
+         {'action': 'store_true',
+          'help': 'Record coverage during tests.'}),
+        (('--verbose', '-v'),
+         {'action': 'store_true',
+          'help': 'Increase verbosity of output from the test runner'}),
+        (('--nocapture', '-s'),
+         {'action': 'store_true',
+          'help': 'Don\'t capture output from tests.'}),
+        (('--stop', '-x'),
+         {'action': 'store_true',
+          'help': 'Stop after first test failure.'}),
+        (('--nologcapture', ),
+         {'action': 'store_true',
+          'help': ('Don\'t capture output from log messages '
+                   'generated during tests.')}),
+        (('--noflaky', '--no-flaky'),
+         {'action': 'store_true',
+          'help': 'Don\'t re-run flaky tests.'}),
+        (('--ci', ),
+         {'action': 'store_true',
+          'help': ('Perform addition operations required for '
+                   'testing on continuous integration services.')}),
+        (('--test-suite', '--test-suites'),
+         {'nargs': '+', 'action': 'extend', 'type': str,
+          'choices': ['all', 'top', 'examples', 'examples_part1',
+                      'examples_part2', 'demos', 'types', 'timing'],
+          'help': 'Test suite(s) that should be run.',
+          'dest': 'test_suites'}),
+        (('--pytest-config', '-c'),
+         {'help': ('Pytest configuration file that should be used '
+                   'instead of the defaults.')}),
+        (('--cov-config', ),
+         {'help': 'Config file for coverage. Default: .coveragerc'}),
+        (('--ignore', ),
+         {'action': 'append',
+          'help': 'Ignore path during collection.'}),
+        (('--rootdir', ),
+         {'help': 'Define root directory for tests.'}),
+        (('--count', ),
+         {'type': int, 'default': 1,
+          'help': 'Number of times to repeat a test.'})]
+    allow_unknown = True
+
+    @classmethod
+    def add_arguments(cls, parser, **kwargs):
+        from yggdrasil import config
+        super(run_tsts, cls).add_arguments(parser, **kwargs)
+        parser = config.get_config_parser(parser)
+
+    @classmethod
+    def parse_args(cls, *args, **kwargs):
+        from yggdrasil import config, platform
+        args = super(run_tsts, cls).parse_args(*args, **kwargs)
+        extra = args._extra_commands
+        if args.ci:
+            args.verbose = True
+            args.withcoverage = True
+            args.setup_cfg = 'setup.cfg'
+            args.pytest_config = args.setup_cfg
+            args.cov_config = '.coveragerc'
+            if not args.ignore:
+                args.ignore = []
+            args.ignore.append('yggdrasil/rapidjson/')
+            args.rootdir = package_dir
+            extra += ['--reruns=2', '--reruns-delay=1']
+        # Separate out paths from options
+        test_paths = []
+        if args.test_suites:
+            if 'all' in args.test_suites:
+                args.test_suites.remove('all')
+                for x in ['top', 'examples', 'demos', 'types', 'timing']:
+                    if x not in args.test_suites:
+                        args.test_suites.append(x)
+            for x in args.test_suites:
+                if x == 'top':
+                    test_paths.append(package_dir)
+                elif x == 'examples':
+                    args.enable_examples = True
+                    test_paths.append('examples')
+                elif x == 'examples_part1':
+                    args.enable_examples = True
+                    if platform._is_win:  # pragma: windows
+                        pattern = 'test_[a-g]*.py'
+                    else:
+                        pattern = 'test_[A-Za-g]*.py'
+                    test_paths.append(os.path.join(
+                        'examples', 'tests', pattern))
+                elif x == 'examples_part2':
+                    args.enable_examples = True
+                    pattern = 'test_[h-z]*.py'
+                    test_paths.append(os.path.join(
+                        'examples', 'tests', pattern))
+                elif x == 'demos':
+                    args.enable_demos = True
+                    test_paths.append('demos')
+                elif x == 'types':
+                    args.enable_examples = True
+                    args.long_running = True
+                    test_paths.append(os.path.join('examples', 'tests',
+                                                   'test_types.py'))
+                elif x == 'timing':
+                    args.long_running = True
+                    args.enable_production_runs = True
+                    test_paths.append(os.path.join('tests', 'test_timing.py'))
+        if (not test_paths) and all(x.startswith('-') for x in extra):
+            test_paths.append(package_dir)
+        # Get expanded tests to allow for paths that are relative to
+        # either the yggdrasil root directory or the current working
+        # directory
+        args = config.resolve_config_parser(args)
+        args.extra = []
+        cls.expand_and_add(extra + test_paths, args.extra,
+                           [package_dir, os.getcwd()],
+                           add_on_nomatch=True)
+            
+        return args
+
+    @classmethod
+    def expand_and_add(cls, path, path_list, dir_list,
+                       add_on_nomatch=False):
+        r"""Expand the specified path and add it's expanded forms to
+        the provided list.
+
+        Args:
+            path (str): Absolute/relative path with or without regex
+                wildcard expressions to expand.
+            path_list (list): Existing list that expansions should be
+                added to.
+            dir_list (list): Directories that should be tried for
+                relative paths.
+            add_on_nomatch (bool, optional): If True, path will be added
+                to path_list even if there are not matches. Defaults to
+                False.
+
+        Returns:
+            int: The number of expansions added to path_list.
+
+        """
+        import glob
+        if isinstance(path, list):
+            nadded = 0
+            for x in path:
+                nadded += cls.expand_and_add(x, path_list, dir_list,
+                                             add_on_nomatch=add_on_nomatch)
+            return nadded
+        if path.startswith('-'):
+            if add_on_nomatch:
+                path_list.append(path)
+                return 1
+            return 0
+        elif path.endswith('yggtest'):
+            return 0
+        elif os.path.isabs(path):
+            matches = sorted(glob.glob(path))
+            path_list += matches
+            if (not matches) and add_on_nomatch:
+                path_list.append(path)
+                return 1
+            return len(matches)
+        # Try checking for function
+        for func_sep in ['::', ':']:
+            if (func_sep in path):
+                prepath, mod = path.rsplit(func_sep, 1)
+                nadded = cls.expand_and_add(prepath, path_list, dir_list)
+                if nadded:
+                    for i in range(-nadded, 0):
+                        path_list[i] += '%s%s' % (func_sep, mod)
+                    return nadded
+        # Try directory prefixes
+        for path_prefix in dir_list:
+            nadded = cls.expand_and_add(os.path.join(path_prefix, path),
+                                        path_list, dir_list)
+            if nadded:
+                return nadded
+        if add_on_nomatch:
+            path_list.append(path)
+            return 1
+        return 0
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import config
+        argv = ['pytest']
+        # test_paths = args.test_paths
+        if args.verbose:
+            argv.append('-v')
+        if args.nocapture:
+            argv.append('-s')
+        if args.stop:
+            argv.append('-x')
+        if args.withcoverage:
+            # See information about getting coverage of test fixtures
+            # https://pytest-cov.readthedocs.io/en/stable/plugins.html
+            argv.append('--cov=%s' % package_dir)
+            # argv.append('--cov-append')
+        if args.noflaky:
+            argv += ['-p', 'no:flaky']
+        if args.pytest_config:
+            argv += ['-c', args.pytest_config]
+        if args.cov_config:
+            argv += ['--cov-config=%s' % args.cov_config]
+        if args.ignore:
+            argv += ['--ignore=%s' % x for x in args.ignore]
+        if args.rootdir:
+            argv += ['--rootdir=%s' % args.rootdir]
+        argv += args.extra
+        # Run test command and perform cleanup before logging any errors
+        logger.info("Running %s from %s", argv, os.getcwd())
+        new_config = {}
+        # pth_file = 'ygg_coverage.pth'
+        # assert(not os.path.isfile(pth_file))
+        # # Get arguments for temporary test environment
+        # if args.withcoverage:
+        #     new_config['COVERAGE_PROCESS_START'] = 'True'
+        #     with open(pth_file, 'w') as fd:
+        #         fd.write("import coverage; coverage.process_startup()")
+        initial_dir = os.getcwd()
+        error_code = 0
+        with config.parser_config(args, **new_config):
+            try:
+                # Perform CI specific pretest operations
+                if args.ci:
+                    if not os.path.isfile(args.setup_cfg):
+                        raise RuntimeError("The CI tests must be run "
+                                           "from the root directory "
+                                           "of the yggdrasil git "
+                                           "repository.")
+                    top_dir = os.path.dirname(os.getcwd())
+                    src_cmd = ('python -c \"import versioneer; '
+                               'print(versioneer.get_version())\"')
+                    dst_cmd = ('python -c \"import yggdrasil; '
+                               'print(yggdrasil.__version__)\"')
+                    src_ver = subprocess.check_output(src_cmd,
+                                                      shell=True)
+                    dst_ver = subprocess.check_output(dst_cmd,
+                                                      shell=True,
+                                                      cwd=top_dir)
+                    if src_ver != dst_ver:  # pragma: debug
+                        raise RuntimeError(("Versions do not match:\n"
+                                            "\tSource version: %s\n"
+                                            "\tBuild  version: %s\n")
+                                           % (src_ver, dst_ver))
+                    subprocess.check_call(
+                        ["flake8", "yggdrasil", "--append-config",
+                         args.setup_cfg])
+                    if os.environ.get("YGG_CONDA", None):
+                        subprocess.check_call(
+                            ["python", "create_coveragerc.py"])
+                    if not os.path.isfile(".coveragerc"):
+                        raise RuntimeError(".coveragerc file dosn't "
+                                           "exist.")
+                    with open(".coveragerc", "r") as fd:
+                        print(fd.read())
+                    subprocess.check_call(["yggdrasil", "info",
+                                           "--verbose"])
+                error_code = 0
+                for x in range(args.count):
+                    x_error_code = subprocess.call(argv)
+                    if x_error_code != 0:
+                        error_code = x_error_code
+                        if args.stop:
+                            break
+            except BaseException:
+                logger.exception('Error in running test.')
+                error_code = -1
+            finally:
+                os.chdir(initial_dir)
+                # if os.path.isfile(pth_file):
+                #     os.remove(pth_file)
+        return error_code
+
+
+class timing_plots(SubCommand):
+    r"""Create performance plots using timing results."""
+
+    name = "timing"
+    help = "Create performance plots using timing results."
+    arguments = [
+        ArgumentSubparser(
+            title='comparison', dest='comparison',
+            description='Comparison plot that should be created.',
+            parsers=[
+                ArgumentParser(
+                    name='commtype',
+                    help='Compare different communication avenues.'),
+                ArgumentParser(
+                    name='language',
+                    help='Compare different programming languages.'),
+                ArgumentParser(
+                    name='os',
+                    help='Compare different operating systems.'),
+                ArgumentParser(
+                    name='python',
+                    help='Compare different versions of Python.'),
+                ArgumentParser(
+                    name='lang2019',
+                    help='Create plots from Lang (2019) paper.')])]
+
+    @classmethod
+    def func(cls, args):
+        from yggdrasil import timing
+        if args.comparison == 'lang2019':
+            _lang_list = timing._lang_list
+            _lang_list_nomatlab = copy.deepcopy(_lang_list)
+            _lang_list_nomatlab.remove('matlab')
+            timing.plot_scalings(compare='platform', python_ver='2.7')
+            # All plots on Linux, no matlab
+            timing.plot_scalings(compare='comm_type',
+                                 platform='Linux',
+                                 python_ver='2.7')
+            timing.plot_scalings(compare='python_ver',
+                                 platform='Linux')
+            timing.plot_scalings(compare='language',
+                                 platform='Linux',
+                                 python_ver='2.7',
+                                 compare_values=_lang_list_nomatlab)
+            # Language comparision on MacOS, with matlab
+            timing.plot_scalings(compare='language',
+                                 platform='MacOS',
+                                 python_ver='2.7',
+                                 compare_values=_lang_list)
+        else:
+            if args.comparison == 'commtype':
+                args.comparison = 'comm_type'
+            elif args.comparison == 'os':
+                args.comparison = 'platform'
+            timing.plot_scalings(compare=args.comparison)
+
+
+class generate_gha_workflow(SubCommand):
+    r"""Re-generate the Github actions workflow yaml."""
+
+    name = "gha"
+    help = (
+        "Generate a Github Actions (GHA) workflow yaml file from "
+        "a version of the file that uses anchors (not supported by "
+        "GHA as of 2021-01-14).")
+    arguments = [
+        (('--base', '--base-file'),
+         {'help': (
+             "Version of GHA workflow yaml that contains anchors.")}),
+        (('--dest', ),
+         {'help': (
+             "Name of target GHA workflow yaml file.")}),
+        (('--verbose', ),
+         {'action': 'store_true', 'help': "Print yaml contents."})]
+
+    @classmethod
+    def func(cls, args, gitdir=None):
+        import yaml
+        from yggdrasil.metaschema.encoder import decode_yaml, encode_yaml
+        from collections import OrderedDict
+
+        class NoAliasDumper(yaml.SafeDumper):
+            def ignore_aliases(self, data):
+                return True
+        if gitdir is None:
+            try:
+                gitdir = subprocess.check_output(
+                    ["git", "rev-parse", "--git-dir"],
+                    stderr=subprocess.PIPE).decode('utf-8').strip()
+            except subprocess.CalledProcessError:
+                return 1
+        if args.base is None:
+            args.base = os.path.join(gitdir, '..', 'utils',
+                                     'test-install-base.yml')
+        if args.dest is None:
+            args.dest = os.path.join(gitdir, '..', '.github', 'workflows',
+                                     'test-install.yml')
+        with open(args.base, 'r') as fd:
+            contents = decode_yaml(fd.read(), sorted_dict_type=OrderedDict)
+            # contents = yaml.load(fd, Loader=yaml.SafeLoader)
+        if args.verbose:
+            pprint.pprint(contents)
+        with open(args.dest, 'w') as fd:
+            fd.write(('# DO NOT MODIFY THIS FILE, IT IS GENERATED.\n'
+                      '# To make changes, modify \'%s\'\n'
+                      '# and run \'ygggha\'\n')
+                     % args.base)
+            fd.write(encode_yaml(contents, sorted_dict_type=OrderedDict,
+                                 Dumper=NoAliasDumper))
+            # yaml.dump(contents, fd, Dumper=NoAliasDumper)
+
+
+def rebuild_c_api():
+    r"""Rebuild the C/C++ API."""
+    ReplacementWarning('yggbuildapi_c', 'yggdrasil compile-deps c cpp')
+    yggcompile(args=['c', 'cpp'] + sys.argv[1:])
 
 
 def yggtime_comm():
     r"""Plot timing statistics comparing the different communication mechanisms."""
-    from yggdrasil import timing
-    timing.plot_scalings(compare='commtype')
+    ReplacementWarning('yggtime_comm', 'yggdrasil timing commtype')
+    timing_plots(args=['commtype'] + sys.argv[1:])
 
 
 def yggtime_lang():
     r"""Plot timing statistics comparing the different languages."""
-    from yggdrasil import timing
-    timing.plot_scalings(compare='language')
+    ReplacementWarning('yggtime_lang', 'yggdrasil timing language')
+    timing_plots(args=['language'] + sys.argv[1:])
 
 
 def yggtime_os():
     r"""Plot timing statistics comparing the different operating systems."""
-    from yggdrasil import timing
-    timing.plot_scalings(compare='platform')
+    ReplacementWarning('yggtime_os', 'yggdrasil timing os')
+    timing_plots(args=['os'] + sys.argv[1:])
 
 
 def yggtime_py():
     r"""Plot timing statistics comparing the different versions of Python."""
-    from yggdrasil import timing
-    timing.plot_scalings(compare='python')
+    ReplacementWarning('yggtime_py', 'yggdrasil timing python')
+    timing_plots(args=['python'] + sys.argv[1:])
 
 
 def yggtime_paper():
     r"""Create plots for timing."""
-    from yggdrasil import timing
-    _lang_list = timing._lang_list
-    _lang_list_nomatlab = copy.deepcopy(_lang_list)
-    _lang_list_nomatlab.remove('matlab')
-    timing.plot_scalings(compare='platform', python_ver='2.7')
-    # All plots on Linux, no matlab
-    timing.plot_scalings(compare='comm_type', platform='Linux', python_ver='2.7')
-    timing.plot_scalings(compare='python_ver', platform='Linux')
-    timing.plot_scalings(compare='language', platform='Linux', python_ver='2.7',
-                         compare_values=_lang_list_nomatlab)
-    # Language comparision on MacOS, with matlab
-    timing.plot_scalings(compare='language', platform='MacOS', python_ver='2.7',
-                         compare_values=_lang_list)
-
-
-def ygginstall():
-    r"""Call installation script."""
-    from yggdrasil.languages import install_languages
-    parser = install_languages.update_argparser()
-    args = parser.parse_args()
-    if (len(args.language) == 0) or ('all' in args.language):
-        install_languages.install_all_languages(args=args)
-    else:
-        for x in args.language:
-            install_languages.install_language(x, args=args)
-
-
-def yggmodelform():
-    r"""Save/print a JSON schema that can be used for generating a
-    form for composing a model specification files."""
-    from yggdrasil.schema import get_model_form_schema
-    parser = argparse.ArgumentParser(
-        description=('Save/print the JSON schema for generating the '
-                     'model specification form.'))
-    parser.add_argument('--file',
-                        help='Path to file where the schema should be saved.')
-    args = parser.parse_args()
-    out = get_model_form_schema(fname_dst=args.file)
-    if not args.file:
-        pprint.pprint(out)
-
-
-def yggdevup():
-    r"""Cleanup old libraries, re-install languages, and re-compile interface
-    libraries following an update to the code (when doing development)."""
-    parser = argparse.ArgumentParser(
-        description=('Perform cleanup and reinitialization following an '
-                     'update to the code during development.'))
-    parser.parse_args()
-    yggclean()
-    ygginstall()
-    yggcompile()
-    ygginfo()
+    ReplacementWarning('yggtime_paper', 'yggdrasil timing lang2019')
+    timing_plots(args=['lang2019'] + sys.argv[1:])
 
 
 if __name__ == '__main__':
diff --git a/yggdrasil/communication/AsyncComm.py b/yggdrasil/communication/AsyncComm.py
index b1624d4f7..19d5b8105 100755
--- a/yggdrasil/communication/AsyncComm.py
+++ b/yggdrasil/communication/AsyncComm.py
@@ -226,6 +226,8 @@ def is_confirmed_send(self):
         for v in self._work_comms.values():
             if (v.direction == 'send') and not v.is_confirmed_send:  # pragma: debug
                 return False
+        if self.is_open_backlog and self.n_msg_backlog_send:  # pragma: debug
+            return False
         return (self.n_msg_direct_send == 0)
 
     @property
diff --git a/yggdrasil/communication/ClientComm.py b/yggdrasil/communication/ClientComm.py
index 6aa26fb45..97b282679 100644
--- a/yggdrasil/communication/ClientComm.py
+++ b/yggdrasil/communication/ClientComm.py
@@ -47,6 +47,29 @@ def __init__(self, name, request_comm=None, response_kwargs=None,
                                          direction='send', no_suffix=True,
                                          address=self.ocomm.address)
 
+    def get_status_message(self, nindent=0, **kwargs):
+        r"""Return lines composing a status message.
+        
+        Args:
+            nindent (int, optional): Number of tabs that should
+                be used to indent each line. Defaults to 0.
+            *kwargs: Additional arguments are passed to the
+                parent class's method.
+                
+        Returns:
+            tuple(list, prefix): Lines composing the status message and the
+                prefix string used for the last message.
+
+        """
+        lines, prefix = super(ClientComm, self).get_status_message(
+            nindent=nindent, **kwargs)
+        lines.append('%s%-15s:' % (prefix, 'request comm'))
+        lines += self.ocomm.get_status_message(nindent=(nindent + 1))[0]
+        lines.append('%s%-15s:' % (prefix, 'response comms'))
+        for x in self.icomm.values():
+            lines += x.get_status_message(nindent=(nindent + 1))[0]
+        return lines, prefix
+    
     @classmethod
     def is_installed(cls, language=None):
         r"""Determine if the necessary libraries are installed for this
diff --git a/yggdrasil/communication/CommBase.py b/yggdrasil/communication/CommBase.py
index 0fb056b48..c59089935 100755
--- a/yggdrasil/communication/CommBase.py
+++ b/yggdrasil/communication/CommBase.py
@@ -428,9 +428,18 @@ def __init__(self, name, address=None, direction='send', dont_open=False,
                 prefix = '%s:' % model_name
                 if model_name and (not self.name.startswith(prefix)):
                     self._name = prefix + self.name
-                if self.name not in os.environ:
-                    raise RuntimeError('Cannot see %s in env.' % self.name)
-            self.address = os.environ[self.name]
+                if (((self.name not in os.environ)
+                     and (self.name.replace(':', '__COLON__')
+                          not in os.environ))):
+                    import pprint
+                    env_str = pprint.pformat(os.environ.copy())
+                    raise RuntimeError(
+                        'Cannot see %s in env (model = %s). Env:\n%s' %
+                        (self.name, model_name, env_str))
+            try:
+                self.address = os.environ[self.name]
+            except KeyError:
+                self.address = os.environ[self.name.replace(':', '__COLON__')]
         else:
             self.address = address
         self.direction = direction
diff --git a/yggdrasil/communication/RMQComm.py b/yggdrasil/communication/RMQComm.py
index ea6e4dbda..99830a8c6 100755
--- a/yggdrasil/communication/RMQComm.py
+++ b/yggdrasil/communication/RMQComm.py
@@ -62,7 +62,9 @@ def check_rmq_server(url=None, **kwargs):
         if not connection.is_open:  # pragma: debug
             raise BaseException("Connection was not openned.")
         connection.close()
-    except BaseException:  # pragma: debug
+    except BaseException as e:  # pragma: debug
+        print("Error when attempting to connect to the RabbitMQ server: "
+              + str(e))
         out = False
     logging.getLogger("pika").propagate = True
     return out
diff --git a/yggdrasil/communication/ServerComm.py b/yggdrasil/communication/ServerComm.py
index 6e5045f95..5cd5ef900 100644
--- a/yggdrasil/communication/ServerComm.py
+++ b/yggdrasil/communication/ServerComm.py
@@ -52,6 +52,29 @@ def __init__(self, name, request_comm=None, response_kwargs=None,
                                          direction='recv', no_suffix=True,
                                          address=self.icomm.address)
 
+    def get_status_message(self, nindent=0, **kwargs):
+        r"""Return lines composing a status message.
+        
+        Args:
+            nindent (int, optional): Number of tabs that should
+                be used to indent each line. Defaults to 0.
+            *kwargs: Additional arguments are passed to the
+                parent class's method.
+                
+        Returns:
+            tuple(list, prefix): Lines composing the status message and the
+                prefix string used for the last message.
+
+        """
+        lines, prefix = super(ServerComm, self).get_status_message(
+            nindent=nindent, **kwargs)
+        lines.append('%s%-15s:' % (prefix, 'request comm'))
+        lines += self.icomm.get_status_message(nindent=(nindent + 1))[0]
+        lines.append('%s%-15s:' % (prefix, 'response comms'))
+        for x in self.ocomm.values():
+            lines += x.get_status_message(nindent=(nindent + 1))[0]
+        return lines, prefix
+        
     @classmethod
     def is_installed(cls, language=None):
         r"""Determine if the necessary libraries are installed for this
diff --git a/yggdrasil/communication/ZMQComm.py b/yggdrasil/communication/ZMQComm.py
index 879663f23..2eceb2a36 100755
--- a/yggdrasil/communication/ZMQComm.py
+++ b/yggdrasil/communication/ZMQComm.py
@@ -1,14 +1,21 @@
 import os
 import tempfile
 import uuid
-import zmq
 import logging
 from yggdrasil import tools
 from yggdrasil import multitasking
 from yggdrasil.communication import CommBase, AsyncComm
+logger = logging.getLogger(__name__)
+try:
+    import zmq
+    _zmq_installed = True
+except ImportError:  # pragma: debug
+    logger.debug("Could not import pyzmq. "
+                 + "ZMQ support will be disabled.")
+    zmq = None
+    _zmq_installed = False
 
 
-logger = logging.getLogger(__name__)
 _socket_type_pairs = [('PUSH', 'PULL'),
                       ('PUB', 'SUB'),
                       ('REP', 'REQ'),
@@ -23,7 +30,10 @@
 _wait_send_t = 0  # 0.0001
 _reply_msg = b'YGG_REPLY'
 _purge_msg = b'YGG_PURGE'
-_global_context = zmq.Context.instance()
+if _zmq_installed:
+    _global_context = zmq.Context.instance()
+else:  # pragma: debug
+    _global_context = None
 
 
 def get_ipc_host():
@@ -212,9 +222,16 @@ def __init__(self, srv_address, zmq_context=None, retry_timeout=-1,
         # Bind backend
         self.srv_socket = zmq_context.socket(zmq.DEALER)
         self.srv_socket.setsockopt(zmq.LINGER, 0)
-        self.srv_address = bind_socket(self.srv_socket, srv_address,
-                                       nretry=nretry,
-                                       retry_timeout=retry_timeout)
+        try:
+            self.srv_address = bind_socket(self.srv_socket, srv_address,
+                                           nretry=nretry,
+                                           retry_timeout=retry_timeout)
+        except zmq.ZMQError:  # pragma: debug
+            self.cli_socket.close()
+            self.cli_socket = None
+            CommBase.unregister_comm(
+                'ZMQComm', 'ROUTER_server_' + self.cli_address)
+            raise
         CommBase.register_comm('ZMQComm', 'DEALER_server_' + self.srv_address,
                                self.srv_socket)
         # Set up poller
diff --git a/yggdrasil/config.py b/yggdrasil/config.py
index e534dc5ad..d7f7f5396 100644
--- a/yggdrasil/config.py
+++ b/yggdrasil/config.py
@@ -335,8 +335,12 @@ def update_language_config(languages=None, skip_warnings=False,
             logger.info(("%s language both enabled and disabled. "
                          "No action will be taken.") % idrv.language)
         elif idrv.language in disable_languages:
+            if not ygg_cfg_usr.has_section(idrv.language):
+                ygg_cfg_usr.add_section(idrv.language)
             ygg_cfg_usr.set(idrv.language, 'disable', 'True')
         elif idrv.language in enable_languages:
+            if not ygg_cfg_usr.has_section(idrv.language):
+                ygg_cfg_usr.add_section(idrv.language)
             ygg_cfg_usr.set(idrv.language, 'disable', 'False')
         if ygg_cfg_usr.get(idrv.language, 'disable', 'False').lower() == 'true':
             continue  # pragma: no cover
@@ -350,6 +354,9 @@ def update_language_config(languages=None, skip_warnings=False,
                            + "Please set this in %s to: %s")
                           % (opt, sect, ygg_cfg_usr.file_to_update, desc),
                           RuntimeWarning)
+    if verbose:
+        with open(usr_config_file, 'r') as fd:
+            print(fd.read())
     
 
 def get_ygg_loglevel(cfg=None, default='DEBUG'):
@@ -412,7 +419,7 @@ def cfg_logging(cfg=None):
     if cfg is None:
         cfg = ygg_cfg
     _LOG_FORMAT = "%(levelname)s:%(module)s.%(funcName)s[%(lineno)d]:%(message)s"
-    logging.basicConfig(level=logging.INFO, format=_LOG_FORMAT)
+    logging.basicConfig(format=_LOG_FORMAT)
     logLevelYGG = eval('logging.%s' % cfg.get('debug', 'ygg', 'NOTSET'))
     logLevelRMQ = eval('logging.%s' % cfg.get('debug', 'rmq', 'INFO'))
     logLevelCLI = eval('logging.%s' % cfg.get('debug', 'client', 'INFO'))
diff --git a/yggdrasil/constants.py b/yggdrasil/constants.py
index caabd4eb3..32f673e74 100644
--- a/yggdrasil/constants.py
+++ b/yggdrasil/constants.py
@@ -1,6 +1,8 @@
 """Constants used by yggdrasil."""
 
 
+# Hard coded to speed-up imports, but there should be a script
+# to modify/generate this
 LANG2EXT = {'python': '.py',
             'matlab': '.m',
             'r': '.R',
@@ -10,3 +12,25 @@
             'fortran': '.f90',
             'yaml': '.yml'}
 EXT2LANG = {v: k for k, v in LANG2EXT.items()}
+LANGUAGES = {
+    'compiled': [
+        'c', 'c++', 'fortran', 'osr', 'cmake', 'make'],
+    'interpreted': [
+        'python', 'matlab', 'r', 'sbml', 'lpy'],
+    'other': [
+        'executable', 'timesync']}
+LANGUAGES['all'] = (
+    LANGUAGES['compiled']
+    + LANGUAGES['interpreted']
+    + LANGUAGES['other'])
+LANGUAGES_WITH_ALIASES = {
+    'compiled': [
+        'c', 'c++', 'cpp', 'cxx', 'fortran', 'osr', 'cmake', 'make'],
+    'interpreted': [
+        'python', 'matlab', 'r', 'R', 'sbml', 'lpy'],
+    'other': [
+        'executable', 'timesync']}
+LANGUAGES_WITH_ALIASES['all'] = (
+    LANGUAGES_WITH_ALIASES['compiled']
+    + LANGUAGES_WITH_ALIASES['interpreted']
+    + LANGUAGES_WITH_ALIASES['other'])
diff --git a/yggdrasil/demos/fspm2020 b/yggdrasil/demos/fspm2020
index aa4320945..0acd22eb2 160000
--- a/yggdrasil/demos/fspm2020
+++ b/yggdrasil/demos/fspm2020
@@ -1,1 +1,1 @@
-Subproject commit aa43209457f1185b0c91eae8d83cf9f04c212245
+Subproject commit 0acd22eb2e811f6180c7438829fa8c5260d186bd
diff --git a/yggdrasil/drivers/CMakeModelDriver.py b/yggdrasil/drivers/CMakeModelDriver.py
index 1c8e1b12e..c1131bc98 100644
--- a/yggdrasil/drivers/CMakeModelDriver.py
+++ b/yggdrasil/drivers/CMakeModelDriver.py
@@ -1,5 +1,6 @@
 import os
 import re
+import sys
 import shutil
 import logging
 import sysconfig
@@ -216,10 +217,11 @@ def call(cls, args, **kwargs):
             if platform._is_win:  # pragma: windows
                 error_MSB4019 = (r'error MSB4019: The imported project '
                                  r'"C:\Microsoft.Cpp.Default.props" was not found.')
+                error_NOVS = r'could not find any instance of Visual Studio.'
                 # This will only be called if the VC 15 build tools
                 # are installed by MSVC 19+ which is not currently
                 # supported by Appveyor CI.
-                if error_MSB4019 in str(e):  # pragma: debug
+                if (error_MSB4019 in str(e)) or (error_NOVS in str(e)):  # pragma: debug
                     old_generator = os.environ.get('CMAKE_GENERATOR', None)
                     new_generator = cls.generator(return_default=True)
                     if old_generator and (old_generator != new_generator):
@@ -403,6 +405,10 @@ def create_include(cls, fname, target, driver=None, toolname=None,
                 are displayed. Defaults to False.
             **kwargs: Additional keyword arguments are ignored.
 
+        Returns:
+            list: Lines that should be added before the executable is defined
+                in the CMakeLists.txt (e.g. LINK_DIRECTORIES commands).
+
         Raises:
             ValueError: If a linker or compiler flag cannot be interpreted.
 
@@ -432,6 +438,7 @@ def create_include(cls, fname, target, driver=None, toolname=None,
             internal_library_flags=internal_library_flags,
             skip_library_libs=True, library_flags=library_flags)
         lines = []
+        pretarget_lines = []
         preamble_lines = []
         library_flags += internal_library_flags + external_library_flags
         # Suppress warnings on windows about the security of strcpy etc.
@@ -454,10 +461,17 @@ def create_include(cls, fname, target, driver=None, toolname=None,
             for x in new_flags:
                 if x not in compile_flags:
                     compile_flags.append(x)
+        # Find Python using cmake
+        # https://martinopilia.com/posts/2018/09/15/building-python-extension.html
+        # preamble_lines.append('find_package(PythonInterp REQUIRED)')
+        # preamble_lines.append('find_package(PythonLibs REQUIRED)')
+        # preamble_lines.append('INCLUDE_DIRECTORIES(${PYTHON_INCLUDE_DIRS})')
+        # lines.append('TARGET_LINK_LIBRARIES(%s ${PYTHON_LIBRARIES})'
+        #              % target)
         python_flags = sysconfig.get_config_var('LIBS')
         if python_flags:
             for x in python_flags.split():
-                if x.startswith('-l') and (x not in linker_flags):
+                if x.startswith(('-L', '-l')) and (x not in linker_flags):
                     linker_flags.append(x)
         # Compilation flags
         for x in compile_flags:
@@ -484,16 +498,19 @@ def create_include(cls, fname, target, driver=None, toolname=None,
                 lines.append('TARGET_LINK_LIBRARIES(%s %s)' % (target, x))
             elif x.startswith('-L'):
                 libdir = cls.fix_path(x.split('-L')[-1], is_gnu=is_gnu)
-                preamble_lines.append('LINK_DIRECTORIES(%s)' % libdir)
+                pretarget_lines.append('LINK_DIRECTORIES(%s)' % libdir)
             elif x.startswith('/LIBPATH:'):  # pragma: windows
                 libdir = x.split('/LIBPATH:')[-1]
                 if '"' in libdir:
                     libdir = libdir.split('"')[1]
                 libdir = cls.fix_path(libdir, is_gnu=is_gnu)
-                preamble_lines.append('LINK_DIRECTORIES(%s)' % libdir)
+                pretarget_lines.append('LINK_DIRECTORIES(%s)' % libdir)
             elif os.path.isfile(x):
                 library_flags.append(x)
-            elif x.startswith('-mlinker-version='):
+            elif x.startswith('-mlinker-version='):  # pragma: version
+                # Currently this only called when clang is >=10
+                # and ld is <520 or mlinker is set in the env
+                # flags via CFLAGS, CXXFLAGS, etc.
                 preamble_lines.insert(0, 'target_link_options(%s PRIVATE %s)'
                                       % (target, x))
             elif x.startswith('-') or x.startswith('/'):
@@ -511,7 +528,7 @@ def create_include(cls, fname, target, driver=None, toolname=None,
             xn = os.path.splitext(xl)[0]
             new_dir = 'LINK_DIRECTORIES(%s)' % xd
             if new_dir not in preamble_lines:
-                preamble_lines.append(new_dir)
+                pretarget_lines.append(new_dir)
             if cls.add_libraries or (xorig in internal_library_flags):
                 # if cls.add_libraries:  # pragma: no cover
                 # Version adding library
@@ -536,19 +553,33 @@ def create_include(cls, fname, target, driver=None, toolname=None,
                              % (xn.upper(), xf, xn, xd))
                 lines.append('TARGET_LINK_LIBRARIES(%s ${%s_LIBRARY})'
                              % (target, xn.upper()))
+        # Link local lib on MacOS because on Mac >=10.14 setting sysroot
+        # clobbers the default paths.
+        # https://stackoverflow.com/questions/54068035/linking-not-working-in
+        # -homebrews-cmake-since-mojave
+        if platform._is_mac:
+            pretarget_lines.append('LINK_DIRECTORIES(/usr/lib)')
+            pretarget_lines.append('LINK_DIRECTORIES(/usr/local/lib)')
         lines = preamble_lines + lines
-        log_msg = 'CMake include file:\n\t' + '\n\t'.join(lines)
+        log_msg = (
+            'CMake compiler flags:\n\t%s\n'
+            'CMake linker flags:\n\t%s\n'
+            'CMake library flags:\n\t%s\n'
+            'CMake include file:\n\t%s') % (
+                ' '.join(compile_flags), ' '.join(linker_flags),
+                ' '.join(library_flags), '\n\t'.join(lines))
         if verbose:
             logger.info(log_msg)
         else:
             logger.debug(log_msg)
         if fname is None:
-            return lines
+            return pretarget_lines + lines
         else:
             if os.path.isfile(fname):
                 os.remove(fname)
             with open(fname, 'w') as fd:
                 fd.write('\n'.join(lines))
+            return pretarget_lines
 
     
 class CMakeBuilder(LinkerBase):
@@ -575,9 +606,9 @@ def call(cls, *args, **kwargs):
                 cache = os.path.join(kwargs['working_dir'], cache)
             if os.path.isfile(cache):
                 with open(cache, 'r') as fd:
-                    print('CMakeCache.txt:\n%s' % fd.read())
+                    logger.info('CMakeCache.txt:\n%s' % fd.read())
             else:
-                print('Cache file does not exist: %s' % cache)
+                logger.error('Cache file does not exist: %s' % cache)
             raise
 
     @classmethod
@@ -800,16 +831,16 @@ def write_wrappers(self, **kwargs):
         else:
             include_base = 'ygg_cmake_%s.txt' % self.target
         include_file = os.path.join(self.sourcedir, include_base)
-        self.get_tool_instance('compiler').create_include(
+        newlines_before = self.get_tool_instance('compiler').create_include(
             include_file, self.target,
             driver=self.target_language_driver,
             toolname=self.target_compiler,
             logging_level=self.logger.getEffectiveLevel(),
-            configuration=self.configuration)
+            configuration=self.configuration,
+            verbose=kwargs.get('verbose', False))
         assert(os.path.isfile(include_file))
         out.append(include_file)
         # Create copy of cmakelists and modify
-        newlines_before = []
         newlines_after = []
         if os.path.isfile(self.buildfile):
             if not os.path.isfile(self.buildfile_copy):
@@ -909,6 +940,40 @@ def compile_model(self, target=None, **kwargs):
                 kwargs.setdefault(k, v)
             return super(CMakeModelDriver, self).compile_model(**kwargs)
 
+    @classmethod
+    def prune_sh_gcc(cls, path, gcc):  # pragma: appveyor
+        r"""Remove instances of sh.exe from the path that are not
+        associated with the selected gcc compiler. This can happen
+        on windows when rtools or git install a version of sh.exe
+        that is added to the path before the compiler.
+
+        Args:
+            path (str): Contents of the path variable.
+            gcc (str): Full path to the gcc executable.
+
+        Returns:
+            str: Modified path that removes the extra instances
+                of sh.exe.
+
+        """
+        # This method is not covered because it is not called on
+        # github actions where bash is always present
+        sh_path = shutil.which('sh', path=path)
+        while sh_path:
+            for k in ['rtools', 'git']:
+                if k in sh_path.lower():
+                    break
+            else:  # pragma: debug
+                break
+            if k not in gcc.lower():
+                paths = path.split(os.pathsep)
+                paths.remove(os.path.dirname(sh_path))
+                path = os.pathsep.join(paths)
+                sh_path = shutil.which('sh', path=path)
+            else:  # pragma: debug
+                break
+        return path
+
     @classmethod
     def update_compiler_kwargs(cls, **kwargs):
         r"""Update keyword arguments supplied to the compiler get_flags method
@@ -925,36 +990,26 @@ def update_compiler_kwargs(cls, **kwargs):
         """
         if platform._is_win and (kwargs.get('target_compiler', None)
                                  in ['gcc', 'g++', 'gfortran']):  # pragma: windows
-            # Remove sh from path for compilation when the rtools
-            # version of sh is on the path, but the gnu compiler being
-            # used is not part of that installation.
             gcc = get_compilation_tool('compiler',
                                        kwargs['target_compiler'],
                                        None)
             if gcc:
-                path = kwargs['env']['PATH']
-                gcc_path = gcc.get_executable(full_path=True)
-                sh_path = shutil.which('sh', path=path)
-                while sh_path:
-                    for k in ['rtools', 'git']:
-                        if k in sh_path.lower():
-                            break
-                    else:  # pragma: debug
-                        break
-                    if k not in gcc_path.lower():
-                        paths = path.split(os.pathsep)
-                        paths.remove(os.path.dirname(sh_path))
-                        path = os.pathsep.join(paths)
-                    else:  # pragma: debug
-                        break
-                    sh_path = shutil.which('sh', path=path)
+                path = cls.prune_sh_gcc(
+                    kwargs['env']['PATH'],
+                    gcc.get_executable(full_path=True))
                 kwargs['env']['PATH'] = path
-                if not sh_path:
+                if not shutil.which('sh', path=path):  # pragma: appveyor
+                    # This will not be run on Github actions where
+                    # the shell is always set
                     kwargs.setdefault('generator', 'MinGW Makefiles')
+                elif shutil.which('make', path=path):
+                    kwargs.setdefault('generator', 'Unix Makefiles')
                 # This is not currently tested
                 # else:
                 #     kwargs.setdefault('generator', 'MSYS Makefiles')
         out = super(CMakeModelDriver, cls).update_compiler_kwargs(**kwargs)
+        out.setdefault('definitions', [])
+        out['definitions'].append('PYTHON_EXECUTABLE=%s' % sys.executable)
         if CModelDriver._osx_sysroot is not None:
             out.setdefault('definitions', [])
             out['definitions'].append(
diff --git a/yggdrasil/drivers/CModelDriver.py b/yggdrasil/drivers/CModelDriver.py
index 43970c4b0..e9ea6e347 100755
--- a/yggdrasil/drivers/CModelDriver.py
+++ b/yggdrasil/drivers/CModelDriver.py
@@ -147,11 +147,16 @@ def get_search_path(cls, *args, **kwargs):
             list: List of paths that the tools will search.
 
         """
+        cfg = kwargs.get('cfg', ygg_cfg)
+        libtype = kwargs.get('libtype', None)
         out = super(CompilerBase, cls).get_search_path(*args, **kwargs)
-        if platform._is_mac and (_osx_sysroot is not None):
-            base_path = os.path.join(_osx_sysroot, 'usr')
-            assert(kwargs.get('libtype', None) == 'include')
-            out.append(os.path.join(base_path, 'include'))
+        macos_sdkroot = cfg.get('c', 'macos_sdkroot', _osx_sysroot)
+        if platform._is_mac and (macos_sdkroot is not None):
+            base_path = os.path.join(macos_sdkroot, 'usr')
+            assert(libtype == 'include')
+            new = os.path.join(base_path, 'include')
+            if new not in out:
+                out.append(new)
         return out
 
 
@@ -285,6 +290,23 @@ class MSVCCompiler(CCompilerBase):
                              search_path_flags=None)
     toolset = 'msvc'
     
+    @staticmethod
+    def before_registration(cls):
+        r"""Operations that should be performed to modify class attributes prior
+        to registration including things like platform dependent properties and
+        checking environment variables for default settings.
+        """
+        compiler_path = shutil.which('cl.exe')
+        linker_path = shutil.which('link.exe')
+        if (((compiler_path and linker_path)
+             and (os.path.dirname(compiler_path)
+                  != os.path.dirname(linker_path)))):
+            cls.linker_attributes['default_executable'] = os.path.join(
+                os.path.dirname(compiler_path), os.path.basename(linker_path))
+            assert(os.path.isfile(
+                cls.linker_attributes['default_executable']))
+        CCompilerBase.before_registration(cls)
+        
     @classmethod
     def tool_version(cls, **kwargs):  # pragma: windows
         r"""Determine the version of this tool.
@@ -384,7 +406,11 @@ def get_flags(cls, *args, **kwargs):
         match = re.search(regex, out)
         if (match is not None) and (int(match.group('version')) >= 10):
             ld_version = LDLinker.tool_version()
-            if float(ld_version) < 520:
+            if float(ld_version.split('.')[0]) < 520:  # pragma: version
+                # No longer covered as the default conda
+                # install no longer causes this configuration
+                # to occur, but this will not be deprecated
+                # as existing installs still have this mismatch
                 kwargs['linker-version'] = ld_version
         out = super(ClangLinker, cls).get_flags(*args, **kwargs)
         if '-lstdc++' not in out:
@@ -468,8 +494,8 @@ class MSVCArchiver(ArchiverBase):
     if (_python_lib is None) or (not os.path.isfile(_python_lib)):  # pragma: no cover
         _python_lib = tools.get_python_c_library(
             allow_failure=False, libtype=libtype_order[0])
-except BaseException:  # pragma: debug
-    warnings.warn("ERROR LOCATING PYTHON LIBRARY")
+except BaseException as e:  # pragma: debug
+    warnings.warn("ERROR LOCATING PYTHON LIBRARY: %s" % e)
     _python_lib = None
 _numpy_inc = numpy_distutils.misc_util.get_numpy_include_dirs()
 _numpy_lib = None  # os.path.join(os.path.dirname(_numpy_inc[0]), 'lib', 'npymath.lib')
@@ -753,7 +779,7 @@ def after_registration(cls, **kwargs):
                     cls.internal_libraries[x]['compiler_flags'].append('-fPIC')
         
     @classmethod
-    def configure(cls, cfg, macos_sdkroot=None):
+    def configure(cls, cfg, macos_sdkroot=None, vcpkg_dir=None):
         r"""Add configuration options for this language. This includes locating
         any required external libraries and setting option defaults.
 
@@ -762,12 +788,35 @@ def configure(cls, cfg, macos_sdkroot=None):
             macos_sdkroot (str, optional): Full path to the root directory for
                 the MacOS SDK that should be used. Defaults to None and is
                 ignored.
+            vcpkg_dir (str, optional): Full path to the root directory containing
+                a vcpkg installation. This should be the directory that contains
+                the vcpkg executable and any packages installed by vcpkg (in
+                subdirectories). Defaults to None and is ignored.
 
         Returns:
             list: Section, option, description tuples for options that could not
                 be set.
 
         """
+        # Set vcpkg_dir & macos_sdkroot before calling parent so that it can be
+        # used in get_search_path when searching for dependencies
+        if (cls.language is not None) and (not cfg.has_section(cls.language)):
+            cfg.add_section(cls.language)
+        if vcpkg_dir is None:
+            vcpkg_dir = os.environ.get('VCPKG_ROOT', None)
+        if vcpkg_dir is not None:
+            vcpkg_dir = os.path.abspath(vcpkg_dir)
+            if not os.path.isdir(vcpkg_dir):  # pragma: debug
+                raise ValueError("Path to vcpkg root directory "
+                                 "does not exist: %s." % vcpkg_dir)
+            cfg.set(cls._language, 'vcpkg_dir', vcpkg_dir)
+        if macos_sdkroot is None:
+            macos_sdkroot = _osx_sysroot
+        if macos_sdkroot is not None:
+            if not os.path.isdir(macos_sdkroot):  # pragma: debug
+                raise ValueError("Path to MacOS SDK root directory "
+                                 "does not exist: %s." % macos_sdkroot)
+            cfg.set(cls._language, 'macos_sdkroot', macos_sdkroot)
         # Call __func__ to avoid direct invoking of class which dosn't exist
         # in after_registration where this is called
         out = CompiledModelDriver.configure.__func__(cls, cfg)
@@ -780,13 +829,6 @@ def configure(cls, cfg, macos_sdkroot=None):
         if (nplib is not None) and os.path.isfile(nplib):
             cfg.set(cls._language, 'numpy_include',
                     os.path.dirname(os.path.dirname(nplib)))
-        if macos_sdkroot is None:
-            macos_sdkroot = _osx_sysroot
-        if macos_sdkroot is not None:
-            if not os.path.isdir(macos_sdkroot):  # pragma: debug
-                raise ValueError("Path to MacOS SDK root directory "
-                                 "does not exist: %s." % macos_sdkroot)
-            cfg.set(cls._language, 'macos_sdkroot', macos_sdkroot)
         return out
 
     @classmethod
@@ -850,7 +892,7 @@ def call_linker(cls, obj, language=None, **kwargs):
     @classmethod
     def update_ld_library_path(cls, env, paths_to_add=None,
                                add_to_front=False, add_libpython_dir=False,
-                               toolname=None, **kwargs):
+                               toolname=None, env_var=None, **kwargs):
         r"""Update provided dictionary of environment variables so that
         LD_LIBRARY_PATH includes the interface directory containing the interface
         libraries.
@@ -867,6 +909,9 @@ def update_ld_library_path(cls, env, paths_to_add=None,
             toolname (str, optional): Name of compiler tool that should be used.
                 Defaults to None and the default compiler for the language will
                 be used.
+            env_var (str, optional): Environment variable where the paths
+                should be added. Defaults to None and is only set for
+                linux (LD_LIBRARY_PATH) and windows (PATH).
             **kwargs: Additional keyword arguments are ignored.
 
         Returns:
@@ -879,11 +924,19 @@ def update_ld_library_path(cls, env, paths_to_add=None,
         if add_libpython_dir:
             paths_to_add = paths_to_add + [os.path.dirname(
                 cls.get_dependency_library('python', toolname=toolname))]
-        env_var = None
-        if platform._is_linux:
-            env_var = 'LD_LIBRARY_PATH'
-        elif platform._is_win:
-            env_var = 'PATH'
+        if platform._is_win and ygg_cfg.get('c', 'vcpkg_dir', None):
+            if platform._is_64bit:
+                arch = 'x64-windows'
+            else:  # pragma: debug
+                arch = 'x86-windows'
+                raise NotImplementedError("Not yet tested on 32bit Python")
+            paths_to_add.append(os.path.join(ygg_cfg.get('c', 'vcpkg_dir'),
+                                             'installed', arch, 'bin'))
+        if env_var is None:
+            if platform._is_linux:
+                env_var = 'LD_LIBRARY_PATH'
+            elif platform._is_win:
+                env_var = 'PATH'
         if env_var is not None:
             path_list = []
             prev_path = env.pop(env_var, '')
diff --git a/yggdrasil/drivers/CPPModelDriver.py b/yggdrasil/drivers/CPPModelDriver.py
index fcb814f2d..ef5e52b4f 100644
--- a/yggdrasil/drivers/CPPModelDriver.py
+++ b/yggdrasil/drivers/CPPModelDriver.py
@@ -108,7 +108,7 @@ class CPPModelDriver(CModelDriver):
     language = 'c++'
     language_ext = ['.cpp', '.CPP', '.cxx', '.C', '.c++', '.cc', '.cp', '.tcc',
                     '.hpp', '.HPP', '.hxx', '.H', '.h++', '.hh', '.hp', '.h']
-    language_aliases = ['cpp']
+    language_aliases = ['cpp', 'cxx']
     base_languages = ['c']
     interface_library = 'ygg++'
     # To prevent inheritance
diff --git a/yggdrasil/drivers/CompiledModelDriver.py b/yggdrasil/drivers/CompiledModelDriver.py
index 6422c08e4..34e0f2773 100644
--- a/yggdrasil/drivers/CompiledModelDriver.py
+++ b/yggdrasil/drivers/CompiledModelDriver.py
@@ -4,7 +4,6 @@
 import copy
 import glob
 import logging
-import warnings
 import subprocess
 import shutil
 from collections import OrderedDict
@@ -757,7 +756,7 @@ def get_env_prefixes(cls):
         return tools.get_env_prefixes()
             
     @classmethod
-    def get_search_path(cls, env_only=False, libtype=None):
+    def get_search_path(cls, env_only=False, libtype=None, cfg=None):
         r"""Determine the paths searched by the tool for external library files.
 
         Args:
@@ -766,11 +765,16 @@ def get_search_path(cls, env_only=False, libtype=None):
                 Defaults to False.
             libtype (str, optional): Library type being searched for.
                 Defaults to None.
+            cfg (YggConfigParser, optional): Configuration object currently
+                being updated. Defaults to the global configuration.
 
         Returns:
             list: List of paths that the tools will search.
 
         """
+        if cfg is None:
+            from yggdrasil.config import ygg_cfg
+            cfg = ygg_cfg
         if (cls.search_path_flags is None) and (cls.search_path_envvar is None):
             raise NotImplementedError("get_search_path method not implemented for "
                                       "%s tool '%s'" % (cls.tooltype, cls.toolname))
@@ -789,10 +793,7 @@ def get_search_path(cls, env_only=False, libtype=None):
         if (cls.search_path_envvar is not None) and (not env_only):
             assert(isinstance(cls.search_path_envvar, list))
             for ienv in cls.search_path_envvar:
-                ienv_paths = os.environ.get(ienv, '').split(os.pathsep)
-                for x in ienv_paths:
-                    if x:
-                        paths.append(x)
+                paths += os.environ.get(ienv, '').split(os.pathsep)
         # Get flags based on path
         if (cls.search_path_flags is not None) and (not env_only):
             output = cls.call(cls.search_path_flags, skip_flags=True,
@@ -804,20 +805,36 @@ def get_search_path(cls, env_only=False, libtype=None):
                 output = re.split(cls.search_regex_end, output)[0]
             # Search for paths
             for r in cls.search_regex:
-                for x in re.findall(r, output):
-                    if os.path.isdir(x):
-                        paths.append(x)
+                paths += re.findall(r, output)
         # Get search paths from the virtualenv/conda environment
         if (cls.search_path_env is not None):
             for iprefix in cls.get_env_prefixes():
                 assert(isinstance(cls.search_path_env, list))
                 for ienv in cls.search_path_env:
-                    ienv_path = os.path.join(iprefix, ienv)
-                    if (ienv_path not in paths) and os.path.isdir(ienv_path):
-                        paths.append(ienv_path)
+                    paths.append(os.path.join(iprefix, ienv))
         # Get libtype specific search paths
         if platform._is_win:  # pragma: windows
             base_paths = []
+            vcpkg_dir = cfg.get('c', 'vcpkg_dir', None)
+            logger.info("vcpkg_dir = '%s'" % vcpkg_dir)
+            if vcpkg_dir is not None:
+                if not os.path.isdir(vcpkg_dir):  # pragma: debug
+                    raise RuntimeError("vcpkg_dir is not valid: '%s'"
+                                       % vcpkg_dir)
+                typ2dir = {'include': 'include',
+                           'shared': 'bin',
+                           'static': 'lib'}
+                if platform._is_64bit:
+                    arch = 'x64-windows'
+                else:  # pragma: debug
+                    arch = 'x86-windows'
+                    raise NotImplementedError("Not yet tested on 32bit Python")
+                if (libtype in typ2dir) and os.path.isdir(vcpkg_dir):
+                    paths.append(os.path.join(vcpkg_dir, 'installed', arch,
+                                              typ2dir[libtype]))
+                    assert(os.path.isdir(paths[-1]))
+            if os.environ.get('ChocolateyInstall'.upper(), None):
+                base_paths.append(os.environ['ChocolateyInstall'])
         else:
             base_paths = ['/usr', os.path.join('/usr', 'local')]
         if platform._is_mac:
@@ -828,7 +845,11 @@ def get_search_path(cls, env_only=False, libtype=None):
             suffix = 'lib'
         for base in base_paths:
             paths.append(os.path.join(base, suffix))
-        return paths
+        out = []
+        for x in paths:
+            if x and (x not in out) and os.path.isdir(x):
+                out.append(x)
+        return out
 
     @classmethod
     def get_executable_command(cls, args, skip_flags=False, unused_kwargs=None,
@@ -1558,7 +1579,7 @@ def before_registration(cls):
             cls.library_ext = '.dll'
             cls.executable_ext = '.exe'
             cls.search_path_env += [
-                'DLLs', os.path.join('library', 'bin')]
+                'DLLs', os.path.join('library', 'bin'), 'Library']
             cls.all_library_ext = ['.dll', '.lib', '.dll.a']
         elif platform._is_mac:
             # TODO: Dynamic library by default on windows?
@@ -1838,7 +1859,8 @@ def before_registration(cls):
             setattr(cls, k, None)
         if platform._is_win:  # pragma: windows
             cls.library_ext = '.lib'
-            cls.search_path_env = [os.path.join('library', 'lib')]
+            cls.search_path_env = [os.path.join('library', 'lib'),
+                                   'Library']
         else:
             cls.library_ext = '.a'
 
@@ -2053,9 +2075,9 @@ def after_registration(cls, **kwargs):
                 if (((default_tool is None)
                      or (not default_tool.is_installed()))):  # pragma: debug
                     if not tools.is_subprocess():
-                        warnings.warn(('Default %s for %s (%s) not installed. '
-                                       'Attempting to locate an alternative .')
-                                      % (k, cls.language, default_tool_name))
+                        logger.debug(('Default %s for %s (%s) not installed. '
+                                      'Attempting to locate an alternative .')
+                                     % (k, cls.language, default_tool_name))
                     setattr(cls, 'default_%s' % k, None)
 
     def parse_arguments(self, args, **kwargs):
@@ -2278,7 +2300,13 @@ def get_tool_static(cls, tooltype, toolname=None,
                             'archiver', return_prop='name', default=None),
                         archiver_flags=cls.get_tool(
                             'archiver', return_prop='flags', default=None))
-                out = get_compatible_tool(toolname, tooltype, cls.language)(**kwargs)
+                try:
+                    out = get_compatible_tool(toolname, tooltype,
+                                              cls.language)(**kwargs)
+                except ValueError:
+                    if default is False:
+                        raise
+                    return default
             else:
                 out_tool = cls.get_tool('compiler', toolname=toolname, default=None)
                 if out_tool is None:  # pragma: debug
@@ -2589,7 +2617,7 @@ def get_dependency_library(cls, dep, default=None, libtype=None,
                                       "libraries of types %s were found.")
                                      % (libtype, dep, libtype_found))
             # TODO: CLEANUP
-            if platform._is_win and out.endswith('.lib'):  # pragma: windows
+            if platform._is_win and out and out.endswith('.lib'):  # pragma: windows
                 if tool is None:
                     tool = cls.get_tool('compiler', language=dep_lang,
                                         toolname=toolname)
@@ -2608,7 +2636,7 @@ def get_dependency_library(cls, dep, default=None, libtype=None,
             if (((libinfo.get('libtype', None) == 'windows_import')
                  and (libtype == 'static'))):
                 # Name import lib using dll
-                import_lib = True
+                import_lib = (tool.toolname == 'cl')
                 libtype = 'shared'
             out = tool.get_output_file(dep, libtype=libtype, no_src_ext=True,
                                        build_library=True,
@@ -2969,6 +2997,8 @@ def update_linker_kwargs(cls, for_api=False, for_model=False, commtype=None,
                 if (((not kwargs.get('dry_run', False))
                      and (not os.path.isfile(dep_lib)))):  # pragma: debug
                     if dep in internal_dependencies:
+                        # If this is called recursively, verify that dep_lib is produced
+                        # by compiling dep.
                         cls.compile_dependencies(toolname=toolname)
                     if not os.path.isfile(dep_lib):
                         raise RuntimeError(
@@ -3181,9 +3211,9 @@ def configure_executable_type(cls, cfg):
             if default_tool_name:
                 default_tool = get_compilation_tool(k, default_tool_name)
                 if not default_tool.is_installed():  # pragma: debug
-                    warnings.warn(('Default %s for %s (%s) not installed. '
-                                   'Attempting to locate an alternative .')
-                                  % (k, cls.language, default_tool_name))
+                    logger.debug(('Default %s for %s (%s) not installed. '
+                                  'Attempting to locate an alternative .')
+                                 % (k, cls.language, default_tool_name))
                     default_tool_name = None
             # Determine compilation tools based on language/platform
             if default_tool_name is None:  # pragma: no cover
@@ -3222,7 +3252,7 @@ def configure_library(cls, cfg, k):
         r"""Add configuration options for an external library.
 
         Args:
-            cfg (CisConfigParser): Config class that options should be set for.
+            cfg (YggConfigParser): Config class that options should be set for.
         
         Returns:
             list: Section, option, description tuples for options that could not
@@ -3259,16 +3289,20 @@ def configure_library(cls, cfg, k):
                 tool = None
                 try:
                     if t == 'include':
-                        tool = cls.get_tool('compiler', default=None)
+                        tool = cls.get_tool('compiler', default=None,
+                                            language=v.get('language', None))
                     elif t == 'shared':
-                        tool = cls.get_tool('linker', default=None)
+                        tool = cls.get_tool('linker', default=None,
+                                            language=v.get('language', None))
                     else:  # pragma: completion
-                        tool = cls.get_tool('archiver', default=None)
+                        tool = cls.get_tool('archiver', default=None,
+                                            language=v.get('language', None))
                 except NotImplementedError:  # pragma: debug
                     pass
                 fpath = None
+                fname = '*'.join(os.path.splitext(fname))
                 if tool is not None:
-                    search_list = tool.get_search_path(libtype=t)
+                    search_list = tool.get_search_path(libtype=t, cfg=cfg)
                     # On windows search for both gnu and msvc library
                     # naming conventions
                     if platform._is_win:  # pragma: windows
@@ -3297,7 +3331,7 @@ def configure_library(cls, cfg, k):
                 #         os.symlink(fpath_orig, fpath)
                 cfg.set(k_lang, opt, fpath)
             else:
-                logger.info('Could not locate %s (search_list = %s)'
+                logger.info('Could not locate %s (search_list = \n\t%s)'
                             % (fname, '\n\t'.join(search_list)))
                 out.append((k_lang, opt, desc))
         return out
@@ -3382,8 +3416,10 @@ def set_env(self, for_compile=False, compile_kwargs=None, toolname=None,
         return out
         
     @classmethod
-    def compile_dependencies(cls, toolname=None, **kwargs):
+    def compile_dependencies(cls, toolname=None, dep=None, **kwargs):
         r"""Compile any required internal libraries, including the interface."""
+        if dep is None:
+            dep = cls.interface_library
         kwargs.setdefault('products', [])
         base_libraries = []
         compiler = cls.get_tool('compiler', toolname=toolname)
@@ -3394,11 +3430,8 @@ def compile_dependencies(cls, toolname=None, **kwargs):
             base_cls = import_component('model', x)
             base_libraries.append(base_cls.interface_library)
             base_cls.compile_dependencies(toolname=toolname, **kwargs)
-        if (((cls.interface_library is not None) and cls.is_installed()
-             and (cls.interface_library not in base_libraries))):
-            # cls.call_compiler(cls.interface_library)
-            dep_order = cls.get_dependency_order(cls.interface_library,
-                                                 toolname=toolname)
+        if (dep is not None) and cls.is_installed() and (dep not in base_libraries):
+            dep_order = cls.get_dependency_order(dep, toolname=toolname)
             for k in dep_order[::-1]:
                 if isinstance(k, tuple):
                     assert(len(k) == 2)
@@ -3409,7 +3442,7 @@ def compile_dependencies(cls, toolname=None, **kwargs):
                     cls.call_compiler(k, toolname=toolname, **kwargs)
 
     @classmethod
-    def cleanup_dependencies(cls, products=None, **kwargs):
+    def cleanup_dependencies(cls, products=None, verbose=False, **kwargs):
         r"""Cleanup dependencies."""
         if products is None:
             products = []
@@ -3425,7 +3458,8 @@ def cleanup_dependencies(cls, products=None, **kwargs):
                 if suffix in products[i]:
                     new_products += glob.glob(products[i].replace(suffix, '*'))
             products += new_products
-        super(CompiledModelDriver, cls).cleanup_dependencies(products=products)
+        super(CompiledModelDriver, cls).cleanup_dependencies(
+            products=products, verbose=verbose)
 
     def compile_model(self, source_files=None, skip_interface_flags=False,
                       **kwargs):
diff --git a/yggdrasil/drivers/ConnectionDriver.py b/yggdrasil/drivers/ConnectionDriver.py
index 731b19b94..12e8c48bb 100755
--- a/yggdrasil/drivers/ConnectionDriver.py
+++ b/yggdrasil/drivers/ConnectionDriver.py
@@ -579,12 +579,16 @@ def cleanup(self):
         super(ConnectionDriver, self).cleanup()
 
     @run_remotely
-    def printStatus(self, beg_msg='', end_msg=''):
+    def printStatus(self, beg_msg='', end_msg='',
+                    verbose=False):
         r"""Print information on the status of the ConnectionDriver.
 
         Arguments:
             beg_msg (str, optional): Additional message to print at beginning.
             end_msg (str, optional): Additional message to print at end.
+            verbose (bool, optional): If True, the status of
+                individual comms will be displayed. Defaults to
+                False.
 
         """
         msg = beg_msg
@@ -603,6 +607,9 @@ def printStatus(self, beg_msg='', end_msg=''):
                 msg += '%-30s' % ('close state: ' + self.close_state)
         msg += end_msg
         print(msg)
+        if verbose:
+            self.icomm.printStatus()
+            self.ocomm.printStatus()
 
     @run_remotely
     def confirm_input(self, timeout=None):
diff --git a/yggdrasil/drivers/FortranModelDriver.py b/yggdrasil/drivers/FortranModelDriver.py
index 065bc6587..c945d39db 100644
--- a/yggdrasil/drivers/FortranModelDriver.py
+++ b/yggdrasil/drivers/FortranModelDriver.py
@@ -381,16 +381,19 @@ def before_registration(cls):
             if cxx_lib not in cls.external_libraries:
                 cls.external_libraries[cxx_lib] = cxx_orig.copy()
                 cls.internal_libraries['fygg']['external_dependencies'].append(cxx_lib)
+                # if platform._is_win:  # pragma: windows
+                #     cls.external_libraries[cxx_lib]['libtype'] = 'windows_import'
         if platform._is_win:  # pragma: windows
             cl_compiler = get_compilation_tool('compiler', 'cl')
             if not cl_compiler.is_installed():  # pragma: debug
-                logger.info("The MSVC compiler could not be located. The Python C API "
-                            "assumes the MSVC CRT will be used on windows so there may "
-                            "be errors when accessing some behavior of the Python C API."
-                            "In particular, segfaults are known to occur when trying to "
-                            "call display_python due to differences in the internal "
-                            "structure of FILE* objects between MSVC and other "
-                            "standards.")
+                logger.info(
+                    "The MSVC compiler could not be located. The Python C API "
+                    "assumes the MSVC CRT will be used on windows so there may "
+                    "be errors when accessing some behavior of the Python C API. "
+                    "In particular, segfaults are known to occur when trying to "
+                    "call display_python due to differences in the internal "
+                    "structure of FILE* objects between MSVC and other "
+                    "standards.")
 
     @classmethod
     def set_env_class(cls, **kwargs):
diff --git a/yggdrasil/drivers/InterpretedModelDriver.py b/yggdrasil/drivers/InterpretedModelDriver.py
index ceedfbcf0..fd29c5abc 100644
--- a/yggdrasil/drivers/InterpretedModelDriver.py
+++ b/yggdrasil/drivers/InterpretedModelDriver.py
@@ -167,7 +167,7 @@ def language_executable(cls):
         return cls.get_interpreter()
 
     @classmethod
-    def executable_command(cls, args, exec_type='interpreter', unused_kwargs={},
+    def executable_command(cls, args, exec_type='interpreter', unused_kwargs=None,
                            skip_interpreter_flags=False, **kwargs):
         r"""Compose a command for running a program in this language with the
         provied arguments. If not already present, the interpreter command and
@@ -207,7 +207,8 @@ def executable_command(cls, args, exec_type='interpreter', unused_kwargs={},
                 args = new_args + args
         elif exec_type != 'direct':
             raise ValueError("Invalid exec_type '%s'" % exec_type)
-        unused_kwargs.update(kwargs)
+        if isinstance(unused_kwargs, dict):
+            unused_kwargs.update(kwargs)
         return args
 
     @classmethod
@@ -226,14 +227,14 @@ def is_interpreter(cls, cmd):
                and (not any([cmd.endswith(e) for e in cls.language_ext])))
         return out
 
-    def set_env(self):
+    def set_env(self, **kwargs):
         r"""Get environment variables that should be set for the model process.
 
         Returns:
             dict: Environment variables for the model process.
 
         """
-        out = super(InterpretedModelDriver, self).set_env()
+        out = super(InterpretedModelDriver, self).set_env(**kwargs)
         if self.path_env_variable is not None:  # pragma: debug
             if self.language != 'matlab':
                 raise NotImplementedError(
diff --git a/yggdrasil/drivers/MakeModelDriver.py b/yggdrasil/drivers/MakeModelDriver.py
index 8c511f90a..772f6f91b 100644
--- a/yggdrasil/drivers/MakeModelDriver.py
+++ b/yggdrasil/drivers/MakeModelDriver.py
@@ -226,7 +226,7 @@ def set_env(cls, logging_level=None, language=None, language_driver=None,
         compiler = language_driver.get_tool('compiler', toolname=language_compiler)
         compile_flags = language_driver.get_compiler_flags(
             for_model=True, skip_defaults=True, dont_skip_env_defaults=True,
-            logging_level=logging_level, dont_link=True, toolnane=language_compiler)
+            logging_level=logging_level, dont_link=True, toolname=language_compiler)
         linker = language_driver.get_tool('linker', toolname=language_compiler)
         linker_flags = language_driver.get_linker_flags(
             for_model=True, skip_defaults=True, dont_skip_env_defaults=True,
diff --git a/yggdrasil/drivers/MatlabModelDriver.py b/yggdrasil/drivers/MatlabModelDriver.py
index 019b092a3..59bbf6523 100755
--- a/yggdrasil/drivers/MatlabModelDriver.py
+++ b/yggdrasil/drivers/MatlabModelDriver.py
@@ -763,12 +763,15 @@ def executable_command(cls, args, **kwargs):
         return super(MatlabModelDriver, cls).executable_command(args, **kwargs)
     
     @classmethod
-    def configure(cls, cfg):
+    def configure(cls, cfg, disable_engine=None):
         r"""Add configuration options for this language. This includes locating
         any required external libraries and setting option defaults.
 
         Args:
             cfg (YggConfigParser): Config class that options should be set for.
+            disable_engine (bool, optional): Disable the use of the
+                Python engine for Matlab. If not provided, this
+                argument will be ignored.
 
         Returns:
             list: Section, option, description tuples for options that could not
@@ -795,6 +798,8 @@ def configure(cls, cfg):
                     cfg.set(cls.language, k, opts[k][1])
                 else:
                     out.append((cls.language, k, opts[k][0]))
+        if disable_engine is not None:
+            cfg.set(cls._language, 'disable_engine', str(disable_engine))
         return out
 
     @classmethod
diff --git a/yggdrasil/drivers/ModelDriver.py b/yggdrasil/drivers/ModelDriver.py
index beff991b2..4187a2f51 100755
--- a/yggdrasil/drivers/ModelDriver.py
+++ b/yggdrasil/drivers/ModelDriver.py
@@ -269,7 +269,7 @@ class ModelDriver(Driver):
                          'schema_table_model_subtype_rst>`.')},
         'args': {'type': 'array',
                  'items': {'type': 'string'}},
-        'inputs': {'type': 'array', 'default': [{'name': 'default'}],
+        'inputs': {'type': 'array', 'default': [],
                    'items': {'$ref': '#/definitions/comm'},
                    'description': (
                        'A mapping object containing the entry for a '
@@ -280,7 +280,7 @@ class ModelDriver(Driver):
                        'entries and the options available for '
                        'channels can be found :ref:`here<'
                        'yaml_comm_options>`.')},
-        'outputs': {'type': 'array', 'default': [{'name': 'default'}],
+        'outputs': {'type': 'array', 'default': [],
                     'items': {'$ref': '#/definitions/comm'},
                     'description': (
                         'A mapping object containing the entry for a '
@@ -291,6 +291,8 @@ class ModelDriver(Driver):
                         'entries and the options available for '
                         'channels can be found :ref:`here<'
                         'yaml_comm_options>`.')},
+        'env': {'type': 'object', 'default': {},
+                'additional_properties': {'type': 'string'}},
         'products': {'type': 'array', 'default': [],
                      'items': {'type': 'string'}},
         'source_products': {'type': 'array', 'default': [],
@@ -742,7 +744,8 @@ def run_executable(cls, args, return_process=False, debug_flags=None,
                 return proc
             out, err = proc.communicate()
             if proc.returncode != 0:
-                logger.error(out)
+                logger.info('%s' % out)
+                logger.info('%s' % err)
                 raise RuntimeError("Command '%s' failed with code %d."
                                    % (' '.join(cmd), proc.returncode))
             out = out.decode("utf-8")
@@ -836,18 +839,26 @@ def is_installed(cls):
                 and cls.is_configured() and (not cls.is_disabled()))
 
     @classmethod
-    def are_base_languages_installed(cls):
+    def are_base_languages_installed(cls, missing=None):
         r"""Determine if the base languages are installed.
 
+        Args:
+            missing (list, optional): A pre-existing list that
+                missing base languages should be appended to.
+
         Returns:
             bool: True if the base langauges are installed. False otherwise.
 
         """
         out = True
         for x in cls.base_languages:
-            if not out:  # pragma: no cover
+            if (not out) and (not isinstance(missing, list)):  # pragma: no cover
                 break
             out = import_component('model', x).is_installed()
+            if isinstance(missing, list) and (not out):
+                missing.append(x)
+        if missing:
+            out = False
         return out
 
     @classmethod
@@ -1047,6 +1058,11 @@ def is_comm_installed(cls, commtype=None, skip_config=False, **kwargs):
             for lib in libraries:
                 if not cls.is_library_installed(lib, **kwargs):
                     return False
+        # Check for server on RabbitMQ
+        if commtype in ['rmq', 'rmq_async']:
+            from yggdrasil.communication.RMQComm import check_rmq_server
+            if not check_rmq_server():
+                return False
         return True
     
     @classmethod
@@ -1168,6 +1184,9 @@ def set_env(self, existing=None, **kwargs):
             env['YGG_SERVER_OUTPUT'] = self.is_server['output']
         if self.logging_level:
             env['YGG_MODEL_DEBUG'] = self.logging_level
+        replace = [k for k in env.keys() if ':' in k]
+        for k in replace:
+            env[k.replace(':', '__COLON__')] = env.pop(k)
         return env
 
     def before_start(self, no_queue_thread=False, **kwargs):
@@ -1362,11 +1381,12 @@ def remove_products(self):
         remove_products(products, source_products, timer_class=self)
             
     @classmethod
-    def cleanup_dependencies(cls, products=[]):
+    def cleanup_dependencies(cls, products=[], verbose=False):
         r"""Cleanup dependencies."""
         for x in products:
             if os.path.isfile(x):
-                print("Removing %s" % x)
+                if verbose:  # pragma: debug
+                    print("Removing %s" % x)
                 os.remove(x)
                 
     # Methods for automated model wrapping
diff --git a/yggdrasil/drivers/OSRModelDriver.py b/yggdrasil/drivers/OSRModelDriver.py
index ac1d24f65..0c4d83bd2 100644
--- a/yggdrasil/drivers/OSRModelDriver.py
+++ b/yggdrasil/drivers/OSRModelDriver.py
@@ -51,6 +51,7 @@ class OSRModelDriver(ExecutableModelDriver):
     language = 'osr'
     language_ext = '.xml'
     base_languages = ['cpp']
+    interface_dependencies = ['make']
     repository = None
     executable_path = None
     repository_url = "https://gitlab.com/langmm/OpenSimRoot.git"
@@ -94,21 +95,45 @@ def parse_arguments(self, *args, **kwargs):
                 os.path.basename(self.model_file))
         # if not (isinstance(self.executable_path, str)
         #         and os.path.isfile(self.executable_path)):
-        self.compile_osr()
+        self.compile_dependencies()
         assert(os.path.isfile(self.executable_path))
 
     @classmethod
-    def compile_osr(cls, target='OpenSimRootYgg'):
+    def is_library_installed(cls, lib, **kwargs):
+        r"""Determine if a dependency is installed.
+
+        Args:
+            lib (str): Name of the library that should be checked.
+            **kwargs: Additional keyword arguments are ignored.
+
+        Returns:
+            bool: True if the library is installed, False otherwise.
+
+        """
+        # Need to treat gnu make as dependency since OSR Makefile is not
+        # compatible with nmake
+        if lib == 'make':
+            return bool(shutil.which('make'))
+        return super(OSRModelDriver, cls).is_library_installed(
+            lib, **kwargs)  # pragma: debug
+
+    @classmethod
+    def compile_dependencies(cls, target='OpenSimRootYgg', toolname=None):
         r"""Compile the OpenSimRoot executable with the yggdrasil flag set.
 
         Args:
-            target (str): Make target that should be build. Defaults to
+            target (str, optional): Make target that should be build. Defaults to
                 'OpenSimRootYgg' (the yggdrasil-instrumented version of the
                 OSR executable).
+            toolname (str, optional): C++ compiler that should be used. Forced
+                to be 'cl.exe' on windows. Otherwise the default C++ compiler will
+                be used.
 
         """
         if (cls.repository is not None) and CPPModelDriver.is_installed():
-            toolname = None
+            if not os.path.isdir(cls.repository):  # pragma: debug
+                # This will only need to be called if the tempdir was cleaned up
+                cls.clone_repository(cls.repository)
             # toolname = CPPModelDriver.get_tool('compiler',
             #                                    return_prop='name',
             #                                    default=None)
@@ -120,18 +145,13 @@ def compile_osr(cls, target='OpenSimRootYgg'):
                 env['YGG_OSR_TOOL'] = toolname
                 if toolname == 'cl':
                     cl_path = shutil.which(toolname + '.exe')
-                    print('cl.exe', shutil.which(toolname), cl_path)
-                    subprocess.check_call(['yggccflags', '--cpp',
-                                           '--toolname=%s' % toolname])
-                    subprocess.check_call(['yggldflags', '--cpp',
-                                           '--toolname=%s' % toolname])
                     if cl_path:
                         msvc_bin = os.path.dirname(cl_path)
                         env['YGG_OSR_CXX'] = cl_path
                         env['YGG_OSR_LINK'] = os.path.join(msvc_bin, 'link.exe')
                         for k in ['CL', '_CL_']:
                             v = os.environ.get(k, None)
-                            if v is not None:
+                            if v is not None:  # pragma: appveyor
                                 env[k] = v.replace('/', '-').replace('\\', '/')
                     else:  # pragma: debug
                         env.pop('YGG_OSR_TOOL')
@@ -239,6 +259,25 @@ def is_language_installed(cls):
 
         """
         return (cls.repository is not None)
+
+    @classmethod
+    def clone_repository(cls, dest=None):
+        r"""Clone the OpenSimRoot repository.
+
+        Args:
+            dest (str, optional): Full path to location where the repository should
+                be cloned. Defaults to '$TEMP/OpenSimRoot'.
+
+        Returns:
+            str: Full path to location where the repository was cloned.
+
+        """
+        if dest is None:
+            dest = os.path.join(tempfile.gettempdir(), 'OpenSimRoot')
+        if not os.path.isdir(dest):  # pragma: config
+            git.Repo.clone_from(cls.repository_url, dest,
+                                branch=cls.repository_branch)
+        return dest
         
     @classmethod
     def configure_executable_type(cls, cfg):
@@ -260,16 +299,14 @@ def configure_executable_type(cls, cfg):
         # if platform._is_win:  # pragma: windows
         #     out.append((cls.language, opt, desc))
         #     return out
-        if not cfg.has_option(cls.language, opt):
+        if (((not cfg.has_option(cls.language, opt))
+             or (not os.path.isdir(cfg.get(cls.language, opt))))):
             fname = 'OpenSimRoot'
             fpath = tools.locate_file(fname)
             if not fpath:
                 logger.info('Could not locate %s, attempting to clone' % fname)
                 try:
-                    fpath = os.path.join(tempfile.gettempdir(), fname)
-                    if not os.path.isdir(fpath):  # pragma: config
-                        git.Repo.clone_from(cls.repository_url, fpath,
-                                            branch=cls.repository_branch)
+                    fpath = cls.clone_repository()
                 except BaseException as e:  # pragma: debug
                     logger.info('Failed to clone from %s. error = %s'
                                 % (cls.repository_url, str(e)))
@@ -334,5 +371,5 @@ def executable_command(cls, args, **kwargs):
     @classmethod
     def cleanup_dependencies(cls, *args, **kwargs):
         r"""Cleanup dependencies."""
-        cls.compile_osr(target='cleanygg')
+        cls.compile_dependencies(target='cleanygg')
         super(OSRModelDriver, cls).cleanup_dependencies(*args, **kwargs)
diff --git a/yggdrasil/drivers/PythonModelDriver.py b/yggdrasil/drivers/PythonModelDriver.py
index 0ad158912..1e7f9c093 100755
--- a/yggdrasil/drivers/PythonModelDriver.py
+++ b/yggdrasil/drivers/PythonModelDriver.py
@@ -58,7 +58,8 @@ class PythonModelDriver(InterpretedModelDriver):
         'and': 'and',
         'indent': 4 * ' ',
         'quote': '\"',
-        'print_generic': 'print({object})',
+        'print_generic': ('from yggdrasil.tools import print_encoded; '
+                          'print_encoded({object})'),
         'print': 'print(\"{message}\")',
         'fprintf': 'print(\"{message}\" % ({variables}))',
         'error': 'raise Exception("{error_msg}")',
@@ -125,29 +126,6 @@ def is_library_installed(cls, lib, **kwargs):
             return False
         return True
 
-    @classmethod
-    def is_comm_installed(cls, **kwargs):
-        r"""Determine if a comm is installed for the associated programming
-        language.
-
-        Args:
-            **kwargs: Additional keyword arguments are passed to the parent
-                class's method.
-
-        Returns:
-            bool: True if a comm is installed for this language.
-
-        """
-        # Call __func__ to avoid direct invoking of class which dosn't exist
-        # in after_registration where this is called
-        out = InterpretedModelDriver.is_comm_installed.__func__(cls, **kwargs)
-        if not kwargs.get('skip_config'):
-            return out
-        if out and (kwargs.get('commtype', None) in ['rmq', 'rmq_async']):
-            from yggdrasil.communication.RMQComm import check_rmq_server
-            out = check_rmq_server()
-        return out
-
     @classmethod
     def format_function_param(cls, key, default=None, **kwargs):
         r"""Return the formatted version of the specified key.
diff --git a/yggdrasil/drivers/RModelDriver.py b/yggdrasil/drivers/RModelDriver.py
index e93bd94d3..ba163cda2 100644
--- a/yggdrasil/drivers/RModelDriver.py
+++ b/yggdrasil/drivers/RModelDriver.py
@@ -137,7 +137,8 @@ def is_library_installed(cls, lib, **kwargs):
             try:
                 cls.run_executable(['-e', 'library(%s)' % lib.split()[0]])
                 cls._library_cache[lib] = True
-            except RuntimeError:
+            except RuntimeError as e:
+                logger.info('Error checking for R library %s: %s' % (lib, e))
                 cls._library_cache[lib] = False
         return cls._library_cache[lib]
         
@@ -166,14 +167,14 @@ def language_version(cls, **kwargs):
     #                 % ' '.join(self.valgrind_flags), '--vanilla', '-f']
     #     return super(RModelDriver, self).debug_flags
         
-    def set_env(self):
+    def set_env(self, **kwargs):
         r"""Get environment variables that should be set for the model process.
 
         Returns:
             dict: Environment variables for the model process.
 
         """
-        out = super(RModelDriver, self).set_env()
+        out = super(RModelDriver, self).set_env(**kwargs)
         out['RETICULATE_PYTHON'] = PythonModelDriver.get_interpreter()
         if CModelDriver.is_language_installed():
             c_linker = CModelDriver.get_tool('linker')
diff --git a/yggdrasil/drivers/SBMLModelDriver.py b/yggdrasil/drivers/SBMLModelDriver.py
index 848db3f97..be49f9c51 100644
--- a/yggdrasil/drivers/SBMLModelDriver.py
+++ b/yggdrasil/drivers/SBMLModelDriver.py
@@ -115,6 +115,7 @@ def model_wrapper(cls, model_file, start_time, steps,
                 v['vars'] = selections
         while True:
             time = None
+            flag = False
             for k, v in input_map.items():
                 flag, value = v['comm'].recv_dict(key_order=v['vars'])
                 if not flag:
diff --git a/yggdrasil/examples/__init__.py b/yggdrasil/examples/__init__.py
index 43d02b86c..726cd5d2b 100644
--- a/yggdrasil/examples/__init__.py
+++ b/yggdrasil/examples/__init__.py
@@ -36,7 +36,8 @@ def register_example(example_dir):
                             'test_%s.py' % example_base)
     if not os.path.isfile(testfile):  # pragma: no cover
         # TODO: Automate test creation
-        if not tools.is_subprocess():
+        if (((not tools.is_subprocess())
+             and (example_base not in ['sbml1', 'sbml2', 'sbml3']))):
             logging.error("Missing test file: %s" % testfile)
     if not os.path.isdir(srcdir):  # pragma: no cover
         if not tools.is_subprocess():
diff --git a/yggdrasil/examples/rpc_lesson3b/Input/input.txt b/yggdrasil/examples/rpc_lesson3b/Input/input.txt
new file mode 100644
index 000000000..d5630eea3
--- /dev/null
+++ b/yggdrasil/examples/rpc_lesson3b/Input/input.txt
@@ -0,0 +1,3 @@
+Hello 1!
+Hello 2!
+Hello 3!
diff --git a/yggdrasil/examples/rpc_lesson3b/client_c.yml b/yggdrasil/examples/rpc_lesson3b/client_c.yml
new file mode 100644
index 000000000..40a79e473
--- /dev/null
+++ b/yggdrasil/examples/rpc_lesson3b/client_c.yml
@@ -0,0 +1,20 @@
+model:
+  name: client
+  language: c
+  args: ./src/client.c
+  function: model_function
+  client_of: server
+  # compiler_flags: [-fopenmp]
+  # linker_flags: [-fopenmp]
+  env:
+    NTHREAD: 10
+  inputs:
+    name: in_buf
+    default_file:
+      name: ./Input/input.txt
+      filetype: ascii
+  outputs:
+    name: out_buf
+    default_file:
+      name: ./client_output.txt
+      in_temp: true
diff --git a/yggdrasil/examples/rpc_lesson3b/rpc_lesson3b.sh b/yggdrasil/examples/rpc_lesson3b/rpc_lesson3b.sh
new file mode 100755
index 000000000..37fc490e1
--- /dev/null
+++ b/yggdrasil/examples/rpc_lesson3b/rpc_lesson3b.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+export YGG_DEBUG="INFO"
+export YGG_NAMESPACE="rpc_lesson3b"
+
+yaml1= 
+yaml2= 
+
+# ----------------Your Commands------------------- #
+case $1 in
+    "" | -p | --python )
+	echo "Running Python"
+	yaml1='server_python.yml'
+	yaml2='client_python.yml'
+	;;
+    -m | --matlab )
+	echo "Running Matlab"
+	yaml1='server_python.yml'
+	yaml2='client_matlab.yml'
+	;;
+    -c | --gcc )
+	echo "Running C"
+	yaml1='server_python.yml'
+	yaml2='client_c.yml'
+	;;
+    --cpp | --g++)
+	echo "Running C++"
+	yaml1='server_python.yml'
+	yaml2='client_cpp.yml'
+	;;
+    -R | -r | --R | --r )
+	echo "Running R"
+	yaml1='server_python.yml'
+	yaml2='client_r.yml'
+	;;
+    -f | --fortran )
+	echo "Running Fortran"
+	yaml1='server_python.yml'
+	yaml2='client_fortran.yml'
+	;;
+esac
+
+yggrun $yaml1 $yaml2
+
+outfile="${TMPDIR}client_output.txt"
+cat $outfile
diff --git a/yggdrasil/examples/rpc_lesson3b/server_python.yml b/yggdrasil/examples/rpc_lesson3b/server_python.yml
new file mode 100644
index 000000000..6a913afbe
--- /dev/null
+++ b/yggdrasil/examples/rpc_lesson3b/server_python.yml
@@ -0,0 +1,6 @@
+model:
+  name: server
+  language: python
+  args: ./src/server.py
+  function: model_function
+  is_server: True
diff --git a/yggdrasil/examples/rpc_lesson3b/src/client.c b/yggdrasil/examples/rpc_lesson3b/src/client.c
new file mode 100644
index 000000000..6cefdf431
--- /dev/null
+++ b/yggdrasil/examples/rpc_lesson3b/src/client.c
@@ -0,0 +1,57 @@
+#include <omp.h>
+#include "YggInterface.h"
+#include <stdio.h>
+
+
+int model_function(char* in_buf, uint64_t length_in_buf,
+		   char** out_buf, uint64_t* length_out_buf) {
+  int error_code = 0;
+  int nthreads = atoi(getenv("NTHREAD"));
+#ifdef _OPENMP
+  omp_set_num_threads(nthreads);
+#pragma omp parallel for shared(error_code)
+#endif
+  for (int i = 0; i < nthreads; ++i){
+    int flag;
+#ifdef _OPENMP
+#pragma omp critical
+    {
+#endif
+      flag = error_code;
+#ifdef _OPENMP
+    }
+#endif
+    if (flag == 0) {
+      char* out_temp = NULL;
+      uint64_t length_out_temp = 0;
+      char** out_buf_temp = &out_temp;
+      uint64_t* length_out_buf_temp = &length_out_temp;
+      if (i == 0) {
+	out_buf_temp = out_buf;
+	length_out_buf_temp = length_out_buf;
+      }
+      
+      // The WITH_GLOBAL_SCOPE macro is required to ensure that the
+      // comm persists between function calls
+      WITH_GLOBAL_SCOPE(yggRpc_t rpc = yggRpcClient("server_client", "%s", "%s"));
+      printf("client(C:%d): Sending %s (length = %d)\n",
+	     i, in_buf, (int)(length_in_buf));
+      int ret = rpcCallRealloc(rpc, in_buf, length_in_buf,
+			       out_buf_temp, length_out_buf_temp);
+      printf("client(C:%d): Received %s (length = %d)\n",
+	     i, in_buf, (int)(length_in_buf));
+      if (ret < 0) {
+	printf("client(C:%d): RPC CALL ERROR\n", i);
+#ifdef _OPENMP
+#pragma omp critical
+	{
+#endif
+	  error_code = -1;
+#ifdef _OPENMP
+	}
+#endif
+      }
+    }
+  }
+  return error_code;
+}
diff --git a/yggdrasil/examples/rpc_lesson3b/src/server.py b/yggdrasil/examples/rpc_lesson3b/src/server.py
new file mode 100644
index 000000000..ad5ed48e7
--- /dev/null
+++ b/yggdrasil/examples/rpc_lesson3b/src/server.py
@@ -0,0 +1,4 @@
+def model_function(in_buf):
+    print("server(Python): %s" % in_buf)
+    out_buf = in_buf
+    return out_buf
diff --git a/yggdrasil/examples/timed_pipe/src/timed_pipe_src.c b/yggdrasil/examples/timed_pipe/src/timed_pipe_src.c
index 50779de15..f36ecc654 100644
--- a/yggdrasil/examples/timed_pipe/src/timed_pipe_src.c
+++ b/yggdrasil/examples/timed_pipe/src/timed_pipe_src.c
@@ -6,10 +6,6 @@ int main(int argc, char *argv[]) {
     printf("Error in C pipe_src: The message count and size must be provided as input arguments.\n");
     return -1;
   }
-  // Prevent C4100 warning on windows by referencing param
-#ifdef _WIN32
-  argc;
-#endif
   int exit_code = 0;
   int ret = 0;
 
diff --git a/yggdrasil/examples/transforms/src/src.py b/yggdrasil/examples/transforms/src/src.py
index 96f92154c..90e833d53 100644
--- a/yggdrasil/examples/transforms/src/src.py
+++ b/yggdrasil/examples/transforms/src/src.py
@@ -1,9 +1,9 @@
-import pprint
+from yggdrasil.tools import pprint_encoded
 from yggdrasil.examples.tests.test_transforms import TestExampleTransforms
 
 
 def src():
     test_data = TestExampleTransforms.get_test_data()
     print('SENDING')
-    pprint.pprint(test_data)
+    pprint_encoded(test_data)
     return test_data
diff --git a/yggdrasil/languages/C/communication/ClientComm.h b/yggdrasil/languages/C/communication/ClientComm.h
index 735034632..7877b83cf 100644
--- a/yggdrasil/languages/C/communication/ClientComm.h
+++ b/yggdrasil/languages/C/communication/ClientComm.h
@@ -22,10 +22,17 @@ static unsigned _client_rand_seeded = 0;
 */
 static inline
 int new_client_address(comm_t *comm) {
+#ifdef _OPENMP
+#pragma omp critical (client)
+  {
+#endif
   if (!(_client_rand_seeded)) {
     srand(ptr2seed(comm));
     _client_rand_seeded = 1;
   }
+#ifdef _OPENMP
+  }    
+#endif
   comm->type = _default_comm;
   return new_default_address(comm);
 };
@@ -38,10 +45,18 @@ int new_client_address(comm_t *comm) {
 static inline
 int init_client_comm(comm_t *comm) {
   int ret = 0;
+  ygglog_debug("init_client_comm: Creating a client comm");
+#ifdef _OPENMP
+#pragma omp critical (client)
+  {
+#endif
   if (!(_client_rand_seeded)) {
     srand(ptr2seed(comm));
     _client_rand_seeded = 1;
   }
+#ifdef _OPENMP
+  }    
+#endif
   // Called to create temp comm for send/recv
   if ((strlen(comm->name) == 0) && (strlen(comm->address) > 0)) {
     comm->type = _default_comm;
diff --git a/yggdrasil/languages/C/communication/CommBase.h b/yggdrasil/languages/C/communication/CommBase.h
index 9a1715de7..37f3429b6 100644
--- a/yggdrasil/languages/C/communication/CommBase.h
+++ b/yggdrasil/languages/C/communication/CommBase.h
@@ -228,6 +228,18 @@ comm_t* init_comm_base(const char *name, const char *direction,
 	address = getenv(full_name);
       }
     }
+    if (address == NULL) {
+      char temp_name[COMM_NAME_SIZE] = "";
+      size_t i;
+      for (i = 0; i < strlen(full_name); i++) {
+	if (full_name[i] == ':') {
+	  strcat(temp_name, "__COLON__");
+	} else {
+	  strncat(temp_name, full_name + i, 1);
+	}
+      }
+      address = getenv(temp_name);
+    }
     ygglog_debug("init_comm_base: model_name = %s, full_name = %s, address = %s",
 		 model_name, full_name, address);
   }
diff --git a/yggdrasil/languages/C/communication/IPCComm.h b/yggdrasil/languages/C/communication/IPCComm.h
index 39fea3764..a712d1b52 100644
--- a/yggdrasil/languages/C/communication/IPCComm.h
+++ b/yggdrasil/languages/C/communication/IPCComm.h
@@ -23,8 +23,6 @@ extern "C" {
 
 #ifdef IPCINSTALLED
 
-/*! Number of temporary channels created. */
-static unsigned _yggChannelsCreated = 0;
 /*! @brief Maximum number of channels. */
 #define _yggTrackChannels 256
 /*! @brief Names of channels in use. */
@@ -57,20 +55,29 @@ int check_channels(comm_t* comm) {
   // Fail if trying to re-use the same channel twice
   unsigned i;
   char *key = comm->address;
+  int error_code = 0;
+#ifdef _OPENMP
+#pragma omp critical (ipc)
+  {
+#endif
   for (i = 0; i < _yggChannelsUsed; i++ ) {
     if (_yggChannelNames[i] == atoi(comm->address)) {
     /* if (0 == strcmp(_yggChannelNames[i], key)) { */
       ygglog_error("Attempt to re-use channel: name=%s, key=%s, i=%d",
 		   comm->name, key, i);
-      return -1;
+      error_code = -1;
+      break;
     }
   }
   // Fail if > _yggTrackChannels channels used
-  if (_yggChannelsUsed >= _yggTrackChannels) {
+  if ((!error_code) && (_yggChannelsUsed >= _yggTrackChannels)) {
     ygglog_error("Too many channels in use, max: %d", _yggTrackChannels);
-    return -1;
+    error_code = -1;
   }
-  return 0;
+#ifdef _OPENMP
+  }
+#endif
+  return error_code;
 };
 
 /*!
@@ -79,8 +86,15 @@ int check_channels(comm_t* comm) {
 */
 static inline
 void add_channel(const comm_t* comm) {
+#ifdef _OPENMP
+#pragma omp critical (ipc)
+  {
+#endif
   // printf("add_channel(%s): %d, %s\n", comm->name, _yggChannelsUsed, comm->address);
   _yggChannelNames[_yggChannelsUsed++] = atoi(comm->address);
+#ifdef _OPENMP
+  }
+#endif
 };
 
 /*!
@@ -104,6 +118,10 @@ int remove_comm(const comm_t* comm, const int close_comm) {
   ret = -1;
   unsigned i;
   int ich = atoi(comm->address);
+#ifdef _OPENMP
+#pragma omp critical (ipc)
+  {
+#endif
   for (i = 0; i < _yggChannelsUsed; i++) {
     if (ich == _yggChannelNames[i]) {
       memmove(_yggChannelNames + i, _yggChannelNames + i + 1,
@@ -121,6 +139,9 @@ int remove_comm(const comm_t* comm, const int close_comm) {
   /*   /\* 	    (_yggTrackChannels - (ich + 1))*sizeof(char*)); *\/ */
   /*   _yggChannelsUsed--; */
   /* } */
+#ifdef _OPENMP
+  }
+#endif
   return ret;
 };
 
@@ -134,13 +155,20 @@ int new_ipc_address(comm_t *comm) {
   int ret;
   // TODO: small chance of reusing same number
   int key = 0;
+#ifdef _OPENMP
+#pragma omp critical (ipc)
+  {
+#endif
   if (!(_ipc_rand_seeded)) {
     srand(ptr2seed(comm));
     _ipc_rand_seeded = 1;
   }
+#ifdef _OPENMP
+  }
+#endif
   while (key == 0) {
     key = rand();
-  } // _yggChannelsUsed + 1;
+  }
   if (strlen(comm->name) == 0) {
     sprintf(comm->name, "tempnewIPC.%d", key);
   } else {
@@ -162,7 +190,6 @@ int new_ipc_address(comm_t *comm) {
   }
   comm->handle = (void*)fid;
   add_channel(comm);
-  _yggChannelsCreated++;
   return 0;
 };
 
diff --git a/yggdrasil/languages/C/communication/ZMQComm.h b/yggdrasil/languages/C/communication/ZMQComm.h
index 947011008..899e81e37 100644
--- a/yggdrasil/languages/C/communication/ZMQComm.h
+++ b/yggdrasil/languages/C/communication/ZMQComm.h
@@ -17,18 +17,141 @@ extern "C" {
 
 static unsigned _zmq_rand_seeded = 0;
 static unsigned _last_port_set = 0;
-static unsigned _yggSocketsCreated = 0;
 static int _last_port = 49152;
 /* static double _wait_send_t = 0;  // 0.0001; */
 static char _reply_msg[100] = "YGG_REPLY";
 static char _purge_msg[100] = "YGG_PURGE";
 static int _zmq_sleeptime = 10000;
+static void *ygg_s_process_ctx = NULL;
+
+
+typedef struct ygg_zsock_t {
+  uint32_t tag;               //  Object tag for runtime detection
+  void *handle;               //  The libzmq socket handle
+  char *endpoint;             //  Last bound endpoint, if any
+  char *cache;                //  Holds last zsock_brecv strings
+  int type;                   //  Socket type
+  size_t cache_size;          //  Current size of cache
+  uint32_t routing_id;        //  Routing ID for server sockets
+} ygg_zsock_t;
+
+
+/*!
+  @brief Initialize zeromq.
+  @returns A zeromq context.
+*/
+#ifdef _OPENMP
+static inline
+void* ygg_zsys_init() {
+#pragma omp critical (zmq)
+  {
+    if (!(ygg_s_process_ctx)) {
+      if (get_thread_id() == 0) {
+	ygglog_debug("ygg_zsys_init: Creating ZMQ context.");
+	ygg_s_process_ctx = zsys_init();
+	if (!(ygg_s_process_ctx)) {
+	  ygglog_error("ygg_zsys_init: ZMQ context is NULL.");
+	}
+      } else {
+	ygglog_error("ygg_zsys_init: Can only initialize the "
+		     "zeromq context on the main thread. Call ygg_init "
+		     "before the threaded porition of your model.");
+      }
+    }
+  }
+  return ygg_s_process_ctx;
+};
+#else
+#define ygg_zsys_init zsys_init
+#endif
+
+
+/*!
+  @brief Shutdown zeromq.
+ */
+#ifdef _OPENMP
+static
+void ygg_zsys_shutdown() {
+#pragma omp critical (zmq)
+  {
+    ygg_s_process_ctx = NULL;
+    zsys_shutdown();
+  }    
+};
+#else
+#define ygg_zsys_shutdown zsys_shutdown
+#endif
+
+
+/*!
+  @brief Destroy a socket in thread safe way.
+  @param[in] self_p zsock_t** Pointer to a CZMQ socket wrapper struct.
+*/
+#ifdef _OPENMP
+static inline
+void ygg_zsock_destroy(zsock_t **self_p) {
+  // Recreation of czmq zsock_destroy that is OMP aware
+  /* assert(self_p); */
+  if (*self_p) {
+    ygg_zsock_t *self = (ygg_zsock_t*)(*self_p);
+    /* assert (zsock_is (*self_p)); */
+    self->tag = 0xDeadBeef;
+    zmq_close (self->handle);
+    freen (self->endpoint);
+    freen (self->cache);
+    freen (self);
+    *self_p = NULL;
+  }
+};
+#else
+#define ygg_zsock_destroy zsock_destroy
+#endif
+
+
+/*!
+  @brief Get a new socket, using the exising context.
+  @param[in] type int Socket type.
+  @returns zsock_t* CZMQ socket wrapper struct.
+*/
+#ifdef _OPENMP
+static inline
+zsock_t *
+ygg_zsock_new(int type) {
+  // Recreation of czmq zsock_new that is OMP aware
+  ygg_zsock_t *self = (ygg_zsock_t *) zmalloc (sizeof (ygg_zsock_t));
+  if (!(self)) {
+    ygglog_error("ygg_zsock_new: Error allocating for new socket.");
+    return NULL;
+  }
+  self->tag = 0xcafe0004;
+  self->type = type;
+  void* ctx = ygg_zsys_init();
+  if (!(ctx)) {
+    ygglog_error("ygg_zsock_new: Context is NULL.");
+    freen(self);
+    return NULL;
+  }
+#pragma omp critical (zmq)
+  {
+    self->handle = zmq_socket (ctx, type);
+  }
+  if (!(self->handle)) {
+    ygglog_error("ygg_zsock_new: Error creating new socket.");
+    freen(self);
+    return NULL;
+  }
+  return (zsock_t*)(self);
+};
+#else
+#define ygg_zsock_new zsock_new
+#endif
+
 
 /*! 
   @brief Struct to store info for reply.
 */
 typedef struct zmq_reply_t {
-  int nsockets; 
+  int nsockets;
   zsock_t **sockets;
   char **addresses;
   int n_msg;
@@ -56,7 +179,7 @@ int free_zmq_reply(zmq_reply_t *x) {
     if (x->sockets != NULL) {
       for (i = 0; i < x->nsockets; i++) {
 	if (x->sockets[i] != NULL) {
-	  zsock_destroy(&(x->sockets[i]));
+	  ygg_zsock_destroy(&(x->sockets[i]));
 	  x->sockets[i] = NULL;
 	}
       }
@@ -283,7 +406,7 @@ char *set_reply_send(const comm_t *comm) {
       return out;
     }
     zrep->nsockets = 1;
-    zrep->sockets[0] = zsock_new(ZMQ_REP);
+    zrep->sockets[0] = ygg_zsock_new(ZMQ_REP);
     zsock_set_linger(zrep->sockets[0], 0);
     if (zrep->sockets[0] == NULL) {
       ygglog_error("set_reply_send(%s): Could not initialize empty socket.",
@@ -295,6 +418,11 @@ char *set_reply_send(const comm_t *comm) {
     if (strcmp(host, "localhost") == 0)
       strncpy(host, "127.0.0.1", 50);
     char address[100];
+    int port = -1;
+#ifdef _OPENMP
+#pragma omp critical (zmqport)
+  {
+#endif
     if (_last_port_set == 0) {
       ygglog_debug("model_index = %s", getenv("YGG_MODEL_INDEX"));
       _last_port = 49152 + 1000 * atoi(getenv("YGG_MODEL_INDEX"));
@@ -302,13 +430,17 @@ char *set_reply_send(const comm_t *comm) {
       ygglog_debug("_last_port = %d", _last_port);
     }
     sprintf(address, "%s://%s:*[%d-]", protocol, host, _last_port + 1);
-    int port = zsock_bind(zrep->sockets[0], "%s", address);
+    port = zsock_bind(zrep->sockets[0], "%s", address);
+    if (port != -1)
+      _last_port = port;
+#ifdef _OPENMP
+  }
+#endif
     if (port == -1) {
       ygglog_error("set_reply_send(%s): Could not bind socket to address = %s",
 		   comm->name, address);
       return out;
     }
-    _last_port = port;
     sprintf(address, "%s://%s:%d", protocol, host, port);
     zrep->addresses = (char**)malloc(sizeof(char*));
     zrep->addresses[0] = (char*)malloc((strlen(address) + 1)*sizeof(char));
@@ -356,7 +488,7 @@ int set_reply_recv(const comm_t *comm, const char* address) {
     // Create new socket
     isock = zrep->nsockets;
     zrep->nsockets++;
-    zrep->sockets[isock] = zsock_new(ZMQ_REQ);
+    zrep->sockets[isock] = ygg_zsock_new(ZMQ_REQ);
     zsock_set_linger(zrep->sockets[isock], 0);
     if (zrep->sockets[isock] == NULL) {
       ygglog_error("set_reply_recv(%s): Could not initialize empty socket.",
@@ -474,15 +606,26 @@ int new_zmq_address(comm_t *comm) {
       (strcmp(protocol, "ipc") == 0)) {
     // TODO: small chance of reusing same number
     int key = 0;
+#ifdef _OPENMP
+#pragma omp critical (zmqport)
+  {
+#endif
     if (!(_zmq_rand_seeded)) {
       srand(ptr2seed(comm));
       _zmq_rand_seeded = 1;
     }
+#ifdef _OPENMP
+  }
+#endif
     while (key == 0) key = rand();
     if (strlen(comm->name) == 0)
       sprintf(comm->name, "tempnewZMQ-%d", key);
     sprintf(address, "%s://%s", protocol, comm->name);
   } else {
+#ifdef _OPENMP
+#pragma omp critical (zmqport)
+  {
+#endif
      if (_last_port_set == 0) {
       ygglog_debug("model_index = %s", getenv("YGG_MODEL_INDEX"));
       _last_port = 49152 + 1000 * atoi(getenv("YGG_MODEL_INDEX"));
@@ -490,10 +633,13 @@ int new_zmq_address(comm_t *comm) {
       ygglog_debug("_last_port = %d", _last_port);
     }
    sprintf(address, "%s://%s:*[%d-]", protocol, host, _last_port + 1);
+#ifdef _OPENMP
+  }
+#endif
     /* strcat(address, ":!"); // For random port */
   }
   // Bind
-  zsock_t *s = zsock_new(ZMQ_PAIR);
+  zsock_t *s = ygg_zsock_new(ZMQ_PAIR);
   if (s == NULL) {
     ygglog_error("new_zmq_address: Could not initialize empty socket.");
     return -1;
@@ -506,17 +652,23 @@ int new_zmq_address(comm_t *comm) {
     return -1;
   }
   // Add port to address
+#ifdef _OPENMP
+#pragma omp critical (zmqport)
+  {
+#endif
   if ((strcmp(protocol, "inproc") != 0) &&
       (strcmp(protocol, "ipc") != 0)) {
     _last_port = port;
     sprintf(address, "%s://%s:%d", protocol, host, port);
   }
+#ifdef _OPENMP
+  }
+#endif
   strncpy(comm->address, address, COMM_ADDRESS_SIZE);
   ygglog_debug("new_zmq_address: Bound socket to %s", comm->address);
   if (strlen(comm->name) == 0)
     sprintf(comm->name, "tempnewZMQ-%d", port);
   comm->handle = (void*)s;
-  _yggSocketsCreated++;
   // Init reply
   int ret = init_zmq_reply(comm);
   return ret;
@@ -533,7 +685,7 @@ int init_zmq_comm(comm_t *comm) {
   if (comm->valid == 0)
     return ret;
   comm->msgBufSize = 100;
-  zsock_t *s = zsock_new(ZMQ_PAIR);
+  zsock_t *s = ygg_zsock_new(ZMQ_PAIR);
   if (s == NULL) {
     ygglog_error("init_zmq_address: Could not initialize empty socket.");
     return -1;
@@ -543,7 +695,7 @@ int init_zmq_comm(comm_t *comm) {
   if (ret == -1) {
     ygglog_error("init_zmq_address: Could not connect socket to address = %s",
   		 comm->address);
-    zsock_destroy(&s);
+    ygg_zsock_destroy(&s);
     return ret;
   }
   ygglog_debug("init_zmq_address: Connected socket to %s", comm->address);
@@ -595,7 +747,7 @@ int free_zmq_comm(comm_t *x) {
     zsock_t *s = (zsock_t*)(x->handle);
     if (s != NULL) {
       ygglog_debug("Destroying socket: %s", x->address);
-      zsock_destroy(&s);
+      ygg_zsock_destroy(&s);
     }
     x->handle = NULL;
   }
@@ -785,6 +937,23 @@ int zmq_comm_recv(const comm_t* x, char **data, const size_t len,
 // Definitions in the case where ZMQ libraries not installed
 #else /*ZMQINSTALLED*/
 
+/*!
+  @brief Print error message about ZMQ library not being installed.
+ */
+static inline
+void ygg_zsys_shutdown() {
+  ygglog_error("Compiler flag 'ZMQINSTALLED' not defined so ZMQ bindings are disabled.");
+};
+
+/*!
+  @brief Print error message about ZMQ library not being installed.
+ */
+static inline
+void* ygg_zsys_init() {
+  ygglog_error("Compiler flag 'ZMQINSTALLED' not defined so ZMQ bindings are disabled.");
+  return NULL;
+};
+
 /*!
   @brief Print error message about ZMQ library not being installed.
  */
diff --git a/yggdrasil/languages/C/communication/communication.h b/yggdrasil/languages/C/communication/communication.h
index 3ab2db99c..60a1022cc 100644
--- a/yggdrasil/languages/C/communication/communication.h
+++ b/yggdrasil/languages/C/communication/communication.h
@@ -21,9 +21,13 @@ extern "C" {
 static void **vcomms2clean = NULL;
 static size_t ncomms2clean = 0;
 static size_t clean_registered = 0;
+static size_t clean_called = 0;
 
 /*! @brief Memory to keep track of global scope comms. */
 static size_t global_scope_comm = 0;
+#ifdef _OPENMP
+#pragma omp threadprivate(global_scope_comm)
+#endif
 #define WITH_GLOBAL_SCOPE(COMM) global_scope_comm = 1; COMM; global_scope_comm = 0
 
 
@@ -35,28 +39,38 @@ static size_t global_scope_comm = 0;
  */
 static
 comm_t* get_global_scope_comm(const char *name) {
-  if (!(global_scope_comm)) {
-    return NULL;
-  }
-  size_t i;
-  comm_t* icomm = NULL;
-  int current_thread = get_thread_id();
-  for (i = 0; i < ncomms2clean; i++) {
-    if (vcomms2clean[i] != NULL) {
-      icomm = (comm_t*)(vcomms2clean[i]);
-      if ((strcmp(icomm->name, name) == 0) && (icomm->thread_id == current_thread)) {
-  	return icomm;
-      } else {
-	const char* YGG_MODEL_NAME = getenv("YGG_MODEL_NAME");
-	char alt_name[100];
-	sprintf(alt_name, "%s:%s", YGG_MODEL_NAME, name);
-	if ((strcmp(icomm->name, alt_name) == 0) && (icomm->thread_id == current_thread)) {
-	  return icomm;
+  comm_t* out = NULL;
+#ifdef _OPENMP
+#pragma omp critical (comms)
+  {
+#endif
+  if (global_scope_comm) {
+    size_t i;
+    comm_t* icomm = NULL;
+    int current_thread = get_thread_id();
+    printf("CURRENT THREAD: %d\n", current_thread);
+    for (i = 0; i < ncomms2clean; i++) {
+      if (vcomms2clean[i] != NULL) {
+	icomm = (comm_t*)(vcomms2clean[i]);
+	if ((strcmp(icomm->name, name) == 0) && (icomm->thread_id == current_thread)) {
+	  out = icomm;
+	  break;
+	} else {
+	  const char* YGG_MODEL_NAME = getenv("YGG_MODEL_NAME");
+	  char alt_name[100];
+	  sprintf(alt_name, "%s:%s", YGG_MODEL_NAME, name);
+	  if ((strcmp(icomm->name, alt_name) == 0) && (icomm->thread_id == current_thread)) {
+	    out = icomm;
+	    break;
+	  }
 	}
       }
     }
   }
-  return NULL;
+#ifdef _OPENMP
+  }
+#endif
+  return out;
 };
 
 
@@ -120,6 +134,10 @@ int free_comm(comm_t *x) {
   int ret = 0;
   if (x == NULL)
     return ret;
+#ifdef _OPENMP
+#pragma omp critical (comms)
+  {
+#endif
   ygglog_debug("free_comm(%s)", x->name);
   // Send EOF for output comms and then wait for messages to be recv'd
   if ((is_send(x->direction)) && (x->valid)) {
@@ -145,6 +163,9 @@ int free_comm(comm_t *x) {
     }
   }
   ygglog_debug("free_comm: Finished");
+#ifdef _OPENMP
+  }
+#endif
   return ret;
 };
 
@@ -154,29 +175,68 @@ int free_comm(comm_t *x) {
 static
 void clean_comms(void) {
   size_t i;
-  ygglog_debug("atexit begin");
-  for (i = 0; i < ncomms2clean; i++) {
-    if (vcomms2clean[i] != NULL) {
-      free_comm((comm_t*)(vcomms2clean[i]));
+#ifdef _OPENMP
+#pragma omp critical (comms)
+  {
+#endif
+  if (!(clean_called)) {
+    ygglog_debug("atexit begin");
+    for (i = 0; i < ncomms2clean; i++) {
+      if (vcomms2clean[i] != NULL) {
+	free_comm((comm_t*)(vcomms2clean[i]));
+      }
     }
-  }
-  if (vcomms2clean != NULL) {
-    free(vcomms2clean);
-    vcomms2clean = NULL;
-  }
-  ncomms2clean = 0;
+    if (vcomms2clean != NULL) {
+      free(vcomms2clean);
+      vcomms2clean = NULL;
+    }
+    ncomms2clean = 0;
 #if defined(ZMQINSTALLED)
-  // #if defined(_MSC_VER) && defined(ZMQINSTALLED)
-  zsys_shutdown();
+    // #if defined(_MSC_VER) && defined(ZMQINSTALLED)
+    ygg_zsys_shutdown();
 #endif
-  if (Py_IsInitialized()) {
-    Py_Finalize();
+    if (Py_IsInitialized()) {
+      Py_Finalize();
+    }
+  /* printf(""); */
+    clean_called = 1;
   }
+#ifdef _OPENMP
+  }
+#endif
   ygglog_debug("atexit done");
   if (_ygg_error_flag != 0) {
     _exit(_ygg_error_flag);
   }
-  /* printf(""); */
+};
+
+/*!
+  @brief Initialize yggdrasil in a thread-safe way
+ */
+static inline
+int ygg_init() {
+  int out = 0;
+#ifdef _OPENMP
+#pragma omp critical (init)
+  {
+#endif
+  ygglog_debug("ygg_init: clean_registered = %d", clean_registered);
+  if (clean_registered == 0) {
+#if defined(ZMQINSTALLED)
+    if (!(ygg_zsys_init())) {
+      out = -1;
+    }
+#endif
+    if (out == 0) {
+      ygglog_debug("ygg_init: Registering cleanup");
+      atexit(clean_comms);
+      clean_registered = 1;
+    }
+  }
+#ifdef _OPENMP
+  }
+#endif
+  return out;
 };
 
 /*!
@@ -187,22 +247,31 @@ void clean_comms(void) {
  */
 static
 int register_comm(comm_t *x) {
-  if (clean_registered == 0) {
-    atexit(clean_comms);
-    clean_registered = 1;
-  }
   if (x == NULL) {
     return 0;
   }
-  void **t_vcomms2clean = (void**)realloc(vcomms2clean, sizeof(void*)*(ncomms2clean + 1));
-  if (t_vcomms2clean == NULL) {
-    ygglog_error("register_comm(%s): Failed to realloc the comm list.", x->name);
-    return -1;
+  int error_flag = 0;
+#ifdef _OPENMP
+#pragma omp critical (comms)
+  {
+#endif
+    if (ygg_init()) {
+      error_flag = 1;
+    } else {
+      void **t_vcomms2clean = (void**)realloc(vcomms2clean, sizeof(void*)*(ncomms2clean + 1));
+      if (t_vcomms2clean == NULL) {
+	ygglog_error("register_comm(%s): Failed to realloc the comm list.", x->name);
+	error_flag = -1;
+      } else {
+	vcomms2clean = t_vcomms2clean;
+	x->index_in_register = (int)ncomms2clean;
+	vcomms2clean[ncomms2clean++] = (void*)x;
+      }
+    }
+#ifdef _OPENMP
   }
-  vcomms2clean = t_vcomms2clean;
-  x->index_in_register = (int)ncomms2clean;
-  vcomms2clean[ncomms2clean++] = (void*)x;
-  return 0;
+#endif
+  return error_flag;
 };
 
 /*!
diff --git a/yggdrasil/languages/C/tools.h b/yggdrasil/languages/C/tools.h
index 199a9768a..98a3b65ee 100644
--- a/yggdrasil/languages/C/tools.h
+++ b/yggdrasil/languages/C/tools.h
@@ -94,6 +94,7 @@ typedef long double _Complex complex_long_double;
 extern "C" {
 #endif
 
+#include <math.h> // Required to prevent error when using mingw on windows
 #ifdef _DEBUG
 #undef _DEBUG
 #include <Python.h>
@@ -320,7 +321,7 @@ int init_python_API() {
  */
 static inline
 void yggLog(const char* prefix, const char* fmt, va_list ap) {
-  fprintf(stdout, "%s: %d: ", prefix, ygg_getpid());
+  fprintf(stdout, "%s: %d:%d ", prefix, ygg_getpid(), get_thread_id());
   vfprintf(stdout, fmt, ap);
   fprintf(stdout, "\n");
   fflush(stdout);
diff --git a/yggdrasil/languages/MATLAB/install.py b/yggdrasil/languages/MATLAB/install.py
index fd1888c62..fd77131d9 100644
--- a/yggdrasil/languages/MATLAB/install.py
+++ b/yggdrasil/languages/MATLAB/install.py
@@ -64,18 +64,18 @@ def install(args=None, as_user=None):
         if PY_MAJOR_VERSION == 3:
             mtl_proc = mtl_proc.decode("utf-8")
     except BaseException as e:
-        logger.error("Error when calling MATLAB from the command line: %s"
-                     % e)
+        logger.warning("Error when calling MATLAB from the command line: %s"
+                       % e)
         return False
     if mtl_id not in mtl_proc:
-        logger.error("ID (%s) not in output from MATLAB process: %s"
-                     % (mtl_id, mtl_proc))
+        logger.warning("ID (%s) not in output from MATLAB process: %s"
+                       % (mtl_id, mtl_proc))
         return False
     mtl_root = mtl_proc.split(mtl_id)[-2]
     # Install engine API
     mtl_setup = os.path.join(mtl_root, 'extern', 'engines', 'python')
     if not os.path.isdir(mtl_setup):
-        logger.error("MATLAB directory dosn't exist: %s" % mtl_setup)
+        logger.warning("MATLAB directory dosn't exist: %s" % mtl_setup)
         return False
     blddir = os.path.join(os.path.expanduser('~'), 'matlab_python_api')
     if not os.path.isdir(blddir):
@@ -91,7 +91,8 @@ def install(args=None, as_user=None):
             result = result.decode("utf-8")
         print(result)
     except subprocess.CalledProcessError as e:
-        logger.error("Error calling install command for Matlab engine: %s" % e)
+        logger.warning("Error calling install command for Matlab engine: %s"
+                       % e)
         return False
     return True
 
diff --git a/yggdrasil/languages/R/DESCRIPTION b/yggdrasil/languages/R/DESCRIPTION
index 18f449b6f..07f28df19 100644
--- a/yggdrasil/languages/R/DESCRIPTION
+++ b/yggdrasil/languages/R/DESCRIPTION
@@ -10,6 +10,7 @@ LazyData: true
 Imports:
     bit64,
     reticulate,
+    Rcpp,
     units,
     zeallot,
     R6
diff --git a/yggdrasil/languages/R/install.py b/yggdrasil/languages/R/install.py
index 16797fc54..a61932e66 100644
--- a/yggdrasil/languages/R/install.py
+++ b/yggdrasil/languages/R/install.py
@@ -6,6 +6,7 @@
 import subprocess
 import logging
 import argparse
+import shutil
 PY_MAJOR_VERSION = sys.version_info[0]
 logger = logging.getLogger(__name__)
 logger.setLevel(level=logging.INFO)
@@ -140,12 +141,10 @@ def install_packages(package_list, update=False, repos=None, **kwargs):
     req_nover = []
     for x in package_list:
         out = re.fullmatch(regex_ver, x).groupdict()
-        print(x, out)
         if out['ver'] and ('=' in out['comparison']):
             req_ver.append((out['name'], out['ver']))
         else:
             req_nover.append(out['name'])
-    print(req_ver, req_nover)
     if repos is None:
         repos = 'http://cloud.r-project.org'
     R_cmd = []
@@ -164,6 +163,7 @@ def install_packages(package_list, update=False, repos=None, **kwargs):
             R_cmd = ['req <- %s' % req_list,
                      'for (x in req) {',
                      '  if (!is.element(x, installed.packages()[,1])) {',
+                     '    print(sprintf("Installing \'%s\' from CRAN.", x))',
                      '    install.packages(x, dep=TRUE, repos="%s")' % repos,
                      '  } else {',
                      '    print(sprintf("%s already installed.", x))',
@@ -211,7 +211,10 @@ def call_R(R_cmd, **kwargs):
         fd.write('\n'.join(R_cmd))
         logger.info('Running:\n    ' + '\n    '.join(R_cmd))
     try:
-        out = make_call(['Rscript', R_script], **kwargs)
+        Rexe = shutil.which('Rscript')
+        if not Rexe:
+            Rexe = 'Rscript'
+        out = make_call([Rexe, R_script], **kwargs)
     finally:
         os.remove(R_script)
     return out
@@ -237,6 +240,8 @@ def make_call(R_cmd, with_sudo=False, **kwargs):
     try:
         logger.info("Calling %s on %s (with_sudo=%s)"
                     % (' '.join(R_cmd), sys.platform, with_sudo))
+        if sys.platform in ['win32', 'cygwin']:
+            kwargs.setdefault('shell', True)
         R_proc = subprocess.check_output(R_cmd, **kwargs)
         if PY_MAJOR_VERSION == 3:
             R_proc = R_proc.decode("utf-8")
diff --git a/yggdrasil/languages/__init__.py b/yggdrasil/languages/__init__.py
index 42191e8e4..89d3b872b 100644
--- a/yggdrasil/languages/__init__.py
+++ b/yggdrasil/languages/__init__.py
@@ -20,9 +20,11 @@ def get_language_dir(lang):
     if lang.lower() in ['cmake', 'lpy']:
         mod_list.insert(0, lang[:2].upper() + lang[2:].lower())
     elif lang.lower() == 'cpp':
-        mod_list = ['c++', 'C++'] + mod_list
+        mod_list = ['c++', 'C++'] + mod_list + ['cxx', 'CXX']
     elif lang.lower() == 'c++':
-        mod_list += ['cpp', 'CPP']
+        mod_list += ['cpp', 'CPP', 'cxx', 'CXX']
+    elif lang.lower() == 'cxx':
+        mod_list += ['c++', 'C++', 'cpp', 'CPP']
     for ilang in mod_list:
         idir = os.path.join(_lang_dir, ilang)
         if os.path.isdir(idir):
diff --git a/yggdrasil/metaschema/datatypes/ScalarMetaschemaType.py b/yggdrasil/metaschema/datatypes/ScalarMetaschemaType.py
index 33497483c..f48b1d39c 100644
--- a/yggdrasil/metaschema/datatypes/ScalarMetaschemaType.py
+++ b/yggdrasil/metaschema/datatypes/ScalarMetaschemaType.py
@@ -188,9 +188,8 @@ def decode_data(cls, obj, typedef):
         # arr = np.fromstring(bytes, dtype=dtype)
         if 'shape' in typedef:
             arr = arr.reshape(typedef['shape'])
-        out = cls.from_array(arr, unit_str=typedef.get('units', None), dtype=dtype)
-        # Cast numpy type to native python type if they are equivalent
-        out = cls.as_python_type(out, typedef)
+        out = cls.from_array(arr, unit_str=typedef.get('units', None),
+                             dtype=dtype, typedef=typedef)
         return out
 
     @classmethod
@@ -213,8 +212,8 @@ def transform_type(cls, obj, typedef=None):
         typedef1.update(**typedef)
         dtype = ScalarMetaschemaProperties.definition2dtype(typedef1)
         arr = cls.to_array(obj).astype(dtype, casting='same_kind')
-        out = cls.from_array(arr, unit_str=typedef0.get('units', None), dtype=dtype)
-        out = cls.as_python_type(out, typedef)
+        out = cls.from_array(arr, unit_str=typedef0.get('units', None),
+                             dtype=dtype, typedef=typedef)
         return units.convert_to(out, typedef1.get('units', None))
 
     @classmethod
@@ -237,7 +236,7 @@ def to_array(cls, obj):
         return arr
         
     @classmethod
-    def from_array(cls, arr, unit_str=None, dtype=None):
+    def from_array(cls, arr, unit_str=None, dtype=None, typedef=None):
         r"""Get object representation of the data.
 
         Args:
@@ -246,16 +245,26 @@ def from_array(cls, arr, unit_str=None, dtype=None):
                 object.
             dtype (np.dtype, optional): Numpy data type that should be maintained
                 as a base class when adding units. Defaults to None and is
-                determined from the object.
+                determined from the object or typedef (if provided).
+            typedef (dict, optional): Type definition that should be used to
+                decode the object. Defaults to None and is determined from the
+                object or dtype (if provided).
 
         Returns:
             object: Object representation of the data in the input array.
 
         """
+        # if (typedef is None) and (dtype is not None):
+        #     typedef = ScalarMetaschemaProperties.dtype2definition(dtype)
+        # elif (dtype is None) and (typedef is not None):
+        #     dtype = ScalarMetaschemaProperties.definition2dtype(typedef)
         if (cls.name not in ['1darray', 'ndarray']) and (arr.ndim > 0):
             out = arr[0]
         else:
             out = arr
+        if typedef is not None:
+            # Cast numpy type to native python type if they are equivalent
+            out = cls.as_python_type(out, typedef)
         if unit_str is not None:
             if dtype is None:
                 dtype = ScalarMetaschemaProperties.data2dtype(out)
diff --git a/yggdrasil/metaschema/datatypes/__init__.py b/yggdrasil/metaschema/datatypes/__init__.py
index fb6481385..c29cefd52 100644
--- a/yggdrasil/metaschema/datatypes/__init__.py
+++ b/yggdrasil/metaschema/datatypes/__init__.py
@@ -9,7 +9,8 @@
 
 
 _schema_dir = os.path.join(os.path.dirname(__file__), 'schemas')
-_base_validator = jsonschema.validators.validator_for({"$schema": ""})
+# _base_validator = jsonschema.validators.validator_for({"$schema": ""})
+_base_validator = jsonschema.validators._LATEST_VERSION
 YGG_MSG_HEAD = b'YGG_MSG_HEAD'
 _property_attributes = ['properties', 'definition_properties',
                         'metadata_properties', 'extract_properties']
diff --git a/yggdrasil/multitasking.py b/yggdrasil/multitasking.py
index 6c832f73b..21615055c 100644
--- a/yggdrasil/multitasking.py
+++ b/yggdrasil/multitasking.py
@@ -558,7 +558,9 @@ def ident(self):
     def kill(self, *args, **kwargs):
         r"""Kill the task."""
         if self.parallel and hasattr(self._base, 'kill'):
-            self._base.kill(*args, **kwargs)
+            return self._base.kill(*args, **kwargs)
+        elif hasattr(self._base, 'terminate'):
+            return self._base.terminate(*args, **kwargs)
 
 
 class DummyQueue(DummyContextObject):  # pragma: no cover
@@ -945,9 +947,9 @@ def daemon(self):
     def exitcode(self):
         r"""Exit code."""
         if self.as_process:
-            out = self.process_instance.exitcode
-            if (out == 0) and self.check_flag_attr('error_flag'):
-                out = 1
+            out = int(self.check_flag_attr('error_flag'))
+            if self.process_instance.exitcode:
+                out = self.process_instance.exitcode
             return out
         else:
             return int(self.check_flag_attr('error_flag'))
diff --git a/yggdrasil/runner.py b/yggdrasil/runner.py
index 3a06ce802..04c022c78 100755
--- a/yggdrasil/runner.py
+++ b/yggdrasil/runner.py
@@ -255,7 +255,7 @@ def createModelDriver(self, yml):
             object: An instance of the specified driver.
 
         """
-        yml['env'] = {}
+        yml.setdefault('env', {})
         for iod in self.io_drivers(yml['name']):
             yml['env'].update(iod['instance'].env)
             iod['models'].append(yml['name'])
diff --git a/yggdrasil/serialize/PandasSerialize.py b/yggdrasil/serialize/PandasSerialize.py
index 7da10f7e1..bf6eddb7e 100644
--- a/yggdrasil/serialize/PandasSerialize.py
+++ b/yggdrasil/serialize/PandasSerialize.py
@@ -157,7 +157,7 @@ def func_serialize(self, args):
                      # Not in pandas <0.24
                      # line_terminator=self.newline.decode("utf-8"),
                      sep=self.delimiter.decode("utf-8"),
-                     mode='wb', encoding='utf8',
+                     mode='w', encoding='utf8',
                      header=(not self.dont_write_header))
         if self.write_header_once:
             self.dont_write_header = True
diff --git a/yggdrasil/serialize/SerializeBase.py b/yggdrasil/serialize/SerializeBase.py
index 905ccac84..d1fac195b 100644
--- a/yggdrasil/serialize/SerializeBase.py
+++ b/yggdrasil/serialize/SerializeBase.py
@@ -214,16 +214,19 @@ def get_testing_options(cls, table_example=False, array_columns=False,
             assert(table_string_type in ['bytes', 'unicode', 'string'])
             if table_string_type == 'string':
                 table_string_type = 'unicode'
+            int_type = np.int32
+            if np.dtype(int) == int_type:  # pragma: windows
+                int_type = int
             if table_string_type == 'bytes':
                 np_dtype_str = 'S'
-                rows = [(b'one', np.int32(1), 1.0),
-                        (b'two', np.int32(2), 2.0),
-                        (b'three', np.int32(3), 3.0)]
+                rows = [(b'one', int_type(1), 1.0),
+                        (b'two', int_type(2), 2.0),
+                        (b'three', int_type(3), 3.0)]
             else:
                 np_dtype_str = 'U'
-                rows = [('one', np.int32(1), 1.0),
-                        ('two', np.int32(2), 2.0),
-                        ('three', np.int32(3), 3.0)]
+                rows = [('one', int_type(1), 1.0),
+                        ('two', int_type(2), 2.0),
+                        ('three', int_type(3), 3.0)]
             out = {'kwargs': {}, 'empty': [], 'dtype': None,
                    'extra_kwargs': {},
                    'typedef': {'type': 'array',
diff --git a/yggdrasil/timing.py b/yggdrasil/timing.py
index c585b8bb6..e4e1cd6f5 100644
--- a/yggdrasil/timing.py
+++ b/yggdrasil/timing.py
@@ -22,7 +22,7 @@
 import matplotlib as mpl
 if os.environ.get('DISPLAY', '') == '':  # pragma: debug
     mpl.use('Agg')
-elif ygg_platform._is_mac:
+elif ygg_platform._is_mac:  # pragma: travis
     mpl.use('TkAgg')
 import matplotlib.pyplot as plt  # noqa: E402
 logger = logging.getLogger(__name__)
@@ -113,9 +113,14 @@ def write_pyperf_script(script_file, nmsg, msg_size,
         'lang_dst = "%s"' % lang_dst,
         'comm_type = "%s"' % comm_type,
         'matlab_running = %s' % str(matlab_running)]
-    if os.environ.get('TMPDIR', ''):
-        lines += [
-            'os.environ["TMPDIR"] = "%s"' % os.environ['TMPDIR']]
+    # if os.environ.get('TMPDIR', ''):
+    #     tmpdir = os.environ['TMPDIR']
+    #     if platform._is_win:  # pragma: windows
+    #         tmpdir = repr(tmpdir)
+    #         assert(tmpdir.startswith('\'') and tmpdir.endswith('\''))
+    #         tmpdir = tmpdir[1:-1]
+    #         # tmpdir = tmpdir.replace('\\', '\\\\')
+    #     lines += ['os.environ["TMPDIR"] = "%s"' % tmpdir]
     lines += [
         'timer = timing.TimedRun(lang_src, lang_dst,'
         '                        comm_type=comm_type,'
@@ -295,6 +300,8 @@ def can_run(self, raise_error=False):
                and (self.lang_dst in self._lang_list)
                and (self.comm_type in self._comm_list))
         if (not out) and raise_error:
+            from yggdrasil.command_line import ygginfo
+            msg_info = ygginfo(args=['--verbose'], return_str=True)
             msg = ['Cannot run test with parameters:',
                    '\tOperating System: %s' % self.platform,
                    '\tPython Version: %s' % self.python_ver,
@@ -307,7 +314,8 @@ def can_run(self, raise_error=False):
                    '\tPython Version: %s' % _python_version,
                    '\tMatlab Running: %s' % MatlabModelDriver.is_matlab_running(),
                    '\tSupported Languages: %s' % ', '.join(self._lang_list),
-                   '\tSupported Communication: %s' % ', '.join(self._comm_list)]
+                   '\tSupported Communication: %s' % ', '.join(self._comm_list),
+                   '\n\n' + msg_info]
             raise RuntimeError('\n'.join(msg))
         return out
 
@@ -637,11 +645,19 @@ def time_run_pyperf(self, nmsg, msg_size, nrep):
                             self.lang_src, self.lang_dst, self.comm_type,
                             nrep=nrep, matlab_running=self.matlab_running,
                             max_errors=self.max_errors)
-        copy_env = ['TMPDIR', 'CONDA_PREFIX']
+        copy_env = ['TMPDIR', 'CONDA_PREFIX', 'PATHEXT'
+                    'CC', 'CFLAGS', 'CXX', 'CXXFLAGS', 'CPPFLAGS',
+                    'F77', 'F90', 'F95', 'FC', 'FFLAGS', 'FORTRANFLAGS',
+                    'GFORTRAN', 'LD', 'LDFLAGS', 'LD_LIBRARY_PATH']
         copy_env += [v['env'] for v in config._cfg_map.values()]
+        if platform._is_mac:
+            copy_env += ['SDKROOT', 'MACOSX_DEPLOYMENT_TARGET',
+                         'CONDA_BUILD_SYSROOT']
         if platform._is_win:  # pragma: windows
-            copy_env += ['HOMEPATH', 'NUMBER_OF_PROCESSORS',
-                         'INCLUDE', 'LIB', 'LIBPATH']
+            copy_env += ['HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH',
+                         'NUMBER_OF_PROCESSORS',
+                         'INCLUDE', 'LIB', 'LIBPATH',
+                         'ChocolateyInstall', 'VCPKG_ROOT']
         cmd = [sys.executable, self.pyperfscript, '--append=' + self.filename,
                '--inherit-environ=' + ','.join(copy_env),
                '--warmups=%d' % _pyperf_warmups]
diff --git a/yggdrasil/tools.py b/yggdrasil/tools.py
index ee9d86888..6da18cbc4 100644
--- a/yggdrasil/tools.py
+++ b/yggdrasil/tools.py
@@ -4,6 +4,7 @@
 import logging
 import pprint
 import os
+import re
 import sys
 import sysconfig
 try:
@@ -219,6 +220,39 @@ def get_fds():  # pragma: debug
     return out.splitlines()[1:]
 
 
+def get_shell():
+    r"""Get the type of shell that yggdrasil was called from.
+
+    Returns:
+        str: Name of the shell.
+
+    """
+    shell = os.environ.get('SHELL', None)
+    if not shell:
+        if platform._is_win:  # pragma: windows
+            shell = os.environ.get('COMSPEC', None)
+        else:
+            shell = '/bin/sh'  # Default used by subprocess
+        assert(shell)
+    # return psutil.Process(os.getppid()).name()
+    if platform._is_win:  # pragma: windows
+        shell = shell.lower()
+    return shell
+
+
+def in_powershell():
+    r"""Determine if yggdrasil is running from a Windows Powershell.
+
+    Returns:
+        bool: True if running from Powershell, False otherwise.
+
+    """
+    if not platform._is_win:
+        return False
+    shell = get_shell()
+    return bool(re.match('pwsh|pwsh.exe|powershell.exe', shell))
+
+
 def check_environ_bool(name, valid_values=['true', '1', True, 1]):
     r"""Check to see if a boolean environment variable is set to True.
 
@@ -269,13 +303,24 @@ def get_python_c_library(allow_failure=False, libtype=None):
         base = '%spython%s%s' % (prefix,
                                  cvars['py_version_nodot'],
                                  libtype2ext[libtype])
-    else:
+    elif sys.version_info[:2] < (3, 8):
         if libtype is None:
             libtype = 'shared'
         libtype2key = {'shared': 'LDLIBRARY', 'static': 'LIBRARY'}
         base = cvars.get(libtype2key[libtype], None)
-        if platform._is_mac and base.endswith('/Python'):  # pragma: no cover
-            base = 'libpython%s.dylib' % cvars['py_version_short']
+    else:
+        if libtype is None:
+            libtype = 'shared'
+        if platform._is_mac:
+            libtype2ext = {'shared': '.dylib', 'static': '.a'}
+        else:
+            libtype2ext = {'shared': '.so', 'static': '.a'}
+        prefix = 'lib'
+        base = '%spython%s%s' % (prefix,
+                                 cvars['py_version_short'],
+                                 libtype2ext[libtype])
+    if platform._is_mac and base.endswith('/Python'):  # pragma: no cover
+        base = 'libpython%s.dylib' % cvars['py_version_short']
     if base is None:  # pragma: debug
         raise RuntimeError(("Could not determine base name for the Python "
                             "C API library.\n"
@@ -284,18 +329,20 @@ def get_python_c_library(allow_failure=False, libtype=None):
                            % (pprint.pformat(paths),
                               pprint.pformat(cvars)))
     dir_try = []
-    if cvars['prefix']:
-        dir_try.append(cvars['prefix'])
-        if platform._is_win:  # pragma: windows
-            dir_try.append(os.path.join(cvars['prefix'], 'libs'))
-        else:
-            dir_try.append(os.path.join(cvars['prefix'], 'lib'))
-    if cvars.get('LIBDIR', None):
-        dir_try.append(cvars['LIBDIR'])
-    if cvars.get('LIBDEST', None):
-        dir_try.append(cvars['LIBDEST'])
+    for x in [get_conda_prefix(), cvars['prefix']]:
+        if x:
+            dir_try.append(x)
+            if platform._is_win:  # pragma: windows
+                dir_try.append(os.path.join(x, 'libs'))
+            else:
+                dir_try.append(os.path.join(x, 'lib'))
+    for k in ["LIBPL", "LIBDIR", "LIBDEST", "Prefix", "ExecPrefix",
+              "BaseExecPrefix"]:
+        if cvars.get(k, None) and (cvars[k] not in dir_try):
+            dir_try.append(cvars[k])
     for k in ['stdlib', 'purelib', 'platlib', 'platstdlib', 'data']:
-        dir_try.append(paths[k])
+        if paths.get(k, None) and (paths[k] not in dir_try):
+            dir_try.append(paths[k])
     dir_try.append(os.path.join(paths['data'], 'lib'))
     if distutils_sysconfig is not None:
         dir_try.append(os.path.dirname(
@@ -1008,6 +1055,18 @@ def popen_nobuffer(*args, **kwargs):
     return YggPopen(*args, **kwargs)
 
 
+def pprint_encoded(obj, *args, **kwargs):
+    r"""Pretty print an object, catching encoding errors as necessary.
+
+    Args:
+        obj (object): Python object to pprint.
+        *args: Additional arguments are passed to pprint.pformat.
+        **kwargs: Additional keyword arguments are passed to pprint.pformat.
+
+    """
+    print_encoded(pprint.pformat(obj, *args, **kwargs))
+
+
 def print_encoded(msg, *args, **kwargs):
     r"""Print bytes to stdout, encoding if possible.
 
@@ -1018,6 +1077,8 @@ def print_encoded(msg, *args, **kwargs):
 
 
     """
+    if not isinstance(msg, (str, bytes)):
+        msg = str(msg)
     try:
         print(bytes2str(msg), *args, **kwargs)
     except (UnicodeEncodeError, UnicodeDecodeError):  # pragma: debug
diff --git a/yggdrasil/yamlfile.py b/yggdrasil/yamlfile.py
index a37822ef6..2dd8fc076 100644
--- a/yggdrasil/yamlfile.py
+++ b/yggdrasil/yamlfile.py
@@ -408,6 +408,9 @@ def parse_model(yml, existing):
             x['model_driver'] = [yml['name']]
             x['partner_language'] = language
             existing = parse_component(x, io[:-1], existing=existing)
+    for k in yml.get('env', {}).keys():
+        if not isinstance(yml['env'][k], str):
+            yml['env'][k] = json.dumps(yml['env'][k])
     return existing
 
 


Test patch:
diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml
new file mode 100644
index 000000000..70e79ea85
--- /dev/null
+++ b/.github/workflows/test-install.yml
@@ -0,0 +1,1266 @@
+# DO NOT MODIFY THIS FILE, IT IS GENERATED.
+# To make changes, modify '.git/../utils/test-install-base.yml'
+# and run 'ygggha'
+name: Test of package installation & execution
+'on':
+  push:
+    branches-ignore:
+    - gh-pages
+    tags:
+    - '*'
+  schedule:
+  - cron: 0 10 * * 1
+env:
+  NUMPY: numpy
+  MATPLOTLIB: matplotlib
+  MPLBACKEND: agg
+  JSONSCHEMA: jsonschema
+  INSTALLR: 1
+  INSTALLC: 1
+  INSTALLFORTRAN: 1
+  INSTALLSBML: 1
+  INSTALLLPY: 0
+  INSTALLZMQ: 1
+  INSTALLRMQ: 0
+  INSTALLTRIMESH: 1
+  INSTALLPYGMENTS: 1
+  INSTALLOMP: 1
+  USEVIRTUALENV: 0
+  BUILDDOCS: 0
+  YGG_GHA_JOB_ID_SUFFIX: null
+jobs:
+  test_pip:
+    name: Test (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method
+      }}, ${{ matrix.test-flags1 }}, ${{ matrix.test-flags2 }}), Install C = ${{ matrix.install-c
+      }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      max-parallel: 20
+      matrix:
+        os:
+        - ubuntu-latest
+        - macos-latest
+        - windows-latest
+        python-version:
+        - 3.6
+        install-method:
+        - pip
+        test-flags1:
+        - --long-running --test-suite=timing --test-suite=demos --test-suite=top
+        - --test-suite=examples
+        - --test-suite=types --skip-languages osr
+        test-flags2:
+        - ''
+        install-c:
+        - true
+        include:
+        - os: windows-latest
+          python-version: 3.6
+          install-method: pip
+          test-flags1: --long-running --languages cpp
+          test-flags2: --long-running --skip-languages c++
+          install-c: false
+      fail-fast: false
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set C installation based on matrix value
+      if: matrix.install-c != true && matrix.install-method != 'conda'
+      run: echo "INSTALLC=0" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: Store an ID string for the job
+      run: echo "YGG_GHA_JOB_ID=${{ matrix.os }}-${{ matrix.install-method }}-py${{
+        matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
+        env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (WINDOWS) Patch TEMP/TMPDIR on Windows
+      if: matrix.os == 'windows-latest'
+      run: 'echo "TMPDIR=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        echo "TEMP=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (WINDOWS,PIP) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: Install R
+      uses: r-lib/actions/setup-r@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+    - name: (LINUX,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (MACOS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
+      with:
+        path: ~/Library/Caches/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (WINDOWS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
+      with:
+        path: ~\AppData\Local\pip\Cache
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (PIP) Set up Python
+      uses: actions/setup-python@v2
+      if: matrix.install-method == 'pip'
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: (PIP) Set up Virtual Env
+      if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
+      uses: syphar/restore-virtualenv@v1
+    - name: (PIP) Install dependencies & package
+      if: matrix.install-method == 'pip'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }}
+    - name: (WINDOWS,PIP) Set the vcpkg root config value
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      run: yggconfig --vcpkg-dir=C:\\vcpkg
+    - name: (WINDOWS,PIP) Update R requirements
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+      run: ygginstall R --update-r-requirements
+    - name: (CONDA) Cache conda
+      uses: actions/cache@v1
+      if: matrix.install-method == 'conda'
+      env:
+        CACHE_NUMBER: 0
+      with:
+        path: ~/conda_pkgs_dir
+        key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('recipe/meta.yaml')
+          }}
+    - name: (WINDOWS,CONDA) Set up miniconda base environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest'
+      with:
+        channels: conda-forge
+        use-only-tar-bz2: true
+        activate-environment: ''
+    - name: (WINDOWS,CONDA) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: (UNIX,CONDA) Add miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
+
+        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (MACOS,CONDA) Change ownership of conda on MacOS
+      if: matrix.install-method == 'conda' && matrix.os == 'macos-latest'
+      run: sudo chown -R $(whoami) $CONDA
+      shell: bash
+    - name: (CONDA) Build package in base environment
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }}
+      shell: bash --noprofile --norc -eo pipefail {0}
+    - name: (UNIX,CONDA) Remove miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (CONDA) Set up miniconda test environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda'
+      with:
+        python-version: ${{ matrix.python-version }}
+        channels: conda-forge
+        use-only-tar-bz2: true
+        channel-priority: strict
+    - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
+        llvm (7)
+      if: matrix.os == 'macos-latest' && matrix.install-method == 'conda'
+      run: ./utils/setup_old_mac_sdk.sh
+    - name: (CONDA) Install dependencies & package from existing build
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Compile libraries
+      run: yggcompile
+    - name: Run cmake test in command prompt
+      if: matrix.os == 'windows-latest'
+      shell: cmd /C CALL {0}
+      run: 'cmake -h
+
+        yggtest --ci --nocapture drivers/tests/test_CMakeModelDriver.py
+
+        '
+    - name: Run tests (1st)
+      id: test1
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }}
+    - name: Re-run test failures (1st)
+      if: ${{ failure() && steps.test1.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
+    - name: Run tests (2nd)
+      if: matrix.test-flags2 != ''
+      id: test2
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }}
+    - name: Re-run test failures (2nd)
+      if: ${{ failure() && steps.test2.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
+    - name: Combine coverage reports
+      continue-on-error: true
+      run: 'ls .*
+
+        coverage combine --append
+
+        '
+    - name: (CONDA) Generate XML version of coverage report
+      if: matrix.install-method == 'conda'
+      run: 'ls .*
+
+        coverage xml
+
+        '
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v1
+      with:
+        name: ${{ env.YGG_GHA_JOB_ID }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+    - name: Download the last successful workflow environment log
+      if: ${{ always() }}
+      continue-on-error: true
+      uses: dawidd6/action-download-artifact@v2
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        workflow: test-install.yml
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: .
+    - name: Create a log of Python package versions
+      if: ${{ always() }}
+      run: python utils/setup_test_env.py log
+    - uses: actions/upload-artifact@v2
+      if: ${{ success() }}
+      with:
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: new_environment_log.txt
+  test_conda:
+    name: Test (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method
+      }}, ${{ matrix.test-flags1 }}, ${{ matrix.test-flags2 }}), Install C = ${{ matrix.install-c
+      }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      max-parallel: 20
+      matrix:
+        os:
+        - ubuntu-latest
+        - macos-latest
+        - windows-latest
+        python-version:
+        - 3.6
+        install-method:
+        - conda
+        test-flags1:
+        - --long-running
+        test-flags2:
+        - --with-examples examples/tests/test_backwards.py
+        install-c:
+        - true
+        include:
+        - os: ubuntu-latest
+          python-version: 3.7
+          install-method: conda
+          test-flags1: --long-running
+          test-flags2: ''
+          install-c: true
+        - os: ubuntu-latest
+          python-version: 3.8
+          install-method: conda
+          test-flags1: --long-running
+          test-flags2: ''
+          install-c: true
+        - os: ubuntu-latest
+          python-version: 3.9
+          install-method: conda
+          test-flags1: --long-running
+          test-flags2: ''
+          install-c: true
+        - os: macos-latest
+          python-version: 3.8
+          install-method: conda
+          test-flags1: tests/test_tools.py --with-examples examples/tests/test_rpc_lesson3.py
+          test-flags2: ''
+          install-c: true
+      fail-fast: false
+    defaults:
+      run:
+        shell: bash -l {0}
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set C installation based on matrix value
+      if: matrix.install-c != true && matrix.install-method != 'conda'
+      run: echo "INSTALLC=0" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: Store an ID string for the job
+      run: echo "YGG_GHA_JOB_ID=${{ matrix.os }}-${{ matrix.install-method }}-py${{
+        matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
+        env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (WINDOWS) Patch TEMP/TMPDIR on Windows
+      if: matrix.os == 'windows-latest'
+      run: 'echo "TMPDIR=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        echo "TEMP=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (WINDOWS,PIP) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: Install R
+      uses: r-lib/actions/setup-r@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+    - name: (LINUX,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (MACOS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
+      with:
+        path: ~/Library/Caches/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (WINDOWS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
+      with:
+        path: ~\AppData\Local\pip\Cache
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (PIP) Set up Python
+      uses: actions/setup-python@v2
+      if: matrix.install-method == 'pip'
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: (PIP) Set up Virtual Env
+      if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
+      uses: syphar/restore-virtualenv@v1
+    - name: (PIP) Install dependencies & package
+      if: matrix.install-method == 'pip'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }}
+    - name: (WINDOWS,PIP) Set the vcpkg root config value
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      run: yggconfig --vcpkg-dir=C:\\vcpkg
+    - name: (WINDOWS,PIP) Update R requirements
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+      run: ygginstall R --update-r-requirements
+    - name: (CONDA) Cache conda
+      uses: actions/cache@v1
+      if: matrix.install-method == 'conda'
+      env:
+        CACHE_NUMBER: 0
+      with:
+        path: ~/conda_pkgs_dir
+        key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('recipe/meta.yaml')
+          }}
+    - name: (WINDOWS,CONDA) Set up miniconda base environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest'
+      with:
+        channels: conda-forge
+        use-only-tar-bz2: true
+        activate-environment: ''
+    - name: (WINDOWS,CONDA) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: (UNIX,CONDA) Add miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
+
+        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (MACOS,CONDA) Change ownership of conda on MacOS
+      if: matrix.install-method == 'conda' && matrix.os == 'macos-latest'
+      run: sudo chown -R $(whoami) $CONDA
+      shell: bash
+    - name: (CONDA) Build package in base environment
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }}
+      shell: bash --noprofile --norc -eo pipefail {0}
+    - name: (UNIX,CONDA) Remove miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (CONDA) Set up miniconda test environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda'
+      with:
+        python-version: ${{ matrix.python-version }}
+        channels: conda-forge
+        use-only-tar-bz2: true
+        channel-priority: strict
+    - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
+        llvm (7)
+      if: matrix.os == 'macos-latest' && matrix.install-method == 'conda'
+      run: ./utils/setup_old_mac_sdk.sh
+    - name: (CONDA) Install dependencies & package from existing build
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Compile libraries
+      run: yggcompile
+    - name: Run cmake test in command prompt
+      if: matrix.os == 'windows-latest'
+      shell: cmd /C CALL {0}
+      run: 'cmake -h
+
+        yggtest --ci --nocapture drivers/tests/test_CMakeModelDriver.py
+
+        '
+    - name: Run tests (1st)
+      id: test1
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }}
+    - name: Re-run test failures (1st)
+      if: ${{ failure() && steps.test1.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
+    - name: Run tests (2nd)
+      if: matrix.test-flags2 != ''
+      id: test2
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }}
+    - name: Re-run test failures (2nd)
+      if: ${{ failure() && steps.test2.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
+    - name: Combine coverage reports
+      continue-on-error: true
+      run: 'ls .*
+
+        coverage combine --append
+
+        '
+    - name: (CONDA) Generate XML version of coverage report
+      if: matrix.install-method == 'conda'
+      run: 'ls .*
+
+        coverage xml
+
+        '
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v1
+      with:
+        name: ${{ env.YGG_GHA_JOB_ID }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+    - name: Download the last successful workflow environment log
+      if: ${{ always() }}
+      continue-on-error: true
+      uses: dawidd6/action-download-artifact@v2
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        workflow: test-install.yml
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: .
+    - name: Create a log of Python package versions
+      if: ${{ always() }}
+      run: python utils/setup_test_env.py log
+    - uses: actions/upload-artifact@v2
+      if: ${{ success() }}
+      with:
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: new_environment_log.txt
+  test_rmq_pip:
+    name: Test RMQ (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method
+      }})
+    runs-on: ${{ matrix.os }}
+    env:
+      INSTALLRMQ: 1
+      INSTALLAPY: 1
+      USEVIRTUALENV: 1
+      YGG_DEFAULT_COMM: IPCComm
+    strategy:
+      matrix:
+        os:
+        - ubuntu-latest
+        python-version:
+        - 3.6
+        install-method:
+        - pip
+        test-flags1:
+        - --long-running --languages python R matlab
+        test-flags2:
+        - ''
+        install-c:
+        - true
+      fail-fast: false
+    services:
+      rabbitmq:
+        image: rabbitmq:latest
+        ports:
+        - 5672:5672
+        options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s
+          --health-timeout 5s --health-retries 5
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set C installation based on matrix value
+      if: matrix.install-c != true && matrix.install-method != 'conda'
+      run: echo "INSTALLC=0" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: Store an ID string for the job
+      run: echo "YGG_GHA_JOB_ID=${{ matrix.os }}-${{ matrix.install-method }}-py${{
+        matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
+        env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (WINDOWS) Patch TEMP/TMPDIR on Windows
+      if: matrix.os == 'windows-latest'
+      run: 'echo "TMPDIR=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        echo "TEMP=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (WINDOWS,PIP) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: Install R
+      uses: r-lib/actions/setup-r@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+    - name: (LINUX,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (MACOS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
+      with:
+        path: ~/Library/Caches/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (WINDOWS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
+      with:
+        path: ~\AppData\Local\pip\Cache
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (PIP) Set up Python
+      uses: actions/setup-python@v2
+      if: matrix.install-method == 'pip'
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: (PIP) Set up Virtual Env
+      if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
+      uses: syphar/restore-virtualenv@v1
+    - name: (PIP) Install dependencies & package
+      if: matrix.install-method == 'pip'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }}
+    - name: (WINDOWS,PIP) Set the vcpkg root config value
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      run: yggconfig --vcpkg-dir=C:\\vcpkg
+    - name: (WINDOWS,PIP) Update R requirements
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+      run: ygginstall R --update-r-requirements
+    - name: (CONDA) Cache conda
+      uses: actions/cache@v1
+      if: matrix.install-method == 'conda'
+      env:
+        CACHE_NUMBER: 0
+      with:
+        path: ~/conda_pkgs_dir
+        key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('recipe/meta.yaml')
+          }}
+    - name: (WINDOWS,CONDA) Set up miniconda base environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest'
+      with:
+        channels: conda-forge
+        use-only-tar-bz2: true
+        activate-environment: ''
+    - name: (WINDOWS,CONDA) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: (UNIX,CONDA) Add miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
+
+        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (MACOS,CONDA) Change ownership of conda on MacOS
+      if: matrix.install-method == 'conda' && matrix.os == 'macos-latest'
+      run: sudo chown -R $(whoami) $CONDA
+      shell: bash
+    - name: (CONDA) Build package in base environment
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }}
+      shell: bash --noprofile --norc -eo pipefail {0}
+    - name: (UNIX,CONDA) Remove miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (CONDA) Set up miniconda test environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda'
+      with:
+        python-version: ${{ matrix.python-version }}
+        channels: conda-forge
+        use-only-tar-bz2: true
+        channel-priority: strict
+    - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
+        llvm (7)
+      if: matrix.os == 'macos-latest' && matrix.install-method == 'conda'
+      run: ./utils/setup_old_mac_sdk.sh
+    - name: (CONDA) Install dependencies & package from existing build
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Compile libraries
+      run: yggcompile
+    - name: Run cmake test in command prompt
+      if: matrix.os == 'windows-latest'
+      shell: cmd /C CALL {0}
+      run: 'cmake -h
+
+        yggtest --ci --nocapture drivers/tests/test_CMakeModelDriver.py
+
+        '
+    - name: Run tests (1st)
+      id: test1
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }}
+    - name: Re-run test failures (1st)
+      if: ${{ failure() && steps.test1.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
+    - name: Run tests (2nd)
+      if: matrix.test-flags2 != ''
+      id: test2
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }}
+    - name: Re-run test failures (2nd)
+      if: ${{ failure() && steps.test2.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
+    - name: Combine coverage reports
+      continue-on-error: true
+      run: 'ls .*
+
+        coverage combine --append
+
+        '
+    - name: (CONDA) Generate XML version of coverage report
+      if: matrix.install-method == 'conda'
+      run: 'ls .*
+
+        coverage xml
+
+        '
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v1
+      with:
+        name: ${{ env.YGG_GHA_JOB_ID }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+    - name: Download the last successful workflow environment log
+      if: ${{ always() }}
+      continue-on-error: true
+      uses: dawidd6/action-download-artifact@v2
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        workflow: test-install.yml
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: .
+    - name: Create a log of Python package versions
+      if: ${{ always() }}
+      run: python utils/setup_test_env.py log
+    - uses: actions/upload-artifact@v2
+      if: ${{ success() }}
+      with:
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: new_environment_log.txt
+  test_rmq_conda:
+    name: Test RMQ (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method
+      }})
+    runs-on: ${{ matrix.os }}
+    env:
+      INSTALLRMQ: 1
+      INSTALLAPY: 1
+      USEVIRTUALENV: 1
+    strategy:
+      matrix:
+        os:
+        - ubuntu-latest
+        python-version:
+        - 3.6
+        install-method:
+        - conda
+        test-flags1:
+        - --long-running --languages python R matlab
+        test-flags2:
+        - ''
+        install-c:
+        - true
+      fail-fast: false
+    services:
+      rabbitmq:
+        image: rabbitmq:latest
+        ports:
+        - 5672:5672
+        options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s
+          --health-timeout 5s --health-retries 5
+    defaults:
+      run:
+        shell: bash -l {0}
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set C installation based on matrix value
+      if: matrix.install-c != true && matrix.install-method != 'conda'
+      run: echo "INSTALLC=0" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: Store an ID string for the job
+      run: echo "YGG_GHA_JOB_ID=${{ matrix.os }}-${{ matrix.install-method }}-py${{
+        matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
+        env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (WINDOWS) Patch TEMP/TMPDIR on Windows
+      if: matrix.os == 'windows-latest'
+      run: 'echo "TMPDIR=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        echo "TEMP=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (WINDOWS,PIP) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: Install R
+      uses: r-lib/actions/setup-r@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+    - name: (LINUX,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (MACOS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
+      with:
+        path: ~/Library/Caches/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (WINDOWS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
+      with:
+        path: ~\AppData\Local\pip\Cache
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (PIP) Set up Python
+      uses: actions/setup-python@v2
+      if: matrix.install-method == 'pip'
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: (PIP) Set up Virtual Env
+      if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
+      uses: syphar/restore-virtualenv@v1
+    - name: (PIP) Install dependencies & package
+      if: matrix.install-method == 'pip'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }}
+    - name: (WINDOWS,PIP) Set the vcpkg root config value
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      run: yggconfig --vcpkg-dir=C:\\vcpkg
+    - name: (WINDOWS,PIP) Update R requirements
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+      run: ygginstall R --update-r-requirements
+    - name: (CONDA) Cache conda
+      uses: actions/cache@v1
+      if: matrix.install-method == 'conda'
+      env:
+        CACHE_NUMBER: 0
+      with:
+        path: ~/conda_pkgs_dir
+        key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('recipe/meta.yaml')
+          }}
+    - name: (WINDOWS,CONDA) Set up miniconda base environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest'
+      with:
+        channels: conda-forge
+        use-only-tar-bz2: true
+        activate-environment: ''
+    - name: (WINDOWS,CONDA) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: (UNIX,CONDA) Add miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
+
+        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (MACOS,CONDA) Change ownership of conda on MacOS
+      if: matrix.install-method == 'conda' && matrix.os == 'macos-latest'
+      run: sudo chown -R $(whoami) $CONDA
+      shell: bash
+    - name: (CONDA) Build package in base environment
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }}
+      shell: bash --noprofile --norc -eo pipefail {0}
+    - name: (UNIX,CONDA) Remove miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (CONDA) Set up miniconda test environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda'
+      with:
+        python-version: ${{ matrix.python-version }}
+        channels: conda-forge
+        use-only-tar-bz2: true
+        channel-priority: strict
+    - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
+        llvm (7)
+      if: matrix.os == 'macos-latest' && matrix.install-method == 'conda'
+      run: ./utils/setup_old_mac_sdk.sh
+    - name: (CONDA) Install dependencies & package from existing build
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Compile libraries
+      run: yggcompile
+    - name: Run cmake test in command prompt
+      if: matrix.os == 'windows-latest'
+      shell: cmd /C CALL {0}
+      run: 'cmake -h
+
+        yggtest --ci --nocapture drivers/tests/test_CMakeModelDriver.py
+
+        '
+    - name: Run tests (1st)
+      id: test1
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }}
+    - name: Re-run test failures (1st)
+      if: ${{ failure() && steps.test1.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
+    - name: Run tests (2nd)
+      if: matrix.test-flags2 != ''
+      id: test2
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }}
+    - name: Re-run test failures (2nd)
+      if: ${{ failure() && steps.test2.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
+    - name: Combine coverage reports
+      continue-on-error: true
+      run: 'ls .*
+
+        coverage combine --append
+
+        '
+    - name: (CONDA) Generate XML version of coverage report
+      if: matrix.install-method == 'conda'
+      run: 'ls .*
+
+        coverage xml
+
+        '
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v1
+      with:
+        name: ${{ env.YGG_GHA_JOB_ID }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+    - name: Download the last successful workflow environment log
+      if: ${{ always() }}
+      continue-on-error: true
+      uses: dawidd6/action-download-artifact@v2
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        workflow: test-install.yml
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: .
+    - name: Create a log of Python package versions
+      if: ${{ always() }}
+      run: python utils/setup_test_env.py log
+    - uses: actions/upload-artifact@v2
+      if: ${{ success() }}
+      with:
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: new_environment_log.txt
+  test_stripped:
+    name: Test stripped install (${{ matrix.python-version }}, ${{ matrix.os }}, ${{
+      matrix.install-method }})
+    runs-on: ${{ matrix.os }}
+    env:
+      INSTALLR: 0
+      INSTALLC: 0
+      INSTALLFORTRAN: 0
+      INSTALLLPY: 0
+      INSTALLZMQ: 0
+      INSTALLRMQ: 0
+      INSTALLTRIMESH: 0
+      INSTALLPYGMENTS: 0
+      YGG_GHA_JOB_ID_SUFFIX: -stripped
+    strategy:
+      matrix:
+        os:
+        - ubuntu-latest
+        - macos-latest
+        - windows-latest
+        python-version:
+        - 3.6
+        install-method:
+        - pip
+        test-flags1:
+        - --long-running
+        test-flags2:
+        - ''
+        install-c:
+        - false
+      fail-fast: false
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set C installation based on matrix value
+      if: matrix.install-c != true && matrix.install-method != 'conda'
+      run: echo "INSTALLC=0" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: Store an ID string for the job
+      run: echo "YGG_GHA_JOB_ID=${{ matrix.os }}-${{ matrix.install-method }}-py${{
+        matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
+        env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (WINDOWS) Patch TEMP/TMPDIR on Windows
+      if: matrix.os == 'windows-latest'
+      run: 'echo "TMPDIR=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        echo "TEMP=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (WINDOWS,PIP) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: Install R
+      uses: r-lib/actions/setup-r@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+    - name: (LINUX,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (MACOS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
+      with:
+        path: ~/Library/Caches/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (WINDOWS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
+      with:
+        path: ~\AppData\Local\pip\Cache
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: '${{ runner.os }}-pip-
+
+          '
+    - name: (PIP) Set up Python
+      uses: actions/setup-python@v2
+      if: matrix.install-method == 'pip'
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: (PIP) Set up Virtual Env
+      if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
+      uses: syphar/restore-virtualenv@v1
+    - name: (PIP) Install dependencies & package
+      if: matrix.install-method == 'pip'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }}
+    - name: (WINDOWS,PIP) Set the vcpkg root config value
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC
+        == 1
+      run: yggconfig --vcpkg-dir=C:\\vcpkg
+    - name: (WINDOWS,PIP) Update R requirements
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR
+        == 1
+      run: ygginstall R --update-r-requirements
+    - name: (CONDA) Cache conda
+      uses: actions/cache@v1
+      if: matrix.install-method == 'conda'
+      env:
+        CACHE_NUMBER: 0
+      with:
+        path: ~/conda_pkgs_dir
+        key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('recipe/meta.yaml')
+          }}
+    - name: (WINDOWS,CONDA) Set up miniconda base environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest'
+      with:
+        channels: conda-forge
+        use-only-tar-bz2: true
+        activate-environment: ''
+    - name: (WINDOWS,CONDA) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest' && env.INSTALLC
+        == 1
+      with:
+        toolset: 14.0
+    - name: (UNIX,CONDA) Add miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
+
+        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (MACOS,CONDA) Change ownership of conda on MacOS
+      if: matrix.install-method == 'conda' && matrix.os == 'macos-latest'
+      run: sudo chown -R $(whoami) $CONDA
+      shell: bash
+    - name: (CONDA) Build package in base environment
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }}
+      shell: bash --noprofile --norc -eo pipefail {0}
+    - name: (UNIX,CONDA) Remove miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+
+        '
+      shell: bash -l {0}
+    - name: (CONDA) Set up miniconda test environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda'
+      with:
+        python-version: ${{ matrix.python-version }}
+        channels: conda-forge
+        use-only-tar-bz2: true
+        channel-priority: strict
+    - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
+        llvm (7)
+      if: matrix.os == 'macos-latest' && matrix.install-method == 'conda'
+      run: ./utils/setup_old_mac_sdk.sh
+    - name: (CONDA) Install dependencies & package from existing build
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Compile libraries
+      run: yggcompile
+    - name: Run cmake test in command prompt
+      if: matrix.os == 'windows-latest'
+      shell: cmd /C CALL {0}
+      run: 'cmake -h
+
+        yggtest --ci --nocapture drivers/tests/test_CMakeModelDriver.py
+
+        '
+    - name: Run tests (1st)
+      id: test1
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }}
+    - name: Re-run test failures (1st)
+      if: ${{ failure() && steps.test1.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
+    - name: Run tests (2nd)
+      if: matrix.test-flags2 != ''
+      id: test2
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }}
+    - name: Re-run test failures (2nd)
+      if: ${{ failure() && steps.test2.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
+    - name: Combine coverage reports
+      continue-on-error: true
+      run: 'ls .*
+
+        coverage combine --append
+
+        '
+    - name: (CONDA) Generate XML version of coverage report
+      if: matrix.install-method == 'conda'
+      run: 'ls .*
+
+        coverage xml
+
+        '
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v1
+      with:
+        name: ${{ env.YGG_GHA_JOB_ID }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+    - name: Download the last successful workflow environment log
+      if: ${{ always() }}
+      continue-on-error: true
+      uses: dawidd6/action-download-artifact@v2
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        workflow: test-install.yml
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: .
+    - name: Create a log of Python package versions
+      if: ${{ always() }}
+      run: python utils/setup_test_env.py log
+    - uses: actions/upload-artifact@v2
+      if: ${{ success() }}
+      with:
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: new_environment_log.txt
+  docs:
+    name: Build the Docs
+    runs-on: ubuntu-latest
+    env:
+      BUILDDOCS: 1
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.8
+    - name: Install dependencies
+      run: python utils/setup_test_env.py install pip
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Build the docs
+      run: 'cd docs
+
+        make autodoc
+
+        cd ../
+
+        '
+    - name: Publish docs to Github pages
+      if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+      uses: JamesIves/github-pages-deploy-action@3.7.1
+      with:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        BRANCH: gh-pages
+        FOLDER: docs/build/html/
+  deploy:
+    name: Publish package
+    needs:
+    - test_pip
+    - test_conda
+    - test_rmq_pip
+    - test_rmq_conda
+    - test_stripped
+    - docs
+    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.8
+    - name: Install dependencies
+      run: python utils/setup_test_env.py install pip
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Build the package
+      run: 'python setup.py sdist
+
+        python setup.py bdist_wheel
+
+        '
+    - name: Publish package to PyPI
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        user: __token__
+        password: ${{ secrets.pypi }}
+        skip_existing: true
diff --git a/requirements_testing.txt b/requirements_testing.txt
index a4ad6da36..c1098d4cb 100644
--- a/requirements_testing.txt
+++ b/requirements_testing.txt
@@ -6,3 +6,5 @@ nose
 pytest>=4.6  # Required by pytest-cov, but not resolved by pip on linux
 pytest-cov
 pytest-timeout
+pytest-drop-dup-tests  # [pip]
+pytest-rerunfailures
diff --git a/utils/setup_test_env.py b/utils/setup_test_env.py
new file mode 100644
index 000000000..5bbe3a2f5
--- /dev/null
+++ b/utils/setup_test_env.py
@@ -0,0 +1,1165 @@
+import os
+import sys
+import argparse
+import uuid
+import pprint
+import shutil
+import subprocess
+import warnings
+import difflib
+from datetime import datetime
+PYVER = ('%s.%s' % sys.version_info[:2])
+PY2 = (sys.version_info[0] == 2)
+_is_osx = (sys.platform == 'darwin')
+_is_linux = ('linux' in sys.platform)
+_is_win = (sys.platform in ['win32', 'cygwin'])
+_is_unix = (_is_osx or _is_linux)
+_on_gha = bool(os.environ.get('GITHUB_ACTIONS', False))
+_on_travis = bool(os.environ.get('TRAVIS_OS_NAME', False))
+_on_appveyor = bool(os.environ.get('APPVEYOR_BUILD_FOLDER', False))
+_on_ci = (_on_gha or _on_travis or _on_appveyor)
+_utils_dir = os.path.dirname(os.path.abspath(__file__))
+_pkg_dir = os.path.dirname(_utils_dir)
+BUILDDOCS = (os.environ.get('BUILDDOCS', '0') == '1')
+CONDA_ENV = os.environ.get('CONDA_DEFAULT_ENV', None)
+CONDA_PREFIX = os.environ.get('CONDA_PREFIX', None)
+CONDA_INDEX = None
+CONDA_ROOT = None
+try:
+    CONDA_CMD_WHICH = shutil.which('conda')
+except AttributeError:
+    if _is_win:
+        CONDA_CMD_WHICH = None
+    else:
+        try:
+            CONDA_CMD_WHICH = subprocess.check_output(
+                ['which', 'conda']).strip().decode('utf-8')
+        except subprocess.CalledProcessError:
+            CONDA_CMD_WHICH = None
+if (not CONDA_PREFIX):
+    if CONDA_CMD_WHICH:
+        CONDA_PREFIX = os.path.dirname(os.path.dirname(CONDA_CMD_WHICH))
+    elif _on_gha and os.environ.get('CONDA', None):
+        CONDA_PREFIX = os.environ['CONDA']
+    if CONDA_PREFIX and (not CONDA_ENV):
+        CONDA_ENV = 'base'
+if ((isinstance(CONDA_PREFIX, str)
+     and os.path.dirname(CONDA_PREFIX).endswith('envs'))):
+    CONDA_PREFIX = os.path.dirname(os.path.dirname(CONDA_PREFIX))
+if CONDA_PREFIX:
+    CONDA_INDEX = os.path.join(CONDA_PREFIX, "conda-bld")
+    if not os.path.isdir(CONDA_INDEX):
+        if _on_gha and _is_win and CONDA_PREFIX.endswith('Library'):
+            CONDA_INDEX = os.path.join(os.path.dirname(CONDA_PREFIX),
+                                       "conda-bld")
+if CONDA_CMD_WHICH:
+    if _is_win:
+        CONDA_CMD = 'call conda'
+    else:
+        CONDA_CMD = 'conda'
+    CONDA_ROOT = os.path.dirname(os.path.dirname(CONDA_CMD_WHICH))
+elif os.environ.get('CONDA', None):
+    if _is_win:
+        CONDA_CMD = 'call %s' % os.path.join(os.environ['CONDA'],
+                                             'condabin', 'conda.bat')
+    else:
+        CONDA_CMD = os.path.join(os.environ['CONDA'], 'bin', 'conda')
+    CONDA_ROOT = os.environ['CONDA']
+else:
+    CONDA_CMD = None
+PYTHON_CMD = sys.executable
+SUMMARY_CMDS = ["%s --version" % PYTHON_CMD,
+                "%s -m pip list" % PYTHON_CMD]
+if CONDA_ENV:
+    SUMMARY_CMDS += ["echo 'CONDA_PREFIX=%s'" % CONDA_PREFIX,
+                     "%s info" % CONDA_CMD,
+                     "%s list" % CONDA_CMD,
+                     "%s config --show-sources" % CONDA_CMD]
+
+
+def call_conda_command(args, **kwargs):
+    r"""Function for calling conda commands as the conda script is not
+    available on subprocesses for windows unless invoked via the shell.
+
+    Args:
+        args (list): Command arguments.
+        **kwargs: Additional keyword arguments are passed to subprocess.check_output.
+
+    Returns:
+        str: The output from the command.
+
+    """
+    if _is_win:
+        args = ' '.join(args)
+        kwargs['shell'] = True  # Conda commands must be run on the shell
+    return subprocess.check_output(args, **kwargs).decode("utf-8")
+
+
+def call_script(lines, force_bash=False):
+    r"""Write lines to a script and call it.
+
+    Args:
+        lines (list): Lines that should be written to the script.
+        force_bash (bool, optional): If True, bash will be used, even
+            on windows. Defaults to False.
+
+    """
+    if not lines:
+        return
+    if _is_win and (not force_bash):  # pragma: windows
+        script_ext = '.bat'
+        error_check = 'if %errorlevel% neq 0 exit /b %errorlevel%'
+        for i in range(len(lines), 0, -1):
+            lines.insert(i, error_check)
+    else:
+        script_ext = '.sh'
+        if lines[0] != '#!/bin/bash':
+            lines.insert(0, '#!/bin/bash')
+        error_check = 'set -e'
+        if error_check not in lines:
+            lines.insert(1, error_check)
+    fname = 'ci_script_%s%s' % (str(uuid.uuid4()), script_ext)
+    try:
+        pprint.pprint(lines)
+        with open(fname, 'w') as fd:
+            fd.write('\n'.join(lines))
+            
+        call_kws = {}
+        if _is_win:  # pragma: windows
+            call_cmd = [fname]
+        else:
+            call_cmd = ['./%s' % fname]
+            os.chmod(fname, 0o755)
+        subprocess.check_call(call_cmd, **call_kws)
+    finally:
+        if os.path.isfile(fname):
+            os.remove(fname)
+
+
+def conda_env_exists(name):
+    r"""Determine if a conda environment already exists.
+
+    Args:
+        name (str): Name of the environment to check.
+
+    Returns:
+        bool: True the the environment exits, False otherwise.
+
+    """
+    args = [CONDA_CMD, 'info', '--envs']
+    out = call_conda_command(args)
+    envs = []
+    for x in out.splitlines():
+        if x.startswith('#') or (not x):
+            continue
+        envs.append(x.split()[0])
+    return (name in envs)
+
+
+def locate_conda_exe(conda_env, name):
+    r"""Determine the full path to an executable in a specific conda environment.
+
+    Args:
+        conda_env (str): Name of conda environment that executable should be
+            returned for.
+        name (str): Name of the executable to locate.
+
+    Returns:
+        str: Full path to the executable.
+
+    """
+    assert(CONDA_ROOT)
+    conda_prefix = os.path.join(CONDA_ROOT, 'envs')
+    if (sys.platform in ['win32', 'cygwin']):
+        if not name.endswith('.exe'):
+            name += '.exe'
+        if name.startswith('python'):
+            out = os.path.join(conda_prefix, conda_env, name)
+        else:
+            out = os.path.join(conda_prefix, conda_env,
+                               'Scripts', name)
+    else:
+        out = os.path.join(conda_prefix, conda_env, 'bin', name)
+    try:
+        assert(os.path.isfile(out))
+    except AssertionError:
+        out = os.path.expanduser(os.path.join('~', '.conda', 'envs', name))
+        if not os.path.isfile(out):
+            raise
+    return out
+
+
+def get_install_opts(old=None):
+    r"""Get optional language/package installation options from CI
+    environment variables.
+
+    Args:
+        old (dict, optional): If provided, the returned mapping will include
+            the values from this dictionary, but will also be updated with any
+            that are missing.
+
+    Returns:
+        dict: Mapping between languages/packages and whether or not they
+            should be installed.
+
+    """
+    if _on_ci:
+        new = {
+            'c': (os.environ.get('INSTALLC', '0') == '1'),
+            'lpy': (os.environ.get('INSTALLLPY', '0') == '1'),
+            'R': (os.environ.get('INSTALLR', '0') == '1'),
+            'fortran': (os.environ.get('INSTALLFORTRAN', '0') == '1'),
+            'zmq': (os.environ.get('INSTALLZMQ', '0') == '1'),
+            'sbml': (os.environ.get('INSTALLSBML', '0') == '1'),
+            'astropy': (os.environ.get('INSTALLAPY', '0') == '1'),
+            'rmq': (os.environ.get('INSTALLRMQ', '0') == '1'),
+            'trimesh': (os.environ.get('INSTALLTRIMESH', '0') == '1'),
+            'pygments': (os.environ.get('INSTALLPYGMENTS', '0') == '1'),
+            'omp': (os.environ.get('INSTALLOMP', '0') == '1'),
+        }
+        if not _is_win:
+            new['c'] = True  # c compiler usually installed by default
+    else:
+        new = {
+            'c': True,
+            'lpy': False,
+            'R': True,
+            'fortran': True,
+            'zmq': True,
+            'sbml': False,
+            'astropy': False,
+            'rmq': False,
+            'trimesh': True,
+            'pygments': True,
+            'omp': False,
+        }
+    if old is None:
+        out = {}
+    else:
+        out = old.copy()
+    for k, v in new.items():
+        out.setdefault(k, v)
+    if not out['c']:
+        out.update(fortran=False, zmq=False)
+    return out
+
+
+def create_env(method, python, name=None, packages=None, init=_on_ci):
+    r"""Setup a test environment on a CI resource.
+
+    Args:
+        method (str): Method that should be used to create an environment.
+            Supported values currently include 'conda' & 'virtualenv'.
+        python (str): Version of Python that should be tested.
+        name (str, optional): Name that should be used for the environment.
+            Defaults to None and will be craeted based on the method and
+            Python version.
+        packages (list, optional): Packages that should be installed in the new
+            environment. Defaults to None and is ignored.
+        init (bool, optional): If True, the environment management program is
+            first configured as if it is one CI so that, some interactive
+            aspects will be disabled. Default is set based on the presence of
+            CI environment variables (it currently checks for Github Actions,
+            Travis CI, and Appveyor)
+
+    Raises:
+        ValueError: If method is not 'conda' or 'pip'.
+
+    """
+    cmds = ["echo Creating test environment using %s..." % method]
+    major, minor = [int(x) for x in python.split('.')]
+    if name is None:
+        name = '%s%s' % (method, python.replace('.', ''))
+    if packages is None:
+        packages = []
+    if 'requests' not in packages:
+        # Not strictly required, but useful for determine the versions of
+        # dependencies required by packages during testing
+        packages.append('requests')
+    if method == 'conda':
+        if conda_env_exists(name):
+            print("Conda env with name '%s' already exists." % name)
+            return
+        if init:
+            cmds += [
+                # Configure conda
+                "%s config --set always_yes yes --set changeps1 no" % CONDA_CMD,
+                "%s config --set channel_priority strict" % CONDA_CMD,
+                "%s config --add channels conda-forge" % CONDA_CMD,
+                "%s update -q conda" % CONDA_CMD,
+                # "%s config --set allow_conda_downgrades true" % CONDA_CMD,
+                # "%s install -n root conda=4.9" % CONDA_CMD,
+            ]
+        cmds += [
+            "%s create -q -n %s python=%s %s" % (CONDA_CMD, name, python,
+                                                 ' '.join(packages))
+        ]
+    elif method == 'virtualenv':
+        python_cmd = PYTHON_CMD
+        if (sys.version_info[0] != major) or (sys.version_info[1] != minor):
+            if _is_osx:
+                try:
+                    call_script(['python%d --version' % major])
+                except BaseException:
+                    cmds.append('brew install python%d' % major)
+                try:
+                    python_cmd = shutil.which('python%d' % major)
+                except AttributeError:
+                    python_cmd = 'python%d' % major
+            else:  # pragma: debug
+                raise RuntimeError(("The version of Python (%d.%d) does not match the "
+                                    "desired version (%s) and virtualenv cannot create "
+                                    "an environment with a different version of Python.")
+                                   % (sys.version_info[0], sys.version_info[1], python))
+        cmds += [
+            "%s -m pip install --upgrade pip virtualenv" % python_cmd,
+            "virtualenv -p %s %s" % (python_cmd, name)
+        ]
+        if packages:
+            cmds.append("%s -m pip install %s" % (python_cmd, ' '.join(packages)))
+    else:  # pragma: debug
+        raise ValueError("Unsupport environment management method: '%s'"
+                         % method)
+    call_script(cmds)
+
+
+def build_pkg(method, python=None, return_commands=False,
+              verbose=False):
+    r"""Build the package on a CI resource.
+
+    Args:
+        method (str): Method that should be used to build the package.
+            Valid values include 'conda' and 'pip'.
+        python (str, optional): Version of Python that package should be
+            built for. Defaults to None if not provided and the current
+            version of Python will be used.
+        return_commands (bool, optional): If True, the commands necessary to
+            build the package are returned instead of running them. Defaults
+            to False.
+        verbose (bool, optional): If True, setup steps are run with verbosity
+            turned up. Defaults to False.
+
+    """
+    if python is None:
+        python = PYVER
+    cmds = []
+    # Upgrade pip and setuptools and wheel to get clean install
+    upgrade_pkgs = ['wheel', 'setuptools']
+    if not _is_win:
+        upgrade_pkgs.insert(0, 'pip')
+    # cmds += ["%s -m pip install --upgrade %s" % (PYTHON_CMD, ' '.join(upgrade_pkgs))]
+    if method == 'conda':
+        if verbose:
+            build_flags = ''
+        else:
+            build_flags = '-q'
+        # Must always build in base to avoid errors (and don't change the
+        # version of Python used in the environment)
+        # https://github.com/conda/conda/issues/9124
+        # https://github.com/conda/conda/issues/7758#issuecomment-660328841
+        assert(CONDA_ENV == 'base')
+        assert(CONDA_INDEX)
+        if _on_gha:
+            cmds += [
+                "%s config --add channels conda-forge" % CONDA_CMD,
+                "%s update -q conda" % CONDA_CMD,
+            ]
+        if _is_win and _on_gha:
+            # The tests issue a command that is too long for the
+            # windows command prompt which is used to build the conda
+            # package on Github Actions
+            build_flags += ' --no-test'
+        cmds += [
+            "%s clean --all" % CONDA_CMD,  # Might invalidate cache
+            # "%s deactivate" % CONDA_CMD,
+            "%s update --all" % CONDA_CMD,
+            "%s install -q -n base conda-build conda-verify" % CONDA_CMD,
+            "%s build %s --python %s %s" % (
+                CONDA_CMD, 'recipe', python, build_flags),
+            "%s index %s" % (CONDA_CMD, CONDA_INDEX),
+            # "%s activate %s" % (CONDA_CMD, CONDA_ENV),
+        ]
+    elif method == 'pip':
+        if verbose:
+            build_flags = ''
+        else:
+            build_flags = '--quiet'
+        # Install from source dist
+        cmds += ["%s -m pip install --upgrade %s" % (PYTHON_CMD, ' '.join(upgrade_pkgs))]
+        cmds += ["%s setup.py %s sdist" % (PYTHON_CMD, build_flags)]
+    else:  # pragma: debug
+        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
+                         % method)
+    if return_commands:
+        return cmds
+    cmds = SUMMARY_CMDS + cmds + SUMMARY_CMDS
+    call_script(cmds)
+    if method == 'conda':
+        assert(CONDA_INDEX and os.path.isdir(CONDA_INDEX))
+
+
+def install_deps(method, return_commands=False, verbose=False, for_development=False,
+                 skip_test_deps=False, include_dev_deps=False, include_doc_deps=False,
+                 windows_package_manager='vcpkg', install_opts=None, conda_env=None,
+                 always_yes=False, only_python=False, fallback_to_conda=False):
+    r"""Install the package dependencies.
+    
+    Args:
+        method (str): Method that should be used to install the package dependencies.
+            Valid values include 'conda' and 'pip'.
+        return_commands (bool, optional): If True, the commands necessary to
+            install the dependencies are returned instead of running them. Defaults
+            to False.
+        verbose (bool, optional): If True, setup steps are run with verbosity
+            turned up. Defaults to False.
+        for_development (bool, optional): If True, dependencies are installed that would
+            be missed when installing in development mode. Defaults to False.
+        skip_test_deps (bool, optional): If True, dependencies required for running
+            tests will not be installed. Defaults to False.
+        include_dev_deps (bool, optional): If True, dependencies used during development
+            will be installed. Defaults to False.
+        include_doc_deps (bool, optional): If True, dependencies used during generating
+            documentation will be installed. Defaults to False.
+        windows_package_manager (str, optional): Name of the package
+            manager that should be used on windows. Defaults to 'vcpkg'.
+        install_opts (dict, optional): Mapping from language/package to bool
+            specifying whether or not the language/package should be installed.
+            If not provided, get_install_opts is used to create it.
+        conda_env (str, optional): Name of the conda environment that packages
+            should be installed in. Defaults to None and is ignored.
+        always_yes (bool, optional): If True, conda commands are called with -y flag
+            so that user interaction is not required. Defaults to False.
+        only_python (bool, optional): If True, only Python packages will be installed.
+            Defaults to False.
+        fallback_to_conda (bool, optional): If True, conda will be used to install
+            non-python dependencies and Python dependencies that cannot be installed
+            via pip. Defaults to False.
+
+    """
+    from install_from_requirements import (
+        install_from_requirements, get_pip_dependency_version)
+    install_opts = get_install_opts(install_opts)
+    python_cmd = PYTHON_CMD
+    conda_flags = ''
+    if conda_env:
+        python_cmd = locate_conda_exe(conda_env, 'python')
+        conda_flags += ' --name %s' % conda_env
+    if always_yes:
+        conda_flags += ' -y'
+    # Uninstall default numpy and matplotlib to allow installation
+    # of specific versions
+    cmds = ["%s -m pip uninstall -y numpy matplotlib" % python_cmd]
+    # Get dependencies
+    # install_req = os.path.join("utils", "install_from_requirements.py")
+    conda_pkgs = []
+    pip_pkgs = []
+    os_pkgs = []
+    choco_pkgs = []
+    vcpkg_pkgs = []
+    requirements_files = ['requirements_optional.txt']
+    skip_pkgs = []
+    if method == 'conda':
+        fallback_to_conda = True
+        default_pkgs = conda_pkgs
+    elif method == 'pip':
+        fallback_to_conda = ((_is_win and _on_appveyor) or install_opts['lpy'])
+        default_pkgs = pip_pkgs
+    else:  # pragma: debug
+        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
+                         % method)
+    # In the case of a development environment install requirements that
+    # would be missed when installing in development mode
+    if for_development:
+        if method == 'conda':
+            requirements_files += ['requirements.txt',
+                                   'requirements_condaonly.txt']
+        elif method == 'pip':
+            # requirements.txt not needed because dev install will
+            # pick up and install those deps
+            requirements_files += ['requirements_piponly.txt']
+        default_pkgs.append('ipython')
+        include_dev_deps = True
+    if not skip_test_deps:
+        requirements_files.append('requirements_testing.txt')
+    if include_dev_deps:
+        requirements_files.append('requirements_development.txt')
+    # Refresh channel
+    # https://github.com/conda/conda/issues/8051
+    if fallback_to_conda and _on_gha:
+        cmds += [
+            "%s config --set channel_priority strict" % CONDA_CMD,
+            # "%s install -n root conda=4.9" % CONDA_CMD,
+            # "%s config --set allow_conda_downgrades true" % CONDA_CMD,
+            "%s config --remove channels conda-forge" % CONDA_CMD,
+            "%s config --add channels conda-forge" % CONDA_CMD,
+        ]
+    if fallback_to_conda:
+        cmds.append("%s update --all" % CONDA_CMD)
+    # Required for non-strict channel priority
+    # https://github.com/conda-forge/conda-forge.github.io/pull/670
+    # https://conda.io/projects/conda/en/latest/user-guide/concepts/ ...
+    #    packages.html?highlight=openblas#installing-numpy-with-blas-variants
+    # if fallback_to_conda:
+    #     conda_pkgs.append("\"blas=*=openblas\"")
+    # Installing via pip causes import error on Windows and
+    # a conflict when installing LPy
+    conda_pkgs += ['scipy', os.environ.get('NUMPY', 'numpy')]
+    for k in ['matplotlib', 'jsonschema']:
+        if os.environ.get(k.upper(), k) != k:
+            default_pkgs.append(os.environ[k.upper()])
+    if _is_linux:
+        os_pkgs += ["strace", "valgrind"]
+    # Valgrind failure:
+    # "valgrind: This formula either does not compile or function as expected
+    # on macOS versions newer than High Sierra due to an upstream
+    # incompatibility."
+    # elif _is_osx:
+    #     os_pkgs += ["valgrind"]
+    if install_opts['omp'] and (not fallback_to_conda):
+        if _is_linux:
+            os_pkgs.append('libomp-dev')
+        elif _is_osx:
+            os_pkgs.append('libomp')
+        elif _is_win:
+            pass
+    if install_opts['fortran'] and (not fallback_to_conda):
+        # Fortran is not installed via conda on linux/macos
+        if _is_linux:
+            os_pkgs.append("gfortran")
+        elif _is_osx:
+            os_pkgs.append("gcc")
+            os_pkgs.append("gfortran")
+        elif _is_win and (not fallback_to_conda):
+            choco_pkgs += ["mingw"]
+            # vcpkg_pkgs.append("vcpkg-gfortran")
+    if install_opts['R'] and (not fallback_to_conda):
+        # TODO: Test split installation where r-base is installed from
+        # conda and the R dependencies are installed from CRAN?
+        if _is_linux:
+            cmds += [
+                ("sudo add-apt-repository 'deb https://cloud"
+                 ".r-project.org/bin/linux/ubuntu xenial-cran35/'"),
+                ("sudo apt-key adv --keyserver keyserver.ubuntu.com "
+                 "--recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9")]
+            if not shutil.which('R'):
+                os_pkgs += ["r-base", "r-base-dev"]
+            os_pkgs += ["libudunits2-dev"]
+        elif _is_osx:
+            if not shutil.which('R'):
+                os_pkgs += ["r"]
+            os_pkgs += ["udunits"]
+        elif _is_win:
+            if not shutil.which('R'):
+                choco_pkgs += [
+                    "r.project --params \"\'/AddToPath\'\"",
+                    "rtools"]
+        else:
+            raise NotImplementedError("Could not determine "
+                                      "R installation method.")
+    if install_opts['zmq'] and (not fallback_to_conda):
+        if _is_linux:
+            os_pkgs += ["libczmq-dev", "libzmq3-dev"]
+        elif _is_osx:
+            # if _on_travis:
+            #     cmds.append("bash ci/install-czmq-osx.sh")
+            # else:
+            os_pkgs += ["czmq", "zmq"]
+        elif _is_win:
+            vcpkg_pkgs += ["czmq", "zeromq"]
+        else:
+            raise NotImplementedError("Could not determine "
+                                      "ZeroMQ installation method.")
+        # cmds.append("echo Installing ZeroMQ...")
+        # if _is_linux:
+        #     cmds.append("./ci/install-czmq-linux.sh")
+        # elif _is_osx:
+        #     cmds.append("bash ci/install-czmq-osx.sh")
+        # # elif _is_win:
+        # #     cmds += ["call ci\\install-czmq-windows.bat",
+        # #              "echo \"%PATH%\""]
+        # else:
+        #     raise NotImplementedError("Could not determine "
+        #                               "ZeroMQ installation method.")
+    if include_doc_deps or BUILDDOCS:
+        requirements_files.append('requirements_documentation.txt')
+        if not fallback_to_conda:
+            os_pkgs.append("doxygen")
+    if _on_gha and _is_linux and fallback_to_conda:
+        conda_prefix = '$CONDA_PREFIX'
+        if conda_env:
+            conda_prefix = os.path.join(CONDA_ROOT, 'envs', conda_env)
+        # Do both to ensure that the path is set for the installation
+        # and in following steps
+        cmds += [
+            "export LD_LIBRARY_PATH=%s/lib:$LD_LIBRARY_PATH" % conda_prefix,
+            "echo -n \"LD_LIBRARY_PATH=\" >> $GITHUB_ENV",
+            "echo %s/lib:$LD_LIBRARY_PATH >> $GITHUB_ENV" % conda_prefix
+        ]
+    # Install dependencies using package manager(s)
+    if (not only_python) and (os_pkgs or (_is_win and (choco_pkgs or vcpkg_pkgs))):
+        if _is_linux:
+            cmds += ["sudo apt update"]
+            cmds += ["sudo apt-get install %s" % ' '.join(os_pkgs)]
+        elif _is_osx:
+            if 'gcc' in os_pkgs:
+                cmds += ["brew reinstall gcc"]
+                os_pkgs.remove('gcc')
+            pkgs_from_src = []
+            if _on_travis:
+                for k in ['zmq', 'czmq', 'zeromq']:
+                    if k in os_pkgs:
+                        pkgs_from_src.append(k)
+                        os_pkgs.remove(k)
+            if pkgs_from_src:
+                cmds += ["brew install --build-from-source %s" % ' '.join(pkgs_from_src)]
+            if os_pkgs:
+                cmds += ["brew install %s" % ' '.join(os_pkgs)]
+        elif _is_win:
+            if windows_package_manager == 'choco':
+                choco_pkgs += os_pkgs
+            elif windows_package_manager == 'vcpkg':
+                vcpkg_pkgs += os_pkgs
+            else:
+                raise NotImplementedError("Invalid package manager: '%s'"
+                                          % windows_package_manager)
+            if choco_pkgs:
+                # cmds += ["choco install %s" % ' '.join(choco_pkgs)]
+                for x in choco_pkgs:
+                    cmds.append("choco install %s --force" % x)
+            if vcpkg_pkgs:
+                cmds += ["%s install %s --triplet x64-windows"
+                         % ('vcpkg.exe', ' '.join(vcpkg_pkgs))]
+        else:
+            raise NotImplementedError("No native package manager supported "
+                                      "on Windows.")
+    if install_opts['sbml'] and fallback_to_conda:
+        # Until the sbml package is updated to allow numpy != 1.19.3,
+        # sbml will need to be installed separately without deps in order
+        # to work in a conda env
+        numpy_ver = 'numpy==1.19.3'
+        try:
+            new_numpy_ver = get_pip_dependency_version(
+                'libroadrunner', 'numpy')
+            if new_numpy_ver != numpy_ver:
+                warnings.warn(
+                    "libroadrunner has updated it's numpy requirement. "
+                    "Assuming the new requirement is not strict (%s), "
+                    "this code can be removed and libroadrunner can be "
+                    "installed normally (numpy should be installed with "
+                    "conda if a conda environment is being used to avoid "
+                    "inconsistencies)." % new_numpy_ver)
+            numpy_ver = new_numpy_ver
+        except (ImportError, ModuleNotFoundError):
+            pass
+        conda_pkgs.insert(0, numpy_ver.replace('==', '>='))
+        skip_pkgs.append('libroadrunner')
+    if install_opts['astropy'] and fallback_to_conda and _on_travis:
+        conda_pkgs.insert(0, 'astropy>=4.1')
+    if ((install_opts['fortran'] and fallback_to_conda
+         and (_on_travis or (_on_gha and _is_osx)))):
+        conda_pkgs.append('fortran-compiler')
+    if not fallback_to_conda:
+        default_pkgs += conda_pkgs
+    if requirements_files:
+        kwargs = dict(conda_env=conda_env, python_cmd=python_cmd,
+                      install_opts=install_opts, append_cmds=cmds,
+                      skip_packages=skip_pkgs, verbose=verbose,
+                      verbose_prune=True)
+        install_from_requirements(method, requirements_files,
+                                  additional_packages=default_pkgs, **kwargs)
+        if (method == 'pip') and fallback_to_conda:
+            requirements_files += ['requirements_condaonly.txt']
+            install_from_requirements('conda', list(set(requirements_files)),
+                                      additional_packages=conda_pkgs,
+                                      unique_to_method=True, **kwargs)
+        elif (method == 'conda'):
+            # Adding these ensures that pip-specific packages are installed
+            requirements_files += ['requirements.txt',
+                                   'requirements_piponly.txt']
+            install_from_requirements('pip', list(set(requirements_files)),
+                                      additional_packages=pip_pkgs,
+                                      unique_to_method=True, **kwargs)
+    if 'libroadrunner' in skip_pkgs:
+        pip_flags = '--no-dependencies'
+        if verbose:
+            pip_flags += ' --verbose'
+        cmds.append('%s -m pip install %s libroadrunner'
+                    % (python_cmd, pip_flags))
+    if install_opts['lpy']:
+        if verbose:
+            install_flags = '-vvv'
+        else:
+            install_flags = '-q'
+        install_flags += conda_flags
+        if fallback_to_conda:
+            cmds += ["%s install %s openalea.lpy boost=1.66.0 -c openalea"
+                     % (CONDA_CMD, install_flags)]
+        else:  # pragma: debug
+            raise RuntimeError("Could not detect conda environment. "
+                               "Cannot proceed with a conda deployment "
+                               "(required for LPy).")
+    if return_commands:
+        return cmds
+    cmds = SUMMARY_CMDS + cmds + SUMMARY_CMDS
+    call_script(cmds)
+
+
+def install_pkg(method, python=None, without_build=False,
+                without_deps=False, verbose=False,
+                skip_test_deps=False, include_dev_deps=False, include_doc_deps=False,
+                windows_package_manager='vcpkg', install_opts=None, conda_env=None,
+                always_yes=False, only_python=False, fallback_to_conda=False):
+    r"""Build and install the package and its dependencies on a CI
+    resource.
+
+    Args:
+        method (str): Method that should be used to build and install
+            the package. Valid values include 'conda' and 'pip'.
+        python (str, optional): Version of Python that package should be
+            built for. Defaults to None if not provided and the current
+            version of Python will be used. This will be ignored if
+            without_build is True.
+        without_build (bool, optional): If True, the package will not be
+            built prior to install. Defaults to False.
+        without_deps (bool, optional): If True the package dependencies will
+            no be installed prior to installing the package. Defaults to
+            False.
+        verbose (bool, optional): If True, setup steps are run with verbosity
+            turned up. Defaults to False.
+        skip_test_deps (bool, optional): If True, dependencies required for running
+            tests will not be installed. Defaults to False.
+        include_dev_deps (bool, optional): If True, dependencies used during development
+            will be installed. Defaults to False.
+        include_doc_deps (bool, optional): If True, dependencies used during generating
+            documentation will be installed. Defaults to False.
+        windows_package_manager (str, optional): Name of the package
+            manager that should be used on windows. Defaults to 'vcpkg'.
+        install_opts (dict, optional): Mapping from language/package to bool
+            specifying whether or not the language/package should be installed.
+            If not provided, get_install_opts is used to create it.
+        conda_env (str, optional): Name of the conda environment that packages
+            should be installed in. Defaults to None and is ignored.
+        always_yes (bool, optional): If True, conda commands are called with -y flag
+            so that user interaction is not required. Defaults to False.
+        only_python (bool, optional): If True, only Python packages will be installed.
+            Defaults to False.
+        fallback_to_conda (bool, optional): If True, conda will be used to install
+            non-python dependencies and Python dependencies that cannot be installed
+            via pip. Defaults to False.
+
+    Raises:
+        ValueError: If method is not 'conda' or 'pip'.
+
+    """
+    install_opts = get_install_opts(install_opts)
+    python_cmd = PYTHON_CMD
+    conda_flags = ''
+    if conda_env:
+        python_cmd = locate_conda_exe(conda_env, 'python')
+        conda_flags += ' --name %s' % conda_env
+    if always_yes:
+        conda_flags += ' -y'
+    cmds = []
+    if method.endswith('-dev'):
+        method_base = method.split('-dev')[0]
+        for_development = True
+        without_build = True
+    else:
+        method_base = method
+        for_development = False
+    if not without_build:
+        cmds += build_pkg(method_base, python=python,
+                          return_commands=True, verbose=verbose)
+        cmds += SUMMARY_CMDS
+    if not without_deps:
+        cmds += install_deps(method_base, return_commands=True, verbose=verbose,
+                             for_development=for_development,
+                             skip_test_deps=skip_test_deps,
+                             include_dev_deps=include_dev_deps,
+                             include_doc_deps=include_doc_deps,
+                             install_opts=install_opts,
+                             windows_package_manager=windows_package_manager,
+                             conda_env=conda_env, always_yes=always_yes,
+                             only_python=only_python,
+                             fallback_to_conda=fallback_to_conda)
+        cmds += SUMMARY_CMDS
+    # Install yggdrasil
+    if method == 'conda':
+        assert(CONDA_INDEX and os.path.isdir(CONDA_INDEX))
+        # Install from conda build
+        # Assumes that the target environment is active
+        if verbose:
+            install_flags = '-vvv'
+        else:
+            install_flags = '-q'
+        install_flags += conda_flags
+        if _is_win:
+            index_channel = CONDA_INDEX
+        else:
+            index_channel = "file:/%s" % CONDA_INDEX
+        cmds += [
+            # Related issues if this stops working again
+            # https://github.com/conda/conda/issues/466#issuecomment-378050252
+            "%s install %s --update-deps -c %s yggdrasil" % (
+                CONDA_CMD, install_flags, index_channel)
+            # Required for non-strict channel priority
+            # https://github.com/conda-forge/conda-forge.github.io/pull/670
+            # https://conda.io/projects/conda/en/latest/user-guide/concepts/ ...
+            #    packages.html?highlight=openblas#installing-numpy-with-blas-variants
+            # "%s install %s --update-deps -c %s yggdrasil \"blas=*=openblas\"" % (
+            #     CONDA_CMD, install_flags, index_channel)
+        ]
+    elif method == 'pip':
+        if verbose:
+            install_flags = '--verbose'
+        else:
+            install_flags = ''
+        if _is_win:  # pragma: windows
+            cmds += [
+                "for %%a in (\"dist\\*.tar.gz\") do set YGGSDIST=%%a",
+                "echo %YGGSDIST%"
+            ]
+            sdist = "%YGGSDIST%"
+        else:
+            sdist = "dist/*.tar.gz"
+        cmds += [
+            "%s -m pip install %s %s" % (python_cmd, install_flags, sdist),
+            "%s create_coveragerc.py" % python_cmd
+        ]
+    elif method.endswith('-dev'):
+        # Call setup.py in separate process from the package directory
+        # cmds += ["%s setup.py develop" % python_cmd]
+        pass
+    else:  # pragma: debug
+        raise ValueError("Invalid method: '%s'" % method)
+    # Print summary of what was installed
+    cmds = SUMMARY_CMDS + cmds + SUMMARY_CMDS
+    call_script(cmds)
+    if method.endswith('-dev'):
+        print(call_conda_command([python_cmd, 'setup.py', 'develop'],
+                                 cwd=_pkg_dir))
+    # Follow up if on Unix as R installation may require sudo
+    if install_opts['R'] and _is_unix:
+        # cmds.append('ygginstall r --sudoR')
+        subprocess.check_call(["ygginstall", "r", "--sudoR"])
+    if method == 'conda':
+        src_dir = os.path.join(os.getcwd(),
+                               os.path.dirname(os.path.dirname(__file__)))
+        subprocess.check_call([python_cmd, "create_coveragerc.py"], cwd=src_dir)
+
+
+def verify_pkg(install_opts=None):
+    r"""Verify that the package was installed correctly.
+
+    Args:
+        install_opts (dict, optional): Mapping from language/package to bool
+            specifying whether or not the language/package should be installed.
+            If not provided, get_install_opts is used to create it.
+
+    """
+    install_opts = get_install_opts(install_opts)
+    if _is_win and (not install_opts['zmq']):
+        install_opts['c'] = False
+        install_opts['fortran'] = False
+    elif (not install_opts['fortran']) and shutil.which('gfortran'):
+        install_opts['fortran'] = True
+    if (not install_opts['R']) and shutil.which('Rscript'):
+        if not (_on_gha and _is_linux):
+            # The installation on GHA-ubuntu machines requires sudo
+            # and so installation will not be complete unless it is
+            # enabled explicitly. This does not seem to be True on
+            # GHA-macos builds and R is not installed by default on
+            # Travis/Appveyor.
+            install_opts['R'] = True
+    if (not install_opts['c']) and shutil.which('gcc') and (not _is_win):
+        install_opts['c'] = True
+    src_dir = os.path.join(os.getcwd(),
+                           os.path.dirname(os.path.dirname(__file__)))
+    src_version = subprocess.check_output(
+        ["python", "-c",
+         "'import versioneer; print(versioneer.get_version())'"],
+        cwd=src_dir)
+    bld_version = subprocess.check_output(
+        ["python", "-c",
+         "'import yggdrasil; print(yggdrasil.__version__)'"],
+        cwd=os.path.dirname(src_dir))
+    if src_version != bld_version:
+        raise RuntimeError("Installed version does not match the version of "
+                           "this source code.\n"
+                           "\tSource version: %s\n\tBuild  version: %s"
+                           % (src_version, bld_version))
+    if install_opts['R']:
+        assert(shutil.which("R"))
+        assert(shutil.which("Rscript"))
+    subprocess.check_call(["flake8", "yggdrasil"], cwd=src_dir)
+    if not os.path.isfile(".coveragerc"):
+        raise RuntimeError(".coveragerc file dosn't exist.")
+    with open(".coveragerc", "r") as fd:
+        print(fd.read())
+    subprocess.check_call(["ygginfo", "--verbose"], cwd=src_dir)
+    if install_opts['c']:
+        subprocess.check_call(["yggccflags"], cwd=src_dir)
+        subprocess.check_call(["yggldflags"], cwd=src_dir)
+    # Verify that languages are installed
+    sys.stdout.flush()
+    from yggdrasil.tools import is_lang_installed, is_comm_installed
+    errors = []
+    for name in ['c', 'R', 'fortran', 'sbml', 'lpy']:
+        flag = install_opts[name]
+        if flag and (not is_lang_installed(name)):
+            errors.append("Language '%s' should be installed, but is not."
+                          % name)
+        elif (not flag) and is_lang_installed(name):
+            errors.append("Language '%s' should NOT be installed, but is."
+                          % name)
+    for name in ['zmq', 'rmq']:
+        flag = install_opts[name]
+        if name == 'rmq':
+            language = 'python'  # rmq dosn't work for C
+        else:
+            language = None
+        if flag and (not is_comm_installed(name, language=language)):
+            errors.append("Comm '%s' should be installed, but is not." % name)
+        elif (not flag) and is_comm_installed(name, language=language):
+            errors.append("Comm '%s' should NOT be installed, but is." % name)
+    if errors:
+        raise AssertionError("One or more languages was not installed as "
+                             "expected\n\t%s" % "\n\t".join(errors))
+    if _is_win:  # pragma: windows
+        if os.environ.get('HOMEDRIVE', None):
+            assert(os.path.expanduser('~').startswith(os.environ['HOMEDRIVE']))
+        else:
+            assert(os.path.expanduser('~').lower().startswith('c:'))
+
+
+def log_environment(new_filename='new_environment_log.txt',
+                    old_filename='old_environment_log.txt'):
+    r"""Create a record of the Python package versions.
+
+    Args:
+        new_filename (str, optional): Name of the file where the
+            environment information should be stored. Defaults to
+            'new_environment_log.txt'.
+        old_filename (str, optional): Name of the file where previous
+            environment information is stored for diff. Defaults to
+            'old_environment_log.txt'.
+
+    """
+    if os.path.isfile(new_filename):
+        if os.path.isfile(old_filename):
+            raise RuntimeError("Package list already exists: '%s'"
+                               % new_filename)
+        else:
+            shutil.move(new_filename, old_filename)
+    now = datetime.now()
+    cmds = ["echo \"%s\" >> %s" % (now.strftime("%Y/%m/%d %H:%M:%S"),
+                                   new_filename),
+            "%s --version >> %s" % (PYTHON_CMD, new_filename),
+            "%s -m pip list >> %s" % (PYTHON_CMD, new_filename)]
+    if shutil.which('conda'):
+        cmds.append("%s list >> %s" % (CONDA_CMD, new_filename))
+    call_script(cmds)
+    assert(os.path.isfile(new_filename))
+    if os.path.isfile(old_filename):
+        with open(new_filename, 'r') as fd:
+            new_contents = fd.read()
+        with open(old_filename, 'r') as fd:
+            old_contents = fd.read()
+        diff = difflib.ndiff(old_contents.splitlines(),
+                             new_contents.splitlines())
+        print('\n'.join(diff))
+
+
+if __name__ == "__main__":
+    install_opts = get_install_opts()
+    parser = argparse.ArgumentParser(
+        "Perform setup operations to test package build and "
+        "installation on continuous integration services.")
+    subparsers = parser.add_subparsers(
+        dest='operation',
+        help="CI setup operation that should be performed.")
+    # Environment creation
+    parser_env = subparsers.add_parser(
+        'env', help="Setup an environment for testing.")
+    parser_env.add_argument(
+        'method', choices=['conda', 'virtualenv'],
+        help=("Method that should be used to create "
+              "the test environment."))
+    parser_env.add_argument(
+        'python',
+        help="Version of python that should be tested.")
+    parser_env.add_argument(
+        '--env-name', default=None,
+        help="Name that should be used for the environment.")
+    # Build package
+    parser_bld = subparsers.add_parser(
+        'build', help="Build the package.")
+    parser_bld.add_argument(
+        'method', choices=['conda', 'pip'],
+        help=("Method that should be used to build the package."))
+    parser_bld.add_argument(
+        '--python', default=None,
+        help="Version of python that package should be built for.")
+    parser_bld.add_argument(
+        '--verbose', action='store_true',
+        help="Turn up verbosity of output.")
+    # Install dependencies
+    parser_dep = subparsers.add_parser(
+        'deps', help="Install the package dependencies.")
+    parser_dep.add_argument(
+        'method', choices=['conda', 'pip'],
+        help=("Method that should be used to install the package dependencies."))
+    parser_dep.add_argument(
+        '--verbose', action='store_true',
+        help="Turn up verbosity of output.")
+    parser_dep.add_argument(
+        '--for-development', action='store_true',
+        help=("Install dependencies used during development and "
+              "that would be missed when installing in development mode. "
+              "Implies --include-dev-deps"))
+    parser_dep.add_argument(
+        '--skip-test-deps', action='store_true',
+        help="Don't install dependencies used for testing.")
+    parser_dep.add_argument(
+        '--include-dev-deps', action='store_true',
+        help="Install dependencies used during development.")
+    parser_dep.add_argument(
+        '--include-doc-deps', action='store_true',
+        help="Install dependencies used during doc generation.")
+    parser_dep.add_argument(
+        '--windows-package-manager', default='vcpkg',
+        help="Package manager that should be used on Windows.",
+        choices=['vcpkg', 'choco'])
+    parser_dep.add_argument(
+        '--conda-env', default=None,
+        help="Conda environment that dependencies should be installed in.")
+    parser_dep.add_argument(
+        '--always-yes', action='store_true',
+        help="Pass -y to conda commands to avoid user interaction.")
+    parser_dep.add_argument(
+        '--only-python', '--python-only', action='store_true',
+        help="Only install python dependencies.")
+    for k, v in install_opts.items():
+        if v:
+            parser_dep.add_argument(
+                '--dont-install-%s' % k, action='store_true',
+                help=("Don't install %s" % k))
+        else:
+            parser_dep.add_argument(
+                '--install-%s' % k, action='store_true',
+                help=("Install %s" % k))
+    # Install package
+    parser_pkg = subparsers.add_parser(
+        'install', help="Install the package.")
+    parser_pkg.add_argument(
+        'method', choices=['conda', 'pip', 'conda-dev', 'pip-dev'],
+        help=("Method that should be used to install the package."))
+    parser_pkg.add_argument(
+        '--python', default=None,
+        help="Version of python that package should be built/installed for.")
+    parser_pkg.add_argument(
+        '--without-build', action='store_true',
+        help=("Perform installation steps without building first. (Assumes "
+              "the package has already been built)."))
+    parser_pkg.add_argument(
+        '--without-deps', action='store_true',
+        help=("Perform installation steps without installing dependencies first. "
+              "(Assumes they have already been installed)."))
+    parser_pkg.add_argument(
+        '--verbose', action='store_true',
+        help="Turn up verbosity of output.")
+    parser_pkg.add_argument(
+        '--skip-test-deps', action='store_true',
+        help="Don't install dependencies used for testing.")
+    parser_pkg.add_argument(
+        '--include-dev-deps', action='store_true',
+        help="Install dependencies used during development.")
+    parser_pkg.add_argument(
+        '--include-doc-deps', action='store_true',
+        help="Install dependencies used during doc generation.")
+    parser_pkg.add_argument(
+        '--windows-package-manager', default='vcpkg',
+        help="Package manager that should be used on Windows.",
+        choices=['vcpkg', 'choco'])
+    parser_pkg.add_argument(
+        '--conda-env', default=None,
+        help="Conda environment that the package should be installed in.")
+    parser_pkg.add_argument(
+        '--always-yes', action='store_true',
+        help="Pass -y to conda commands to avoid user interaction.")
+    parser_pkg.add_argument(
+        '--only-python', '--python-only', action='store_true',
+        help="Only install python dependencies.")
+    for k, v in install_opts.items():
+        if v:
+            parser_pkg.add_argument(
+                '--dont-install-%s' % k, action='store_true',
+                help=("Don't install %s" % k))
+        else:
+            parser_pkg.add_argument(
+                '--install-%s' % k, action='store_true',
+                help=("Install %s" % k))
+    # Installation verification
+    parser_ver = subparsers.add_parser(
+        'verify', help="Verify that the package was installed correctly.")
+    for k, v in install_opts.items():
+        if v:
+            parser_ver.add_argument(
+                '--dont-install-%s' % k, action='store_true',
+                help=("Verify that %s is not installed." % k))
+        else:
+            parser_ver.add_argument(
+                '--install-%s' % k, action='store_true',
+                help=("Verify that %s is installed." % k))
+    # Environment logging
+    parser_log = subparsers.add_parser(
+        'log', help="Create a log of the Python environment.")
+    parser_log.add_argument(
+        '--new-filename', default='new_environment_log.txt',
+        help="File where the new environment log should be saved.")
+    parser_log.add_argument(
+        '--old-filename', default='old_environment_log.txt',
+        help=("File containing previous environment log that the new "
+              "log should be diffed against."))
+    # Call methods
+    args = parser.parse_args()
+    if args.operation in ['deps', 'install', 'verify']:
+        new_opts = {}
+        for k, v in install_opts.items():
+            if v and getattr(args, 'dont_install_%s' % k, False):
+                new_opts[k] = False
+            elif (not v) and getattr(args, 'install_%s' % k, False):
+                new_opts[k] = True
+        install_opts.update(new_opts)
+    if args.operation in ['env', 'setup']:
+        create_env(args.method, args.python, name=args.env_name)
+    elif args.operation == 'build':
+        build_pkg(args.method, python=args.python,
+                  verbose=args.verbose)
+    elif args.operation == 'deps':
+        install_deps(args.method, verbose=args.verbose,
+                     skip_test_deps=args.skip_test_deps,
+                     include_dev_deps=args.include_dev_deps,
+                     include_doc_deps=args.include_doc_deps,
+                     for_development=args.for_development,
+                     windows_package_manager=args.windows_package_manager,
+                     install_opts=install_opts,
+                     conda_env=args.conda_env, always_yes=args.always_yes,
+                     only_python=args.only_python)
+    elif args.operation == 'install':
+        install_pkg(args.method, python=args.python,
+                    without_build=args.without_build,
+                    without_deps=args.without_deps,
+                    verbose=args.verbose,
+                    skip_test_deps=args.skip_test_deps,
+                    include_dev_deps=args.include_dev_deps,
+                    include_doc_deps=args.include_doc_deps,
+                    windows_package_manager=args.windows_package_manager,
+                    install_opts=install_opts,
+                    conda_env=args.conda_env, always_yes=args.always_yes,
+                    only_python=args.only_python)
+    elif args.operation == 'verify':
+        verify_pkg(install_opts=install_opts)
+    elif args.operation == 'log':
+        log_environment(new_filename=args.new_filename,
+                        old_filename=args.old_filename)
diff --git a/utils/test-install-base.yml b/utils/test-install-base.yml
new file mode 100644
index 000000000..fc1adec6f
--- /dev/null
+++ b/utils/test-install-base.yml
@@ -0,0 +1,437 @@
+name: Test of package installation & execution
+
+'on':  # quotes required so that pyyaml doesn't make this a bool
+  push:
+    branches-ignore:
+      - 'gh-pages'
+    tags:
+      - '*'
+  schedule:
+    - cron: "0 10 * * 1"  # 3AM PST
+
+env:
+  NUMPY: numpy
+  MATPLOTLIB: matplotlib
+  MPLBACKEND: agg
+  JSONSCHEMA: jsonschema
+  INSTALLR: 1
+  INSTALLC: 1
+  INSTALLFORTRAN: 1
+  INSTALLSBML: 1
+  INSTALLLPY: 0
+  INSTALLZMQ: 1
+  INSTALLRMQ: 0
+  INSTALLTRIMESH: 1
+  INSTALLPYGMENTS: 1
+  INSTALLOMP: 1
+  USEVIRTUALENV: 0
+  BUILDDOCS: 0
+  YGG_GHA_JOB_ID_SUFFIX:
+
+jobs:
+  test_pip:
+    name: Test (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method }}, ${{ matrix.test-flags1 }}, ${{ matrix.test-flags2 }}), Install C = ${{ matrix.install-c }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      max-parallel: 20
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: [3.6]
+        install-method: [pip]
+        test-flags1: [--long-running --test-suite=timing --test-suite=demos --test-suite=top, --test-suite=examples, --test-suite=types --skip-languages osr]
+        test-flags2: [""]
+        install-c: [true]
+        include:
+          - os: windows-latest
+            python-version: 3.6
+            install-method: pip
+            test-flags1: --long-running --languages cpp
+            test-flags2: --long-running --skip-languages c++
+            install-c: false
+      fail-fast: false
+
+    steps: &steps
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set C installation based on matrix value
+      if: matrix.install-c != true && matrix.install-method != 'conda'
+      run: echo "INSTALLC=0" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: Store an ID string for the job
+      run: echo "YGG_GHA_JOB_ID=${{ matrix.os }}-${{ matrix.install-method }}-py${{ matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{ env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (WINDOWS) Patch TEMP/TMPDIR on Windows
+      if: matrix.os == 'windows-latest'
+      run: |
+        echo "TMPDIR=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+        echo "TEMP=$USERPROFILE\\AppData\\Local\\Temp" >> $GITHUB_ENV
+      shell: bash -l {0}
+
+    # ========================================================
+    # Pip specific installation steps
+    # ========================================================
+    - name: (WINDOWS,PIP) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC == 1
+      with:
+        toolset: 14.0
+    - name: Install R
+      uses: r-lib/actions/setup-r@v1
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR == 1
+    # - name: (WINDOWS,PIP) Set VCPKG_ROOT on Windows
+    #   if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC == 1
+    #   run: echo "VCPKG_ROOT=C:\\vcpkg" >> $GITHUB_ENV
+    #   shell: bash -l {0}
+    - name: (LINUX,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
+      with:
+        path: ~/.cache/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: |
+          ${{ runner.os }}-pip-
+    - name: (MACOS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
+      with:
+        path: ~/Library/Caches/pip
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: |
+          ${{ runner.os }}-pip-
+    - name: (WINDOWS,PIP) Cache pip
+      uses: actions/cache@v2
+      if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
+      with:
+        path: ~\AppData\Local\pip\Cache
+        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+        restore-keys: |
+          ${{ runner.os }}-pip-
+    - name: (PIP) Set up Python
+      uses: actions/setup-python@v2
+      if: matrix.install-method == 'pip'
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: (PIP) Set up Virtual Env
+      if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
+      uses: syphar/restore-virtualenv@v1
+      # run: |
+      #   python utils/setup_test_env.py env virtualenv ${{ matrix.python-version }} --env-name=test
+      #   source test/bin/activate
+    - name: (PIP) Install dependencies & package
+      if: matrix.install-method == 'pip'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }}
+    - name: (WINDOWS,PIP) Set the vcpkg root config value
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLC == 1
+      run: yggconfig --vcpkg-dir=C:\\vcpkg
+    - name: (WINDOWS,PIP) Update R requirements
+      if: matrix.os == 'windows-latest' && matrix.install-method == 'pip' && env.INSTALLR == 1
+      run: ygginstall R --update-r-requirements
+    # ========================================================
+
+    # ========================================================
+    # Conda specific installation steps
+    # ========================================================
+    - name: (CONDA) Cache conda
+      uses: actions/cache@v1
+      if: matrix.install-method == 'conda'
+      env:
+        # Increase this value to reset cache if file not changed
+        CACHE_NUMBER: 0
+      with:
+        path: ~/conda_pkgs_dir
+        key:
+          ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{
+          hashFiles('recipe/meta.yaml') }}
+    - name: (WINDOWS,CONDA) Set up miniconda base environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest'
+      with:
+        channels: conda-forge
+        use-only-tar-bz2: true
+        activate-environment: ''
+        # auto-activate-base: false
+    # The compiler must be initialized after the base environment for
+    # conda so that it dosn't interfere with the base environment
+    # initialization
+    # This step might need to stay here so that dev prompt comes before
+    # conda?
+    - name: (WINDOWS,CONDA) Set up MSVC Compiler
+      uses: ilammy/msvc-dev-cmd@v1
+      if: matrix.install-method == 'conda' && matrix.os == 'windows-latest' && env.INSTALLC == 1
+      with:
+        toolset: 14.0
+    # This step and the next allows conda to be called before the
+    # conda-incubator/setup-miniconda@v2 action is used to create the
+    # test environment so that the package can be built in the base
+    # environment.
+    - name: (UNIX,CONDA) Add miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: |
+        echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
+        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (MACOS,CONDA) Change ownership of conda on MacOS
+      if: matrix.install-method == 'conda' && matrix.os == 'macos-latest'
+      run: sudo chown -R $(whoami) $CONDA
+      shell: bash
+    - name: (CONDA) Build package in base environment
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python ${{ matrix.python-version }}
+      shell: bash --noprofile --norc -eo pipefail {0}
+    - name: (UNIX,CONDA) Remove miniconda bin to path
+      if: matrix.install-method == 'conda' && matrix.os != 'windows-latest'
+      run: |
+        echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+      shell: bash -l {0}
+    - name: (CONDA) Set up miniconda test environment
+      uses: conda-incubator/setup-miniconda@v2
+      if: matrix.install-method == 'conda'
+      with:
+        # TODO: Move autoupdate into script?
+        # auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
+        channels: conda-forge
+        use-only-tar-bz2: true
+        channel-priority: strict
+    - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda llvm (7)
+      if: matrix.os == 'macos-latest' && matrix.install-method == 'conda'
+      run: ./utils/setup_old_mac_sdk.sh
+    - name: (CONDA) Install dependencies & package from existing build
+      if: matrix.install-method == 'conda'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+    # ========================================================
+    
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Compile libraries
+      run: yggcompile
+    - name: Run cmake test in command prompt
+      if: matrix.os == 'windows-latest'
+      shell: cmd /C CALL {0}
+      run: |
+        cmake -h
+        yggtest --ci --nocapture drivers/tests/test_CMakeModelDriver.py
+    - name: Run tests (1st)
+      id: test1
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }}
+    - name: Re-run test failures (1st)
+      if: ${{ failure() && steps.test1.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
+    - name: Run tests (2nd)
+      if: matrix.test-flags2 != ''
+      id: test2
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }}
+    - name: Re-run test failures (2nd)
+      if: ${{ failure() && steps.test2.outcome == 'failure' }}
+      run: yggtest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
+    # - name: Start SSH session
+    #   uses: luchihoratiu/debug-via-ssh@main
+    #   if: ${{ failure() && matrix.install-method == 'conda' && matrix.os == 'macos-latest' }}
+    #   with:
+    #     NGROK_AUTH_TOKEN: ${{ secrets.NGROK_AUTH_TOKEN }}
+    #     SSH_PASS: ${{ secrets.SSH_PASS }}
+    - name: Combine coverage reports
+      continue-on-error: true
+      run: |
+        ls .*
+        coverage combine --append
+    - name: (CONDA) Generate XML version of coverage report
+      if: matrix.install-method == 'conda'
+      run: |
+        ls .*
+        coverage xml
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v1
+      with:
+        name: ${{ env.YGG_GHA_JOB_ID }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+    - name: Download the last successful workflow environment log
+      if: ${{ always() }}
+      continue-on-error: true
+      uses: dawidd6/action-download-artifact@v2
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        workflow: test-install.yml
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: .
+    - name: Create a log of Python package versions
+      if: ${{ always() }}
+      run: python utils/setup_test_env.py log
+    - uses: actions/upload-artifact@v2
+      if: ${{ success() }}
+      with:
+        name: environment-log-${{ env.YGG_GHA_JOB_ID }}
+        path: new_environment_log.txt
+
+  test_conda:
+    name: Test (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method }}, ${{ matrix.test-flags1 }}, ${{ matrix.test-flags2 }}), Install C = ${{ matrix.install-c }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      max-parallel: 20
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: [3.6]
+        install-method: [conda]
+        test-flags1: [--long-running]
+        test-flags2: [--with-examples examples/tests/test_backwards.py]
+        install-c: [true]
+        include:
+          - os: ubuntu-latest
+            python-version: 3.7
+            install-method: conda
+            test-flags1: --long-running
+            test-flags2: ""
+            install-c: true
+          - os: ubuntu-latest
+            python-version: 3.8
+            install-method: conda
+            test-flags1: --long-running
+            test-flags2: ""
+            install-c: true
+          - os: ubuntu-latest
+            python-version: 3.9
+            install-method: conda
+            test-flags1: --long-running
+            test-flags2: ""
+            install-c: true
+          - os: macos-latest
+            python-version: 3.8
+            install-method: conda
+            test-flags1: tests/test_tools.py --with-examples examples/tests/test_rpc_lesson3.py
+            test-flags2: ""
+            install-c: true
+      fail-fast: false
+    defaults:
+      run:
+        shell: bash -l {0}
+    steps: *steps
+
+  test_rmq_pip:
+    name: Test RMQ (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method }})
+    runs-on: ${{ matrix.os }}
+    env:
+      INSTALLRMQ: 1
+      INSTALLAPY: 1
+      USEVIRTUALENV: 1
+      YGG_DEFAULT_COMM: IPCComm
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        python-version: [3.6]
+        install-method: [pip]
+        test-flags1: [--long-running --languages python R matlab]
+        test-flags2: [""]
+        install-c: [true]
+      fail-fast: false
+    services: &rmq_services
+      rabbitmq:
+        image: rabbitmq:latest
+        ports:
+          - 5672:5672
+        options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s --health-timeout 5s --health-retries 5
+    steps: *steps
+    
+  test_rmq_conda:
+    name: Test RMQ (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method }})
+    runs-on: ${{ matrix.os }}
+    env:
+      INSTALLRMQ: 1
+      INSTALLAPY: 1
+      USEVIRTUALENV: 1
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        python-version: [3.6]
+        install-method: [conda]
+        test-flags1: [--long-running --languages python R matlab]
+        test-flags2: [""]
+        install-c: [true]
+      fail-fast: false
+    services: *rmq_services
+    defaults:
+      run:
+        shell: bash -l {0}
+    steps: *steps
+    
+  test_stripped:
+    name: Test stripped install (${{ matrix.python-version }}, ${{ matrix.os }}, ${{ matrix.install-method }})
+    runs-on: ${{ matrix.os }}
+    env:
+      INSTALLR: 0
+      INSTALLC: 0
+      INSTALLFORTRAN: 0
+      INSTALLLPY: 0
+      INSTALLZMQ: 0
+      INSTALLRMQ: 0
+      INSTALLTRIMESH: 0
+      INSTALLPYGMENTS: 0
+      YGG_GHA_JOB_ID_SUFFIX: -stripped
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: [3.6]
+        install-method: [pip]
+        test-flags1: [--long-running]
+        test-flags2: [""]
+        install-c: [false]
+      fail-fast: false
+    steps: *steps
+
+  docs:
+    name: Build the Docs
+    runs-on: ubuntu-latest
+    env:
+      BUILDDOCS: 1
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.8
+    - name: Install dependencies
+      run: python utils/setup_test_env.py install pip
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Build the docs
+      run: |
+        cd docs
+        make autodoc
+        cd ../
+    - name: Publish docs to Github pages
+      if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+      uses: JamesIves/github-pages-deploy-action@3.7.1
+      with:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        BRANCH: gh-pages
+        FOLDER: docs/build/html/
+  deploy:
+    name: Publish package
+    needs: [test_pip, test_conda, test_rmq_pip, test_rmq_conda, test_stripped, docs]
+    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        submodules: true
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.8
+    - name: Install dependencies
+      run: python utils/setup_test_env.py install pip
+    - name: Verify installation
+      run: python utils/setup_test_env.py verify
+    - name: Build the package
+      run: |
+        python setup.py sdist
+        python setup.py bdist_wheel
+    - name: Publish package to PyPI
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        user: __token__
+        password: ${{ secrets.pypi }}
+        skip_existing: true
+        
diff --git a/yggdrasil/communication/tests/test_ZMQComm.py b/yggdrasil/communication/tests/test_ZMQComm.py
index 154fa0bc6..693cc03a7 100644
--- a/yggdrasil/communication/tests/test_ZMQComm.py
+++ b/yggdrasil/communication/tests/test_ZMQComm.py
@@ -1,5 +1,4 @@
 import unittest
-import zmq
 import copy
 from yggdrasil import platform
 from yggdrasil.tests import assert_raises, assert_equal
@@ -48,6 +47,7 @@ def test_invalid_protocol():
 @unittest.skipIf(platform._is_win, "Testing on Windows")
 def test_error_on_send_open_twice():
     r"""Test creation of the same send socket twice for an error."""
+    import zmq
     for s, r in ZMQComm._socket_type_pairs:
         # Send comm
         name1 = 'test_%s' % s
diff --git a/yggdrasil/drivers/tests/test_CModelDriver.py b/yggdrasil/drivers/tests/test_CModelDriver.py
index f823a6978..bb414465d 100644
--- a/yggdrasil/drivers/tests/test_CModelDriver.py
+++ b/yggdrasil/drivers/tests/test_CModelDriver.py
@@ -130,6 +130,9 @@ def test_GCCCompiler_dll2a(self):
                 kws['libtype'] = 'shared'
             dll = self.import_cls.get_dependency_library('python', **kws)
             gcc.dll2a(dll, overwrite=True)
+        else:
+            self.assert_raises(ValueError, self.import_cls.get_tool,
+                               'compiler', toolname='gcc')
 
 
 class TestCModelDriverNoStart(TestCModelParam,
diff --git a/yggdrasil/drivers/tests/test_ConnectionDriver.py b/yggdrasil/drivers/tests/test_ConnectionDriver.py
index 8e9bb4eb0..58fab6df7 100644
--- a/yggdrasil/drivers/tests/test_ConnectionDriver.py
+++ b/yggdrasil/drivers/tests/test_ConnectionDriver.py
@@ -1,4 +1,5 @@
 import unittest
+import pytest
 from yggdrasil import tools, platform
 from yggdrasil.tests import MagicTestError, assert_raises
 from yggdrasil.schema import get_schema
@@ -336,12 +337,17 @@ def setup(self, *args, **kwargs):
             assert(self.recv_comm.is_open)
         self.nmsg_recv = 1
 
+    def test_init_del(self):
+        r"""Test driver creation and deletion."""
+        self.instance.printStatus(verbose=True)
+
     def test_early_close(self):
         r"""Test early deletion of message queue."""
         self.instance.close_comm()
         self.instance.open_comm()
         assert(self.instance.is_comm_closed)
 
+    @pytest.mark.timeout(timeout=600)
     def test_send_recv(self):
         r"""Test sending/receiving small message."""
         flag = self.send_comm.send(self.test_msg)
@@ -358,6 +364,7 @@ def test_send_recv(self):
         if self.comm_name != 'CommBase':
             self.assert_equal(self.instance.n_msg, 0)
 
+    @pytest.mark.timeout(timeout=600)
     def test_send_recv_nolimit(self):
         r"""Test sending/receiving large message."""
         assert(len(self.msg_long) > self.maxMsgSize)
diff --git a/yggdrasil/drivers/tests/test_ServerDriver.py b/yggdrasil/drivers/tests/test_ServerDriver.py
index 6a159bf39..0dfb1d531 100644
--- a/yggdrasil/drivers/tests/test_ServerDriver.py
+++ b/yggdrasil/drivers/tests/test_ServerDriver.py
@@ -144,6 +144,7 @@ def test_client_count(self):
         # Clean up
         cli_drv2.terminate()
 
+    @pytest.mark.timeout(600)
     def test_send_recv(self, msg_send=None):
         r"""Test routing of a short message between client and server."""
         if msg_send is None:
@@ -168,7 +169,7 @@ def test_send_recv(self, msg_send=None):
         assert_equal(cli_msg, msg_send)
 
     @flaky.flaky(max_runs=3)
-    @pytest.mark.timeout(60)
+    @pytest.mark.timeout(600)
     def test_send_recv_nolimit(self):
         r"""Test routing of a large message between client and server."""
         self.test_send_recv(msg_send=self.msg_long)
diff --git a/yggdrasil/examples/tests/__init__.py b/yggdrasil/examples/tests/__init__.py
index 5393708e8..f9d2a579f 100644
--- a/yggdrasil/examples/tests/__init__.py
+++ b/yggdrasil/examples/tests/__init__.py
@@ -6,6 +6,7 @@
 import shutil
 import itertools
 import flaky
+import pytest
 from yggdrasil.components import ComponentMeta, import_component
 from yggdrasil import runner, tools, platform
 from yggdrasil.examples import (
@@ -92,6 +93,7 @@ def __new__(cls, name, bases, dct):
         if dct.get('example_name', None) is not None:
             dct.setdefault('iter_list_language',
                            get_example_languages(dct['example_name']))
+        timeout = dct.get('timeout', 600)
         iter_lists = []
         iter_keys = []
         test_name_fmt = 'test'
@@ -125,6 +127,9 @@ def __new__(cls, name, bases, dct):
                         **{k: v for k, v in
                            zip(iter_keys, x)})
                     itest_func.__name__ = itest_name
+                    if timeout is not None:
+                        itest_func = pytest.mark.timeout(timeout=timeout)(
+                            itest_func)
                     dct[itest_name] = itest_func
         out = super(ExampleMeta, cls).__new__(cls, name, bases, dct)
         if out.example_name is not None:
@@ -263,7 +268,6 @@ def run_example(self):
                 raise unittest.SkipTest("%s not installed." % x)
             check_enabled_languages(x)
         # Copy platform specific makefile
-        print(self.language, platform._is_win)
         if self.language == 'make':
             makefile = os.path.join(self.yamldir, 'src', 'Makefile')
             if platform._is_win:  # pragma: windows
diff --git a/yggdrasil/examples/tests/test_osr.py b/yggdrasil/examples/tests/test_osr.py
index 14d5ff6a5..6d6ddc5ea 100644
--- a/yggdrasil/examples/tests/test_osr.py
+++ b/yggdrasil/examples/tests/test_osr.py
@@ -5,3 +5,4 @@ class TestExampleOSR(ExampleTstBase):
     r"""Test the OSR example."""
 
     example_name = 'osr'
+    timeout = None
diff --git a/yggdrasil/examples/tests/test_rpc_lesson3b.py b/yggdrasil/examples/tests/test_rpc_lesson3b.py
new file mode 100644
index 000000000..d1b969c98
--- /dev/null
+++ b/yggdrasil/examples/tests/test_rpc_lesson3b.py
@@ -0,0 +1,18 @@
+import os
+from yggdrasil.examples.tests import ExampleTstBase
+
+
+class TestExampleRPC3b(ExampleTstBase):
+    r"""Test the rpc_lesson3 example."""
+    
+    example_name = 'rpc_lesson3b'
+
+    @property
+    def input_files(self):
+        r"""Input file."""
+        return [os.path.join(self.yamldir, 'Input', 'input.txt')]
+    
+    @property
+    def output_files(self):
+        r"""Output file."""
+        return [os.path.join(self.tempdir, 'client_output.txt')]
diff --git a/yggdrasil/examples/tests/test_transforms.py b/yggdrasil/examples/tests/test_transforms.py
index 74cf61a6f..8d652fe4c 100644
--- a/yggdrasil/examples/tests/test_transforms.py
+++ b/yggdrasil/examples/tests/test_transforms.py
@@ -1,6 +1,5 @@
 import os
 import copy
-import pprint
 import numpy as np
 from yggdrasil import tools, units
 from yggdrasil.tests import assert_equal
@@ -79,9 +78,9 @@ def t(x):
                 return x
         x_sent = t(cls.get_test_data())
         print('RECEIVED:')
-        pprint.pprint(x_recv)
+        tools.pprint_encoded(x_recv)
         print('EXPECTED:')
-        pprint.pprint(x_sent)
+        tools.pprint_encoded(x_sent)
         assert_equal(x_recv, x_sent)
 
     @classmethod
diff --git a/yggdrasil/languages/Python/test_YggInterface.py b/yggdrasil/languages/Python/test_YggInterface.py
index d73447e77..605181c6d 100644
--- a/yggdrasil/languages/Python/test_YggInterface.py
+++ b/yggdrasil/languages/Python/test_YggInterface.py
@@ -1,5 +1,6 @@
 import os
 import numpy as np
+import flaky
 from yggdrasil.communication import get_comm
 from yggdrasil.interface import YggInterface
 from yggdrasil.tools import (
@@ -384,6 +385,7 @@ def __init__(self, *args, **kwargs):
         self._inst_kwargs = {'format_str': self.fmt_str_matlab}
 
 
+@flaky.flaky
 class TestYggRpcClient(TestYggOutput):
     r"""Test client-side RPC communication with Python."""
 
@@ -416,7 +418,8 @@ def test_msg(self):
             assert(msg_flag)
             self.assert_equal(msg_recv, msg)
         
-        
+
+@flaky.flaky
 class TestYggRpcClientMatlab(TestYggRpcClient):
     r"""Test client-side RPC communication with Python as passed through Matlab."""
     def __init__(self, *args, **kwargs):
@@ -425,6 +428,7 @@ def __init__(self, *args, **kwargs):
         self._inst_args = [self.name, self.fmt_str_matlab, self.fmt_str_matlab]
 
 
+@flaky.flaky
 class TestYggRpcServer(TestYggInput):
     r"""Test server-side RPC communication with Python."""
 
@@ -458,6 +462,7 @@ def test_msg(self):
             self.assert_equal(msg_recv, msg)
         
         
+@flaky.flaky
 class TestYggRpcServerMatlab(TestYggRpcServer):
     r"""Test server-side RPC communication with Python as passed through Matlab."""
     def __init__(self, *args, **kwargs):
diff --git a/yggdrasil/tests/__init__.py b/yggdrasil/tests/__init__.py
index 85ad13b5d..33cb3341d 100644
--- a/yggdrasil/tests/__init__.py
+++ b/yggdrasil/tests/__init__.py
@@ -52,8 +52,9 @@
     else:
         scripts[k] = os.path.join(script_dir, v)
 # scripts = {k: os.path.join(script_dir, v) for k, v in script_list}
-if platform._is_win:  # pragma: windows
-    scripts['executable'] = ['timeout', '0']
+if platform._is_win and (not tools.in_powershell()):  # pragma: windows
+    # scripts['executable'] = ['timeout', '0']
+    scripts['executable'] = ['ping', '-n', '1', '127.0.0.1']
 else:
     scripts['executable'] = ['sleep', 0.1]
     
@@ -86,33 +87,45 @@
 skip_extra_examples = tools.check_environ_bool("YGG_SKIP_EXTRA_EXAMPLES")
 
 
-def check_enabled_languages(language):
+def check_enabled_languages(language, return_decorators=False):
     r"""Determine if the specified language is enabled by the value
     or values specified in YGG_TEST_LANGUAGE.
 
     Args:
         language (str): Language to check.
+        return_decorators (bool, optional): If True, a skip error is
+            not raised and a list of decorators is returned. Defaults
+            to False.
 
     Raises:
         unittest.SkipTest: If the specified language is not enabled.
 
     """
     enabled = os.environ.get('YGG_TEST_LANGUAGE', None)
+    decorators = []
     if enabled is not None:
         enabled = [x.lower() for x in json.loads(enabled)]
-        if ('c++' in enabled) or ('cpp' in enabled):
-            enabled += ['c++', 'cpp']
-        if language.lower() not in enabled:
-            raise unittest.SkipTest("Tests for language %s not enabled.",
-                                    language)
+        if ('c++' in enabled) or ('cpp' in enabled) or ('cxx' in enabled):
+            enabled += ['c++', 'cpp', 'cxx']
+        msg = "Tests for language %s not enabled." % language
+        cond = (language.lower() not in enabled)
+        if return_decorators:
+            decorators.append(unittest.skipIf(cond, msg))
+        elif cond:
+            raise unittest.SkipTest(msg)
     disabled = os.environ.get('YGG_TEST_SKIP_LANGUAGE', None)
     if disabled is not None:
         disabled = [x.lower() for x in json.loads(disabled)]
         if ('c++' in disabled) or ('cpp' in disabled):
             disabled += ['c++', 'cpp']
-        if language.lower() in disabled:
-            raise unittest.SkipTest("Tests for language %s disabled.",
-                                    language)
+        msg = "Tests for language %s disabled." % language
+        cond = (language.lower() in disabled)
+        if return_decorators:
+            decorators.append(unittest.skipIf(cond, msg))
+        elif cond:  # pragma: debug
+            raise unittest.SkipTest(msg)
+    if return_decorators:
+        return decorators
 
 
 def requires_language(language, installed=True):
@@ -147,10 +160,7 @@ def wrapper(function):
         elif installed is False:
             skips.append(unittest.skipIf(drv.is_installed(),
                                          "%s installed"))
-        skips.append(unittest.skipIf(
-            (test_language is not None)
-            and (test_language != drv.language.lower()),
-            "Test for language %s not enabled." % drv.language))
+        skips += check_enabled_languages(language, return_decorators=True)
         for s in skips:
             function = s(function)
         return function
@@ -160,7 +170,7 @@ def wrapper(function):
 
 def pprint_diff(x, y):  # pragma: no cover
     r"""Get the diff between the pprint.pformat string for two objects."""
-    print('\n'.join(difflib.ndiff(
+    tools.print_encoded('\n'.join(difflib.ndiff(
         pprint.pformat(x).splitlines(),
         pprint.pformat(y).splitlines())))
 
diff --git a/yggdrasil/tests/test_config.py b/yggdrasil/tests/test_config.py
index 2990c5dda..1f5e6a5c6 100644
--- a/yggdrasil/tests/test_config.py
+++ b/yggdrasil/tests/test_config.py
@@ -1,5 +1,6 @@
 import os
 import json
+import shutil
 from yggdrasil.tests import assert_equal, assert_raises
 from yggdrasil import config, tools
 
@@ -21,16 +22,33 @@ def test_YggConfigParser():
 def test_update_language_config():
     r"""Test updating configuration for installed languages."""
     languages = tools.get_supported_lang()
-    config.update_language_config(overwrite=True, verbose=True)
+    cfg_orig = config.ygg_cfg_usr.file_to_update
+    cfg_copy = '_copy'.join(os.path.splitext(cfg_orig))
+    shutil.copy2(cfg_orig, cfg_copy)
     try:
-        config.update_language_config(
-            languages[0], disable_languages=[languages[0]])
-        config.update_language_config(
-            languages[0], disable_languages=[languages[0]],
-            enable_languages=[languages[0]])
+        if config.ygg_cfg_usr.has_option('c', 'vcpkg_dir'):
+            config.update_language_config(
+                'c', lang_kwargs={'c': {
+                    'vcpkg_dir': config.ygg_cfg_usr.get(
+                        'c', 'vcpkg_dir')}})
+        config.update_language_config(overwrite=True, verbose=True)
+        if len(languages) > 0:
+            config.ygg_cfg_usr.remove_section(languages[0])
+            config.update_language_config(
+                languages[0], disable_languages=[languages[0]])
+            config.update_language_config(
+                languages[0], disable_languages=[languages[0]],
+                enable_languages=[languages[0]])
+            config.update_language_config(
+                languages[0], enable_languages=[languages[0]])
+        if len(languages) > 1:
+            config.ygg_cfg_usr.remove_section(languages[1])
+            config.update_language_config(
+                languages[1], enable_languages=[languages[1]])
     finally:
-        config.update_language_config(
-            languages[0], enable_languages=[languages[0]])
+        shutil.move(cfg_copy, cfg_orig)
+        config.ygg_cfg_usr.reload()
+        config.ygg_cfg.reload()
 
 
 def test_cfg_logging():
diff --git a/yggdrasil/tests/test_multitasking.py b/yggdrasil/tests/test_multitasking.py
index 024eea860..a7cc5f7b1 100644
--- a/yggdrasil/tests/test_multitasking.py
+++ b/yggdrasil/tests/test_multitasking.py
@@ -154,7 +154,7 @@ def __init__(self, *args, **kwargs):
         self.debug_flag = False
 
     @staticmethod
-    def target(cls):  # pragma: no cover
+    def target():  # pragma: no cover
         tools.sleep(10.0)
 
     def test_id(self):
@@ -179,6 +179,7 @@ def test_kill(self):
         if self._inst_kwargs.get('task_method', None) == 'process':
             self.instance.start()
         self.instance.kill()
+        self.instance.exitcode
 
     def test_flag_manipulation(self):
         r"""Test flag manipulation."""
diff --git a/yggdrasil/tests/test_timing.py b/yggdrasil/tests/test_timing.py
index 15a1d7738..128655394 100644
--- a/yggdrasil/tests/test_timing.py
+++ b/yggdrasil/tests/test_timing.py
@@ -2,6 +2,7 @@
 import sys
 import copy
 import unittest
+import flaky
 from yggdrasil import tools, timing, platform
 from yggdrasil.tests import YggTestClass, assert_raises, long_running
 
@@ -307,10 +308,12 @@ def time_run_kwargs(self):
         out['overwrite'] = True
         return out
 
+    @flaky.flaky
     def test_pyperf_func(self):
         r"""Test pyperf_func."""
         timing.pyperf_func(1, self.instance, self.count, self.size, 0)
 
+    @flaky.flaky
     def test_run_overwrite(self):
         r"""Test performing a run twice, the second time with ovewrite."""
         self.time_run()
diff --git a/yggdrasil/tests/test_tools.py b/yggdrasil/tests/test_tools.py
index 78f4c6202..d71cd374e 100644
--- a/yggdrasil/tests/test_tools.py
+++ b/yggdrasil/tests/test_tools.py
@@ -78,6 +78,16 @@ def test_display_source_diff():
         os.remove(fname2)
 
 
+def test_get_shell():
+    r"""Test get_shell."""
+    tools.get_shell()
+
+
+def test_in_powershell():
+    r"""Test in_powershell."""
+    tools.in_powershell()
+
+
 def test_get_conda_prefix():
     r"""Test get_conda_prefix."""
     tools.get_conda_prefix()
diff --git a/yggdrasil/tests/test_yamlfile.py b/yggdrasil/tests/test_yamlfile.py
index 85b18b0fd..5c42b9077 100644
--- a/yggdrasil/tests/test_yamlfile.py
+++ b/yggdrasil/tests/test_yamlfile.py
@@ -187,7 +187,9 @@ class TestYamlModelOnly(YamlTestBase):
                  ['models:',
                   '  name: modelD',
                   '  driver: GCCModelDriver',
-                  '  args: ./src/modelD.c'], )
+                  '  args: ./src/modelD.c',
+                  '  env:',
+                  '    TEST_VAR: 1'], )
 
 
 class TestYamlServerClient(YamlTestBase):