Case Study 2: Shipping It
"Code that only builds on your machine is a hobby. Code that anyone can build is software."
Executive Summary
Where the first case study read an existing stack, this one asks you to build one. You have a working
heat solver — modules, a derived type, a namelist config, error handling — but it lives as a pile of .f90
files and a build command you keep in your head. In this study you turn it into a real, shareable fpm
package: a project with a manifest, an external dependency, an automated test that guards its correctness,
and generated documentation. This is the Chapter 16 Project Checkpoint carried to its conclusion, and it is
how Part III ends — with your solver ready for the numerical, performance, and parallel
work ahead, packaged the way real scientific software is packaged.
Skills applied: creating and structuring an fpm project (§16.3); writing an fpm.toml with metadata and a
dependency (§16.3); adding a test (§16.5, forward to Chapter 37); documenting with FORD (§16.5); the reproducibility discipline of pinning
dependencies (§16.3); building on modules (Chapter 8) and error handling (Chapter 13).
Background
Your solver, as of the end of Part II, is a set of files you compile roughly like this:
$ gfortran -c kinds.f90 && gfortran -c heat_solver.f90 && gfortran -c heat_io.f90 \
&& gfortran main.f90 kinds.o heat_solver.o heat_io.o -o heat
It works — but only for you, only in this directory, and only as long as you remember the file order. Nobody
else can clone it and build it in one command; there is no test that fails when someone breaks the physics;
there is no documentation but the source. We fix all three by packaging it with fpm. The target is a project a
collaborator can git clone and build with a single fpm build, that self-tests with fpm test, and whose
API is documented by ford.
Phase 1 — Scaffold and Populate
Create the skeleton and drop your existing code into fpm's fixed layout:
$ fpm new heat-solver
$ # move the library modules into src/ :
$ # kinds.f90, heat_solver.f90, heat_io.f90, heat_types.f90
$ # move the driver `program heat` into app/main.f90
$ fpm build
The layout is not decoration; it is the contract that lets fpm do your bookkeeping:
heat-solver/
├── fpm.toml
├── README.md
├── app/
│ └── main.f90 program heat (the driver) -> the executable
├── src/
│ ├── kinds.f90 module kinds (owns dp)
│ ├── heat_types.f90 module heat_types (the field_t derived type)
│ ├── heat_solver.f90 module heat_solver (the update)
│ └── heat_io.f90 module heat_io (namelist read, field write)
└── test/
└── test_step.f90 a correctness check (Phase 3)
You did not write a Makefile, and you did not tell fpm that heat_solver depends on kinds. fpm reads the
use statements, builds the dependency graph, and compiles in order — the very ordering you tracked by hand in
Chapter 8. That automation is the first
thing packaging buys you.
Phase 2 — The Manifest and a Dependency
The manifest gives the project an identity and pulls in stdlib, so you can replace hand-rolled utilities (a mean, a standard deviation, a text-table load) with reviewed library code:
name = "heat-solver"
version = "0.1.0"
license = "MIT"
author = "Your Name"
maintainer = "you@example.com"
copyright = "Copyright 2026, Your Name"
[build]
auto-executables = true
auto-tests = true
[dependencies]
# Pin a tag for reproducibility — a moving 'latest' can change your build overnight.
stdlib = { git = "https://github.com/fortran-lang/stdlib", tag = "v0.7.0" }
The tag is the reproducibility discipline that
Chapter 37 will make
non-negotiable: an unpinned dependency means "whatever that repository happens to contain today," which is
poison for a result you intend to publish. (The repository URL is canonical; the exact tag string is the kind
of Tier-2 detail to confirm against stdlib's current releases rather than copy from memory — including from
this book.)
Phase 3 — A Test That Guards the Physics
Here is the payoff that matters most for scientific code. Add a test that pins a known-correct result, so that
when you optimize the stencil in Part VII or parallelize it in
Part VIII, a single fpm test tells you instantly whether
you changed the answer. On a 3×3 plate with a hot top edge (100) and cold sides (0), one averaging step must
set the interior cell to exactly $\tfrac{1}{4}(100+0+0+0) = 25$. A test asserts precisely that:
! test/test_step.f90 — a regression check standing in for a pFUnit/test-drive test.
! (fpm test compiles and runs everything under test/)
program test_step
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp) :: u(3,3)
real(dp), parameter :: expected = 25.0_dp, tol = 1.0e-12_dp
u = 0.0_dp
u(1,:) = 100.0_dp ! hot top edge
call jacobi_step(u)
if (abs(u(2,2) - expected) < tol) then
print '(a)', 'PASS: one relaxation step gives u(2,2) = 25.0'
else
print '(a, f8.4)', 'FAIL: u(2,2) = ', u(2,2)
end if
contains
subroutine jacobi_step(f)
real(dp), intent(inout) :: f(:,:)
real(dp), allocatable :: fn(:,:)
integer :: i, j
fn = f
do j = 2, size(f,2) - 1
do i = 2, size(f,1) - 1
fn(i,j) = 0.25_dp * ( f(i-1,j) + f(i+1,j) + f(i,j-1) + f(i,j+1) )
end do
end do
f = fn
end subroutine jacobi_step
end program test_step
! Hand computation: u(2,2) = 0.25*(100+0+0+0) = 25.0; |25.0 - 25.0| < tol -> PASS.
!
! Expected output:
! PASS: one relaxation step gives u(2,2) = 25.0
A real project would write this as a pFUnit or test-drive test with an @assertEqual, and pFUnit could even
run it under MPI once the solver is parallel — but the idea is identical: encode a value you know to be
correct, and let the build tell you when it changes. In production the test would call the heat_solver module
from src/; it is inlined here so the file compiles on its own under the book's no-execution policy.
Why a tolerance, not equality? We compare with
abs(... ) < tolrather than==because floating-point results are rarely bit-exact after real arithmetic — a habit Chapter 20 will justify in full. Here the arithmetic is exact, but the tolerance is the right reflex.
Phase 4 — Documentation with FORD
Annotate the public API with FORD doc comments — ordinary comments the compiler ignores and FORD turns into a website:
!> Advance the temperature field by one relaxation step.
subroutine step(field, alpha, dt)
type(field_t), intent(inout) :: field !! the temperature field to update
real(dp), intent(in) :: alpha !! thermal diffusivity
real(dp), intent(in) :: dt !! time-step size
! ...
end subroutine step
Then a tiny FORD project file configures the doc build:
---
project: heat-solver
summary: A 2D heat-equation solver, built across the book.
src_dir: ./src
output_dir: ./doc
---
Documentation for the heat solver. Generated by FORD from the source and its doc comments.
Run ford project.md and you have a browsable manual of every module, type, and procedure. We wire this into
continuous integration — docs and tests running automatically on every change — in
Chapter 37.
Phase 5 — Ship It
Two commands now do what your fragile shell one-liner did, plus everything it did not:
$ fpm build # fetch & build stdlib, then the library, driver, and tests, in order
$ fpm test # run test/test_step.f90 -> PASS
Add a README.md (what it solves, how to build, how to run), commit the pinned fpm.toml, and your solver is
genuinely software: a collaborator clones the repository, runs fpm build, and it works — dependency and
all — on their machine, not just yours. That is the line the epigraph draws between a hobby and software, and
crossing it is what Part III has been quietly building toward.
Discussion Questions
- Packaging added a manifest, a dependency, a test, and docs. Rank these four by how much risk each removes from a scientific project, and defend your top choice.
- The test asserts one hand-known value (25.0). What is the strongest argument that a single such test is worth writing before any optimization work begins? (Connect to the regression-test idea previewed in Chapter 37.)
- Why pin the stdlib dependency to a
tagrather than track the latest commit? Give a concrete scenario where not pinning silently corrupts a published result.
Your Turn: Extensions
- Option A. Actually do it: run
fpm new, drop in the smallest version of the solver you have (even just the Jacobi step), add thetest/program above, and getfpm testto print PASS. Predict the output first. - Option B. Add a second test that checks a different known value — e.g., a plate with all edges at 50 should leave the interior at 50 after one step (a fixed point). Compute the expected value by hand, write the assertion, and confirm it passes.
- Option C. Replace one hand-rolled utility in the solver (say, a mean of the field for a sanity print)
with the equivalent
stdlib_statscall, add the dependency, and rebuild with fpm. Note what you deleted.
Key Takeaways
- Packaging with fpm turns a pile of files and a remembered build command into software anyone can clone and
build in one command — fpm reads your
usegraph and compiles the modules in order for you. - The
fpm.tomlmanifest gives the project identity and dependencies; pin dependencies to a tag for reproducible builds. - A single test that pins a hand-known result (
fpm test→ PASS) is the cheapest insurance in scientific computing: it catches the day you accidentally change the numbers while "just optimizing." - FORD comments are free — they are ordinary comments — and they turn into a real manual. Tests, docs, and CI are developed fully in Chapter 37; this study is your first working version of all three.