Developing Galacticus¶
The Galacticus Build System¶
Galacticus uses a GNU Make-based build system. Dependencies are automatically discovered, and extensive meta-programming and preprocessing of source files is undertaken as part of the build. As a result, the build system is complicated. This section describes it in detail: the Makefile and its configuration knobs, the incremental-build machinery, the special comment markers that the build scripts read from source files, the automatic-discovery and code-generation steps, and the files everything produces.
A build proceeds, conceptually, in four phases:
Cataloging: the source tree is scanned for directives, module declarations,
usestatements,includestatements, andprogramunits, producing the generated makefiles and XML catalogs under$(BUILDPATH)that drive the rest of the build (see Automatic Discovery).Preprocessing: each Fortran source file is run through the source-tree preprocessor (see Section Galacticus Preprocessor Directives), which expands directives into generated code, producing
*.p.F90files.Compilation: preprocessed sources are compiled, with compiler diagnostics remapped back to original source line numbers.
Linking: for each executable, the set of required objects is read from its
.dfile, input-parameter and source-digest metadata objects are generated and compiled, and everything is linked with the required external libraries in the correct order.
The Makefile¶
Files generated during the build (with the exception of final executables) are written to the directory specified by the BUILDPATH variable. This is normally work/build/ (and is set by the Makefile) unless a special build configuration is requested via GALACTICUS_BUILD_OPTION:
defaultStandard build (
BUILDPATH=./work/build).MPIMPI-enabled build (
BUILDPATH=./work/buildMPI); compilers default tompif90/mpicc/mpic++(overridable viaMPIFCCOMPILERetc.) and-DUSEMPIis defined.libShared-library build of
libgalacticus.so(BUILDPATH=./work/buildLib); adds-fPICand heap trampolines, and activates the library-interface generators (see Section The Shared-Library Interface).gprof,perf,odeprofProfiling variants (
-pg,-fno-omit-frame-pointer, and-DPROFILErespectively), each with its own build directory and executableSUFFIX.compileprofBuild-profiling mode: every recipe is run through the
profiler.shwrapper SHELL to collect timing and memory data (see Section Profiling the Build).
Configuration knobs¶
The following make variables (settable on the command line or from the environment) control the build:
FCCOMPILER,CCOMPILER,CPPCOMPILERThe Fortran, C, and C++ compilers (defaults:
gfortran,gcc,g++; MPI builds default to the MPI wrappers, overridable viaMPIFCCOMPILERetc.).GALACTICUS_FCFLAGS,GALACTICUS_CFLAGS,GALACTICUS_CPPFLAGS,GALACTICUS_F77FLAGSExtra per-language flags appended to the built-in flag sets — typically
-I/-Lpaths for locally-installed dependencies. Note that the availability probes for ANN, qhull, and libmatheval compile against onlyGALACTICUS_CPPFLAGS(see Feature-availability probes).LTOenabled(default) ordisabled. Link-time optimization uses-flto=jobserverso that the parallelism of the link-time recompilation is governed by make’s job server rather than the machine’s CPU count; disable on platforms where LTO causes problems (e.g.dsymutilmemory exhaustion on Apple Silicon).LOCKMD5yes(default) orno. Whether the source-digest computation takesflocklocks on its per-file.md5sidecar caches. Locks are only actually used under parallel make (-j); they serialize concurrent builds sharing a build directory (e.g. several executables linking at once, each runningsourceDigests.py). The Makefile forcesnowhen only a single target was named on the command line.OFDLOCKSenabled(default) ordisabled: whether to use open-file-description locks (-DOFDLOCKSvs.-DNOOFDLOCKS) for run-time file locking.USEGIT2Set to
noto force the build to not uselibgit2(falling back to thegitcommand line) even when it is available. Static builds force this automatically.GALACTICUS_OBJECTS_DEBUGSet to
yesto define-DOBJECTDEBUG, enabling object life-cycle debugging output.SUFFIXAppended to executable names (set automatically by the profiling build options, e.g.
Galacticus.exe_gprof).PREPROCESSORName of the C preprocessor, recorded in the build metadata (the
.p.Inc→.incrule currently invokescppdirectly).
Feature-availability probes¶
Optional dependencies are detected by compile probes whose results are written to $(BUILDPATH)/Makefile_Config_<Name> fragments (included into the Makefile) that append -D<SYMBOL>AVAIL or -D<SYMBOL>UNAVAIL to the relevant flags variables. The identically-shaped probes (OFD file locking, FFTW3, ANN, qhull, libmatheval) share the CONFIG_PROBE canned recipe in the Makefile — adding a new probe is a -include line, a rule, and one $(call CONFIG_PROBE,...). The Proc probe (which must also run its compiled binary) and the libgit2 probe (which has static-build and opt-out variants) are hand-written.
Two caveats a maintainer should know:
The C/C++ probes for ANN, qhull, and libmatheval compile against only
GALACTICUS_CPPFLAGS, not the fullCPPFLAGS. The latter adds-I$(BUILDPATH)/, which can contain zero-byte header stubs left by an earlier unavailable-result build (created by the Makefile’s generic%.hrule for headers that were preprocessed out). Such a stub satisfies the probe’s#includeand flips the probe to a spurious available result on a machine without the real headers — after which the real compile fails. The libgit2 probe hit this trap first and carries the original explanation.Probe results are cached in the
Makefile_Config_*fragments and only regenerated when the probe source changes — not when the machine’s installed libraries change. After installing or removing an optional dependency, delete the corresponding$(BUILDPATH)/Makefile_Config_*file (or the whole build directory) to re-probe.
Executable linking¶
There is no generic %.exe pattern rule: findExecutables.py discovers every program in the source tree and writes an explicit rule for each into $(BUILDPATH)/Makefile_All_Execs. Those generated rules contain only a dependency line and a $(call LINK_EXECUTABLE,...); the link procedure itself lives in two canned recipes in the main Makefile, so it has a single owner:
LINK_METADATAEnumerates every input parameter the target can accept (
parameterDependencies.py, compiled into<stem>.parameters.o) and computes per-type source digests (sourceDigests.py, compiled into<stem>.md5s.o). Shared by executables andlibgalacticus.so.LINK_EXECUTABLELINK_METADATAplus the link step, which reads the object list from the target’s.dfile, appends the library flags computed bylibraryDependencies.py, and captures the linker’s exit status so that a failed link fails the recipe (see Build Status Propagation).
Incremental-Build Machinery¶
Because so much of the build is generated, naive use of file timestamps would cause huge rebuild cascades: regenerating a file whose content did not change would still advance its mtime and invalidate everything downstream. The build system uses three cooperating mechanisms to keep incremental builds minimal. All three follow the same principle — a generated file’s mtime advances only when its content actually changes — so understanding them is essential before modifying any generator.
Update-sentinel (“.up”) files¶
For a generated file X whose generator should re-run when its inputs change, but whose consumers should re-run only when X’s content changes, the build uses a sentinel pair:
$(BUILDPATH)/X.up : <inputs>
<generator writes X only-if-changed, then touches X.up>
$(BUILDPATH)/X : $(BUILDPATH)/X.up
@true
Make compares the inputs against the sentinel X.up (touched on every generator run), while downstream targets compare against X itself (mtime preserved when content is unchanged). The only-if-changed write plus sentinel touch is implemented by Galacticus.Build.FileChanges.update(..., prove_update=True): the generator writes to a temporary file, which either replaces X (if different) or is discarded (if identical). This pattern is used for preprocessed sources (*.p.F90), generated include files (*.Inc), and the OpenMP critical-section catalog, among others.
Per-file cache blobs¶
The cataloging scripts each keep a pickle cache (“blob”) under $(BUILDPATH) recording per-source-file scan results, so a re-run rescans only changed files. The shared protocol (implemented in Galacticus.Build.ScanCache and documented in its docstring) is:
Each file’s entry is keyed by a canonical identifier of its path, and records the list of files whose content contributed to the entry (the file itself plus any followed includes or directive-referenced files).
The blob’s own mtime is the freshness reference: any tracked file with mtime newer than the blob forces a rescan of that entry. Adding or removing source files forces a full rescan.
A missing, corrupt, or wrong-format blob silently degrades to a full rescan — cache problems can never fail the build. Scripts whose recorded entry structure changes bump an embedded version stamp so old-format caches are discarded.
Blobs are themselves rewritten only-if-changed (preserving the blob mtime when nothing changed), which keeps the freshness window identical to a serial run.
codeDirectivesParse.py’s worker results are canonicalized before pickling for exactly this reason — so a parallel scan pickles byte-identically to a serial one.
Module-file update avoidance¶
Compiling a Fortran source regenerates the .mod files of every module it defines, which would invalidate every user of those modules even when the module interfaces did not change. The %.o rule therefore compiles with -J$(BUILDPATH)/moduleBuild/ (a staging directory) and then, for each module listed in the source’s .m file, compares the staged .mod against the current one — moving it into place only if it differs. The generated <module>.mod: <object> rules in Makefile_Module_Dependencies likewise rebuild the owning object only when the .mod file is actually missing. Together these stop recompilation cascades through the module graph.
The directive-catalog stamp¶
The three directive-catalog scripts (codeDirectivesParse.py, stateStorables.py, deepCopyActions.py) run in a single recipe, in that order, because the latter two read the directiveLocations.xml written by the first. Their outputs (Makefile_Directives, directiveLocations.xml, stateStorables.xml, and deepCopyActions.xml) are all products of that one recipe: a stamp file, $(BUILDPATH)/directiveCatalogs.stamp, records when the scripts last ran, and each output that other rules name as a prerequisite has an empty-recipe rule on the stamp. Since the scripts write their outputs only-if-changed, downstream targets rebuild only when catalog content changes, while the stamp’s mtime stops the scripts re-running when nothing is out of date.
Source-Code Markers¶
The build scripts key on a number of special comment markers in the source. None of these affect compilation directly — they exist purely for the build system — and a maintainer grepping for “why does this file depend on that?” usually ends up at one of these:
!![…!!]Delimits an embedded XML directive block (a code directive — see Code Directive Parsing). Parsed by
Galacticus.Build.Directivesand by the source-tree preprocessor; skipped by the plain-text scanners.!!{RST…!!}Delimits an embedded documentation block, written in reStructuredText. Skipped by every scanner (prose inside could otherwise look like code, e.g. a
programstatement).!/ excludeIn a file containing a
programunit: build the executable, but exclude it from thealltarget (findExecutables.py).!; <library>The containing object requires linking against
<library>. Recorded in the object’s.flfile and consumed bylibraryDependencies.pyat link time (useDependencies.py).!: <path> …(Fortran) ///: <path> …(C/C++)Explicit object-file dependencies: the named objects (usually
$(BUILDPATH)-relative.opaths) are added to the containing object’s dependencies — used where a dependency exists throughbind(c)interfaces or other channels invisible to theuse-statement scan (useDependencies.py).! NO_USESAppended to an
includestatement of a generated include file: opts that include out of the automatically-generated dependency chain that ordersMakefile_Use_Dependenciesafter include generation (includeDependencies.py). Used for includes (such as build-metadata files) whose content must not retrigger dependency analysis.!$GLC …Directives consumed by
postprocess.pyto suppress spurious compiler warnings:!$GLC function attributes unused ::(unused module procedures),!$GLC attributes unused ::(unused variables, per enclosing unit),!$GLC attributes initialized ::,!$GLC attributes interoperable ::,!$GLC ignore outlive ::, and!$GLC ignore unused ::.!-->Instrumentation lines written by the source-introspection machinery (including the
.lmapline maps); ignored by directive extraction.
Two ordinary Fortran constructs also carry hard-wired dependency implications in useDependencies.py: a named !$omp critical(<name>) section implies a dependency on openmp_utilities_data.mod (the section enumeration — see OpenMP critical sections), a !$omp parallel construct implies events_filters.mod, and a program unit implies iso_varying_string.mod.
Automatic Discovery¶
Interdependencies between source files, together with requirements for auto-generated code, are automatically discovered during the build process by processing of source files. All such automatic discovery is described below. The scanning scripts share several conventions: they parallelize their per-file scans through Galacticus.Build.ParallelScan (a fork-based pool that preserves result order, so outputs are byte-identical to a serial run, and reads make’s -j setting from MAKEFLAGS); they cache per-file results in blobs (see Per-file cache blobs); and they emit their generated makefiles and catalogs only-if-changed.
Code Directive Parsing¶
The modular nature of Galacticus is enabled through the use of directives embedded within the source code (XML documents inside !![ … !!] comment blocks) which instruct the build system how to link together the various functions. Parsing of these directives is handled by the codeDirectivesParse.py script.
The script also writes $(BUILDPATH)/Makefile_Directives, which carries the extra build dependencies implied by certain directives: for each functionClass and for the componentBuilder directive it makes the preprocessed source depend on the other files that feed it (e.g. _class.p.F90 depends on every <component> directive file and on the component generator’s Python sources), and it orders Makefile_Use_Dependencies and Makefile_Component_Includes after the preprocessed _class file.
The script also outputs $(BUILDPATH)/directiveLocations.xml, which lists the files containing each directive. This is used by the other cataloging scripts to permit rapid processing of files associated with each directive.
State storables and deep-copy actions¶
Two further catalogs are generated in the same recipe (see the directive-catalog stamp above), and are read by the source-tree preprocessor when generating state-store and deep-copy code — which is why every preprocessing rule lists them as prerequisites:
stateStorables.xml(fromstateStorables.py): everyfunctionClass, its concrete implementations,stateStorabletypes, and static event hooks.deepCopyActions.xml(fromdeepCopyActions.py): every derived type participating in adeepCopyActionsdirective, expanded over each type’s inheritance chain.
OpenMP critical sections¶
enumerateOpenMPCriticalSections.py scans every Fortran source for named !$omp critical(<name>) sections and assigns each name an ID (in sorted-name order — a determinism contract the instrumented code relies on), writing $(BUILDPATH)/openMPCriticalSections.xml plus the openMPCriticalSections.count.inc and openMPCriticalSections.enumerate.inc include fragments used by the OpenMP wait-time instrumentation. The rule depends on all Fortran sources through an .up sentinel, so adding, removing, or renaming a critical section regenerates the enumeration, while a no-op regeneration does not cascade into re-preprocessing (the %.p.F90.up rules depend on the .xml).
Executable Files¶
Files which produce executables (including the Galacticus.exe executable) are discovered by the scripts/build/findExecutables.py script: all Fortran files in the source directory (excluding vendored code under source/external/) are parsed, and executable files are identified as those containing a program statement. For each, a rule invoking the Makefile’s LINK_EXECUTABLE canned recipe is written to $(BUILDPATH)/Makefile_All_Execs, and the executable is added to the all target unless the file carries a !/ exclude marker. Build artifacts mirror the hierarchical source tree (e.g. $(BUILDPATH)/tests/nodes.o) while executables keep the historical flat, dot-separated names (tests.nodes.exe) so existing invocations keep working.
Modules Provided¶
Determination of which files provide which Fortran modules is carried out by the scripts/build/moduleDependencies.py script, which generates rules describing these dependencies (and how to build the module file by compiling the corresponding source file) and writes them to $(BUILDPATH)/Makefile_Module_Dependencies.
Additionally, rules are generated which describe how to build the following classes of file:
*.mod.dContains a list of all object files upon which the module depends. Used in constructing the final set of objects which must be linked to build an executable.
*.mod.gvContains a list of all source files upon which the module depends. Used in constructing GraphViz visualizations of dependencies.
*.mContains a list of all modules provided by the corresponding object file. Used when building object files to test whether corresponding module files have been changed — allows avoidance of recompilation cascades if modules do not need to be updated (see Incremental-Build Machinery).
functionClass submodules¶
Each concrete implementation of a functionClass is built on the fly as a Fortran submodule of the module hosting the class. moduleDependencies.py discovers these by walking each implementation file listed in directiveLocations.xml for the class’s directive, synthesizing one submodule record per derived type (named <implementation>_, with parent submodules following the type’s extends chain). The generated rules encode two special behaviors: an implementation’s preprocessed source (<impl>_.p.F90) is produced as a side effect of preprocessing the functionClass base file, so the rules gate each implementation behind its base (and force-regenerate the base if an implementation’s file is missing); and <module>@<submodule>.smod targets tie submodule files to their owning objects.
Modules Used¶
Determination of which files use which Fortran modules is carried out by the scripts/build/useDependencies.py script, which generates rules describing these dependencies and writes them to $(BUILDPATH)/Makefile_Use_Dependencies. Beyond literal use statements, the scan accounts for the implicit dependencies implied by directives (functionClass method modules, event hooks, global functions, enumerations, …) and by the source markers described in Source-Code Markers.
Additionally, rules are generated which describe how to build the following classes of file:
*.dContains a list of all object files upon which each object file depends. Used in constructing the final set of objects which must be linked to build an executable.
*.gvContains a list of all source files upon which each file depends by virtue of
usestatements. Used in constructing GraphViz visualizations of dependencies.*.flContains a list of external libraries (one name per line) upon which each object file depends — populated from the module-to-library and include-to-library tables in
Galacticus.Build.Librariesplus explicit!; <library>markers. Consumed bylibraryDependencies.pyto determine the final link line.
Conditional compilation¶
The scanner honors preprocessor conditionals so that, for example, MPI-only use statements do not create dependencies in non-MPI builds. The set of defined macros is assembled from -D flags on active FCFLAGS lines of the Makefile and the Makefile_Config_* fragments (approximating ifdef/ifndef against the environment), from GALACTICUS_FCFLAGS, and from the C compiler’s predefined macros. Conditions that cannot be evaluated — ifeq/ifneq over make variables on the Makefile side, and #if <expression> or #elif chains on the source side — are treated as active in every branch: the scan deliberately over-approximates, since an unnecessary dependency merely costs a rebuild while a missed one silently produces stale builds. This behavior is pinned by the tests in scripts/build/tests/test_use_dependencies_conditionals.py.
Included Files¶
Dependencies on files due to the use of “include” statements (in both Fortran and C source files) are discovered by the scripts/build/includeDependencies.py script, which generates rules describing these dependencies and writes them to $(BUILDPATH)/Makefile_Include_Dependencies.
All Fortran and C/C++ source files in the source directory are parsed. Files specified in any include statement in a source file are added as a dependency of that source file unless the source file is C/C++ and the named include file can be found in either source/, a standard system include path, or in any include path specified in the GALACTICUS_CFLAGS or GALACTICUS_CPPFLAGS environment variables. This script also emits the ordering rule that makes Makefile_Use_Dependencies depend on every generated include file — the linchpin that guarantees generated includes exist before the use-scan reads them (opt out per include with ! NO_USES).
Parameter Dependencies¶
All runtime parameters upon which a given executable may depend are discovered by the scripts/build/parameterDependencies.py script, which runs at link time (from LINK_METADATA). It reads the target’s .d file to find the participating objects, scans each corresponding source (preferring the preprocessed .p.F90, and including the per-functionClass <stem>.p parameter listings dropped by the preprocessor) for inputParameter and objectBuilder directives, and writes $(BUILDPATH)/<stem>.parameters.F90 — a knownParameterNames subroutine enumerating the de-duplicated, sorted parameter names, which the executable uses to detect unrecognized input parameters. Per-file results are cached in <stem>.blob, and entries for files that leave the target are pruned.
Source Digests¶
To allow saved run states to be validated against the code that produced them, every functionClass/sourceDigest type participating in an executable carries an MD5 digest of its source. scripts/build/sourceDigests.py runs at link time (from LINK_METADATA): for each type it hashes the defining source file plus every file it includes (skipping non-deterministic inputs such as the build-environment and version includes), then composes each type’s digest with those of its declared dependencies and inheritance chain, and emits $(BUILDPATH)/<stem>.md5s.c containing one char <type>MD5[] definition per type. Per-file digests are cached both in <stem>.md5.blob and in per-source .md5/.md5c sidecar files under $(BUILDPATH); sidecar access is serialized with flock locks when LOCKMD5 is active (see Configuration knobs).
Code Generation¶
During build, code is generated automatically. Much of this is to link together functions within Galacticus (thereby permitting the modular nature of Galacticus), but also includes generation of various utility functions. All code generation is described below.
The node-component class hierarchy¶
The nodeComponent class hierarchy is generated from the <component> directives spread across source/objects/nodes/components/ by the componentBuilder source-tree process hook (python/Galacticus/Build/SourceTree/Process/ComponentBuilder.py). A single <componentBuilder/> directive in source/objects/nodes/_class.F90 triggers the hook while that file is preprocessed (see Section Galacticus Preprocessor Directives): it gathers, validates, and parses every <component> directive through the generator package python/Galacticus/Build/Components, runs the generator’s phased pipeline to synthesize the Fortran source, processes that fragment through the full preprocessor pipeline (expanding any directives the generator embedded in its own output), and grafts the result in place of the directive. The generated code therefore lands directly in $(BUILDPATH)/objects/nodes/_class.p.F90 rather than in a separately-generated include file.
Pre-processing and Post-processing¶
Before being compiled, all Fortran source files are passed through a tree-based preprocessor. The functionality of this preprocessor is described in Section Galacticus Preprocessor Directives. The preprocessor is invoked on each source file by the preprocess.py script, which takes a source/*.F90 file as input and outputs a preprocessed file $(BUILDPATH)/*.p.F90 (written only-if-changed — see Update-sentinel (“.up”) files), together with a *.p.F90.lmap line-map sidecar. Each .lmap record has the form:
!--> <original-line> <preprocessed-line> "<original-source-file>"
marking the start of a run of lines in the preprocessed file that maps back to the given original location. The full set of source-tree process hooks is registered by importing Galacticus.Build.SourceTree.Process.all — every driver that runs the preprocessor imports this one module, so a new Process submodule is added in exactly one place.
Because the preprocessor is itself a Python program, the Fortran it emits depends on preprocess.py and on every first-party module that script imports — the source-tree parser, all of the Process hooks, the node-component generator, and so on — just as much as it depends on the Fortran input. Make cannot see that dependency, since the closure is discovered by Python’s import machinery rather than declared anywhere, so scripts/build/preprocessorSources.py computes it: it walks the import graph statically (using ast, so nothing is imported and no side effects are triggered), resolving module names against python/ and against the directory of the entry-point script, and writes $(BUILDPATH)/preprocessorSources.digest — one MD5 per file in the closure — only-if-changed (see Update-sentinel (“.up”) files). The three preprocessing rules take that digest as a prerequisite.
The two granularities are deliberately different. The digest rule is triggered by any Python source at all, because the closure can only be known by inspecting the imports. The digest itself covers only the closure, so re-running the preprocessor over every source file — the expensive consequence — follows only from editing a module the preprocessor really uses; editing, say, an analysis script recomputes the digest, finds it unchanged, and stops there. Run ./scripts/build/preprocessorSources.py <galacticusPath> --list to print the closure.
Unlike the source-scanning rules above, this rule does not list the directories containing its prerequisites. Those rules use directory mtimes to notice a deleted source, but the interpreter writes __pycache__ into every directory it imports from — including during the build’s own ~1800 preprocess.py invocations — so listing them would bump their mtimes mid-build and leave the rule permanently out of date, re-running its recipe on every build. And not only on every build: because the preprocessed _class.p.F90 is a prerequisite of the generated Makefile_Use_Dependencies, this digest is remade inside make’s makefile-remaking phase, so a rule that re-arms itself there runs again in every restart pass. Nothing is lost by omitting them: deleting a module the preprocessor imports fails loudly at the next preprocess.py run, and deleting one it does not import changes nothing to catch.
Without this, editing a generator and rebuilding regenerated nothing: make reported success, the binary relinked, and the stale preprocessed Fortran was compiled — which also made “change a generator, rebuild, and diff the generated code”, the natural way to verify a code-generation change, report a false “no change”.
Output from the compiler is captured to a diagnostics file and fed through the postprocess.py script, which:
uses the
.lmapto translate line numbers in errors and warnings from the preprocessed source back to the original source;filters out known-spurious warnings (e.g. GCC PR58175 scalar-finalizer warnings) and warnings suppressed by
!$GLCmarkers (see Source-Code Markers);exits immediately when the compiler produced no diagnostics (the common case).
Diagnostics are written to a file and postprocessed after capturing the compiler’s exit status — not piped — because in a pipeline the recipe’s status would be the postprocessor’s, and a hard compiler failure that emits no recognizable error line (an internal compiler error, a segfault, an out-of-memory kill) would be reported as success. The recipe fails if either the compiler or the postprocessor reports failure. Link steps are handled identically, with postprocessLinker.py filtering irrelevant static-link warnings and flagging undefined reference / ld returned N exit status diagnostics as a second line of defense. The profiler.sh wrapper SHELL used by compileprof builds likewise propagates the wrapped command’s exit status.
Parameter Catalog and Schema¶
Separate from the compile path, the build can generate machine-readable descriptions of every functionClass implementation’s input parameters:
make parameters-catalogRuns
scripts/build/parameterCatalog.pyto produce$(BUILDPATH)/parameters.catalog.json— names, inferred types, defaults, and nesting for every parameter. Consumed by parameter-file validation tooling; CI checks its provenance coverage withparameterCatalogCheck.py.make parameters-schemaRuns
scripts/build/parameterSchema.pyto regenerate the committed editor-assistance schemaschema/parameters.xsdfrom the catalog; CI checks that it is up to date.parameterValidate.pyvalidates parameter files against the catalog (used by CI and the migration tooling).
Build Files¶
All files involved in the build process are summarized below.
Scripts (all under scripts/build/):
buildProfiler.pyGenerates an HTML report from a
compileprofbuild log (see Section Profiling the Build);codeDirectivesParse.pyDiscovers and parses directives embedded in source files. Generates
makerules for the resulting dependencies (inMakefile_Directives) and a file providing a mapping of which files contain each directive (directiveLocations.xml);deepCopyActions.pyCatalogs every derived type participating in a
deepCopyActionsdirective (see Automatic Discovery);enumerateOpenMPCriticalSections.pyEnumerates named OpenMP critical sections (see Section OpenMP critical sections);
findExecutables.pyDiscovers (and generates rules for) files which generate executables (see Section Executable Files);
includeDependencies.pyDiscovers (and generates rules for) dependencies between files arising from the use of “
include” statements (see Section Included Files);libraryDependencies.pyDetermines the set of libraries that must be linked with each executable (from its
.flfiles and the dependency graph inGalacticus.Build.Libraries), and ensures they are ordered correctly for static linking;libraryInterfaces.py,libraryInterfacesDependencies.py,libraryInterfacesAudit.pyGenerate (and audit coverage of) the shared-library interface (see Section The Shared-Library Interface);
moduleDependencies.pyDiscovers (and generates rules for) dependencies of module files on their source file statements (see Section Modules Provided);
parameterCatalog.py,parameterCatalogCheck.py,parameterSchema.py,parameterValidate.pyGenerate and validate the typed parameter catalog and editor schema;
parameterDependencies.pyDetermines all parameters upon which a given executable depends (see Section Parameter Dependencies);
postprocess.py,postprocessLinker.pyPostprocess compiler and linker diagnostics (see Section Pre-processing and Post-processing);
preprocess.pyPreprocesses Fortran source code to provide various extended functionality (see Section Pre-processing and Post-processing);
preprocessorSources.pyDigests the Python sources implementing the preprocessor, so that editing one re-preprocesses the Fortran (see Section Build Preprocessor Sources);
profiler.shSHELL wrapper used by
compileprofbuilds to time every recipe (see Section Profiling the Build);sourceDigests.pyComputes per-type source MD5 digests for state-file validation (see Section Source Digests);
stateStorables.pyCatalogs functionClasses, their implementations, and stateStorable types (see Automatic Discovery);
staticRelinker.pyCI-only helper (macOS static builds): re-runs a logged link command with system dynamic libraries hidden so static archives are picked up;
useDependencies.pyDiscovers (and generates rules for) dependencies between files originating from module
usestatements (see Section Modules Used).
Generated files (all under $(BUILDPATH) unless noted):
Makefile_All_ExecsRules for building each executable — generated by
findExecutables.py(see Section Executable Files);Makefile_Component_IncludesDependencies of the
nodeComponentclass hierarchy module (objects/nodes/_class) on the per-implementationbound_functionsinclude files; written by the component generator as_class.F90is preprocessed (see Section The node-component class hierarchy);Makefile_Config_*Feature-availability probe results (see Feature-availability probes);
Makefile_DirectivesExtra build dependencies implied by
functionClassandcomponentBuilderdirectives (see Section Code Directive Parsing);Makefile_Include_DependenciesDependencies of source files on files included via “
include” statements — generated byincludeDependencies.py(see Section Included Files);Makefile_Library_DependenciesPreprocess/compile rules for the generated library wrapper units (library builds only; see Section The Shared-Library Interface);
Makefile_Module_DependenciesRules which describe how to build module files from their source files (see Section Modules Provided);
Makefile_Use_DependenciesDependencies between source files originating from module
usestatements (see Section Modules Used);directiveCatalogs.stampTimestamp of the last directive-catalog run (see Incremental-Build Machinery);
directiveLocations.xmlA map of which files contain each code directive (see Section Code Directive Parsing);
stateStorables.xml,deepCopyActions.xmlCatalogs read by the preprocessor when generating state-store and deep-copy code (see Automatic Discovery);
openMPCriticalSections.xml,openMPCriticalSections.count.inc,openMPCriticalSections.enumerate.incThe named-critical-section enumeration (see Section OpenMP critical sections);
*.dLists of dependencies on object files, accumulated to find the final set of files which must be linked to build each executable;
*.mLists of modules provided by each object file, used when testing whether modules have been updated (see Incremental-Build Machinery);
*.flLists of libraries upon which each object file depends (one name per line), used to determine the final set of libraries to link;
*.gvLists of source files upon which each file depends, used in the construction of GraphViz representations of file dependencies;
*.p.Inc/*.incPreprocessor-intermediate / fully-processed include files generated from
source/*.Incfiles (e.g. the per-implementationbound_functions.Incsources). The intermediate is deliberately named.p.Incrather than.Inc: the latter would differ from the processed.inconly by case and the two collide on case-insensitive filesystems (macOS APFS);*.p.F90Preprocessed Fortran source files;
*.p.F90.lmapLine maps from preprocessed back to original sources (see Section Pre-processing and Post-processing);
*.upUpdate sentinels recording when a generator last ran (see Update-sentinel (“.up”) files);
preprocessorSources.digestMD5s of the Python sources implementing the preprocessor (see Section Build Preprocessor Sources);
*.blob,*.md5.blobPer-file pickle caches used by the cataloging and metadata scripts (see Per-file cache blobs);
*.md5,*.md5cPer-source digest sidecar caches (see Section Source Digests);
<stem>.parameters.F90Generated
knownParameterNamessubroutine for an executable (see Section Parameter Dependencies);<stem>.md5s.cGenerated per-type source-digest definitions for an executable (see Section Source Digests);
libgalacticus.Inc,libgalacticus/,galacticus.py(repository root)The generated shared-library interface (see Section The Shared-Library Interface).
Profiling the Build¶
Tooling is provided to help profile the build process. This is useful to identify bottlenecks during build. Profiling can be switched on using the compileprof build option. For example:
make -j16 GALACTICUS_BUILD_OPTION=compileprof Galacticus.exe >& build.log
Every command run by Make will be timed, its peak memory usage measured, and the results output. In the above these outputs are collected to the build.log file. Result lines have the form:
++Task: {<start>|<stop>|<maxRSS>} '<command>'
with RFC 3339 start/stop times and the peak resident set size in kilobytes (\(-1\) when memory usage could not be measured, for example because GNU time is unavailable) — this format is the contract between profiler.sh (producer) and buildProfiler.py (consumer). While you can look through these manually a script is provided that generates a report from these timing data as a web page. For example:
./scripts/build/buildProfiler.py build.log profile.html --durationMinimum 10
will parse timing data from build.log and generate a web page profile.html showing the results. In this case the --durationMinimum 10 option specifies that only commands which took 10 or more seconds to run should be included in the report. (Reducing this limit will lead to a very long report.)
The profile report begins by listing the total time taken for the build, together with an estimate of the peak memory used by the build—found by summing, for each second of the build, the peak memory of all included tasks running at that time. Following that is a list of all commands performed ordered by completion time. Next to each command is a bar which extends from the start to the end time of the command. Each second of the bar is colored to indicate the degree of build parallelism at that time—green shows maximum parallelism, while red shows no parallelism (i.e. a single command was running at that time).
Finally, each command is assigned a “cost”, \(\chi\). This is defined as:
where \(i_\mathrm{start}\) and \(i_\mathrm{end}\) are the start and end times of the command, and \(N_i\) is the number of commands running in parallel at time \(i\). Cost is therefore higher for commands which run longer and for commands which are executed with less parallelism. A ranked list of commands, from most to least costly is included in the report. The report also shows the peak memory used by each command, together with a ranking of commands from most to least memory-hungry, to help identify the largest memory consumers in the build.
A historical note¶
The build scripts were ported file-by-file from Perl originals in 2026. The Perl sources were removed in commits 5278aadf8 and 33dfc7939 and remain retrievable from git history. Comments in the Python code do not cite the Perl originals; where the port deliberately preserved non-obvious behavior (for example, to keep generated code or build caches stable), the constraint is documented in place, in its own terms. Consult the removed Perl sources via git history if you need to trace the origin of such a behavior.