Skip to content

Add build-time git version metadata and CLI --version / --build-info#59

Open
d-diaz wants to merge 3 commits into
USDAForestService:developmentfrom
d-diaz:feat/version-info
Open

Add build-time git version metadata and CLI --version / --build-info#59
d-diaz wants to merge 3 commits into
USDAForestService:developmentfrom
d-diaz:feat/version-info

Conversation

@d-diaz

@d-diaz d-diaz commented Jun 14, 2026

Copy link
Copy Markdown

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.

  • New FVSVERSION module (base/version.f) with compile-time constants injected via -D flags from git at make time
  • CLI calls to FVS<variant> --version and --build-info will exit before initializing a simulation (no keyfile required)
  • Run headers and DB case records use the same version string via FVSVER_VERSION instead of hardcoded INCLUDESVN.F77 / per-variant REVISE dates
  • Removes common/INCLUDESVN.F77 (version was manually edited each release and may not reflect the actual build date or version tag)

Changes

Build (bin/makefile)

At make invocation, git metadata is collected from the repo root (FVS_ROOT, so it works when building from *_buildDir copies):

  • Version — git describe --tags (exact tag on HEAD, else nearest tag)
  • Org — parsed from origin remote URL
  • Commit — short SHA
  • Date — commit date (YYYYMMDD)
  • Branch — current branch
  • Remote — origin URL

If there are uncommitted changes in the repo, -dirty is appended to version and commit hash and makefile emits warnings.

Version-related values are passed to the compiler as -DFVS_GIT_* flags and compiled into MODULE FVSVERSION.

version.f and main.f are 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-form flag 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

  • All variant grohed.f files (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-variant SELECT CASE hardcoded dates)
  • dbs/dbscase.f, dbsqlite/dbscase.f: SVN = FVSVER_VERSION

Source lists

../base/version.f added to all 24 bin/FVS*_sourceList.txt files (before main.f). INCLUDESVN.F77 removed from all lists.

Other

  • .gitignore: local build artifacts (bin/FVS??, *_buildDir, shared libs)

Out of scope (this PR)

  • CMakeLists.txt changes - this file is left unmodified, so builds using cmake do 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 this cmake workflow to mirror the makefile approach.
  • makefile_Xbuild changes
  • rFVS / shared-library buffer wrappers for version strings (module subs with allocatable outputs are sufficient for CLI; rFVS can follow the existing .Fortran buffer pattern)

CLI behavior (base/main.f)

On my fork, I did:

$ git tag DDTest
$ git push origin feat/version-info
$ cd bin
$ make FVSbm

$ ./FVSbm --version
DDTest (d-diaz)

$ ./FVSbm --build-info
Version: DDTest
Org:     d-diaz
Remote:  https://github.com/d-diaz/ForestVegetationSimulator.git
Branch:  feat/version-info
Commit:  3049857f
Date:    20260613

Test plan

  • Clean tree, tag HEAD (e.g. git tag FSTEST), cd bin && make clean && make FVSbm
  • ./FVSbm --version — shows tag and org
  • ./FVSbm --build-info — shows full provenance block
  • Run a short keyword file; confirm run header shows matching version string
  • Dirty tree build (create a new, uncommited file in the repo then build) — version/commit show -dirty, makefile warnings appear
  • Optional: build FVSbc or FVSon to exercise Canada revise.f paths

@d-diaz

d-diaz commented Jun 14, 2026

Copy link
Copy Markdown
Author

The output header line generated by grohed.f still calls revise.f to get the latest version’s date. That variable is now already available from the USE FVSVERSION statement so we could drop the call to revise.f and remove that file entirely. There are several other call sites for the REVISE subroutine, so opted to retain them as-is to try and limit the ripple effects from this initial PR (if desired those changes would probably make better sense for a subsequent PR rather than expanding this one).

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:

FOREST VEGETATION SIMULATOR   VERSION <SVN> -- ACAIDAIN   RV:<REV>   <DAT>  <TIM>

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:

FOREST VEGETATION SIMULATOR (<FVS_GIT_ORG>) -- ACADIAN VARIANT --  VERSION <FVS_GIT_VERSION> RV:<FVS_GIT_DATE>  -- <DAT>  <TIM>

@d-diaz

d-diaz commented Jun 14, 2026

Copy link
Copy Markdown
Author

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 bin folder, after a a build you can then do:
python3 test_fvs_version_ctypes.py ./FVSbm.so. This was written to work on Linux or WSL. Let me know if you need a windows-native version.

"""
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())

@d-diaz

d-diaz commented Jun 15, 2026

Copy link
Copy Markdown
Author

I'm noticing that many of the fortran files had windows CRLF line endings, that have now been changed to Unix LF pattern. This shows up with many of the fortran file diffs looking like all lines have been changed.

I experimented with modifying .gitattributes so that fortran files would retrain CRLF line endings, but doing so and renormalizing the files in the repository led to a lot of files being changed from LF line endings. So it seems like the current repo has a mix of both line endings in the fortran files, which would only be an issue for compiling using ifort/ifx. So long as we're sticking with gfortran for compilation, I don't think this is a problem, and didn't want to make any more changes to line-endings to files not modified in this scope for adding version functionality.

@d-diaz d-diaz force-pushed the feat/version-info branch from 01f5350 to 7028d1a Compare June 15, 2026 16:52
Comment thread base/version.f
CHARACTER(LEN=FVS_BUILDINFO_BUFLEN) :: tmp
INTEGER :: i

tmp = 'Version: ' // FVSVER_VERSION // CHAR(10) // &

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self, test if CHAR(10) (line feed) used here will be portable to windows

@wagnerds wagnerds left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual review looks good. Will build and test. Thanks!

@d-diaz

d-diaz commented Jun 15, 2026

Copy link
Copy Markdown
Author

@wagnerds I do have some changes drafted to CMakeLists.txt that produce similar behavior if you'd like those included. Just not sure what y'all's standard compile workflow is for releasing. It seems like the bin/BuildAll*.bat scripts are obsolete at this point too.

@wagnerds

Copy link
Copy Markdown
Collaborator

@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.

@d-diaz

d-diaz commented Jun 15, 2026

Copy link
Copy Markdown
Author

Ok. Got the CMakeLists.txt update and removed the legacy *.bat build scripts and the makefile_Xbuild. Figured we could revisit more platform support in a subsequent extension to these scripts and get rid of the versions that have diverged from current repo contents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants