Skip to content

Instantly share code, notes, and snippets.

@SomajitDey
Last active December 25, 2022 23:21
Show Gist options
  • Save SomajitDey/4462675881cc1340b76d45279764cc2f to your computer and use it in GitHub Desktop.
Save SomajitDey/4462675881cc1340b76d45279764cc2f to your computer and use it in GitHub Desktop.
Sample Makefile for a Modern Fortran project

Fortran Build Automation

The accompanying Makefile may be reused for any Modern Fortran project conforming to the following:

  • All source files (.f90) are inside /src.
  • All program names are prefixed with the package name followed by an underscore. E.g. if package is ccd, all program names start with ccd_, e.g.
program ccd_run
  ! code
end program ccd_run
  • Each program file has the same name as the program it contains. E.g. if program name is ccd_run, the host file is named ccd_run.f90
  • Module file name: mod_<module name>.f90. E.g. name of the module file that contains module files should be mod_files.f90
  • All executable scripts are inside /scripts, their names prefixed with <package>_, e.g. ccd_cpt_to_xy.
  • The package is accessed using a single driver script <package> with command line:
<package> [<global options>] [<subcommand> [[<options>] <args>]]

The driver calls other scripts and executables of the form: <package>_<subcommand> with the user-provided command line arguments. See the accompanying driver.template script for illustration.

  • Note that you don't have to tinker with the attached driver.template script. Makefile uses it as template and gives you an appropriate driver script named <package> which also reports the build version.
  • The Makefile also generates a Bash-completion script for your <subcommand>s and installs it at the appropriate system path.
  • Executables and scripts whose names end with an underscore or a file extension do not appear in the completion script, i.e. are effectively hidden from the user. This helps <package>_<subcommand> use <package>_<utility>_ and <package>_<utility>.sh without <utility> or <utility>.sh appearing in the subcommand list.

To use the provided Makefile, you only need to set the PACKAGE variables to your package name.

Possible Enhancement

Include a .PHONY rule called lint:

lint: 
  fortran-linter --linelength $(LINELENGTH) --indent-size $(INDENT_SIZE) --inplace --verbose

Provide an order-only prerequisite telling the user to install fortran-linter first.

