From 45f2ef7fef722f1bcf00bfc3a381bdb8ce1f7fe0 Mon Sep 17 00:00:00 2001 From: David Scott Date: Thu, 10 Sep 2026 14:34:56 -0400 Subject: [PATCH] Packaging: containerised CI, Flatpak/AppImage, bundled simd, licence notices, and release-please (#53) --- .github/workflows/ci.yaml | 421 +++++++++++++++++- .github/workflows/pr-build.yaml | 27 ++ .github/workflows/release-please.yml | 38 ++ .gitignore | 9 + .release-please-manifest.json | 1 + CMakeLists.txt | 72 +-- .../io.github.spacefreak18.monocoque.desktop | 9 + ...github.spacefreak18.monocoque.metainfo.xml | 27 ++ flatpak/io.github.spacefreak18.monocoque.yml | 320 +++++++++++++ packaging/licenses/glu-LICENSE.txt | 40 ++ packaging/licenses/lua-5.4-LICENSE.txt | 19 + release-please-config.json | 31 ++ src/monocoque/helper/confighelper.h | 2 + tests/CMakeLists.txt | 11 + tests/confighelper_test.c | 45 ++ tests/{ => manual}/getmem.c | 0 tests/{ => manual}/hidtest.c | 0 tests/{ => manual}/pa_devs.c | 0 tests/{ => manual}/patest_longsine.c | 0 tests/{ => manual}/revburnerparsetest.c | 0 tests/{ => manual}/runmemtest.sh | 0 tests/{ => manual}/setmem.c | 0 tests/{ => manual}/setsimdata.c | 0 tests/{ => manual}/sharedmemoryproducer.c | 0 tests/{ => manual}/sharedmemoryunlink.c | 0 tests/{ => manual}/simlighttest.c | 0 tests/{ => manual}/testlibusb.c | 0 tests/{ => manual}/testrevburner.c | 0 tests/{ => manual}/usesharedmemory.c | 0 tools/appimage-collect-licenses.sh | 75 ++++ .../distro/debian/dpkg/debian-latest/control | 2 +- .../distro/debian/dpkg/ubuntu-latest/control | 2 +- tools/distro/fedora/rpm/fedora.spec | 19 +- tools/generate-third-party-notices.py | 59 +++ tools/monocoque.desktop | 2 +- tools/monocoque.svg | 49 ++ version.txt | 1 + 37 files changed, 1199 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/pr-build.yaml create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 flatpak/io.github.spacefreak18.monocoque.desktop create mode 100644 flatpak/io.github.spacefreak18.monocoque.metainfo.xml create mode 100644 flatpak/io.github.spacefreak18.monocoque.yml create mode 100644 packaging/licenses/glu-LICENSE.txt create mode 100644 packaging/licenses/lua-5.4-LICENSE.txt create mode 100644 release-please-config.json create mode 100644 tests/CMakeLists.txt create mode 100644 tests/confighelper_test.c rename tests/{ => manual}/getmem.c (100%) rename tests/{ => manual}/hidtest.c (100%) rename tests/{ => manual}/pa_devs.c (100%) rename tests/{ => manual}/patest_longsine.c (100%) rename tests/{ => manual}/revburnerparsetest.c (100%) rename tests/{ => manual}/runmemtest.sh (100%) rename tests/{ => manual}/setmem.c (100%) rename tests/{ => manual}/setsimdata.c (100%) rename tests/{ => manual}/sharedmemoryproducer.c (100%) rename tests/{ => manual}/sharedmemoryunlink.c (100%) rename tests/{ => manual}/simlighttest.c (100%) rename tests/{ => manual}/testlibusb.c (100%) rename tests/{ => manual}/testrevburner.c (100%) rename tests/{ => manual}/usesharedmemory.c (100%) create mode 100755 tools/appimage-collect-licenses.sh create mode 100755 tools/generate-third-party-notices.py create mode 100644 tools/monocoque.svg create mode 100644 version.txt diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4a7d8f..8bd0088 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,31 +3,84 @@ name: Make Packages on: push: tags: [ "*" ] + workflow_dispatch: {} + # Called by release-please.yml once it has cut a release. A tag pushed by + # release-please carries the default GITHUB_TOKEN, and GitHub deliberately + # refuses to start a workflow from an event that token created -- so the + # `push: tags` trigger above never fires for a release-please tag. Being + # callable means the release build runs inside release-please's own run + # instead, with no personal access token to store on a public repo. + workflow_call: + inputs: + release_tag: + description: >- + Tag release-please just created. Supplies the package version and + names the release to upload to, both of which would otherwise come + from github.ref_name. + type: string + required: true jobs: build-monocoque-debs: + # Previously this matrix used `runs-on: ${{ matrix.os }}` with + # debian-latest/debian-stable as runner *labels*, which only resolve to + # self-hosted runners. With none online those two legs sat queued + # indefinitely (observed: four runs stuck 17-60min, only the + # ubuntu-latest leg ever completing) until cancelled by hand. Building + # each distro in a container on ordinary GitHub-hosted runners removes + # the self-hosted dependency entirely. + # + # matrix.os stays the distro *name* because it's load-bearing: it picks + # the per-distro control file under tools/distro/debian/dpkg// and + # names the released .deb. matrix.image is what it actually builds in. strategy: + fail-fast: false matrix: - os: [ubuntu-latest, debian-latest, debian-stable] - runs-on: ${{ matrix.os }} - + include: + - os: ubuntu-latest + image: ubuntu:latest + - os: debian-latest + image: debian:testing + - os: debian-stable + image: debian:stable-slim + runs-on: ubuntu-latest + container: + image: ${{ matrix.image }} permissions: contents: write steps: - - uses: actions/checkout@v1 - - name: Checkout submodules - run: git submodule update --init --recursive - - name: Update apt - run: sudo apt update - - name: Install Dependencies - run: sudo apt install -y libgtk-3-dev libuv1-dev libargtable2-dev libserialport-dev libconfig-dev libhidapi-dev liblua5.4-dev libxdg-basedir-dev libxml2-dev libpulse-dev libproc2-dev libcurl4-openssl-dev libglu1-mesa-dev + # Must come before actions/checkout: these images are minimal and ship + # no git, so checkout would fall back to a tarball download and silently + # skip submodules -- simapi and nappgui_src are both required to build. + # No sudo either; container jobs already run as root. + - name: Install build dependencies + run: | + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates curl git \ + build-essential cmake pkg-config \ + libgtk-3-dev libuv1-dev libargtable2-dev libserialport-dev \ + libconfig-dev libhidapi-dev liblua5.4-dev libxdg-basedir-dev \ + libxml2-dev libpulse-dev libproc2-dev libcurl4-openssl-dev \ + libglu1-mesa-dev + + - uses: actions/checkout@v4 + with: + submodules: recursive + + # $GITHUB_WORKSPACE, not ${{ github.workspace }}: in a container job the + # expression still renders the *host* path (/home/runner/work/...) while + # the repo is actually mounted at /__w/... inside the container. Using + # the expression made cmake fail with `The source directory + # "/home/runner/work/monocoque/monocoque" does not exist`. The env var is + # set to the container-side path, so it's correct either way. - name: Set build dir id: strings shell: bash run: | - echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + echo "build-output-dir=$GITHUB_WORKSPACE/build" >> "$GITHUB_OUTPUT" github_sha_hash=${{ github.sha }} echo "github-sha-short=${github_sha_hash:0:7}" >> $GITHUB_OUTPUT @@ -38,7 +91,7 @@ jobs: -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc -DCMAKE_BUILD_TYPE=Release - -S ${{ github.workspace }} + -S "$GITHUB_WORKSPACE" - name: Build run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release @@ -51,48 +104,380 @@ jobs: mkdir -p PKG_SOURCE/usr/share/applications mkdir -p PKG_SOURCE/usr/share/pixmaps mkdir -p PKG_SOURCE/usr/share/icons/hicolor/scalable/apps + mkdir -p PKG_SOURCE/usr/share/monocoque - name: Download xpm icon run: | curl -sSL https://repo.spacefreak18.xyz/monocoque/icons/monocoque.xpm -o PKG_SOURCE/usr/share/pixmaps/monocoque.xpm - name: Download svg icon run: | curl -sSL https://repo.spacefreak18.xyz/monocoque/icons/monocoque.svg -o PKG_SOURCE/usr/share/icons/hicolor/scalable/apps/monocoque.svg + - name: Determine package version + id: pkgver + shell: bash + env: + RELEASE_TAG: ${{ inputs.release_tag }} + run: | + # The control files carry a hardcoded `Version: 1` that nobody + # maintains, so every .deb ever released claims version 1 regardless + # of its tag. Take the version from the tag instead. + # + # Guarded rather than used blindly: a Debian version must start with a + # digit, so a branch build (workflow_dispatch) or a non-release tag + # like `pkgtest-5` would make dpkg-deb fail outright. Anything that + # doesn't look like a version falls back to 0.0.0. + v="" + # RELEASE_TAG is set only when release-please calls this workflow; + # its tag exists but the run itself is on a branch, so GITHUB_REF_* + # would report master and fall through to 0.0.0. + v="${RELEASE_TAG#v}" + [ -z "$v" ] && [ "$GITHUB_REF_TYPE" = tag ] && v="${GITHUB_REF_NAME#v}" + case "$v" in + [0-9]*) ;; + *) v="0.0.0" ;; + esac + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "packaging as version $v" + - name: Copy script files around to stop .github from being added to the package then build the package run: | cp ./tools/distro/debian/dpkg/${{ matrix.os }}/control ./PKG_SOURCE/DEBIAN/control + sed -i "s/^Version:.*/Version: ${{ steps.pkgver.outputs.version }}/" ./PKG_SOURCE/DEBIAN/control cp ./tools/distro/debian/dpkg/copyright ./PKG_SOURCE/DEBIAN/copyright cp ./build/monocoque ./PKG_SOURCE/usr/bin/monocoque cp ./build/gmonocoque ./PKG_SOURCE/usr/bin/gmonocoque cp ./tools/monocoque.desktop ./PKG_SOURCE/usr/share/applications/monocoque.desktop dpkg-deb --build PKG_SOURCE monocoque-${{ matrix.os }}.deb + echo "--- control as packaged ---" + dpkg-deb --field monocoque-${{ matrix.os }}.deb Package Version - name: Release the Package + # Only a tag can publish a release; without this a workflow_dispatch run + # builds everything correctly and then reports failure on a step that + # cannot work, which makes a green build look broken. + if: github.ref_type == 'tag' || inputs.release_tag != '' uses: softprops/action-gh-release@v1 with: + tag_name: ${{ inputs.release_tag || github.ref_name }} files: monocoque-${{ matrix.os }}.deb + # release-please writes the release body from the changelog it just + # generated. Letting this action generate its own notes as well would + # overwrite them, so it only does so for a hand-pushed tag. + generate_release_notes: ${{ inputs.release_tag == '' }} build-monocoque-rpms: + # Same self-hosted-label problem as the deb jobs above (fedora-43 was a + # runner label with nothing online to claim it); now a container on an + # ordinary GitHub-hosted runner, with fedora-44 added alongside 43. strategy: + fail-fast: false matrix: - os: [fedora-43] - runs-on: ${{ matrix.os }} + include: + - os: fedora-43 + image: fedora:43 + - os: fedora-44 + image: fedora:44 + runs-on: ubuntu-latest + container: + image: ${{ matrix.image }} permissions: contents: write steps: + # The -devel list is the one fedora.spec's own header comment documents, + # plus gtk3/libcurl/mesa-libGLU to match its `Requires:` line -- the spec + # configures with -DBUILD_GUI=on, so the GUI's deps are build deps too. + # + # Retried because this pulls openh264 from Fedora's third-party Cisco + # repo (ciscobinary.openh264.org), which is a single-mirror CDN outside + # Fedora's own infrastructure and has proven flaky: the fedora-43 leg of + # the first run died on `Curl error (7): Could not connect to server` + # for it while fedora-44 downloaded the very same package fine in the + # same run -- i.e. transient, not a dependency or package-name problem. + # Not disabling that repo outright: openh264 is still pulled in even + # with weak deps off, so something here requires it outright (which + # package wasn't traced) and dropping the repo would likely break + # dependency resolution rather than fix anything. + - name: Install build dependencies + run: | + for attempt in 1 2 3; do + dnf install -y --setopt=install_weak_deps=False \ + rpm-build cmake gcc gcc-c++ make git curl \ + pulseaudio-libs-devel argtable-devel libconfig-devel hidapi-devel \ + libserialport-devel lua-devel libuv-devel libxdg-basedir-devel \ + libxml2-devel procps-ng-devel gtk3-devel libcurl-devel \ + mesa-libGLU-devel && exit 0 + echo "dnf install failed (attempt $attempt), retrying in 15s..." + sleep 15 + done + echo "dnf install failed after 3 attempts" >&2 + exit 1 + + # submodules: recursive is new here. It didn't matter while %prep cloned + # upstream and ran `git submodule update` itself; now that the staged + # checkout is what gets built, an empty nappgui_src fails configure with + # "does not contain a CMakeLists.txt file". Missed by the local fedora:44 + # rehearsal, which mounted a working tree that already had its submodules. + - uses: actions/checkout@v4 + with: + submodules: recursive - name: create rpmbuild dirs run: mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} - - name: get spec file - run: curl -o ~/fedora.spec https://raw.githubusercontent.com/Spacefreak18/monocoque/refs/heads/master/tools/distro/fedora/rpm/fedora.spec + + # The spec's %prep used to `git clone` upstream master, so the rpm's + # contents tracked master rather than the tag being built -- an rpm built + # from a tag would quietly ship master's code. Staged here and copied by + # %prep instead, so a package's contents are the commit it was built from. + - name: Stage the checked-out tree as the rpm source + run: | + rm -rf ~/rpmbuild/SOURCES/monocoque + cp -r "$GITHUB_WORKSPACE" ~/rpmbuild/SOURCES/monocoque + rm -rf ~/rpmbuild/SOURCES/monocoque/.github + + # Uses the spec from the checked-out tree instead of curl'ing it from + # upstream's master branch, so a tag builds the spec that tag contains. + # + # The spec's `Version: 0.0.5` is stale and hand-maintained -- real tags are + # well past it, so released rpms have been installing as monocoque-0.0.5. + # Stamped from the tag here rather than edited in the spec, so the file + # stays a working default for local rpmbuild while CI releases carry the + # real version. Same digit guard as the deb job: rpm versions can't start + # with a non-digit, and scratch tags / branch builds would break it. + # + # NOTE (pre-existing, deliberately not fixed here): the spec's own %prep + # still does a fresh `git clone` of upstream master for the *sources*, + # so the rpm's contents track master rather than the tag being built. + # Worth fixing separately -- it needs the spec parameterised, which is a + # bigger change than getting these jobs running again. - name: run spec file - run: rpmbuild -ba ~/fedora.spec + shell: bash + env: + RELEASE_TAG: ${{ inputs.release_tag }} + run: | + v="" + # RELEASE_TAG is set only when release-please calls this workflow; + # its tag exists but the run itself is on a branch, so GITHUB_REF_* + # would report master and fall through to 0.0.0. + v="${RELEASE_TAG#v}" + [ -z "$v" ] && [ "$GITHUB_REF_TYPE" = tag ] && v="${GITHUB_REF_NAME#v}" + case "$v" in + [0-9]*) ;; + *) v="0.0.0" ;; + esac + echo "packaging as version $v" + sed -i "s/^Version:.*/Version: $v/" tools/distro/fedora/rpm/fedora.spec + rpmbuild -ba tools/distro/fedora/rpm/fedora.spec + + # Glob rather than a hardcoded monocoque-0.0.5-1.x86_64.rpm: the filename + # tracks the spec's Version/Release, which is now stamped per-tag above. - name: rename file - run: cp ~/rpmbuild/RPMS/x86_64/monocoque-0.0.5-1.x86_64.rpm $GITHUB_WORKSPACE/monocoque-${{ matrix.os }}.rpm + run: | + cp ~/rpmbuild/RPMS/x86_64/monocoque-*.rpm "$GITHUB_WORKSPACE/monocoque-${{ matrix.os }}.rpm" + echo "--- rpm identity as built ---" + rpm -qp --qf '%{NAME} %{VERSION}-%{RELEASE}\n' "$GITHUB_WORKSPACE/monocoque-${{ matrix.os }}.rpm" + - name: Release the Package + # Only a tag can publish a release; without this a workflow_dispatch run + # builds everything correctly and then reports failure on a step that + # cannot work, which makes a green build look broken. + if: github.ref_type == 'tag' || inputs.release_tag != '' uses: softprops/action-gh-release@v1 with: + tag_name: ${{ inputs.release_tag || github.ref_name }} files: monocoque-${{ matrix.os }}.rpm + # release-please writes the release body from the changelog it just + # generated. Letting this action generate its own notes as well would + # overwrite them, so it only does so for a hand-pushed tag. + generate_release_notes: ${{ inputs.release_tag == '' }} + + + build-monocoque-appimage: + # An AppImage only runs where glibc is at least as new as the one it + # linked against, so it wants the OLDEST base that can build the code -- + # building on current Ubuntu silently excludes older distros and defeats + # the point of shipping one. + # + # That floor is set by the compiler, not by choice: simapi's F1 2018 + # headers use C23 enum-with-fixed-underlying-type (`enum TrackID : + # uint8_t`), which GCC only implements from 13 onwards. ubuntu-22.04 + # ships GCC 11 and fails outright with `expected identifier or '(' before + # ':' token` -- confirmed by a real run, and no -std= value rescues it + # (verified separately: the construct is gated on compiler version, not + # on the standard flag). ubuntu-24.04 / GCC 13 is therefore the oldest + # usable base, giving a glibc 2.39 floor. + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + # librsvg2-common and libgtk-3-bin are for linuxdeploy's gtk plugin, not + # the build itself -- it needs an svg pixbuf loader and + # gtk-update-icon-cache to assemble a self-contained GTK runtime. + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake pkg-config \ + libgtk-3-dev libuv1-dev libargtable2-dev libserialport-dev \ + libconfig-dev libhidapi-dev liblua5.4-dev libxdg-basedir-dev \ + libxml2-dev libpulse-dev libproc2-dev libcurl4-openssl-dev \ + libglu1-mesa-dev \ + desktop-file-utils file patchelf wget \ + librsvg2-common libgtk-3-bin + + # -std=gnu2x for the C23 enum syntax above: GCC 13 implements it but + # still defaults to gnu17, unlike GCC 15+ (which the container-based + # deb/rpm legs get, and which accept it without the flag). gnu2x rather + # than c2x so GNU extensions stay on -- simapi also relies on asprintf. + - name: Build + run: | + cmake -B build -DBUILD_GUI=on -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS=-std=gnu2x -S . + cmake --build build --config Release + + # Uses the install() rules rather than hand-copying binaries the way the + # deb job does -- that's what they were added for. + - name: Populate AppDir + run: | + DESTDIR="$PWD/AppDir" cmake --install build --prefix /usr + # Icon basename must match the desktop file's `Icon=monocoque`. + # tools/monocoque.svg is the square copy shared with the Flatpak + # build; the published 100x50 artwork is rejected by icon tooling. + install -Dm644 tools/monocoque.svg \ + AppDir/usr/share/icons/hicolor/scalable/apps/monocoque.svg + + # APPIMAGE_EXTRACT_AND_RUN=1 because these tools are themselves + # AppImages and there is no FUSE on the runner to mount them with. + - name: Build AppImage + env: + APPIMAGE_EXTRACT_AND_RUN: 1 + RELEASE_TAG: ${{ inputs.release_tag }} + run: | + # An AppImage carries no package metadata, so its filename is the only + # place a version can live -- hence versioning the output name here + # rather than stamping a field as the deb/rpm jobs do. Same digit + # guard, so scratch tags and branch builds still produce a valid name. + v="" + # RELEASE_TAG is set only when release-please calls this workflow; + # its tag exists but the run itself is on a branch, so GITHUB_REF_* + # would report master and fall through to 0.0.0. + v="${RELEASE_TAG#v}" + [ -z "$v" ] && [ "$GITHUB_REF_TYPE" = tag ] && v="${GITHUB_REF_NAME#v}" + case "$v" in + [0-9]*) ;; + *) v="0.0.0" ;; + esac + export OUTPUT="monocoque-${v}-x86_64.AppImage" + echo "packaging as $OUTPUT" + + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/raw/master/linuxdeploy-plugin-gtk.sh + chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-gtk.sh + + # Deploy dependencies first, without packaging, so there is a populated + # AppDir to take licences from -- an AppImage distributes every library + # it bundles, so their copyright files have to travel with it. A .deb + # needs none of this because its dependencies stay separate packages. + PATH="$PWD:$PATH" ./linuxdeploy-x86_64.AppImage \ + --appdir AppDir \ + --plugin gtk \ + --desktop-file AppDir/usr/share/applications/monocoque.desktop \ + --icon-file tools/monocoque.svg + + tools/appimage-collect-licenses.sh AppDir + + # monocoque itself and its vendored components are built here rather + # than installed from packages, so dpkg cannot account for them. + install -Dm644 LICENSE.rst AppDir/usr/share/doc/monocoque/LICENSE.rst + install -Dm644 tools/distro/debian/dpkg/copyright AppDir/usr/share/doc/monocoque/copyright + install -Dm644 src/monocoque/mgui/nappgui_src/LICENSE AppDir/usr/share/doc/nappgui/LICENSE + + # Now package the AppDir, licences included. + PATH="$PWD:$PATH" ./linuxdeploy-x86_64.AppImage \ + --appdir AppDir \ + --output appimage + ls -la monocoque*.AppImage + echo "--- licence files in the image ---" + find AppDir/usr/share/doc -name copyright | wc -l + + - name: Upload the AppImage as a build artifact + uses: actions/upload-artifact@v4 + with: + name: monocoque-x86_64.AppImage + path: monocoque*.AppImage + + - name: Release the Package + # Only a tag can publish a release; without this a workflow_dispatch run + # builds everything correctly and then reports failure on a step that + # cannot work, which makes a green build look broken. + if: github.ref_type == 'tag' || inputs.release_tag != '' + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ inputs.release_tag || github.ref_name }} + files: monocoque*.AppImage + # release-please writes the release body from the changelog it just + # generated. Letting this action generate its own notes as well would + # overwrite them, so it only does so for a hand-pushed tag. + generate_release_notes: ${{ inputs.release_tag == '' }} + + + build-monocoque-flatpak: + # Folded in from the standalone flatpak.yml so every artifact lands on + # one release. Kept as continue-on-error because it is the newest and + # least battle-tested leg: a Flatpak failure must never turn a release + # red or block the deb/rpm/AppImage artifacts, which are independent + # jobs and upload regardless. + runs-on: ubuntu-22.04 + continue-on-error: true + permissions: + contents: write + steps: + # No submodules: recursive here -- the manifest fetches simapi and + # nappgui_src itself as explicit `type: git` sources (pinned commits, + # reproducible regardless of what's checked out on disk). Confirmed + # via a failed CI run: recursive submodule checkout populates those + # paths with real .git files first, which then collides with + # flatpak-builder's own git source trying to clone into the same spot + # ("cannot overwrite non-directory .git with directory"). + - uses: actions/checkout@v4 + + # ppa:flatpak/stable, not Ubuntu's own package: 22.04's flatpak-builder + # predates freedesktop Sdk 25.08 dropping the legacy appstream-compose + # binary in favour of `appstreamcli compose`, and fails at the finish + # stage without that fallback. + - name: Install flatpak-builder + freedesktop runtime/SDK + run: | + sudo add-apt-repository -y ppa:flatpak/stable + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder + sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + sudo flatpak install -y --noninteractive flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08 + + - name: Build .flatpak + working-directory: flatpak + run: | + flatpak-builder --force-clean --repo=repo build-dir io.github.spacefreak18.monocoque.yml + flatpak build-bundle repo monocoque.flatpak io.github.spacefreak18.monocoque + + - name: Upload .flatpak as a build artifact + uses: actions/upload-artifact@v4 + with: + name: monocoque.flatpak + path: flatpak/monocoque.flatpak + + - name: Release the Package + if: github.ref_type == 'tag' || inputs.release_tag != '' + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ inputs.release_tag || github.ref_name }} + files: flatpak/monocoque.flatpak + # release-please writes the release body from the changelog it just + # generated. Letting this action generate its own notes as well would + # overwrite them, so it only does so for a hand-pushed tag. + generate_release_notes: ${{ inputs.release_tag == '' }} #arch-build: # runs-on: ubuntu-latest diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml new file mode 100644 index 0000000..9b8d83b --- /dev/null +++ b/.github/workflows/pr-build.yaml @@ -0,0 +1,27 @@ +name: PR Build & Test + +on: + pull_request: + push: + branches: [ "master" ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Update apt + run: sudo apt update + - name: Install dependencies + # Same list as ci.yaml's ubuntu-latest leg, minus the GUI-only deps + # (libgtk-3-dev, libglu1-mesa-dev) and libcurl4-openssl-dev — this + # workflow builds the CLI + test suite only, not the GUI. + run: sudo apt install -y libuv1-dev libargtable2-dev libserialport-dev libconfig-dev libhidapi-dev liblua5.4-dev libxdg-basedir-dev libxml2-dev libpulse-dev libproc2-dev + - name: Configure CMake + run: cmake -B build -DENABLE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug + - name: Build + run: cmake --build build --config Debug + - name: Run tests + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..a89c953 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,38 @@ +name: release-please + +on: + push: + branches: [ master ] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # Builds and uploads the packages for the release that was just cut. + # + # This calls ci.yaml rather than relying on its `push: tags` trigger, + # because a tag pushed with the default GITHUB_TOKEN does not start a new + # workflow -- GitHub blocks that deliberately, to stop workflows triggering + # each other in a loop. Calling it keeps the whole release inside one run + # and avoids storing a personal access token on a public repository. + build-release: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + uses: ./.github/workflows/ci.yaml + with: + release_tag: ${{ needs.release-please.outputs.tag_name }} + permissions: + contents: write diff --git a/.gitignore b/.gitignore index a142b79..cf9bd69 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ /src/monocoque/simulatorapi/simapi .vscode + +# flatpak-builder outputs from local builds -- CI regenerates these. +# flatpak/repo in particular is an OSTree store: ~1000 binary objects. +/flatpak/repo/ +/flatpak/build-dir/ +/flatpak/.flatpak-builder/ +# The bundle itself. Missing from this list once already, and a `git add -A` +# duly committed a 6MB build artifact. +*.flatpak diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..43c512a --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1 @@ +{ ".": "0.3.6" } diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a2c551..6ba8653 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ SET_SOURCE_FILES_PROPERTIES( src/monocoque.c PROPERTIES LANGUAGE C) option(BUILD_GUI "Build GUI support" OFF) option(BUILD_SHARED "Build monocoque shared library" OFF) +option(ENABLE_TESTS "Build test suite" OFF) project(monocoque) @@ -72,9 +73,15 @@ pkg_check_modules(LIBPROC2 libproc2) if (LIBPROCPS_FOUND) #add_compile_definitions(USE_OLD_PID_VAL=0) elseif (LIBPROC2_FOUND) - if (LIBPROC2_VERSION VERSION_GREATER_EQUAL "4.0.5") - #add_compile_definitions(USE_OLD_PID_VAL=0) - else() + # Some distro packagings (e.g. the freedesktop Flatpak SDK) ship a + # libproc2.pc with `Version: UNKNOWN` rather than a real version + # string. VERSION_GREATER_EQUAL against that always evaluates false, + # silently falling through to the old (pre-4.0.5) PIDS_VAL API and + # failing to compile against an actually-current libproc2 whose real + # header has already moved to the new 3-argument macro. Only take the + # old-API branch when the version string is genuinely parseable and + # actually old. + if (LIBPROC2_VERSION MATCHES "^[0-9]" AND LIBPROC2_VERSION VERSION_LESS "4.0.5") add_compile_definitions(USE_OLD_PID_VAL=1) endif() else() @@ -107,57 +114,6 @@ endif() -#add_executable(listusb tests/testlibusb.c) -#target_include_directories(listusb PUBLIC) -#target_link_libraries(listusb portaudio hidapi-hidraw) -#add_test(listusb list-usb-devices listusb) - -#add_executable(testrevburner tests/testrevburner.c) -#target_include_directories(testrevburner PUBLIC) -#target_link_libraries(testrevburner hidapi-hidraw) -#add_test(testrevburner testrevburner) - -#add_executable(listsound tests/pa_devs.c) -#target_include_directories(listsound PUBLIC) -#target_link_libraries(listsound m portaudio) -#add_test(list-sound-devices listsound) -# -#add_executable(longsine tests/patest_longsine.c) -#target_include_directories(longsine PUBLIC ${LIBXML_INCLUDE_DIR}) -#target_link_libraries(longsine m portaudio) -#add_test(longsine longsine) -# -#add_executable(parserevburnerxml tests/revburnerparsetest.c) -#target_include_directories(parserevburnerxml PUBLIC ${LIBXML_INCLUDE_DIR}) -#target_link_libraries(parserevburnerxml portaudio xml2) -#add_test(parserevburnerxml parserevburnerxml) -# -add_executable(setmem tests/setmem.c) -target_include_directories(setmem PUBLIC) -target_link_libraries(setmem) -add_test(setmem setmem) - -add_executable(getmem tests/getmem.c) -target_include_directories(getmem PUBLIC) -target_link_libraries(getmem) -add_test(getmem getmem) -# -#add_executable(setsimdata tests/setsimdata.c) -#target_include_directories(setsimdata PUBLIC) -#target_link_libraries(setsimdata) -#add_test(setsimdata setsimdata) -# -#add_executable(hidtest tests/hidtest.c) -#target_include_directories(hidtest PUBLIC) -#target_link_libraries(hidtest hidapi-hidraw) -#add_test(hidtest hidtest) -# -add_executable(simlighttest tests/simlighttest.c) -target_include_directories(simlighttest PUBLIC) -target_link_libraries(simlighttest serialport) -add_test(simlighttest simlighttest) - - # used for enabling additional compiler options if supported include(CheckCXXCompilerFlag) @@ -221,6 +177,12 @@ endif() # unit tests --only enable if requested AND we're not building as a sub-project if(ENABLE_TESTS AND NOT MONOCOQUE_SUBPROJECT) message(STATUS "[monocoque] Unit Tests Enabled") - add_subdirectory(tests) enable_testing() + add_subdirectory(tests) endif() + +install(TARGETS monocoque-cli RUNTIME DESTINATION bin) +if(BUILD_GUI) + install(TARGETS monocoque-gui RUNTIME DESTINATION bin) +endif() +install(FILES tools/monocoque.desktop DESTINATION share/applications) diff --git a/flatpak/io.github.spacefreak18.monocoque.desktop b/flatpak/io.github.spacefreak18.monocoque.desktop new file mode 100644 index 0000000..5e0c2fb --- /dev/null +++ b/flatpak/io.github.spacefreak18.monocoque.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Monocoque +Comment=Device manager for racing sims +Exec=gmonocoque +Icon=io.github.spacefreak18.monocoque +Terminal=false +Categories=Game;Development; diff --git a/flatpak/io.github.spacefreak18.monocoque.metainfo.xml b/flatpak/io.github.spacefreak18.monocoque.metainfo.xml new file mode 100644 index 0000000..df48103 --- /dev/null +++ b/flatpak/io.github.spacefreak18.monocoque.metainfo.xml @@ -0,0 +1,27 @@ + + + io.github.spacefreak18.monocoque + Monocoque + Device manager for racing sims + CC0-1.0 + GPL-3.0-only + +

+ Monocoque bridges telemetry from supported racing simulators to + peripheral devices such as haptic shakers, LED strips, shift lights, + and sim wind fans, and exposes a local API for third-party dashboard + and configuration tools. +

+
+ io.github.spacefreak18.monocoque.desktop + + monocoque + gmonocoque + + https://spacefreak18.github.io/simapi/monocoque.html + https://github.com/Spacefreak18/monocoque + + + + +
diff --git a/flatpak/io.github.spacefreak18.monocoque.yml b/flatpak/io.github.spacefreak18.monocoque.yml new file mode 100644 index 0000000..073e746 --- /dev/null +++ b/flatpak/io.github.spacefreak18.monocoque.yml @@ -0,0 +1,320 @@ +# Flatpak manifest for monocoque -- NOT submitted anywhere (Flathub or +# otherwise) yet. Built 2026-08-30 on the patterns proven working for the +# sibling typiql-tauri/typiql project's own Flatpak pipeline (see this +# repo's .github/workflows/flatpak.yml header for the full list of bugs +# found and fixed here, monocoque-specific beyond what that sibling project +# already covered). VERIFIED LOCALLY end-to-end: built, installed, and ran +# both `monocoque --help` and `gmonocoque` from the resulting bundle. Not +# yet verified against real CI or real hardware (HID/serial/USB device +# access under the sandbox). +# +# org.freedesktop.Platform, not org.gnome.Platform: monocoque's GUI +# (gmonocoque) only needs GTK3, which freedesktop.Platform//25.08 already +# ships -- no need for the heavier GNOME runtime typiql needs for WebKitGTK. +# simd is NOT bundled. It is a separate daemon with its own packaging, and +# monocoque does not need it inside the sandbox to use it: simapi_get_sim() +# checks /dev/shm/SIMAPI.DAT before it ever scans /proc, so a simd running on +# the host -- however it was installed -- publishes telemetry this sandbox +# reads directly. That is what --device=shm below is for, and it is the only +# thing this manifest needs in order to work with simd. +# +# simapi itself is still present, but only as monocoque's own statically +# linked submodule, exactly as upstream's CMakeLists builds it. There is no +# separate simapi module here. +app-id: io.github.spacefreak18.monocoque +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +command: gmonocoque +finish-args: + # --socket=x11, not fallback-x11: nappgui (the GUI toolkit gmonocoque uses) + # goes to X11 even when a Wayland display is available. fallback-x11 is + # deliberately inactive whenever Wayland works, so under a Wayland session + # DISPLAY stayed empty and gmonocoque died with `cannot open display` -- + # confirmed on a real launch, with WAYLAND_DISPLAY=wayland-0 present and the + # socket visible inside the sandbox. Granting real X11 routes it through + # XWayland instead. --socket=wayland is kept so the toolkit can use it + # directly if that ever changes. + - --socket=x11 + - --socket=wayland + # Standard companion to X11 access: without it MIT-SHM is unavailable and + # some toolkits fall over. + - --share=ipc + - --socket=pulseaudio + # HID/serial/USB access for wheels, pedals, shifters, and shaker/LED + # devices -- known risk area, Flatpak's sandboxing of raw device nodes is + # not always well-behaved; needs real hardware testing, not just a + # successful build. + - --device=all + # Retained, but not for the reason first written here: the GraphQL API on + # port 9000 is served by typiql's own Rust backend, not by monocoque -- + # monocoque has no network code of its own, and the only libcurl in the + # binary is vendored nappgui's HTTP module, which nothing here calls. The + # two programs talk through ~/.config/monocoque and the process table, not + # a socket. This flag can probably be dropped. + - --share=network + # monocoque builds its paths from $HOME directly (e.g. + # "%s/.config/monocoque/diameters.config" in monocoque-cli.c), not from + # XDG_CONFIG_HOME, so it reads and writes the host's real directories. + # Without these grants a real launch failed with + # Failed to read config file '/home/david/.config/monocoque/monocoque.config' + # slog_open_file: Failed to open file: /home/david/.cache/monocoque/...log + # -- the manifest previously granted no filesystem access at all. Sharing + # the real config directory is also what makes a Flatpak monocoque and a + # native one agree, and what lets typiql configure it. + - --filesystem=xdg-config/monocoque:create + - --filesystem=xdg-cache/monocoque:create + # Host /dev/shm, where a host simd publishes telemetry as POSIX shared memory + # (/dev/shm/SIMAPI.DAT, mapped in simapi's simmapper.c). This is the flag + # that makes a Flatpak monocoque work with a simd installed on the host, so + # it matters more now that simd is not bundled, not less. + # + # shm is its own device option and is NOT implied by --device=all above -- + # measured, with simd running: `ls /dev/shm` inside this sandbox returned 0 + # entries against 18 on the host. The failure it caused is silent rather + # than loud: `monocoque play` started, read its config, connected + # pulseaudio, and then sat at "setting initial app state" forever, never + # logging "Opening universal shared memory api", because simd looked absent. + # The identical run with this flag reached that line one second in. + - --device=shm + +modules: + - name: libconfig + buildsystem: cmake-ninja + config-opts: + - -DBUILD_TESTS=OFF + # libconfig 1.7.3's CMakeLists.txt predates CMake 3.5's minimum + # version policy; confirmed via a failed local build. + - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/libconfig/$(basename "$f"); else echo "WARNING: no licence file found for libconfig"; fi' + sources: + - type: archive + url: https://github.com/hyperrealm/libconfig/archive/refs/tags/v1.7.3.tar.gz + sha256: 68757e37c567fd026330c8a8449aa5f9cac08a642f213f2687186b903bd7e94e + + - name: argtable2 + buildsystem: cmake-ninja + config-opts: + # Same CMake-minimum-version issue as libconfig -- this one's + # CMakeLists.txt is even older (2011). + - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + # 2011-era code missing / includes for + # isspace/toupper/bzero; GCC 14+ made implicit-function-declaration a + # hard error by default where it used to just warn. Confirmed via a + # failed local build. + - -DCMAKE_C_FLAGS=-Wno-error=implicit-function-declaration + # add_library(argtable2 ...) has no explicit STATIC/SHARED, so it + # defaults to CMake's global BUILD_SHARED_LIBS (off by default -> a + # static .a). monocoque's own target_link_libraries() lists argtable2 + # before helper (which needs its symbols, e.g. arg_end/arg_parse in + # parameters.c) -- fine for a shared library (all symbols always + # available regardless of link-line position, which is what real + # distro packages of this ship), but a static archive only pulls + # symbols already needed by things linked *before* it, so helper's + # later reference to argtable2's symbols never resolves. Confirmed + # via a failed local build ("undefined reference to arg_end" etc. + # from helper's parameters.c, even though libargtable2.a genuinely + # contained those symbols). Building shared instead matches how this + # dependency normally ships and avoids the link-order sensitivity + # entirely, rather than reordering every target_link_libraries() call + # in CMakeLists.txt. + - -DBUILD_SHARED_LIBS=ON + # This CMake port has no install() rules at all (confirmed: no + # CMakeLists.txt in the tree calls install()), so the default + # cmake-ninja `ninja install` step silently does nothing -- monocoque's + # own build then fails on a missing argtable2.h. build-commands here + # replace the default install step: `ninja` still does the real + # compile, then copy the header and whatever library form actually got + # built (glob rather than a fixed versioned filename, since the exact + # .so version suffix isn't predictable without a build). + build-commands: + - ninja + - install -Dm644 src/argtable2.h /app/include/argtable2.h + - | + for f in src/libargtable2.so* src/libargtable2.a; do + if [ -e "$f" ]; then cp -P "$f" /app/lib/; fi + done + true + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/argtable2/$(basename "$f"); else echo "WARNING: no licence file found for argtable2"; fi' + sources: + - type: archive + url: https://sourceforge.net/projects/argtable/files/argtable/argtable-2.13/argtable2-13.tar.gz/download + sha256: 8f77e8a7ced5301af6e22f47302fdbc3b1ff41f2b83c43c77ae5ca041771ddbf + dest-filename: argtable2-13.tar.gz + + - name: lua5.4 + # Lua's own Makefile, not CMake/autotools. `posix` (not `linux`) avoids + # a readline dependency only needed for the interactive REPL binary, + # which monocoque doesn't use -- only the library. + buildsystem: simple + build-commands: + - make posix MYCFLAGS=-fPIC + - make install INSTALL_TOP=/app + sources: + - type: archive + url: https://www.lua.org/ftp/lua-5.4.7.tar.gz + sha256: 9fbf5e28ef86c69858f6d3d34eccc32e911c1a28b4120ff3e84aaa70cfbf1e30 + + - name: libserialport + buildsystem: autotools + # No pre-generated configure script in the git-archive tarball, only + # autogen.sh. + build-commands: + - ./autogen.sh + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/libserialport/$(basename "$f"); else echo "WARNING: no licence file found for libserialport"; fi' + sources: + - type: archive + url: https://github.com/sigrokproject/libserialport/archive/refs/tags/libserialport-0.1.2.tar.gz + sha256: cbb1192a09ff31d34e7efdb17a2f50d9d1974461c0b81c29bb449515d78d8950 + + - name: libxdg-basedir + buildsystem: autotools + build-commands: + - ./autogen.sh + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/libxdg-basedir/$(basename "$f"); else echo "WARNING: no licence file found for libxdg-basedir"; fi' + sources: + - type: archive + url: https://github.com/devnev/libxdg-basedir/archive/refs/tags/libxdg-basedir-1.2.3.tar.gz + sha256: ff30c60161f7043df4dcc6e7cdea8e064e382aa06c73dcc3d1885c7d2c77451d + + - name: hidapi + buildsystem: cmake-ninja + config-opts: + # Same CMake-minimum-version issue as libconfig/argtable2, despite + # this otherwise being a current, actively maintained release. + - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + # monocoque links hidapi-hidraw specifically -- no need for the + # libusb-backed variant, which would pull in an extra dependency. + - -DHIDAPI_WITH_LIBUSB=OFF + - -DHIDAPI_WITH_HIDRAW=ON + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/hidapi/$(basename "$f"); else echo "WARNING: no licence file found for hidapi"; fi' + sources: + - type: archive + url: https://github.com/libusb/hidapi/archive/refs/tags/hidapi-0.14.0.tar.gz + sha256: a5714234abe6e1f53647dd8cba7d69f65f71c558b7896ed218864ffcf405bcbd + + - name: glu + buildsystem: meson + sources: + - type: archive + url: https://gitlab.freedesktop.org/mesa/glu/-/archive/glu-9.0.3/glu-glu-9.0.3.tar.gz + sha256: 7e919cbc1b2677b01d65fc28fd36a19d1f3e23d76663020e0f3b82b991475e8b + + - name: libuv + buildsystem: cmake-ninja + config-opts: + - -DBUILD_TESTING=OFF + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/libuv/$(basename "$f"); else echo "WARNING: no licence file found for libuv"; fi' + sources: + - type: archive + url: https://github.com/libuv/libuv/archive/refs/tags/v1.49.2.tar.gz + sha256: 388ffcf3370d4cf7c4b3a3205504eea06c4be5f9e80d2ab32d19f8235accc1cf + + - name: monocoque + buildsystem: cmake-ninja + config-opts: + - -DBUILD_GUI=on + - -DCMAKE_BUILD_TYPE=Release + # CMakeLists.txt links most of the dependencies built as separate + # flatpak modules above (hidapi-hidraw, serialport, argtable2, config, + # uv, xdg-basedir) by bare library name with no explicit -L, relying on + # the linker's default search path -- fine on a real system where these + # install to /usr/lib, but /app/lib isn't in that default path. + # -DCMAKE_EXE_LINKER_FLAGS=-L/app/lib didn't work: CMake appends + # EXE_LINKER_FLAGS at the *end* of the link command, after the -l + # flags that need it, and ld resolves -l against -L directories seen + # so far, left to right -- confirmed via a failed local build showing + # the exact same missing-library set with that flag present but + # useless. LIBRARY_PATH is a GCC driver environment variable, applied + # to its search regardless of where in the command line things land. + build-options: + env: + LIBRARY_PATH: /app/lib + sources: + # One level up from flatpak/manifest.yml is the repo root -- unlike + # typiql-tauri/typiql's dual-checkout ($GITHUB_WORKSPACE containing + # two separate repos side by side, needing an extra level), monocoque + # is checked out on its own with the standard single-repo layout, so + # this doesn't need the same `../..`/`../../..` juggling that session + # got wrong twice before finding the right depth for that project. + - type: dir + path: .. + # Skip the submodule paths. If the tree being built has them checked + # out, the copy brings along a real `.git` *file* at each path, and the + # git sources below then fail to clone into the same spot: "cannot + # overwrite non-directory .git with directory". Without this the + # manifest only builds from a checkout that deliberately omits + # submodules -- which is why CI's flatpak job checks out without them, + # and why a local build right after `git submodule update --init` + # breaks. Skipping makes it work either way. + skip: + - src/monocoque/simulatorapi/simapi + - src/monocoque/mgui/nappgui_src + # Two git submodules, each pinned to the exact commit .gitmodules/the + # tree tracks -- confirmed via a failed local build that the `dir` + # source above doesn't pull submodule content in (same class of issue + # as typiql-tauri/typiql's own submodules, src/lib/per-form etc.). + - type: git + url: https://github.com/spacefreak18/simapi + commit: 50fd181681862256780b2a0f74ef4bc459ed631a + dest: src/monocoque/simulatorapi/simapi + # nappgui_src is add_subdirectory()'d directly into monocoque's own + # CMake tree (src/monocoque/mgui/CMakeLists.txt), not a separately + # installed library -- so it's a source here, not its own flatpak + # module. + - type: git + url: https://github.com/frang75/nappgui_src + commit: 29c7bd3e0243eb38aa9a7e2eafe6cf74830c4ee8 + dest: src/monocoque/mgui/nappgui_src + - type: file + path: io.github.spacefreak18.monocoque.desktop + - type: file + path: io.github.spacefreak18.monocoque.metainfo.xml + # Icon comes from tools/monocoque.svg via the `dir` source above (no + # separate file source needed). That file is a square 100x100 copy of + # the upstream artwork -- the published monocoque.svg the deb job + # downloads is 100x50, which Flatpak's export rejects outright, while + # appstreamcli separately hard-requires any desktop-application to + # have an icon at all (both confirmed via failed real CI runs). It is + # padded via a , not stretched. Shared + # with the AppImage job, which has the same square-icon need. + post-install: + # This package's own licence, the vendored nappgui's (a source rather + # than a module, so the per-module collection above misses it), and the + # notices file naming every bundled dependency's origin. + - install -Dm644 LICENSE.rst /app/share/licenses/monocoque/LICENSE.rst + # The Debian copyright file is upstream's own account of what is in this + # source tree and under what terms -- slog (MIT), nappgui (MIT), simapi + # (LGPL) -- which a bundle has to carry because it distributes all of + # them, unlike a .deb whose dependencies are separate packages. Shipped + # verbatim rather than restated. + - install -Dm644 tools/distro/debian/dpkg/copyright /app/share/licenses/monocoque/copyright + # lua and glu ship no licence file in their tarballs -- Lua's MIT text + # lives in doc/readme.html, glu's SGI Free B only in source headers -- so + # checked-in copies are installed from here, where the repo is the build + # directory. (Doing it in their own modules fails: those build + # out-of-tree, so their source directory is not the working directory.) + - install -Dm644 packaging/licenses/lua-5.4-LICENSE.txt /app/share/licenses/lua5.4/LICENSE + - install -Dm644 packaging/licenses/glu-LICENSE.txt /app/share/licenses/glu/LICENSE + - install -Dm644 src/monocoque/mgui/nappgui_src/LICENSE /app/share/licenses/nappgui/LICENSE + - sh -c 'python3 tools/generate-third-party-notices.py flatpak/io.github.spacefreak18.monocoque.yml > /tmp/notices.md && install -Dm644 /tmp/notices.md /app/share/licenses/THIRD-PARTY-NOTICES.md' + # Confirmed via a failed local build that flatpak-builder's cmake-ninja + # buildsystem runs the automatic build+install step regardless of + # whether build-commands are also given -- these run in *addition*, + # not instead. So the fix for simlighttest (a stray, always-on scratch + # test executable that failed to link -lserialport, a pre-existing + # CMakeLists.txt bug unrelated to Flatpak) had to be commenting it out + # at the source, not overriding the build here. With that gone, the + # automatic build+install (using this repo's own new install() rules) + # already installs monocoque/gmonocoque correctly; these build-commands + # only need to add the extra files CMake doesn't know about. + build-commands: + - install -Dm644 io.github.spacefreak18.monocoque.desktop /app/share/applications/io.github.spacefreak18.monocoque.desktop + - install -Dm644 io.github.spacefreak18.monocoque.metainfo.xml /app/share/metainfo/io.github.spacefreak18.monocoque.metainfo.xml + - install -Dm644 tools/monocoque.svg /app/share/icons/hicolor/scalable/apps/io.github.spacefreak18.monocoque.svg diff --git a/packaging/licenses/glu-LICENSE.txt b/packaging/licenses/glu-LICENSE.txt new file mode 100644 index 0000000..33308ae --- /dev/null +++ b/packaging/licenses/glu-LICENSE.txt @@ -0,0 +1,40 @@ + +SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008) +Copyright (C) 1991-2000 Silicon Graphics, Inc. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice including the dates of first publication and +either this permission notice or a reference to +http://oss.sgi.com/projects/FreeB/ +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +SILICON GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Except as contained in this notice, the name of Silicon Graphics, Inc. +shall not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization from +Silicon Graphics, Inc. +/ + Pixel storage modes */ +typedef struct { + GLint pack_alignment; + GLint pack_row_length; + GLint pack_skip_rows; + GLint pack_skip_pixels; + GLint pack_lsb_first; + GLint pack_swap_bytes; + GLint pack_skip_images; + GLint pack_image_height; + diff --git a/packaging/licenses/lua-5.4-LICENSE.txt b/packaging/licenses/lua-5.4-LICENSE.txt new file mode 100644 index 0000000..c5ba51a --- /dev/null +++ b/packaging/licenses/lua-5.4-LICENSE.txt @@ -0,0 +1,19 @@ +Copyright © 1994–2026 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..6810b19 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "simple", + "package-name": "monocoque", + + "//": "The 79 existing tags are bare semver (0.3.6), not v0.3.6, and there is only one package here. Both flags keep release-please tagging in that same series instead of starting a parallel one.", + "include-component-in-tag": false, + "include-v-in-tag": false, + + "//bump": "While the major version is 0, a breaking change bumps the minor (0.3.6 -> 0.4.0) rather than declaring 1.0.0. Reaching 1.0.0 stays a deliberate act.", + "bump-minor-pre-major": true, + + "draft": false, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements", "hidden": true }, + { "type": "revert", "section": "Reverts", "hidden": true }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "chore", "section": "Miscellaneous Chores", "hidden": true }, + { "type": "refactor", "section": "Code Refactoring", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ] + } + } +} diff --git a/src/monocoque/helper/confighelper.h b/src/monocoque/helper/confighelper.h index 5d664c3..472930b 100644 --- a/src/monocoque/helper/confighelper.h +++ b/src/monocoque/helper/confighelper.h @@ -245,6 +245,8 @@ DeviceSettings; int strtogame(const char* game, MonocoqueSettings* ms); +int strtodevsubsubtype(const char* device_subsubtype, DeviceSettings* ds); + int devsetup(const char* device_type, const char* device_subtype, const char* config_files, MonocoqueSettings* ms, DeviceSettings* ds, config_setting_t* device_settings); int settingsfree(DeviceSettings ds); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..82b8692 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +# Automated CI test suite — only built/run when ENABLE_TESTS is on (see +# root CMakeLists.txt). Manual, interactive, and hardware-dependent scratch +# tools live in tests/manual/ instead and are never built by this file. + +add_executable(confighelper_test confighelper_test.c) +# helper (confighelper.c specifically) references slog/xml2/simulatorapi +# symbols elsewhere in the same translation unit — static libs resolve per +# object file, so all of these are needed even though this test only +# exercises strtodevsubsubtype. +target_link_libraries(confighelper_test helper config slog xml2 simulatorapi m proc2) +add_test(NAME confighelper_test COMMAND confighelper_test) diff --git a/tests/confighelper_test.c b/tests/confighelper_test.c new file mode 100644 index 0000000..57e249f --- /dev/null +++ b/tests/confighelper_test.c @@ -0,0 +1,45 @@ +// Proof-of-plumbing test for the new CTest wiring — not an attempt at +// comprehensive coverage. Exercises strtodevsubsubtype (helper/confighelper.c), +// a pure string->enum mapping with no I/O/hardware, including the R8/R3 +// aliasing onto SIMDEVSUBTYPE_MOZAR5 and the fallback for unrecognized input. +#include +#include + +#include "../src/monocoque/helper/confighelper.h" + +static int failures = 0; + +static void check(const char* input, DeviceSubSubType expected) +{ + DeviceSettings ds = {0}; + strtodevsubsubtype(input, &ds); + if (ds.dev_subsubtype != expected) + { + fprintf(stderr, "FAIL: strtodevsubsubtype(\"%s\") = %d, expected %d\n", + input, ds.dev_subsubtype, expected); + failures++; + } +} + +int main(void) +{ + check("MozaR5", SIMDEVSUBTYPE_MOZAR5); + check("MozaR8", SIMDEVSUBTYPE_MOZAR5); // aliased onto the same protocol as R5 + check("MozaR3", SIMDEVSUBTYPE_MOZAR5); // same + check("MozaNew", SIMDEVSUBTYPE_MOZA_NEW); + check("MozaKSProWheel", SIMDEVSUBTYPE_MOZA_KS_PRO_WHEEL); + check("CammusC5", SIMDEVSUBTYPE_CAMMUSC5); + check("CammusC12", SIMDEVSUBTYPE_CAMMUSC12); + check("LogitechG29", SIMDEVSUBTYPE_LOGITECH_G29); + check("not-a-real-subtype", SIMDEVSUBTYPE_UNKNOWN); + check("", SIMDEVSUBTYPE_UNKNOWN); + + if (failures > 0) + { + fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + + printf("confighelper_test: all checks passed\n"); + return 0; +} diff --git a/tests/getmem.c b/tests/manual/getmem.c similarity index 100% rename from tests/getmem.c rename to tests/manual/getmem.c diff --git a/tests/hidtest.c b/tests/manual/hidtest.c similarity index 100% rename from tests/hidtest.c rename to tests/manual/hidtest.c diff --git a/tests/pa_devs.c b/tests/manual/pa_devs.c similarity index 100% rename from tests/pa_devs.c rename to tests/manual/pa_devs.c diff --git a/tests/patest_longsine.c b/tests/manual/patest_longsine.c similarity index 100% rename from tests/patest_longsine.c rename to tests/manual/patest_longsine.c diff --git a/tests/revburnerparsetest.c b/tests/manual/revburnerparsetest.c similarity index 100% rename from tests/revburnerparsetest.c rename to tests/manual/revburnerparsetest.c diff --git a/tests/runmemtest.sh b/tests/manual/runmemtest.sh similarity index 100% rename from tests/runmemtest.sh rename to tests/manual/runmemtest.sh diff --git a/tests/setmem.c b/tests/manual/setmem.c similarity index 100% rename from tests/setmem.c rename to tests/manual/setmem.c diff --git a/tests/setsimdata.c b/tests/manual/setsimdata.c similarity index 100% rename from tests/setsimdata.c rename to tests/manual/setsimdata.c diff --git a/tests/sharedmemoryproducer.c b/tests/manual/sharedmemoryproducer.c similarity index 100% rename from tests/sharedmemoryproducer.c rename to tests/manual/sharedmemoryproducer.c diff --git a/tests/sharedmemoryunlink.c b/tests/manual/sharedmemoryunlink.c similarity index 100% rename from tests/sharedmemoryunlink.c rename to tests/manual/sharedmemoryunlink.c diff --git a/tests/simlighttest.c b/tests/manual/simlighttest.c similarity index 100% rename from tests/simlighttest.c rename to tests/manual/simlighttest.c diff --git a/tests/testlibusb.c b/tests/manual/testlibusb.c similarity index 100% rename from tests/testlibusb.c rename to tests/manual/testlibusb.c diff --git a/tests/testrevburner.c b/tests/manual/testrevburner.c similarity index 100% rename from tests/testrevburner.c rename to tests/manual/testrevburner.c diff --git a/tests/usesharedmemory.c b/tests/manual/usesharedmemory.c similarity index 100% rename from tests/usesharedmemory.c rename to tests/manual/usesharedmemory.c diff --git a/tools/appimage-collect-licenses.sh b/tools/appimage-collect-licenses.sh new file mode 100755 index 0000000..69a63c0 --- /dev/null +++ b/tools/appimage-collect-licenses.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Collect licence and copyright information for everything an AppImage bundles. +# +# usage: tools/appimage-collect-licenses.sh +# +# A .deb declares only what it ships, because its dependencies remain separate +# packages carrying their own copyright files. An AppImage carries those +# libraries inside it, so their notices have to travel with it -- and most of +# them here are LGPL (GTK, glib, pango and friends), where the obligation also +# covers saying where the corresponding source can be had. +# +# The mapping is mechanical rather than researched: every bundled library came +# from a Debian/Ubuntu package on the build host, so dpkg can name it, and the +# archive already holds a copyright file for each. Run this after linuxdeploy +# has populated the AppDir and before it packages the image. +set -euo pipefail + +APPDIR="${1:?AppDir path}" +DOCDIR="$APPDIR/usr/share/doc" +MANIFEST="$DOCDIR/BUNDLED-LIBRARIES.txt" + +mkdir -p "$DOCDIR" + +{ + echo "Libraries bundled in this AppImage" + echo "==================================" + echo + echo "Each library below was taken from the Debian/Ubuntu package named" + echo "beside it on the build host. That package's copyright file is included" + echo "under usr/share/doc//copyright in this image." + echo + echo "The corresponding source for any of them can be obtained with:" + echo " apt-get source =" + echo "from the distribution and release recorded below." + echo + echo "Build host: $( (. /etc/os-release && echo "$PRETTY_NAME") 2>/dev/null || echo unknown)" + echo + printf '%-44s %-34s %s\n' "LIBRARY" "PACKAGE" "VERSION" +} > "$MANIFEST" + +found=0 +unowned=0 + +while IFS= read -r so; do + base="$(basename "$so")" + pkg="" + # Try the usual multiarch location first, then fall back to a name search. + for candidate in "/usr/lib/x86_64-linux-gnu/$base" "/lib/x86_64-linux-gnu/$base" "/usr/lib/$base"; do + pkg="$(dpkg -S "$candidate" 2>/dev/null | head -1 | cut -d: -f1 || true)" + [ -n "$pkg" ] && break + done + if [ -z "$pkg" ]; then + pkg="$(dpkg -S "$base" 2>/dev/null | head -1 | cut -d: -f1 || true)" + fi + + if [ -n "$pkg" ]; then + ver="$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || echo unknown)" + printf '%-44s %-34s %s\n' "$base" "$pkg" "$ver" >> "$MANIFEST" + if [ -f "/usr/share/doc/$pkg/copyright" ] && [ ! -f "$DOCDIR/$pkg/copyright" ]; then + install -Dm644 "/usr/share/doc/$pkg/copyright" "$DOCDIR/$pkg/copyright" + fi + found=$((found + 1)) + else + # Built by this job rather than installed from a package -- its licence is + # collected separately, from its own source tree. + printf '%-44s %-34s %s\n' "$base" "(built from source in this job)" "-" >> "$MANIFEST" + unowned=$((unowned + 1)) + fi +done < <(find "$APPDIR" -name '*.so*' -type f | sort) + +echo >> "$MANIFEST" +echo "$found libraries from distribution packages, $unowned built in this job." >> "$MANIFEST" + +echo "collected copyright for $found bundled libraries ($unowned built here)" +echo "manifest: $MANIFEST" diff --git a/tools/distro/debian/dpkg/debian-latest/control b/tools/distro/debian/dpkg/debian-latest/control index 0e41096..8cff81b 100644 --- a/tools/distro/debian/dpkg/debian-latest/control +++ b/tools/distro/debian/dpkg/debian-latest/control @@ -5,4 +5,4 @@ Priority: optional Architecture: amd64 Maintainer: Paul Jones Description: Racing Simulator Device Manager -Depends: libpulse0,libconfig11,libargtable2-0,liblua5.4-0,libuv1t64,libserialport0,libxml2,libxdg-basedir1,libhidapi-hidraw0,libgtk-3-0t64,libcurl4t64,libglu1-mesa +Depends: libpulse0,libconfig11,libargtable2-0,liblua5.4-0,libuv1t64,libserialport0,libxml2-16,libxdg-basedir1,libhidapi-hidraw0,libgtk-3-0t64,libcurl4t64,libglu1-mesa diff --git a/tools/distro/debian/dpkg/ubuntu-latest/control b/tools/distro/debian/dpkg/ubuntu-latest/control index c5eff6e..86678a9 100644 --- a/tools/distro/debian/dpkg/ubuntu-latest/control +++ b/tools/distro/debian/dpkg/ubuntu-latest/control @@ -5,4 +5,4 @@ Priority: optional Architecture: amd64 Maintainer: Paul Jones Description: Racing Simulator Device Manager -Depends: libpulse0,libconfig9,libargtable2-0,liblua5.4-0,libuv1,libserialport0,libxml2,libxdg-basedir1,libhidapi-hidraw0,libglu1-mesa,libcurl4,libgtk-3-0 +Depends: libpulse0,libconfig11,libargtable2-0,liblua5.4-0,libuv1t64,libserialport0,libxml2-16,libxdg-basedir1,libhidapi-hidraw0,libglu1-mesa,libcurl4t64,libgtk-3-0t64 diff --git a/tools/distro/fedora/rpm/fedora.spec b/tools/distro/fedora/rpm/fedora.spec index cea6ecc..a3cb526 100644 --- a/tools/distro/fedora/rpm/fedora.spec +++ b/tools/distro/fedora/rpm/fedora.spec @@ -15,14 +15,21 @@ Requires: pulseaudio-libs argtable libconfig hidapi libserialport libuv libxdg-b %description A device manager for Racing sims +# Builds whatever tree has been staged at %{_sourcedir}/monocoque, cloning it +# from upstream master only if nothing is staged. The unconditional clone this +# replaced meant an rpm's contents tracked master rather than the tag being +# built -- so a fix on the branch being released was absent from its own +# release. CI stages the checked-out tree; a bare `rpmbuild -ba` on a +# workstation still works exactly as before. %prep rm -rf $RPM_BUILD_DIR/monocoque -rm -rf $RPM_SOURCE_DIR/monocoque -cd $RPM_SOURCE_DIR -git clone https://github.com/spacefreak18/monocoque -cd monocoque -git submodule update --init --recursive -cd .. +if [ ! -d $RPM_SOURCE_DIR/monocoque ]; then + cd $RPM_SOURCE_DIR + git clone https://github.com/spacefreak18/monocoque + cd monocoque + git submodule update --init --recursive + cd .. +fi cp -r $RPM_SOURCE_DIR/monocoque $RPM_BUILD_DIR/ %build diff --git a/tools/generate-third-party-notices.py b/tools/generate-third-party-notices.py new file mode 100755 index 0000000..bff8f8c --- /dev/null +++ b/tools/generate-third-party-notices.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Generate a third-party notices file from the Flatpak manifest. + +A .deb lists only what it actually ships, because its dependencies are separate +packages carrying their own copyright files. A Flatpak or AppImage distributes +those libraries itself, so the notices have to travel with the artifact -- and +for the LGPL ones (libconfig, argtable2, libserialport, orcania, yder) that +includes saying where the corresponding source can be obtained. + +The manifest already pins every dependency by URL and sha256, which is exactly +that information, so this is generated from it rather than maintained by hand +where it would drift. +""" +import re +import sys +from pathlib import Path + +manifest = Path(sys.argv[1] if len(sys.argv) > 1 + else "flatpak/io.github.spacefreak18.monocoque.yml") +text = manifest.read_text() + +entries, name = [], None +for line in text.splitlines(): + m = re.match(r"^ - name: (\S+)", line) + if m: + name = m.group(1) + entries.append({"name": name, "url": None, "sha256": None, "commit": None, "tag": None}) + elif entries: + for key in ("url", "sha256", "commit", "tag"): + k = re.match(rf"^\s+{key}: (\S+)", line) + if k and entries[-1][key] is None: + entries[-1][key] = k.group(1) + +out = [ + "# Third-party notices", + "", + "This package bundles the libraries below. Each one's own licence text is", + "installed alongside it under `/app/share/licenses//`.", + "", + "Where a bundled library is under the LGPL, the corresponding source is the", + "exact archive listed here, identified by URL and SHA-256; the same version", + "can be rebuilt from it, and the Flatpak links these libraries dynamically.", + "", + "| Component | Source | Checksum / revision |", + "|---|---|---|", +] +for e in entries: + if not e["url"]: + continue + rev = e["sha256"] or e["commit"] or e["tag"] or "" + out.append(f"| {e['name']} | {e['url']} | `{rev[:16]}{'…' if len(rev) > 16 else ''}` |") + +out += [ + "", + "Generated by tools/generate-third-party-notices.py from the Flatpak", + "manifest -- do not edit by hand.", + "", +] +print("\n".join(out)) diff --git a/tools/monocoque.desktop b/tools/monocoque.desktop index 82e7e08..549fcd0 100644 --- a/tools/monocoque.desktop +++ b/tools/monocoque.desktop @@ -3,7 +3,7 @@ Version=1.0 Type=Application Name=Monocoque Comment=Launch Monocoque -Exec=/usr/bin/gmonocoque +Exec=gmonocoque Icon=monocoque Terminal=false Categories=Game;Development; diff --git a/tools/monocoque.svg b/tools/monocoque.svg new file mode 100644 index 0000000..a21aaa4 --- /dev/null +++ b/tools/monocoque.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..449d7e7 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.3.6