Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1fd006d
Add macOS arm64 build script and OpenDSSC source patch
samtalki May 3, 2026
4f593dc
Wire macOS-C++ backend into DSS loader and packaging
samtalki May 3, 2026
c6b065b
Treat Darwin like Linux for per-test subprocess isolation
samtalki May 3, 2026
4d8d7c8
Add macOS install instructions to README
samtalki May 3, 2026
0451ab5
Sync README, conftest, and System docs for macOS
samtalki May 3, 2026
d02758f
Move macos.patch to macos/, add Build_macOS_doc.md, plumb ARCH override
samtalki May 3, 2026
d70c2a6
Use anchored regex for the per-test result marker in conftest
samtalki May 3, 2026
14ad29b
Use pytest.approx(rel=1e-9) for IEEE-754 vector comparisons
samtalki May 3, 2026
0708ef9
xfail Cat 1/Cat 2 tests where macOS-C++ diverges from Windows baseline
samtalki May 3, 2026
5c656ed
Add test_api_surface.py to lock the public attribute set on DSS()
samtalki May 3, 2026
025f636
Add macOS arm64 wheel pipeline and CI workflow
samtalki May 3, 2026
14b9b44
Skip TestSwtControls13Bus on Darwin (flaky engine crash under isolation)
samtalki May 3, 2026
7798803
Make test_dssproperties_read_active_property self-contained
samtalki May 3, 2026
3846e89
Set FP comparison tolerance to rel=1e-7, abs=1e-9; drop 9 Cat 1 xfails
samtalki May 3, 2026
0639175
Bump FP comparison tolerance to rel=1e-6, drop test_total_powers xfail
samtalki May 3, 2026
5811070
Implement RelaysV cases 1-4 in macos.patch; recover 6 relay xfails
samtalki May 3, 2026
3cadc59
Rename Recloser property keys to PhaseInst/GroundInst; recover 2 xfails
samtalki May 3, 2026
f28497c
Fix TPointerList leak in TControlElem::RemoveSelfFromControlelementList
samtalki May 3, 2026
2b72228
Add per-phase TRelayObj::get_States/set_States, drop write_state xfail
samtalki May 4, 2026
818bcc4
Fix per-phase state realloc in SwtControl/Recloser/Fuse, drop swtcont…
samtalki May 4, 2026
e651e51
Reset SolutionAbort/Redirect_Abort in TExecutive::Clear and ClearAllC…
samtalki May 4, 2026
6797b22
Fix DSS()-instance leak: TMyApplication::DoRun reuses TExecutive
samtalki May 4, 2026
db32ef0
draft macOS port patch notes
samtalki May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/macos-arm64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: macOS arm64

on:
push:
branches: [master]
tags: ['v*']
pull_request:
branches: [master]

jobs:
build-and-test:
runs-on: macos-14
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install build deps
run: brew install cmake

- name: Build OpenDSS C++ engine
run: bash OpenDSSMacOSCPPForRepo.sh

- name: Install package
run: pip install -e ".[dev]"

- name: Run tests
run: pytest tests/ --tb=short

wheels:
runs-on: macos-14
if: startsWith(github.ref, 'refs/tags/v')
needs: build-and-test
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: |
3.9
3.10
3.11
3.12
3.13

- name: Install build deps
run: |
brew install cmake
pip install build delocate

- name: Build wheels
run: bash macos/build_macos_wheels.sh

