Add build-time git version metadata and CLI --version / --build-info#59
Add build-time git version metadata and CLI --version / --build-info#59d-diaz wants to merge 3 commits into
Conversation
|
The output header line generated by Another question is whether now would be the time to make any tweaks to the information printed on the header of the out file. The current pattern is: I’ll make a change to correct the misspelling of the variant name, but we could also update it to include more build info if desired. I would advocate that the org name be added, for example: |
|
Because callers from R or Python would not know the length of the version strings ahead of time, these new subroutines use fixed-length buffers. You can test this from python using the following script: If you put this script in the """
test_fvs_version_ctypes.py
Exercise fvsGetVersion and fvsGetBuildInfo via ctypes against a built
FVS shared library.
Usage:
python3 tools/tests/test_fvs_version_ctypes.py <path/to/libFVSpn.so>
Or with environment variable:
FVS_LIB=builddir/libFVSpn.so python3 tools/tests/test_fvs_version_ctypes.py
The script prints the results of both calls and exits non-zero on any
failure, so it can be used as a simple smoke check in CI.
"""
from __future__ import annotations
import ctypes
import os
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Buffer size constants — must match FVS_VERSION_BUFLEN and
# FVS_BUILDINFO_BUFLEN in base/version.f
# ---------------------------------------------------------------------------
FVS_VERSION_BUFLEN = 64
FVS_BUILDINFO_BUFLEN = 512
def load_library(lib_path: Path) -> ctypes.CDLL:
try:
return ctypes.CDLL(str(lib_path))
except OSError as e:
print(f"ERROR: Could not load {lib_path}: {e}", file=sys.stderr)
sys.exit(1)
def resolve_symbols(lib: ctypes.CDLL) -> tuple:
"""
Resolve the mangled Fortran module subroutine symbols.
gfortran module subroutine mangling:
Linux/macOS: __<module>_MOD_<subroutine> (no trailing underscore)
Windows: __<module>_MOD_<subroutine> (no trailing underscore)
Both subroutines have signature:
(buf: char*, buflen: int*, nch: int*) -> void
"""
names = {
"get_version": "__fvsversion_MOD_fvsgetversion",
"get_buildinfo": "__fvsversion_MOD_fvsgetbuildinfo",
}
symbols = {}
for key, name in names.items():
try:
sym = getattr(lib, name)
except AttributeError:
print(
f"ERROR: Symbol {name!r} not found in library.\n"
f"Check mangling with: nm -D <lib> | grep -i fvsver",
file=sys.stderr,
)
sys.exit(1)
sym.restype = None
sym.argtypes = [
ctypes.c_char_p, # buf (out)
ctypes.POINTER(ctypes.c_int), # buflen (in)
ctypes.POINTER(ctypes.c_int), # nch (out)
]
symbols[key] = sym
return symbols["get_version"], symbols["get_buildinfo"]
def fvs_get_version(sym) -> str:
buf = ctypes.create_string_buffer(FVS_VERSION_BUFLEN)
buflen = ctypes.c_int(FVS_VERSION_BUFLEN)
nch = ctypes.c_int(0)
sym(buf, ctypes.byref(buflen), ctypes.byref(nch))
return buf.raw[: nch.value].decode("ascii")
def fvs_get_build_info(sym) -> str:
buf = ctypes.create_string_buffer(FVS_BUILDINFO_BUFLEN)
buflen = ctypes.c_int(FVS_BUILDINFO_BUFLEN)
nch = ctypes.c_int(0)
sym(buf, ctypes.byref(buflen), ctypes.byref(nch))
return buf.raw[: nch.value].decode("ascii")
def check_version(version: str) -> list[str]:
"""
Basic sanity checks on --version output.
Returns list of failure strings, empty on success.
"""
failures = []
if not version:
failures.append("version string is empty")
return failures
if "(" not in version or ")" not in version:
failures.append(f"version string missing org in parens: {version!r}")
if "unknown" in version:
failures.append(
f'version string contains "unknown" — '
f"build metadata was not injected: {version!r}"
)
if len(version) >= FVS_VERSION_BUFLEN:
failures.append(
f"version string length {len(version)} >= buffer size "
f"{FVS_VERSION_BUFLEN} — possible truncation"
)
return failures
def check_build_info(info: str) -> list[str]:
"""
Basic sanity checks on --build-info output.
Returns list of failure strings, empty on success.
"""
failures = []
if not info:
failures.append("build info string is empty")
return failures
expected_fields = ("Version:", "Org:", "Remote:", "Branch:", "Commit:", "Date:")
for field in expected_fields:
if field not in info:
failures.append(f"missing field {field!r} in build info")
if "unknown" in info:
failures.append(
'build info contains "unknown" — build metadata was not injected'
)
if len(info) >= FVS_BUILDINFO_BUFLEN:
failures.append(
f"build info length {len(info)} >= buffer size "
f"{FVS_BUILDINFO_BUFLEN} — possible truncation"
)
return failures
def main() -> int:
# Resolve library path from argument or environment
if len(sys.argv) > 1:
lib_path = Path(sys.argv[1])
elif "FVS_LIB" in os.environ:
lib_path = Path(os.environ["FVS_LIB"])
else:
print(
"Usage: test_fvs_version_ctypes.py <path/to/libFVS.so>\n"
" or: FVS_LIB=<path> test_fvs_version_ctypes.py",
file=sys.stderr,
)
return 1
if not lib_path.exists():
print(f"ERROR: Library not found: {lib_path}", file=sys.stderr)
return 1
print(f"Loading: {lib_path}")
lib = load_library(lib_path)
get_version_sym, get_buildinfo_sym = resolve_symbols(lib)
failures = []
# -- fvsGetVersion -------------------------------------------------------
print("\n--- fvsGetVersion ---")
version = fvs_get_version(get_version_sym)
print(version)
vfailures = check_version(version)
if vfailures:
for f in vfailures:
print(f" FAIL: {f}")
failures.extend(vfailures)
else:
print(" ok")
# -- fvsGetBuildInfo ------------------------------------------------------
print("\n--- fvsGetBuildInfo ---")
build_info = fvs_get_build_info(get_buildinfo_sym)
print(build_info)
bfailures = check_build_info(build_info)
if bfailures:
for f in bfailures:
print(f" FAIL: {f}")
failures.extend(bfailures)
else:
print(" ok")
# -- Summary --------------------------------------------------------------
print()
if failures:
print(f"FAILED: {len(failures)} check(s) failed")
return 1
else:
print("PASSED")
return 0
if __name__ == "__main__":
sys.exit(main()) |
|
I'm noticing that many of the fortran files had windows I experimented with modifying |
…d on CLI ignore build artifacts
01f5350 to
7028d1a
Compare
| CHARACTER(LEN=FVS_BUILDINFO_BUFLEN) :: tmp | ||
| INTEGER :: i | ||
|
|
||
| tmp = 'Version: ' // FVSVER_VERSION // CHAR(10) // & |
There was a problem hiding this comment.
note to self, test if CHAR(10) (line feed) used here will be portable to windows
wagnerds
left a comment
There was a problem hiding this comment.
Manual review looks good. Will build and test. Thanks!
|
@wagnerds I do have some changes drafted to |
|
@d-diaz We pretty much just use the makefile but there may be some users out there that do use the CMakeLists if you would like to submit them. Thanks. |
|
Ok. Got the |
Summary
Adds build-time git-derived version metadata for FVS binaries built via
bin/makefile, exposed on the CLI and in run output headers. This PR wires version metadata to the source repo at build time so binaries self-describe how they were built. Fixes #58.FVSVERSIONmodule (base/version.f) with compile-time constants injected via-Dflags from git atmaketimeFVS<variant> --versionand--build-infowill exit before initializing a simulation (no keyfile required)FVSVER_VERSIONinstead of hardcodedINCLUDESVN.F77/ per-variantREVISEdatescommon/INCLUDESVN.F77(version was manually edited each release and may not reflect the actual build date or version tag)Changes
Build (
bin/makefile)At
makeinvocation, git metadata is collected from the repo root (FVS_ROOT, so it works when building from*_buildDircopies):git describe --tags(exact tag on HEAD, else nearest tag)originremote URLYYYYMMDD)originURLIf there are uncommitted changes in the repo,
-dirtyis appended to version and commit hash and makefile emits warnings.Version-related values are passed to the compiler as
-DFVS_GIT_*flags and compiled intoMODULE FVSVERSION.version.fandmain.fare compiled with-ffree-form(free-form Fortran required for the module and CLI handling). It would be possible to implement this logic in these files in legacy fixed-form format if that is preferable. That would avoid the need for the-ffree-formflag in the compile scripts, but would defer to y'all as to whether that's a substantial concern or potential point of confusion going forward. Considering there is interest among FVS team to migrate to more modern Fortran, this seems to fit that approach.Run headers and revision dates
grohed.ffiles (US + Canada):USE FVSVERSION;SVN = FVSVER_VERSION(widened to*32)vbase/revise.f,canada/bc/revise.f,canada/on/revise.f:REV = FVSVER_DATE(replaces per-variantSELECT CASEhardcoded dates)dbs/dbscase.f,dbsqlite/dbscase.f:SVN = FVSVER_VERSIONSource lists
../base/version.fadded to all 24bin/FVS*_sourceList.txtfiles (beforemain.f).INCLUDESVN.F77removed from all lists.Other
.gitignore: local build artifacts (bin/FVS??,*_buildDir, shared libs)Out of scope (this PR)
CMakeLists.txtchanges - this file is left unmodified, so builds usingcmakedo not have any of the new version flags being gathered or injected. Let me know if this is a problem I should address to update thiscmakeworkflow to mirror themakefileapproach.makefile_Xbuildchanges.Fortranbuffer pattern)CLI behavior (
base/main.f)On my fork, I did:
Test plan
git tag FSTEST),cd bin && make clean && make FVSbm./FVSbm --version— shows tag and org./FVSbm --build-info— shows full provenance block-dirty, makefile warnings appearFVSbcorFVSonto exercise Canadarevise.fpaths