Skip to content

Commit e77f59f

Browse files
committed
Update to CMake build
The CABLE build system was transitioned from Makefile to CMake: see CABLE-LSM/CABLE#216, CABLE-LSM/CABLE#200. This change adds support for building CABLE via CMake in benchcab so that CABLE versions which branch from the HEAD of main can be tested. Closes #258
1 parent ef0dc85 commit e77f59f

File tree

9 files changed

+80
-205
lines changed

9 files changed

+80
-205
lines changed

Diff for: src/benchcab/benchcab.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,7 @@ def build(self, config_path: str, mpi=False):
259259
self.logger.info(
260260
f"Compiling CABLE {build_mode} for realisation {repo.name}..."
261261
)
262-
repo.pre_build(mpi=mpi)
263-
repo.run_build(modules=config["modules"], mpi=mpi)
264-
repo.post_build(mpi=mpi)
262+
repo.build(modules=config["modules"], mpi=mpi)
265263
self.logger.info(f"Successfully compiled CABLE for realisation {repo.name}")
266264

267265
def fluxsite_setup_work_directory(self, config_path: str):
@@ -366,7 +364,6 @@ def spatial(self, config_path: str, skip: list):
366364
def run(self, config_path: str, skip: list[str]):
367365
"""Endpoint for `benchcab run`."""
368366
self.checkout(config_path)
369-
self.build(config_path)
370367
self.build(config_path, mpi=True)
371368
self.fluxsite_setup_work_directory(config_path)
372369
self.spatial_setup_work_directory(config_path)

Diff for: src/benchcab/data/test/integration.sh

+1-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ mkdir -p $TEST_DIR
1818
# Clone local checkout for CABLE
1919
git clone $CABLE_REPO $CABLE_DIR
2020
cd $CABLE_DIR
21-
# Note: This is temporary, to be removed once #258 is fixed
22-
git reset --hard 67a52dc5721f0da78ee7d61798c0e8a804dcaaeb
2321

