Initial commit
This commit is contained in:
197
README.md
Executable file
197
README.md
Executable file
@@ -0,0 +1,197 @@
|
|||||||
|
# Firedrake + icepack via conda
|
||||||
|
|
||||||
|
Install [Firedrake](https://www.firedrakeproject.org/) and [icepack](https://icepack.github.io/) into a self-contained conda environment with **no system packages required** beyond conda itself.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
This is a hybrid conda + pip approach:
|
||||||
|
|
||||||
|
- **conda** provides all the difficult-to-compile native dependencies: PETSc (with MUMPS, hypre, SuperLU, etc.), MPI, HDF5, BLAS/LAPACK, compilers, and the C toolchain needed for Firedrake's JIT compilation.
|
||||||
|
- **pip** installs Firedrake and its Python ecosystem (UFL, FIAT, TSFC, PyOP2, loopy) from git, since these are not on conda-forge.
|
||||||
|
- **Activation scripts** automatically set `PETSC_DIR`, `CC=mpicc`, `HDF5_MPI=ON`, and other environment variables when you `conda activate` so that Firedrake's runtime code generation compiles against the conda-provided libraries.
|
||||||
|
|
||||||
|
The key insight is that conda-forge's PETSc is built with all the external packages Firedrake needs, and PETSc installed in "prefix" mode (`PETSC_DIR=$CONDA_PREFIX`, no `PETSC_ARCH`) is compatible with how Firedrake discovers PETSc at runtime.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **conda** or **mamba** — [Install miniforge](https://github.com/conda-forge/miniforge) (recommended) or [miniconda](https://docs.conda.io/en/latest/miniconda.html)
|
||||||
|
- **Git** — needed to clone Firedrake repositories
|
||||||
|
- ~5 GB disk space for the full environment
|
||||||
|
- ~15 minutes for setup (mostly downloading conda packages)
|
||||||
|
|
||||||
|
No `sudo`, no system package manager, no Docker required.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <this-repo> firedrake-conda
|
||||||
|
cd firedrake-conda
|
||||||
|
bash setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
By default this creates a conda environment named `firedrake` with Firedrake
|
||||||
|
release **2025.10.2**. Both can be customised:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom environment name
|
||||||
|
bash setup.sh my-firedrake
|
||||||
|
|
||||||
|
# Environment at a specific path (uses conda -p)
|
||||||
|
bash setup.sh ./envs/firedrake
|
||||||
|
bash setup.sh ~/projects/icesheet/env
|
||||||
|
|
||||||
|
# Change Firedrake version
|
||||||
|
bash setup.sh firedrake main
|
||||||
|
bash setup.sh firedrake 2025.10.1
|
||||||
|
|
||||||
|
# Or parameterize using environment variables (takes precedence over commandline arguments)
|
||||||
|
FIREDRAKE_ENV=~/fd FIREDRAKE_REF=2025.10.2 bash setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
1. Create a conda environment (named or at a path) with all native dependencies
|
||||||
|
2. Set up activation scripts for the environment variables
|
||||||
|
3. Clone Firedrake and icepack with visible git progress
|
||||||
|
4. pip-install them, patching out conda-provided deps (petsc4py, mpi4py, h5py)
|
||||||
|
to avoid build failures
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate firedrake
|
||||||
|
python
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
import firedrake
|
||||||
|
import icepack
|
||||||
|
|
||||||
|
# Everything works as documented in icepack tutorials
|
||||||
|
mesh = firedrake.Mesh("glacier.msh")
|
||||||
|
Q = firedrake.FunctionSpace(mesh, "CG", 2)
|
||||||
|
V = firedrake.VectorFunctionSpace(mesh, "CG", 2)
|
||||||
|
|
||||||
|
model = icepack.models.IceStream()
|
||||||
|
solver = icepack.solvers.FlowSolver(model)
|
||||||
|
u = solver.diagnostic_solve(velocity=u0, thickness=h, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
The environment variables (`PETSC_DIR`, `CC`, `HDF5_MPI`, etc.) are automatically set when you `conda activate firedrake` and restored when you deactivate.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate firedrake
|
||||||
|
python verify.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This tests environment variables, library linking, Python imports, JIT compilation, a Poisson solve, and icepack model instantiation.
|
||||||
|
|
||||||
|
If something fails, run the diagnostic script for detailed information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash diagnose.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `pip install firedrake` fails to build extensions
|
||||||
|
|
||||||
|
This usually means the C compiler can't find PETSc headers. Check that:
|
||||||
|
```bash
|
||||||
|
echo $PETSC_DIR # should be $CONDA_PREFIX
|
||||||
|
echo $CC # should be mpicc
|
||||||
|
mpicc --version # should work and show conda's compiler
|
||||||
|
```
|
||||||
|
|
||||||
|
If these aren't set, deactivate and reactivate:
|
||||||
|
```bash
|
||||||
|
conda deactivate
|
||||||
|
conda activate firedrake
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import errors about missing symbols (`libpetsc.so`)
|
||||||
|
|
||||||
|
This means a Python extension was linked against a different PETSc than the one in the conda env. Fix by rebuilding the offending package:
|
||||||
|
```bash
|
||||||
|
pip cache purge
|
||||||
|
pip install --force-reinstall --no-binary :all: --no-build-isolation petsc4py
|
||||||
|
```
|
||||||
|
|
||||||
|
### `h5py` MPI errors
|
||||||
|
|
||||||
|
h5py must be built with MPI support. The conda environment provides this, but if pip reinstalls h5py it may pull a non-MPI wheel. Force the conda version:
|
||||||
|
```bash
|
||||||
|
conda install -c conda-forge "h5py=*=mpi_mpich_*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### JIT-compiled kernels segfault
|
||||||
|
|
||||||
|
This usually means the JIT compiler is using the system compiler instead of conda's `mpicc`. Verify:
|
||||||
|
```bash
|
||||||
|
which mpicc # should be in $CONDA_PREFIX/bin/
|
||||||
|
mpicc -show # should reference $CONDA_PREFIX libraries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
|
||||||
|
To update Firedrake and icepack while keeping conda dependencies:
|
||||||
|
```bash
|
||||||
|
conda activate firedrake
|
||||||
|
# Pull latest sources (or checkout a different tag)
|
||||||
|
cd ~/.firedrake-conda/clones/firedrake
|
||||||
|
git fetch --tags origin
|
||||||
|
git checkout 2025.10.2 # or: git checkout main && git pull
|
||||||
|
cd ~/.firedrake-conda/clones/icepack && git pull --progress
|
||||||
|
# Reinstall using the constraints file
|
||||||
|
pip install --upgrade --no-build-isolation \
|
||||||
|
-c $CONDA_PREFIX/constraints.txt \
|
||||||
|
~/.firedrake-conda/clones/firedrake ~/.firedrake-conda/clones/icepack
|
||||||
|
```
|
||||||
|
|
||||||
|
To update all conda dependencies (then regenerate constraints):
|
||||||
|
```bash
|
||||||
|
conda update --all -n firedrake
|
||||||
|
# Re-run setup.sh to regenerate constraints.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## What conda provides vs what pip provides
|
||||||
|
|
||||||
|
| Package | Source | Why |
|
||||||
|
|---------|--------|-----|
|
||||||
|
| PETSc + petsc4py | conda-forge | Complex build with MUMPS, hypre, etc. |
|
||||||
|
| SLEPc + slepc4py | conda-forge | Must match PETSc version |
|
||||||
|
| mpich + mpi4py | conda-forge | MPI ABI consistency |
|
||||||
|
| HDF5 + h5py | conda-forge | Must be MPI-enabled |
|
||||||
|
| compilers (gcc/clang) | conda-forge | Runtime JIT needs consistent ABI |
|
||||||
|
| BLAS/LAPACK | conda-forge | Linked by PETSc |
|
||||||
|
| numpy, scipy, sympy | conda-forge | Standard scientific Python |
|
||||||
|
| islpy | conda-forge | Has compiled C extension |
|
||||||
|
| **firedrake** | **pip (git)** | Not on conda-forge |
|
||||||
|
| fenics-ufl | pip (git, via firedrake) | Firedrake tracks development branch |
|
||||||
|
| firedrake-fiat | pip (git, via firedrake) | Firedrake's own fork |
|
||||||
|
| tsfc, pyop2, finat | pip (via firedrake) | Part of Firedrake monorepo |
|
||||||
|
| loopy | pip (via firedrake) | Code generation stack |
|
||||||
|
| libsupermesh | pip (git) | Small C lib, builds against conda MPI |
|
||||||
|
| **icepack** | **pip (git)** | Pure Python, depends on firedrake |
|
||||||
|
|
||||||
|
## Platform support
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux x86_64 | Primary | Best tested |
|
||||||
|
| macOS ARM64 (Apple Silicon) | Supported | Uses clang from conda |
|
||||||
|
| macOS x86_64 (Intel) | Untested | Should work but Firedrake dropped official support |
|
||||||
|
| Linux aarch64 | Should work | conda-forge has ARM builds |
|
||||||
|
| Windows (WSL2) | Should work | Via WSL2's Linux environment |
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
firedrake-conda/
|
||||||
|
├── environment.yml # Conda env spec (all native deps)
|
||||||
|
├── setup.sh # One-command setup script
|
||||||
|
├── verify.py # Post-install verification
|
||||||
|
├── diagnose.sh # Detailed diagnostics for debugging
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
191
diagnose.sh
Executable file
191
diagnose.sh
Executable file
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# diagnose.sh — Diagnose linking and configuration issues
|
||||||
|
#
|
||||||
|
# Run this if verify.py fails. It prints detailed information about
|
||||||
|
# the environment that helps debug library conflicts.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# conda activate firedrake
|
||||||
|
# bash diagnose.sh
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
echo "Firedrake conda environment diagnostics"
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Platform ─────────────────────────────────────────────────"
|
||||||
|
echo "OS: $(uname -s)"
|
||||||
|
echo "Arch: $(uname -m)"
|
||||||
|
echo "Kernel: $(uname -r)"
|
||||||
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||||
|
echo "Distro: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
|
||||||
|
echo "glibc: $(ldd --version 2>&1 | head -1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Conda ────────────────────────────────────────────────────"
|
||||||
|
echo "conda: $(conda --version 2>/dev/null || echo 'not found')"
|
||||||
|
echo "mamba: $(mamba --version 2>/dev/null | head -1 || echo 'not found')"
|
||||||
|
echo "CONDA_PREFIX: ${CONDA_PREFIX:-not set}"
|
||||||
|
echo "Active env: ${CONDA_DEFAULT_ENV:-none}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Key environment variables ────────────────────────────────"
|
||||||
|
for var in PETSC_DIR PETSC_ARCH CC CXX FC HDF5_MPI HDF5_DIR \
|
||||||
|
PYOP2_CFLAGS PYOP2_LDFLAGS OMP_NUM_THREADS \
|
||||||
|
LD_LIBRARY_PATH DYLD_LIBRARY_PATH PATH; do
|
||||||
|
val="${!var:-<unset>}"
|
||||||
|
# Truncate long PATH values
|
||||||
|
if [[ ${#val} -gt 120 ]]; then
|
||||||
|
val="${val:0:120}..."
|
||||||
|
fi
|
||||||
|
printf " %-22s %s\n" "$var" "$val"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Compilers ────────────────────────────────────────────────"
|
||||||
|
for cmd in cc gcc mpicc mpicxx mpifort; do
|
||||||
|
loc=$(which "$cmd" 2>/dev/null || echo "not found")
|
||||||
|
printf " %-10s %s\n" "$cmd" "$loc"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── MPI ────────────────────────────────────────────────────────"
|
||||||
|
if command -v mpicc &> /dev/null; then
|
||||||
|
echo "mpicc -show:"
|
||||||
|
mpicc -show 2>/dev/null || mpicc --showme 2>/dev/null || echo " (could not determine)"
|
||||||
|
fi
|
||||||
|
if command -v mpiexec &> /dev/null; then
|
||||||
|
mpiexec --version 2>&1 | head -3
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Shared libraries in CONDA_PREFIX/lib ─────────────────────"
|
||||||
|
if [[ -n "${CONDA_PREFIX:-}" ]]; then
|
||||||
|
for lib in libpetsc libslepc libmpi libhdf5 libopenblas libsuperlu_dist \
|
||||||
|
libdmumps libscotch libmetis libparmetis; do
|
||||||
|
found=$(ls "${CONDA_PREFIX}/lib/${lib}"* 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$found" ]]; then
|
||||||
|
printf " %-25s %s\n" "$lib" "$(basename "$found")"
|
||||||
|
else
|
||||||
|
printf " %-25s %s\n" "$lib" "NOT FOUND"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Python packages ──────────────────────────────────────────"
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
print(f' Python: {sys.version}')
|
||||||
|
print(f' Prefix: {sys.prefix}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
pkgs = [
|
||||||
|
'numpy', 'mpi4py', 'petsc4py', 'slepc4py', 'h5py',
|
||||||
|
'ufl', 'FIAT', 'finat', 'tsfc', 'loopy', 'pyop2',
|
||||||
|
'firedrake', 'icepack',
|
||||||
|
]
|
||||||
|
for name in pkgs:
|
||||||
|
try:
|
||||||
|
mod = __import__(name)
|
||||||
|
ver = getattr(mod, '__version__', '?')
|
||||||
|
loc = getattr(mod, '__file__', '?')
|
||||||
|
# Shorten path
|
||||||
|
loc = loc.replace(sys.prefix, '\$PREFIX')
|
||||||
|
print(f' {name:15s} {ver:20s} {loc}')
|
||||||
|
except ImportError as e:
|
||||||
|
print(f' {name:15s} IMPORT FAILED: {e}')
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── petsc4py linking ─────────────────────────────────────────"
|
||||||
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||||
|
PETSC4PY_SO=$(python3 -c "import petsc4py; print(petsc4py.__file__)" 2>/dev/null)
|
||||||
|
if [[ -n "$PETSC4PY_SO" ]]; then
|
||||||
|
PETSC4PY_DIR=$(dirname "$PETSC4PY_SO")
|
||||||
|
SO_FILE=$(find "$PETSC4PY_DIR" -name "PETSc*.so" 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$SO_FILE" ]]; then
|
||||||
|
echo " petsc4py .so: $SO_FILE"
|
||||||
|
echo " Links against:"
|
||||||
|
LD_LIBRARY_PATH="${CONDA_PREFIX}/lib:${LD_LIBRARY_PATH:-}" ldd "$SO_FILE" 2>/dev/null | grep -E "petsc|mpi|hdf5|blas|lapack" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
PETSC4PY_SO=$(python3 -c "import petsc4py; print(petsc4py.__file__)" 2>/dev/null)
|
||||||
|
if [[ -n "$PETSC4PY_SO" ]]; then
|
||||||
|
PETSC4PY_DIR=$(dirname "$PETSC4PY_SO")
|
||||||
|
SO_FILE=$(find "$PETSC4PY_DIR" -name "PETSc*.so" -o -name "PETSc*.dylib" 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$SO_FILE" ]]; then
|
||||||
|
echo " petsc4py .so: $SO_FILE"
|
||||||
|
echo " Links against:"
|
||||||
|
otool -L "$SO_FILE" 2>/dev/null | grep -E "petsc|mpi|hdf5|blas|lapack" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── JIT compilation test ─────────────────────────────────────"
|
||||||
|
python3 -c "
|
||||||
|
import os, tempfile, subprocess, sys
|
||||||
|
|
||||||
|
cc = os.environ.get('CC', 'mpicc')
|
||||||
|
prefix = os.environ.get('CONDA_PREFIX', '')
|
||||||
|
|
||||||
|
# Write a minimal C file that links against PETSc
|
||||||
|
src = '''
|
||||||
|
#include <petsc.h>
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
PetscInitialize(&argc, &argv, NULL, NULL);
|
||||||
|
PetscPrintf(PETSC_COMM_WORLD, \"PETSc JIT test OK\\\\n\");
|
||||||
|
PetscFinalize();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.c', mode='w', delete=False) as f:
|
||||||
|
f.write(src)
|
||||||
|
src_path = f.name
|
||||||
|
|
||||||
|
out_path = src_path.replace('.c', '')
|
||||||
|
|
||||||
|
# Build command similar to what PyOP2 would use
|
||||||
|
cmd = [
|
||||||
|
cc,
|
||||||
|
src_path,
|
||||||
|
'-o', out_path,
|
||||||
|
f'-I{prefix}/include',
|
||||||
|
f'-L{prefix}/lib',
|
||||||
|
'-lpetsc',
|
||||||
|
f'-Wl,-rpath,{prefix}/lib',
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f' Compile command: {\" \".join(cmd)}')
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(' ✓ Compilation succeeded')
|
||||||
|
run = subprocess.run([out_path], capture_output=True, text=True)
|
||||||
|
if run.returncode == 0:
|
||||||
|
print(f' ✓ Execution succeeded: {run.stdout.strip()}')
|
||||||
|
else:
|
||||||
|
print(f' ✗ Execution failed: {run.stderr.strip()}')
|
||||||
|
else:
|
||||||
|
print(f' ✗ Compilation failed:')
|
||||||
|
print(f' stdout: {result.stdout.strip()}')
|
||||||
|
print(f' stderr: {result.stderr.strip()}')
|
||||||
|
|
||||||
|
os.unlink(src_path)
|
||||||
|
try:
|
||||||
|
os.unlink(out_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
" 2>&1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
echo "Done. If issues persist, share this output when seeking help."
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
98
environment.yml
Executable file
98
environment.yml
Executable file
@@ -0,0 +1,98 @@
|
|||||||
|
# Firedrake + icepack conda environment
|
||||||
|
#
|
||||||
|
# This provides all the compiled/native dependencies (PETSc, MPI, compilers,
|
||||||
|
# etc.) from conda-forge so that no system packages are needed beyond conda
|
||||||
|
# itself. Firedrake's own Python packages are then pip-installed on top.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# conda env create -f environment.yml
|
||||||
|
# conda activate firedrake
|
||||||
|
# bash setup.sh
|
||||||
|
#
|
||||||
|
# Tested on: Linux x86_64, macOS ARM64 (Apple Silicon)
|
||||||
|
|
||||||
|
name: firedrake
|
||||||
|
channels:
|
||||||
|
- conda-forge
|
||||||
|
- nodefaults
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
# ── Python ──
|
||||||
|
- python >=3.11,<3.13
|
||||||
|
|
||||||
|
# ── Compilers (needed at runtime for Firedrake JIT) ──
|
||||||
|
# The `compilers` metapackage provides gcc/gfortran on Linux and
|
||||||
|
# clang/gfortran on macOS, all targeting the conda env's sysroot.
|
||||||
|
- compilers
|
||||||
|
- cmake
|
||||||
|
- make
|
||||||
|
|
||||||
|
# ── MPI ──
|
||||||
|
# mpich is more portable across platforms in conda-forge than openmpi.
|
||||||
|
# All MPI-linked packages below will be built against this same mpich.
|
||||||
|
- mpich
|
||||||
|
|
||||||
|
# ── PETSc ecosystem ──
|
||||||
|
# conda-forge PETSc includes: MUMPS, hypre, SuperLU, SuperLU_dist,
|
||||||
|
# METIS, ParMETIS, ScaLAPACK, SuiteSparse, pt-scotch.
|
||||||
|
- petsc =*=real*
|
||||||
|
- petsc4py
|
||||||
|
- slepc
|
||||||
|
- slepc4py
|
||||||
|
|
||||||
|
# ── HDF5 with MPI support ──
|
||||||
|
- hdf5 =*=mpi_mpich_*
|
||||||
|
- h5py =*=mpi_mpich_*
|
||||||
|
|
||||||
|
# ── Linear algebra ──
|
||||||
|
- libopenblas
|
||||||
|
- libblas =*=*openblas*
|
||||||
|
- liblapack =*=*openblas*
|
||||||
|
- scalapack
|
||||||
|
|
||||||
|
# ── Other compiled libs Firedrake needs ──
|
||||||
|
- patchelf # [linux]
|
||||||
|
- libspatialindex
|
||||||
|
- gmsh
|
||||||
|
|
||||||
|
# ── Core Python dependencies ──
|
||||||
|
- numpy
|
||||||
|
- scipy
|
||||||
|
- cython
|
||||||
|
- mpi4py
|
||||||
|
- sympy
|
||||||
|
- cachetools
|
||||||
|
- packaging
|
||||||
|
- decorator
|
||||||
|
- requests
|
||||||
|
- setuptools
|
||||||
|
- pip
|
||||||
|
- wheel
|
||||||
|
- meson
|
||||||
|
- meson-python
|
||||||
|
- pkgconfig
|
||||||
|
- scikit-build-core
|
||||||
|
- ninja
|
||||||
|
- pybind11
|
||||||
|
- hatchling
|
||||||
|
- flit-core
|
||||||
|
|
||||||
|
# ── islpy / loopy code generation stack ──
|
||||||
|
- islpy
|
||||||
|
|
||||||
|
# ── Visualization (optional but useful) ──
|
||||||
|
- matplotlib
|
||||||
|
|
||||||
|
# ── Pip-installed packages ──
|
||||||
|
# These are the Firedrake-specific packages that are NOT on conda-forge.
|
||||||
|
# They're installed in the setup script, listed here for documentation.
|
||||||
|
# - pip:
|
||||||
|
# - fenics-ufl (from git, Firedrake-compatible version)
|
||||||
|
# - firedrake-fiat (Firedrake's FIAT fork)
|
||||||
|
# - FInAT
|
||||||
|
# - loopy (latest, for code generation)
|
||||||
|
# - tsfc
|
||||||
|
# - pyop2
|
||||||
|
# - libsupermesh
|
||||||
|
# - firedrake
|
||||||
|
# - icepack
|
||||||
575
setup.sh
Executable file
575
setup.sh
Executable file
@@ -0,0 +1,575 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# setup.sh — Install Firedrake + icepack into a conda environment
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Creates a conda environment with all native dependencies from conda-forge
|
||||||
|
# 2. Sets environment variables so Firedrake finds conda's PETSc, MPI, etc.
|
||||||
|
# 3. pip-installs Firedrake and icepack (skipping binary builds for
|
||||||
|
# MPI-linked packages so they compile against conda's libraries)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash setup.sh # env named "firedrake"
|
||||||
|
# bash setup.sh my-env # env named "my-env"
|
||||||
|
# bash setup.sh ./envs/firedrake # env at path ./envs/firedrake
|
||||||
|
# FIREDRAKE_ENV=~/fd FIREDRAKE_REF=main bash setup.sh
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - conda or mamba installed
|
||||||
|
# - internet access (to download packages)
|
||||||
|
#
|
||||||
|
# Tested on: Linux x86_64, macOS ARM64
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Environment name or path — accepts:
|
||||||
|
# - A simple name like "firedrake" → conda env -n firedrake
|
||||||
|
# - An absolute/relative path like "./envs/fd" → conda env -p ./envs/fd
|
||||||
|
# Set via FIREDRAKE_ENV env var, first positional arg, or default "firedrake".
|
||||||
|
ENV_TARGET="${FIREDRAKE_ENV:-${1:-firedrake}}"
|
||||||
|
|
||||||
|
# Detect if it's a path (contains / or starts with .)
|
||||||
|
if [[ "$ENV_TARGET" == */* ]] || [[ "$ENV_TARGET" == .* ]]; then
|
||||||
|
# Resolve to absolute path, creating parent dir if needed
|
||||||
|
_parent="$(dirname "$ENV_TARGET")"
|
||||||
|
mkdir -p "$_parent" 2>/dev/null || true
|
||||||
|
ENV_TARGET="$(cd "$_parent" && pwd)/$(basename "$ENV_TARGET")"
|
||||||
|
ENV_IS_PATH=true
|
||||||
|
CONDA_ENV_FLAG="-p"
|
||||||
|
else
|
||||||
|
ENV_IS_PATH=false
|
||||||
|
CONDA_ENV_FLAG="-n"
|
||||||
|
fi
|
||||||
|
ENV_DISPLAY="$ENV_TARGET"
|
||||||
|
ENV_ACTIVATE="$ENV_TARGET"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Firedrake version: set to a release tag (e.g. "2025.10.2") or "main"
|
||||||
|
FIREDRAKE_REF="${FIREDRAKE_REF:-${2:-2025.10.2}}"
|
||||||
|
|
||||||
|
# Prefer mamba if available (much faster solver)
|
||||||
|
if command -v mamba &> /dev/null; then
|
||||||
|
CONDA=mamba
|
||||||
|
elif command -v conda &> /dev/null; then
|
||||||
|
CONDA=conda
|
||||||
|
else
|
||||||
|
echo "ERROR: Neither conda nor mamba found. Please install one first."
|
||||||
|
echo " See: https://docs.conda.io/en/latest/miniconda.html"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Colors ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m' # No color
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; }
|
||||||
|
|
||||||
|
# ── Live rolling tail ─────────────────────────────────────────────────────
|
||||||
|
# Runs a command while showing a live, continuously-updating view of the
|
||||||
|
# last N lines of its combined stdout/stderr. When the command finishes,
|
||||||
|
# the rolling display is cleared and replaced with a summary.
|
||||||
|
#
|
||||||
|
# Usage: run_live <n_lines> <description> <command...>
|
||||||
|
# Returns the exit code of the command.
|
||||||
|
|
||||||
|
TAIL_LINES=20
|
||||||
|
|
||||||
|
run_live() {
|
||||||
|
local nlines="$1"; shift
|
||||||
|
local desc="$1"; shift
|
||||||
|
|
||||||
|
local logfile
|
||||||
|
logfile=$(mktemp /tmp/firedrake-install-XXXXXX.log)
|
||||||
|
|
||||||
|
echo -e "${DIM}─── ${desc} ───${NC}"
|
||||||
|
|
||||||
|
# Run the actual command in the background, directing all output to logfile
|
||||||
|
"$@" > "$logfile" 2>&1 &
|
||||||
|
local cmd_pid=$!
|
||||||
|
|
||||||
|
# Tail the logfile in real-time, showing last N lines with in-place rewriting
|
||||||
|
python3 -u -c "
|
||||||
|
import sys, os, time
|
||||||
|
|
||||||
|
nlines = int(sys.argv[1])
|
||||||
|
path = sys.argv[2]
|
||||||
|
pid = int(sys.argv[3])
|
||||||
|
cols = int(os.environ.get('COLUMNS', 120))
|
||||||
|
|
||||||
|
buf = []
|
||||||
|
shown = 0
|
||||||
|
|
||||||
|
def render():
|
||||||
|
global shown
|
||||||
|
# Move up to overwrite previous output
|
||||||
|
if shown > 0:
|
||||||
|
sys.stdout.write(f'\033[{shown}A')
|
||||||
|
shown = 0
|
||||||
|
for b in buf:
|
||||||
|
sys.stdout.write(f'\033[2K {b}\n')
|
||||||
|
shown += 1
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def pid_alive(p):
|
||||||
|
try:
|
||||||
|
os.kill(p, 0)
|
||||||
|
return True
|
||||||
|
except ProcessLookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if line:
|
||||||
|
line = line.rstrip('\n')[:cols]
|
||||||
|
buf.append(line)
|
||||||
|
if len(buf) > nlines:
|
||||||
|
buf = buf[-nlines:]
|
||||||
|
render()
|
||||||
|
else:
|
||||||
|
# No new data — check if process is still running
|
||||||
|
if not pid_alive(pid):
|
||||||
|
# Drain any remaining lines
|
||||||
|
for line in f:
|
||||||
|
line = line.rstrip('\n')[:cols]
|
||||||
|
buf.append(line)
|
||||||
|
if len(buf) > nlines:
|
||||||
|
buf = buf[-nlines:]
|
||||||
|
render()
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Clear the rolling display
|
||||||
|
if shown > 0:
|
||||||
|
sys.stdout.write(f'\033[{shown}A')
|
||||||
|
for _ in range(shown):
|
||||||
|
sys.stdout.write('\033[2K\n')
|
||||||
|
sys.stdout.write(f'\033[{shown}A')
|
||||||
|
sys.stdout.flush()
|
||||||
|
" "$nlines" "$logfile" "$cmd_pid"
|
||||||
|
|
||||||
|
# Collect the exit code
|
||||||
|
local exit_code=0
|
||||||
|
wait "$cmd_pid" || exit_code=$?
|
||||||
|
|
||||||
|
if [[ $exit_code -ne 0 ]]; then
|
||||||
|
warn "${desc} failed (exit code ${exit_code}). Full log: ${logfile}"
|
||||||
|
echo -e "${DIM}─── Last 30 lines of ${logfile} ───${NC}"
|
||||||
|
tail -30 "$logfile"
|
||||||
|
echo -e "${DIM}────────────────────────────────────${NC}"
|
||||||
|
else
|
||||||
|
rm -f "$logfile"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $exit_code
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Step 1: Create conda environment ──────────────────────────────────────
|
||||||
|
|
||||||
|
info "Creating conda environment '${ENV_DISPLAY}' from environment.yml ..."
|
||||||
|
|
||||||
|
# Check if environment already exists
|
||||||
|
ENV_EXISTS=false
|
||||||
|
if [[ "$ENV_IS_PATH" == true ]]; then
|
||||||
|
[[ -d "${ENV_TARGET}/conda-meta" ]] && ENV_EXISTS=true
|
||||||
|
else
|
||||||
|
$CONDA env list 2>/dev/null | grep -q "^${ENV_TARGET} " && ENV_EXISTS=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ENV_EXISTS" == true ]]; then
|
||||||
|
warn "Environment '${ENV_DISPLAY}' already exists."
|
||||||
|
read -p " Recreate it? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
$CONDA env remove ${CONDA_ENV_FLAG} "${ENV_TARGET}" -y
|
||||||
|
else
|
||||||
|
info "Reusing existing environment."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
$CONDA env create ${CONDA_ENV_FLAG} "${ENV_TARGET}" -f "${SCRIPT_DIR}/environment.yml" || \
|
||||||
|
$CONDA env update ${CONDA_ENV_FLAG} "${ENV_TARGET}" -f "${SCRIPT_DIR}/environment.yml"
|
||||||
|
|
||||||
|
ok "Conda environment created."
|
||||||
|
|
||||||
|
# ── Step 2: Activate and detect paths ─────────────────────────────────────
|
||||||
|
|
||||||
|
# We need to source conda's shell hook to use `conda activate` in a script
|
||||||
|
eval "$(conda shell.bash hook)"
|
||||||
|
conda activate "${ENV_ACTIVATE}"
|
||||||
|
|
||||||
|
CONDA_PREFIX="${CONDA_PREFIX}"
|
||||||
|
info "CONDA_PREFIX = ${CONDA_PREFIX}"
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
OS="$(uname -s)"
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
info "Platform: ${OS} ${ARCH}"
|
||||||
|
|
||||||
|
if [[ "$OS" == "Darwin" ]]; then
|
||||||
|
SOEXT="dylib"
|
||||||
|
else
|
||||||
|
SOEXT="so"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 3: Verify conda packages ────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Verifying critical conda packages ..."
|
||||||
|
|
||||||
|
# Check PETSc
|
||||||
|
if [[ ! -f "${CONDA_PREFIX}/lib/libpetsc.${SOEXT}" ]]; then
|
||||||
|
fail "PETSc shared library not found at ${CONDA_PREFIX}/lib/libpetsc.${SOEXT}"
|
||||||
|
fi
|
||||||
|
ok "PETSc found."
|
||||||
|
|
||||||
|
# Check MPI compiler wrappers
|
||||||
|
if ! command -v mpicc &> /dev/null; then
|
||||||
|
fail "mpicc not found. MPI compiler wrappers are missing."
|
||||||
|
fi
|
||||||
|
ok "MPI compilers found: $(which mpicc)"
|
||||||
|
|
||||||
|
# Check HDF5
|
||||||
|
if [[ ! -f "${CONDA_PREFIX}/lib/libhdf5.${SOEXT}" ]]; then
|
||||||
|
fail "HDF5 not found."
|
||||||
|
fi
|
||||||
|
ok "HDF5 found."
|
||||||
|
|
||||||
|
# ── Step 4: Set environment variables ─────────────────────────────────────
|
||||||
|
|
||||||
|
info "Setting environment variables for Firedrake ..."
|
||||||
|
|
||||||
|
# PETSc: conda installs in "prefix" mode (no PETSC_ARCH)
|
||||||
|
export PETSC_DIR="${CONDA_PREFIX}"
|
||||||
|
export PETSC_ARCH=""
|
||||||
|
|
||||||
|
# Compilers: use MPI wrappers from conda
|
||||||
|
export CC=mpicc
|
||||||
|
export CXX=mpicxx
|
||||||
|
export FC=mpifort
|
||||||
|
|
||||||
|
# HDF5: tell h5py and Firedrake to use MPI-enabled HDF5
|
||||||
|
export HDF5_MPI="ON"
|
||||||
|
export HDF5_DIR="${CONDA_PREFIX}"
|
||||||
|
|
||||||
|
# Firedrake recommends single-threaded BLAS
|
||||||
|
export OMP_NUM_THREADS=1
|
||||||
|
|
||||||
|
# PyOP2 JIT: conda's mpicc wraps x86_64-conda-linux-gnu-cc which PyOP2
|
||||||
|
# doesn't recognise → falls back to generic compiler without -shared/-fPIC
|
||||||
|
export PYOP2_CFLAGS="-fPIC -O2"
|
||||||
|
export PYOP2_LDFLAGS="-shared -Wl,-rpath,${CONDA_PREFIX}/lib"
|
||||||
|
|
||||||
|
# Tell pip not to use cached wheels for MPI-linked packages
|
||||||
|
# (they may be linked against the wrong MPI/PETSc)
|
||||||
|
export PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
info " PETSC_DIR = ${PETSC_DIR}"
|
||||||
|
info " PETSC_ARCH = (empty — prefix install)"
|
||||||
|
info " CC = ${CC}"
|
||||||
|
info " HDF5_MPI = ${HDF5_MPI}"
|
||||||
|
|
||||||
|
# ── Step 5: Pip install Firedrake ecosystem ───────────────────────────────
|
||||||
|
|
||||||
|
info "Installing Firedrake and dependencies via pip ..."
|
||||||
|
info "This may take several minutes (compiling C extensions) ..."
|
||||||
|
|
||||||
|
# Purge pip cache to avoid stale binaries linked against wrong libs
|
||||||
|
pip cache purge 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install packages that have C extensions and must link against conda's libs.
|
||||||
|
# We use --no-binary to force compilation against the conda environment.
|
||||||
|
# The order matters: dependencies first, then Firedrake, then icepack.
|
||||||
|
|
||||||
|
info " [1/5] Installing build dependencies ..."
|
||||||
|
# These are needed as build backends / build-time deps by libsupermesh
|
||||||
|
# and firedrake. We install them now so --no-build-isolation works
|
||||||
|
# (which ensures C extensions link against conda's MPI/PETSc, not
|
||||||
|
# some isolated copy).
|
||||||
|
# Install ALL build backends that Firedrake's dependency tree uses.
|
||||||
|
# --no-build-isolation means pip won't auto-install these, so we need
|
||||||
|
# them all present before building anything.
|
||||||
|
run_live $TAIL_LINES "Installing build backends" \
|
||||||
|
pip install \
|
||||||
|
setuptools wheel \
|
||||||
|
scikit-build-core \
|
||||||
|
meson-python meson ninja \
|
||||||
|
hatchling hatch-vcs hatch-fancy-pypi-readme \
|
||||||
|
flit-core \
|
||||||
|
cython \
|
||||||
|
pybind11 \
|
||||||
|
petsctools
|
||||||
|
ok "Build dependencies installed."
|
||||||
|
|
||||||
|
info " [2/5] Installing mpi4py, petsc4py, h5py (from source against conda libs) ..."
|
||||||
|
# These should already be installed by conda, but if pip needs to reinstall
|
||||||
|
# them (e.g. for Firedrake version constraints), force source builds.
|
||||||
|
# Usually conda's versions are fine and pip will skip them.
|
||||||
|
|
||||||
|
info " [3/5] Installing libsupermesh ..."
|
||||||
|
run_live $TAIL_LINES "libsupermesh" \
|
||||||
|
pip install --no-build-isolation libsupermesh || {
|
||||||
|
warn "libsupermesh pip install failed, trying from git ..."
|
||||||
|
run_live $TAIL_LINES "libsupermesh (git)" \
|
||||||
|
pip install --no-build-isolation \
|
||||||
|
"libsupermesh @ git+https://github.com/firedrakeproject/libsupermesh.git"
|
||||||
|
}
|
||||||
|
ok "libsupermesh installed."
|
||||||
|
|
||||||
|
info " [4/5] Installing Firedrake (${FIREDRAKE_REF}) ..."
|
||||||
|
# Firedrake's pyproject.toml lists dependencies like petsc4py, mpi4py, h5py
|
||||||
|
# as git URLs. Conda already provides working versions of these, so we
|
||||||
|
# generate a pip constraints file that pins them to the conda-installed
|
||||||
|
# versions. This prevents pip from cloning & rebuilding petsc4py (which
|
||||||
|
# fails on release tags due to a setuptools dry_run incompatibility).
|
||||||
|
#
|
||||||
|
# Clone first so the user can see git progress (pip hides it entirely).
|
||||||
|
# Clones live in ~/.firedrake-conda/clones/ so they persist across
|
||||||
|
# environment recreations (conda env remove + re-create).
|
||||||
|
CLONE_DIR="${HOME}/.firedrake-conda/clones"
|
||||||
|
mkdir -p "${CLONE_DIR}"
|
||||||
|
|
||||||
|
info " Cloning Firedrake (${FIREDRAKE_REF}) ..."
|
||||||
|
if [[ -d "${CLONE_DIR}/firedrake" ]]; then
|
||||||
|
info " (updating existing clone)"
|
||||||
|
# Unshallow if needed (shallow clones can't switch tags)
|
||||||
|
if [[ -f "${CLONE_DIR}/firedrake/.git/shallow" ]]; then
|
||||||
|
git -C "${CLONE_DIR}/firedrake" fetch --progress --unshallow --tags origin 2>&1 || \
|
||||||
|
git -C "${CLONE_DIR}/firedrake" fetch --progress --tags origin 2>&1
|
||||||
|
else
|
||||||
|
git -C "${CLONE_DIR}/firedrake" fetch --progress --tags origin 2>&1
|
||||||
|
fi
|
||||||
|
git -C "${CLONE_DIR}/firedrake" checkout "${FIREDRAKE_REF}" 2>&1
|
||||||
|
if [[ "${FIREDRAKE_REF}" == "main" ]]; then
|
||||||
|
git -C "${CLONE_DIR}/firedrake" reset --hard origin/main
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Full clone for tags; shallow clone for main
|
||||||
|
if [[ "${FIREDRAKE_REF}" == "main" ]]; then
|
||||||
|
git clone --progress --depth 1 --branch main \
|
||||||
|
https://github.com/firedrakeproject/firedrake.git \
|
||||||
|
"${CLONE_DIR}/firedrake" 2>&1
|
||||||
|
else
|
||||||
|
git clone --progress --branch "${FIREDRAKE_REF}" \
|
||||||
|
https://github.com/firedrakeproject/firedrake.git \
|
||||||
|
"${CLONE_DIR}/firedrake" 2>&1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Generate pip constraints file ──────────────────────────────────────
|
||||||
|
# Pin every conda-provided package so pip doesn't try to rebuild them
|
||||||
|
# from git URLs in Firedrake's pyproject.toml.
|
||||||
|
info " Generating pip constraints for conda packages ..."
|
||||||
|
CONSTRAINTS="${CONDA_PREFIX}/constraints.txt"
|
||||||
|
python3 - <<'PYEOF' > "${CONSTRAINTS}"
|
||||||
|
import importlib.metadata, sys
|
||||||
|
|
||||||
|
# Packages that conda provides and pip must not rebuild
|
||||||
|
CONDA_OWNS = [
|
||||||
|
"petsc4py", "slepc4py", "mpi4py", "h5py", "numpy", "scipy",
|
||||||
|
"sympy", "cython", "pybind11", "pkgconfig", "rtree",
|
||||||
|
"scikit-build-core", "hatchling", "flit-core", "setuptools",
|
||||||
|
]
|
||||||
|
|
||||||
|
for pkg in CONDA_OWNS:
|
||||||
|
try:
|
||||||
|
version = importlib.metadata.version(pkg)
|
||||||
|
print(f"{pkg}=={version}")
|
||||||
|
except importlib.metadata.PackageNotFoundError:
|
||||||
|
pass
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
info " Constraints file:"
|
||||||
|
cat "${CONSTRAINTS}" | sed 's/^/ /'
|
||||||
|
|
||||||
|
# ── Patch pyproject.toml to remove conda-provided deps ─────────────────
|
||||||
|
# Firedrake's release tags may list petsc4py, slepc4py, mpi4py, h5py as
|
||||||
|
# git URL deps (@ git+https://...) or version pins. Pip can't satisfy
|
||||||
|
# direct URL refs from conda, and tries to rebuild them — which fails
|
||||||
|
# (e.g. petsc4py's setuptools dry_run bug). We strip these deps since
|
||||||
|
# conda already provides them.
|
||||||
|
info " Patching pyproject.toml to skip conda-provided deps ..."
|
||||||
|
PYPROJECT="${CLONE_DIR}/firedrake/pyproject.toml"
|
||||||
|
python3 - "${PYPROJECT}" <<'PYEOF'
|
||||||
|
import re, sys
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
text = open(path).read()
|
||||||
|
|
||||||
|
# Packages conda provides — remove any line in dependencies[] that
|
||||||
|
# starts with these names (handles version pins AND @ git+... URLs)
|
||||||
|
CONDA_PKGS = ["petsc4py", "slepc4py", "mpi4py", "h5py"]
|
||||||
|
|
||||||
|
lines = text.split("\n")
|
||||||
|
patched = []
|
||||||
|
removed = []
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip().strip('"').strip("'").strip(",").strip()
|
||||||
|
skip = False
|
||||||
|
for pkg in CONDA_PKGS:
|
||||||
|
# Match "petsc4py...", " petsc4py...", '"petsc4py...', etc.
|
||||||
|
if re.match(rf"^{re.escape(pkg)}(\s|>|<|=|@|;|$)", stripped):
|
||||||
|
skip = True
|
||||||
|
removed.append(stripped)
|
||||||
|
break
|
||||||
|
if not skip:
|
||||||
|
patched.append(line)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
open(path, "w").write("\n".join(patched))
|
||||||
|
for r in removed:
|
||||||
|
print(f" Removed: {r}")
|
||||||
|
else:
|
||||||
|
print(" (no conda-provided deps found to remove)")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# ── pip install with constraints ───────────────────────────────────────
|
||||||
|
info " pip install Firedrake (from local clone) ..."
|
||||||
|
run_live $TAIL_LINES "Firedrake" \
|
||||||
|
pip install --no-build-isolation -c "${CONSTRAINTS}" "${CLONE_DIR}/firedrake"
|
||||||
|
ok "Firedrake installed."
|
||||||
|
|
||||||
|
info " [5/5] Installing icepack ..."
|
||||||
|
info " Cloning icepack ..."
|
||||||
|
if [[ -d "${CLONE_DIR}/icepack" ]]; then
|
||||||
|
info " (updating existing clone)"
|
||||||
|
git -C "${CLONE_DIR}/icepack" pull --progress 2>&1
|
||||||
|
else
|
||||||
|
git clone --progress --depth 1 \
|
||||||
|
https://github.com/icepack/icepack.git \
|
||||||
|
"${CLONE_DIR}/icepack" 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info " pip install icepack (from local clone) ..."
|
||||||
|
run_live $TAIL_LINES "icepack" \
|
||||||
|
pip install --no-build-isolation -c "${CONSTRAINTS}" "${CLONE_DIR}/icepack"
|
||||||
|
ok "icepack installed."
|
||||||
|
|
||||||
|
# ── Step 6: Install activation script ────────────────────────────────────
|
||||||
|
|
||||||
|
info "Installing conda activation/deactivation scripts ..."
|
||||||
|
|
||||||
|
ACTIVATE_DIR="${CONDA_PREFIX}/etc/conda/activate.d"
|
||||||
|
DEACTIVATE_DIR="${CONDA_PREFIX}/etc/conda/deactivate.d"
|
||||||
|
mkdir -p "${ACTIVATE_DIR}" "${DEACTIVATE_DIR}"
|
||||||
|
|
||||||
|
cat > "${ACTIVATE_DIR}/firedrake-env-vars.sh" << 'ACTIVATE_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Set environment variables needed by Firedrake's JIT compiler.
|
||||||
|
# These are automatically set on `conda activate`.
|
||||||
|
|
||||||
|
# Save previous values so we can restore on deactivate
|
||||||
|
export _FIREDRAKE_OLD_CC="${CC:-}"
|
||||||
|
export _FIREDRAKE_OLD_CXX="${CXX:-}"
|
||||||
|
export _FIREDRAKE_OLD_FC="${FC:-}"
|
||||||
|
export _FIREDRAKE_OLD_PETSC_DIR="${PETSC_DIR:-}"
|
||||||
|
export _FIREDRAKE_OLD_PETSC_ARCH="${PETSC_ARCH:-}"
|
||||||
|
export _FIREDRAKE_OLD_HDF5_MPI="${HDF5_MPI:-}"
|
||||||
|
export _FIREDRAKE_OLD_HDF5_DIR="${HDF5_DIR:-}"
|
||||||
|
export _FIREDRAKE_OLD_OMP_NUM_THREADS="${OMP_NUM_THREADS:-}"
|
||||||
|
export _FIREDRAKE_OLD_PYOP2_CFLAGS="${PYOP2_CFLAGS:-}"
|
||||||
|
export _FIREDRAKE_OLD_PYOP2_LDFLAGS="${PYOP2_LDFLAGS:-}"
|
||||||
|
|
||||||
|
# PETSc
|
||||||
|
export PETSC_DIR="${CONDA_PREFIX}"
|
||||||
|
export PETSC_ARCH=""
|
||||||
|
|
||||||
|
# Compilers — use MPI wrappers so JIT-compiled kernels link against
|
||||||
|
# the correct MPI and can be loaded alongside PETSc.
|
||||||
|
export CC=mpicc
|
||||||
|
export CXX=mpicxx
|
||||||
|
export FC=mpifort
|
||||||
|
|
||||||
|
# HDF5
|
||||||
|
export HDF5_MPI="ON"
|
||||||
|
export HDF5_DIR="${CONDA_PREFIX}"
|
||||||
|
|
||||||
|
# Firedrake recommends single-threaded BLAS to avoid oversubscription
|
||||||
|
export OMP_NUM_THREADS=1
|
||||||
|
|
||||||
|
# PyOP2 JIT flags — conda's mpicc wraps x86_64-conda-linux-gnu-cc
|
||||||
|
# which PyOP2 doesn't recognise, so it falls back to a generic compiler
|
||||||
|
# class that omits -shared and -fPIC. These env vars inject the flags.
|
||||||
|
export PYOP2_CFLAGS="-fPIC -O2"
|
||||||
|
export PYOP2_LDFLAGS="-shared -Wl,-rpath,${CONDA_PREFIX}/lib"
|
||||||
|
ACTIVATE_EOF
|
||||||
|
|
||||||
|
cat > "${DEACTIVATE_DIR}/firedrake-env-vars.sh" << 'DEACTIVATE_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Restore environment variables on `conda deactivate`.
|
||||||
|
|
||||||
|
export CC="${_FIREDRAKE_OLD_CC}"
|
||||||
|
export CXX="${_FIREDRAKE_OLD_CXX}"
|
||||||
|
export FC="${_FIREDRAKE_OLD_FC}"
|
||||||
|
export PETSC_DIR="${_FIREDRAKE_OLD_PETSC_DIR}"
|
||||||
|
export PETSC_ARCH="${_FIREDRAKE_OLD_PETSC_ARCH}"
|
||||||
|
export HDF5_MPI="${_FIREDRAKE_OLD_HDF5_MPI}"
|
||||||
|
export HDF5_DIR="${_FIREDRAKE_OLD_HDF5_DIR}"
|
||||||
|
export OMP_NUM_THREADS="${_FIREDRAKE_OLD_OMP_NUM_THREADS}"
|
||||||
|
export PYOP2_CFLAGS="${_FIREDRAKE_OLD_PYOP2_CFLAGS}"
|
||||||
|
export PYOP2_LDFLAGS="${_FIREDRAKE_OLD_PYOP2_LDFLAGS}"
|
||||||
|
|
||||||
|
unset _FIREDRAKE_OLD_CC
|
||||||
|
unset _FIREDRAKE_OLD_CXX
|
||||||
|
unset _FIREDRAKE_OLD_FC
|
||||||
|
unset _FIREDRAKE_OLD_PETSC_DIR
|
||||||
|
unset _FIREDRAKE_OLD_PETSC_ARCH
|
||||||
|
unset _FIREDRAKE_OLD_HDF5_MPI
|
||||||
|
unset _FIREDRAKE_OLD_HDF5_DIR
|
||||||
|
unset _FIREDRAKE_OLD_OMP_NUM_THREADS
|
||||||
|
unset _FIREDRAKE_OLD_PYOP2_CFLAGS
|
||||||
|
unset _FIREDRAKE_OLD_PYOP2_LDFLAGS
|
||||||
|
|
||||||
|
# Clean up empty vars
|
||||||
|
[[ -z "$CC" ]] && unset CC
|
||||||
|
[[ -z "$CXX" ]] && unset CXX
|
||||||
|
[[ -z "$FC" ]] && unset FC
|
||||||
|
[[ -z "$PETSC_DIR" ]] && unset PETSC_DIR
|
||||||
|
[[ -z "$PETSC_ARCH" ]] && unset PETSC_ARCH
|
||||||
|
[[ -z "$HDF5_MPI" ]] && unset HDF5_MPI
|
||||||
|
[[ -z "$HDF5_DIR" ]] && unset HDF5_DIR
|
||||||
|
[[ -z "$OMP_NUM_THREADS" ]] && unset OMP_NUM_THREADS
|
||||||
|
[[ -z "$PYOP2_CFLAGS" ]] && unset PYOP2_CFLAGS
|
||||||
|
[[ -z "$PYOP2_LDFLAGS" ]] && unset PYOP2_LDFLAGS
|
||||||
|
DEACTIVATE_EOF
|
||||||
|
|
||||||
|
chmod +x "${ACTIVATE_DIR}/firedrake-env-vars.sh"
|
||||||
|
chmod +x "${DEACTIVATE_DIR}/firedrake-env-vars.sh"
|
||||||
|
|
||||||
|
ok "Activation scripts installed."
|
||||||
|
|
||||||
|
# ── Step 7: Verify ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Running verification ..."
|
||||||
|
|
||||||
|
python "${SCRIPT_DIR}/verify.py" && ok "All checks passed!" || {
|
||||||
|
warn "Some checks failed. See output above."
|
||||||
|
warn "You may need to debug linking issues."
|
||||||
|
warn "Try: conda activate ${ENV_ACTIVATE} && python verify.py"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Done ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
ok "Installation complete!"
|
||||||
|
echo ""
|
||||||
|
echo " To use Firedrake + icepack:"
|
||||||
|
echo ""
|
||||||
|
echo " conda activate ${ENV_ACTIVATE}"
|
||||||
|
echo " python -c 'import firedrake; import icepack; print(\"Ready!\")'"
|
||||||
|
echo ""
|
||||||
|
echo " Environment variables (PETSC_DIR, CC, etc.) are set automatically"
|
||||||
|
echo " when you activate the environment."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
276
verify.py
Executable file
276
verify.py
Executable file
@@ -0,0 +1,276 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify that the Firedrake conda environment is correctly configured.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
1. Environment variables (PETSC_DIR, CC, etc.)
|
||||||
|
2. Conda-provided libraries (PETSc, MPI, HDF5)
|
||||||
|
3. Python imports (petsc4py, mpi4py, firedrake, icepack)
|
||||||
|
4. JIT compilation (the most likely failure point)
|
||||||
|
5. A minimal PDE solve
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
conda activate firedrake
|
||||||
|
python verify.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
import ctypes
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PASS = "\033[92m✓\033[0m"
|
||||||
|
FAIL = "\033[91m✗\033[0m"
|
||||||
|
WARN = "\033[93m!\033[0m"
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
warned = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(description, condition, detail=""):
|
||||||
|
global passed, failed
|
||||||
|
if condition:
|
||||||
|
print(f" {PASS} {description}")
|
||||||
|
passed += 1
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
msg = f" {FAIL} {description}"
|
||||||
|
if detail:
|
||||||
|
msg += f" ({detail})"
|
||||||
|
print(msg)
|
||||||
|
failed += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def warn_check(description, detail=""):
|
||||||
|
global warned
|
||||||
|
msg = f" {WARN} {description}"
|
||||||
|
if detail:
|
||||||
|
msg += f" ({detail})"
|
||||||
|
print(msg)
|
||||||
|
warned += 1
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n1. Environment variables")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
conda_prefix = os.environ.get("CONDA_PREFIX", "")
|
||||||
|
check("CONDA_PREFIX is set", bool(conda_prefix), conda_prefix or "not set")
|
||||||
|
|
||||||
|
petsc_dir = os.environ.get("PETSC_DIR", "")
|
||||||
|
check(
|
||||||
|
"PETSC_DIR points to conda prefix",
|
||||||
|
petsc_dir == conda_prefix,
|
||||||
|
f"PETSC_DIR={petsc_dir!r}, expected {conda_prefix!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
petsc_arch = os.environ.get("PETSC_ARCH", "")
|
||||||
|
check(
|
||||||
|
"PETSC_ARCH is empty (prefix install)",
|
||||||
|
petsc_arch == "",
|
||||||
|
f"PETSC_ARCH={petsc_arch!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cc = os.environ.get("CC", "")
|
||||||
|
check("CC is set to mpicc", cc == "mpicc", f"CC={cc!r}")
|
||||||
|
|
||||||
|
hdf5_mpi = os.environ.get("HDF5_MPI", "")
|
||||||
|
check("HDF5_MPI=ON", hdf5_mpi == "ON", f"HDF5_MPI={hdf5_mpi!r}")
|
||||||
|
|
||||||
|
pyop2_cflags = os.environ.get("PYOP2_CFLAGS", "")
|
||||||
|
check("PYOP2_CFLAGS has -fPIC", "-fPIC" in pyop2_cflags, f"PYOP2_CFLAGS={pyop2_cflags!r}")
|
||||||
|
|
||||||
|
pyop2_ldflags = os.environ.get("PYOP2_LDFLAGS", "")
|
||||||
|
check("PYOP2_LDFLAGS has -shared", "-shared" in pyop2_ldflags, f"PYOP2_LDFLAGS={pyop2_ldflags!r}")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n2. Shared libraries")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
if conda_prefix:
|
||||||
|
lib_dir = Path(conda_prefix) / "lib"
|
||||||
|
ext = "dylib" if sys.platform == "darwin" else "so"
|
||||||
|
|
||||||
|
for lib_name in ["libpetsc", "libmpi", "libhdf5"]:
|
||||||
|
lib_path = lib_dir / f"{lib_name}.{ext}"
|
||||||
|
if not lib_path.exists():
|
||||||
|
# Try finding any matching file
|
||||||
|
matches = list(lib_dir.glob(f"{lib_name}*"))
|
||||||
|
found = len(matches) > 0
|
||||||
|
detail = str(matches[0]) if found else f"no {lib_name}* in {lib_dir}"
|
||||||
|
else:
|
||||||
|
found = True
|
||||||
|
detail = str(lib_path)
|
||||||
|
check(f"{lib_name} found", found, detail)
|
||||||
|
else:
|
||||||
|
warn_check("Skipping library checks (CONDA_PREFIX not set)")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n3. Python imports")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
modules = [
|
||||||
|
("numpy", "numpy"),
|
||||||
|
("mpi4py", "mpi4py"),
|
||||||
|
("petsc4py", "petsc4py"),
|
||||||
|
("h5py", "h5py"),
|
||||||
|
("ufl", "ufl"),
|
||||||
|
("FIAT", "FIAT"),
|
||||||
|
("finat", "finat"),
|
||||||
|
("loopy", "loopy"),
|
||||||
|
("tsfc", "tsfc"),
|
||||||
|
("pyop2", "pyop2"),
|
||||||
|
("firedrake", "firedrake"),
|
||||||
|
("icepack", "icepack"),
|
||||||
|
]
|
||||||
|
|
||||||
|
imported = {}
|
||||||
|
for display_name, module_name in modules:
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(module_name)
|
||||||
|
version = getattr(mod, "__version__", "?")
|
||||||
|
check(f"import {display_name}", True, f"v{version}")
|
||||||
|
imported[module_name] = mod
|
||||||
|
except Exception as e:
|
||||||
|
check(f"import {display_name}", False, str(e))
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n4. PETSc configuration")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
if "petsc4py" in imported:
|
||||||
|
from petsc4py import PETSc as petsc
|
||||||
|
|
||||||
|
# Check that PETSc has key external packages
|
||||||
|
has_mumps = petsc.Sys.hasExternalPackage("mumps")
|
||||||
|
check("PETSc has MUMPS", has_mumps)
|
||||||
|
|
||||||
|
has_hypre = petsc.Sys.hasExternalPackage("hypre")
|
||||||
|
check("PETSc has hypre", has_hypre)
|
||||||
|
|
||||||
|
has_superlu = petsc.Sys.hasExternalPackage("superlu_dist")
|
||||||
|
check("PETSc has SuperLU_dist", has_superlu)
|
||||||
|
|
||||||
|
# Scalar type
|
||||||
|
import numpy as np
|
||||||
|
scalar = petsc.ScalarType
|
||||||
|
is_real = scalar in (float, np.float64, np.float32)
|
||||||
|
check("PETSc scalar type is real", is_real, f"scalar={scalar}")
|
||||||
|
else:
|
||||||
|
warn_check("Skipping PETSc config checks (import failed)")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n5. JIT compilation test")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
if "firedrake" in imported:
|
||||||
|
try:
|
||||||
|
import firedrake
|
||||||
|
|
||||||
|
# This triggers the full JIT pipeline:
|
||||||
|
# UFL form → TSFC → loopy → C code → compile → dlopen
|
||||||
|
mesh = firedrake.UnitSquareMesh(4, 4)
|
||||||
|
V = firedrake.FunctionSpace(mesh, "CG", 1)
|
||||||
|
u = firedrake.TrialFunction(V)
|
||||||
|
v = firedrake.TestFunction(V)
|
||||||
|
a = firedrake.inner(firedrake.grad(u), firedrake.grad(v)) * firedrake.dx
|
||||||
|
A = firedrake.assemble(a)
|
||||||
|
|
||||||
|
check("JIT compilation works", True, "assembled a Laplacian matrix")
|
||||||
|
except Exception as e:
|
||||||
|
check("JIT compilation works", False, str(e))
|
||||||
|
# Try to find and display the actual compiler error
|
||||||
|
import glob
|
||||||
|
err_files = sorted(
|
||||||
|
glob.glob("/tmp/pyop2-tempcache-*/**/*.err", recursive=True),
|
||||||
|
key=lambda f: Path(f).stat().st_mtime,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
log_files = sorted(
|
||||||
|
glob.glob("/tmp/pyop2-tempcache-*/**/*.log", recursive=True),
|
||||||
|
key=lambda f: Path(f).stat().st_mtime,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
for label, files in [("COMPILE ERRORS", err_files), ("COMPILE LOG", log_files)]:
|
||||||
|
if files:
|
||||||
|
content = Path(files[0]).read_text().strip()
|
||||||
|
if content:
|
||||||
|
print(f"\n ── {label}: {files[0]} ──")
|
||||||
|
for line in content.splitlines()[-20:]:
|
||||||
|
print(f" {line}")
|
||||||
|
else:
|
||||||
|
warn_check("Skipping JIT test (firedrake import failed)")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n6. Minimal PDE solve")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
if "firedrake" in imported:
|
||||||
|
try:
|
||||||
|
import firedrake
|
||||||
|
from firedrake import *
|
||||||
|
|
||||||
|
mesh = UnitSquareMesh(8, 8)
|
||||||
|
V = FunctionSpace(mesh, "CG", 1)
|
||||||
|
|
||||||
|
u = TrialFunction(V)
|
||||||
|
v = TestFunction(V)
|
||||||
|
|
||||||
|
x, y = SpatialCoordinate(mesh)
|
||||||
|
f = sin(pi * x) * sin(pi * y)
|
||||||
|
|
||||||
|
a = inner(grad(u), grad(v)) * dx
|
||||||
|
L = f * v * dx
|
||||||
|
bc = DirichletBC(V, 0, "on_boundary")
|
||||||
|
|
||||||
|
u_sol = Function(V)
|
||||||
|
solve(a == L, u_sol, bcs=bc)
|
||||||
|
|
||||||
|
# Check the solution is reasonable
|
||||||
|
import numpy as np
|
||||||
|
u_data = u_sol.dat.data_ro
|
||||||
|
check(
|
||||||
|
"Poisson solve produces valid output",
|
||||||
|
np.all(np.isfinite(u_data)) and np.max(np.abs(u_data)) > 0,
|
||||||
|
f"max|u| = {np.max(np.abs(u_data)):.6f}",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
check("Poisson solve", False, str(e))
|
||||||
|
else:
|
||||||
|
warn_check("Skipping PDE solve (firedrake import failed)")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n7. icepack smoke test")
|
||||||
|
print("─" * 40)
|
||||||
|
|
||||||
|
if "icepack" in imported and "firedrake" in imported:
|
||||||
|
try:
|
||||||
|
import icepack
|
||||||
|
import firedrake
|
||||||
|
|
||||||
|
# Check that core model classes exist and instantiate
|
||||||
|
model = icepack.models.IceShelf()
|
||||||
|
check("icepack.models.IceShelf()", True)
|
||||||
|
|
||||||
|
model = icepack.models.IceStream()
|
||||||
|
check("icepack.models.IceStream()", True)
|
||||||
|
|
||||||
|
# Check rate factor function
|
||||||
|
A = icepack.rate_factor(260.0)
|
||||||
|
check(
|
||||||
|
"icepack.rate_factor(260 K)",
|
||||||
|
A > 0,
|
||||||
|
f"A = {A:.4e}",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
check("icepack smoke test", False, str(e))
|
||||||
|
else:
|
||||||
|
warn_check("Skipping icepack test (import failed)")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n" + "═" * 50)
|
||||||
|
print(f"Results: {PASS} {passed} passed {FAIL} {failed} failed {WARN} {warned} warnings")
|
||||||
|
print("═" * 50)
|
||||||
|
|
||||||
|
sys.exit(0 if failed == 0 else 1)
|
||||||
Reference in New Issue
Block a user