Skip to content
Merged
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
126 changes: 116 additions & 10 deletions .github/workflows/publish-container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ on:
tags:
- "v*"
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to build without moving it, such as v2.12.2"
required: true
type: string
publish_latest:
description: "Also update latest when recovering this release"
required: false
default: false
type: boolean

permissions:
contents: read
Expand All @@ -14,12 +24,55 @@ env:
IMAGE_GHCR: ghcr.io/${{ github.repository }}

concurrency:
group: publish-container-${{ github.ref }}
cancel-in-progress: true
group: publish-container-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false

jobs:
release:
name: Resolve Release Tag
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
tag: ${{ steps.tag.outputs.tag }}
sha: ${{ steps.source.outputs.sha }}
steps:
- name: Validate release tag
id: tag
shell: bash
env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
run: |
set -euo pipefail
if [[ ! "$RELEASE_TAG" =~ ^v[0-9][A-Za-z0-9._-]*$ ]]; then
echo "::error::An existing version tag is required, such as v2.12.2"
exit 1
fi
git check-ref-format "refs/tags/$RELEASE_TAG"
printf 'tag=%s\n' "$RELEASE_TAG" >> "$GITHUB_OUTPUT"

- name: Checkout tagged source
uses: actions/checkout@v5
with:
ref: refs/tags/${{ steps.tag.outputs.tag }}

- name: Verify source version and resolve commit
id: source
shell: bash
env:
RELEASE_TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
version=$(tr -d '\r\n' < docs/source/bskVersion.txt)
if [[ "$RELEASE_TAG" != "v$version" ]]; then
echo "::error::Release tag does not match the tagged source version"
exit 1
fi
printf 'sha=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

build:
name: Build and Push (${{ matrix.arch }})
needs: release
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
Expand All @@ -36,6 +89,22 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ needs.release.outputs.sha }}

- name: Free runner disk for container build
shell: bash
run: |
set -euo pipefail
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
docker system prune -af
echo "Disk space after cleanup:"
df -h

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand All @@ -58,10 +127,14 @@ jobs:
latest=false
suffix=-${{ matrix.arch }}
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=${{ needs.release.outputs.tag }}
type=raw,value=latest,enable=${{ github.event_name == 'push' || inputs.publish_latest }}
labels: |
org.opencontainers.image.revision=${{ needs.release.outputs.sha }}
org.opencontainers.image.version=${{ needs.release.outputs.tag }}

- name: Build and push arch image
id: image
uses: docker/build-push-action@v6
with:
context: .
Expand All @@ -76,11 +149,31 @@ jobs:
# Cache export is best-effort and must not invalidate a published image.
cache-to: type=gha,mode=max,scope=container-${{ matrix.arch }},ignore-error=true

- name: Verify published image
shell: bash
env:
IMAGE_TAGS: ${{ steps.meta.outputs.tags }}
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
BSK_RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
image_tag=${IMAGE_TAGS%%$'\n'*}
docker run --rm -e BSK_RELEASE_TAG "$image_tag@$IMAGE_DIGEST" python -c '
import os
import Basilisk
from Basilisk.architecture import messaging
from Basilisk.simulation import camera, spacecraft, vizInterface
expected = os.environ["BSK_RELEASE_TAG"][1:]
assert Basilisk.__version__ == expected, (Basilisk.__version__, expected)
assert all(Basilisk.hasBuildFeature(name) for name in ("opNav", "mujoco", "vizInterface"))
print("Container import and version checks passed:", Basilisk.__version__)
'

publish:
name: Publish Multi-Arch Manifests
runs-on: ubuntu-24.04
timeout-minutes: 30
needs: build
needs: [release, build]

steps:
- name: Set up Docker Buildx
Expand All @@ -100,19 +193,32 @@ jobs:
with:
images: |
${{ env.IMAGE_GHCR }}
flavor: latest=false
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=${{ needs.release.outputs.tag }}
type=raw,value=latest,enable=${{ github.event_name == 'push' || inputs.publish_latest }}

- name: Create and push manifest lists
shell: bash
env:
IMAGE_TAGS: ${{ steps.meta.outputs.tags }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
release_image="${IMAGE_GHCR,,}:$RELEASE_TAG"
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
docker buildx imagetools create \
-t "$tag" \
"$tag-amd64" \
"$tag-arm64"
"$release_image-amd64" \
"$release_image-arm64"
docker buildx imagetools inspect "$tag"
done <<< "${{ steps.meta.outputs.tags }}"
docker buildx imagetools inspect "$tag" --raw | python3 -c '
import json, sys
manifest = json.load(sys.stdin)
architectures = {item["platform"]["architecture"] for item in manifest["manifests"]
if item.get("platform", {}).get("os") == "linux"}
assert architectures == {"amd64", "arm64"}, architectures
print("Verified Linux architectures:", ", ".join(sorted(architectures)))
'
done <<< "$IMAGE_TAGS"
11 changes: 7 additions & 4 deletions docs/source/Support/Developer/CodingGuidlines.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ the coding guidelines developed by Stroustrup and Sutter.

**Python foundational guidelines**: the `PEP
8 <https://www.python.org/dev/peps/pep-0008/>`__ guidelines are to be
followed for python code development.
followed for Python code development, with the exceptions described below.

The both of these foundational sets of guidelines are extensive and
where the Basilisk document is silent these guidelines will prevail.
Expand Down Expand Up @@ -278,9 +278,12 @@ C/C++ Exceptions
Python Exceptions
-----------------

- General-purpose Python implementation and tooling names are to follow
PEP 8. This includes using ``under_scored`` names for functions, methods,
local variables, and parameters.
- Python functions, methods, local variables, and parameters may use either
``snake_case`` or ``lowerCamelCase``. This applies to general-purpose
implementation and tooling as well as BSK architecture code. Follow the
established convention in the surrounding file or API, and avoid mixing
naming styles within the same interface. Do not rename existing interfaces
solely to change naming style.
- Public BSK module attributes, message variables, mathematical quantities,
and names shared across C/C++ and Python interfaces are to retain the
established Basilisk naming conventions. This includes lower ``camelCase``
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
- Since 2.12.0, assigning a C++ module output to a stand-alone message copied
its initial contents instead of redirecting subsequent writes. This left
shared circles messages in the OpNav scenarios at zero despite successful
image processing. Python output assignment now redirects reads and writes
again and retains the gateway for producers, readers, and recorders, including
inputs connected through module registration methods. Native C++ copies and
moves of redirected messages take independent snapshots without consuming the
gateway payload.
- GitHub issue 1226: Fixed C++ output redirection and message lifetimes,
restoring shared OpNav outputs affected since 2.12.0. Message-vector access
now preserves the correct source owner across later module imports, preventing
premature deletion and false deleted-module errors.
- Configure redirection before connecting readers or recorders, and keep modules
alive during simulation. Use stand-alone gateways for redirected feedback;
native ownership cycles remain unsupported and are not automatically detected.
Redirecting through raw pointer-vector copies or slices raises
``ReferenceError``; use the original module output or a Python list selection.
Keep the module's output-vector entries intact.
- Camera and Vizard image buffers are valid only until the producer replaces
them. Message recorders do not archive image bytes; save images or copy their
bytes while current.
- Fixed Python iterator crashes and numeric-vector access errors that depended
on module import order, including storage-message reads and :ref:`linkBudget`
lookup tables.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Container release builds now free runner disk space and support recovery from
an existing release tag without moving it.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Container release builds now free runner disk space before Docker setup and support rebuilding an existing release tag without moving it, with image-version and multi-architecture manifest checks.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed a lifetime issue when redirecting a C++ output to another module's embedded output: the source module now remains alive while its storage is used by the redirected output, readers, or recorders. This includes outputs accessed through temporary MuJoCo body and site proxies. Redirecting to an output whose module has already been deleted raises ``ReferenceError``.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fixed C++ output redirection through message vectors so indexing, front/back access, and iteration preserve the actual module owner, including iterator copies and movement after later module imports.
- Redirecting to a borrowed message with an unknown storage owner, including elements of raw pointer-vector copies and slices, now raises ``ReferenceError``. Use the original module-owned output, a stand-alone message, or a selection from ``list(module.outputMsgs)``.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Documented message-lifetime limits for output redirection: configure wiring before simulation, retain participating modules, and use stand-alone gateways for redirected feedback connections. Native ownership cycles remain unsupported and are not automatically detected.
- Clarified that camera and Vizard image buffers belong to their producers and must be consumed before replacement; message recorders do not archive the image bytes.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed import-order-dependent numeric-vector binding errors affecting storage-message reads after importing :ref:`simpleInstrument`, ``zeroWindModel.VectorDouble``, and :ref:`linkBudget` lookup tables. Indexing, slicing, and mutation now work with the shared messaging vector without changing the native ABI.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed Python interpreter aborts in shared SWIG iterator element access and boundary-error handling that depended on module import order. Native class layouts and the messaging ABI are unchanged.
113 changes: 110 additions & 3 deletions src/architecture/_GeneralModuleFiles/swig_conly_data.i
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,99 @@
%include "stdint.i"
%include "architecture/messaging/pythonSourceOwner.ih"

// Capture the owning proxy only for instance methods, including member getters.
// The shared local also exists for free/static Message-returning functions, where
// it stays null. This avoids treating their first argument as an owning instance.
%typemap(check) SWIGTYPE *self (PyObject *_global_bskMessageParent = nullptr) {
_global_bskMessageParent = $self;
(void)_global_bskMessageParent;
}

%fragment("bsk_message_parent", "header") %{
// Borrowed MuJoCo and configuration proxies retain their native storage owner
// through this chain. Return an owned reference to its terminal Python owner.
static PyObject* _bsk_message_storage_owner(PyObject* parent) {
PyObject* owner = PyObject_GetAttrString(parent, "_swig_bsk_owner");
if (!owner) {
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) return nullptr;
PyErr_Clear();
Py_INCREF(parent);
return parent;
}
if (Py_EnterRecursiveCall(" while resolving a message storage owner")) {
Py_DECREF(owner);
return nullptr;
}
PyObject* result = _bsk_message_storage_owner(owner);
Py_LeaveRecursiveCall();
Py_DECREF(owner);
return result;
}

// Flatten borrowed container views to their actual owner. A stand-alone pointer
// vector does not own its elements and must not masquerade as their owner.
static int _bsk_remember_message_parent(PyObject* proxy, PyObject* parent) {
PyObject* owner = _bsk_message_storage_owner(parent);
if (!owner) return -1;
PyObject* reference = PyObject_GetAttrString(owner, "_bskMessageOwner");
if (!reference) {
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
Py_DECREF(owner);
return -1;
}
PyErr_Clear();
PyObject* pointerVector = PyObject_GetAttrString(owner, "_bskPointerVector");
if (pointerVector) {
int isPointerVector = PyObject_IsTrue(pointerVector);
Py_DECREF(pointerVector);
if (isPointerVector != 0) {
Py_DECREF(owner);
return isPointerVector < 0 ? -1 : 0;
}
} else {
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
Py_DECREF(owner);
return -1;
}
PyErr_Clear();
}
reference = PyWeakref_NewRef(owner, nullptr);
}
Py_DECREF(owner);
if (!reference) return -1;
PyObject* name = PyUnicode_FromString("_bskMessageOwner");
int status = name ? PyObject_GenericSetAttr(proxy, name, reference) : -1;
Py_XDECREF(name);
Py_DECREF(reference);
return status;
}
%}

