Thin wrapper around netcdf-fortran (nf90_*) with error checking
!! Thin wrapper around netcdf-fortran (nf90_*) with error checking module rdb_io_netcdf !! Provides NetCDF read/write primitives and an error-checking helper. !! Used by bathymetry loader and output writer. use, intrinsic :: iso_fortran_env, only: real32 use, intrinsic :: iso_c_binding, only: c_char, c_int, c_null_char, c_ptr, c_loc use netcdf, only: nf90_noerr, nf90_strerror, nf90_create, nf90_open, & nf90_close, nf90_def_dim, nf90_def_var, nf90_put_att, nf90_enddef, & nf90_put_var, nf90_get_var, nf90_inq_dimid, nf90_inquire_dimension, & nf90_inq_varid, nf90_get_att, nf90_def_var_deflate, & nf90_clobber, nf90_netcdf4, nf90_nowrite, nf90_write, & nf90_unlimited, nf90_double, nf90_float, nf90_int, nf90_global, & nf90_redef, nf90_sync use rdb_constants, only: wp use pic_logger, only: logger => global_logger use pic_strings, only: to_string use rdb_error_ring, only: fail implicit none private public :: nc_check public :: ensure_directory_exists public :: nc_create_file public :: nc_open_read public :: nc_open_write public :: nc_close public :: nc_def_dim public :: nc_def_var_2d public :: nc_def_var_3d public :: nc_put_att public :: nc_put_att_real public :: nc_put_att_int public :: nc_put_att_global public :: nc_put_att_global_int public :: nc_get_att_int public :: nc_enddef public :: nc_put_var_2d public :: nc_put_var_3d_slice public :: nc_get_var_2d public :: nc_get_var_3d public :: nc_get_var_1d public :: nc_get_dim_len public :: nc_get_varid public :: rdb_def_var_1d public :: rdb_put_var_1d public :: rdb_put_var_2d_slice public :: nc_def_var_4d public :: nc_put_var_4d_slice public :: nc_get_var_slab_3d public :: nc_get_var_slab_2d public :: nc_get_att_text public :: nc_put_att_real_r4 public :: nc_put_var_3d_slice_r4 public :: nc_put_var_4d_slice_r4 public :: NC_WP public :: NC_R4 public :: output_rank_filename #ifdef RDB_DOUBLE_PRECISION integer, parameter :: NC_WP = nf90_double !! NetCDF type matching working precision #else integer, parameter :: NC_WP = nf90_float !! NetCDF type matching working precision #endif integer, parameter :: NC_R4 = nf90_float !! Explicit 32-bit NetCDF element type. Used ONLY by the ocean !! diagnostic stream when `&ocean_diag_nml output_precision="single"` !! asks for a halved-width output file. Restarts, gauges, coastal !! output and console/conservation totals stay on `NC_WP` — a lossy !! restart must not be requestable. The 64-bit counterpart is !! `NC_WP` itself — deliberately NOT given an `NC_R8` alias, so !! "double" has exactly one spelling and cannot drift away from the !! build's working precision. contains subroutine nc_check(status, context, ierr) !! Check NetCDF return status and log error if it failed integer, intent(in) :: status !! Return code from nf90_* call character(len=*), intent(in) :: context !! Description of what was being attempted integer, intent(out), optional :: ierr !! Non-zero (the raw `nf90_*` status) on failure when present; !! absent behaves as today (`error stop`). The specific reason !! is always logged via `global_logger%error` first, so the !! caller-facing code only needs to say THAT a read/write failed. if (status /= nf90_noerr) then call fail("NetCDF error in "//trim(context)//": "// & trim(nf90_strerror(status)), ierr, status) return end if if (present(ierr)) ierr = nf90_noerr end subroutine nc_check subroutine nc_create_file(filename, ncid, ierr) !! Create a new NetCDF-4 file (overwrites if exists) character(len=*), intent(in) :: filename integer, intent(out) :: ncid integer, intent(out), optional :: ierr !! Non-zero (e.g. a missing output directory) when present; absent !! behaves as today (`error stop`). call nc_check(nf90_create(trim(filename), & ior(nf90_clobber, nf90_netcdf4), ncid), & "creating "//trim(filename), ierr) end subroutine nc_create_file subroutine nc_open_read(filename, ncid, ierr) !! Open an existing NetCDF file for reading character(len=*), intent(in) :: filename integer, intent(out) :: ncid integer, intent(out), optional :: ierr !! Non-zero (a missing/unreadable file) when present; absent !! behaves as today (`error stop`). call nc_check(nf90_open(trim(filename), nf90_nowrite, ncid), & "opening "//trim(filename), ierr) end subroutine nc_open_read subroutine nc_open_write(filename, ncid, ierr) !! Open an existing NetCDF file for writing character(len=*), intent(in) :: filename integer, intent(out) :: ncid integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_open(trim(filename), nf90_write, ncid), & "opening for write "//trim(filename), ierr) end subroutine nc_open_write subroutine nc_close(ncid, ierr) !! Close a NetCDF file integer, intent(in) :: ncid integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_close(ncid), "closing file", ierr) end subroutine nc_close subroutine nc_def_dim(ncid, name, length, dimid, ierr) !! Define a dimension (use nf90_unlimited for unlimited) integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(in) :: length integer, intent(out) :: dimid integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_def_dim(ncid, name, length, dimid), & "defining dimension "//trim(name), ierr) end subroutine nc_def_dim subroutine rdb_def_var_1d(ncid, name, dimid, varid, deflate_level, ierr) !! Define a 1D variable with working precision type integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(in) :: dimid integer, intent(out) :: varid integer, intent(in), optional :: deflate_level !! Compression level (0=none, 1-9=deflate). Enables shuffle when > 0. integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_def_var(ncid, name, NC_WP, [dimid], varid), & "defining variable "//trim(name), ierr) if (present(ierr)) then if (ierr /= nf90_noerr) return end if if (present(deflate_level)) then if (deflate_level > 0) then call nc_check(nf90_def_var_deflate(ncid, varid, & shuffle=1, deflate=1, deflate_level=deflate_level), & "enabling compression for "//trim(name), ierr) end if end if end subroutine rdb_def_var_1d subroutine rdb_put_var_1d(ncid, varid, data, ierr) !! Write a full 1D array integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(in) :: data(:) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data), "writing 1D variable", ierr) end subroutine rdb_put_var_1d subroutine rdb_put_var_2d_slice(ncid, varid, data, time_index, ierr) !! Write a 1D slice into a 2D variable at a given time index integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(in) :: data(:) integer, intent(in) :: time_index integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data, & start=[1, time_index], & count=[size(data), 1]), & "writing 2D slice", ierr) end subroutine rdb_put_var_2d_slice subroutine nc_def_var_2d(ncid, name, dimids, varid, deflate_level, ierr) !! Define a 2D variable with working precision type integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(in) :: dimids(2) integer, intent(out) :: varid integer, intent(in), optional :: deflate_level !! Compression level (0=none, 1-9=deflate). Enables shuffle when > 0. integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_def_var(ncid, name, NC_WP, dimids, varid), & "defining variable "//trim(name), ierr) if (present(ierr)) then if (ierr /= nf90_noerr) return end if if (present(deflate_level)) then if (deflate_level > 0) then call nc_check(nf90_def_var_deflate(ncid, varid, & shuffle=1, deflate=1, deflate_level=deflate_level), & "enabling compression for "//trim(name), ierr) end if end if end subroutine nc_def_var_2d subroutine nc_def_var_3d(ncid, name, dimids, varid, deflate_level, xtype, ierr) !! Define a 3D variable (x, y, time) with working precision type integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(in) :: dimids(3) integer, intent(out) :: varid integer, intent(in), optional :: deflate_level !! Compression level (0=none, 1-9=deflate). Enables shuffle when > 0. integer, intent(in), optional :: xtype !! NetCDF element type. Absent => `NC_WP` (working precision), so !! every legacy call site is unchanged. The ocean diagnostic !! stream passes `NC_R4` when single-precision output is requested; !! the caller is then responsible for handing `nf90_put_var` real32 !! data (see `nc_put_var_3d_slice_r4`). integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). integer :: xt xt = NC_WP if (present(xtype)) xt = xtype call nc_check(nf90_def_var(ncid, name, xt, dimids, varid), & "defining variable "//trim(name), ierr) if (present(ierr)) then if (ierr /= nf90_noerr) return end if if (present(deflate_level)) then if (deflate_level > 0) then call nc_check(nf90_def_var_deflate(ncid, varid, & shuffle=1, deflate=1, deflate_level=deflate_level), & "enabling compression for "//trim(name), ierr) end if end if end subroutine nc_def_var_3d subroutine nc_put_att(ncid, varid, name, value, ierr) !! Write a character attribute to a variable integer, intent(in) :: ncid integer, intent(in) :: varid character(len=*), intent(in) :: name character(len=*), intent(in) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_att(ncid, varid, name, value), & "writing attribute "//trim(name), ierr) end subroutine nc_put_att subroutine nc_put_att_real(ncid, varid, name, value, ierr) !! Write a real-valued attribute to a variable integer, intent(in) :: ncid integer, intent(in) :: varid character(len=*), intent(in) :: name real(wp), intent(in) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_att(ncid, varid, name, value), & "writing attribute "//trim(name), ierr) end subroutine nc_put_att_real subroutine nc_put_att_real_r4(ncid, varid, name, value, ierr) !! Write a 32-bit real attribute to a variable. NetCDF rejects a !! `_FillValue` whose type differs from the variable's, so a variable !! defined with `NC_R4` must have its `_FillValue` / `missing_value` !! written through here, not through `nc_put_att_real` (real64). integer, intent(in) :: ncid integer, intent(in) :: varid character(len=*), intent(in) :: name real(real32), intent(in) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_att(ncid, varid, name, value), & "writing real32 attribute "//trim(name), ierr) end subroutine nc_put_att_real_r4 subroutine nc_put_att_global(ncid, name, value, ierr) !! Write a global character attribute integer, intent(in) :: ncid character(len=*), intent(in) :: name character(len=*), intent(in) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_att(ncid, nf90_global, name, value), & "writing global attribute "//trim(name), ierr) end subroutine nc_put_att_global subroutine nc_put_att_int(ncid, varid, name, value, ierr) !! Write an integer attribute to a variable integer, intent(in) :: ncid integer, intent(in) :: varid character(len=*), intent(in) :: name integer, intent(in) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_att(ncid, varid, name, value), & "writing attribute "//trim(name), ierr) end subroutine nc_put_att_int subroutine nc_put_att_global_int(ncid, name, value, ierr) !! Write a global integer attribute integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(in) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_att(ncid, nf90_global, name, value), & "writing global attribute "//trim(name), ierr) end subroutine nc_put_att_global_int subroutine nc_get_att_int(ncid, name, value, ierr) !! Read a global integer attribute integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(out) :: value integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_get_att(ncid, nf90_global, name, value), & "reading global attribute "//trim(name), ierr) end subroutine nc_get_att_int subroutine nc_enddef(ncid, ierr) !! End define mode, switch to data mode integer, intent(in) :: ncid integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_enddef(ncid), "ending define mode", ierr) end subroutine nc_enddef subroutine nc_put_var_2d(ncid, varid, data, ierr) !! Write a full 2D array integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(in) :: data(:, :) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data), "writing 2D variable", ierr) end subroutine nc_put_var_2d subroutine nc_put_var_3d_slice(ncid, varid, data, time_index, ierr) !! Write a 2D slice into a 3D variable at a given time index integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(in) :: data(:, :) integer, intent(in) :: time_index integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data, & start=[1, 1, time_index], & count=[size(data, 1), size(data, 2), 1]), & "writing 3D slice", ierr) end subroutine nc_put_var_3d_slice subroutine nc_put_var_3d_slice_r4(ncid, varid, data, time_index, ierr) !! Write a 32-bit 2D slice into a 3D variable at a given time index. !! The real32 twin of `nc_put_var_3d_slice`: handing real64 data to !! an `NF90_FLOAT` variable is legal (netcdf converts) but writes the !! same bytes at twice the host->library traffic, so the diag writer !! stages the conversion itself and calls this. integer, intent(in) :: ncid integer, intent(in) :: varid real(real32), intent(in) :: data(:, :) integer, intent(in) :: time_index integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data, & start=[1, 1, time_index], & count=[size(data, 1), size(data, 2), 1]), & "writing 3D slice (real32)", ierr) end subroutine nc_put_var_3d_slice_r4 subroutine nc_put_var_4d_slice_r4(ncid, varid, data, time_index, ierr) !! Write a 32-bit 3D slice into a 4D variable at a given time index. !! The real32 twin of `nc_put_var_4d_slice`. integer, intent(in) :: ncid integer, intent(in) :: varid real(real32), intent(in) :: data(:, :, :) integer, intent(in) :: time_index integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data, & start=[1, 1, 1, time_index], & count=[size(data, 1), size(data, 2), size(data, 3), 1]), & "writing 4D slice (real32)", ierr) end subroutine nc_put_var_4d_slice_r4 subroutine nc_get_var_2d(ncid, varid, data, ierr) !! Read a full 2D array integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(inout) :: data(:, :) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_get_var(ncid, varid, data), "reading 2D variable", ierr) end subroutine nc_get_var_2d subroutine nc_get_var_3d(ncid, varid, data, ierr) !! Read a full 3D array. Mirrors `nc_get_var_2d`; the target's !! shape must match the file variable's Fortran storage order !! (the caller handles any C/Fortran dimension reversal, as !! `rdb_bathymetry` does for 2D). integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(inout) :: data(:, :, :) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_get_var(ncid, varid, data), "reading 3D variable", ierr) end subroutine nc_get_var_3d subroutine nc_get_var_1d(ncid, varid, data, ierr) !! Read a full 1D array (the read sibling of `rdb_put_var_1d`). integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(inout) :: data(:) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_get_var(ncid, varid, data), "reading 1D variable", ierr) end subroutine nc_get_var_1d subroutine nc_get_dim_len(ncid, name, length, ierr) !! Get the length of a named dimension integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(out) :: length integer, intent(out), optional :: ierr !! Non-zero (missing dimension) when present; absent behaves as !! today (`error stop`). integer :: dimid call nc_check(nf90_inq_dimid(ncid, name, dimid), & "finding dimension "//trim(name), ierr) if (present(ierr)) then if (ierr /= nf90_noerr) return end if call nc_check(nf90_inquire_dimension(ncid, dimid, len=length), & "querying dimension "//trim(name), ierr) end subroutine nc_get_dim_len subroutine nc_def_var_4d(ncid, name, dimids, varid, deflate_level, xtype, ierr) !! Define a 4D variable (x, y, z, time) with working precision type integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(in) :: dimids(4) integer, intent(out) :: varid integer, intent(in), optional :: deflate_level !! Compression level (0=none, 1-9=deflate). Enables shuffle when > 0. integer, intent(in), optional :: xtype !! NetCDF element type. Absent => `NC_WP` (working precision). !! See `nc_def_var_3d` — same contract, 4D shape. integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). integer :: xt xt = NC_WP if (present(xtype)) xt = xtype call nc_check(nf90_def_var(ncid, name, xt, dimids, varid), & "defining variable "//trim(name), ierr) if (present(ierr)) then if (ierr /= nf90_noerr) return end if if (present(deflate_level)) then if (deflate_level > 0) then call nc_check(nf90_def_var_deflate(ncid, varid, & shuffle=1, deflate=1, deflate_level=deflate_level), & "enabling compression for "//trim(name), ierr) end if end if end subroutine nc_def_var_4d subroutine nc_put_var_4d_slice(ncid, varid, data, time_index, ierr) !! Write a 3D slice into a 4D variable at a given time index integer, intent(in) :: ncid integer, intent(in) :: varid real(wp), intent(in) :: data(:, :, :) integer, intent(in) :: time_index integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_put_var(ncid, varid, data, & start=[1, 1, 1, time_index], & count=[size(data, 1), size(data, 2), size(data, 3), 1]), & "writing 4D slice", ierr) end subroutine nc_put_var_4d_slice subroutine nc_get_var_slab_3d(ncid, varid, start, count, data, ierr) !! Strided slab read: `start`/`count` are the NetCDF start/count !! vectors in FORTRAN dimension order, with length matching the !! target variable's rank (3 for a 2D-field-plus-time variable, 4 !! for a 3D-field-plus-time variable). `data` is always a plain !! 3D Fortran array; for the 2D-plus-time case pass a target with !! a trailing singleton extent (`data(:,:,1)`) — the netcdf-fortran !! generic interface dispatches on the array RANK of `data`, not !! the length of `start`/`count`, so one routine covers both !! shapes. This is the first strided (non-full-variable) read in !! the tree — mirrors `nc_put_var_4d_slice` for the start/count !! shape, on the read side. integer, intent(in) :: ncid, varid integer, intent(in) :: start(:), count(:) real(wp), intent(inout) :: data(:, :, :) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_get_var(ncid, varid, data, start=start, count=count), & "reading NetCDF slab", ierr) end subroutine nc_get_var_slab_3d subroutine nc_get_var_slab_2d(ncid, varid, start, count, data, ierr) !! Windowed read of a 2D variable: `start`/`count` (length 2, FORTRAN !! dimension order) select the block that lands in `data`, whose !! shape must equal `count`. The per-rank readers (bathymetry, !! supergrid) use it to load only the rows their tile needs. integer, intent(in) :: ncid, varid integer, intent(in) :: start(2), count(2) real(wp), intent(inout) :: data(:, :) integer, intent(out), optional :: ierr !! Non-zero on failure when present; absent behaves as today !! (`error stop`). call nc_check(nf90_get_var(ncid, varid, data, start=start, count=count), & "reading NetCDF 2D window", ierr) end subroutine nc_get_var_slab_2d subroutine nc_get_att_text(ncid, varid, name, value, ok) !! Read a character (text) attribute from a variable — used for the !! CF `units` attribute on a time axis. Non-fail-loud: `ok` is !! `.false.` (and `value` blank) when the attribute is absent or the !! wrong type, so callers can fall back to a default rather than !! aborting on a missing/optional attribute. integer, intent(in) :: ncid, varid character(len=*), intent(in) :: name character(len=:), allocatable, intent(out) :: value !! Deferred-length (not assumed-length `intent(out)`, which risks !! silent truncation — fortitude C072): filled from a fixed local !! buffer sized for a CF `units` string. logical, intent(out) :: ok integer :: status character(len=1024) :: buf buf = "" status = nf90_get_att(ncid, varid, name, buf) ok = (status == nf90_noerr) if (ok) then value = trim(buf) else value = "" end if end subroutine nc_get_att_text subroutine nc_get_varid(ncid, name, varid, ierr) !! Get the variable ID for a named variable integer, intent(in) :: ncid character(len=*), intent(in) :: name integer, intent(out) :: varid integer, intent(out), optional :: ierr !! Non-zero (no matching variable) when present; absent behaves !! as today (`error stop`). call nc_check(nf90_inq_varid(ncid, name, varid), & "finding variable "//trim(name), ierr) end subroutine nc_get_varid function output_rank_filename(base_dir, prefix, rank) result(fname) !! Generate a per-rank filename: <base_dir>/<prefix>_rank_NNNNNN.nc character(len=*), intent(in) :: base_dir !! Output directory character(len=*), intent(in) :: prefix !! File prefix (e.g. "output" or "restart") integer, intent(in) :: rank !! MPI rank number character(len=512) :: fname write (fname, '(A,"/",A,"_rank_",I6.6,".nc")') & trim(base_dir), trim(prefix), rank end function output_rank_filename subroutine ensure_directory_exists(path, ierr) !! Create `path` via libc `mkdir(2)` if it does not already exist !! (single path component — not a recursive `mkdir -p`; a missing !! grandparent still fails, loudly, at the `inquire` re-check !! below). Exists to close the headline P7 crash: `&ocean_diag_nml`/ !! `&output_nml` default `output_dir = "./output"` !! (`rdb_config.F90:2316`), so a first-time `create()` against a !! fresh checkout with no `./output/` directory would otherwise !! reach `nc_create_file` -> `nf90_create` -> ENOENT -> !! `error stop`, killing the whole host process. !! !! Never `error stop`s itself, present `ierr` or not — the caller !! decides how to fail (via its OWN `nc_create_file(..., ierr=)` !! call on the resulting still-missing directory, or by checking !! this routine's `ierr` directly). `ierr` is a plain 0-ok/ !! non-zero-failed flag (not an `nf90_*` status), because `mkdir` !! is not a NetCDF call and has no `nc_check` counterpart. character(len=*), intent(in) :: path integer, intent(out), optional :: ierr !! Non-zero iff `path` still does not exist after the `mkdir` !! attempt. Absent is legal — the routine just does its best and !! lets the caller's own I/O call fail loud. logical :: exists integer(c_int) :: rc character(kind=c_char), allocatable, target :: c_path(:) interface function c_mkdir(dirpath, mode) bind(C, name="mkdir") result(r) import :: c_ptr, c_int implicit none type(c_ptr), value, intent(in) :: dirpath integer(c_int), value, intent(in) :: mode integer(c_int) :: r end function c_mkdir end interface if (present(ierr)) ierr = 0 if (len_trim(path) == 0) return inquire (file=trim(path), exist=exists) if (exists) return c_path = path_to_c_string(trim(path)) rc = c_mkdir(c_loc(c_path), int(o'755', c_int)) if (rc /= 0_c_int) then call logger%warning("ensure_directory_exists: mkdir('"//trim(path)// & "') returned "//to_string(int(rc))// & " -- the subsequent NetCDF create/open will "// & "report the exact failure if the directory "// & "is still missing") end if inquire (file=trim(path), exist=exists) if (.not. exists) then if (present(ierr)) ierr = -1 end if end subroutine ensure_directory_exists pure function path_to_c_string(s) result(c) !! Pack a Fortran string into a null-terminated c_char array for !! the `mkdir` binding above. Private duplicate of !! `rdb_ocean_restart_io`'s `string_to_c` (that one is private to !! its own module) — small enough that sharing it is not worth a !! cross-module dependency for a two-line helper. character(len=*), intent(in) :: s character(kind=c_char), allocatable :: c(:) integer :: i, n n = len_trim(s) allocate (c(n + 1)) do i = 1, n c(i) = s(i:i) end do c(n + 1) = c_null_char end function path_to_c_string end module rdb_io_netcdf