Programs API Design
Experimental API
This API is experimental and may change or be removed in future versions without following normal deprecation procedures.
This document describes the design of the Programs API (GitHub issue #263). It is intended to be developer-facing, not user-facing, though users may also find it informative.
Background
Currently, program information is maintained in pymake, which serves dual purposes: (1) maintaining a database of program metadata (download URLs, versions, build configuration), and (2) providing build capabilities. The latter is pymake’s explicit responsibility, the former accidental and located in pymake just for convenience.
Some preliminary work has begun to transfer program metadata responsibilities to modflow-devtools. The existing modflow_devtools.programs module provides a minimal read-only interface to a database (programs.csv) copied more or less directly from the pymake database, including information like each program’s:
name
version
source code download URL
build information (e.g. double precision)
This approach has several limitations:
Static coupling: Pymake (or any other project containing such a program database, like
modflow-devtools) must be updated whenever any program is released, creating a maintenance bottleneck.No introspection: Limited ability to query available program versions or builds
Manual maintenance: Developers must manually update the CSV file
No install support: The API only provides metadata, not installation capabilities
Objective
Create a Programs API that:
Decouples program releases from devtools releases
Discovers and synchronizes program metadata from remote sources
Supports installation and management of program binaries
Facilitates the eventual retirement of pymake by consolidating program database responsibilities in devtools
Mirrors Models API and DFNs API architecture/UX for consistency
Overview
Make MODFLOW ecosystem repositories responsible for publishing their own metadata.
Make modflow-devtools responsible for:
Defining the program registry publication contract
Providing registry-creation machinery
Storing bootstrap information locating program repositories
Discovering remote registries at install time or on demand
Caching registry metadata locally
Exposing a synchronized view of available programs
Installing program binaries
Program maintainers can publish registries as release assets, either manually or in CI.
Architecture
The Programs API mirrors the Models API architecture with adaptations for program-specific concerns like platform-specific binary distributions.
Bootstrap file
The bootstrap file tells modflow-devtools where to look for programs. This file will be checked into the repository at modflow_devtools/programs/programs.toml and distributed with the package.
Bootstrap file contents
At the top level, the bootstrap file consists of a table of sources, each describing a repository distributing one or more programs.
Each source entry has:
repo: Repository identifier (owner/name)refs: List of release tags to sync by default
User config overlay
Users can customize or extend the bundled bootstrap configuration by creating a user config file at:
Linux/macOS:
~/.config/modflow-devtools/programs.toml(respects$XDG_CONFIG_HOME)Windows:
%APPDATA%/modflow-devtools/programs.toml
The user config follows the same format as the bundled bootstrap file. Sources defined in the user config will override or extend those in the bundled config, allowing users to:
Add custom program repositories
Point to forks of existing repositories (useful for testing)
Override default refs for existing sources
Implementation note: The user config path logic (get_user_config_path("programs")) is shared across all three APIs (Models, Programs, DFNs) via modflow_devtools.config, but each API implements its own merge_bootstrap() function using API-specific bootstrap schemas.
Sample bootstrap file
[sources.modflow6]
repo = "MODFLOW-ORG/modflow6"
refs = ["6.6.3"]
# Provides mf6, zbud6, mf5to6, libmf6
[sources.modpath7]
repo = "MODFLOW-ORG/modpath7"
refs = ["7.2.001"]
[sources.mt3d-usgs]
repo = "MODFLOW-ORG/mt3d-usgs"
refs = ["1.1.0"]
[sources.executables]
repo = "MODFLOW-ORG/executables"
refs = ["latest"]
# Consolidated repo for legacy programs (mf2005, mfnwt, etc).
# TODO: replace with separate repos as they become available.
Note: A source repository described in the bootstrap file may provide a single program or multiple programs. E.g., the modflow6 repository provides mf6, zbud6, and mf5to6).
Registry files
Each source repository must make a program registry file available. Program registries describe available programs and metadata needed for installation.
Registry file format
Registry files shall be named programs.toml (not registry.toml - the specific naming distinguishes it from the Models and DFNs registries) and contain, at minimum, a dictionary programs enumerating programs provided by the source repository. For instance:
schema_version = "1.0"
# Example 1: Distribution-specific exe paths (when archive structures differ)
[programs.mf6]
description = "MODFLOW 6 groundwater flow model"
license = "CC0-1.0"
[[programs.mf6.dists]]
name = "linux"
asset = "mf6.7.0_linux.zip"
exe = "mf6.7.0_linux/bin/mf6" # Each platform has different top-level dir
hash = "sha256:..."
[[programs.mf6.dists]]
name = "mac"
asset = "mf6.7.0_mac.zip"
exe = "mf6.7.0_mac/bin/mf6"
hash = "sha256:..."
[[programs.mf6.dists]]
name = "win64"
asset = "mf6.7.0_win64.zip"
exe = "mf6.7.0_win64/bin/mf6.exe" # Note: .exe extension required
hash = "sha256:..."
# Example 2: Program-level exe path (when all platforms share same structure)
[programs.mfnwt]
exe = "bin/mfnwt" # Same relative path for all platforms (.exe auto-added on Windows)
description = "MODFLOW-NWT with Newton formulation"
license = "CC0-1.0"
[[programs.mfnwt.dists]]
name = "linux"
asset = "linux.zip" # Contains bin/mfnwt
hash = "sha256:..."
[[programs.mfnwt.dists]]
name = "win64"
asset = "win64.zip" # Contains bin/mfnwt.exe (extension auto-added)
hash = "sha256:..."
# Example 3: Default exe path (when executable is at bin/{program})
[programs.zbud6]
# No exe specified - defaults to "bin/zbud6" (or "bin/zbud6.exe" on Windows)
description = "MODFLOW 6 Zonebudget utility"
license = "CC0-1.0"
[[programs.zbud6.dists]]
name = "linux"
asset = "mf6.7.0_linux.zip"
hash = "sha256:..."
[[programs.zbud6.dists]]
name = "win64"
asset = "mf6.7.0_win64.zip"
hash = "sha256:..."
Executable path resolution:
The exe field can be specified at three levels, checked in this order:
Distribution-level (
[[programs.{name}.dists]]entry withexefield)Supports any custom path within the archive
Use when different platforms have different archive structures
Most specific - overrides program-level and default
Example:
exe = "mf6.7.0_win64/bin/mf6.exe"Example:
exe = "custom/nested/path/to/program"
Program-level (
[programs.{name}]section withexefield)Supports any custom path shared across all platforms
Use when all platforms share the same relative path structure
Example:
exe = "bin/mfnwt"Example:
exe = "special/location/program"
Default (neither specified)
Automatically detects executable location when installing
Tries common patterns in order:
Nested with bin/:
{archive_name}/bin/{program}Nested without bin/:
{archive_name}/{program}Flat with bin/:
bin/{program}Flat without bin/:
{program}
Example: For
mf6, automatically finds binary whether inmf6.7.0_linux/bin/mf6,bin/mf6, or other common layoutsOnly used when no explicit
exefield is provided
Archive structure patterns:
The API supports four common archive layouts:
Nested with bin/ (e.g., MODFLOW 6):
mf6.7.0_linux.zip └── mf6.7.0_linux/ └── bin/ └── mf6Nested without bin/:
program.1.0_linux.zip └── program.1.0_linux/ └── programFlat with bin/:
program.zip └── bin/ └── programFlat without bin/:
program.zip └── program
The make_registry tool automatically detects which pattern each archive uses and only stores non-default exe paths in the registry.
Windows .exe extension handling:
The
.exeextension is automatically added on Windows platforms if not presentYou can specify
exe = "mfnwt"and it becomesmfnwt.exeon WindowsOr explicitly include it:
exe = "path/to/mfnwt.exe"
Format notes:
Version and repository information come from the release tag and bootstrap configuration, not from the registry file
The
schema_versionfield is optional but recommended for future compatibility
Platform identifiers are as defined in the modflow-devtools OS tag specification: linux, mac, win64.
Binary asset URLs: The asset field contains just the filename (no full URL stored). Full download URLs are constructed dynamically at runtime from bootstrap metadata as:
https://github.com/{repo}/releases/download/{tag}/{asset}
For example: https://github.com/MODFLOW-ORG/modflow6/releases/download/6.6.3/mf6.6.3_linux.zip
This dynamic URL construction allows users to test against forks by simply changing the bootstrap configuration.
Registries vs. installation metadata
The Programs API maintains two distinct layers of metadata:
Registry files (registry.toml) - Published by program maintainers:
Describe what’s available from a release
GitHub-coupled by design (asset names, not full URLs)
Controlled by program repositories
Cached locally after sync
Installation metadata ({program}.json) - Maintained by modflow-devtools:
Track what’s installed locally and where
Source-agnostic (store full
asset_url, not just asset name)Enable executable discovery and version management
Support any installation source
This separation provides architectural flexibility:
Future extensibility: Installation metadata format doesn’t change if we add support for:
Mirror sites (different URLs, same metadata structure)
Direct binary URLs (no GitHub release required)
Local builds (user-compiled binaries)
Import from get-modflow or other tools
Clean responsibilities: Registry files describe “what exists”, metadata tracks “what I installed from where”
Portability: Users could theoretically register manually-installed binaries using the same metadata format
While registries are currently tied to GitHub releases (which is pragmatic and appropriate for the MODFLOW ecosystem), the installation metadata layer remains flexible for future needs.
Registry discovery
Program registries are published as GitHub release assets alongside binary distributions. Registry file assets must be named programs.toml.
Registry discovery URL pattern:
https://github.com/{org}/{repo}/releases/download/{tag}/programs.toml
Examples:
https://github.com/MODFLOW-ORG/modflow6/releases/download/6.6.3/programs.toml
https://github.com/MODFLOW-ORG/modpath7/releases/download/7.2.001/programs.toml
Registry discovery procedure
At sync time, modflow-devtools discovers remote registries for each configured source and release tag:
Check for release tag: Look for a GitHub release with the specified tag
Fetch registry asset: Download
programs.tomlfrom the release assetsFailure cases:
If release tag doesn’t exist:
ProgramRegistryDiscoveryError( f"Release tag '{tag}' not found for {repo}" )
If release exists but lacks
programs.tomlasset:ProgramRegistryDiscoveryError( f"Program registry file 'programs.toml' not found as release asset " f"for {repo}@{tag}" )
Registry/program metadata caching
Cache structure:
~/.cache/modflow-devtools/
├── programs/
│ ├── registries/
│ │ ├── modflow6/ # by source repo
│ │ │ └── 6.6.3/
│ │ │ └── registry.toml
│ │ ├── modpath7/
│ │ │ └── 7.2.001/
│ │ │ └── registry.toml
│ │ └── executables/
│ │ └── latest/
│ │ └── registry.toml
│ ├── archives/
│ │ ├── mf6/ # downloaded archives
│ │ │ └── 6.6.3/
│ │ │ └── linux/
│ │ │ └── mf6.6.6.3_linux.zip
│ │ └── mp7/
│ │ └── 7.2.001/
│ │ └── linux/
│ │ └── mp7.7.2.001_linux.zip
│ ├── binaries/
│ │ ├── mf6/ # extracted binaries (all versions)
│ │ │ ├── 6.6.3/
│ │ │ │ └── linux/
│ │ │ │ └── bin/
│ │ │ │ └── mf6
│ │ │ └── 6.5.0/
│ │ │ └── linux/
│ │ │ └── bin/
│ │ │ └── mf6
│ │ ├── zbud6/
│ │ │ └── 6.6.3/
│ │ │ └── linux/
│ │ │ └── ...
│ │ └── mp7/
│ │ └── 7.2.001/
│ │ └── ...
│ └── metadata/
│ ├── mf6.json # installation tracking per program
│ ├── zbud6.json
│ └── mp7.json
Metadata tracking (inspired by get-modflow):
Each program has a metadata JSON file at
~/.cache/modflow-devtools/programs/metadata/{program}.jsonTracks all installations and versions:
Program name, installed versions, platform
For each installation: bindir, version, installation timestamp
Source repository, tag, asset URL, SHA256 hash
Currently active version in each bindir
Enables executable discovery, version management, and fast re-switching
Example metadata file (mf6.json):
{
"program": "mf6",
"installations": [
{
"version": "6.6.3",
"platform": "linux",
"bindir": "/usr/local/bin",
"installed_at": "2024-01-15T10:30:00Z",
"source": {
"repo": "MODFLOW-ORG/modflow6",
"tag": "6.6.3",
"asset_url": "https://github.com/.../mf6.6.6.3_linux.zip",
"hash": "sha256:..."
},
"executables": ["mf6"],
"active": true
},
{
"version": "6.5.0",
"platform": "linux",
"bindir": "/home/user/.local/bin",
"installed_at": "2024-01-10T14:20:00Z",
"source": {...},
"active": false
}
]
}
Cache management:
Registry files are cached per source repository and release tag
Downloaded archives are cached and verified against registry hashes before reuse
Binary distributions (all versions) are cached per program name, version, and platform
Installed binaries are copies from cache to user’s chosen bindir (not symlinks)
Cache can be cleared with
programs cleancommand (with options for archives, binaries, or registries)Users can list cached/installed programs with
programs listCache is optional after installation - only needed for version switching without re-download
Registry synchronization
Synchronization updates the local registry cache with remote program metadata.
Manual sync
Exposed as a CLI command and Python API:
# Sync all configured sources and release tags
mf programs sync
# Sync specific source
mf programs sync --source modflow6
# Force re-download
mf programs sync --force
# Show sync status
mf programs info
# List available programs
mf programs list
Or via Python API:
from modflow_devtools.programs import sync_registries, get_sync_status
# Sync all
sync_registries()
# Sync specific
sync_registries(repo="MODFLOW-ORG/modflow6", version="6.6.3")
# Check status
status = get_sync_status()
Automatic sync
At install time: Best-effort sync during package installation (fail silently on network errors)
On first use: If registry cache is empty, attempt to sync before raising errors
Configurable (Experimental): Auto-sync is opt-in via environment variable:
MODFLOW_DEVTOOLS_AUTO_SYNC=1(set to “1”, “true”, or “yes”)
Force semantics
The --force flag has different meanings depending on the command, maintaining separation of concerns:
sync --force: Forces re-downloading of registry metadata
Re-fetches
programs.tomlfrom GitHub even if already cachedUse when registry files have been updated upstream
Does not affect installed programs or downloaded archives
Network operation required
install --force: Forces re-installation of program binaries
Re-extracts from cached archive and re-copies to installation directory
Does not re-sync registry metadata (registries and installations are decoupled)
Use when installation is corrupted or when reinstalling to different location
Works offline if archive is already cached
Network operation only if archive not cached
Design rationale:
Separation of concerns: Sync manages metadata discovery, install manages binary deployment
Offline workflows: Users can reinstall without network access if archives are cached
Performance: Avoids unnecessary network calls when registry hasn’t changed
Explicit control: Users explicitly choose when to refresh metadata vs reinstall binaries
Debugging: Easier to isolate issues between registry discovery and installation
Common patterns:
# Update to latest registry and install
mf programs sync --force
mf programs install mf6
# Repair installation without touching registry (offline-friendly)
mf programs install mf6 --force
# Complete refresh of both metadata and installation
mf programs sync --force
mf programs install mf6 --force
Program installation
Installation extends beyond metadata to actually providing program executables by downloading and managing pre-built platform-specific binaries.
# Install from binary (auto-detects platform)
mf programs install mf6
# Install specific version
mf programs install mf6@6.6.3
# Install to custom location
mf programs install mf6 --bindir /usr/local/bin
# Install multiple versions side-by-side (cached separately)
mf programs install mf6@6.6.3
mf programs install mf6@6.5.0
# List installation history
mf programs history
mf programs history mf6
# Uninstall specific version
mf programs uninstall mf6@6.6.3
# Uninstall all versions
mf programs uninstall mf6 --all
Python API:
from modflow_devtools.programs import install_program, list_installed, get_executable
# Install
install_program("mf6", version="6.6.3")
# Install to custom bindir
install_program("mf6", version="6.6.3", bindir="/usr/local/bin")
# Get executable path (looks up active version in bindir)
mf6_path = get_executable("mf6")
# Get specific version
mf6_path = get_executable("mf6", version="6.6.3")
# List installed
installed = list_installed()
Installation process (adapted from get-modflow):
Resolve program name to registry entry
Detect platform (or use specified platform)
Check if binary distribution available for platform
Determine bindir (interactive selection, explicit path, or default from previous install)
Check cache for existing archive (verify hash if present)
Download archive to cache if needed:
~/.cache/modflow-devtools/programs/archives/{program}/{version}/{platform}/Extract to binaries cache:
~/.cache/modflow-devtools/programs/binaries/{program}/{version}/{platform}/Copy executables from cache to user’s chosen bindir (not symlink)
Apply executable permissions on Unix (
chmod +x)Update metadata file:
~/.cache/modflow-devtools/programs/metadata/{program}.jsonReturn paths to installed executables
Version management:
Multiple versions cached separately in
~/.cache/modflow-devtools/programs/binaries/{program}/{version}/User can install to different bindirs (e.g.,
/usr/local/bin,~/.local/bin)Only one version is “active” per bindir (the actual copy at that location)
selectcommand re-copies a different version from cache to bindirMetadata tracks which version is active in each bindir
Version switching is fast (copy operation, milliseconds for typical MODFLOW binaries)
Why copy instead of symlink?
Simplicity: Single code path for all platforms (Unix, Windows, macOS)
Consistency: Same behavior everywhere
Robustness: Installed binary is independent of cache (cache can be cleared)
User expectations: Binary is actually where they asked for it, not a symlink
No Windows symlink issues: Avoids admin privilege requirements on older Windows
Note: Programs are expected to publish pre-built binaries for all supported platforms. Building from source is not supported - program repositories are responsible for releasing platform-specific binaries.
Source program integration
For program repositories to integrate, they can generate registry files in two ways:
Mode 1: Local Assets (CI/Build Pipeline)
Use this mode when you have local distribution files during CI builds:
# Generate registry from local distribution files
python -m modflow_devtools.programs.make_registry \
--dists *.zip \
--programs mf6 zbud6 libmf6 mf5to6 \
--version 6.6.3 \
--repo MODFLOW-ORG/modflow6 \
--compute-hashes \
--output programs.toml
How it works:
Uses
--diststo specify a glob pattern for local distribution files (e.g.,*.zip)Scans the local filesystem for matching files
Requires
--versionand--repoargumentsOptionally computes SHA256 hashes from local files with
--compute-hashesCreates asset entries from local file names
Auto-detects platform from file names (linux, mac, win64, etc.)
Automatic pattern detection:
Inspects archives to detect executable locations
Recognizes nested and flat archive patterns
Automatically optimizes exe paths (only stores non-default paths)
Detects when all distributions use the same relative path
Caches downloaded assets to avoid redundant downloads when multiple programs share the same archive
Example CI integration (GitHub Actions):
- name: Generate program registry
run: |
python -m modflow_devtools.programs.make_registry \
--dists *.zip \
--programs mf6 zbud6 libmf6 mf5to6 \
--version ${{ github.ref_name }} \
--repo ${{ github.repository }} \
--compute-hashes \
--output programs.toml
- name: Upload registry to release
uses: softprops/action-gh-release@v1
with:
files: programs.toml
Mode 2: GitHub Release (Testing/Regeneration)
Use this mode to generate a registry from an existing GitHub release:
# Generate registry from existing GitHub release
python -m modflow_devtools.programs.make_registry \
--repo MODFLOW-ORG/modflow6 \
--version 6.6.3 \
--programs mf6 zbud6 libmf6 mf5to6 \
--output programs.toml
How it works:
Fetches release assets from GitHub API using repo and version (tag)
Downloads assets to detect exe paths and enable pattern optimization
Optionally computes SHA256 hashes with
--compute-hashesUseful for testing or regenerating a registry for an existing release
No
--distsargument needed - pulls from GitHub directlyAutomatic pattern detection (same as Mode 1):
Inspects archives to find executables
Detects nested/flat patterns automatically
Only stores non-default exe paths in registry
Caches downloads when processing multiple programs from same release
Additional options:
# With custom executable paths (if not bin/{program})
python -m modflow_devtools.programs.make_registry \
--dists *.zip \
--programs mf6:bin/mf6 zbud6:bin/zbud6 custom:path/to/exe \
--version 6.6.3 \
--repo MODFLOW-ORG/modflow6
# With description and license metadata
python -m modflow_devtools.programs.make_registry \
--dists *.zip \
--programs mf6 \
--version 6.6.3 \
--repo MODFLOW-ORG/modflow6 \
--description "MODFLOW 6 groundwater flow model" \
--license "CC0-1.0"
Program addressing
Format: {program}@{version}
Examples:
mf6@6.6.3- MODFLOW 6 version 6.6.3zbud6@6.6.3- MODFLOW 6 Zonebudget version 6.6.3mf5to6@6.6.3- MODFLOW 5 to 6 converter version 6.6.3mp7@7.2.001- MODPATH 7 version 7.2.001mf2005@1.12.00- MODFLOW-2005 version 1.12.00
Benefits:
Simple, intuitive addressing
Explicit versioning
Prevents version conflicts
Enables side-by-side installations
Note: Program names are assumed to be globally unique across all sources. The source repository is an implementation detail of registry discovery - users just need to know the program name and version. All versions correspond to GitHub release tags.
Registry classes
The Programs API uses a consolidated object-oriented design with Pydantic models and concrete classes.
ProgramDistribution
Represents platform-specific distribution information:
class ProgramDistribution(BaseModel):
"""Distribution-specific information."""
name: str # Distribution name (e.g., linux, mac, win64)
asset: str # Release asset filename
exe: str | None # Executable path within archive (optional, overrides program-level exe)
hash: str | None # SHA256 hash
ProgramMetadata
Program metadata in registry:
class ProgramMetadata(BaseModel):
"""Program metadata in registry."""
description: str | None
license: str | None
exe: str | None # Optional: defaults to bin/{program}
dists: list[ProgramDistribution] # Available distributions
def get_exe_path(self, program_name: str, platform: str | None = None) -> str:
"""Get executable path, using default if not specified."""
ProgramRegistry
Top-level registry data model:
class ProgramRegistry(BaseModel):
"""Program registry data model."""
schema_version: str | None
programs: dict[str, ProgramMetadata]
ProgramCache
Manages local caching of program registries:
class ProgramCache:
"""Manages local caching of program registries."""
def save(self, registry: ProgramRegistry, source: str, ref: str) -> Path
def load(self, source: str, ref: str) -> ProgramRegistry | None
def has(self, source: str, ref: str) -> bool
def list(self) -> list[tuple[str, str]]
def clear(self)
ProgramSourceRepo
Represents a single program source repository:
class ProgramSourceRepo(BaseModel):
"""A single program source repository."""
repo: str
name: str | None
refs: list[str]
def discover(self, ref: str) -> DiscoveredProgramRegistry
def sync(self, ref: str | None, force: bool, verbose: bool) -> SyncResult
def is_synced(self, ref: str) -> bool
def list_synced_refs(self) -> list[str]
ProgramSourceConfig
Configuration for program sources:
class ProgramSourceConfig(BaseModel):
"""Configuration for program sources."""
sources: dict[str, ProgramSourceRepo]
@property
def status(self) -> dict[str, ProgramSourceRepo.SyncStatus]
def sync(self, source, force, verbose) -> dict[str, SyncResult]
@classmethod
def load(cls, bootstrap_path, user_config_path) -> "ProgramSourceConfig"
ProgramInstallation
Tracks a single program installation:
class ProgramInstallation(BaseModel):
"""A single program installation."""
version: str
platform: str
bindir: Path
installed_at: datetime
source: dict[str, str] # repo, tag, asset_url, hash
executables: list[str]
active: bool
InstallationMetadata
Manages installation metadata for a program:
class InstallationMetadata:
"""Manages installation metadata for a program."""
def __init__(self, program: str, cache: ProgramCache | None = None)
def load(self) -> bool
def save(self) -> None
def add_installation(self, installation: ProgramInstallation) -> None
def remove_installation(self, version: str, bindir: Path | None) -> None
def get_installation(self, version: str, bindir: Path | None) -> ProgramInstallation | None
def list_installations(self) -> list[ProgramInstallation]
def get_active_installation(self, bindir: Path | None) -> ProgramInstallation | None
def set_active(self, version: str, bindir: Path) -> None
ProgramManager
High-level manager for program installation and version management:
class ProgramManager:
"""High-level program installation manager."""
def __init__(self, cache: ProgramCache | None = None)
@property
def config(self) -> ProgramSourceConfig
def install(
self,
program: str,
version: str | None = None,
bindir: Path | None = None,
platform: str | None = None,
force: bool = False,
verbose: bool = False,
) -> list[Path]
def select(
self,
program: str,
version: str,
bindir: Path | None = None,
verbose: bool = False,
) -> list[Path]
def uninstall(
self,
program: str,
version: str | None = None,
bindir: Path | None = None,
all_versions: bool = False,
remove_cache: bool = False,
verbose: bool = False,
) -> None
def get_executable(
self,
program: str,
version: str | None = None,
bindir: Path | None = None,
) -> Path
def list_installed(
self,
program: str | None = None,
) -> dict[str, list[ProgramInstallation]]
Python API
The Programs API provides both object-oriented and functional interfaces.
Object-Oriented API (using ProgramManager):
from modflow_devtools.programs import ProgramManager
# Create manager (or use _DEFAULT_MANAGER)
manager = ProgramManager()
# Install programs
paths = manager.install("mf6", version="6.6.3", verbose=True)
# Switch versions
manager.select("mf6", version="6.5.0", verbose=True)
# Get executable path
mf6_path = manager.get_executable("mf6")
# List installed programs
installed = manager.list_installed()
# Uninstall
manager.uninstall("mf6", version="6.5.0")
Functional API (convenience wrappers):
from modflow_devtools.programs import (
install_program,
select_version,
get_executable,
list_installed,
uninstall_program,
)
# Install
paths = install_program("mf6", version="6.6.3", verbose=True)
# Switch versions
select_version("mf6", version="6.5.0")
# Get executable path
mf6_path = get_executable("mf6")
# List installed
installed = list_installed()
# Uninstall
uninstall_program("mf6", version="6.5.0")
Registry and Configuration API:
from modflow_devtools.programs import (
_DEFAULT_CACHE,
ProgramSourceConfig,
ProgramSourceRepo,
ProgramRegistry,
)
# Load configuration and sync
config = ProgramSourceConfig.load()
results = config.sync(verbose=True)
# Access cached registries
registry = _DEFAULT_CACHE.load("modflow6", "6.6.3")
programs = registry.programs # dict[str, ProgramMetadata]
# Work with specific sources
source = config.sources["modflow6"]
result = source.sync(ref="6.6.3", force=True, verbose=True)
Status and Next Steps
The Programs API is fully implemented. The next step is upstream integration: program repositories (starting with modflow6) should add registry generation to their CI workflows and publish registries as release assets. Once mature, pymake’s program database functionality can be deprecated.
Relationship to Models API
The Programs API deliberately mirrors the Models API architecture:
Aspect |
Models API |
Programs API |
|---|---|---|
Bootstrap file |
|
|
Registry format |
TOML with files/models/examples |
TOML with programs/binaries |
Discovery |
Release assets or version control |
Release assets only |
Caching |
|
|
Addressing |
|
|
CLI |
|
|
Key classes |
|
|
Key differences:
Programs API adds installation capabilities (Models API just provides file access)
Programs API handles platform-specific binaries (no building from source)
Programs have simpler addressing (just
program@version, no source or path components)Programs only use release asset discovery (no version-controlled registries)
Shared patterns:
Bootstrap-driven discovery
Remote sync with caching
Registry merging and composition
CLI command structure
Fallback to bundled data during migration
This consistency benefits both developers and users with a familiar experience across both APIs.
Relationship to get-modflow
The Programs API should eventually supersede flopy’s get-modflow utility. Many of its patterns are directly applicable and can be adapted or reused.
The Programs API incorporates key patterns from get-modflow:
Platform detection and OS tag mapping
Installation metadata tracking (JSON-based per-program tracking)
Writable directory discovery and bindir selection
Archive caching with hash verification
GitHub API interaction with token auth and retry logic
Executable permission handling
Key enhancements over get-modflow:
Registry-driven discovery: Use TOML registries instead of hard-coded repos
Multiple versions: Support side-by-side caching with fast version switching
Unified cache structure: Organize by program/version/platform hierarchy
Comprehensive metadata: Track all installations across different bindirs and versions
Users can migrate gradually - both tools can coexist during transition.
Cross-API Consistency
The Programs API follows the same design patterns as the Models and DFNs APIs for consistency. See the Cross-API Consistency section in models.md for full details.
Key shared patterns:
Pydantic-based registry classes (not ABCs)
Dynamic URL construction (URLs built at runtime, not stored in registries)
Bootstrap and user config files with identical naming (
programs.toml), distinguished by locationTop-level
schema_versionmetadata fieldDistinctly named registry file (
programs.toml)Shared config utility:
get_user_config_path("programs")
Unique to Programs API:
Discovery via release assets only (not version control)
Installation capabilities (binary downloads, version management)
No
MergedRegistry(program names globally unique)
Design Decisions
Initial implementation
These features are in scope for the initial implementation:
Multiple versions side-by-side: Users can install multiple versions of the same program in cache. Copy selected version to bindir to make it active. Fast version switching via re-copy from cache.
Installation metadata tracking: Maintain metadata about each installation (similar to flopy’s
get-modflow) to support executable discovery and version management.Executable discovery: Provide utilities to locate previously installed executables.
Platform error messages: When a platform-specific binary isn’t available, show helpful error messages indicating which platforms are supported.
PATH management: Support adding installed programs to PATH (similar to flopy’s
get-modflow).flopy integration: This API should eventually supersede flopy’s
get-modflowutility. See “Relationship to get-modflow” section for reusable patterns.
Explicitly out of scope
Cross-platform installations: No support for installing Windows binaries on Linux, etc.
Dependency handling: Programs don’t depend on each other, so no dependency modeling needed.
Mirror URLs: Use GitHub releases only (no mirror support).
Future enhancements
These features are desirable but can be added after the initial implementation:
Semantic version ranges: Support version specifiers like
mf6@^6.6to install any compatible version satisfying the range.Aliases and special versions: Support aliasing (e.g.,
mf6-latest→mf6@6.6.3) and special version identifiers likemf6@latestormf6@stable.Checksum/signature verification: Verify checksums or signatures on binary distributions for security and integrity.
Update notifications: Notify users when newer versions are available.