%typemap(out, fragment="bsk_message_parent") Message * (PyObject *_global_bskMessageParent = nullptr) {
$result = SWIG_NewPointerObj(SWIG_as_voidptr($1), $descriptor, $owner | %newpointer_flags);
if ($result && $1 && !$owner && _global_bskMessageParent) {
if (_bsk_remember_message_parent($result, _global_bskMessageParent) < 0) {
Py_CLEAR($result);
SWIG_fail;
}
}
}

%typemap(out) Message & = Message *;
%typemap(out) const Message & = Message *;

// Only borrowed member vectors have an enclosing storage owner. Copies and
// slices own a new array but not its pointees, so do not inherit that owner.
%typemap(out, fragment="bsk_message_parent") std::vector * (PyObject *_global_bskMessageParent = nullptr) {
$result = SWIG_NewPointerObj(SWIG_as_voidptr($1), $descriptor, $owner | %newpointer_flags);
if ($result && $1 && !$owner && _global_bskMessageParent) {
if (_bsk_remember_message_parent($result, _global_bskMessageParent) < 0) {
Py_CLEAR($result);
SWIG_fail;
}
}
}

// Preserve the original Python argument for the Message member setter below.
%typemap(in) Message * (void *pointer = nullptr, PyObject *messageOwner_$1 = nullptr) {
int result = SWIG_ConvertPtr($input, &pointer, $descriptor, $disown | %convertptr_flags);
Expand All @@ -34,15 +127,29 @@
}
$1 = reinterpret_cast<$1_ltype>(pointer);
messageOwner_$1 = $input;
(void)messageOwner_$1; // Only member setters consume the saved Python owner.
}

// Python output assignment connects and retains a gateway; native assignment copies a value.
%typemap(memberin, fragment="bsk_python_source_owner_init") Message {
%typemap(memberin, fragment="bsk_python_source_owner_init,bsk_message_parent") Message {
if (!$input) {
SWIG_exception_fail(SWIG_ValueError, "A message gateway cannot be None");
}
$1.redirectTo(*$input, messageOwner_$input,
&_bsk_readfunctor_acquire, &_bsk_readfunctor_release);
PyObject *storageOwner = PyObject_CallMethod(messageOwner_$input, "_redirection_owner", nullptr);
if (!storageOwner) {
SWIG_fail;
}
PyObject *enclosingOwner = _global_bskMessageParent
? _bsk_message_storage_owner(_global_bskMessageParent) : nullptr;
if (_global_bskMessageParent && !enclosingOwner) {
Py_DECREF(storageOwner);
SWIG_fail;
}
$1.redirectTo(*$input, storageOwner,
&_bsk_readfunctor_acquire, &_bsk_readfunctor_release,
enclosingOwner);
Py_XDECREF(enclosingOwner);
Py_DECREF(storageOwner);
}

%define ARRAYASLIST(type, fromfunc, asfunc)
Expand Down
Loading
Loading