Dealing with version numbers in Fortran

Hi all,

I’m interested in knowing other people’s approach to versioning software in Fortran. Specifically, how to tell the Fortran code itself what version it is (rather than how to release versions in GitHub).

I have a model, and currently I tell the model what version it is through a MODEL_VERSION variable in a separate module. To avoid having to update this manually each time I create a new version, I get my build script to run git describe --tags and rewrite the MODEL_VERSION variable with the output. For example, in CMake I use regex to rewrite the VersionModule.f90 file.

This is a bit clunky (and requires the user to have Git installed - not ideal), but it’s worked for a while now. However, I’m now building the model with fpm and, as far as I can tell, fpm doesn’t (yet?) have support for custom build scripts, so I can’t easily take the same approach.

I was going to ask a question along the lines of “how do I get fpm to rewrite my MODEL_VERSION variable”, but I thought a broader question about different approaches to versioning Fortran code would be more useful. To my mind, the obvious options are:

  • Don’t tell the code what version it is (not good for reproducibility)
  • Manually update the a version variable (MODEL_VERSION in my case) each time I create a new release.
  • My approach: Write a script to update the version number each time the code is built. How could I do this in fpm?

Thanks!

2 Likes

There is a project

In general I track such projects in Fortran Tools.

3 Likes

I would usually use github Release for version control and include a .rc file to my project to embed the build version.
In other words I use a script that:

  1. Retrieves the latest version on the github repo
function Get-LastTag {
	param(
		[parameter(Mandatory=$true, Position=0)][string] $repositoryName,
		[parameter(Mandatory=$true)][string] $gitHubOrganization,
		[parameter(Mandatory=$true)][string] $gitHubApiKey
    )
	
	process {
		
		try {
			$releasesUri = "https://api.github.com/repos/$gitHubOrganization/$repositoryName/releases";
			[hashtable] $gitHubHead = @{ Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($gitHubApiKey + ":x-oauth-basic")); }
			$lastTag = (Invoke-RestMethod -Method GET -Uri $releasesUri -Headers $gitHubHead)[0].tag_name 
		}
		catch {
			$lastTag = "0.0.0.0"
		}

		if($lastTag -notmatch "^\d*\.\d*\.\d*\.\d*$") { 
			throw "Tag not well formed : $($lastTag)" 
		}
		return $lastTag
	}
}
  1. Increment the version number. I use major.minor.sprint.revision
  2. Update the .rc file
  3. Compile and make the pull request to publish the new release with the updated version number.
function Set-NewTag {
	param(
		[parameter(Mandatory=$true)][string] $repositoryName,
		[parameter(Mandatory=$true)][string] $gitHubOrganization,
		[parameter(Mandatory=$true)][string] $gitHubApiKey,
		[parameter(Mandatory=$true)][string] $targetBranch,
		[parameter(Mandatory=$true)][string] $tagVersion,
		[parameter(Mandatory=$true)][string] $releaseNotes,
		[parameter(Mandatory=$true)][bool] $isPrerelease 
    )
	
	process {

		If($releaseNotes = "") {$releaseNotes = " "}
		
		try {
			$releaseData = @{
						tag_name = $tagVersion;
						target_commitish = $targetBranch;
						name = $tagVersion;
						body = $releaseNotes;
						draft = $false;
						prerelease = $isPrerelease;
					}
					
			$releaseParams = @{
				Uri = "https://api.github.com/repos/$gitHubOrganization/$repositoryName/releases";
				Method = "POST";
				Headers = @{ Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($gitHubApiKey + ":x-oauth-basic")); };
				ContentType = "application/json";
				Body = (ConvertTo-Json $releaseData -Compress)
			}

			Invoke-RestMethod @releaseParams 
		
		} catch {
			throw "Error during Set-NewTag - Exception : $($_.Exception.Message)"
		}
    }
}

That works fine on Windows, but .rc file are not mandatory here. The version can also be hard coded in any source file I guess.
Scripts are in powershell but despite the ugly syntax they should be pretty easy to follow.

1 Like

Thanks both, they’re interesting approaches.

I’ve just come up with a workaround for fpm. I was already using response files to set build options, and I have just learnt that you can include system commands, so as a simple example, I can write a response file like the following (this would be to install the model):

system git describe --tags > VERSION
options install
options --profile release

Then I can run fpm @install to run this. This writes the output of git describe --tags to the file VERSION. The model would need to read this file, but that’s easily achieved, though as it would be read at runtime, it requires the file to be always locatable by the model (hmm…). Or I could modify the response file script to write directly to the VersionModule.f90 file, but that might raise more cross-platform compatibility issues.

Here is a shell script that I use to put various compile time environment strings info into my codes.

#!/bin/sh

# shell script to create a *.F90 file with embedded compile-time-environment information.

# output is written to stdout.
# usage:  cte_mod.sh > cte_mod.F90

XgitX=`git log -1 --format=format:"%H"`   # git hash for the current commit.
XdateX=`date`
XhostX=`hostname -s`
XunameX=`uname -sr`    # -a is over 132 characters, so keep it short.
XuserX=`whoami`
XcolverX=`cat $COLUMBUS/doc/version`

cat <<EOF | sed \
-e s/XGITX/"$XgitX"/ \
-e s/XDATEX/"$XdateX"/ \
-e s/XHOSTX/"$XhostX"/ \
-e s/XUNAMEX/"$XunameX"/ \
-e s/XUSERX/"$XuserX"/ \
-e s/XCOLVERX/"$XcolverX"/
module cte_mod

   ! this module defines some compile time environment (cte) parameters.

   use, intrinsic :: iso_fortran_env, only: compiler_version, compiler_options

   implicit none

   ! the following constants are defined by the script cte_mod.sh.

   character(len=*), parameter :: cte_git    = 'XGITX'    ! hash for the current git commit.
   character(len=*), parameter :: cte_date   = 'XDATEX'   ! compile date and time.
   character(len=*), parameter :: cte_host   = 'XHOSTX'   ! compile time hostname.
   character(len=*), parameter :: cte_uname  = 'XUNAMEX'  ! OS info at compile time.
   character(len=*), parameter :: cte_user   = 'XUSERX'   ! user name who compiled the code.
   character(len=*), parameter :: cte_colver = 'XCOLVERX' ! COLUMBUS version.
#ifdef __INTEL_COMPILER
   character(len=*), parameter :: cte_cver   = 'ifort unknown version'
   character(len=*), parameter :: cte_copt   = 'ifort options unavailable'
#else
   character(len=*), parameter :: cte_cver   = compiler_version()
   character(len=*), parameter :: cte_copt   = compiler_options()
#endif

contains
   subroutine cte_print( nlist )
      ! print out the entire list of cte* parameters to unit nlist.
      implicit none
      integer, intent(in) :: nlist
      character(len=*), parameter :: cfmt='(2a)'

      write(nlist,cfmt) 'cte_git    = ', cte_git
      write(nlist,cfmt) 'cte_date   = ', cte_date
      write(nlist,cfmt) 'cte_host   = ', cte_host
      write(nlist,cfmt) 'cte_uname  = ', cte_uname
      write(nlist,cfmt) 'cte_user   = ', cte_user
      write(nlist,cfmt) 'cte_colver = ', cte_colver
      write(nlist,cfmt) 'cte_cver   = ', cte_cver
      write(nlist,cfmt) 'cte_copt   = ', cte_copt

      return
   end subroutine cte_print
end module cte_mod

#ifdef EXE
! compile with -DEXE to create a stand-alone test program.
program cte_test
   use, intrinsic :: iso_fortran_env, only: output_unit
   use cte_mod
   implicit none
   call cte_print(output_unit)
end program cte_test
#endif
EOF

This script now assumes a git repository, but previous versions have used svn, cvs, etc. I also notice that this version sidesteps the ifort bug for the intrinsics compiler_version() and compiler_options(). I have not tested lately, maybe those are fixed now in newer ifort/ifx.

On unix/posix machines, if the script is called cte_mod.sh, then it is invoked as

cte_mod.sh > cte_mod.F90

The output is a fortran file that can be compiled and linked with your code. There is an embedded main program that can be activated with the preprocessor macro -DEXE that you can use to test the module; this creates a one-file stand-alone program.

This is part of a larger program system, so you should change the line that defines the XcolverX string to something else:

XcolverX='Insert String Here'

I normally use make to build the codes that use this module, so within Makefile I define cte_mod.o as a dependency, and cte_mod.F90 as a .PHONY target. This forces a new cte_mod.F90 to be created and compiled every time a code is built, so the info is never out of date or inconsistent.

2 Likes

Thanks, that’s a nice approach and kind of the way I’ve been going (though in a much simpler way).

I need Windows support too and so I can’t really get away without having to create a separate batch script or similar for this. I did try using the echo command in a response file (as per above) to write a .f90 file, but it turns out there’s enough differences between the command on Linux and Windows to make this approach difficult (e.g. different escape characters for new lines…). Maybe I should succumb to having different build scripts for different platforms.

1 Like

Bash scripts, such as @RonShepard’s, can run on Windows too! If you have git installed, then you can run bash scripts within Git Bash on Windows.

I haven’t dealt with the version number problem, but I do use a single shared bash script as a universal build script which runs on Windows, Linux, and macOS. You can even use if/else logic within the script to perform slightly different actions on different OS’s, e.g. setting a different file path, or using a different CMake generator. I have an example build script here: syntran/build.sh at main · JeffIrwin/syntran · GitHub

Another approach would be to use python as a universal build script language. I prefer bash because python is a bit of a heavy dependency just for a build script, and it’s not always easy to install.

2 Likes

Yeah that’s a good point, though I was trying to avoid the end user from needing Git Bash (or similar) installed. Ideally they could do everything in PowerShell/cmd.

This is definitely something I’m thinking about. We already have a few Python scripts that are used for compiling data for the model, so it wouldn’t be too much of a leap to require Python for the build too. Something to ponder on!

1 Like

You can also use the de facto standard preprocessor to substitute strings within the fortran source code, but for characters, you have to be careful with the quotes. Here is an example.

program xxx
   character(*), parameter :: version = "&
        &VERSION"
   write(*,'(a,1x,i0)') version, len(version)
end program xxx

$ gfortran -DVERSION=1.23.4 version.F90 && a.out
1.23.4 6

If you put the leading quote on the same line as the macro (version="VERSION"), then that blocks the preprocessor substitution. Also, this is kind of an edge case for the preprocessor, so I don’t know how portable this particular behavior is in general. That is why I ended up using sed to do the substitutions in my codes.

1 Like

I haven’t used fpm yet, but I have been using this shell code

set date=`date +%Y%m%d`
cat *.for | m4 -P -Dcompdate=$date >taxsim.f

to put the compilation date in the source. I have a write statement:

write(*,*) compdate 0d-1

so that I can compile with or without a non-empty compdate macro variable. It compiles under linux, freebsd, osx and windows for me with gfortran) , and no user has reported difficulties compiling on any other host.

If you posted the problematic echo command, I wonder if someone here wouldn’t offer a solution to getting that to work on multiple prlatforms.

2 Likes