# Brief: This is the bash-completion script for command: __package__
## It completes only the 1st argument non-trivially. Other arguments are completed as usual by GNU Readline
## Words enclosed with __ are placeholders
package='__package__'
_complete(){
local sub_cmds='__subcmds__'
[[ "${COMP_CWORD}" == 1 ]] || return 1
COMPREPLY=( $(compgen -W "${sub_cmds}" "${2}") )
}
complete -F _complete -o default "${package}"
#!/usr/bin/env bash
# Brief: Driver script for <package>
# Usage: <package> [<global options>] [<subcommand> [[<options>] <args>]]
# Specs:
# When user wants to run <package> she usually provides subcommands [with options and arguments] to this script
# This script then invokes the executable <package>_<subcommand> (found in PATH) with the user-provided opts and args
# This script also loads <package>_config.sh on startup and <package>_exit.sh before exit.
# Processing of the global options is done by the config script. This driver doesn't "see" those options.
# Providing no subcommand executes the executable: <package>_
whereami(){
# Returns the absolute path of the directory this script is in
case "${BASH_SOURCE}" in
/*)
echo -n "${BASH_SOURCE%/*}" ;;
*/*)
echo -n "${PWD}/${BASH_SOURCE%/*}" ;;
*)
echo -n "${PWD}";;
esac
}
this_script_is_at="$(whereami)"
invocation="${0}"
package="${invocation##*/}"
export PATH="${this_script_is_at}/${package}_:${PATH}"
# Loading config script, if any
command -v ${package}_config.sh &>/dev/null && source ${package}_config.sh
subcmd="${1}"
shift
if [[ -z "${subcmd}" ]]; then
${package}_ || exit $?
else
${package}_${subcmd} "${@}" || exit $?
fi
# Loading exit script, if any
command -v ${package}_exit.sh &>/dev/null && source ${package}_exit.sh
# Disable all of make's built-in rules (similar to Fortran's implicit none)
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Fortran Compiler
FC := gfortran
# The following must have non-empty value if OpenMP is required
OMP :=
# The following must have non-empty value if Debugging compiler options are required
DEBUG :=
# Dependency Generator
DEPGEN := fortdepend
DEPGEN_INSTALL_DOCS := [https://github.com/ZedThree/fort_depend.py]
# Compiler Flags
ifeq ($(FC), gfortran)
ifdef DEBUG
FF += -march=native -O0 -fautomatic -static-libgfortran
FF += -Wall -Warray-temporaries -Wextra -pedantic
FF += -g -fbacktrace -fcheck=all -ffpe-trap=invalid,zero,overflow -finit-real=snan
else
FF += -march=native -O3 -static-libgfortran -funroll-loops -fautomatic -w
endif
ifdef OMP
FF += -fopenmp
endif
else ifeq ($(FC), ifort)
ifdef DEBUG
FF += -march=native -static -O0 -auto -fp-stack-check -g -traceback -warn -check all
else
FF += -march=native -O3 -fast -auto -w
endif
ifdef OMP
FF += -qopenmp
endif
endif
# Linker and linker Flags
LD := $(FC)
ifeq ($(LD), gfortran)
LF += -fopenmp
else ifeq ($(LD), ifort)
LF += -qopenmp
endif
# Package name
PACKAGE := ccd
# List of all source files
SRC_DIR := src
SRCS := $(notdir $(wildcard $(SRC_DIR)/*.f90))
# List of all object files
BUILD_DIR := build
OBJS := $(addprefix $(BUILD_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
# Include path (to be searched by compiler for *.mod files)
IP := $(BUILD_DIR)
ifeq ($(FC), gfortran)
FF += -J $(IP)
else ifeq ($(FC), ifort)
FF += -module $(IP)
endif
# Target executable(s)
EXECS := $(basename $(filter $(PACKAGE)_%, $(SRCS)))
EXECS := $(addprefix $(BUILD_DIR)/, $(EXECS))
# List of all executable scripts
SCRIPT_DIR := scripts
SCRIPTS := $(wildcard $(SCRIPT_DIR)/$(PACKAGE)_*)
# Path to the DRIVER script that represents the entire package
DRIVER_TEMPLATE := $(SCRIPT_DIR)/driver.template
DRIVER := $(SCRIPT_DIR)/$(PACKAGE)
# Bash Completion script
SUBCMDS := $(filter-out %_ %.sh, $(patsubst $(PACKAGE)_%, %, $(notdir $(EXECS) $(SCRIPTS))))
BASHCOMP := $(PACKAGE)_completion.sh
BASHCOMP_TEMPLATE := $(SCRIPT_DIR)/completion.template
# Dependency file to be generated using `fortdepend`
DEPFILE := .dependencies
# Intrinsic modules in standard Fortran for `fortdepend` to ignore
IMODS := omp_lib omp_lib_kinds iso_fortran_env ieee_arithmetic ieee_exceptions ieee_features iso_c_binding
# Font colors to be used by `echo`
RED='\e[1;31m'
GREEN='\e[1;32m'
BLUE='\e[1;34m'
NOCOLOR='\e[0m'
# Where to seek prerequisites
VPATH := $(SRC_DIR)
# System path where executables would be installed
# The following must be a system path that exists. `Main` basically means the `Driver`.
INSTALL_PATH_MAIN := /usr/local/bin
# The following must be a custom directory that doesn't exist by default such that it's existence implies previous installation
INSTALL_PATH_INTERNALS := $(INSTALL_PATH_MAIN)/$(PACKAGE)_
BASHCOMP_INSTALL_PATH := /etc/bash_completion.d
# Shell which runs the recipes
SHELL := bash
.PHONY: all clean rebuild install uninstall $(DEPGEN)
all: $(EXECS) $(BASHCOMP) $(DRIVER)
@echo -e \\n$(GREEN)"make: Success"$(NOCOLOR)
$(EXECS): % : %.o $(filter-out $(BUILD_DIR)/$(PACKAGE)_%.o, $(OBJS))
$(LD) $(LF) -o $@ $^
@echo -e $(BLUE)"make: Built $@"$(NOCOLOR)
$(OBJS): $(BUILD_DIR)/%.o : %.f90
$(FC) -c $(FF) -o $@ $<
# Rebuild all object files when this Makefile or dependency changes
# Create build directory only if non-existent (implemented as "order-only prerequisite")
$(OBJS): $(MAKEFILE_LIST) $(DEPFILE) | $(BUILD_DIR)
$(BUILD_DIR):
mkdir $@
# Generate fresh dependency file whenever the codebase (sources) is modified or this Makefile changes
$(DEPFILE): $(SRCS) $(MAKEFILE_LIST) | $(DEPGEN)
@echo -e $(BLUE)"make: Generating dependencies:"$(NOCOLOR)
$(DEPGEN) --files $(addprefix $(SRC_DIR)/,$(SRCS)) --build $(BUILD_DIR) --ignore-modules $(IMODS) --output $(DEPFILE) --overwrite
# Define dependencies between object files
# Note: In fortran, object files are interdependent through .mod files
# Note: .mod file is generated only when module code is compiled into object file
include $(DEPFILE)
$(DEPGEN):
@which $@ > /dev/null || { echo -e $(RED)"make: Please install $@ first. $(DEPGEN_INSTALL_DOCS)"$(NOCOLOR) && false;}
$(BASHCOMP): $(EXECS) $(SCRIPTS)
@echo -e $(BLUE)"make: Creating Bash completion script: $(BASHCOMP)"$(NOCOLOR)
sed -n -e 's/__package__/$(PACKAGE)/g' $(BASHCOMP_TEMPLATE) -e 's/__subcmds__/$(SUBCMDS)/g;p' > $(BASHCOMP)
$(DRIVER): $(EXECS) $(SCRIPTS)
@echo -e $(BLUE)"make: Creating driver script with version info: $(DRIVER)"$(NOCOLOR)
@cat $(DRIVER_TEMPLATE) <(echo "echo '$(PACKAGE) Build Version: $$(sha1sum $(EXECS) $(SCRIPTS) | awk NF=1 | sha1sum | awk NF=1)'") \
> $(DRIVER)
chmod +x $(DRIVER)
clean:
rm -rf $(BUILD_DIR)
rm -f $(DEPFILE) $(BASHCOMP) $(DRIVER)
rebuild: clean all
install:
@sudo mkdir $(INSTALL_PATH_INTERNALS) || { echo -e $(RED)"make: Uninstall earlier installation first"$(NOCOLOR) && false;}
sudo install -t $(INSTALL_PATH_INTERNALS) $(EXECS) $(SCRIPTS)
sudo install -T $(DRIVER) $(INSTALL_PATH_MAIN)/$(PACKAGE)
sudo install -t $(BASHCOMP_INSTALL_PATH) $(BASHCOMP)
uninstall:
sudo rm $(INSTALL_PATH_MAIN)/$(PACKAGE)
sudo rm -rf $(INSTALL_PATH_INTERNALS)
sudo rm $(BASHCOMP_INSTALL_PATH)/$(BASHCOMP)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment