Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
-DWITH_LUA=OFF
-DWITH_OPENMP=OFF
-DWITH_PLUGINS=OFF
-DWITH_PYBIND=OFF
-DWITH_SRC=OFF
-DWITH_TESTS=OFF
-DWITH_TOOLS=OFF
Expand Down Expand Up @@ -90,6 +91,11 @@ jobs:
- name: Build
run: cmake --build build ${{ env.CMAKE_BUILD_OPTS }}

# WITH_PYBIND is silently disabled when pybind11 is missing
- name: Verify the Python bindings were built
if: matrix.distro == 'fedora'
run: test -f build/python/binding/_core*.so

- name: Upload Build Artifacts
uses: actions/upload-artifact@v7
with:
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ jobs:
ref: ${{ needs.setup.outputs.ref }}
ref_name: ${{ needs.setup.outputs.ref_name }}

packaging-python:
name: Build Python Wheels
needs: [setup]
secrets: inherit
uses: ./.github/workflows/packaging-python.yaml
with:
container_image: ${{ needs.setup.outputs.container_image }}
ref: ${{ needs.setup.outputs.ref }}

packaging-nix:
name: Bundle Artifacts with Nix
needs: [setup, nix]
Expand Down Expand Up @@ -162,6 +171,7 @@ jobs:
- test
- packaging-container
- packaging-nix
- packaging-python
- deploy
- deploy-nix
runs-on: ubuntu-latest
Expand Down
144 changes: 144 additions & 0 deletions .github/workflows/packaging-python.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# SPDX-FileCopyrightText: 2026 Institute for Automation of Complex Power Systems, RWTH Aachen University
# SPDX-License-Identifier: Apache-2.0

name: Packaging (Python)

on:
workflow_call:
inputs:
container_image:
required: true
type: string
ref:
description: 'Revision under test'
required: true
type: string
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
sdist:
name: Source distribution
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.sha }}
allow-unsafe-pr-checkout: true
submodules: recursive

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'

- name: Build source distribution
run: pipx run build --sdist --outdir dist/

- name: Archive source distribution
uses: actions/upload-artifact@v7
with:
name: python-sdist
path: dist/

image:
name: Prepare Wheel Build Image
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.image }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.sha }}
allow-unsafe-pr-checkout: true
submodules: recursive

- name: Login to Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Set up Buildx
uses: docker/setup-buildx-action@v4

# Tagged by recipe, so the image is built once and pulled afterwards
- name: Compute image name
id: image
env:
CONTAINER_IMAGE: ${{ inputs.container_image }}
REPOSITORY: ${{ github.repository }}
run: |
HASH=$(cat packaging/docker/Dockerfile.manylinux packaging/deps.sh \
$(find packaging/patches -type f | sort) | sha256sum | cut -c1-16)
echo "image=${CONTAINER_IMAGE:-ghcr.io/${REPOSITORY,,}}/manylinux:${HASH}" >> "$GITHUB_OUTPUT"
echo "latest=${CONTAINER_IMAGE:-ghcr.io/${REPOSITORY,,}}/manylinux:latest" >> "$GITHUB_OUTPUT"

- name: Check for an existing image
id: existing
env:
IMAGE: ${{ steps.image.outputs.image }}
run: |
if docker buildx imagetools inspect "${IMAGE}" >/dev/null 2>&1; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi

- name: Build and Push Image
if: steps.existing.outputs.found == 'false'
uses: docker/build-push-action@v7
with:
context: .
file: packaging/docker/Dockerfile.manylinux
push: true
cache-from: type=gha,scope=manylinux
cache-to: ${{ github.event_name != 'pull_request_target' && 'type=gha,mode=max,scope=manylinux' || '' }}
tags: |
${{ steps.image.outputs.image }}
${{ github.event_name == 'push' && steps.image.outputs.latest || '' }}

wheels:
name: Wheel (${{ matrix.python }}, ${{ matrix.variant }})
needs: [image]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: [cp310, cp311, cp312, cp313, cp314]
variant: [apache, gpl]
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.sha }}
allow-unsafe-pr-checkout: true
submodules: recursive

- name: Login to Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Build wheel
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_BUILD: ${{ matrix.python }}-manylinux_x86_64
CIBW_MANYLINUX_X86_64_IMAGE: ${{ needs.image.outputs.image }}
CIBW_ENVIRONMENT_PASS_LINUX: SKBUILD_CMAKE_DEFINE VILLAS_WHEEL_VARIANT
SKBUILD_CMAKE_DEFINE: ${{ matrix.variant == 'gpl' && 'WITHOUT_GPL=OFF' || '' }}
VILLAS_WHEEL_VARIANT: ${{ matrix.variant }}

- name: Archive wheel
uses: actions/upload-artifact@v7
with:
name: python-wheel-${{ matrix.variant }}-${{ matrix.python }}
path: wheelhouse/
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ find_package(Criterion)
find_package(OpalOrchestra)
find_package(LibXml2)
find_package(OpalAsyncApi)
find_package(Python3 COMPONENTS Interpreter Development.Module)
find_package(pybind11 CONFIG QUIET)

