diff --git a/.github/workflows/comment-evolution-plot.yml b/.github/workflows/comment-evolution-plot.yml
new file mode 100644
index 000000000..ed7208168
--- /dev/null
+++ b/.github/workflows/comment-evolution-plot.yml
@@ -0,0 +1,98 @@
+name: Comment PR with Evolution Plot
+
+on:
+ workflow_run:
+ workflows:
+ - COMPAS compile test
+ types:
+ - completed
+
+permissions:
+ actions: read
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ comment-with-plot:
+ name: Post evolution plot comment
+ runs-on: ubuntu-22.04
+ if: >
+ github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.pull_requests[0].number
+
+ env:
+ ARTIFACT_NAME: detailedEvolutionPlot.png
+
+ steps:
+ - name: Download evolution plot artifact from triggering run
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+ const runId = context.payload.workflow_run.id;
+ const runNumber = context.payload.workflow_run.run_number;
+ const artifactName = `evolution-plot-${runNumber}`;
+ const { owner, repo } = context.repo;
+
+ const artifacts = await github.paginate(
+ github.rest.actions.listWorkflowRunArtifacts,
+ { owner, repo, run_id: runId, per_page: 100 }
+ );
+ const artifact = artifacts.find((entry) => entry.name === artifactName);
+
+ if (!artifact) {
+ core.setFailed(`Artifact '${artifactName}' not found for workflow run ${runId}`);
+ return;
+ }
+
+ const download = await github.rest.actions.downloadArtifact({
+ owner,
+ repo,
+ artifact_id: artifact.id,
+ archive_format: 'zip',
+ });
+
+ fs.writeFileSync(
+ path.join(process.env.GITHUB_WORKSPACE, 'evolution-plot.zip'),
+ Buffer.from(download.data)
+ );
+
+ - name: Unpack evolution plot artifact
+ run: |
+ mkdir -p evolution-plot
+ unzip -o evolution-plot.zip -d evolution-plot
+ test -f "evolution-plot/${ARTIFACT_NAME}"
+
+ - name: Create report with evolution plot
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
+ HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
+ RUN_ID: ${{ github.event.workflow_run.id }}
+ run: |
+ echo "## ✅ COMPAS Build Successful!" >> report.md
+ echo "" >> report.md
+ echo "| Item | Value |" >> report.md
+ echo "|------|-------|" >> report.md
+ echo "| **Commit** | [\`$(echo "$HEAD_SHA" | cut -c1-7)\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${HEAD_SHA}) |" >> report.md
+ echo "| **Logs** | [View workflow](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}) |" >> report.md
+ echo "" >> report.md
+
+ if [ -f "evolution-plot/${ARTIFACT_NAME}" ]; then
+ echo "### Detailed Evolution Plot" >> report.md
+ echo "Click to view evolution plot
" >> report.md
+ echo "" >> report.md
+ echo "" >> report.md
+ echo " " >> report.md
+ else
+ echo "### ⚠️ Evolution plot not found" >> report.md
+ fi
+
+ echo "" >> report.md
+ echo "---" >> report.md
+ echo "Generated by COMPAS CI " >> report.md
+
+ npx -y @dvcorg/cml comment create --pr "$PR_NUMBER" report.md
diff --git a/.github/workflows/compas-compile-ci.yml b/.github/workflows/compas-compile-ci.yml
index 4ee1eb40d..e11255923 100644
--- a/.github/workflows/compas-compile-ci.yml
+++ b/.github/workflows/compas-compile-ci.yml
@@ -19,6 +19,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+permissions:
+ contents: read
+
jobs:
compas:
env:
@@ -40,7 +43,7 @@ jobs:
echo "Building COMPAS from source..."
cd src
make clean 2>/dev/null || echo 'No existing build to clean'
- make -j $(nproc) -f Makefile
+ make DOCKER_BUILD=1 CPP="g++" -j "$(nproc)" -f Makefile
./COMPAS -v
echo "COMPAS build completed successfully!"
@@ -106,51 +109,3 @@ jobs:
echo "- Python Dependencies: Success" >> $GITHUB_STEP_SUMMARY
echo "- Example COMPAS Job: Success" >> $GITHUB_STEP_SUMMARY
echo "- Pytest Suite: Success" >> $GITHUB_STEP_SUMMARY
-
- comment-with-plot:
- name: Comment PR with Evolution Plot
- runs-on: ubuntu-22.04
- container: docker://ghcr.io/iterative/cml:0-dvc2-base1
- needs: compas
- if: github.event_name == 'pull_request'
-
- env:
- ARTIFACT_NAME: detailedEvolutionPlot.png
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Download evolution plot
- uses: actions/download-artifact@v4
- with:
- name: evolution-plot-${{ github.run_number }}
-
- - name: Create report with evolution plot
- env:
- REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- echo "## ✅ COMPAS Build Successful!" >> report.md
- echo "" >> report.md
- echo "| Item | Value |" >> report.md
- echo "|------|-------|" >> report.md
- echo "| **Commit** | [\`$(echo $GITHUB_SHA | cut -c1-7)\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}) |" >> report.md
- echo "| **Logs** | [View workflow](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) |" >> report.md
- echo "" >> report.md
-
- if [ -f "${{ env.ARTIFACT_NAME }}" ]; then
- echo "### Detailed Evolution Plot" >> report.md
- echo "Click to view evolution plot
" >> report.md
- echo "" >> report.md
- echo "" >> report.md
- echo " " >> report.md
- else
- echo "### ⚠️ Evolution plot not found" >> report.md
- fi
-
- echo "" >> report.md
- echo "---" >> report.md
- echo "Generated by COMPAS CI " >> report.md
-
- # Post the report using CML
- cml comment create report.md
\ No newline at end of file
diff --git a/.github/workflows/native-linux-bundle.yml b/.github/workflows/native-linux-bundle.yml
new file mode 100644
index 000000000..564731330
--- /dev/null
+++ b/.github/workflows/native-linux-bundle.yml
@@ -0,0 +1,199 @@
+name: Native Linux bundle
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - dev
+ paths:
+ - src/**
+ - misc/cicd-scripts/**
+ - .github/workflows/native-linux-bundle.yml
+ push:
+ branches:
+ - dev
+ paths:
+ - src/**
+ - misc/cicd-scripts/**
+ - .github/workflows/native-linux-bundle.yml
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ native-linux-bundle:
+ name: Build native Linux bundle
+ runs-on: ubuntu-22.04
+
+ env:
+ BUNDLE_DIR: dist/COMPAS-linux-x86_64
+ BUNDLE_TARBALL: dist/COMPAS-linux-x86_64.tar.gz
+ CCACHE_DIR: ${{ github.workspace }}/.ccache
+ CCACHE_BASEDIR: ${{ github.workspace }}
+ CCACHE_COMPILERCHECK: content
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Restore compiler cache
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.CCACHE_DIR }}
+ key: ${{ runner.os }}-native-linux-bundle-ccache-${{ hashFiles('src/Makefile', 'misc/cicd-scripts/linux-dependencies', '.github/workflows/native-linux-bundle.yml') }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-native-linux-bundle-ccache-${{ hashFiles('src/Makefile', 'misc/cicd-scripts/linux-dependencies', '.github/workflows/native-linux-bundle.yml') }}-
+
+ - name: Install native build dependencies
+ run: |
+ # Keep this workflow lean: install only the native compiler and link dependencies.
+ bash misc/cicd-scripts/linux-dependencies native-build
+
+ - name: Build COMPAS from source
+ working-directory: src
+ run: |
+ ccache --zero-stats || true
+ ccache --max-size=500M || true
+ make DOCKER_BUILD=1 CPP="ccache g++" -j "$(nproc)" -f Makefile
+ ./COMPAS -v
+ ccache --show-stats || true
+
+ - name: Run native smoke test
+ working-directory: src
+ run: |
+ rm -rf smoke-output
+ ./COMPAS \
+ -n 1 \
+ --initial-mass-1 35 \
+ --initial-mass-2 31 \
+ -a 3.5 \
+ --random-seed 0 \
+ --metallicity 0.001 \
+ --quiet \
+ -o smoke-output
+ test -d smoke-output
+ find smoke-output -maxdepth 2 -type f | sort | sed -n '1,40p'
+
+ - name: Inspect runtime dependencies
+ run: |
+ echo "Runtime dependencies for native build:"
+ ldd src/COMPAS
+
+ - name: Package portable Linux bundle
+ run: |
+ bash misc/cicd-scripts/package-linux-bundle.sh src/COMPAS "$BUNDLE_DIR"
+
+ - name: Test bundled launcher
+ run: |
+ "$BUNDLE_DIR/run_compas.sh" -v
+
+ BUNDLE_SMOKE_DIR="${RUNNER_TEMP}/compas-bundle-smoke"
+ rm -rf "$BUNDLE_SMOKE_DIR"
+
+ "$BUNDLE_DIR/run_compas.sh" \
+ -n 1 \
+ --initial-mass-1 35 \
+ --initial-mass-2 31 \
+ -a 3.5 \
+ --random-seed 0 \
+ --metallicity 0.001 \
+ --quiet \
+ -o "$BUNDLE_SMOKE_DIR"
+
+ test -d "$BUNDLE_SMOKE_DIR"
+
+ echo "Runtime dependencies for bundled binary with bundled lib/:"
+ LD_LIBRARY_PATH="${GITHUB_WORKSPACE}/$BUNDLE_DIR/lib" ldd "$BUNDLE_DIR/bin/COMPAS"
+
+ - name: Archive bundle tarball
+ run: |
+ tar -C dist -czf "$BUNDLE_TARBALL" COMPAS-linux-x86_64
+
+ - name: Upload bundle artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: COMPAS-linux-x86_64
+ path: ${{ env.BUNDLE_TARBALL }}
+ if-no-files-found: error
+
+ verify-native-linux-bundle:
+ name: Verify bundled artifact on fresh runner
+ runs-on: ubuntu-22.04
+ needs: native-linux-bundle
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Download bundle artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: COMPAS-linux-x86_64
+ path: downloaded-artifact
+
+ - name: Install Python runner only
+ run: |
+ python -m pip install --no-deps -e .
+
+ - name: Unpack bundled artifact
+ run: |
+ mkdir -p bundle-test
+ tar -xzf downloaded-artifact/COMPAS-linux-x86_64.tar.gz -C bundle-test
+ test -x bundle-test/COMPAS-linux-x86_64/run_compas.sh
+
+ - name: Inspect downloaded bundle dependencies
+ run: |
+ LD_LIBRARY_PATH="${GITHUB_WORKSPACE}/bundle-test/COMPAS-linux-x86_64/lib" \
+ ldd bundle-test/COMPAS-linux-x86_64/bin/COMPAS | tee bundle-test/ldd.txt
+
+ ! grep -q 'not found' bundle-test/ldd.txt
+ grep -q 'bundle-test/COMPAS-linux-x86_64/lib/libhdf5_serial' bundle-test/ldd.txt
+ grep -q 'bundle-test/COMPAS-linux-x86_64/lib/libgsl' bundle-test/ldd.txt
+ grep -q 'bundle-test/COMPAS-linux-x86_64/lib/libboost_program_options' bundle-test/ldd.txt
+
+ - name: Run downloaded bundle smoke test
+ run: |
+ ./bundle-test/COMPAS-linux-x86_64/run_compas.sh -v
+
+ ARTIFACT_SMOKE_DIR="${RUNNER_TEMP}/compas-downloaded-bundle-smoke"
+ rm -rf "$ARTIFACT_SMOKE_DIR"
+
+ ./bundle-test/COMPAS-linux-x86_64/run_compas.sh \
+ -n 1 \
+ --initial-mass-1 35 \
+ --initial-mass-2 31 \
+ -a 3.5 \
+ --random-seed 0 \
+ --metallicity 0.001 \
+ --quiet \
+ -o "$ARTIFACT_SMOKE_DIR"
+
+ test -d "$ARTIFACT_SMOKE_DIR"
+
+ - name: Run Python runner against downloaded bundle
+ env:
+ COMPAS_BUNDLE_ROOT: ${{ github.workspace }}/bundle-test/COMPAS-linux-x86_64
+ run: |
+ compas_run --print-path
+ compas_run -v
+
+ PYTHON_RUNNER_SMOKE_DIR="${RUNNER_TEMP}/compas-python-runner-smoke"
+ rm -rf "$PYTHON_RUNNER_SMOKE_DIR"
+
+ compas_run \
+ -n 1 \
+ --initial-mass-1 35 \
+ --initial-mass-2 31 \
+ -a 3.5 \
+ --random-seed 0 \
+ --metallicity 0.001 \
+ --quiet \
+ -o "$PYTHON_RUNNER_SMOKE_DIR"
+
+ test -d "$PYTHON_RUNNER_SMOKE_DIR"
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
new file mode 100644
index 000000000..5c6f5a72f
--- /dev/null
+++ b/.github/workflows/publish-pypi.yml
@@ -0,0 +1,193 @@
+name: Publish compas-popsynth to PyPI
+
+on:
+ push:
+ branches:
+ - dev
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ mode:
+ description: Build only or publish to PyPI
+ required: true
+ default: dry-run
+ type: choice
+ options:
+ - dry-run
+ - publish
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-linux-wheel:
+ name: Build and verify Linux wheel
+ runs-on: ubuntu-22.04
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Build Linux wheel with cibuildwheel
+ uses: pypa/cibuildwheel@v3.1.2
+ env:
+ CIBW_BUILD: cp311-manylinux_x86_64
+ CIBW_ARCHS_LINUX: x86_64
+ CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
+ CIBW_ENVIRONMENT_LINUX: COMPAS_BINARY_WHEEL=1
+ CIBW_BEFORE_ALL_LINUX: >
+ bash {project}/misc/cicd-scripts/manylinux-dependencies &&
+ make -C {project}/src DOCKER_BUILD=1 CPP=g++ -j "$(nproc)" -f Makefile &&
+ {project}/src/COMPAS -v &&
+ bash {project}/misc/cicd-scripts/package-linux-bundle.sh {project}/src/COMPAS {project}/dist/COMPAS-linux-x86_64 &&
+ rm -rf {project}/compas_python_utils/bundled/COMPAS-linux-x86_64 &&
+ mkdir -p {project}/compas_python_utils/bundled &&
+ cp -a {project}/dist/COMPAS-linux-x86_64 {project}/compas_python_utils/bundled/ &&
+ test -f {project}/compas_python_utils/bundled/COMPAS-linux-x86_64/run_compas.sh
+ CIBW_TEST_COMMAND_LINUX: >
+ compas_run --print-path &&
+ compas_run -v &&
+ WHEEL_SMOKE_DIR="$(mktemp -d)" &&
+ compas_run -n 1 --initial-mass-1 35 --initial-mass-2 31 -a 3.5 --random-seed 0 --metallicity 0.001 --quiet -o "$WHEEL_SMOKE_DIR" &&
+ test -d "$WHEEL_SMOKE_DIR"
+
+ - name: Upload wheel artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: compas-popsynth-wheel-linux
+ path: wheelhouse/*.whl
+ if-no-files-found: error
+
+ build-macos-arm64-wheel:
+ name: Build and verify macOS arm64 wheel
+ runs-on: macos-14
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Build macOS arm64 wheel with cibuildwheel
+ uses: pypa/cibuildwheel@v3.1.2
+ env:
+ CIBW_BUILD: cp311-macosx_arm64
+ CIBW_ARCHS_MACOS: arm64
+ CIBW_ENVIRONMENT_MACOS: COMPAS_BINARY_WHEEL=1 MACOSX_DEPLOYMENT_TARGET=14.0
+ CIBW_REPAIR_WHEEL_COMMAND_MACOS: ""
+ CIBW_BEFORE_ALL_MACOS: >
+ bash {project}/misc/cicd-scripts/macos-wheel-dependencies &&
+ make -C {project}/src CPP=clang++ -j "$(sysctl -n hw.logicalcpu)" -f Makefile &&
+ test -x {project}/src/COMPAS &&
+ bash {project}/misc/cicd-scripts/package-macos-bundle.sh {project}/src/COMPAS arm64 {project}/dist/COMPAS-macos-arm64 &&
+ rm -rf {project}/compas_python_utils/bundled/COMPAS-macos-arm64 &&
+ mkdir -p {project}/compas_python_utils/bundled &&
+ cp -R {project}/dist/COMPAS-macos-arm64 {project}/compas_python_utils/bundled/ &&
+ test -f {project}/compas_python_utils/bundled/COMPAS-macos-arm64/run_compas.sh
+ CIBW_TEST_COMMAND_MACOS: >
+ compas_run --print-path &&
+ compas_run -v &&
+ WHEEL_SMOKE_DIR="$(mktemp -d)" &&
+ compas_run -n 1 --initial-mass-1 35 --initial-mass-2 31 -a 3.5 --random-seed 0 --metallicity 0.001 --quiet -o "$WHEEL_SMOKE_DIR" &&
+ test -d "$WHEEL_SMOKE_DIR"
+
+ - name: Upload wheel artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: compas-popsynth-wheel-macos-arm64
+ path: wheelhouse/*.whl
+ if-no-files-found: error
+
+ build-macos-x86_64-wheel:
+ name: Build and verify macOS x86_64 wheel
+ runs-on: macos-15-intel
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Build macOS x86_64 wheel with cibuildwheel
+ uses: pypa/cibuildwheel@v3.1.2
+ env:
+ CIBW_BUILD: cp311-macosx_x86_64
+ CIBW_ARCHS_MACOS: x86_64
+ CIBW_ENVIRONMENT_MACOS: COMPAS_BINARY_WHEEL=1 MACOSX_DEPLOYMENT_TARGET=14.0
+ CIBW_REPAIR_WHEEL_COMMAND_MACOS: ""
+ CIBW_BEFORE_ALL_MACOS: >
+ bash {project}/misc/cicd-scripts/macos-wheel-dependencies &&
+ make -C {project}/src CPP=clang++ -j "$(sysctl -n hw.logicalcpu)" -f Makefile &&
+ test -x {project}/src/COMPAS &&
+ bash {project}/misc/cicd-scripts/package-macos-bundle.sh {project}/src/COMPAS x86_64 {project}/dist/COMPAS-macos-x86_64 &&
+ rm -rf {project}/compas_python_utils/bundled/COMPAS-macos-x86_64 &&
+ mkdir -p {project}/compas_python_utils/bundled &&
+ cp -R {project}/dist/COMPAS-macos-x86_64 {project}/compas_python_utils/bundled/ &&
+ test -f {project}/compas_python_utils/bundled/COMPAS-macos-x86_64/run_compas.sh
+ CIBW_TEST_COMMAND_MACOS: >
+ compas_run --print-path &&
+ compas_run -v &&
+ WHEEL_SMOKE_DIR="$(mktemp -d)" &&
+ compas_run -n 1 --initial-mass-1 35 --initial-mass-2 31 -a 3.5 --random-seed 0 --metallicity 0.001 --quiet -o "$WHEEL_SMOKE_DIR" &&
+ test -d "$WHEEL_SMOKE_DIR"
+
+ - name: Upload wheel artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: compas-popsynth-wheel-macos-x86_64
+ path: wheelhouse/*.whl
+ if-no-files-found: error
+
+ publish-pypi:
+ name: Publish wheel to PyPI
+ runs-on: ubuntu-22.04
+ needs:
+ - build-linux-wheel
+ - build-macos-arm64-wheel
+ - build-macos-x86_64-wheel
+ if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'publish') }}
+ environment: pypi
+ permissions:
+ id-token: write
+
+ steps:
+ - name: Download Linux wheel artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: compas-popsynth-wheel-linux
+ path: dist
+
+ - name: Download macOS arm64 wheel artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: compas-popsynth-wheel-macos-arm64
+ path: dist
+
+ - name: Download macOS x86_64 wheel artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: compas-popsynth-wheel-macos-x86_64
+ path: dist
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: dist/
+ verbose: true
+ attestations: false
diff --git a/.gitignore b/.gitignore
index 519579538..14eb83f46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,10 +21,12 @@ Thumbs.db
# Others
.ipynb_checkpoints/
__pycache__/
+.ccache/
COMPAS_Output*/
output*/
*.coverage
test_artifacts/
+build/
COMPAS
*.log
@@ -39,6 +41,8 @@ COMPAS
*.synctex(busy)
*.synctex.gz
*.synctex.gz(busy)
+dist/
+compas_python_utils/bundled/COMPAS-linux-x86_64/
# docs html
*_build
diff --git a/README.md b/README.md
index 11ff49b4f..bdba8e989 100644
--- a/README.md
+++ b/README.md
@@ -105,5 +105,3 @@ Furthermore,
### Highlighted papers that have made use of COMPAS are listed at https://compas.science/science.html ; see https://ui.adsabs.harvard.edu/public-libraries/gzRk1qpbRUy4cP2GydR36Q for a full ADS library
-
-
diff --git a/compas_python_utils/__init__.py b/compas_python_utils/__init__.py
index bb4b3c31a..3756caee1 100644
--- a/compas_python_utils/__init__.py
+++ b/compas_python_utils/__init__.py
@@ -1,11 +1,35 @@
+import re
+from pathlib import Path
+
+try:
+ from importlib.metadata import PackageNotFoundError, version as package_version
+except ImportError: # pragma: no cover
+ from importlib_metadata import PackageNotFoundError, version as package_version
+
+
+def _version_from_changelog() -> str:
+ changelog_path = Path(__file__).resolve().parent.parent / "src" / "changelog.h"
+ version_match = re.search(
+ r'VERSION_STRING = ["\']([^"\']+)["\']',
+ changelog_path.read_text(encoding="utf-8"),
+ )
+ if not version_match:
+ return "0.0.0"
+ return ".".join(str(int(part)) for part in version_match.group(1).split("."))
+
+
__all__ = []
__author__ = "Team COMPAS"
-__email__ = "compas email"
+__email__ = "teamcompas@users.noreply.github.com"
__uri__ = "https://github.com/TeamCOMPAS/COMPAS"
__license__ = "MIT"
__description__ = "COMPAS"
__copyright__ = "Copyright 2022 COMPAS developers"
__contributors__ = "https://github.com/TeamCOMPAS/COMPAS/graphs/contributors"
-__version__ = "0.0.1"
\ No newline at end of file
+
+try:
+ __version__ = package_version("compas-popsynth")
+except PackageNotFoundError:
+ __version__ = _version_from_changelog()
diff --git a/compas_python_utils/compas_runner.py b/compas_python_utils/compas_runner.py
new file mode 100644
index 000000000..052b440fc
--- /dev/null
+++ b/compas_python_utils/compas_runner.py
@@ -0,0 +1,137 @@
+import argparse
+import os
+import platform
+import subprocess
+import sys
+from pathlib import Path
+from typing import Iterable, Optional, Sequence
+
+
+PACKAGE_ROOT = Path(__file__).resolve().parent
+REPO_ROOT = PACKAGE_ROOT.parent
+BUNDLED_DIRECTORIES = (
+ "COMPAS-linux-x86_64",
+ "COMPAS-macos-arm64",
+ "COMPAS-macos-x86_64",
+)
+
+
+def _is_runnable_file(path: Path) -> bool:
+ return path.is_file() and (os.access(path, os.X_OK) or path.suffix == ".sh")
+
+
+def _validate_explicit_path(path: Path, variable_name: str) -> str:
+ if not _is_runnable_file(path):
+ raise FileNotFoundError(
+ f"{variable_name} points to a non-runnable path: {path}"
+ )
+ return str(path)
+
+
+def _normalized_machine() -> str:
+ machine = platform.machine().lower()
+ if machine in {"amd64", "x86_64"}:
+ return "x86_64"
+ if machine in {"arm64", "aarch64"}:
+ return "arm64"
+ return machine
+
+
+def _preferred_bundle_directories() -> Sequence[str]:
+ system = platform.system()
+ machine = _normalized_machine()
+
+ if system == "Linux":
+ preferred = [f"COMPAS-linux-{machine}"]
+ elif system == "Darwin":
+ preferred = [f"COMPAS-macos-{machine}"]
+ else:
+ preferred = []
+
+ return tuple(dict.fromkeys([*preferred, *BUNDLED_DIRECTORIES]))
+
+
+def _candidate_paths() -> Iterable[Path]:
+ bundle_root = os.environ.get("COMPAS_BUNDLE_ROOT")
+ if bundle_root:
+ bundle_path = Path(bundle_root)
+ yield bundle_path / "run_compas.sh"
+ yield bundle_path / "bin" / "COMPAS"
+
+ for bundle_directory in _preferred_bundle_directories():
+ yield PACKAGE_ROOT / "bundled" / bundle_directory / "run_compas.sh"
+ yield PACKAGE_ROOT / "bundled" / bundle_directory / "bin" / "COMPAS"
+
+ compas_root = Path(os.environ.get("COMPAS_ROOT_DIR", REPO_ROOT))
+ yield compas_root / "src" / "COMPAS"
+ yield compas_root / "bin" / "COMPAS"
+
+ yield REPO_ROOT / "src" / "COMPAS"
+ yield REPO_ROOT / "bin" / "COMPAS"
+
+
+def resolve_compas_executable() -> str:
+ explicit_path = os.environ.get("COMPAS_EXECUTABLE_PATH")
+ if explicit_path:
+ return _validate_explicit_path(Path(explicit_path), "COMPAS_EXECUTABLE_PATH")
+
+ bundle_root = os.environ.get("COMPAS_BUNDLE_ROOT")
+ if bundle_root:
+ candidates = [Path(bundle_root) / "run_compas.sh", Path(bundle_root) / "bin" / "COMPAS"]
+ for candidate in candidates:
+ if _is_runnable_file(candidate):
+ return str(candidate)
+ raise FileNotFoundError(
+ "COMPAS_BUNDLE_ROOT is set, but neither run_compas.sh nor bin/COMPAS "
+ f"exists under {bundle_root}"
+ )
+
+ for candidate in _candidate_paths():
+ if _is_runnable_file(candidate):
+ return str(candidate)
+
+ raise FileNotFoundError(
+ "Unable to locate a COMPAS executable. Set COMPAS_EXECUTABLE_PATH to an "
+ "existing executable, or set COMPAS_BUNDLE_ROOT to an extracted bundle directory. "
+ f"Detected platform: {platform.system()} ({_normalized_machine()})."
+ )
+
+
+def run_compas(
+ compas_args: Optional[Sequence[str]] = None,
+ executable: Optional[str] = None,
+ check: bool = True,
+ **kwargs,
+) -> subprocess.CompletedProcess:
+ resolved_executable = executable or resolve_compas_executable()
+ executable_path = Path(resolved_executable)
+ if executable_path.suffix == ".sh":
+ command = ["bash", resolved_executable, *(compas_args or [])]
+ else:
+ command = [resolved_executable, *(compas_args or [])]
+ return subprocess.run(command, check=check, **kwargs)
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ parser = argparse.ArgumentParser(
+ description="Run the COMPAS executable from Python.",
+ )
+ parser.add_argument(
+ "--print-path",
+ action="store_true",
+ help="Print the resolved COMPAS executable path and exit.",
+ )
+ args, compas_args = parser.parse_known_args(argv)
+
+ executable = resolve_compas_executable()
+
+ if args.print_path:
+ print(executable)
+ return 0
+
+ completed_process = run_compas(compas_args=compas_args, executable=executable, check=False)
+ return completed_process.returncode
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/compas_python_utils/preprocessing/runSubmit.py b/compas_python_utils/preprocessing/runSubmit.py
index 7df4c46cd..68ed4c00d 100644
--- a/compas_python_utils/preprocessing/runSubmit.py
+++ b/compas_python_utils/preprocessing/runSubmit.py
@@ -6,6 +6,8 @@
import argparse
import warnings
+from compas_python_utils.compas_runner import resolve_compas_executable
+
# Check if we are using python 3
python_version = sys.version_info[0]
print("python_version =", python_version)
@@ -43,15 +45,14 @@ def __init__(self, config_file=DEFAULT_CONFIG_FILE, grid_filename=None,
self.stringChoices = config['stringChoices'] if config['stringChoices'] else {}
self.listChoices = config['listChoices'] if config['listChoices'] else {}
- compas_root_dir = os.environ.get('COMPAS_ROOT_DIR', REPO_ROOT)
+ compas_root_dir = os.environ.get('COMPAS_ROOT_DIR')
if compas_root_dir is None:
warnings.warn(
'COMPAS_ROOT_DIR environment variable not set. Setting '
f'`export COMPAS_ROOT_DIR={REPO_ROOT}`'
)
os.environ['COMPAS_ROOT_DIR'] = REPO_ROOT
- compas_exe = os.path.join(compas_root_dir, 'src/COMPAS')
- compas_executable_override = os.environ.get('COMPAS_EXECUTABLE_PATH', compas_exe)
+ compas_executable_override = resolve_compas_executable()
print('compas_executable_override', compas_executable_override)
self.compas_executable = compas_executable_override
diff --git a/misc/cicd-scripts/linux-dependencies b/misc/cicd-scripts/linux-dependencies
old mode 100644
new mode 100755
index 7accaa532..610dcf7a1
--- a/misc/cicd-scripts/linux-dependencies
+++ b/misc/cicd-scripts/linux-dependencies
@@ -1,17 +1,48 @@
-#! /bin/sh
+#!/usr/bin/env bash
+set -euo pipefail
-sudo apt-get install -y \
- texlive-latex-recommended \
- texlive-latex-extra \
- texlive-fonts-recommended \
- texlive-fonts-extra \
- texlive-xetex \
- latexmk \
- xindy \
- dvipng \
- cm-super
-sudo apt install texlive-latex-extra --fix-missing
-sudo apt update && sudo apt install g++ libboost-all-dev libgsl-dev libhdf5-serial-dev
-sudo apt-get install gcc libpq-dev -y
-sudo apt-get install python3-dev python3-pip python3-venv python3-wheel -y
\ No newline at end of file
+PROFILE="${1:-full}"
+
+COMMON_BUILD_PACKAGES=(
+ ccache
+ g++
+ libboost-all-dev
+ libgsl-dev
+ libhdf5-serial-dev
+)
+
+FULL_ONLY_PACKAGES=(
+ texlive-latex-recommended
+ texlive-latex-extra
+ texlive-fonts-recommended
+ texlive-fonts-extra
+ texlive-xetex
+ latexmk
+ xindy
+ dvipng
+ cm-super
+ gcc
+ libpq-dev
+ python3-dev
+ python3-pip
+ python3-venv
+ python3-wheel
+)
+
+sudo apt-get update
+
+case "$PROFILE" in
+ native-build)
+ sudo apt-get install -y "${COMMON_BUILD_PACKAGES[@]}"
+ ;;
+ full)
+ sudo apt-get install -y "${FULL_ONLY_PACKAGES[@]}"
+ sudo apt-get install -y --fix-missing texlive-latex-extra
+ sudo apt-get install -y "${COMMON_BUILD_PACKAGES[@]}"
+ ;;
+ *)
+ echo "Usage: $0 [full|native-build]" >&2
+ exit 1
+ ;;
+esac
diff --git a/misc/cicd-scripts/macos-wheel-dependencies b/misc/cicd-scripts/macos-wheel-dependencies
new file mode 100755
index 000000000..b594fd195
--- /dev/null
+++ b/misc/cicd-scripts/macos-wheel-dependencies
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+# Install the Homebrew packages required to compile and bundle the macOS PyPI
+# wheel on GitHub's macOS runners.
+
+set -euo pipefail
+
+if ! command -v brew >/dev/null 2>&1; then
+ echo "Homebrew is required for macOS wheel builds" >&2
+ exit 1
+fi
+
+brew update
+brew install boost gsl hdf5
diff --git a/misc/cicd-scripts/manylinux-dependencies b/misc/cicd-scripts/manylinux-dependencies
new file mode 100755
index 000000000..9bd7eda9c
--- /dev/null
+++ b/misc/cicd-scripts/manylinux-dependencies
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+# Install the native system packages needed to compile and repair the Linux
+# PyPI wheel inside the manylinux container used by cibuildwheel.
+
+set -euo pipefail
+
+if command -v dnf >/dev/null 2>&1; then
+ PKG_MANAGER="dnf"
+elif command -v yum >/dev/null 2>&1; then
+ PKG_MANAGER="yum"
+else
+ echo "Expected dnf or yum in the manylinux build environment" >&2
+ exit 1
+fi
+
+"$PKG_MANAGER" install -y \
+ boost-devel \
+ gcc-c++ \
+ gsl-devel \
+ hdf5-devel \
+ make \
+ patchelf \
+ which
diff --git a/misc/cicd-scripts/package-linux-bundle.sh b/misc/cicd-scripts/package-linux-bundle.sh
new file mode 100755
index 000000000..063ea0166
--- /dev/null
+++ b/misc/cicd-scripts/package-linux-bundle.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+
+# Create the redistributable Linux COMPAS bundle used by the native tarball
+# workflow and by the Linux PyPI wheel build.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
+REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
+
+BINARY_PATH="${1:-$REPO_ROOT/src/COMPAS}"
+BUNDLE_DIR="${2:-$REPO_ROOT/dist/COMPAS-linux-x86_64}"
+BIN_DIR="$BUNDLE_DIR/bin"
+LIB_DIR="$BUNDLE_DIR/lib"
+README_PATH="$BUNDLE_DIR/README.txt"
+TMP_LDD="$(mktemp)"
+
+cleanup() {
+ rm -f "$TMP_LDD"
+}
+
+trap cleanup EXIT
+
+maybe_strip() {
+ local path="$1"
+
+ if ! command -v strip >/dev/null 2>&1; then
+ return 0
+ fi
+
+ # Best-effort size reduction only. Some binaries or shared libraries may
+ # already be stripped or may not support the requested mode.
+ strip --strip-unneeded "$path" 2>/dev/null || strip "$path" 2>/dev/null || true
+}
+
+if [ ! -x "$BINARY_PATH" ]; then
+ echo "Expected executable COMPAS binary at '$BINARY_PATH'" >&2
+ exit 1
+fi
+
+if ! command -v ldd >/dev/null 2>&1; then
+ echo "ldd is required to package the Linux bundle" >&2
+ exit 1
+fi
+
+rm -rf "$BUNDLE_DIR"
+mkdir -p "$BIN_DIR" "$LIB_DIR"
+
+cp -Lf "$BINARY_PATH" "$BIN_DIR/COMPAS"
+cp "$SCRIPT_DIR/run_compas.sh" "$BUNDLE_DIR/run_compas.sh"
+chmod 755 "$BIN_DIR/COMPAS" "$BUNDLE_DIR/run_compas.sh"
+maybe_strip "$BIN_DIR/COMPAS"
+
+ldd "$BIN_DIR/COMPAS" | tee "$TMP_LDD"
+
+if grep -q 'not found' "$TMP_LDD"; then
+ echo "Cannot package bundle with unresolved runtime dependencies" >&2
+ exit 1
+fi
+
+awk '
+ /=>/ && $3 ~ /^\// { print $3; next }
+ $1 ~ /^\// { print $1; next }
+' "$TMP_LDD" | sort -u | while IFS= read -r lib; do
+ case "$lib" in
+ /lib64/ld-linux-*|/lib/x86_64-linux-gnu/ld-linux-*|*/libc.so.*|*/libm.so.*|*/libpthread.so.*|*/libdl.so.*|*/librt.so.*|*/libresolv.so.*|*/libnss_*.so.*|*/libcrypt.so.*|*/libutil.so.*|*/libanl.so.*)
+ echo "Skipping system runtime: $lib"
+ ;;
+ *)
+ cp -Ln "$lib" "$LIB_DIR/"
+ maybe_strip "$LIB_DIR/$(basename "$lib")"
+ ;;
+ esac
+done
+
+cat > "$README_PATH" <<'EOF'
+COMPAS bundled Linux artifact
+
+Supported platform:
+- Ubuntu 22.04
+- Linux x86_64
+
+Contents:
+- bin/COMPAS: bundled COMPAS executable
+- lib/: shared libraries packaged with this build
+- run_compas.sh: launcher that sets LD_LIBRARY_PATH for the bundled libs
+
+How to run:
+1. Unpack this directory on a supported Linux x86_64 machine.
+2. From inside the unpacked directory, run:
+ ./run_compas.sh -v
+
+Notes and caveats:
+- This bundle is intended to run without separately installing Boost, GSL, or HDF5.
+- It still relies on the target machine's glibc and other base system libraries being compatible with Ubuntu 22.04.
+- If you invoke bin/COMPAS directly, the bundled lib/ directory will not be added automatically; use run_compas.sh instead.
+EOF
+
+echo
+echo "Created bundle at: $BUNDLE_DIR"
+echo "Bundled shared libraries:"
+find "$LIB_DIR" -maxdepth 1 -type f | sort
diff --git a/misc/cicd-scripts/package-macos-bundle.sh b/misc/cicd-scripts/package-macos-bundle.sh
new file mode 100755
index 000000000..57df6f2a8
--- /dev/null
+++ b/misc/cicd-scripts/package-macos-bundle.sh
@@ -0,0 +1,190 @@
+#!/usr/bin/env bash
+
+# Create the redistributable macOS COMPAS bundle embedded in the platform-
+# specific macOS wheels built by cibuildwheel.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
+REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
+
+BINARY_PATH="${1:-$REPO_ROOT/src/COMPAS}"
+ARCH_NAME="${2:-$(uname -m)}"
+BUNDLE_DIR="${3:-$REPO_ROOT/dist/COMPAS-macos-$ARCH_NAME}"
+BIN_DIR="$BUNDLE_DIR/bin"
+LIB_DIR="$BUNDLE_DIR/lib"
+README_PATH="$BUNDLE_DIR/README.txt"
+SEEN_DEPS="$(mktemp)"
+
+cleanup() {
+ rm -f "$SEEN_DEPS"
+}
+
+trap cleanup EXIT
+
+if [ ! -x "$BINARY_PATH" ]; then
+ echo "Expected executable COMPAS binary at '$BINARY_PATH'" >&2
+ exit 1
+fi
+
+for required_command in codesign install_name_tool otool; do
+ if ! command -v "$required_command" >/dev/null 2>&1; then
+ echo "$required_command is required to package the macOS bundle" >&2
+ exit 1
+ fi
+done
+
+maybe_strip() {
+ local path="$1"
+
+ if ! command -v strip >/dev/null 2>&1; then
+ return 0
+ fi
+
+ strip -x "$path" 2>/dev/null || true
+}
+
+is_system_library() {
+ case "$1" in
+ /System/*|/usr/lib/*)
+ return 0
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+dependencies_for() {
+ otool -L "$1" | tail -n +2 | awk '{print $1}'
+}
+
+resolve_dependency_path() {
+ local dependency="$1"
+ local referencing_file="$2"
+ local reference_dir
+
+ case "$dependency" in
+ @loader_path/*)
+ reference_dir="$(CDPATH= cd -- "$(dirname -- "$referencing_file")" && pwd)"
+ echo "$reference_dir/${dependency#@loader_path/}"
+ ;;
+ @executable_path/*)
+ echo "$BIN_DIR/${dependency#@executable_path/}"
+ ;;
+ @rpath/*)
+ return 1
+ ;;
+ *)
+ echo "$dependency"
+ ;;
+ esac
+}
+
+copy_dependency() {
+ local source_path="$1"
+ local dest_path="$LIB_DIR/$(basename "$source_path")"
+
+ if [ ! -f "$dest_path" ]; then
+ cp -f "$source_path" "$dest_path"
+ chmod 755 "$dest_path"
+ maybe_strip "$dest_path"
+ fi
+}
+
+rewrite_references() {
+ local target="$1"
+ local target_dir_mode="$2"
+ local dependency
+ local dependency_name
+
+ while IFS= read -r dependency; do
+ dependency_name="$(basename "$dependency")"
+ if [ -f "$LIB_DIR/$dependency_name" ]; then
+ if [ "$target_dir_mode" = "binary" ]; then
+ install_name_tool -change "$dependency" "@loader_path/../lib/$dependency_name" "$target" || true
+ else
+ install_name_tool -change "$dependency" "@loader_path/$dependency_name" "$target" || true
+ fi
+ fi
+ done < <(dependencies_for "$target")
+}
+
+rm -rf "$BUNDLE_DIR"
+mkdir -p "$BIN_DIR" "$LIB_DIR"
+
+cp -f "$BINARY_PATH" "$BIN_DIR/COMPAS"
+cp "$SCRIPT_DIR/run_compas.sh" "$BUNDLE_DIR/run_compas.sh"
+chmod 755 "$BIN_DIR/COMPAS" "$BUNDLE_DIR/run_compas.sh"
+maybe_strip "$BIN_DIR/COMPAS"
+
+declare -a queue=("$BINARY_PATH")
+
+while [ "${#queue[@]}" -gt 0 ]; do
+ current_file="${queue[0]}"
+ queue=("${queue[@]:1}")
+
+ while IFS= read -r dependency; do
+ resolved_dependency="$(resolve_dependency_path "$dependency" "$current_file" || true)"
+
+ case "$dependency" in
+ @rpath/*|"")
+ continue
+ ;;
+ esac
+
+ if [ -z "$resolved_dependency" ] || [ ! -e "$resolved_dependency" ]; then
+ continue
+ fi
+
+ if is_system_library "$resolved_dependency"; then
+ continue
+ fi
+
+ dependency_name="$(basename "$resolved_dependency")"
+ copy_dependency "$resolved_dependency"
+
+ if ! grep -qx "$dependency_name" "$SEEN_DEPS" 2>/dev/null; then
+ echo "$dependency_name" >> "$SEEN_DEPS"
+ queue+=("$resolved_dependency")
+ fi
+ done < <(dependencies_for "$current_file")
+done
+
+for library_path in "$LIB_DIR"/*; do
+ [ -f "$library_path" ] || continue
+ library_name="$(basename "$library_path")"
+ install_name_tool -id "@loader_path/$library_name" "$library_path" || true
+ rewrite_references "$library_path" "library"
+ codesign --force --sign - "$library_path" >/dev/null 2>&1 || true
+done
+
+rewrite_references "$BIN_DIR/COMPAS" "binary"
+codesign --force --sign - "$BIN_DIR/COMPAS" >/dev/null 2>&1 || true
+
+cat > "$README_PATH" <` for more details.
+Running COMPAS via Python
+=========================
+
+A convenient method of managing the many program options provided by COMPAS is to run COMPAS via Python, using a script to manage and
+specify the values of the program options.
+
+Bundled PyPI executable
+-----------------------
+
+On supported platforms, the PyPI package can include the native COMPAS
+executable directly. After installing::
+
+ pip install compas-popsynth
+
+you can run the packaged executable with::
+
+ compas_run -v
+
+and a minimal smoke test with::
+
+ compas_run -n 1 --initial-mass-1 35 --initial-mass-2 31 -a 3.5 --random-seed 0 --metallicity 0.001 --quiet -o compas-smoke-output
+
+``compas_run`` resolves to the bundled native executable when a supported wheel
+is installed. If you want to point the Python launcher at a separate local build
+or an extracted bundle, set ``COMPAS_EXECUTABLE_PATH`` or ``COMPAS_BUNDLE_ROOT``.
+
+An example Python script is provided in the COMPAS suite on github: ``runSubmit.py``. Additionally, the default COMPAS options are specified on ``compasConfigDefault.yaml``. Users should copy the ``runSubmit.py`` and ``runSubmit.py`` scripts and modify the ``compasConfigDefault.yaml`` copy to match their experimental requirements. Refer to the :doc:`Getting started guide <../../Getting started/getting-started>` for more details.
To run COMPAS via Python using the ``runSubmit.py`` script provided, set the shell environment variable ``COMPAS_ROOT_DIR``
to the parent directory of the directory in which the COMPAS executable resides, then type `python /path-to-runSubmit/runSubmit.py`.
diff --git a/py_tests/test_compas_runner.py b/py_tests/test_compas_runner.py
new file mode 100644
index 000000000..174a0bf1a
--- /dev/null
+++ b/py_tests/test_compas_runner.py
@@ -0,0 +1,52 @@
+import os
+
+from compas_python_utils.compas_runner import main, resolve_compas_executable, run_compas
+
+
+def _make_fake_executable(path, contents=None):
+ executable_contents = contents or "#!/usr/bin/env bash\nprintf 'fake-compas %s\\n' \"$*\"\n"
+ path.write_text(executable_contents)
+ os.chmod(path, 0o755)
+
+
+def test_resolve_compas_executable_from_env(monkeypatch, tmp_path):
+ fake_executable = tmp_path / "COMPAS"
+ _make_fake_executable(fake_executable)
+
+ monkeypatch.setenv("COMPAS_EXECUTABLE_PATH", str(fake_executable))
+ monkeypatch.delenv("COMPAS_BUNDLE_ROOT", raising=False)
+
+ assert resolve_compas_executable() == str(fake_executable)
+
+
+def test_resolve_compas_executable_from_bundle_root(monkeypatch, tmp_path):
+ bundle_root = tmp_path / "COMPAS-linux-x86_64"
+ bundle_root.mkdir()
+ launcher = bundle_root / "run_compas.sh"
+ _make_fake_executable(launcher)
+
+ monkeypatch.delenv("COMPAS_EXECUTABLE_PATH", raising=False)
+ monkeypatch.setenv("COMPAS_BUNDLE_ROOT", str(bundle_root))
+
+ assert resolve_compas_executable() == str(launcher)
+
+
+def test_run_compas_executes_resolved_binary(monkeypatch, tmp_path):
+ fake_executable = tmp_path / "COMPAS"
+ _make_fake_executable(fake_executable)
+
+ monkeypatch.setenv("COMPAS_EXECUTABLE_PATH", str(fake_executable))
+ completed_process = run_compas(["-v"], capture_output=True, text=True)
+
+ assert completed_process.returncode == 0
+ assert "fake-compas -v" in completed_process.stdout
+
+
+def test_main_print_path(monkeypatch, tmp_path, capsys):
+ fake_executable = tmp_path / "COMPAS"
+ _make_fake_executable(fake_executable)
+
+ monkeypatch.setenv("COMPAS_EXECUTABLE_PATH", str(fake_executable))
+
+ assert main(["--print-path"]) == 0
+ assert str(fake_executable) in capsys.readouterr().out
diff --git a/setup.py b/setup.py
index 8cf5abe20..928f0df5d 100644
--- a/setup.py
+++ b/setup.py
@@ -3,44 +3,62 @@
import re
import sys
-from setuptools import find_packages, setup
+from setuptools import Distribution, find_packages, setup
+
+try:
+ from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
+except ImportError:
+ _bdist_wheel = None
python_version = sys.version_info
if python_version < (3, 8):
sys.exit("Python < 3.8 is not supported, aborting setup")
NAME = "compas_python_utils"
+DIST_NAME = "compas-popsynth"
PACKAGES = find_packages()
HERE = os.path.dirname(os.path.realpath(__file__))
META_PATH = os.path.join(NAME, "__init__.py")
CPP_VERSION_FILE = os.path.join("src", "changelog.h")
+BUILD_BINARY_WHEEL = os.environ.get("COMPAS_BINARY_WHEEL") == "1"
CLASSIFIERS = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
+ "Operating System :: POSIX :: Linux",
+ "Operating System :: MacOS",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
-INSTALL_REQUIRES = [
+CORE_RUNTIME_REQUIRES = [
"numpy>=1.16",
+ "PyYAML",
+]
+ANALYSIS_REQUIRES = [
"h5py",
- "argparse",
"stroopwafel",
- "pytest>=3.6",
- "pre-commit",
- "flake8",
- "black==22.10.0",
- "isort",
"matplotlib>=3.3.2",
"pandas",
"astropy>=4.0",
"scipy>=1.5.0",
- "latex",
- "PyYAML",
"tqdm",
- "corner"
+ "corner",
+]
+DEV_ONLY_REQUIRES = [
+ "pytest>=3.6",
+ "pytest-cov",
+ "pre-commit",
+ "flake8",
+ "black==22.10.0",
+ "isort",
+ "coverage-badge",
+ "deepdiff",
+ "jupytext",
+ "jupyter-autotime",
+ "memory_profiler",
+ "nbconvert",
+ "ipykernel",
]
EXTRA_REQUIRE = dict(
docs=[
@@ -58,24 +76,31 @@
"sphinx-togglebutton",
"linuxdoc>=20210324"
],
- dev=[
- "pytest-cov",
- "pre-commit",
- "flake8",
- "black==22.10.0",
- "isort",
- "coverage-badge",
- "deepdiff",
- "jupytext",
- "jupyter-autotime",
- "memory_profiler",
- "nbconvert",
- "ipykernel",
- ],
+ full=[],
+ dev=DEV_ONLY_REQUIRES,
gpu=["cupy"],
)
+class COMPASDistribution(Distribution):
+ def has_ext_modules(self):
+ return BUILD_BINARY_WHEEL
+
+
+cmdclass = {}
+if BUILD_BINARY_WHEEL and _bdist_wheel is not None:
+ class COMPASBdistWheel(_bdist_wheel):
+ def finalize_options(self):
+ super().finalize_options()
+ self.root_is_pure = False
+
+ def get_tag(self):
+ _, _, platform_tag = super().get_tag()
+ return "py3", "none", platform_tag
+
+ cmdclass["bdist_wheel"] = COMPASBdistWheel
+
+
def read(*parts):
with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
return f.read()
@@ -95,14 +120,16 @@ def find_version(version_file=read(CPP_VERSION_FILE)):
r"VERSION_STRING = ['\"]([^'\"]*)['\"]", version_file, re.M
)
if version_match:
- return version_match.group(1)
+ raw_version = version_match.group(1)
+ normalized_parts = [str(int(part)) for part in raw_version.split(".")]
+ return ".".join(normalized_parts)
raise RuntimeError("Unable to find version string.")
if __name__ == "__main__":
setup(
- name=NAME,
- version=find_meta("version"),
+ name=DIST_NAME,
+ version=find_version(),
author=find_meta("author"),
author_email=find_meta("email"),
maintainer=find_meta("author"),
@@ -113,22 +140,32 @@ def find_version(version_file=read(CPP_VERSION_FILE)):
long_description=read("README.md"),
long_description_content_type="text/markdown",
packages=PACKAGES,
+ python_requires=">=3.8",
package_data={
+ NAME: [
+ "bundled/*",
+ "bundled/*/*.sh",
+ "bundled/*/bin/*",
+ "bundled/*/lib/*",
+ ],
f"{NAME}.preprocessing": ["*.txt", "*.yaml"],
f"{NAME}.detailed_evolution_plotter": ["van_den_heuvel_figures/*"],
f"{NAME}.cosmic_integration": ["SNR_Grid*"],
},
include_package_data=True,
- install_requires=INSTALL_REQUIRES,
+ install_requires=CORE_RUNTIME_REQUIRES + ANALYSIS_REQUIRES,
extras_require=EXTRA_REQUIRE,
classifiers=CLASSIFIERS,
- zip_safe=True,
+ zip_safe=not BUILD_BINARY_WHEEL,
+ distclass=COMPASDistribution,
+ cmdclass=cmdclass,
entry_points={
"console_scripts": [
f"compas_h5view= {NAME}.h5view:main",
f"compas_h5copy= {NAME}.h5copy:main",
f"compas_h5sample= {NAME}.h5sample:main",
f"compas_plot_detailed_evolution={NAME}.detailed_evolution_plotter.plot_detailed_evolution:main",
+ f"compas_run={NAME}.compas_runner:main",
f"compas_run_submit={NAME}.preprocessing.runSubmit:main",
f"compas_sample_stroopwafel={NAME}.preprocessing.stroopwafelInterface:main",
f"compas_sample_moe_di_stefano={NAME}.preprocessing.sampleMoeDiStefano:main",
diff --git a/src/Makefile b/src/Makefile
index ddab836d3..ae3d83836 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,4 +1,4 @@
-CPP := g++
+CPP ?= g++
UNAME_S := $(shell uname -s)
HOMEBREW_PREFIX := $(shell brew --prefix 2>/dev/null)