2422
# Clone the example repo
2523
git clone $EXAMPLE_REPO $TEST_DIR
@@ -36,7 +34,6 @@ realisations:
3634
- repo:
3735
git:
3836
branch: main
39-
commit: 67a52dc5721f0da78ee7d61798c0e8a804dcaaeb # Note: This is temporary, to be removed once #258 is fixed
4037
modules: [
4138
intel-compiler/2021.1.1,
4239
netcdf/4.7.4,
@@ -50,4 +47,4 @@ fluxsite:
5047
- scratch/$PROJECT
5148
EOL
5249

53-
benchcab run -v
50+
benchcab run -v

Diff for: src/benchcab/internal.py

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212

1313
CONFIG_REQUIRED_KEYS = ["realisations", "modules"]
1414

15+
# CMake module used for compilation:
16+
CMAKE_MODULE = "cmake/3.24.2"
17+
18+
# Number of parallel jobs used when compiling with CMake:
19+
CMAKE_BUILD_PARALLEL_LEVEL = 4
20+
1521
# Parameters for job script:
1622
QSUB_FNAME = "benchmark_cable_qsub.sh"
1723
FLUXSITE_DEFAULT_PBS: PBSConfig = {

Diff for: src/benchcab/model.py

+28-51
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from benchcab import internal
1414
from benchcab.environment_modules import EnvironmentModules, EnvironmentModulesInterface
1515
from benchcab.utils import get_logger
16-
from benchcab.utils.fs import chdir, copy2, rename
16+
from benchcab.utils.fs import chdir, prepend_path
1717
from benchcab.utils.repo import GitRepo, LocalRepo, Repo
1818
from benchcab.utils.subprocess import SubprocessWrapper, SubprocessWrapperInterface
1919

@@ -87,7 +87,7 @@ def get_exe_path(self, mpi=False) -> Path:
8787
exe = internal.CABLE_MPI_EXE if mpi else internal.CABLE_EXE
8888
if self.install_dir:
8989
return internal.SRC_DIR / self.name / self.install_dir / exe
90-
return internal.SRC_DIR / self.name / self.src_dir / "offline" / exe
90+
return internal.SRC_DIR / self.name / "bin" / exe
9191

9292
def custom_build(self, modules: list[str]):
9393
"""Build CABLE using a custom build script."""
@@ -118,60 +118,37 @@ def custom_build(self, modules: list[str]):
118118
with chdir(build_script_path.parent), self.modules_handler.load(modules):
119119
self.subprocess_handler.run_cmd(f"./{tmp_script_path.name}")
120120

121-
def pre_build(self, mpi=False):
122-
"""Runs CABLE pre-build steps."""
121+
def build(self, modules: list[str], mpi=False):
122+
"""Build CABLE with CMake."""
123123
path_to_repo = internal.SRC_DIR / self.name
124-
tmp_dir = (
125-
path_to_repo
126-
/ self.src_dir
127-
/ (internal.TMP_BUILD_DIR_MPI if mpi else internal.TMP_BUILD_DIR)
128-
)
129-
if not tmp_dir.exists():
130-
self.logger.debug(f"mkdir {tmp_dir}")
131-
tmp_dir.mkdir()
132-
133-
for pattern in internal.OFFLINE_SOURCE_FILES:
134-
for path in (path_to_repo / self.src_dir).glob(pattern):
135-
if not path.is_file():
136-
continue
137-
copy2(path, tmp_dir)
138-
139-
copy2(path_to_repo / self.src_dir / "offline" / "Makefile", tmp_dir)
140-
141-
def run_build(self, modules: list[str], mpi=False):
142-
"""Runs CABLE build scripts."""
143-
path_to_repo = internal.SRC_DIR / self.name
144-
tmp_dir = (
145-
path_to_repo
146-
/ self.src_dir
147-
/ (internal.TMP_BUILD_DIR_MPI if mpi else internal.TMP_BUILD_DIR)
148-
)
149-
150-
with chdir(tmp_dir), self.modules_handler.load(modules):
124+
cmake_args = [
125+
"-DCMAKE_BUILD_TYPE=Release",
126+
"-DCABLE_MPI=" + ("ON" if mpi else "OFF"),
127+
]
128+
with chdir(path_to_repo), self.modules_handler.load(
129+
[internal.CMAKE_MODULE, *modules]
130+
):
151131
env = os.environ.copy()
152-
env["NCDIR"] = f"{env['NETCDF_ROOT']}/lib/Intel"
153-
env["NCMOD"] = f"{env['NETCDF_ROOT']}/include/Intel"
154-
env["CFLAGS"] = "-O2 -fp-model precise"
155-
env["LDFLAGS"] = f"-L{env['NETCDF_ROOT']}/lib/Intel -O0"
156-
env["LD"] = "-lnetcdf -lnetcdff"
157-
env["FC"] = "mpif90" if mpi else "ifort"
132+
# This is required so that the netcdf-fortran library is discoverable by
133+
# pkg-config:
134+
prepend_path(
135+
"PKG_CONFIG_PATH", f"{env['NETCDF_BASE']}/lib/Intel/pkgconfig", env=env
136+
)
158137

159-
self.subprocess_handler.run_cmd("make mpi" if mpi else "make", env=env)
138+
if self.modules_handler.module_is_loaded("openmpi"):
139+
# This is required so that the openmpi MPI libraries are discoverable
140+
# via CMake's `find_package` mechanism:
141+
prepend_path(
142+
"CMAKE_PREFIX_PATH", f"{env['OPENMPI_BASE']}/include/Intel", env=env
143+
)
160144

161-
def post_build(self, mpi=False):
162-
"""Runs CABLE post-build steps."""
163-
path_to_repo = internal.SRC_DIR / self.name
164-
tmp_dir = (
165-
path_to_repo
166-
/ self.src_dir
167-
/ (internal.TMP_BUILD_DIR_MPI if mpi else internal.TMP_BUILD_DIR)
168-
)
169-
exe = internal.CABLE_MPI_EXE if mpi else internal.CABLE_EXE
145+
env["CMAKE_BUILD_PARALLEL_LEVEL"] = str(internal.CMAKE_BUILD_PARALLEL_LEVEL)
170146

171-
rename(
172-
tmp_dir / exe,
173-
path_to_repo / self.src_dir / "offline" / exe,
174-
)
147+
self.subprocess_handler.run_cmd(
148+
"cmake -S . -B build " + " ".join(cmake_args), env=env
149+
)
150+
self.subprocess_handler.run_cmd("cmake --build build ", env=env)
151+
self.subprocess_handler.run_cmd("cmake --install build --prefix .", env=env)
175152

176153

177154
def remove_module_lines(file_path: Path) -> None:

Diff for: src/benchcab/utils/fs.py

+16
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,19 @@ def mkdir(new_path: Path, **kwargs):
7272
"""
7373
get_logger().debug(f"Creating {new_path} directory")
7474
new_path.mkdir(**kwargs)
75+
76+
77+
def prepend_path(var: str, path: str, env=os.environ):
78+
"""Prepend path to environment variable.
79+
80+
Parameters
81+
----------
82+
var : str
83+
Name of environment variable.
84+
path : str
85+
Path to prepend.
86+
env : dict
87+
Environment dictionary.
88+
89+
"""
90+
env[var] = path + (os.pathsep + env[var] if var in env else "")

Diff for: tests/test_fluxsite.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import f90nml
1111
import netCDF4
1212
import pytest
13-
1413
from benchcab import __version__, internal
1514
from benchcab.fluxsite import (
1615
CableError,
@@ -107,7 +106,7 @@ def _setup(self, task):
107106
(internal.FLUXSITE_DIRS["OUTPUT"]).mkdir(parents=True)
108107
(internal.FLUXSITE_DIRS["LOG"]).mkdir(parents=True)
109108

110-
exe_build_dir = internal.SRC_DIR / "test-branch" / "offline"
109+
exe_build_dir = internal.SRC_DIR / "test-branch" / "bin"
111110
exe_build_dir.mkdir(parents=True)
112111
(exe_build_dir / internal.CABLE_EXE).touch()
113112

@@ -170,7 +169,7 @@ def _setup(self, task):
170169
(internal.FLUXSITE_DIRS["OUTPUT"]).mkdir(parents=True)
171170
(internal.FLUXSITE_DIRS["LOG"]).mkdir(parents=True)
172171

173-
exe_build_dir = internal.SRC_DIR / "test-branch" / "offline"
172+
exe_build_dir = internal.SRC_DIR / "test-branch" / "bin"
174173
exe_build_dir.mkdir(parents=True)
175174
(exe_build_dir / internal.CABLE_EXE).touch()
176175

Diff for: tests/test_fs.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"""
77

88
import logging
9+
import os
910
from pathlib import Path
1011

1112
import pytest
12-
13-
from benchcab.utils.fs import chdir, mkdir, next_path
13+
from benchcab.utils.fs import chdir, mkdir, next_path, prepend_path
1414

1515

1616
class TestNextPath:
@@ -68,3 +68,25 @@ def test_mkdir(self, test_path, kwargs):
6868
mkdir(test_path, **kwargs)
6969
assert test_path.exists()
7070
test_path.rmdir()
71+
72+
73+
class TestPrependPath:
74+
"""Tests for `prepend_path()`."""
75+
76+
var = "PATHS"
77+
new_path = "/path/to/bar"
78+
79+
@pytest.mark.parametrize(
80+
("env", "expected"),
81+
[
82+
({}, new_path),
83+
(
84+
{var: "/path/to/foo"},
85+
f"{new_path}{os.pathsep}/path/to/foo",
86+
),
87+
],
88+
)
89+
def test_prepend_path(self, env, expected):
90+
"""Success case: test prepend_path for unset and existing variables."""
91+
prepend_path(self.var, "/path/to/bar", env=env)
92+
assert env[self.var] == expected

0 commit comments

Comments
 (0)