diff --git a/.github/workflows/deploy-tap.yml b/.github/workflows/deploy-tap.yml new file mode 100644 index 0000000000..2075d26e6c --- /dev/null +++ b/.github/workflows/deploy-tap.yml @@ -0,0 +1,102 @@ +name: Deploy Homebrew Tap + +on: + push: + branches: [ main, master, homebrew-formula ] + paths: + - 'packaging/homebrew/mfc.rb' + - 'packaging/homebrew/README.md' + tags: + - 'v*.*.*' + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy-tap: + name: Sync/bump formula in tap + runs-on: macos-14 + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout MFC repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine event metadata + id: meta + run: | + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + VERSION="${GITHUB_REF_NAME#v}" + URL="https://github.com/${{ github.repository }}/archive/refs/tags/v${VERSION}.tar.gz" + else + # Extract URL from current formula to re-audit and sync + URL="$(grep -Eo 'https://github.com/.*/archive/refs/tags/v[0-9]+\.[0-9]+\.[0-9]+\.tar\.gz' packaging/homebrew/mfc.rb | head -n1)" + VERSION="$(echo "${URL}" | sed -E 's/.*v([0-9]+\.[0-9]+\.[0-9]+)\.tar\.gz/\1/')" + fi + SHASUM="$(curl -sL "${URL}" | shasum -a 256 | awk '{print $1}')" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "url=${URL}" >> $GITHUB_OUTPUT + echo "sha256=${SHASUM}" >> $GITHUB_OUTPUT + + - name: Update formula (for tag events) + if: github.ref_type == 'tag' + run: | + /usr/bin/sed -i '' "s@^ url \".*\"@ url \"${{ steps.meta.outputs.url }}\"@" packaging/homebrew/mfc.rb + /usr/bin/sed -i '' "s@^ sha256 \".*\"@ sha256 \"${{ steps.meta.outputs.sha256 }}\"@" packaging/homebrew/mfc.rb + + - name: Setup Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Audit/style formula before pushing + run: | + brew style packaging/homebrew/mfc.rb + # Create temporary tap to audit the formula + brew tap-new mfc/local + cp packaging/homebrew/mfc.rb "$(brew --repository)/Library/Taps/mfc/homebrew-local/Formula/mfc.rb" + brew audit --online --strict --new --except=homepage mfc/local/mfc || brew audit --online mfc/local/mfc + brew untap mfc/local + + - name: Clone or bootstrap tap repository + env: + TAP_TOKEN: ${{ secrets.TAP_REPO_TOKEN }} + run: | + set -euo pipefail + REPO="https://x-access-token:${TAP_TOKEN}@github.com/MFlowCode/homebrew-mfc.git" + if git ls-remote "${REPO}" HEAD >/dev/null 2>&1; then + git clone "${REPO}" tap-repo + else + # Repo exists but might be empty; fall back to bootstrap + mkdir -p tap-repo + cd tap-repo + git init -b main + git remote add origin "${REPO}" + touch .keep + git add .keep + git -c user.name="github-actions[bot]" -c user.email="github-actions[bot]@users.noreply.github.com" commit -m "Initialize tap" + git push -u origin main + fi + + - name: Copy formula and README into tap + run: | + mkdir -p tap-repo/Formula + cp packaging/homebrew/mfc.rb tap-repo/Formula/mfc.rb + cp packaging/homebrew/README.md tap-repo/README.md + + - name: Commit & push if changed + env: + TAP_TOKEN: ${{ secrets.TAP_REPO_TOKEN }} + run: | + cd tap-repo + git add Formula/mfc.rb README.md + if git diff --cached --quiet; then + echo "No changes in Formula/mfc.rb or README.md; skipping push." + exit 0 + fi + git -c user.name="github-actions[bot]" -c user.email="github-actions[bot]@users.noreply.github.com" \ + commit -m "mfc: v${{ steps.meta.outputs.version }}" + git push origin HEAD:main + diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml new file mode 100644 index 0000000000..3dfffea1e7 --- /dev/null +++ b/.github/workflows/homebrew.yml @@ -0,0 +1,264 @@ +name: Homebrew Formula Test + +on: + push: + branches: + - master + - homebrew-formula + paths: + - 'packaging/homebrew/**' + - '.github/workflows/homebrew.yml' + pull_request: + branches: + - master + paths: + - 'packaging/homebrew/**' + - '.github/workflows/homebrew.yml' + workflow_dispatch: + +jobs: + # Fast smoke tests that run before expensive operations + smoke-test: + name: Quick Formula Validation + runs-on: ubuntu-latest # Use Linux for speed (Homebrew works on Linux too) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Validate formula syntax with brew style + run: | + echo "Checking formula syntax..." + brew style packaging/homebrew/mfc.rb + + - name: Run brew audit (without installation) + run: | + echo "Configuring git for brew tap-new..." + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + echo "Creating temporary local tap..." + brew tap-new mflowcode/test + cp packaging/homebrew/mfc.rb $(brew --repository)/Library/Taps/mflowcode/homebrew-test/Formula/mfc.rb + + echo "Running brew audit (online checks)..." + brew audit --online --skip-style mflowcode/test/mfc || true + + echo "Cleaning up tap..." + brew untap mflowcode/test + + - name: Validate Ruby syntax + run: | + echo "Checking Ruby syntax..." + ruby -c packaging/homebrew/mfc.rb + + - name: Check for common formula issues + run: | + echo "Checking for common issues..." + + # Check that required fields are present + grep -q 'desc "' packaging/homebrew/mfc.rb || (echo "❌ Missing desc"; exit 1) + grep -q 'homepage "' packaging/homebrew/mfc.rb || (echo "❌ Missing homepage"; exit 1) + grep -q 'url "' packaging/homebrew/mfc.rb || (echo "❌ Missing url"; exit 1) + grep -q 'sha256 "' packaging/homebrew/mfc.rb || (echo "❌ Missing sha256"; exit 1) + grep -q 'license "' packaging/homebrew/mfc.rb || (echo "❌ Missing license"; exit 1) + + # Check that install method exists + grep -q 'def install' packaging/homebrew/mfc.rb || (echo "❌ Missing install method"; exit 1) + + # Check that test block exists + grep -q 'test do' packaging/homebrew/mfc.rb || (echo "❌ Missing test block"; exit 1) + + echo "✅ All required formula components present" + + - name: Verify URL is reachable + run: | + echo "Checking that source URL is reachable..." + URL=$(grep -E 'url "https://[^"]+' packaging/homebrew/mfc.rb | head -1 | sed 's/.*url "\([^"]*\)".*/\1/') + + if [ -z "$URL" ]; then + echo "❌ Could not extract URL from formula" + exit 1 + fi + + echo "URL: $URL" + HTTP_CODE=$(curl -sI -w "%{http_code}" -o /dev/null "$URL") + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then + echo "✅ URL is reachable (HTTP $HTTP_CODE)" + else + echo "⚠️ URL returned HTTP $HTTP_CODE (may indicate an issue)" + # Don't fail here - could be a temporary issue + fi + + - name: Verify SHA256 checksum + run: | + echo "Verifying SHA256 checksum matches URL..." + URL=$(grep -E 'url "https://[^"]+' packaging/homebrew/mfc.rb | head -1 | sed 's/.*url "\([^"]*\)".*/\1/') + EXPECTED_SHA=$(grep 'sha256 "' packaging/homebrew/mfc.rb | head -1 | sed 's/.*sha256 "\([^"]*\)".*/\1/') + + if [ -z "$URL" ] || [ -z "$EXPECTED_SHA" ]; then + echo "❌ Could not extract URL or SHA256 from formula" + exit 1 + fi + + echo "Downloading tarball to compute checksum..." + ACTUAL_SHA=$(curl -sL "$URL" | shasum -a 256 | awk '{print $1}') + + echo "Expected SHA256: $EXPECTED_SHA" + echo "Actual SHA256: $ACTUAL_SHA" + + if [ "$EXPECTED_SHA" = "$ACTUAL_SHA" ]; then + echo "✅ SHA256 checksum matches!" + else + echo "❌ SHA256 mismatch!" + exit 1 + fi + + # Full installation test (only runs if smoke tests pass) + test-formula: + name: Full Installation Test + needs: smoke-test # Only run after smoke tests pass + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Homebrew + run: | + echo "Homebrew version:" + brew --version + echo "Updating Homebrew..." + brew update + + - name: Install formula dependencies + run: | + echo "Installing MFC dependencies..." + brew install cmake gcc python@3.12 boost fftw hdf5 open-mpi openblas + + - name: Install MFC from formula + run: | + echo "Creating temporary local tap..." + brew tap-new mflowcode/test + + echo "Copying formula to tap..." + cp packaging/homebrew/mfc.rb $(brew --repository)/Library/Taps/mflowcode/homebrew-test/Formula/mfc.rb + + echo "Installing MFC from local tap..." + # Note: brew may exit with code 1 due to dylib fixup warnings on some Python packages (non-fatal) + # We verify installation using brew commands rather than parsing log output + set +e # Don't fail immediately on error + brew install --build-from-source --verbose mflowcode/test/mfc 2>&1 | tee /tmp/brew-install.log + brew_exit_code=$? + set -e + + # Verify installation using brew list (more robust than log parsing) + if brew list mflowcode/test/mfc &>/dev/null; then + echo "✅ MFC installed successfully (ignoring dylib relocation warnings)" + # Optionally verify with brew info + brew info mflowcode/test/mfc + exit 0 + else + echo "❌ MFC installation failed" + exit $brew_exit_code + fi + + - name: Display error logs on failure + if: failure() + run: | + echo "=== Displaying last 200 lines of brew install log ===" + if [ -f /tmp/brew-install.log ]; then + tail -200 /tmp/brew-install.log + fi + + echo -e "\n=== Displaying Homebrew log files ===" + if [ -d ~/Library/Logs/Homebrew/mfc/ ]; then + for logfile in ~/Library/Logs/Homebrew/mfc/*; do + if [ -f "$logfile" ]; then + echo -e "\n\n====== $logfile ======" + cat "$logfile" + fi + done + fi + + echo -e "\n=== Searching for Cantera config.log ===" + cantera_config_log=$(find /private/tmp -name "config.log" -path "*/mfc--cantera*" 2>/dev/null | head -1) + if [ -n "$cantera_config_log" ] && [ -f "$cantera_config_log" ]; then + echo -e "\n\n====== Cantera config.log ======" + echo "Found at: $cantera_config_log" + cat "$cantera_config_log" + # Copy to a known location for artifact upload + mkdir -p /tmp/cantera-logs + cp "$cantera_config_log" /tmp/cantera-logs/config.log + else + echo "Cantera config.log not found" + echo "Searching in all /private/tmp directories:" + find /private/tmp -name "config.log" 2>/dev/null || echo "No config.log files found" + fi + + - name: Upload Homebrew logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: homebrew-logs + path: | + /tmp/brew-install.log + /tmp/cantera-logs/ + ~/Library/Logs/Homebrew/mfc/ + if-no-files-found: ignore + + - name: Test MFC installation + run: | + echo "=== Testing MFC Installation ===" + + echo "1. Checking binaries exist and are executable..." + test -f $(brew --prefix)/bin/mfc && test -x $(brew --prefix)/bin/mfc + test -f $(brew --prefix)/bin/pre_process && test -x $(brew --prefix)/bin/pre_process + test -f $(brew --prefix)/bin/simulation && test -x $(brew --prefix)/bin/simulation + test -f $(brew --prefix)/bin/post_process && test -x $(brew --prefix)/bin/post_process + echo " ✓ All binaries exist and are executable" + + echo "2. Verifying installation structure..." + test -f $(brew --prefix mfc)/libexec/mfc.sh + test -d $(brew --prefix mfc)/toolchain + echo " ✓ Installation structure verified" + + echo "3. Checking Python venv..." + test -d $(brew --prefix mfc)/libexec/venv + test -f $(brew --prefix mfc)/libexec/venv/bin/python + test -f $(brew --prefix mfc)/libexec/venv/bin/pip + echo " ✓ Python venv exists" + + echo "4. Checking examples..." + test -d $(brew --prefix mfc)/examples + test -f $(brew --prefix mfc)/examples/1D_sodshocktube/case.py + echo " ✓ Examples installed" + + echo "5. Testing mfc wrapper..." + mfc --help + echo " ✓ mfc --help succeeded" + + echo "=== All tests passed! ===" + + - name: Run MFC test case + run: | + echo "Running a simple test case (1D Sod shock tube)..." + TESTDIR=$(mktemp -d) + cp $(brew --prefix mfc)/examples/1D_sodshocktube/case.py "$TESTDIR/" + + echo "Running with $(sysctl -n hw.ncpu) processors..." + # Use absolute path since mfc wrapper creates its own tmpdir + mfc run "$TESTDIR/case.py" -j $(sysctl -n hw.ncpu) + + echo "Test case completed successfully!" + + - name: Uninstall and cleanup + if: always() + run: | + echo "Cleaning up..." + brew uninstall mfc || true + brew cleanup diff --git a/.gitignore b/.gitignore index 30393c710b..a56c6a6749 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,6 @@ benchmarks/*.png *.mov *.mkv *.avi + +packaging/spack/spack-test +.spack \ No newline at end of file diff --git a/.typos.toml b/.typos.toml index 1fb0c90272..a595990e72 100644 --- a/.typos.toml +++ b/.typos.toml @@ -20,6 +20,7 @@ Strang = "Strang" TKE = "TKE" HSA = "HSA" infp = "infp" +Sur = "Sur" [files] extend-exclude = ["docs/documentation/references*", "tests/", "toolchain/cce_simulation_workgroup_256.sh"] diff --git a/README.md b/README.md index aafc401a1e..4d410f8a65 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ MFC runs at exascale on the world's fastest supercomputers: | Path | Command | | --- | --- | | **Codespaces** (fastest) | Click the "Codespaces" badge above to launch in 1 click | +| **Homebrew (macOS)** | `brew install mflowcode/mfc/mfc && mfc run $(brew --prefix mfc)/examples/1D_sodshocktube/case.py -n 2` | | **Local build** | `./mfc.sh build -j $(nproc) && ./mfc.sh test -j $(nproc)` | **Welcome!** @@ -136,7 +137,23 @@ Click <> Code (green button at top right) → Codespaces You can navigate [to this webpage](https://mflowcode.github.io/documentation/md_getting-started.html) to get you get started using MFC on your local machine, cluster, or supercomputer! It's rather straightforward. -We'll give a brief introdocution for MacOS below. + +### macOS quick start (Homebrew) + +Install the prebuilt package and run an example: + +```bash +brew install mflowcode/mfc/mfc +mkdir -p ~/mfc_quickstart && cd ~/mfc_quickstart +cp $(brew --prefix mfc)/examples/1D_sodshocktube/case.py . +mfc run case.py -n 2 +``` + +Use `-n X` to select the number of MPI processes. For developer commands (`build`, `test`, etc.), clone the repo and use `./mfc.sh`. + +### macOS from source + +We'll give a brief introduction for building from source on MacOS below. Using [brew](https://brew.sh), install MFC's dependencies: ```shell brew install coreutils python cmake fftw hdf5 gcc boost open-mpi lapack diff --git a/docs/documentation/getting-started.md b/docs/documentation/getting-started.md index 9478a72ce2..3494a14b4a 100644 --- a/docs/documentation/getting-started.md +++ b/docs/documentation/getting-started.md @@ -9,6 +9,28 @@ git clone https://github.com/MFlowCode/MFC.git cd MFC ``` +## Install via Homebrew (macOS) + +On macOS, install prebuilt MFC via Homebrew: + +```bash +brew install mflowcode/mfc/mfc +``` + +Run a quick example: + +```bash +mkdir -p ~/mfc_quickstart && cd ~/mfc_quickstart +cp $(brew --prefix mfc)/examples/1D_sodshocktube/case.py . +# Use -n X to choose the number of MPI processes +mfc run case.py -n 2 +``` + +Notes: +- The Homebrew wrapper supports only `mfc run ...`. Developer commands like `build`, `test`, `clean` are available when you clone the repo and use `./mfc.sh`. +- The package bundles a Python venv and prebuilt binaries; no additional setup is required. +- Examples are installed at `$(brew --prefix mfc)/examples/`. + ## Build Environment MFC can be built in multiple ways on various operating systems. diff --git a/docs/documentation/running.md b/docs/documentation/running.md index eb389e4ffb..7438fc5912 100644 --- a/docs/documentation/running.md +++ b/docs/documentation/running.md @@ -8,6 +8,20 @@ A full (and up-to-date) list of available arguments can be acquired with `./mfc. MFC supports running simulations locally (Linux, MacOS, and Windows) as well as several supercomputer clusters, both interactively and through batch submission. +## Using the Homebrew package (macOS) + +If you installed MFC via Homebrew, run cases with the `mfc` wrapper: + +```bash +mfc run -n 2 +``` + +- Use `-n X` to control the number of MPI processes (ranks). +- Only the `run` command is supported in the Homebrew wrapper. +- To use developer commands (`build`, `test`, `clean`, etc.), clone the repository and use `./mfc.sh`. +- The wrapper passes through runtime flags like `-t pre_process simulation`, `-n`, and others; it always runs with preinstalled binaries. +- Examples live at `$(brew --prefix mfc)/examples/`. + > [!IMPORTANT] > Running simulations locally should work out of the box. On supported clusters, > you can append `-c ` on the command line to instruct the MFC toolchain diff --git a/packaging/homebrew/README.md b/packaging/homebrew/README.md new file mode 100644 index 0000000000..dd99d2be7f --- /dev/null +++ b/packaging/homebrew/README.md @@ -0,0 +1,60 @@ +# MFC Homebrew Tap + +Official Homebrew tap for [MFC (Multiphase Flow Code)](https://github.com/MFlowCode/MFC) – an exascale multiphase/multiphysics compressible flow solver. + +## Installation + +```bash +brew install mflowcode/mfc/mfc +``` + +## Quick Start + +Run the 1D Sod shock tube example: + +```bash +mkdir -p ~/mfc_example && cd ~/mfc_example +cp $(brew --prefix mfc)/examples/1D_sodshocktube/case.py . +mfc run case.py -n 2 +``` + +## What's Included + +- **Prebuilt binaries**: `pre_process`, `simulation`, `post_process` +- **Python toolchain**: Pre-configured virtual environment with all dependencies +- **Examples**: Located at `$(brew --prefix mfc)/examples/` +- **`mfc` wrapper**: Simplified command-line interface for running cases + +## Usage + +```bash +mfc run -n +``` + +Use `-n X` to set the number of MPI processes. + +**Note**: The Homebrew wrapper supports only `mfc run`. For developer commands (`build`, `test`, `clean`, etc.), [clone the repository](https://github.com/MFlowCode/MFC) and use `./mfc.sh`. + +## Documentation + +- [MFC Documentation](https://mflowcode.github.io/) +- [Getting Started Guide](https://mflowcode.github.io/documentation/md_getting-started.html) +- [Running Cases](https://mflowcode.github.io/documentation/md_running.html) + +## About This Tap + +This tap is automatically maintained by the [MFC repository](https://github.com/MFlowCode/MFC) via GitHub Actions. The formula is synced from [`packaging/homebrew/mfc.rb`](https://github.com/MFlowCode/MFC/blob/master/packaging/homebrew/mfc.rb). + +For issues or contributions related to the Homebrew formula, please visit the [main MFC repository](https://github.com/MFlowCode/MFC). + +## Uninstall + +```bash +brew uninstall mfc +brew untap mflowcode/mfc +``` + +## License + +MFC is licensed under the [MIT License](https://github.com/MFlowCode/MFC/blob/master/LICENSE). + diff --git a/packaging/homebrew/mfc.rb b/packaging/homebrew/mfc.rb new file mode 100644 index 0000000000..0496498303 --- /dev/null +++ b/packaging/homebrew/mfc.rb @@ -0,0 +1,275 @@ +# typed: strict +# frozen_string_literal: true + +# Homebrew formula for MFC (Multiphase Flow Code) +# This formula is automatically deployed to the MFlowCode/homebrew-mfc tap by GitHub Actions workflow +class Mfc < Formula + desc "Exascale multiphase/multiphysics compressible flow solver" + homepage "https://mflowcode.github.io/" + url "https://github.com/MFlowCode/MFC/archive/refs/tags/v5.1.0.tar.gz" + sha256 "4684bee6a529287f243f8929fb7edb0dfebbb04df7c1806459761c9a6c9261cf" + license "MIT" + head "https://github.com/MFlowCode/MFC.git", branch: "master" + + depends_on "cmake" => :build + depends_on "gcc" => :build + + depends_on "boost" + depends_on "fftw" + depends_on "hdf5" + depends_on "open-mpi" + depends_on "openblas" + depends_on "python@3.12" + + # Skip relocation for Python C extensions in the venv + # The venv is self-contained in libexec and doesn't need Homebrew's relocation + skip_clean "libexec/venv" + + def install + # Create Python virtual environment inside libexec (inside Cellar for proper bottling) + venv = libexec/"venv" + system Formula["python@3.12"].opt_bin/"python3.12", "-m", "venv", venv + system venv/"bin/pip", "install", "--upgrade", "pip", "setuptools", "wheel" + + # Install Cantera from PyPI using pre-built wheel (complex package, doesn't need custom flags) + # Cantera has CMake compatibility issues when building from source with newer CMake versions + system venv/"bin/pip", "install", "cantera==3.1.0" + + # Install MFC Python package and dependencies into venv + # Use editable install (-e) to avoid RECORD file issues when venv is symlinked at runtime + # Dependencies will use pre-built wheels from PyPI + # Keep toolchain in buildpath for now - mfc.sh needs it there + system venv/"bin/pip", "install", "-e", buildpath/"toolchain" + + # Create symlink so mfc.sh uses our pre-installed venv + mkdir_p "build" + ln_sf venv, "build/venv" + + # Now build MFC with pre-configured venv + # Set VIRTUAL_ENV so mfc.sh uses existing venv instead of creating new one + ENV["VIRTUAL_ENV"] = venv + + # Build MFC using pre-configured venv + # Must run from buildpath (MFC root directory) where toolchain/ exists + Dir.chdir(buildpath) do + system "./mfc.sh", "build", "-t", "pre_process", "simulation", "post_process", "-j", ENV.make_jobs.to_s + end + + # After build completes, install Python toolchain to prefix + prefix.install "toolchain" + + # Install only executable files from hashed build dirs + Dir.glob("build/install/*/bin/*").each do |p| + next if !File.file?(p) || !File.executable?(p) + + bin.install p + (bin/File.basename(p)).chmod 0755 + end + + # Install main mfc.sh script + libexec.install "mfc.sh" + + # Install examples + prefix.install "examples" + + # Create smart wrapper script in libexec; bin wrapper will be generated by Homebrew + (libexec/"mfc").write <<~EOS + #!/usr/bin/env bash + set -euo pipefail + + # Unset VIRTUAL_ENV to ensure mfc.sh uses our configured venv + unset VIRTUAL_ENV || true + + # Save original working directory + ORIG_DIR="$(pwd)" + + # Process arguments and convert relative paths to absolute paths + declare -a ARGS=() + for arg in "$@"; do + # If argument looks like a file path and exists as relative path + if [[ "$arg" =~ \\.(py|txt|json|yaml|yml)$ ]] && [ -e "${ORIG_DIR}/${arg}" ]; then + ARGS+=("${ORIG_DIR}/${arg}") + else + ARGS+=("$arg") + fi + done + + SUBCMD="${ARGS[0]-}" + + # Friendly help and guardrails + if [[ ${#ARGS[@]} -eq 0 ]] || [[ "${SUBCMD}" == "--help" ]] || [[ "${SUBCMD}" == "-h" ]]; then + cat <<'HHELP' + MFC (Homebrew) #{version} + + Usage: + mfc run [options] + + Examples: + mfc run case.py -j 1 + + Notes: + - This Homebrew wrapper uses prebuilt binaries and a preinstalled venv. + - Developer commands (build, clean, test, etc.) are not available here. + Clone the MFC repo for the full developer workflow. + HHELP + exit 0 + fi + + if [[ "${SUBCMD}" != "run" ]]; then + echo "mfc (Homebrew): only 'run' is supported in the Homebrew package." + echo "Use 'mfc run ' or clone the repository for developer commands." + exit 2 + fi + + # Create a temporary working directory (Cellar is read-only) + TMPDIR="$(mktemp -d)" + trap 'rm -rf "${TMPDIR}"' EXIT + cd "${TMPDIR}" + + # Copy only mfc.sh (small, fast) + cp "#{libexec}/mfc.sh" . + + # Create a minimal CMakeLists.txt to prevent the cat error + # mfc.sh reads this to get version info + cat > CMakeLists.txt << 'CMAKE_EOF' + cmake_minimum_required(VERSION 3.18) + project(MFC VERSION #{version}) + CMAKE_EOF + + # Symlink toolchain (read-only is fine, no copy needed) + ln -s "#{prefix}/toolchain" toolchain + + # Symlink examples (read-only is fine) + ln -s "#{prefix}/examples" examples + + # Create build directory structure + mkdir -p build + + # Symlink the persistent venv (no copy) + ln -s "#{libexec}/venv" build/venv + + # Copy only pyproject.toml (tiny file, prevents reinstall checks) + cp "#{prefix}/toolchain/pyproject.toml" build/pyproject.toml + + # Create a sitecustomize.py file that patches MFC paths before any imports + # This runs automatically at Python startup and ensures paths are correct + cat > sitecustomize.py << 'SITECUSTOMIZE_EOF' + import sys + import os + + # Add toolchain to path + sys.path.insert(0, "#{prefix}/toolchain") + + # Patch MFC paths before mfc.common is imported anywhere + _mfc_temp_root = os.getcwd() + + def _patch_mfc_common(): + """Patches mfc.common module to use temp directory for writable files.""" + import mfc.common + mfc.common.MFC_ROOT_DIR = _mfc_temp_root + mfc.common.MFC_BUILD_DIR = os.path.join(_mfc_temp_root, "build") + mfc.common.MFC_LOCK_FILEPATH = os.path.join(mfc.common.MFC_BUILD_DIR, "lock.yaml") + # Keep toolchain and examples pointing to Homebrew installation + mfc.common.MFC_TOOLCHAIN_DIR = "#{prefix}/toolchain" + mfc.common.MFC_EXAMPLE_DIRPATH = "#{prefix}/examples" + + def _patch_mfc_build(): + """Patches MFCTarget to use pre-installed binaries.""" + from mfc.build import MFCTarget + + # Override get_install_binpath to use pre-installed binaries + _original_get_install_binpath = MFCTarget.get_install_binpath + def _homebrew_get_install_binpath(self, case): + return "#{bin}/" + self.name + MFCTarget.get_install_binpath = _homebrew_get_install_binpath + + # Override is_buildable to skip building main targets + _original_is_buildable = MFCTarget.is_buildable + def _homebrew_is_buildable(self): + if self.name in ["pre_process", "simulation", "post_process", "syscheck"]: + return False + return _original_is_buildable(self) + MFCTarget.is_buildable = _homebrew_is_buildable + + # Apply patches immediately + _patch_mfc_common() + _patch_mfc_build() + SITECUSTOMIZE_EOF + + # Set PYTHONPATH to include current directory so sitecustomize.py is found + export PYTHONPATH="${TMPDIR}:#{prefix}/toolchain:${PYTHONPATH:-}" + + # Always run with --no-build in Homebrew package + exec ./mfc.sh "${ARGS[@]}" --no-build + EOS + (libexec/"mfc").chmod 0755 + + # Create a thin exec wrapper in bin that calls the libexec script (audit-friendly) + bin.write_exec_script libexec/"mfc" + end + + def post_install + # Fix executable permissions for libexec wrapper + (libexec/"mfc").chmod 0755 + end + + def caveats + <<~EOS + MFC has been installed successfully! + + To run a case: + mfc run + + Pre-built binaries are also available directly: + pre_process, simulation, post_process + + Examples are available in: + #{prefix}/examples + + Example: + cp #{prefix}/examples/1D_sodshocktube/case.py . + mfc run case.py + + Note: Cantera 3.1.0 is pre-installed in the MFC virtual environment. + EOS + end + + test do + # Test that all binaries exist and are executable + %w[pre_process simulation post_process].each do |prog| + assert_path_exists bin/prog + assert_predicate bin/prog, :executable? + end + + # Test that toolchain is installed + assert_path_exists prefix/"toolchain" + + # Test that venv exists and has required packages + assert_path_exists libexec/"venv" + assert_predicate (libexec/"venv/bin/python"), :executable? + + # Test that examples exist + assert_path_exists prefix/"examples" + + # Test that mfc wrapper works + system bin/"mfc", "--help" + + # Test running a simple 1D Sod shock tube case from a separate directory + # This ensures the wrapper script correctly handles relative paths + testpath_case = testpath/"test_run" + testpath_case.mkpath + + # Copy case.py from examples to an independent test directory + cp prefix/"examples/1D_sodshocktube/case.py", testpath_case/"case.py" + + # Run the case from the test directory (this will execute pre_process and simulation) + # Limit to 1 processor and reduce runtime for testing + cd testpath_case do + system bin/"mfc", "run", "case.py", "-j", "1" + end + + # Verify output files were created in the test directory + assert_path_exists testpath_case/"silo_hdf5" + assert_predicate testpath_case/"silo_hdf5", :directory? + end +end