# Check for tools
find_program(PROTOBUFC_COMPILER NAMES protoc-c)
Expand Down Expand Up @@ -190,6 +192,7 @@ cmake_dependent_option(WITH_HOOKS "Build with support for processi
cmake_dependent_option(WITH_LUA "Build with Lua" "${WITH_DEFAULTS}" "LUA_FOUND" OFF)
cmake_dependent_option(WITH_OPENMP "Build with support for OpenMP for parallel hooks" "${WITH_DEFAULTS}" "OPENMP_FOUND" OFF)
cmake_dependent_option(WITH_PLUGINS "Build plugins" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF)
cmake_dependent_option(WITH_PYBIND "Build Python bindings" "${WITH_DEFAULTS}" "pybind11_FOUND; Python3_FOUND" OFF)
cmake_dependent_option(WITH_SRC "Build executables" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF)
cmake_dependent_option(WITH_TESTS "Run tests" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF)
cmake_dependent_option(WITH_TOOLS "Build auxilary tools" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF)
Expand Down Expand Up @@ -301,6 +304,7 @@ add_feature_info(HOOKS WITH_HOOKS "Build with
add_feature_info(LUA WITH_LUA "Build with Lua support")
add_feature_info(OPENMP WITH_OPENMP "Build with OpenMP support")
add_feature_info(PLUGINS WITH_PLUGINS "Build plugins")
add_feature_info(PYBIND WITH_PYBIND "Build Python bindings")
add_feature_info(SRC WITH_SRC "Build executables")
add_feature_info(TESTS WITH_TESTS "Run tests")
add_feature_info(TOOLS WITH_TOOLS "Build auxilary tools")
Expand Down
5 changes: 4 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@
packagesWith = pkgs: rec {
default = villas-node;

villas-node-python = pkgs.callPackage (nixDir + "/python.nix") { src = ./.; };
villas-node-python = pkgs.callPackage (nixDir + "/python.nix") {
src = ./.;
villas = villas-node-minimal;
};

villas-node-minimal = pkgs.callPackage (nixDir + "/villas.nix") {
src = ./.;
Expand Down
7 changes: 5 additions & 2 deletions lib/hook_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ void HookList::dump(Logger logger, std::string subject) const {
logger->debug("Hooks of {}:", subject);

unsigned i = 0;
for (auto h : *this)
logger->debug(" {}: {}", i++, h->getFactory()->getName());
for (auto h : *this) {
auto *f = h->getFactory();

logger->debug(" {}: {}", i++, f ? f->getName() : "unnamed");
}
}
2 changes: 1 addition & 1 deletion lib/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ Node *NodeFactory::make(json_t *json, const uuid_t &id,
std::string type;
Node *n;

if (json_is_object(json))
if (!json_is_object(json))
throw ConfigError(json, "node-config-node",
"Node configuration must be an object");

Expand Down
2 changes: 1 addition & 1 deletion lib/node_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ int node_restart(vnode *n) {

int node_destroy(vnode *n) {
auto *nc = (Node *)n;
nc->~Node();
delete nc;
return 0;
}

Expand Down
75 changes: 75 additions & 0 deletions packaging/check-wheel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
SPDX-FileCopyrightText: 2026 Institute for Automation of Complex Power Systems, RWTH Aachen University
SPDX-License-Identifier: Apache-2.0
""" # noqa: E501

import json
import os

import villas.node as vn

# Node-types disappear silently when a dependency is missing at configure time
REQUIRED = {
"amqp",
"c37.118",
"can",
"comedi",
"exec",
"file",
"infiniband",
"influxdb",
"kafka",
"loopback",
"modbus",
"mqtt",
"nanomsg",
"ngsi",
"redis",
"rtp",
"shmem",
"signal.v2",
"socket",
"stats",
"temper",
"test_rtt",
"uldaq",
"webrtc",
"websocket",
}

GPL = {
"ethercat",
"iec60870-5-104",
"iec61850-8-1",
"iec61850-9-2",
"zeromq",
}

gpl = os.environ.get("VILLAS_WHEEL_VARIANT") == "gpl"

types = set(vn.node_types())

missing = sorted((REQUIRED | GPL if gpl else REQUIRED) - types)
if missing:
raise SystemExit(f"node-types missing from the wheel: {missing}")

if not gpl and GPL & types:
raise SystemExit(f"GPL node-types in an Apache-2.0 wheel: {sorted(GPL & types)}")

config = {
"type": "signal.v2",
"limit": 1,
"rate": 100.0,
"in": {"signals": [{"name": "sine", "signal": "sine"}]},
}

node = vn.Node(json.dumps(config), "check")
node.check()
node.prepare()
node.start()
sample = node.read()[0]
node.stop()

print(
f"{len(types)} node-types, {len(vn.hook_types())} hooks, read {sample.length} value"
)
7 changes: 4 additions & 3 deletions packaging/docker/Dockerfile.fedora
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ RUN dnf -y install \
openssh-clients \
jq nmap-ncat \
iproute iproute-tc \
python python-devel python-pip \
python python-devel python-pip pybind11-devel \
gdb gdb-gdbserver \
cppcheck \
xmlto dblatex rubygem-asciidoctor \
Expand Down Expand Up @@ -85,8 +85,9 @@ RUN bash /deps/deps.sh
RUN ln -s /usr/lib64/tc /usr/lib/tc

# Install Python dev tools for CI
COPY ./python /python
RUN pip install /python[dev] && \
COPY ./pyproject.toml ./LICENSE /python/
COPY ./python /python/python
RUN pip install --config-settings=wheel.cmake=false "/python[dev]" && \
rm -r /python

# Expose ports for HTTP and WebSocket frontend
Expand Down
Loading
Loading