From dc3decf9bd74bb6d033709e71095cd7cc0d76c18 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Mon, 21 Sep 2026 16:27:05 -0600 Subject: [PATCH 1/3] Prevent circular symlinks from overwriting atmosphere files The previous logic allowed symlinks to overwrite original files when processing was run multiple times. This new logic should prevent that. --- .../ismip7_forcing/ocean_thermal/process_thermal_forcing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py b/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py index 595d90b964..b8550c3ee3 100644 --- a/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py +++ b/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py @@ -214,7 +214,7 @@ def _link_atmosphere_outputs(self, output_base_path, atm_forcing_group, for fname in os.listdir(src_dir): src = os.path.join(src_dir, fname) - if not os.path.isfile(src): + if os.path.islink(src) or not os.path.isfile(src): continue dst = os.path.join(dst_dir, fname) if os.path.lexists(dst): From d7416f8d10eb32921f6457db8415cba7d1e53f8f Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Thu, 24 Sep 2026 13:01:55 -0600 Subject: [PATCH 2/3] Add realpath guards to prevent circular symlinks in atmosphere mirroring The previous fix (dc3decf9b) added os.path.islink(src) to skip symlinks on the source side, but did not prevent the case where the destination directory itself resolves to the source directory (e.g., when OCX_main is a symlink to OCX, or when OCX_main/atmosphere is a symlink to OCX/atmosphere). This change adds two realpath-based guards: 1. Directory-level: Skip the entire mirror operation if the destination directory resolves to the source directory. 2. File-level: Skip individual files if the destination file would resolve to the source file. Both guards use os.path.realpath() which fully resolves symlink chains, so the original atmosphere files can never be overwritten by the mirroring logic, regardless of directory aliasing. Co-Authored-By: Claude Sonnet 4.5 --- .../ocean_thermal/process_thermal_forcing.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py b/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py index b8550c3ee3..3f7c60e3dd 100644 --- a/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py +++ b/compass/landice/tests/ismip7_forcing/ocean_thermal/process_thermal_forcing.py @@ -210,6 +210,17 @@ def _link_atmosphere_outputs(self, output_base_path, atm_forcing_group, dst_dir = os.path.join(output_base_path, choice_forcing_group, "atmosphere") + + # Guard against destination directory resolving to source directory + # (e.g., if choice_forcing_group is a symlink to atm_forcing_group) + if os.path.realpath(dst_dir) == os.path.realpath(src_dir): + logger.warning( + f"Skipping atmosphere mirror for {choice_forcing_group}: " + f"destination directory {dst_dir} resolves to source " + f"directory {src_dir}. Relinking would overwrite the " + f"original files.") + return + os.makedirs(dst_dir, exist_ok=True) for fname in os.listdir(src_dir): @@ -217,6 +228,15 @@ def _link_atmosphere_outputs(self, output_base_path, atm_forcing_group, if os.path.islink(src) or not os.path.isfile(src): continue dst = os.path.join(dst_dir, fname) + + # Guard against destination file resolving to source file + if os.path.realpath(dst) == os.path.realpath(src): + logger.warning( + f"Skipping {fname}: destination {dst} resolves to " + f"source {src}. Relinking would overwrite the " + f"original.") + continue + if os.path.lexists(dst): os.remove(dst) os.symlink(src, dst) From a2b29655f63635f82ec652a25bb5dc4a94f36f6b Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Thu, 24 Sep 2026 12:07:24 -0700 Subject: [PATCH 3/3] Update AIS OCX ocean forcing to v2 --- compass/landice/ismip7/ice_sheet_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/landice/ismip7/ice_sheet_params.py b/compass/landice/ismip7/ice_sheet_params.py index d991eae45c..222f202d89 100644 --- a/compass/landice/ismip7/ice_sheet_params.py +++ b/compass/landice/ismip7/ice_sheet_params.py @@ -44,7 +44,7 @@ }, 'ais': { 'atm_version': 'v1', - 'ocean_version': 'v1', + 'ocean_version': 'v2', 'atm_model': 'RACMO2.3p2-ERA', 'ocean_model': None, # AIS OCX ocean files have no model token and live in per-choice