From 1c35728e1eef796657529e1518bc23b8ee938a5c Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:57:58 +0000 Subject: [PATCH 1/7] incremenental with core working --- build/extract/get_git_sources.py | 186 +++++++++++++++++++++++++++++++ build/local_build.py | 95 ++++++---------- 2 files changed, 223 insertions(+), 58 deletions(-) create mode 100644 build/extract/get_git_sources.py diff --git a/build/extract/get_git_sources.py b/build/extract/get_git_sources.py new file mode 100644 index 000000000..bd790dd18 --- /dev/null +++ b/build/extract/get_git_sources.py @@ -0,0 +1,186 @@ +# *****************************COPYRIGHT******************************* +# (C) Crown copyright Met Office. All rights reserved. +# For further details please refer to the file COPYRIGHT.txt +# which you should have received as part of this distribution. +# *****************************COPYRIGHT******************************* +""" +Clone sources for a rose-stem run for use with git bdiff module in scripts +""" + +import re +import subprocess +from typing import Optional +from pathlib import Path +from shutil import rmtree +import shlex + + +def get_source( + source: str, + ref: str, + dest: Path, + repo: str, + use_mirrors: bool = False, + mirror_loc: Path = "", +) -> None: + + if ".git" in source: + if use_mirrors: + mirror_loc = Path(mirror_loc) / "MetOffice" / repo + print(f"Cloning/Updating {repo} from {mirror_loc} at ref {ref}") + clone_repo_mirror(source, repo, mirror_loc, dest) + else: + print(f"Cloning/Updating {repo} from {source} at ref {ref}") + clone_repo(source, ref, dest) + else: + print(f"Syncing {repo} at ref {ref}") + sync_repo(source, ref, dest) + + +def run_command( + command: str, rval: bool = False, check: bool = True +) -> Optional[subprocess.CompletedProcess]: + """ + Run a subprocess command and return the result object + Inputs: + - command, str with command to run + Outputs: + - result object from subprocess.run + """ + command = shlex.split(command) + result = subprocess.run( + command, + capture_output=True, + text=True, + timeout=300, + shell=False, + check=False, + ) + if check and result.returncode: + print(result.stdout, end="\n\n\n") + raise RuntimeError( + f"[FAIL] Issue found running command {command}\n\n{result.stderr}" + ) + if rval: + return result + + +def clone_repo_mirror( + source: str, repo_ref: str, parent: str, mirror_loc: Path, loc: Path +) -> None: + """ + Clone a repo source using a local git mirror. + Assume the mirror is set up as per the Met Office + """ + + # Remove if this clone already exists + if not loc.exists(): + command = f"git clone {mirror_loc} {loc}" + run_command(command) + + # If not provided a ref, return + if not repo_ref: + run_command(f"git -C {loc} pull") + return + + source = source.removeprefix("git@github.com:") + user = source.split("/")[0] + # Check that the user is different to the Upstream User + if user in parent.split("/")[0]: + user = None + + # If the ref is a hash then we don't need the fork user as part of the fetch. + # Equally, if the user is the Upstream User, it's not needed + if not user or re.match(r"^\s*([0-9a-f]{40})\s*$", repo_ref): + fetch = repo_ref + else: + fetch = f"{user}/{repo_ref}" + commands = ( + f"git -C {loc} fetch origin {fetch}", + f"git -C {loc} checkout FETCH_HEAD", + ) + for command in commands: + run_command(command) + + +def clone_repo(repo_source: str, repo_ref: str, loc: Path) -> None: + """ + Clone the repo and checkout the provided ref + Only if a remote source + """ + + # Remove if this clone already exists + if not loc.exists(): + # Create a clean clone location + loc.mkdir(parents=True) + + commands = ( + f"git -C {loc} init", + f"git -C {loc} remote add origin {repo_source}", + f"git -C {loc} fetch origin {repo_ref}", + f"git -C {loc} checkout FETCH_HEAD", + f"git -C {loc} fetch origin main:main", + ) + for command in commands: + run_command(command) + else: + commands = ( + f"git -C {loc} fetch origin {repo_ref}", + f"git -C {loc} checkout FETCH_HEAD", + ) + for command in commands: + run_command(command) + + +def sync_repo(repo_source: str, repo_ref: str, loc: Path) -> None: + """ + Rsync a local git clone and checkout the provided ref + """ + + # Remove if this clone already exists + if loc.exists(): + rmtree(loc) + + # Create a clean clone location + loc.mkdir(parents=True) + + exclude_dirs = ( + "applications/*/working", + "applications/*/test", + "applications/*/bin", + "science/*/working", + "science/*/test", + "science/*/bin", + "interfaces/*/working", + "interfaces/*/test", + "interfaces/*/bin", + "components/*/working", + "components/*/test", + "components/*/bin", + "infrastructure/*/working", + "infrastructure/*/test", + "infrastructure/*/bin", + "mesh_tools/*/working", + "mesh_tools/*/test", + "mesh_tools/*/bin", + ) + + # Trailing slash required for rsync + command = f"rsync -av {repo_source}/ {loc}" + for item in exclude_dirs: + command = f"{command} --exclude '{item}'" + run_command(command) + + # Fetch the main branch from origin + # Ignore errors - these are likely because the main branch already exists + # Instead write them as warnings + command = f"git -C {loc} fetch origin main:main" + result = run_command(command, check=False, rval=True) + if result.returncode: + print("Warning - fetching main from origin resulted in an error") + print("This is likely due to the main branch already existing") + print(f"Error message:\n\n{result.stderr}") + + if repo_ref: + command = f"git -C {loc} checkout {repo_ref}" + run_command(command) diff --git a/build/local_build.py b/build/local_build.py index 2c9d445e4..03212b3df 100755 --- a/build/local_build.py +++ b/build/local_build.py @@ -17,6 +17,8 @@ import subprocess import argparse import yaml +from pathlib import Path +from extract.get_git_sources import get_source def subprocess_run(command): @@ -45,7 +47,7 @@ def get_root_path(): Get the root path of the current working copy """ - return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + return Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def determine_core_source(root_dir): @@ -56,7 +58,7 @@ def determine_core_source(root_dir): # Read through the dependencies file and populate revision and source # variables for requested repo - with open(os.path.join(root_dir, "dependencies.yaml"), "r") as stream: + with open(root_dir / "dependencies.yaml", "r") as stream: dependencies = yaml.safe_load(stream) return dependencies["lfric_core"] @@ -70,9 +72,9 @@ def determine_project_path(project, root_dir): # Find the project in either science/ interfaces/ or applications/ for drc in ["science/", "interfaces/", "applications/"]: - path = os.path.join(root_dir, drc) + path = root_dir / drc for item in os.listdir(path): - item_path = os.path.join(path, item) + item_path = path / item if item_path and item == project: return item_path @@ -82,45 +84,6 @@ def determine_project_path(project, root_dir): ) -def clone_dependency(values, temp_dep): - """ - Clone the physics dependencies into a temporary directory - """ - - source = values["source"] - ref = values["ref"] - - commands = ( - f"git -C {temp_dep} init", - f"git -C {temp_dep} remote add origin {source}", - f"git -C {temp_dep} fetch origin {ref}", - f"git -C {temp_dep} checkout FETCH_HEAD" - ) - for command in commands: - subprocess_run(command) - - -def get_lfric_core(core_source, working_dir): - """ - Clone the lfric_core source if the source is a git url - rsync this export into the working dir as the lfric_core source - done so - incremental builds can still be used. - If core_source is a local working copy just rsync from there. - """ - - if core_source["source"].endswith(".git"): - print("Cloning LFRic Core from Github") - lfric_core_loc = f"{working_dir}/scratch/core" - clone_dependency(core_source["source"], core_source["ref"], lfric_core_loc) - print("rsyncing the exported lfric_core source") - else: - lfric_core_loc = core_source["source"] - print("rsyncing the local lfric_core source") - - rsync_command = f"rsync -acvzq {lfric_core_loc}/ {working_dir}/lfric_core" - subprocess_run(rsync_command) - - def build_makefile( root_dir, project_path, @@ -140,13 +103,13 @@ def build_makefile( if target == "clean": working_path = working_dir else: - working_path = os.path.join(working_dir, f"{target}_{project}") + working_path = working_dir / f"{target}_{project}" print(f"Calling make command for makefile at {project_path}") make_command = ( f"make {target} -C {project_path} -j {ncores} " f"WORKING_DIR={working_path} " - f"CORE_ROOT_DIR={working_dir}/lfric_core " + f"CORE_ROOT_DIR={working_dir / 'scratch' / 'lfric_core'} " f"APPS_ROOT_DIR={root_dir} " ) if optlevel: @@ -171,14 +134,24 @@ def main(): ) parser.add_argument( "project", - help="project to build. Will search in both " - "science and projects dirs.", + help="project to build. Will search in both " "science and projects dirs.", ) parser.add_argument( "-c", "--core_source", default=None, - help="Source for lfric_core. Defaults to looking in " "dependencies file.", + help="Source for lfric_core. Defaults to looking in dependencies file.", + ) + parser.add_argument( + "-m", + "--mirrors", + action="store_true", + help="If true, attempts to use local git mirrors", + ) + parser.add_argument( + "--mirror_loc", + default="/data/users/gitassist/git_mirrors", + help="Location of github mirrors", ) parser.add_argument( "-w", @@ -237,24 +210,30 @@ def main(): # Set the working dir default of the project directory if not args.working_dir: - args.working_dir = os.path.join(project_path, "working") + args.working_dir = Path(project_path) / "working" else: # If the working dir doesn't end in working, set that here if not args.working_dir.strip("/").endswith("working"): - args.working_dir = os.path.join(args.working_dir, "working") - # Ensure that working_dir is an absolute path - args.working_dir = os.path.abspath(args.working_dir) - # Create the working_dir - subprocess_run(f"mkdir -p {args.working_dir}") + args.working_dir = Path(args.working_dir) / "working" + # Ensure that working_dir is an absolute path and make the directory + args.working_dir = args.working_dir.resolve() + args.working_dir.mkdir(parents=True, exist_ok=True) # Determine the core source if not provided if args.core_source is None: core_source = determine_core_source(root_dir) else: - core_source = {"source": args.core_source} - - # Export and rsync the lfric_core source - get_lfric_core(core_source, args.working_dir) + core_source = {"source": args.core_source, "ref": ""} + + # Clone/Sync the lfric core source + get_source( + core_source["source"], + core_source["ref"], + args.working_dir / "scratch" / "lfric_core", + "lfric_core", + use_mirrors=args.mirrors, + mirror_loc=args.mirror_loc, + ) # Build the makefile build_makefile( From cc1d00d689c31b1e640697db939b830e8b815823 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:17:41 +0000 Subject: [PATCH 2/7] incremental physics builds --- applications/lfric_atm/Makefile | 2 +- applications/lfric_coupled/Makefile | 2 +- applications/ngarch/Makefile | 2 +- build/extract/extract_physics.mk | 4 +- build/extract/extract_science.py | 63 +++++++++---------- build/extract/get_git_sources.py | 2 +- build/local_build.py | 21 +++---- interfaces/jules_interface/build/import.mk | 2 +- interfaces/socrates_interface/build/import.mk | 2 +- 9 files changed, 43 insertions(+), 57 deletions(-) diff --git a/applications/lfric_atm/Makefile b/applications/lfric_atm/Makefile index 98f689dee..b7dd2db48 100644 --- a/applications/lfric_atm/Makefile +++ b/applications/lfric_atm/Makefile @@ -75,7 +75,7 @@ build: export BIN_DIR ?= $(PROJECT_DIR)/bin build: export CXX_LINK = YES build: export PROGRAMS := $(basename $(notdir $(shell find source -maxdepth 1 -name '*.[Ff]90' -exec egrep -l "^\s*program" {} \;))) build: export PROJECT = lfric_atm -build: export SCRATCH_DIR := $(WORKING_DIR)/../physics_scratch +build: export SCRATCH_DIR := $(WORKING_DIR)/.. build: export WORKING_DIR := $(WORKING_DIR) build: export LDFLAGS_GROUPS = OPENMP diff --git a/applications/lfric_coupled/Makefile b/applications/lfric_coupled/Makefile index bf65bb5d7..cdd86b357 100644 --- a/applications/lfric_coupled/Makefile +++ b/applications/lfric_coupled/Makefile @@ -102,7 +102,7 @@ document-api: api-documentation build: export BIN_DIR ?= $(PROJECT_DIR)/bin build: export CXX_LINK = TRUE build: export PROGRAMS := $(basename $(notdir $(shell find source -maxdepth 1 -name '*.[Ff]90' -print))) -build: export SCRATCH_DIR := $(WORKING_DIR)/../physics_scratch +build: export SCRATCH_DIR := $(WORKING_DIR)/.. build: export PROJECT = $(PROJECT_NAME) build: export WORKING_DIR := $(WORKING_DIR)/$(PROJECT_NAME) diff --git a/applications/ngarch/Makefile b/applications/ngarch/Makefile index 9fa685560..8b2a32300 100644 --- a/applications/ngarch/Makefile +++ b/applications/ngarch/Makefile @@ -100,7 +100,7 @@ document-api: api-documentation build: export BIN_DIR ?= $(PROJECT_DIR)/bin build: export CXX_LINK = TRUE build: export PROGRAMS := $(basename $(notdir $(shell find source -maxdepth 1 -name '*.[Ff]90' -print))) -build: export SCRATCH_DIR := $(WORKING_DIR)/../physics_scratch +build: export SCRATCH_DIR := $(WORKING_DIR)/.. build: export PROJECT = $(PROJECT_NAME) build: export WORKING_DIR := $(WORKING_DIR) diff --git a/build/extract/extract_physics.mk b/build/extract/extract_physics.mk index a4df2535b..9f8c60e3f 100644 --- a/build/extract/extract_physics.mk +++ b/build/extract/extract_physics.mk @@ -21,6 +21,6 @@ extract: # Retrieve and preprocess the UKCA and CASIM code python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(SCRATCH_DIR) -e $(APPS_ROOT_DIR)/build/extract/extract.yaml - $Qrsync -acvz $(SCRATCH_DIR)/ukca $(WORKING_DIR)/science/ - $Qrsync -acvz $(SCRATCH_DIR)/casim $(WORKING_DIR)/science/ + $Qrsync -acvz $(SCRATCH_DIR)/scratch/ukca $(WORKING_DIR)/science/ + $Qrsync -acvz $(SCRATCH_DIR)/scratch/casim $(WORKING_DIR)/science/ diff --git a/build/extract/extract_science.py b/build/extract/extract_science.py index 042fe6f32..dea295974 100755 --- a/build/extract/extract_science.py +++ b/build/extract/extract_science.py @@ -1,11 +1,11 @@ import argparse import subprocess import os -import tempfile import yaml -from shutil import rmtree from pathlib import Path from typing import Dict, List +from get_git_sources import get_source +import shlex def run_command(command): @@ -14,7 +14,7 @@ def run_command(command): Inputs: - command, str with command to run """ - command = command.split() + command = shlex.split(command) result = subprocess.run( command, capture_output=True, @@ -40,24 +40,6 @@ def load_yaml(fpath: Path) -> Dict: return sources -def clone_dependency(values: Dict, temp_dep: Path) -> None: - """ - Clone the physics dependencies into a temporary directory - """ - - source = values["source"] - ref = values["ref"] - - commands = ( - f"git -C {temp_dep} init", - f"git -C {temp_dep} remote add origin {source}", - f"git -C {temp_dep} fetch origin {ref}", - f"git -C {temp_dep} checkout FETCH_HEAD" - ) - for command in commands: - run_command(command) - - def extract_files(dependency: str, values: Dict, files: List[str], working: Path): """ Clone the dependency to a temporary location @@ -65,30 +47,41 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path Then delete the temporary directory """ - tempdir = Path(tempfile.mkdtemp()) + use_mirrors: bool = (os.getenv('LOCAL_BUILD_MIRRORS', 'False') == 'True') + mirror_loc: Path = os.getenv("MIRROR_LOC") + if ( "PHYSICS_ROOT" not in os.environ or not Path(os.environ["PHYSICS_ROOT"]).exists() ): - temp_dep = tempdir / dependency - temp_dep.mkdir(parents=True) - clone_dependency(values, temp_dep) + clone_loc = working / "clones" / dependency + get_source( + values["source"], + values["ref"], + clone_loc, + dependency, + use_mirrors, + mirror_loc + ) else: - temp_dep = Path(os.environ["PHYSICS_ROOT"]) / dependency + clone_loc = Path(os.environ["PHYSICS_ROOT"]) / dependency - working_dep = working / dependency + working_dep = working / "scratch" / dependency # make the working directory location - working_dep.mkdir(parents=True) + working_dep.mkdir(parents=True, exist_ok=True) + # rsync extract files from clone loc to the working directory + copy_command = "rsync --include='**/' " for extract_file in files: - source_file = temp_dep / extract_file - dest_file = working_dep / extract_file - run_command(f"mkdir -p {dest_file.parents[0]}") - copy_command = f"cp -r {source_file} {dest_file}" - run_command(copy_command) - - rmtree(tempdir) + if not extract_file: + continue + if Path(clone_loc / extract_file).is_dir(): + extract_file = extract_file.rstrip("/") + extract_file += "/**" + copy_command += f"--include='{extract_file}' " + copy_command += f"--exclude='*' -avmq {clone_loc}/ {working_dep}" + run_command(copy_command) def parse_args() -> argparse.Namespace: diff --git a/build/extract/get_git_sources.py b/build/extract/get_git_sources.py index bd790dd18..2c46108fb 100644 --- a/build/extract/get_git_sources.py +++ b/build/extract/get_git_sources.py @@ -27,7 +27,7 @@ def get_source( if ".git" in source: if use_mirrors: mirror_loc = Path(mirror_loc) / "MetOffice" / repo - print(f"Cloning/Updating {repo} from {mirror_loc} at ref {ref}") + print(f"Cloning/Updating {repo} from mirror {mirror_loc} at ref {ref}") clone_repo_mirror(source, repo, mirror_loc, dest) else: print(f"Cloning/Updating {repo} from {source} at ref {ref}") diff --git a/build/local_build.py b/build/local_build.py index 03212b3df..c4cfde039 100755 --- a/build/local_build.py +++ b/build/local_build.py @@ -93,7 +93,6 @@ def build_makefile( target, optlevel, psyclone, - um_fcm_platform, verbose, ): """ @@ -116,8 +115,6 @@ def build_makefile( make_command += f"PROFILE={optlevel} " if psyclone: make_command += f"PSYCLONE_TRANSFORMATION={psyclone} " - if um_fcm_platform: - make_command += f"UM_FCM_TARGET_PLATFORM={um_fcm_platform} " if verbose: make_command += "VERBOSE=1 " @@ -186,14 +183,6 @@ def main(): help="Value passed to PSYCLONE_TRANSFORMATION variable in makefile. " "Defaults to the makefile default", ) - parser.add_argument( - "-u", - "--um_fcm_platform", - default=None, - help="Value passed to UM_FCM_TARGET_PLATFORM variable in makefile, " - "used for build settings for extracted UM physics. Defaults to the " - "makefile default.", - ) parser.add_argument( "-v", "--verbose", @@ -202,6 +191,11 @@ def main(): ) args = parser.parse_args() + # If using mirrors, set environment variable for science extract step + if args.mirrors: + os.environ["USE_MIRRORS"] = True + os.environ["LOCAL_BUILD_MIRRORS"] = args.mirror_loc + # Find the root directory of the working copy root_dir = get_root_path() @@ -231,8 +225,8 @@ def main(): core_source["ref"], args.working_dir / "scratch" / "lfric_core", "lfric_core", - use_mirrors=args.mirrors, - mirror_loc=args.mirror_loc, + args.mirrors, + args.mirror_loc, ) # Build the makefile @@ -245,7 +239,6 @@ def main(): args.target, args.optlevel, args.psyclone, - args.um_fcm_platform, args.verbose, ) diff --git a/interfaces/jules_interface/build/import.mk b/interfaces/jules_interface/build/import.mk index bb1c8870b..bfc1bff97 100644 --- a/interfaces/jules_interface/build/import.mk +++ b/interfaces/jules_interface/build/import.mk @@ -9,7 +9,7 @@ export PROJECT_SOURCE = $(APPS_ROOT_DIR)/interfaces/jules_interface/source import-jules_interface: # Get a copy of the source code from the JULES repository python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(SCRATCH_DIR) -e $(APPS_ROOT_DIR)/interfaces/jules_interface/build/extract.yaml - $Qrsync -acvz $(SCRATCH_DIR)/jules $(WORKING_DIR)/ + $Qrsync -acvz $(SCRATCH_DIR)/scratch/jules $(WORKING_DIR)/ # Extract the interface code $Q$(MAKE) $(QUIET_ARG) -f $(LFRIC_BUILD)/extract.mk \ diff --git a/interfaces/socrates_interface/build/import.mk b/interfaces/socrates_interface/build/import.mk index 35e5a09b3..fa488ad74 100644 --- a/interfaces/socrates_interface/build/import.mk +++ b/interfaces/socrates_interface/build/import.mk @@ -9,7 +9,7 @@ export PROJECT_SOURCE = $(APPS_ROOT_DIR)/interfaces/socrates_interface/source import-socrates_interface: # Get a copy of the source code from the SCORATES repository python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(SCRATCH_DIR) -e $(APPS_ROOT_DIR)/interfaces/socrates_interface/build/extract.yaml - $Qrsync -acvz $(SCRATCH_DIR)/socrates $(WORKING_DIR)/ + $Qrsync -acvz $(SCRATCH_DIR)/scratch/socrates $(WORKING_DIR)/ # Extract the interface code $Q$(MAKE) $(QUIET_ARG) -f $(LFRIC_BUILD)/extract.mk \ From 132ccf8c799a6ee76344d1ea8ae37804353f5c75 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:31:11 +0000 Subject: [PATCH 3/7] incremental builds --- applications/lfric_atm/build/compile_options.mk | 3 ++- build/extract/extract_physics.mk | 5 +---- build/extract/extract_science.py | 12 ++++++------ interfaces/jules_interface/build/import.mk | 3 +-- interfaces/socrates_interface/build/import.mk | 3 +-- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/applications/lfric_atm/build/compile_options.mk b/applications/lfric_atm/build/compile_options.mk index 143f09ae6..842a61d60 100644 --- a/applications/lfric_atm/build/compile_options.mk +++ b/applications/lfric_atm/build/compile_options.mk @@ -13,7 +13,8 @@ $(info UM physics specific compile options) include $(PROJECT_DIR)/build/fortran/$(FORTRAN_COMPILER).mk -science/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) +casim/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) +ukca/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) jules/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) socrates/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) legacy/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) diff --git a/build/extract/extract_physics.mk b/build/extract/extract_physics.mk index 9f8c60e3f..7e43cd1b0 100644 --- a/build/extract/extract_physics.mk +++ b/build/extract/extract_physics.mk @@ -20,7 +20,4 @@ extract: # Retrieve and preprocess the UKCA and CASIM code - python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(SCRATCH_DIR) -e $(APPS_ROOT_DIR)/build/extract/extract.yaml - $Qrsync -acvz $(SCRATCH_DIR)/scratch/ukca $(WORKING_DIR)/science/ - $Qrsync -acvz $(SCRATCH_DIR)/scratch/casim $(WORKING_DIR)/science/ - + python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(WORKING_DIR) -e $(APPS_ROOT_DIR)/build/extract/extract.yaml diff --git a/build/extract/extract_science.py b/build/extract/extract_science.py index dea295974..5bde7a834 100755 --- a/build/extract/extract_science.py +++ b/build/extract/extract_science.py @@ -54,7 +54,7 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path "PHYSICS_ROOT" not in os.environ or not Path(os.environ["PHYSICS_ROOT"]).exists() ): - clone_loc = working / "clones" / dependency + clone_loc = working / ".." / "scratch" / dependency get_source( values["source"], values["ref"], @@ -66,10 +66,10 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path else: clone_loc = Path(os.environ["PHYSICS_ROOT"]) / dependency - working_dep = working / "scratch" / dependency # make the working directory location - working_dep.mkdir(parents=True, exist_ok=True) + working_dir = working / dependency + working_dir.mkdir(parents=True, exist_ok=True) # rsync extract files from clone loc to the working directory copy_command = "rsync --include='**/' " @@ -80,7 +80,7 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path extract_file = extract_file.rstrip("/") extract_file += "/**" copy_command += f"--include='{extract_file}' " - copy_command += f"--exclude='*' -avmq {clone_loc}/ {working_dep}" + copy_command += f"--exclude='*' -avmq {clone_loc}/ {working_dir}" run_command(copy_command) @@ -94,10 +94,10 @@ def parse_args() -> argparse.Namespace: "-d", "--dependencies", default="./dependencies.yaml", - help="The dependencies file for the apps working copy.", + help="The dependencies file for the apps working copy", ) parser.add_argument( - "-w", "--working", default=".", help="Location to perform extract steps in." + "-w", "--working", default=".", help="Build location" ) parser.add_argument( "-e", diff --git a/interfaces/jules_interface/build/import.mk b/interfaces/jules_interface/build/import.mk index bfc1bff97..73b35d289 100644 --- a/interfaces/jules_interface/build/import.mk +++ b/interfaces/jules_interface/build/import.mk @@ -8,8 +8,7 @@ export PROJECT_SOURCE = $(APPS_ROOT_DIR)/interfaces/jules_interface/source .PHONY: import-jules_interface import-jules_interface: # Get a copy of the source code from the JULES repository - python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(SCRATCH_DIR) -e $(APPS_ROOT_DIR)/interfaces/jules_interface/build/extract.yaml - $Qrsync -acvz $(SCRATCH_DIR)/scratch/jules $(WORKING_DIR)/ + python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(WORKING_DIR) -e $(APPS_ROOT_DIR)/interfaces/jules_interface/build/extract.yaml # Extract the interface code $Q$(MAKE) $(QUIET_ARG) -f $(LFRIC_BUILD)/extract.mk \ diff --git a/interfaces/socrates_interface/build/import.mk b/interfaces/socrates_interface/build/import.mk index fa488ad74..a12f78739 100644 --- a/interfaces/socrates_interface/build/import.mk +++ b/interfaces/socrates_interface/build/import.mk @@ -8,8 +8,7 @@ export PROJECT_SOURCE = $(APPS_ROOT_DIR)/interfaces/socrates_interface/source .PHONY: import-socrates_interface import-socrates_interface: # Get a copy of the source code from the SCORATES repository - python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(SCRATCH_DIR) -e $(APPS_ROOT_DIR)/interfaces/socrates_interface/build/extract.yaml - $Qrsync -acvz $(SCRATCH_DIR)/scratch/socrates $(WORKING_DIR)/ + python $(APPS_ROOT_DIR)/build/extract/extract_science.py -d $(APPS_ROOT_DIR)/dependencies.yaml -w $(WORKING_DIR) -e $(APPS_ROOT_DIR)/interfaces/socrates_interface/build/extract.yaml # Extract the interface code $Q$(MAKE) $(QUIET_ARG) -f $(LFRIC_BUILD)/extract.mk \ From 82f3795839762d771f25e5a6509201cb7e6f7a41 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:02:44 +0000 Subject: [PATCH 4/7] minor changes --- applications/jules/build/compile_options.mk | 3 ++- applications/lfric_coupled/build/compile_options.mk | 3 ++- applications/ngarch/build/compile_options.mk | 3 ++- build/extract/get_git_sources.py | 2 +- build/local_build.py | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/applications/jules/build/compile_options.mk b/applications/jules/build/compile_options.mk index bf4bad539..3ab2dcbf3 100644 --- a/applications/jules/build/compile_options.mk +++ b/applications/jules/build/compile_options.mk @@ -13,7 +13,8 @@ $(info UM physics specific compile options) include $(PROJECT_DIR)/build/fortran/$(FORTRAN_COMPILER).mk -science/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) +casim/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) +ukca/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) jules/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) socrates/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) legacy/%.o: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) diff --git a/applications/lfric_coupled/build/compile_options.mk b/applications/lfric_coupled/build/compile_options.mk index 10ad895f2..9623d56e1 100644 --- a/applications/lfric_coupled/build/compile_options.mk +++ b/applications/lfric_coupled/build/compile_options.mk @@ -9,7 +9,8 @@ $(info UM physics specific compile options for $(FORTRAN_COMPILER) compiler) include $(PROJECT_DIR)/build/fortran/$(FORTRAN_COMPILER).mk -science/%.o science/%.mod: private FFLAGS_EXTRA += $(FFLAGS_UM_PHYSICS) +casim/%.o science/%.mod: private FFLAGS_EXTRA += $(FFLAGS_UM_PHYSICS) +ukca/%.o science/%.mod: private FFLAGS_EXTRA += $(FFLAGS_UM_PHYSICS) jules/%.o jules/%.mod: private FFLAGS_EXTRA += $(FFLAGS_UM_PHYSICS) socrates/%.o socrates/%.mod: private FFLAGS_EXTRA += $(FFLAGS_UM_PHYSICS) legacy/%.o legacy/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) diff --git a/applications/ngarch/build/compile_options.mk b/applications/ngarch/build/compile_options.mk index 810698d65..4ef9b69bd 100644 --- a/applications/ngarch/build/compile_options.mk +++ b/applications/ngarch/build/compile_options.mk @@ -13,7 +13,8 @@ $(info UM physics specific compile options) include $(PROJECT_DIR)/build/fortran/$(FORTRAN_COMPILER).mk -science/%.o science/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) +casim/%.o science/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) +ukca/%.o science/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) jules/%.o jules/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) socrates/%.o socrates/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) legacy/%.o legacy/%.mod: private FFLAGS_EXTRA = $(FFLAGS_UM_PHYSICS) diff --git a/build/extract/get_git_sources.py b/build/extract/get_git_sources.py index 2c46108fb..ef890f895 100644 --- a/build/extract/get_git_sources.py +++ b/build/extract/get_git_sources.py @@ -28,7 +28,7 @@ def get_source( if use_mirrors: mirror_loc = Path(mirror_loc) / "MetOffice" / repo print(f"Cloning/Updating {repo} from mirror {mirror_loc} at ref {ref}") - clone_repo_mirror(source, repo, mirror_loc, dest) + clone_repo_mirror(source, ref, repo, mirror_loc, dest) else: print(f"Cloning/Updating {repo} from {source} at ref {ref}") clone_repo(source, ref, dest) diff --git a/build/local_build.py b/build/local_build.py index c4cfde039..864052cc5 100755 --- a/build/local_build.py +++ b/build/local_build.py @@ -193,7 +193,7 @@ def main(): # If using mirrors, set environment variable for science extract step if args.mirrors: - os.environ["USE_MIRRORS"] = True + os.environ["USE_MIRRORS"] = "True" os.environ["LOCAL_BUILD_MIRRORS"] = args.mirror_loc # Find the root directory of the working copy From 35903fb308c44f129e1d48122535c70354c008bd Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:25:13 +0000 Subject: [PATCH 5/7] catch missing env var --- build/extract/extract_science.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build/extract/extract_science.py b/build/extract/extract_science.py index 5bde7a834..43f7a7581 100755 --- a/build/extract/extract_science.py +++ b/build/extract/extract_science.py @@ -48,7 +48,12 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path """ use_mirrors: bool = (os.getenv('LOCAL_BUILD_MIRRORS', 'False') == 'True') - mirror_loc: Path = os.getenv("MIRROR_LOC") + mirror_loc: Path = os.getenv("MIRROR_LOC", "") + if not mirror_loc and use_mirrors: + raise KeyError( + "Use Mirrors is set true, but the MIRROR_LOC environment variable hasn't" + "been set" + ) if ( "PHYSICS_ROOT" not in os.environ From b6137e1f72a5673a8606808ee9e23654265a4ea2 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:15:56 +0000 Subject: [PATCH 6/7] update local build docs --- .../source/developer_guide/local_builds.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/documentation/source/developer_guide/local_builds.rst b/documentation/source/developer_guide/local_builds.rst index 9e5bd9310..c57f67e62 100644 --- a/documentation/source/developer_guide/local_builds.rst +++ b/documentation/source/developer_guide/local_builds.rst @@ -67,3 +67,19 @@ This table lists the command line arguments available: | | | will request verbose output | | | | from the makefile. | +----------------------+-----------------------------+-----------------------------+ +| ``-m --mirrors`` | False | If True, this will attempt | +| ``store_true`` | | to extract using local | +| | | github mirrors | ++----------------------+-----------------------------+-----------------------------+ +| ``--mirror-loc`` | MetOffice Mirror Location | The path to the github | +| | | mirror location | ++----------------------+-----------------------------+-----------------------------+ + +Incremental Builds +------------------ + +The local build script will attempt to build incrementally if a previous attempt +at the build exists. This should happen automatically if the working directory +is the same. If there are large changes then it may be sensible to start the +build afresh by cleaning the build ``-t clean`` (or deleting the working +directory). From 7500ed610246266dd7df186cb2ad2a65666719e4 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:06:13 +0000 Subject: [PATCH 7/7] review comments --- build/extract/extract_science.py | 8 +------- build/extract/get_git_sources.py | 33 ++++++++++++++++++++++++-------- build/local_build.py | 10 +++++----- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/build/extract/extract_science.py b/build/extract/extract_science.py index 43f7a7581..3ab52c9c3 100755 --- a/build/extract/extract_science.py +++ b/build/extract/extract_science.py @@ -47,13 +47,7 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path Then delete the temporary directory """ - use_mirrors: bool = (os.getenv('LOCAL_BUILD_MIRRORS', 'False') == 'True') mirror_loc: Path = os.getenv("MIRROR_LOC", "") - if not mirror_loc and use_mirrors: - raise KeyError( - "Use Mirrors is set true, but the MIRROR_LOC environment variable hasn't" - "been set" - ) if ( "PHYSICS_ROOT" not in os.environ @@ -65,7 +59,7 @@ def extract_files(dependency: str, values: Dict, files: List[str], working: Path values["ref"], clone_loc, dependency, - use_mirrors, + bool(mirror_loc), mirror_loc ) else: diff --git a/build/extract/get_git_sources.py b/build/extract/get_git_sources.py index ef890f895..2b5c1455b 100644 --- a/build/extract/get_git_sources.py +++ b/build/extract/get_git_sources.py @@ -4,7 +4,7 @@ # which you should have received as part of this distribution. # *****************************COPYRIGHT******************************* """ -Clone sources for a rose-stem run for use with git bdiff module in scripts +Helper functions for cloning git sources in command line builds """ import re @@ -66,25 +66,40 @@ def run_command( def clone_repo_mirror( - source: str, repo_ref: str, parent: str, mirror_loc: Path, loc: Path + repo_source: str, repo_ref: str, parent: str, mirror_loc: Path, loc: Path ) -> None: """ Clone a repo source using a local git mirror. Assume the mirror is set up as per the Met Office + - repo_source: ssh url of the source repository + - repo_ref: git ref for the source. An empty string will get the default branch + - parent: Owner of the github repository being cloned (required to construct the + mirror path) + - mirror_loc: path to the local git mirrors + - loc: path to clone the repository to """ - # Remove if this clone already exists - if not loc.exists(): + # If the repository exists and isn't a git repo, exit now as we don't want to + # overwrite it + if loc.exists(): + if not Path(loc / ".git").exists(): + raise RuntimeError( + f"The destination for the clone of {repo_source} already exists but " + "isn't a git directory. Exiting so as to not overwrite it." + ) + + # Clone if the repo doesn't exist + else: command = f"git clone {mirror_loc} {loc}" run_command(command) - # If not provided a ref, return + # If not provided a ref, pull the latest repository and return if not repo_ref: run_command(f"git -C {loc} pull") return - source = source.removeprefix("git@github.com:") - user = source.split("/")[0] + repo_source = repo_source.removeprefix("git@github.com:") + user = repo_source.split("/")[0] # Check that the user is different to the Upstream User if user in parent.split("/")[0]: user = None @@ -107,9 +122,11 @@ def clone_repo(repo_source: str, repo_ref: str, loc: Path) -> None: """ Clone the repo and checkout the provided ref Only if a remote source + - repo_source: ssh url of the source repository + - repo_ref: git ref for the source. An empty string will get the default branch + - loc: path to clone the repository to """ - # Remove if this clone already exists if not loc.exists(): # Create a clean clone location loc.mkdir(parents=True) diff --git a/build/local_build.py b/build/local_build.py index 864052cc5..7c63d825a 100755 --- a/build/local_build.py +++ b/build/local_build.py @@ -47,7 +47,7 @@ def get_root_path(): Get the root path of the current working copy """ - return Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + return Path(__file__).absolute().parent.parent def determine_core_source(root_dir): @@ -154,7 +154,8 @@ def main(): "-w", "--working_dir", default=None, - help="Working directory where builds occur. Default to the project " + type=Path, + help="Working directory where builds occur. Defaults to the project " "directory in the working copy.", ) parser.add_argument( @@ -193,8 +194,7 @@ def main(): # If using mirrors, set environment variable for science extract step if args.mirrors: - os.environ["USE_MIRRORS"] = "True" - os.environ["LOCAL_BUILD_MIRRORS"] = args.mirror_loc + os.environ["USE_MIRRORS"] = args.mirror_loc # Find the root directory of the working copy root_dir = get_root_path() @@ -207,7 +207,7 @@ def main(): args.working_dir = Path(project_path) / "working" else: # If the working dir doesn't end in working, set that here - if not args.working_dir.strip("/").endswith("working"): + if not args.working_dir.name == "working": args.working_dir = Path(args.working_dir) / "working" # Ensure that working_dir is an absolute path and make the directory args.working_dir = args.working_dir.resolve()