- uses: actions/upload-artifact@v4
with:
name: macos-arm64-wheels
path: wheelhouse/*.whl
if-no-files-found: error
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,13 @@ tests/py_dss_interface/cases/13Bus/IEEE13Nodeckt_EXP_SEQVOLTAGES.CSV
tests/py_dss_interface/cases/13Bus/IEEE13Nodeckt_VLL.Txt
tests/py_dss_interface/cases/13Bus/IEEE13Nodeckt_VLN.Txt
tests/py_dss_interface/cases/13Bus/IEEE13Nodeckt_VLN_Node.Txt

# macOS C++ engine build (created by OpenDSSMacOSCPPForRepo.sh). The build
# script's working directory; the actual dylibs land under
# src/py_dss_interface/opendss_official/macos/cpp/ and are also untracked.
/build_macos_src/
src/py_dss_interface/opendss_official/macos/cpp/*.dylib
src/py_dss_interface/opendss_official/macos/cpp/*.dylib.*

# Local working notes (Track C upstream PR draft, etc.)
/notes/
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ recursive-include src/py_dss_interface/opendss_official/windows/delphi/x86 *.dll
recursive-include src/py_dss_interface/opendss_official/windows/cpp/x64 *.dll
recursive-include src/py_dss_interface/opendss_official/windows/cpp/x86 *.dll
recursive-include src/py_dss_interface/opendss_official/linux/cpp/ *.so
recursive-include src/py_dss_interface/opendss_official/macos/cpp/ *.dylib

include tox.ini .travis.yml .appveyor.yml .readthedocs.yml

global-exclude *.py[cod] __pycache__/* *.dylib
global-exclude *.py[cod] __pycache__/*
98 changes: 98 additions & 0 deletions OpenDSSMacOSCPPForRepo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
# Build the OpenDSSC C++ engine as a macOS arm64 dylib and stage it for
# inclusion in the py-dss-interface package.
#
# Mirrors OpenDSSLinuxCPPForRepo.sh. The OpenDSS C++ source ships as
# VersionC.zip at the repo root; this script extracts it, applies
# macos/macos.patch (Apple Clang / Apple Silicon portability fixes), runs
# CMake/Clang, and copies the resulting dylibs into
# src/py_dss_interface/opendss_official/macos/cpp/.
#
# Prerequisites: cmake, unzip, install_name_tool, otool (the latter two ship
# with Xcode Command Line Tools). The script does not auto-install anything;
# missing tools are reported with a brew install hint.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_ZIP="${REPO_ROOT}/VersionC.zip"
PATCH_FILE="${REPO_ROOT}/macos/macos.patch"
WORK_DIR="${REPO_ROOT}/build_macos_src"
OUT_DIR="${REPO_ROOT}/src/py_dss_interface/opendss_official/macos/cpp"
ARCH="${ARCH:-arm64}"
DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}"

require_tool() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: required tool '$1' not found on PATH." >&2
echo " Install with: $2" >&2
exit 1
fi
}

echo "==> Checking build prerequisites"
require_tool cmake "brew install cmake"
require_tool unzip "(included with macOS)"
require_tool install_name_tool "xcode-select --install"
require_tool otool "xcode-select --install"

if [[ ! -f "${SRC_ZIP}" ]]; then
echo "ERROR: ${SRC_ZIP} not found." >&2
exit 1
fi
if [[ ! -f "${PATCH_FILE}" ]]; then
echo "ERROR: ${PATCH_FILE} not found." >&2
exit 1
fi

echo "==> Preparing working directory at ${WORK_DIR}"
rm -rf "${WORK_DIR}"
mkdir -p "${WORK_DIR}"
unzip -q "${SRC_ZIP}" -d "${WORK_DIR}"

echo "==> Applying ${PATCH_FILE}"
# Normalize the patch file to CRLF line endings before applying. The OpenDSS
# source ships with CRLF, and BSD patch (the default on macOS) doesn't have
# GNU's --strip-trailing-cr flag. Converting the patch is the portable fix.
PATCH_TMP="$(mktemp)"
awk '{ sub(/\r$/, ""); printf "%s\r\n", $0 }' "${PATCH_FILE}" > "${PATCH_TMP}"
patch -p1 -d "${WORK_DIR}/VersionC" < "${PATCH_TMP}"
rm -f "${PATCH_TMP}"

echo "==> Configuring CMake (${ARCH}, Release, SHARED, deployment target=${DEPLOYMENT_TARGET})"
cmake \
-DCMAKE_BUILD_TYPE=Release \
-DMyOutputType:STRING=SHARED \
-DCMAKE_OSX_ARCHITECTURES=${ARCH} \
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET} \
-S "${WORK_DIR}/VersionC" \
-B "${WORK_DIR}/build"

echo "==> Building"
cmake --build "${WORK_DIR}/build" --config Release -j"$(sysctl -n hw.logicalcpu)"

echo "==> Staging dylibs into ${OUT_DIR}"
mkdir -p "${OUT_DIR}"
# Copy the main dylib and the klusolve dependency. Preserve symlinks so the
# loader resolves @rpath/libklusolve_all.0.dylib without an extra fixup.
cp -P "${WORK_DIR}/build/libOpenDSSC.dylib" "${OUT_DIR}/"
cp -P "${WORK_DIR}/build/libklusolve_all"*.dylib "${OUT_DIR}/"

echo "==> Patching @rpath to @loader_path"
# CMake bakes the build directory into LC_RPATH; rewrite to @loader_path so the
# dylib finds its klusolve dependency next to itself once relocated into the
# package tree.
for dylib in "${OUT_DIR}/libOpenDSSC.dylib" "${OUT_DIR}"/libklusolve_all*.dylib; do
[[ -L "${dylib}" ]] && continue
chmod u+w "${dylib}"
while read -r rpath; do
[[ -n "${rpath}" ]] && install_name_tool -delete_rpath "${rpath}" "${dylib}" || true
done < <(otool -l "${dylib}" | awk '/cmd LC_RPATH/{flag=1} flag && /path /{print $2; flag=0}')
install_name_tool -add_rpath @loader_path "${dylib}"
done

echo
echo "==> Done."
echo " Architecture: $(lipo -archs "${OUT_DIR}/libOpenDSSC.dylib")"
echo " Dependencies:"
otool -L "${OUT_DIR}/libOpenDSSC.dylib" | sed 's/^/ /'
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 🐍 py-dss-interface: Python Control for OpenDSS Powered by EPRI

![PyPI](https://img.shields.io/pypi/v/py-dss-interface)
![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-brightgreen)
![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen)
![License](https://img.shields.io/github/license/PauloRadatz/py_dss_interface)
![AppVeyor](https://ci.appveyor.com/api/projects/status/github/PauloRadatz/py_dss_interface?branch=master&svg=true)
![PyPI Downloads](https://static.pepy.tech/badge/py-dss-interface)
Expand All @@ -26,9 +26,9 @@ Use the full power of Python to control and automate your OpenDSS workflows:
Compared to the COM interface, `py-dss-interface` provides a modern, Pythonic experience:

- 💡 **Code Completion**: Works seamlessly with IDEs like PyCharm for better developer productivity.
- 📦 **No OpenDSS Installation Required**: No need to install OpenDSS since `py-dss-interface` comes bundled with a tested version of OpenDSS (Windows and Linux). However, you may also use a different OpenDSS version if desired. Please note that compatibility is only guaranteed for the bundled version.
- 📦 **No OpenDSS Installation Required**: No need to install OpenDSS since `py-dss-interface` comes bundled with a tested version of OpenDSS (Windows, Linux, and macOS). However, you may also use a different OpenDSS version if desired. Please note that compatibility is only guaranteed for the bundled version.
- 🔄 **Supports Multiple DSS Objects**: Interact with multiple OpenDSS objects at the same time.
- 🌐 **Multi-Platform**: Supports both **Windows** and **Linux** (pre-built wheels available; build from source if needed).
- 🌐 **Cross platform**: Runs on **Windows**, **Linux**, and **macOS** (Apple Silicon). Pre-built wheels are published for Windows and Linux; macOS users build from source today.
- ⚡ **Better Performance**: Some examples comparing performance will be provided later.
- 🔐 **Version Control Friendly**: Avoid breaking your Python code when updating the OpenDSS version on your computer.

Expand Down Expand Up @@ -72,6 +72,30 @@ To use `py-dss-interface` on Linux, you'll need to build the OpenDSS C++ engine
pip install path/to/py_dss_interface
```

### 🍎 macOS

No pre-built wheels are published for macOS. Build the OpenDSS C++ engine from source and install in editable mode.

1. **Clone the repository**
```bash
git clone https://github.com/PauloRadatz/py_dss_interface.git
cd py_dss_interface
```

2. **Build the OpenDSS C++ engine**
```bash
bash OpenDSSMacOSCPPForRepo.sh
```
📌 **Notes:**
- Requires Xcode Command Line Tools and `cmake` (`brew install cmake`).
- Builds for Apple Silicon (`arm64`) only. Intel users can edit `OpenDSSMacOSCPPForRepo.sh` and change `-DCMAKE_OSX_ARCHITECTURES=arm64` to `x86_64`.
- The build script applies `macos/macos.patch` to a working copy of `VersionC.zip`; the bundled archive is left untouched.

3. **Install the package**
```bash
pip install path/to/py_dss_interface
```

---

## 📦 Quickstart Example
Expand Down Expand Up @@ -102,14 +126,14 @@ pytest tests/

#### Automatic Individual Execution (Default)

On **Linux** or when using the **C++ backend on Windows**, tests automatically run individually in separate subprocesses to prevent memory leak issues. This is handled automatically by the test framework.
On **Linux**, **macOS**, or when using the **C++ backend on Windows**, tests automatically run individually in separate subprocesses to prevent memory leak issues. The test framework handles this automatically.

#### Running All Tests Together

To run all tests together (faster execution, but may have memory issues if there are leaks), use the `--run-together` flag:

```bash
# Run all tests together (even on Linux/C++)
# Run all tests together (even on Linux/macOS/C++)
pytest tests/ --run-together
```

Expand Down
89 changes: 89 additions & 0 deletions macos/Build_macOS_doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Building the macOS arm64 dylib and wheels

## What this builds

The OpenDSS C++ engine as `libOpenDSSC.dylib` for Apple Silicon, plus optional
Python wheels for 3.9–3.13. Both come from `VersionC.zip` at the repo root via
`OpenDSSMacOSCPPForRepo.sh` and `macos/build_macos_wheels.sh`.

## Prerequisites

- macOS 11 (Big Sur) or newer on Apple Silicon.
- Xcode Command Line Tools: `xcode-select --install`. Provides `patch`,
`install_name_tool`, `otool`, `unzip`, and Apple Clang.
- CMake: `brew install cmake`.
- For wheels only: `pyenv` (or system installs) covering Python 3.9–3.13, and
`pip install build delocate` inside whichever venv runs the script.

## Build the dylib

```bash
bash OpenDSSMacOSCPPForRepo.sh
```

Output goes to `src/py_dss_interface/opendss_official/macos/cpp/`:

- `libOpenDSSC.dylib` — the engine, with `LC_RPATH` rewritten to
`@loader_path` so it resolves `libklusolve_all.0.dylib` next to itself.
- `libklusolve_all.0.0.0.dylib` and the two compatibility symlinks.

Verify:

```bash
lipo -archs src/py_dss_interface/opendss_official/macos/cpp/libOpenDSSC.dylib
otool -l src/py_dss_interface/opendss_official/macos/cpp/libOpenDSSC.dylib | grep -A2 LC_RPATH
```

The first should print `arm64`; the second `path @loader_path`.

## Build wheels

```bash
bash macos/build_macos_wheels.sh
```

Output goes to `wheelhouse/`. One wheel per Python version found, tagged
`cp3X-none-macosx_11_0_arm64` (no CPython extension, so ABI tag is
`none`). The script calls `OpenDSSMacOSCPPForRepo.sh` first to stage
dylibs, then loops `python -m build --wheel` per interpreter and runs
`delocate-wheel` to bundle the klusolve dependency inside each wheel.

The wheel script resolves Python interpreters in this order: pyenv shims,
`/Library/Frameworks/Python.framework/Versions/X.Y/bin/pythonX.Y`,
`pythonX.Y` on `PATH`, `python3` if its version matches. Missing
versions are reported and skipped, not fatal.

## What the patch does

`macos/macos.patch` is applied to an extracted working copy of `VersionC.zip`;
the bundled archive itself is never modified. The patch header (top of the
file) lists every change. Two-line summary: it adds an `elseif(APPLE)` branch
to `VersionC/CMakeLists.txt`, and it gates glibc-only headers and APIs
(`<linux/stat.h>`, `<linux/unistd.h>`, `<error.h>`, `__errno_location`,
`strerror_l`, `syscall(SYS_exit_group)`, `__LC_*`, `_NL_MONETARY_CRNCYSTR`,
`/proc/self/exe`, `xdg-open`, `SYS_getdents64`) behind `#ifndef __APPLE__`
with macOS equivalents.

## Troubleshooting

- `patch: command not found` — Xcode Command Line Tools not installed. Run
`xcode-select --install`.
- `ld: library 'klusolve_all' not found` — dylibs were copied without
symlinks. Re-run `OpenDSSMacOSCPPForRepo.sh`; it uses `cp -P` to preserve
the symlink chain.
- `delocate-wheel: error` — `delocate` is not installed in the venv used to
build. Run `pip install delocate` and rerun.
- The build hangs or produces an `x86_64` dylib on an Apple Silicon Mac —
confirm `lipo -archs` reports `arm64`. CMake reads `CMAKE_OSX_ARCHITECTURES`
from the script.

## Advanced: build for a different architecture

The build script reads `ARCH` from the environment (default `arm64`):

```bash
ARCH=x86_64 bash OpenDSSMacOSCPPForRepo.sh
```

`x86_64` is plumbed through the script but not validated in CI. Universal2
(`arm64;x86_64`) requires a CMake variable list and is not supported here.
Loading