diff --git a/.github/workflows/conda_package.yml b/.github/workflows/conda_package.yml new file mode 100644 index 0000000..23edd46 --- /dev/null +++ b/.github/workflows/conda_package.yml @@ -0,0 +1,121 @@ +name: Build and Publish Conda package + +on: + workflow_dispatch: + inputs: + publish: + description: "Upload built artifacts to Anaconda channel" + type: boolean + required: false + default: false + push: + tags: + - "v*" + +jobs: + build: + name: Build conda package (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.allow_failure }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + allow_failure: false + - os: macos-15 + allow_failure: false + - os: windows-latest + allow_failure: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + auto-activate-base: true + channels: conda-forge + channel-priority: strict + conda-remove-defaults: true + + - name: Install build tooling + shell: bash -l {0} + run: | + conda install -y conda-build conda-index anaconda-client + + - name: Build package + shell: bash -l {0} + run: | + bash packaging/conda/build_local.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: conda-build-${{ matrix.os }} + path: | + packaging/conda/build-artifacts/**/*.conda + packaging/conda/build-artifacts/**/repodata.json + packaging/conda/build-artifacts/**/current_repodata.json + if-no-files-found: error + + publish: + name: Publish to Anaconda channel + needs: build + runs-on: ubuntu-latest + if: >- + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs.publish) + + steps: + - name: Download all conda artifacts + uses: actions/download-artifact@v4 + with: + path: conda-artifacts + pattern: conda-build-* + merge-multiple: true + + - name: List downloaded artifacts + shell: bash + run: | + find conda-artifacts -type f -name '*.conda' -print + + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + auto-activate-base: true + channels: conda-forge + channel-priority: strict + conda-remove-defaults: true + + - name: Install upload tooling + shell: bash -l {0} + run: | + conda install -y anaconda-client + + - name: Upload to Anaconda + shell: bash -l {0} + env: + ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }} + ANACONDA_CHANNEL: ${{ secrets.ANACONDA_CHANNEL }} + run: | + if [[ -z "${ANACONDA_API_TOKEN:-}" ]]; then + echo "ANACONDA_API_TOKEN secret is not set" >&2 + exit 1 + fi + if [[ -z "${ANACONDA_CHANNEL:-}" ]]; then + echo "ANACONDA_CHANNEL secret is not set" >&2 + exit 1 + fi + + mapfile -t files < <(find conda-artifacts -type f -name '*.conda' | sort) + if [[ ${#files[@]} -eq 0 ]]; then + echo "No .conda artifacts found to upload" >&2 + exit 1 + fi + + anaconda -t "${ANACONDA_API_TOKEN}" upload \ + --user "${ANACONDA_CHANNEL}" \ + --label main \ + "${files[@]}" diff --git a/README.md b/README.md index 2661e4d..aa1bf7a 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,24 @@ To install gcrack, follow these steps: conda activate gcrack ``` -2. **Install the required dependencies**: +2. **Install gcrack**: ```shell - conda install -c conda-forge numpy sympy mpich python-gmsh fenics-dolfinx pyvista jax jaxlib=*=*cpu* + pip install . # If you cloned the repo + pip install gcrack # If you want to install from PyPI ``` -3. **Install gcrack**: +3. **Alternative (fully conda-managed stack)**: ```shell - pip install . # If you cloned the repo - pip install gcrack # If you want to install from pypi + conda install -c conda-forge numpy sympy mpich python-gmsh fenics-dolfinx fenics-ufl jax jaxlib=*=*cpu* matplotlib ``` +When testing a locally built conda artifact, do not install the `.conda` file path directly. +Use a local channel path and package name so dependencies are solved: + +```shell +conda install -c file:///ABSOLUTE/PATH/TO/gcrack/packaging/conda/build-artifacts -c conda-forge gcrack +``` + ## How to Use Examples are provided in the `gcrack/examples/` directory. Each example typically includes: diff --git a/docs/code_audit_findings_2026-02-25.md b/docs/code_audit_findings_2026-02-25.md new file mode 100644 index 0000000..8a1a068 --- /dev/null +++ b/docs/code_audit_findings_2026-02-25.md @@ -0,0 +1,265 @@ +# gcrack Code Audit Findings + +Date: 2026-02-25 +Scope: `src/gcrack` <>, packaging/CI context reviewed for cross-checks + +## Executive Summary + +This audit identified: + +- **2 high-confidence runtime bug risks** +- **4 medium-severity robustness/scientific caveats** +- **several maintainability and packaging quality improvements** + +No syntax/compile diagnostics were raised by workspace tooling during this review, but several logic and API risks remain. + +--- + +## Severity Key + +- **Critical**: likely crash or invalid core output in normal usage +- **High**: strong bug risk or major correctness concern +- **Medium**: robustness/scientific caveat likely to surface in edge/common workflows +- **Low**: maintainability, clarity, or non-blocking quality issue + +--- + +## Findings + +## 1) Missing constructor argument in `ICrackBase` load-factor solver call + +- **Severity**: High +- **Location**: `src/gcrack/icrack.py` (around `load_factor_solver = LoadFactorSolver(model, self.Gc)`) +- **Evidence**: + - `LoadFactorSolver.__init__` expects `(model, Gc_func, xc)` in `src/gcrack/optimization_solvers.py` + - `ICrackBase.run()` currently passes only `(model, self.Gc)` +- **Risk/Impact**: + - Strong runtime failure risk (`TypeError`) when this code path is executed. +- **Recommendation**: + - Pass crack-tip position consistently (same approach as `GCrackBase` / `FCrackBase`). + +### Fix (explicit) + +1. In `src/gcrack/icrack.py`, update solver creation to: + - `load_factor_solver = LoadFactorSolver(model, self.Gc, crack_points[-1])` +2. Run one `ICrackBase`-derived example to confirm no constructor error. +3. Add/extend a test that executes this run path to prevent regressions. + +--- + +## 2) `FCrackBase.__post_init__` may fail when `da` is intentionally unset + +- **Severity**: High +- **Location**: `src/gcrack/fcrack.py` (`__post_init__`) +- **Evidence**: + - `da` is optional in dataclass (`da: Optional[float] = None`) + - `__post_init__` computes radii with `self.da` immediately + - control mode (`da` vs `dN`) is validated later in `run()` +- **Risk/Impact**: + - If user selects `dN` mode (`da=None`), radii computation can fail before useful validation. +- **Recommendation**: + - Validate control mode before radius computation, or lazily derive radii once `da` is known. + +### Fix (explicit) + +1. In `src/gcrack/fcrack.py`, guard `__post_init__`: + - if `self.da is None`, defer `R_int/R_ext` initialization. +2. In `run()`, after selecting control mode: + - if `control_type == "dN"`, either require a reference `da` for SIF radii or compute radii from a dedicated parameter. +3. Raise a clear `ValueError` if radii cannot be defined. +4. Add a test for `dN` mode with `da=None` to ensure predictable behavior. + +--- + +## 3) Duplicate CSV export behavior in `ICrackBase` + +- **Severity**: Medium +- **Location**: `src/gcrack/icrack.py` (`for sif_name in SIFs` block) +- **Evidence**: + - `export_res_to_csv(res, ...)` is inside loop over SIF keys. +- **Risk/Impact**: + - Multiple rows can be written for one simulation step, causing duplicated/ambiguous logs. +- **Recommendation**: + - Move CSV export outside SIF loop so one row is written per step. + +### Fix (explicit) + +1. In `src/gcrack/icrack.py`, keep only SIF assignments inside: + - `for sif_name in SIFs: res[sif_name] = SIFs[sif_name]` +2. Move `export_res_to_csv(res, dir_name / "results.csv")` to immediately after the loop. +3. Validate CSV row count equals number of simulated steps (plus header/initial state row). + +--- + +## 4) Unresolved scientific TODO in SIF scaling (T-stress) + +- **Severity**: Medium (scientific correctness) +- **Location**: `src/gcrack/sif.py` (Williams interpolation branch) +- **Evidence**: + - Explicit TODO comment indicates scaling may be wrong. +- **Risk/Impact**: + - T-stress estimates may be biased, affecting optimization and crack path/load outcomes. +- **Recommendation**: + - Add verification case(s) with analytical/benchmark references and confirm scaling constant. + +### Fix (explicit) + +1. In `src/gcrack/sif.py`, isolate current T-stress scaling in a named constant/function. +2. Build 1–2 benchmark cases with known `T` (or validated reference data). +3. Compare recovered `T` vs reference for mesh/radius sensitivity. +4. Update scaling factor and remove TODO once validated. +5. Add regression test around accepted tolerance. + +--- + +## 5) Placeholder string value in fatigue post-processing + +- **Severity**: Medium +- **Location**: `src/gcrack/fcrack.py` (`res["fracture_dissipation"] = "TODO"`) +- **Risk/Impact**: + - Breaks numeric expectations in downstream CSV analysis/plotting. +- **Recommendation**: + - Replace placeholder with numeric computation or explicitly nullable numeric field. + +### Fix (explicit) + +1. In `src/gcrack/fcrack.py`, replace: + - `res["fracture_dissipation"] = "TODO"` + with a numeric value (`float`) every step. +2. If physics model is pending, use `np.nan` (documented) rather than a string. +3. Ensure CSV consumers can parse the column as numeric. + +--- + +## 6) Ambiguous DOF emptiness check in nodal displacement handling + +- **Severity**: Medium +- **Location**: `src/gcrack/boundary_conditions.py` (`if not dof:`) +- **Risk/Impact**: + - Depending on returned type, truth-value evaluation may be ambiguous or incorrect. +- **Recommendation**: + - Use explicit cardinality checks (`len(...)`, `.size`, or API-specific equivalent). + +### Fix (explicit) + +1. In `src/gcrack/boundary_conditions.py`, replace `if not dof:` with explicit check: + - e.g., `if len(dof) == 0:` (or API-appropriate `.size == 0`). +2. Keep current error message but include boundary/node context for debugging. +3. Add a small test case where nodal target is missing. + +--- + +## 7) Probe evaluation without collision guard + +- **Severity**: Medium +- **Location**: `src/gcrack/postprocess.py` (`cell = colliding_cells.array[0]`) +- **Risk/Impact**: + - Fails when probe point is outside mesh or no containing cell is found. +- **Recommendation**: + - Guard empty results and raise actionable error message. + +### Fix (explicit) + +1. In `src/gcrack/postprocess.py`, before `colliding_cells.array[0]`, check: + - whether `colliding_cells.array` is non-empty. +2. If empty, raise `ValueError` with point coordinates and a hint (point outside mesh / tolerance issue). +3. Optionally add fallback nearest-cell probing only if physically acceptable. + +--- + +## 8) Side-effect mutation of user-provided measurement point list + +- **Severity**: Low +- **Location**: `src/gcrack/postprocess.py` (`x.append(0)`) +- **Risk/Impact**: + - Mutates data returned by user callback, potentially causing subtle repeated-call behavior. +- **Recommendation**: + - Work on a copy instead of mutating `x` in place. + +### Fix (explicit) + +1. In `src/gcrack/postprocess.py`, avoid `x.append(0)`. +2. Use non-mutating construction: + - `x_local = list(x)` then append to `x_local`, or create a new array directly. +3. Keep callback output immutable from caller perspective. + +--- + +## 9) Heavy imports at package initialization + +- **Severity**: Low/Medium (ergonomics & failure surface) +- **Location**: `src/gcrack/__init__.py` +- **Evidence**: + - Top-level imports pull major solver classes immediately. +- **Risk/Impact**: + - `import gcrack` can fail early due to heavy optional stack, and import time increases. +- **Recommendation**: + - Consider lazy exports or lightweight package init. + +### Fix (explicit) + +1. In `src/gcrack/__init__.py`, avoid eager imports of heavy modules. +2. Prefer either: + - lightweight public symbols only, or + - lazy attribute loading (`__getattr__`) for `GCrackBase`, `ICrackBase`, `FCrackBase`. +3. Validate `import gcrack` in a minimal environment to confirm improved resilience. + +--- + +## 10) API/typing and consistency quality notes + +- **Severity**: Low +- **Locations**: + - `src/gcrack/postprocess.py`: force vector allocated with fixed length 3 for all assumptions + - `src/gcrack/utils/expression_parsers.py`: symbolic parser currently based on a single symbol `x` + - various docstrings/typos (non-blocking) +- **Risk/Impact**: + - Confusion and brittle behavior in edge cases. +- **Recommendation**: + - Align shapes with model assumptions; clarify parser expectations and supported expression forms. + +### Fix (explicit) + +1. In `src/gcrack/postprocess.py`, shape force arrays by active component count where practical. +2. In `src/gcrack/utils/expression_parsers.py`, document supported expression grammar and variable convention (`x` as coordinate vector). +3. Add examples for 2D expressions (e.g., `x[0]`, `x[1]`) and validate with unit tests. +4. Clean minor docstring typos during next maintenance pass. + +--- + +## Cross-Cutting Caveats (Packaging/CI affecting runtime confidence) + +These are not direct source bugs but influence reliability: + +1. **Conda dependency naming and availability sensitivity** (notably `fenics-*` stack). +2. **Windows CI is non-blocking by configuration**, so publish may proceed even when Windows build fails. +3. **Multi-Python matrix now includes 3.14**, but scientific stack availability may lag by platform. + +--- + +## Prioritized Fix Order + +1. Fix `ICrackBase -> LoadFactorSolver` constructor call mismatch. +2. Fix `FCrackBase.__post_init__` handling for `da=None` paths. +3. Move `ICrackBase` CSV write outside SIF loop. +4. Replace `fracture_dissipation = "TODO"` with numeric value (or explicit null strategy). +5. Add robust guards in point probing and DOF checks. +6. Validate/lock SIF T-stress scaling with benchmarks. + +--- + +## Suggested Validation After Fixes + +- Run existing tests under `tests/`. +- Add/extend targeted tests for: + - `ICrackBase` run path solver invocation + - `FCrackBase` with `dN` control and `da=None` + - one-row-per-step CSV invariant + - postprocess probe behavior outside mesh + - SIF benchmark(s) for Williams branch + +--- + +## Notes + +This report intentionally avoids applying changes and is meant to serve as an actionable backlog for incremental hardening. diff --git a/examples/E_HETEROGENEOUS_bi-material_CT/README.md b/examples/E_HETEROGENEOUS_01_bi-material_CT/README.md similarity index 100% rename from examples/E_HETEROGENEOUS_bi-material_CT/README.md rename to examples/E_HETEROGENEOUS_01_bi-material_CT/README.md diff --git a/examples/E_HETEROGENEOUS_bi-material_CT/display_results.py b/examples/E_HETEROGENEOUS_01_bi-material_CT/display_results.py similarity index 100% rename from examples/E_HETEROGENEOUS_bi-material_CT/display_results.py rename to examples/E_HETEROGENEOUS_01_bi-material_CT/display_results.py diff --git a/examples/E_HETEROGENEOUS_bi-material_CT/makefile b/examples/E_HETEROGENEOUS_01_bi-material_CT/makefile similarity index 100% rename from examples/E_HETEROGENEOUS_bi-material_CT/makefile rename to examples/E_HETEROGENEOUS_01_bi-material_CT/makefile diff --git a/examples/E_HETEROGENEOUS_bi-material_CT/run.py b/examples/E_HETEROGENEOUS_01_bi-material_CT/run.py similarity index 100% rename from examples/E_HETEROGENEOUS_bi-material_CT/run.py rename to examples/E_HETEROGENEOUS_01_bi-material_CT/run.py diff --git a/packaging/conda/.gitignore b/packaging/conda/.gitignore new file mode 100644 index 0000000..2c44c1c --- /dev/null +++ b/packaging/conda/.gitignore @@ -0,0 +1,3 @@ +build-artifacts/ +*.tar.bz2 +*.conda diff --git a/packaging/conda/README.md b/packaging/conda/README.md new file mode 100644 index 0000000..453519f --- /dev/null +++ b/packaging/conda/README.md @@ -0,0 +1,133 @@ +# Conda packaging for gcrack + +This directory contains a `conda-build` recipe and scripts to: + +1. Build locally from this repository. +2. Upload the built package to your Anaconda channel. + +## 1) Create a build environment + +```bash +conda create -n gcrack-conda-build -c conda-forge python=3.14 conda-build anaconda-client -y +conda activate gcrack-conda-build +``` + +The recipe builds variants for Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`. + +## 2) Build locally + +From the repository root: + +```bash +bash packaging/conda/build_local.sh +``` + +Artifacts are written under: + +```bash +packaging/conda/build-artifacts//gcrack-*.conda +``` + +Optional local installation test: + +```bash +conda create -n gcrack-local-test -c file:///Users/administrator/Desktop/repos/gcrack/packaging/conda/build-artifacts -c conda-forge python=3.12 gcrack -y +conda activate gcrack-local-test +python -c "import gcrack; print(gcrack.__name__)" +``` + +Note: installing a `.conda` file path directly (for example `.../gcrack-2026.01.26-py314_0.conda`) is an explicit spec and can bypass normal dependency solving. +Always install by package name from the local channel directory (`-c file:///.../build-artifacts`) so `run` dependencies are resolved. + +You can also use the helper script from repository root: + +```bash +bash packaging/conda/test_local_install.sh gcrack-local-test-312 3.12 +``` + +Run all supported Python local-install checks: + +```bash +bash packaging/conda/test_local_install_matrix.sh +``` + +## 3) Upload to your Anaconda channel + +Two authentication options are supported by `upload_channel.sh`: + +- `ANACONDA_API_TOKEN` environment variable (recommended for automation) +- interactive `anaconda login` + +Upload command: + +```bash +bash packaging/conda/upload_channel.sh +``` + +Install from your channel: + +```bash +conda install -c -c conda-forge gcrack +``` + +## 4) Release checklist + +For each new release: + +1. Update version in `pyproject.toml`. +2. Update `package.version` in `packaging/conda/meta.yaml`. +3. Rebuild with `bash packaging/conda/build_local.sh`. +4. Upload with `bash packaging/conda/upload_channel.sh `. + +## 5) GitHub CI/CD for Conda + +This repository includes: + +- `.github/workflows/conda_package.yml` + +What it does: + +1. Builds conda packages on `ubuntu-latest`, `macos-15`, and `windows-latest`. +2. Uploads build artifacts to the workflow run. +3. Optionally publishes artifacts to your Anaconda channel. + +Note: Windows is configured as non-blocking in CI because some scientific dependencies may be unavailable there depending on channel state. + +Trigger modes: + +- Manual: `workflow_dispatch` (set input `publish=true` to upload). +- Automatic publish: push a tag like `v2026.01.26`. + +Required repository secrets: + +- `ANACONDA_API_TOKEN`: token created on anaconda.org. +- `ANACONDA_CHANNEL`: your anaconda username/channel. + +After pushing a tag, install from your channel with: + +```bash +conda install -c -c conda-forge gcrack +``` + +## 6) Troubleshooting warnings + +If you see this during `conda build`: + +- `No numpy version specified in conda_build_config.yaml`: + - Fixed by `packaging/conda/conda_build_config.yaml` in this repository. +- `Number of parsed outputs does not match detected raw metadata blocks`: + - Usually a parser warning from templated metadata. The recipe here avoids Jinja output blocks to reduce this. +- `RequestsDependencyWarning ... urllib3/chardet/charset_normalizer`: + - This comes from your build environment, not `gcrack` itself. + - Refresh the build env packages, for example: + +```bash +conda activate gcrack-conda-build +conda install -c conda-forge "requests>=2.32" "urllib3>=2" "charset-normalizer>=3" -y +``` + +If dependency solving fails with missing packages, verify your channels expose these recipe names: + +- `fenics-dolfinx` +- `fenics-ufl` +- `python-gmsh` diff --git a/packaging/conda/build_local.sh b/packaging/conda/build_local.sh new file mode 100644 index 0000000..86d9d80 --- /dev/null +++ b/packaging/conda/build_local.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +OUTPUT_DIR="${SCRIPT_DIR}/build-artifacts" + +if ! command -v conda >/dev/null 2>&1; then + echo "Error: conda command not found in PATH." >&2 + exit 1 +fi + +mkdir -p "${OUTPUT_DIR}" + +echo "Building gcrack conda package from ${REPO_ROOT}" +conda build "${SCRIPT_DIR}" --override-channels -c conda-forge --output-folder "${OUTPUT_DIR}" "$@" + +echo +echo "Build completed. Artifacts are in:" +echo " ${OUTPUT_DIR}" diff --git a/packaging/conda/conda_build_config.yaml b/packaging/conda/conda_build_config.yaml new file mode 100644 index 0000000..a5b3667 --- /dev/null +++ b/packaging/conda/conda_build_config.yaml @@ -0,0 +1,9 @@ +python: + - 3.10 + - 3.11 + - 3.12 + - 3.13 + - 3.14 + +numpy: + - 1.26 diff --git a/packaging/conda/meta.yaml b/packaging/conda/meta.yaml new file mode 100644 index 0000000..ed487b1 --- /dev/null +++ b/packaging/conda/meta.yaml @@ -0,0 +1,50 @@ +package: + name: gcrack + version: 2026.01.26 + +source: + path: ../.. + +build: + number: 0 + skip: true # [py<310] + script: "{{ PYTHON }} -m pip install . -vv" + +requirements: + host: + - python >=3.10 + - pip + - setuptools >=77 + run: + - python >=3.10 + - numpy + - sympy + - jax + - jaxlib + - python-gmsh + - gmsh + - fenics-dolfinx + - fenics-ufl + - mpi4py + - matplotlib-base + +test: + requires: + - pip + imports: + - gcrack + commands: + - python -m pip check + - python -c "import gcrack; print('gcrack import ok')" + +about: + home: https://github.com/floiseau/gcrack + summary: Solver for Linear Elastic Fracture Mechanics problems + license: Apache-2.0 + license_file: LICENSE + doc_url: https://floiseau.github.io/gcrack + dev_url: https://github.com/floiseau/gcrack + +extra: + recipe-maintainers: + - floiseau diff --git a/packaging/conda/test_local_install.sh b/packaging/conda/test_local_install.sh new file mode 100644 index 0000000..ce4c9fa --- /dev/null +++ b/packaging/conda/test_local_install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ENV_NAME="${1:-gcrack-local-test}" +PYTHON_VERSION="${2:-3.14}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOCAL_CHANNEL="file://${SCRIPT_DIR}/build-artifacts" + +if ! command -v conda >/dev/null 2>&1; then + echo "Error: conda command not found in PATH." >&2 + exit 1 +fi + +echo "Creating test environment '${ENV_NAME}' and installing gcrack from local channel" +conda create -n "${ENV_NAME}" -c "${LOCAL_CHANNEL}" -c conda-forge python="${PYTHON_VERSION}" gcrack -y + +echo +echo "To validate in the new environment, run:" +echo " conda activate ${ENV_NAME}" +echo " conda list | grep -E 'gcrack|fenics-dolfinx|fenics-ufl|python-gmsh|jax|numpy'" +echo " python -c \"import gcrack; print(gcrack.__name__)\"" diff --git a/packaging/conda/test_local_install_matrix.sh b/packaging/conda/test_local_install_matrix.sh new file mode 100644 index 0000000..7e7847b --- /dev/null +++ b/packaging/conda/test_local_install_matrix.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PREFIX="${1:-gcrack-local-test}" + +PYTHON_VERSIONS=(3.10 3.11 3.12 3.13 3.14) + +echo "Testing local conda install matrix for: ${PYTHON_VERSIONS[*]}" + +for pyver in "${PYTHON_VERSIONS[@]}"; do + env_name="${PREFIX}-${pyver//./}" + echo + echo "=== [${pyver}] Creating environment: ${env_name} ===" + bash "${SCRIPT_DIR}/test_local_install.sh" "${env_name}" "${pyver}" + + echo "=== [${pyver}] Running smoke checks ===" + conda run -n "${env_name}" python -m pip check + conda run -n "${env_name}" python -c "import gcrack; print(gcrack.__name__)" +done + +echo +echo "Matrix test completed successfully." diff --git a/packaging/conda/upload_channel.sh b/packaging/conda/upload_channel.sh new file mode 100644 index 0000000..149fac6 --- /dev/null +++ b/packaging/conda/upload_channel.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [label]" >&2 + echo "Example: $0 floiseau" >&2 + echo "Example: $0 floiseau dev" >&2 + exit 1 +fi + +CHANNEL="$1" +LABEL="${2:-main}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ARTIFACT_GLOB="${SCRIPT_DIR}/build-artifacts/*/gcrack-*.conda" + +shopt -s nullglob +artifacts=( ${ARTIFACT_GLOB} ) +shopt -u nullglob + +if [[ ${#artifacts[@]} -eq 0 ]]; then + echo "Error: no package artifact found under ${SCRIPT_DIR}/build-artifacts" >&2 + echo "Run: bash packaging/conda/build_local.sh" >&2 + exit 1 +fi + +if ! command -v anaconda >/dev/null 2>&1; then + echo "Error: anaconda client not found. Install with: conda install -c conda-forge anaconda-client" >&2 + exit 1 +fi + +if [[ -n "${ANACONDA_API_TOKEN:-}" ]]; then + anaconda -t "${ANACONDA_API_TOKEN}" whoami >/dev/null + AUTH_ARGS=( -t "${ANACONDA_API_TOKEN}" ) +else + echo "ANACONDA_API_TOKEN not set. Falling back to interactive login (anaconda login)." + anaconda login + AUTH_ARGS=() +fi + +echo "Uploading ${#artifacts[@]} artifact(s) to channel '${CHANNEL}' with label '${LABEL}'" +anaconda "${AUTH_ARGS[@]}" upload --user "${CHANNEL}" --label "${LABEL}" "${artifacts[@]}" + +echo "Upload complete." diff --git a/pyproject.toml b/pyproject.toml index 20c61eb..13f52b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,29 @@ description = "Solver for Linear Elastic Fracture Mechanics problems " readme = "README.md" license = { file="LICENSE" } requires-python = ">=3.10.0" -# dependencies = [ -# "numpy", -# "jax", -# "sympy", -# "pyvista", -# "gmsh", -# "fenics-dolfinx", -# "fenics-ufl", -# ] +dependencies = [ + "numpy", + "sympy", + "jax", + "jaxlib", + "gmsh", + "mpi4py", + "matplotlib", + "fenics-dolfinx", + "fenics-ufl", +] + +[project.optional-dependencies] +viz = [ + "pyvista", +] [build-system] requires = ["setuptools >= 77.0.3"] build-backend = "setuptools.build_meta" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"]