From 1599243b3633e4cb45e4396750f1d19f2b971c7d Mon Sep 17 00:00:00 2001
From: Mark Piper <mark.piper@colorado.edu>
Date: Fri, 15 Mar 2024 11:58:19 -0600
Subject: [PATCH] Build babelized projects with meson (#90)

* Move zest.releaser config to pyproject.toml

* Set up Meson build for a single Fortran component

This is WIP, but the project files are in the right place.

* Remove setuptools config from pyproject.toml

* Set version, but not readme, in dynamic metadata

It looks like meson doesn't allow readme in dynamic metadata.

Be sure to install _version.py.

* Read package dependencies from babel.toml file

I got help on this from https://stackoverflow.com/a/30517735.

* Filter dependencies on default empty string

* Update bmi-example-fortran

* Get the name of the first component to babelize

This is a temporary fix until I can handle multiple components
in the `meson.build` file.

* Set --no-build-isolation for editable installs

This is a *meson-python* behavior. See https://meson-python.readthedocs.io/en/stable/how-to-guides/editable-installs.html

* Include package requirements for example

* use conda; install bmiheatf in _inst; create bmiheatf.pc

* add bmiheatf as a dependency

* remove requirements from babel.toml

* install build tools on a per language basis

* add bmi-tester as a testing dependency

* install language-specific build tools

* create a pkg-config file for heatf; back to using conda

* add c and cpp to builds

* update setup-miniconda

* install pkg-config in test environment

* fix heatf pkg-config file and bmic, bmicxx pkg-config names

* update c, cxx submodules

* back to lowercase for pkg-config names

* use setuptools for pure-python projects

* build multiple extension; remove bmiheat dependency

* remove setup.py, setup_utils.py

* bump versions of setup-miniconda, checkout

* remove meson.build for python projects

* remove setup.py setup_utils.py from prettification

* Update bmi-example-fortran to tip of mdpiper/use-pkgconfig branch

* Remove hardcoded pkg-config files

* Clean lint

* Add a news fragment

* Update bmi-example-fortran to v2.1.3

* Remove Makefile; trim .gitignore

* Include brief instructions for installing from source

---------

Co-authored-by: mcflugen <mcflugen@gmail.com>
---
 .github/workflows/test-langs.yml              |   6 +-
 babelizer/data/hooks/post_gen_project.py      |   4 +
 .../.github/workflows/test.yml                |  11 +-
 .../{{cookiecutter.package_name}}/.gitignore  | 105 ++-------------
 .../{{cookiecutter.package_name}}/Makefile    |  94 -------------
 .../{{cookiecutter.package_name}}/README.rst  |  44 +++++--
 .../{{cookiecutter.package_name}}/meson.build |  99 ++++++++++++++
 .../pyproject.toml                            |  36 +++--
 .../{{cookiecutter.package_name}}/setup.cfg   |   3 -
 .../{{cookiecutter.package_name}}/setup.py    |  14 --
 .../setup_utils.py                            | 123 ------------------
 babelizer/render.py                           |   2 -
 external/bmi-example-c                        |   2 +-
 external/bmi-example-cxx                      |   2 +-
 external/bmi-example-fortran                  |   2 +-
 external/tests/test_fortran/babel.toml        |   2 +-
 news/90.feature                               |   3 +
 noxfile.py                                    |  22 ++--
 pyproject.toml                                |   1 +
 19 files changed, 193 insertions(+), 382 deletions(-)
 delete mode 100644 babelizer/data/{{cookiecutter.package_name}}/Makefile
 create mode 100644 babelizer/data/{{cookiecutter.package_name}}/meson.build
 delete mode 100644 babelizer/data/{{cookiecutter.package_name}}/setup.py
 delete mode 100644 babelizer/data/{{cookiecutter.package_name}}/setup_utils.py
 create mode 100644 news/90.feature

diff --git a/.github/workflows/test-langs.yml b/.github/workflows/test-langs.yml
index 2b32dd0e..2acae11e 100644
--- a/.github/workflows/test-langs.yml
+++ b/.github/workflows/test-langs.yml
@@ -29,11 +29,10 @@ jobs:
         with:
           submodules: true
 
-      - uses: conda-incubator/setup-miniconda@v2
+      - uses: conda-incubator/setup-miniconda@v3
         with:
           auto-update-conda: true
           python-version: ${{ matrix.python-version }}
-          mamba-version: "*"
           channels: conda-forge
           channel-priority: true
 
@@ -41,7 +40,8 @@ jobs:
         run: python -m pip install nox tomli
 
       - name: Install compilers
-        run: mamba install c-compiler cxx-compiler fortran-compiler
+        if: matrix.language != 'python'
+        run: conda install ${{ matrix.language }}-compiler cmake make
 
       - name: Run the language tests
         run: nox -s "test-langs-${{ matrix.python-version }}(lang='${{ matrix.language }}')" --python ${{ matrix.python-version }} --verbose
diff --git a/babelizer/data/hooks/post_gen_project.py b/babelizer/data/hooks/post_gen_project.py
index e6218f5a..27f2fbe6 100644
--- a/babelizer/data/hooks/post_gen_project.py
+++ b/babelizer/data/hooks/post_gen_project.py
@@ -114,6 +114,10 @@ def write_api_yaml(folderpath, **kwds):
     if "Not open source" == "{{ cookiecutter.open_source_license }}":
         remove_file("LICENSE")
 
+    {%- if cookiecutter.language == 'python' %}
+    remove_file("meson.build")
+    {%- endif %}
+
     datadir = Path("meta")
     package_datadir = Path("{{ cookiecutter.package_name }}") / "data"
     if not package_datadir.exists():
diff --git a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml b/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml
index f69190dd..35416232 100644
--- a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml
+++ b/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml
@@ -23,9 +23,9 @@ jobs:
         python-version: [{{ cookiecutter.ci.python_version | join(", ") }}]
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
-      - uses: conda-incubator/setup-miniconda@v2
+      - uses: conda-incubator/setup-miniconda@v3
         with:
           auto-update-conda: true
           python-version: ${{ '{{' }} matrix.python-version {{ '}}' }}
@@ -39,16 +39,15 @@ jobs:
 
       - name: Install requirements
         run: |
-          conda install mamba
-          mamba install --file=requirements-build.txt --file=requirements-library.txt
-          mamba list
+          conda install --file=requirements-build.txt --file=requirements-library.txt
+          conda list
 
       - name: Build and install package
         run: |
           pip install -e .
 
       - name: Install testing dependencies
-        run: mamba install --file=requirements-testing.txt
+        run: conda install --file=requirements-testing.txt
 
       - name: Test
         run: |
diff --git a/babelizer/data/{{cookiecutter.package_name}}/.gitignore b/babelizer/data/{{cookiecutter.package_name}}/.gitignore
index 3b80cb7c..35c0374f 100644
--- a/babelizer/data/{{cookiecutter.package_name}}/.gitignore
+++ b/babelizer/data/{{cookiecutter.package_name}}/.gitignore
@@ -1,114 +1,25 @@
-# Byte-compiled / optimized / DLL files
+.DS_Store
 __pycache__/
 *.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
+.ipynb_checkpoints
 build/
-develop-eggs/
 dist/
-downloads/
-eggs/
-.eggs/
-# lib/
-lib64/
-parts/
 sdist/
-var/
-wheels/
 *.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
 htmlcov/
-.tox/
 .coverage
-.coverage.*
-.cache
-nosetests.xml
 coverage.xml
-*.cover
-.hypothesis/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
 docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# pyenv
-.python-version
-
-# celery beat schedule file
-celerybeat-schedule
-
-# SageMath parsed files
-*.sage.py
-
-# dotenv
-.env
-
-# virtualenv
+docs/_generated/
 .venv
-venv/
-ENV/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-
-# nox virtual envs
 .nox/
+*.so
+*.o
+*.mod
+*.smod
 
-{% if cookiecutter.language == 'fortran' -%}
+{%- if cookiecutter.language != 'python' -%}
 {%- for babelized_class in cookiecutter.components %}
-# Fortran files generated by the babelizer
-{{cookiecutter.package_name}}/lib/bmi_interoperability.mod
-{{cookiecutter.package_name}}/lib/bmi_interoperability.smod
-{{cookiecutter.package_name}}/lib/bmi_interoperability.o
 {{cookiecutter.package_name}}/lib/{{ babelized_class|lower }}.c
 {%- endfor %}
 {%- endif %}
diff --git a/babelizer/data/{{cookiecutter.package_name}}/Makefile b/babelizer/data/{{cookiecutter.package_name}}/Makefile
deleted file mode 100644
index 03d43225..00000000
--- a/babelizer/data/{{cookiecutter.package_name}}/Makefile
+++ /dev/null
@@ -1,94 +0,0 @@
-.PHONY: clean clean-test clean-pyc clean-build docs help
-.DEFAULT_GOAL := help
-
-define BROWSER_PYSCRIPT
-import os, webbrowser, sys
-
-try:
-	from urllib import pathname2url
-except:
-	from urllib.request import pathname2url
-
-webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
-endef
-export BROWSER_PYSCRIPT
-
-define PRINT_HELP_PYSCRIPT
-import re, sys
-
-for line in sys.stdin:
-	match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
-	if match:
-		target, help = match.groups()
-		print("%-20s %s" % (target, help))
-endef
-export PRINT_HELP_PYSCRIPT
-
-BROWSER := python -c "$$BROWSER_PYSCRIPT"
-
-help:
-	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
-
-clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
-
-clean-build: ## remove build artifacts
-	rm -fr build/
-	rm -fr dist/
-	rm -fr .eggs/
-	find . -name '*.egg-info' -exec rm -fr {} +
-	find . -name '*.egg' -exec rm -f {} +
-
-clean-pyc: ## remove Python file artifacts
-	find . -name '*.pyc' -exec rm -f {} +
-	find . -name '*.pyo' -exec rm -f {} +
-	find . -name '*~' -exec rm -f {} +
-	find . -name '__pycache__' -exec rm -fr {} +
-
-clean-test: ## remove test and coverage artifacts
-	rm -fr .tox/
-	rm -f .coverage
-	rm -fr htmlcov/
-	rm -fr .pytest_cache
-
-lint: ## check style with flake8
-	flake8 {{ cookiecutter.package_name }}
-
-pretty:
-	find {{ cookiecutter.package_name }} -name '*.py' | xargs isort
-	black setup.py {{ cookiecutter.package_name }}
-
-test: ## run tests quickly with the default Python
-{%- for babelized_class, component in cookiecutter.components|dictsort %}
-	bmi-test {{ cookiecutter.package_name }}.bmi:{{ babelized_class }} -vvv
-{%- endfor %}
-
-test-all: ## run tests on every Python version with tox
-	tox
-
-coverage: ## check code coverage quickly with the default Python
-	coverage run --source {{ cookiecutter.package_name }} -m pytest
-	coverage report -m
-	coverage html
-	$(BROWSER) htmlcov/index.html
-
-docs: ## generate Sphinx HTML documentation, including API docs
-	rm -f docs/{{ cookiecutter.package_name }}.rst
-	rm -f docs/modules.rst
-	sphinx-apidoc -o docs/ {{ cookiecutter.package_name }}
-	$(MAKE) -C docs clean
-	$(MAKE) -C docs html
-	$(BROWSER) docs/_build/html/index.html
-
-servedocs: docs ## compile the docs watching for changes
-	watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
-
-release: dist ## package and upload a release
-	twine upload dist/*
-
-dist: clean ## builds source and wheel package
-	python setup.py sdist
-	python setup.py bdist_wheel
-	ls -l dist
-
-install: clean ## install the package to the active Python's site-packages
-	pip install -e .
diff --git a/babelizer/data/{{cookiecutter.package_name}}/README.rst b/babelizer/data/{{cookiecutter.package_name}}/README.rst
index 39a573a8..05ccf4f9 100644
--- a/babelizer/data/{{cookiecutter.package_name}}/README.rst
+++ b/babelizer/data/{{cookiecutter.package_name}}/README.rst
@@ -65,20 +65,44 @@ Quickstart
 
 .. start-quickstart
 
-To get started you will need to install the *{{ cookiecutter.package_name }}* package, which is currently distributed
-on *conda-forge*. The easiest way to install *{{ cookiecutter.package_name }}* into your current environment using either *mamba* or *conda*.
+To get started you will need to install the *{{ cookiecutter.package_name }}* package.
+Here are two ways to do so.
 
-.. tab:: mamba
+Install from conda-forge
+------------------------
 
-  .. code:: bash
+If the *{{ cookiecutter.package_name }}* package is distributed on *conda-forge*, install it into your current environment with *conda*.
 
-    mamba install {{ cookiecutter.package_name }}
+.. code:: bash
 
-.. tab:: conda
+  conda install -c conda-forge {{ cookiecutter.package_name }}
 
-  .. code:: bash
+Install from source
+-------------------
+
+You can build and install the *{{ cookiecutter.package_name }}* package from source using *conda* and *pip*.
+
+First, from the source directory, install package dependencies into your current environment with *conda*.
+
+.. code:: bash
+
+  conda install -c conda-forge --file requirements.txt --file requirements-build.txt --file requirements-library.txt
+
+Then install the package itself with *pip*.
+{%- if cookiecutter.language == 'python' %}
 
-    conda install {{ cookiecutter.package_name }}
+.. code:: bash
+
+  pip install -e .
+
+{%- else %}
+
+.. code:: bash
+
+  pip install --no-build-isolation --editable .
+
+Note that for an editable install, the ``--no-build-isolation`` flag must be set.
+{%- endif %}
 
 .. end-quickstart
 
@@ -87,8 +111,8 @@ Usage
 
 .. start-usage
 
-There are two ways to use the data components provided by this package: directly through it's Basic
-Model Interface, or as a PyMT plugin.
+There are two ways to use the components provided by this package: directly through its Basic
+Model Interface (BMI), or as a PyMT plugin.
 
 A BMI is provided by each component in this package:
 {%- for babelized_class, component in cookiecutter.components|dictsort -%}
diff --git a/babelizer/data/{{cookiecutter.package_name}}/meson.build b/babelizer/data/{{cookiecutter.package_name}}/meson.build
new file mode 100644
index 00000000..6edc29d5
--- /dev/null
+++ b/babelizer/data/{{cookiecutter.package_name}}/meson.build
@@ -0,0 +1,99 @@
+project(
+    '{{ cookiecutter.package_name }}',
+{%- if cookiecutter.language == 'c' %}
+    'c',
+{%- elif cookiecutter.language == 'c++' %}
+    'cpp',
+{%- elif cookiecutter.language == 'fortran' %}
+    'fortran',
+{%- endif %}
+    'cython',
+    version: '{{ cookiecutter.package_version }}',
+)
+
+py = import('python').find_installation(pure: false)
+
+{%- if cookiecutter.language == 'c' %}
+compiler = meson.get_compiler('c')
+{%- elif cookiecutter.language == 'c++' %}
+compiler = meson.get_compiler('cpp')
+{%- elif cookiecutter.language == 'fortran' %}
+compiler = meson.get_compiler('fortran')
+{%- endif %}
+
+# python_inc = py.get_path('data') / 'include'
+numpy_inc = run_command(
+    py,
+    [
+        '-c',
+        'import numpy; print(numpy.get_include())'
+    ],
+    check: true
+).stdout().strip()
+incs = include_directories(
+    [
+        '{{ cookiecutter.package_name }}/lib',
+        # python_inc,
+        numpy_inc,
+    ]
+)
+
+{% set dependency_list = cookiecutter.package_requirements.split(',') -%}
+deps = [
+{%- for dependency in dependency_list if dependency != '' %}
+    compiler.find_library('{{ dependency }}'),
+{%- endfor %}
+]
+
+# Files get copied to <python directory>/site-packages/<subdir>
+install_pkg_srcs = [
+    '{{ cookiecutter.package_name }}/__init__.py',
+    '{{ cookiecutter.package_name }}/_bmi.py',
+    '{{ cookiecutter.package_name }}/_version.py',
+]
+py.install_sources(
+    install_pkg_srcs,
+    subdir: '{{ cookiecutter.package_name }}',
+)
+
+install_lib_srcs = [
+    '{{ cookiecutter.package_name }}/lib/__init__.py',
+{%- for babelized_class in cookiecutter.components|list|sort %}
+    '{{ cookiecutter.package_name }}/lib/{{ babelized_class|lower }}.pyx',
+{%- endfor %}
+]
+py.install_sources(
+    install_lib_srcs,
+    subdir: '{{ cookiecutter.package_name }}/lib',
+)
+
+
+{%- for babelized_class, component in cookiecutter.components|dictsort %}
+py.extension_module(
+    '{{ babelized_class|lower }}',
+    [
+{%- if cookiecutter.language == 'fortran' %}
+        '{{ cookiecutter.package_name }}/lib/bmi_interoperability.f90',
+{%- endif %}
+        '{{ cookiecutter.package_name }}/lib/{{ babelized_class|lower }}.pyx',
+    ],
+    dependencies: [
+        dependency('{{ component.library }}', method : 'pkg-config'),
+    ],
+    include_directories: incs,
+    install: true,
+    subdir: '{{ cookiecutter.package_name }}/lib',
+{%- if cookiecutter.language == 'c++' %}
+    override_options : ['cython_language=cpp'],
+{%- endif %}
+)
+
+install_subdir(
+    'meta/{{ babelized_class }}',
+    install_dir: py.get_install_dir() / '{{ cookiecutter.package_name }}/data',
+)
+
+{%- endfor %}
+
+# This is a temporary fix for editable installs.
+run_command('cp', '-r', '{{ cookiecutter.package_name }}/data', 'build')
diff --git a/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml b/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml
index b0077dce..7df13701 100644
--- a/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml
+++ b/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml
@@ -1,5 +1,13 @@
 [build-system]
-requires = ["cython", "numpy", "setuptools", "wheel"]
+{%- if cookiecutter.language == 'python' %}
+build-backend = "setuptools.build_meta"
+requires = [
+    "setuptools >=61",
+]
+{% else %}
+build-backend = "mesonpy"
+requires = ["cython", "numpy", "meson-python", "wheel"]
+{% endif %}
 
 [project]
 name = "{{cookiecutter.package_name}}"
@@ -24,7 +32,7 @@ classifiers=[
 ]
 requires-python = ">=3.10"
 keywords=["bmi", "pymt"]
-dynamic = ["readme", "version"]
+dynamic = ["version"]
 dependencies = [
   "numpy",
 ]
@@ -39,6 +47,9 @@ homepage = "https://github.com/{{ cookiecutter.info.github_username }}/{{ cookie
 
 [project.optional-dependencies]
 dev = [
+  "meson",
+  "meson-python",
+  "ninja",
   "nox",
 ]
 docs = [
@@ -54,18 +65,11 @@ testing = [
   "bmi-tester>=0.5.4",
 ]
 
-[tool.setuptools.dynamic]
-readme = {file = ["README.rst", "CREDITS.rst", "CHANGES.rst", "LICENSE.rst"]}
-version = {attr = "{{ cookiecutter.package_name }}._version.__version__"}
-
-[tool.setuptools]
-include-package-data = true
-packages = ["{{ cookiecutter.package_name }}"]
-
-[tool.setuptools.package-data]
-{{ cookiecutter.package_name }} = [
-  "data/*",
-]
+{%- if cookiecutter.language == 'python' %}
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["{{cookiecutter.package_name}}*"]
+{% endif %}
 
 [tool.pytest.ini_options]
 minversion = "5.0"
@@ -107,3 +111,7 @@ underlines = "-^\""
 issue_format = "`#{issue} <https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/issues/{issue}>`_"
 title_format = "{version} ({project_date})"
 wrap = true
+
+[tool.zest-releaser]
+tag-format = "v{version}"
+python-file-with-version = "{{ cookiecutter.package_name }}/_version.py"
diff --git a/babelizer/data/{{cookiecutter.package_name}}/setup.cfg b/babelizer/data/{{cookiecutter.package_name}}/setup.cfg
index 45fca16c..f3d7d5c3 100644
--- a/babelizer/data/{{cookiecutter.package_name}}/setup.cfg
+++ b/babelizer/data/{{cookiecutter.package_name}}/setup.cfg
@@ -5,6 +5,3 @@ ignore =
 	E501
 	W503
 max-line-length = 88
-
-[zest.releaser]
-tag-format = v{version}
diff --git a/babelizer/data/{{cookiecutter.package_name}}/setup.py b/babelizer/data/{{cookiecutter.package_name}}/setup.py
deleted file mode 100644
index 76102434..00000000
--- a/babelizer/data/{{cookiecutter.package_name}}/setup.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from setuptools import setup
-
-{%- if cookiecutter.language in ['c', 'c++', 'fortran'] %}
-from setup_utils import build_ext, get_extension_modules
-{% endif %}
-
-setup(
-{%- if cookiecutter.language in ['c', 'c++', 'fortran'] %}
-    ext_modules=get_extension_modules(),
-{%- endif %}
-{%- if cookiecutter.language == 'fortran' %}
-    cmdclass={"build_ext": build_ext},
-{%- endif %}
-)
diff --git a/babelizer/data/{{cookiecutter.package_name}}/setup_utils.py b/babelizer/data/{{cookiecutter.package_name}}/setup_utils.py
deleted file mode 100644
index b2bbca13..00000000
--- a/babelizer/data/{{cookiecutter.package_name}}/setup_utils.py
+++ /dev/null
@@ -1,123 +0,0 @@
-import contextlib
-import os
-import subprocess
-import sys
-
-import numpy as np
-from numpy.distutils.fcompiler import new_fcompiler
-from setuptools import Extension
-from setuptools.command.build_ext import build_ext as _build_ext
-
-
-def get_compiler_flags():
-    flags = {
-        "include_dirs": [
-            np.get_include(),
-            os.path.join(sys.prefix, "include"),
-            {%- for dir in cookiecutter.build.include_dirs %}
-                "{{ dir|trim }}",
-            {% endfor %}
-        ],
-        "library_dirs": [
-            {%- for libdir in cookiecutter.build.library_dirs %}
-                "{{ libdir|trim }}",
-            {% endfor %}
-        ],
-        "define_macros": [
-            {%- if cookiecutter.build.define_macros -%}
-            {%- for item in cookiecutter.build.define_macros %}
-            {%- set key_value = item.split('=') %}
-                ("{{ key_value[0]|trim }}", "{{ key_value[1]|trim }}"),{% endfor %}
-            {%- endif %}
-        ],
-        "undef_macros": [
-            {%- if cookiecutter.build.undef_macros -%}
-            {%- for macro in cookiecutter.build.undef_macros %}
-                "{{ macro|trim }}",{% endfor %}
-            {%- endif %}
-        ],
-        "extra_compile_args": [
-            {%- if cookiecutter.build.extra_compile_args -%}
-            {%- for arg in cookiecutter.build.extra_compile_args %}
-                "{{ arg|trim }}",{% endfor %}
-            {%- endif %}
-        ],
-    {%- if cookiecutter.language == 'fortran' %}
-        "language": "c",
-    {% else %}
-        "language": "{{ cookiecutter.language }}",
-    {% endif -%}
-    }
-
-    # Locate directories under Windows %LIBRARY_PREFIX%.
-    if sys.platform.startswith("win"):
-        flags["include_dirs"].append(os.path.join(sys.prefix, "Library", "include"))
-        flags["library_dirs"].append(os.path.join(sys.prefix, "Library", "lib"))
-
-    return flags
-
-
-def get_extension_modules():
-    flags = get_compiler_flags()
-
-    libraries = [
-    {%- if cookiecutter.build.libraries -%}
-    {%- for lib in cookiecutter.build.libraries %}
-        "{{ lib|trim }}",{% endfor %}
-    {%- endif %}
-    ]
-
-    ext_modules = [
-    {%- for babelized_class, component in cookiecutter.components|dictsort %}
-        Extension(
-            "{{cookiecutter.package_name}}.lib.{{ babelized_class|lower }}",
-            ["{{cookiecutter.package_name}}/lib/{{ babelized_class|lower }}.pyx"],
-            libraries=libraries + ["{{ component.library }}"],
-            {% if cookiecutter.language == 'fortran' -%}
-            extra_objects=["{{cookiecutter.package_name}}/lib/bmi_interoperability.o"],
-            {% endif -%}
-            **flags,
-        ),
-    {%- endfor %}
-    ]
-
-    return ext_modules
-
-
-@contextlib.contextmanager
-def as_cwd(path):
-    prev_cwd = os.getcwd()
-    os.chdir(path)
-    yield
-    os.chdir(prev_cwd)
-
-
-def build_interoperability():
-    compiler = new_fcompiler()
-    compiler.customize()
-
-    flags = get_compiler_flags()
-
-    cmd = []
-    cmd.append(compiler.compiler_f90[0])
-    cmd.append(compiler.compile_switch)
-    if sys.platform.startswith("win") is False:
-        cmd.append("-fPIC")
-    for include_dir in flags["include_dirs"]:
-        if os.path.isabs(include_dir) is False:
-            include_dir = os.path.join(sys.prefix, "include", include_dir)
-        cmd.append(f"-I{include_dir}")
-    cmd.append("bmi_interoperability.f90")
-
-    try:
-        subprocess.check_call(cmd)
-    except subprocess.CalledProcessError:
-        raise
-
-
-class build_ext(_build_ext):
-
-    def run(self):
-        with as_cwd("{{cookiecutter.package_name}}/lib"):
-            build_interoperability()
-        _build_ext.run(self)
diff --git a/babelizer/render.py b/babelizer/render.py
index e7d4d1df..5edb0d46 100644
--- a/babelizer/render.py
+++ b/babelizer/render.py
@@ -168,8 +168,6 @@ def prettify_python(path_to_repo):
     module_name = meta["package"]["name"]
 
     files_to_fix = [
-        path_to_repo / "setup.py",
-        path_to_repo / "setup_utils.py",
         path_to_repo / module_name / "_bmi.py",
         path_to_repo / module_name / "__init__.py",
         path_to_repo / "docs" / "conf.py",
diff --git a/external/bmi-example-c b/external/bmi-example-c
index 5d39005f..f47d0a65 160000
--- a/external/bmi-example-c
+++ b/external/bmi-example-c
@@ -1 +1 @@
-Subproject commit 5d39005f1aad3c2bb38c2661d68c8e453269cefe
+Subproject commit f47d0a65edf848ccd9d57d375e9fdd90354c1f4f
diff --git a/external/bmi-example-cxx b/external/bmi-example-cxx
index 1c2d0902..07c3dd51 160000
--- a/external/bmi-example-cxx
+++ b/external/bmi-example-cxx
@@ -1 +1 @@
-Subproject commit 1c2d0902963894a96d80d852777769a8d14f0779
+Subproject commit 07c3dd518ddcd7db50e22be0db6ff3fc067053fb
diff --git a/external/bmi-example-fortran b/external/bmi-example-fortran
index 819757c7..98b4e339 160000
--- a/external/bmi-example-fortran
+++ b/external/bmi-example-fortran
@@ -1 +1 @@
-Subproject commit 819757c77a779d13f8c67431cc3d13f6f36b0dcc
+Subproject commit 98b4e3394db6f2b032d8945c125e77435c6d5d03
diff --git a/external/tests/test_fortran/babel.toml b/external/tests/test_fortran/babel.toml
index 60c816b9..0aaf3760 100644
--- a/external/tests/test_fortran/babel.toml
+++ b/external/tests/test_fortran/babel.toml
@@ -15,7 +15,7 @@ extra_compile_args = []
 
 [package]
 name = "pymt_heatf"
-requirements = [""]
+requirements = []
 
 [info]
 github_username = "pymt-lab"
diff --git a/news/90.feature b/news/90.feature
new file mode 100644
index 00000000..3798cf28
--- /dev/null
+++ b/news/90.feature
@@ -0,0 +1,3 @@
+
+Use *meson* to build babelized projects with Fortran, C, and C++ sources.
+Projects with Python sources are still built with *setuptools*.
diff --git a/noxfile.py b/noxfile.py
index bc1c44aa..e5fc20b9 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -23,11 +23,7 @@ def test(session: nox.Session) -> None:
     session.run("pytest", *args)
 
 
-@nox.session(
-    name="test-langs",
-    python=PYTHON_VERSIONS,
-    venv_backend="mamba",
-)
+@nox.session(name="test-langs", python=PYTHON_VERSIONS, venv_backend="conda")
 @nox.parametrize("lang", ["c", "cxx", "fortran", "python"])
 def test_langs(session: nox.session, lang) -> None:
     datadir = ROOT / "external" / "tests" / f"test_{lang}"
@@ -42,7 +38,6 @@ def test_langs(session: nox.session, lang) -> None:
 
     build_examples(session, lang)
 
-    session.conda_install("pip", "bmi-tester>=0.5.4", "cmake")
     session.install(".[testing]")
 
     with session.chdir(tmpdir):
@@ -56,7 +51,7 @@ def test_langs(session: nox.session, lang) -> None:
             session.debug(f"{k}: {v!r}")
 
         with session.chdir(package):
-            session.run("python", "-m", "pip", "install", "-e", ".")
+            session.run("python", "-m", "pip", "install", ".[dev]")
 
     with session.chdir(testdir):
         shutil.copy(datadir / config_file, ".")
@@ -85,17 +80,19 @@ def _get_package_metadata(datadir):
     return package, library, config_files[0]
 
 
-@nox.session(name="build-examples", venv_backend="mamba")
+@nox.session(name="build-examples", python=PYTHON_VERSIONS, venv_backend="conda")
 @nox.parametrize("lang", ["c", "cxx", "fortran", "python"])
 def build_examples(session: nox.Session, lang):
     """Build the language examples."""
     srcdir = ROOT / "external" / f"bmi-example-{lang}"
-    builddir = pathlib.Path(session.create_tmp()) / "_build"
+    tmpdir = ROOT / pathlib.Path(session.create_tmp())
+    builddir = tmpdir / "_build"
+    instdir = pathlib.Path(session.virtualenv.location)
 
     if lang == "python":
-        session.conda_install("bmipy", "make")
+        session.install("bmipy")
     else:
-        session.conda_install(f"bmi-{lang}", "cmake", "make", "pkg-config")
+        session.conda_install(f"bmi-{lang}", "pkg-config")
 
     for k, v in sorted(session.env.items()):
         session.debug(f"{k}: {v!r}")
@@ -111,9 +108,10 @@ def build_examples(session: nox.Session, lang):
                 str(srcdir),
                 "-B",
                 ".",
-                f"-DCMAKE_INSTALL_PREFIX={session.env['CONDA_PREFIX']}",
+                f"-DCMAKE_INSTALL_PREFIX={instdir}",
             )
             session.run("make", "install")
+    return instdir
 
 
 @nox.session(name="test-cli")
diff --git a/pyproject.toml b/pyproject.toml
index f5b36f1b..04357a5a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,6 +78,7 @@ docs = [
     "sphinxcontrib.towncrier",
 ]
 testing = [
+    "bmi-tester>=0.5.9",
     "coverage[toml]",
     "coveralls",
     "pytest",