diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4a7d8f..8acd7bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,31 +3,85 @@ 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 \ + libyder-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 +92,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 +105,438 @@ 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 + # monocoque does nothing without simd -- with no daemon publishing + # /dev/shm/SIMAPI.DAT it falls back to scanning /proc for the game + # itself. Shipped in the same package rather than expecting users to + # build simapi separately. + cp ./build/simd ./PKG_SOURCE/usr/bin/simd + # simd still runs without ~/.config/simd/simd.config, but loses its + # auto-bridge: it logs "Disabling Automatic Bridge Mode" and never + # launches the Windows bridge under Proton, which is how a sim's + # telemetry reaches /dev/shm in the first place. Shipped as an example + # because a package must not write into $HOME. + cp ./src/monocoque/simulatorapi/simapi/simd/conf/simd.config \ + ./PKG_SOURCE/usr/share/monocoque/simd.config 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 + + # yder (and orcania beneath it) is what simd logs through, and neither is + # packaged for Fedora -- `dnf search yder orcania` finds nothing on 43 or + # 44, while Debian and Ubuntu both ship libyder2.0t64. Built here as + # *static* libraries on purpose: linked into simd they leave the rpm with + # no runtime dependency that Fedora cannot satisfy, so `Requires:` stays + # as it was and the package installs on a stock system. + - name: Build vendored orcania and yder (static) + run: | + set -eux + # BUILD_SHARED=OFF as well as BUILD_STATIC=ON: both projects default to + # shared-only, and if a libyder.so is left in the prefix the linker + # picks it over the archive, putting a runtime dependency back into the + # rpm that no Fedora repo can satisfy. + # + # CMAKE_C_FLAGS_RELEASE carries -Wno-error because both projects append + # `-Wall -Werror` to CMAKE_C_FLAGS themselves, and orcania 2.3.3 does + # not build clean on Fedora's GCC: orcania.c:372 assigns the result of + # strstr() on a const pointer to a char*, which is -Werror=discarded- + # qualifiers. Setting CMAKE_C_FLAGS would be overridden (they append to + # it); the _RELEASE flags land after it on the command line, where the + # last of -Werror/-Wno-error wins. Found by rehearsing this whole job + # in a fedora:44 container before spending a CI round on it. + cflags='-O2 -DNDEBUG -Wno-error' + + git clone --depth 1 --branch v2.3.3 https://github.com/babelouest/orcania /tmp/orcania + cmake -S /tmp/orcania -B /tmp/orcania/build \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS_RELEASE="$cflags" \ + -DBUILD_SHARED=OFF -DBUILD_STATIC=ON -DBUILD_ORCANIA_TESTING=OFF + cmake --build /tmp/orcania/build --target install + + git clone --depth 1 --branch v1.4.20 https://github.com/babelouest/yder /tmp/yder + # Journald support would drag in systemd-devel for a logging backend + # simd never selects; file and syslog are what its config offers. + cmake -S /tmp/yder -B /tmp/yder/build \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS_RELEASE="$cflags" \ + -DBUILD_SHARED=OFF -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF \ + -DBUILD_YDER_TESTING=OFF + cmake --build /tmp/yder/build --target install + + # Fail loudly here rather than shipping a broken rpm: nothing later in + # this job would notice a dynamically linked simd. + test -f /usr/lib64/libyder.a || test -f /usr/lib/libyder.a + + # 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 -- flagged as a + # known bug on the previous pass and now load-bearing: simd only exists in + # this tree's CMakeLists, so a package built from upstream master would + # quietly ship without it. Staged here and copied by %prep instead. + - 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 libyder-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..ae03d80 --- /dev/null +++ b/.github/workflows/pr-build.yaml @@ -0,0 +1,31 @@ +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. + # + # libyder-dev is for the simd target, which builds by default. Kept + # here rather than turning BUILD_SIMD off so a PR that breaks simd + # fails its checks instead of only the release build. + 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 libyder-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..d4f6d11 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,9 @@ /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/ 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..58db5c0 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,77 @@ 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() + +# simd -- the telemetry daemon everything else here reads from. simapi ships +# its own CMakeLists with a BUILD_SIMD option, but this tree never invokes it: +# src/monocoque/simulatorapi compiles simapi's sources straight into the static +# `simulatorapi` library instead. So the target is declared here over that same +# library, which also means the simd in a monocoque package is built from the +# exact simapi commit monocoque itself was built against, rather than whatever +# happens to be installed system-wide. +# +# Packaged alongside monocoque because monocoque does nothing without it: with +# no simd running, simapi_get_sim() finds no /dev/shm/SIMAPI.DAT and falls back +# to scanning /proc for the game itself. +option(BUILD_SIMD "Build the simd telemetry daemon alongside monocoque" ON) +if(BUILD_SIMD) + set(SIMAPI_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/monocoque/simulatorapi/simapi) + add_executable(simd + ${SIMAPI_SRC_DIR}/simd/simd.c + ${SIMAPI_SRC_DIR}/simd/parameters.c + ${SIMAPI_SRC_DIR}/simd/confighelper.c + ${SIMAPI_SRC_DIR}/simd/dirhelper.c + ${SIMAPI_SRC_DIR}/simd/poke.c + # Not part of the simulatorapi library above, which only pulls in + # mapacdata.c from simmap/. + ${SIMAPI_SRC_DIR}/simmap/mapsimdata.c) + # simd's own build prefers argtable3 and falls back to argtable2. monocoque + # links argtable2 everywhere, so pin the same one rather than let two halves + # of one package disagree about which argtable they need at runtime. + # simd.c carries its own copy of is_pid_running(), and so does simapi's + # getpid.c. Upstream links simd against libsimapi.so, where the executable's + # definition simply wins; here simapi is a static archive and the linker + # refuses the duplicate outright. Renaming simd's copy for its own + # translation units keeps both definitions and leaves each caller bound to + # the one it was written against -- no patch to the pinned submodule, and no + # --allow-multiple-definition silently picking a winner. + target_compile_definitions(simd PRIVATE HAVE_ARGTABLE2 is_pid_running=simd_is_pid_running) + target_include_directories(simd PRIVATE + ${SIMAPI_SRC_DIR}/simapi + ${SIMAPI_SRC_DIR}/include + ${SIMAPI_SRC_DIR}/simmap + ${SIMAPI_SRC_DIR}/simd) + target_link_libraries(simd m dl uv yder argtable2 config simulatorapi proc2) + # orcania sits under yder. A shared libyder.so records that dependency and + # pulls it in on its own, which is why Debian and Ubuntu need nothing here; + # a static libyder.a records nothing, so every o_malloc/split_string it + # calls comes back undefined. Fedora has no yder package at all and its rpm + # job vendors both as static archives, so the link has to name orcania + # explicitly -- after yder, which is the order a static link needs. + find_library(ORCANIA_LIBRARY orcania) + if(ORCANIA_LIBRARY) + target_link_libraries(simd ${ORCANIA_LIBRARY}) + endif() +endif() + +install(TARGETS monocoque-cli RUNTIME DESTINATION bin) +if(BUILD_GUI) + install(TARGETS monocoque-gui RUNTIME DESTINATION bin) +endif() +if(BUILD_SIMD) + install(TARGETS simd RUNTIME DESTINATION bin) + # simd runs without ~/.config/simd/simd.config, but degraded: it logs + # "Error with config file" then "Disabling Automatic Bridge Mode" and + # carries on mapping telemetry (observed by running it with an empty home + # bind-mounted over the real one). What it loses is the auto-bridge -- the + # per-game launchexe/liveexe table that starts the Windows bridge under + # Proton, which is how a sim's telemetry reaches /dev/shm at all. simapi's + # own build installs this file straight into $HOME, which a package must + # not do, so it ships as an example to copy. Note simd resolves that path + # from getpwuid(), not $HOME. + install(FILES ${SIMAPI_SRC_DIR}/simd/conf/simd.config DESTINATION share/monocoque) 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..34a13e8 --- /dev/null +++ b/flatpak/io.github.spacefreak18.monocoque.yml @@ -0,0 +1,419 @@ +# 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. +# BUNDLED HERE: simapi (libsimapi.so) and simd, built from simapi's own +# CMakeLists at the same commit monocoque's submodule pins, plus the yder and +# orcania modules simd logs through -- neither is in the freedesktop runtime. +# +# A sandboxed simd only works because it steps outside the sandbox to do its +# looking. simapi identifies a running sim by scanning /proc for 19 process +# names (get_process_match() in simmapper.c) and reads /proc//environ for +# the Steam compat vars behind its auto-bridge, while Flatpak gives every +# sandbox its own PID namespace and no way to share the host's -- measured +# here: 5 pids visible inside against 497 outside, unchanged by --allow=devel +# or --filesystem=host, and simd has no flag to force a sim either (see +# simd/parameters.c). So simapi-flatpak-host-spawn.patch, applied to the simapi +# module below, routes the sim lookup, the environ read, both liveness checks, +# notify-send and the bridge exec through `flatpak-spawn --host`, guarded on +# /.flatpak-info so native builds keep the original code paths. That is what +# --talk-name=org.freedesktop.Flatpak in finish-args is for. +# +# monocoque itself is unaffected either way: simapi_get_sim() checks +# /dev/shm/SIMAPI.DAT before it ever scans /proc, so it reads the telemetry a +# *host* simd publishes perfectly well. +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 + # The bundled simd reads ~/.config/simd/simd.config, and builds that path + # from getpwuid() rather than $HOME or XDG_CONFIG_HOME (simd.c), so it wants + # the host's real directory for the same reason monocoque does above. + # Without it simd runs on but logs "Disabling Automatic Bridge Mode". + - --filesystem=xdg-config/simd:create + # What the bundled simd's escape hatch depends on: without it every + # `flatpak-spawn --host` in the patched simapi is refused and simd is back to + # seeing 5 pids and no sim. It also makes this sandbox largely decorative -- + # an app that can run arbitrary host commands can do anything the user can. + # Packaged this way because Flatpak is a convenient delivery mechanism on + # immutable distros, not because it isolates anything here. + - --talk-name=org.freedesktop.Flatpak + # Host /dev/shm, where simd publishes telemetry as POSIX shared memory + # (/dev/shm/SIMAPI.DAT, mapped in simapi's simmapper.c). 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 + + # orcania and yder are what simd logs through. Neither is in the freedesktop + # runtime and neither is packaged for Fedora either (the rpm legs vendor both + # as static libraries); here they are ordinary /app/lib shared libraries that + # ship inside the bundle. + # + # CMAKE_C_FLAGS_RELEASE carries -Wno-error because both projects append their + # own `-Wall -Werror` to CMAKE_C_FLAGS -- setting that variable would be + # overridden, while the _RELEASE flags land after it where the last of + # -Werror/-Wno-error wins. orcania 2.3.3 assigns strstr() on a const pointer + # to a char*, which newer compilers reject outright. + - name: orcania + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DCMAKE_C_FLAGS_RELEASE=-O2 -DNDEBUG -Wno-error + - -DBUILD_ORCANIA_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/orcania/$(basename "$f"); else echo "WARNING: no licence file found for orcania"; fi' + sources: + - type: git + url: https://github.com/babelouest/orcania + commit: ffc8b55d09a3488f4f6be38034b33bc64bf8b0ce + + - name: yder + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DCMAKE_C_FLAGS_RELEASE=-O2 -DNDEBUG -Wno-error + # Journald would need systemd-devel for a backend simd never selects; + # its config offers file and syslog. + - -DWITH_JOURNALD=OFF + - -DBUILD_YDER_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/yder/$(basename "$f"); else echo "WARNING: no licence file found for yder"; fi' + sources: + - type: git + url: https://github.com/babelouest/yder + commit: dffe82c0483bb95d0d518ba1e36c568e63a24628 + + # simapi built through its *own* CMakeLists, unlike the monocoque module + # below which compiles the same sources into a static library of its own. + # That is what produces libsimapi.so and, with BUILD_SIMD, the simd binary -- + # pinned to the exact commit monocoque's submodule tracks, so the daemon and + # the app in this bundle agree on the SimData layout they map. + - name: simapi + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DBUILD_SIMD=ON + # Both default ON and write into $ENV{HOME} -- a systemd user unit and a + # default config -- which during a flatpak-builder run would land in the + # build user's home rather than the prefix. + - -DINSTALL_SYSTEMD_SERVICE=off + - -DINSTALL_DEFAULT_CONFIG=off + build-options: + env: + LIBRARY_PATH: /app/lib + post-install: + # The config simd looks for in ~/.config/simd. Shipped as an example, in + # the same place the deb/rpm/AppImage put it, since nothing may write to + # a user's home at install time. + - install -Dm644 simd/conf/simd.config /app/share/monocoque/simd.config + sources: + - type: git + url: https://github.com/spacefreak18/simapi + commit: 50fd181681862256780b2a0f74ef4bc459ed631a + # Teaches simapi's process layer to look at the *host* when sandboxed: + # the sim lookup, the /proc//environ read behind the auto-bridge, + # both liveness checks, notify-send, and the bridge exec itself all route + # through `flatpak-spawn --host`. Carried as a patch rather than a fork so + # the pinned upstream commit stays the source of truth; if it ever lands + # upstream this source simply goes away. + - type: patch + path: simapi-flatpak-host-spawn.patch + + - name: monocoque + buildsystem: cmake-ninja + config-opts: + - -DBUILD_GUI=on + - -DCMAKE_BUILD_TYPE=Release + # Off here only because the simapi module above already installs simd + # from simapi's own build -- this tree's target exists for the distro + # packages, where simapi is not built as a separate library. Two copies + # of the same binary in one prefix would just be confusing. + - -DBUILD_SIMD=off + # 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/flatpak/monocoque.flatpak b/flatpak/monocoque.flatpak new file mode 100644 index 0000000..de0ca6a Binary files /dev/null and b/flatpak/monocoque.flatpak differ diff --git a/flatpak/simapi-flatpak-host-spawn.patch b/flatpak/simapi-flatpak-host-spawn.patch new file mode 100644 index 0000000..516c436 --- /dev/null +++ b/flatpak/simapi-flatpak-host-spawn.patch @@ -0,0 +1,473 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index d3e0678..a4975b7 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -20,9 +20,13 @@ 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 -- the freedesktop Flatpak SDK among them -- ship ++ # a libproc2.pc whose Version field is the literal string UNKNOWN. Compared ++ # against that, VERSION_GREATER_EQUAL is always false, so this fell through ++ # to the pre-4.0.5 four-argument PIDS_VAL API and failed to compile against ++ # a current libproc2 whose header has long since moved to three. Take the ++ # old-API branch only when the version really parses and really is old. ++ if (LIBPROC2_VERSION MATCHES "^[0-9]" AND LIBPROC2_VERSION VERSION_LESS "4.0.5") + add_compile_definitions(USE_OLD_PID_VAL) + endif() + else() +diff --git a/simapi/getpid.c b/simapi/getpid.c +index c88ac84..5b183cb 100644 +--- a/simapi/getpid.c ++++ b/simapi/getpid.c +@@ -1,4 +1,5 @@ + #include ++#include + #include + #include + #include +@@ -38,6 +39,11 @@ static int isMatch(const char* possibleMatch, const char* checkAgainst) + + int is_pid_running(pid_t pid) + { ++ if (simapi_in_flatpak()) ++ { ++ return simapi_host_pid_alive(pid); ++ } ++ + if (pid <= 0) + { + return 0; +@@ -114,16 +120,31 @@ char* getEnvValueForPid(pid_t pid, const char* envName) + + sprintf( &path[6], "%d/environ", pid ); + +- envFile = fopen(path, "r"); +- if ( envFile == NULL ) ++ /* The pid came from the host's process table, so its /proc entry exists ++ * only out there -- see simapi_host_capture(). */ ++ if ( simapi_in_flatpak() ) + { +-// errno = ESRCH; +- return NULL; ++ char hostcmd[96]; ++ snprintf(hostcmd, sizeof(hostcmd), "cat /proc/%d/environ", (int) pid); ++ buf = simapi_host_capture(hostcmd, &maxIdx); ++ if ( buf == NULL ) ++ { ++ return NULL; ++ } + } ++ else ++ { ++ envFile = fopen(path, "r"); ++ if ( envFile == NULL ) ++ { ++// errno = ESRCH; ++ return NULL; ++ } + +- maxIdx = readData(&buf, envFile); ++ maxIdx = readData(&buf, envFile); + +- fclose(envFile); ++ fclose(envFile); ++ } + + envNameLen = strlen(envName); + cur = buf; +@@ -171,9 +192,249 @@ int check_if_number (char* str) + } + + ++ ++/* ------------------------------------------------------------------------ ++ * Looking at the host from inside a Flatpak sandbox. ++ * ++ * A sandbox gets its own PID namespace -- measured on a real install, 5 pids ++ * visible inside against 497 on the host -- so the /proc walk below finds only ++ * the sandbox itself and no sim is ever detected. Flatpak has no option to ++ * share the host's namespace (--allow=devel and --filesystem=host both change ++ * nothing), leaving `flatpak-spawn --host`, which asks the session helper to ++ * run a command outside. That needs --talk-name=org.freedesktop.Flatpak in the ++ * manifest. ++ * ++ * The output comes back through a FILE, not a pipe. A popen()'d ++ * `flatpak-spawn --host ...` hands the pipe's write end to the session helper, ++ * and a copy of it outlives every process the sandbox can see: reading to EOF ++ * never returns. Measured -- simd sat in anon_pipe_read with no children left ++ * and no further log line, forever. So the host writes to a file under ++ * $XDG_RUNTIME_DIR/app/$FLATPAK_ID, the one directory mounted at the same path ++ * on both sides, and renames it into place; the sandbox then reads an ordinary ++ * file where EOF means what it says. The rename is what makes "the file ++ * exists" mean "the output is complete". ++ * ++ * None of this is reached outside a sandbox: every caller keeps its original ++ * code path, so native builds are byte-for-byte unaffected. ++ * --------------------------------------------------------------------- */ ++int simapi_in_flatpak(void) ++{ ++ static int cached = -1; ++ if (cached < 0) ++ { ++ cached = (access("/.flatpak-info", F_OK) == 0) ? 1 : 0; ++ } ++ return cached; ++} ++ ++/* Visible under the same path inside the sandbox and out. */ ++static const char* host_share_dir(void) ++{ ++ static char dir[256]; ++ if (dir[0] == '\0') ++ { ++ const char* runtime = getenv("XDG_RUNTIME_DIR"); ++ const char* app_id = getenv("FLATPAK_ID"); ++ if (runtime != NULL && app_id != NULL) ++ { ++ snprintf(dir, sizeof(dir), "%s/app/%s", runtime, app_id); ++ } ++ else ++ { ++ snprintf(dir, sizeof(dir), "/tmp"); ++ } ++ } ++ return dir; ++} ++ ++/* Runs `hostcmd` on the host and returns its stdout, or NULL. Caller frees. */ ++char* simapi_host_capture(const char* hostcmd, size_t* out_len) ++{ ++ static unsigned long seq = 0; ++ char out[320]; ++ char cmd[1024]; ++ ++ if (out_len != NULL) ++ { ++ *out_len = 0; ++ } ++ ++ snprintf(out, sizeof(out), "%s/simapi-host-%d-%lu", host_share_dir(), (int) getpid(), seq++); ++ ++ /* Written to .part and renamed, so the file only appears complete. ++ * The exit status is deliberately ignored: simd runs a libuv loop whose ++ * SIGCHLD handling reaps children this call did not spawn, which makes ++ * system()'s own waitpid fail and lose the status. */ ++ snprintf(cmd, sizeof(cmd), ++ /* hostcmd is grouped: it may itself be a sequence, and without ++ * the braces the redirect would bind to its last command only -- ++ * which silently dropped half the process table. */ ++ "flatpak-spawn --host sh -c '{ %s ; } > %s.part 2>/dev/null; mv %s.part %s' >/dev/null 2>&1", ++ hostcmd, out, out, out); ++ (void) system(cmd); ++ ++ /* system() normally returns only once the command is done; the wait can ++ * still be lost to that same reaping, so give the rename a moment. */ ++ FILE* f = NULL; ++ for (int waited = 0; waited < 2000; waited += 20) ++ { ++ f = fopen(out, "r"); ++ if (f != NULL) ++ { ++ break; ++ } ++ usleep(20 * 1000); ++ } ++ ++ if (f == NULL) ++ { ++ return NULL; ++ } ++ ++ size_t cap = 8192; ++ size_t len = 0; ++ char* buf = malloc(cap); ++ ++ if (buf != NULL) ++ { ++ for (;;) ++ { ++ size_t n = fread(buf + len, 1, cap - len - 1, f); ++ len += n; ++ if (len + 1 < cap) ++ { ++ break; /* short read on a real file means EOF */ ++ } ++ cap *= 2; ++ char* grown = realloc(buf, cap); ++ if (grown == NULL) ++ { ++ break; ++ } ++ buf = grown; ++ } ++ buf[len] = '\0'; ++ } ++ ++ fclose(f); ++ unlink(out); ++ ++ if (out_len != NULL) ++ { ++ *out_len = len; ++ } ++ return buf; ++} ++ ++/* Is a pid in the *host's* namespace alive? kill(2) would resolve the number ++ * against the sandbox's own processes -- usually nothing, occasionally the ++ * wrong one. */ ++int simapi_host_pid_alive(pid_t pid) ++{ ++ char hostcmd[96]; ++ snprintf(hostcmd, sizeof(hostcmd), "kill -0 %d 2>/dev/null && echo alive", (int) pid); ++ ++ char* out = simapi_host_capture(hostcmd, NULL); ++ int alive = (out != NULL && strncmp(out, "alive", 5) == 0); ++ free(out); ++ return alive; ++} ++ ++/* Does a path exist on the *host*? For paths that are the host's by nature -- ++ * a Proton binary under STEAM_COMPAT_TOOL_PATHS, say -- since the sandbox's ++ * own filesystem knows nothing about them. Checking locally is how the bridge ++ * launch failed silently: Proton was never found, so the fork never happened ++ * and nothing was logged above debug level. */ ++int simapi_host_file_exists(const char* path) ++{ ++ char hostcmd[1024]; ++ snprintf(hostcmd, sizeof(hostcmd), "test -e '%s' && echo yes", path); ++ ++ char* out = simapi_host_capture(hostcmd, NULL); ++ int exists = (out != NULL && strncmp(out, "yes", 3) == 0); ++ free(out); ++ return exists; ++} ++ ++static struct SimProcessInfo pidof_host(char* pname[], int num) ++{ ++ struct SimProcessInfo p; ++ p.pid = -1; ++ p.pos = -1; ++ ++ /* Two ps runs, each line tagged, because a single `ps -o pid=,comm=,args=` ++ * cannot be parsed: comm may contain spaces. Wine names Assetto Corsa's ++ * process "AC: main thread", which a naive scanf read as comm="AC:" and ++ * argv[0]="main" -- nothing matched "acs.exe", simapi never set simstatus, ++ * and simd mapped no telemetry at all while the game ran. Splitting the ++ * two fields into their own lines keeps each one unambiguous: "A ++ * " gives argv[0] as the first token after the pid, and ++ * "C " gives comm as the whole rest of the line, which is ++ * the same pair libproc2 hands the native path below. */ ++ char* table = simapi_host_capture( ++ "ps -A -o pid=,args= | sed \"s/^/A /\"; ps -A -o pid=,comm= | sed \"s/^/C /\"", ++ NULL); ++ if (table == NULL) ++ { ++ return p; ++ } ++ ++ char* saveptr = NULL; ++ for (char* line = strtok_r(table, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r(NULL, "\n", &saveptr)) ++ { ++ char kind = line[0]; ++ if (kind != 'A' && kind != 'C') ++ { ++ continue; ++ } ++ ++ int pid = 0; ++ int consumed = 0; ++ if (sscanf(line + 1, " %d %n", &pid, &consumed) < 1 || consumed == 0) ++ { ++ continue; ++ } ++ ++ char* field = line + 1 + consumed; ++ if (kind == 'A') ++ { ++ /* argv[0] only. The whole of `ps -o args=` would be looser than ++ * libproc2's PIDS_CMDLINE_V first element: a shell whose arguments ++ * merely mention a sim's exe -- a Steam launch command, say -- ++ * would match, and simd would track that shell as the game. */ ++ char* end = strchr(field, ' '); ++ if (end != NULL) ++ { ++ *end = '\0'; ++ } ++ } ++ ++ for (int i = 0; pname[i] != NULL && i < num; i++) ++ { ++ if (strcasestr(field, pname[i]) != NULL) ++ { ++ p.pid = pid; ++ p.pos = i; ++ free(table); ++ return p; ++ } ++ } ++ } ++ ++ free(table); ++ return p; ++} ++ + struct SimProcessInfo pidof (char* pname[], int num) + { + ++ if (simapi_in_flatpak()) ++ { ++ return pidof_host(pname, num); ++ } ++ + struct SimProcessInfo p; + p.pid = -1; + p.pos = -1; +diff --git a/simapi/simapi.h b/simapi/simapi.h +index 3d53b12..4667bbc 100644 +--- a/simapi/simapi.h ++++ b/simapi/simapi.h +@@ -113,6 +113,10 @@ struct SimProcessInfo + + void simapi_set_faux_siminfo(SimInfo* si); + ++int simapi_in_flatpak(void); ++char* simapi_host_capture(const char* hostcmd, size_t* out_len); ++int simapi_host_pid_alive(pid_t pid); ++int simapi_host_file_exists(const char* path); + int is_pid_running(pid_t pid); + struct SimProcessInfo get_process_match(char* pidstrings[], int num); + char* getEnvValueForPid(pid_t pid, const char* envName); +diff --git a/simd/simd.c b/simd/simd.c +index f422f76..320ad7a 100644 +--- a/simd/simd.c ++++ b/simd/simd.c +@@ -350,6 +350,21 @@ int startudp(int port) + return err; + } + ++/* Paths handed to the bridge launch are the host's whenever this is a Flatpak ++ * build; everywhere else this is the ordinary local check. */ ++static bool bridge_file_exists(const char* path) ++{ ++ if (path == NULL) ++ { ++ return false; ++ } ++ if (simapi_in_flatpak()) ++ { ++ return simapi_host_file_exists(path) ? true : false; ++ } ++ return does_file_exist(path); ++} ++ + int is_pid_running(pid_t pid) + { + if (pid <= 0) +@@ -357,6 +372,14 @@ int is_pid_running(pid_t pid) + return 0; + } + ++ /* Sandboxed, the pids simd tracks belong to the host's namespace, where ++ * kill(2) cannot reach them. Shared with simapi so both halves agree on ++ * how the question is asked. */ ++ if (simapi_in_flatpak()) ++ { ++ return simapi_host_pid_alive(pid); ++ } ++ + // send signal 0 (no actual signal) + if (kill(pid, 0) == 0) + { +@@ -395,7 +418,8 @@ void bridgeclosecallback(uv_timer_t* handle) + if(simds.notify == true) + { + char cmd[512]; +- snprintf(cmd, sizeof(cmd), "notify-send -t 3000 \"%s\" \"game stopped\"", "simd"); ++ snprintf(cmd, sizeof(cmd), "%snotify-send -t 3000 \"%s\" \"game stopped\"", ++ simapi_in_flatpak() ? "flatpak-spawn --host " : "", "simd"); + system(cmd); + } + +@@ -519,7 +543,10 @@ void gamefindcallback(uv_timer_t* handle) + { + char* pathcheck1 = NULL; + asprintf(&pathcheck1, "%s/dist/bin/wine", token); +- if(does_file_exist(pathcheck1) == true) ++ /* Proton lives on the host, so ask the host. Locally this ++ * always answered false under Flatpak and the bridge was ++ * never launched. */ ++ if(bridge_file_exists(pathcheck1) == true) + { + wineexe = strdup(pathcheck1); + } +@@ -531,7 +558,7 @@ void gamefindcallback(uv_timer_t* handle) + if(wineexe == NULL) + { + asprintf(&pathcheck1, "%s/files/bin/wine", token); +- if(does_file_exist(pathcheck1) == true) ++ if(bridge_file_exists(pathcheck1) == true) + { + wineexe = strdup(pathcheck1); + } +@@ -598,13 +625,41 @@ void gamefindcallback(uv_timer_t* handle) + close(devnull); + } + +- if(env_simd_wrap_exe == NULL) ++ char* target = (env_simd_wrap_exe == NULL) ? wineexe : env_simd_wrap_exe; ++ ++ if(simapi_in_flatpak()) + { +- ret = execve(wineexe, newargv, newenviron); ++ /* Proton, its prefix and the bridge exe are all on the ++ * host; execve here would look for them inside the ++ * runtime. --env carries the environment across, since ++ * flatpak-spawn does not pass this one through, and ++ * --watch-bus ties the host process's lifetime to this ++ * one so the existing SIGTERM teardown still ends it. */ ++ char* hostargv[16]; ++ char envopts[4][512]; ++ int n = 0; ++ int e = 0; ++ ++ hostargv[n++] = "flatpak-spawn"; ++ hostargv[n++] = "--host"; ++ hostargv[n++] = "--watch-bus"; ++ for(int i = 0; newenviron[i] != NULL && e < 4; i++) ++ { ++ snprintf(envopts[e], sizeof(envopts[e]), "--env=%s", newenviron[i]); ++ hostargv[n++] = envopts[e]; ++ e++; ++ } ++ for(int i = 0; newargv[i] != NULL && n < 15; i++) ++ { ++ hostargv[n++] = newargv[i]; ++ } ++ hostargv[n] = NULL; ++ ++ ret = execvp("flatpak-spawn", hostargv); + } + else + { +- ret = execve(env_simd_wrap_exe, newargv, newenviron); ++ ret = execve(target, newargv, newenviron); + } + _exit(127); + } +@@ -642,7 +697,8 @@ void gamefindcallback(uv_timer_t* handle) + { + char cmd[512]; + const char* gamename = simapi_gametofullstr(sim); +- snprintf(cmd, sizeof(cmd), "notify-send -t 3000 \"%s\" \"Detected %s (%i)\"", "simd", gamename, sim); ++ snprintf(cmd, sizeof(cmd), "%snotify-send -t 3000 \"%s\" \"Detected %s (%i)\"", ++ simapi_in_flatpak() ? "flatpak-spawn --host " : "", "simd", gamename, sim); + system(cmd); + } + } 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/devices/sounddevice.c b/src/monocoque/devices/sounddevice.c index 9210178..5deec6e 100644 --- a/src/monocoque/devices/sounddevice.c +++ b/src/monocoque/devices/sounddevice.c @@ -211,8 +211,16 @@ int sounddev_init(SoundDevice* sounddevice, const char* devname, SoundDeviceSett } - usb_generic_shaker_init(sounddevice, mainloop, context, devname, sds.volume, sds.pan, sds.channels, streamname); - //usb_generic_shaker_init(sounddevice); + // Returned, not discarded: this function is declared int and used to fall + // off its end, so new_sound_device() read whatever happened to be in the + // return register and treated most devices as failures -- "Could not + // initialize Sound Device" for 22 of 24 configured shakers, while their + // PulseAudio streams had in fact connected. The devices were freed and + // never fed telemetry, so the graph looked correct in qpwgraph and nothing + // shook. Being undefined behaviour it varied by build, which is why the + // same config worked against a locally compiled monocoque and not the + // packaged one. + return usb_generic_shaker_init(sounddevice, mainloop, context, devname, sds.volume, sds.pan, sds.channels, streamname); } static const vtable engine_sound_simdevice_vtable = { &sounddev_engine_update, &sounddev_free }; 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/src/monocoque/mgui/uiconfighelper.c b/src/monocoque/mgui/uiconfighelper.c index b9481b8..4b484ab 100644 --- a/src/monocoque/mgui/uiconfighelper.c +++ b/src/monocoque/mgui/uiconfighelper.c @@ -21,6 +21,11 @@ typedef struct pa_devicelist { char description[256]; } pa_devicelist_t; +static int matches_any(const char *value) +{ + return strcmp(value, "default") == 0 || strcmp(value, "all") == 0; +} + int find_default_config(config_setting_t *configs) { int count = config_setting_length(configs); @@ -29,20 +34,20 @@ int find_default_config(config_setting_t *configs) { config_setting_t *entry = config_setting_get_elem(configs, i); - const char *sim; - const char *api; - const char *car; + // A key that isn't in the entry doesn't constrain the match. This used + // to require all three, which no real config has: conf/monocoque.config + // -- the example the README points at -- sets sim and car and never + // mentions api, so every entry was skipped, -1 came back, and the + // caller dereferenced the NULL that produced. + const char *sim = "default"; + const char *api = "default"; + const char *car = "default"; - if (!config_setting_lookup_string(entry, "sim", &sim)) - continue; - if (!config_setting_lookup_string(entry, "api", &api)) - continue; - if (!config_setting_lookup_string(entry, "car", &car)) - continue; + config_setting_lookup_string(entry, "sim", &sim); + config_setting_lookup_string(entry, "api", &api); + config_setting_lookup_string(entry, "car", &car); - if ((strcmp(sim, "default") == 0 || strcmp(sim, "all") == 0) && - (strcmp(api, "default") == 0 || strcmp(api, "all") == 0) && - (strcmp(car, "default") == 0 || strcmp(car, "all") == 0)) + if (matches_any(sim) && matches_any(api) && matches_any(car)) { return i; } @@ -137,11 +142,31 @@ void populate_device_list(ListBox *listbox, config_t* cfg) config = config_lookup(cfg, "configs"); int config_num = find_default_config(config); + if (config_num < 0) + { + // No entry claims to be the default one: list the first, which is what + // a single-entry config means anyway, rather than nothing. + config_num = 0; + } config_setting_t* selectedconfig = config_setting_get_elem(config, config_num); + if (selectedconfig == NULL) + { + fprintf(stderr, "No config entry to list devices for\n"); + return; + } + config_setting_t* config_devices = NULL; config_devices = config_setting_lookup(selectedconfig, "devices"); - + if (config_devices == NULL) + { + // config_setting_lookup dereferences its argument, so reaching here + // with a NULL selectedconfig used to segfault inside libconfig -- + // gmonocoque died on launch, before drawing a window, for anyone whose + // config didn't produce a match above. + fprintf(stderr, "Config entry has no devices section\n"); + return; + } count = config_setting_length(config_devices); diff --git a/src/monocoque/monocoque-cli.c b/src/monocoque/monocoque-cli.c index aadb1f4..797e9bf 100644 --- a/src/monocoque/monocoque-cli.c +++ b/src/monocoque/monocoque-cli.c @@ -88,8 +88,16 @@ int main(int argc, char** argv) return 0; } Parameters* p = NULL; - p = malloc(sizeof(Parameters)); - MonocoqueSettings* ms = malloc(sizeof(MonocoqueSettings));; + // calloc, not malloc: a --help or --version run jumps straight to + // cleanup_final before a single field is set, and the cleanup path frees + // every pointer in both structs. Uninitialised heap made that a free() of + // whatever junk was there -- confirmed on Debian forky, where the + // released .deb aborted with `free(): invalid pointer` in + // monocoquesettingsfree() on `monocoque --help`. Older glibc happened not + // to notice, which is the only reason this looked fine on stable and + // Fedora. + p = calloc(1, sizeof(Parameters)); + MonocoqueSettings* ms = calloc(1, sizeof(MonocoqueSettings)); ConfigError ppe = getParameters(argc, argv, p); if (ppe == E_SUCCESS_AND_EXIT || ppe == E_SOMETHING_BAD) diff --git a/src/monocoque/monocoque-gui.c b/src/monocoque/monocoque-gui.c index a2d5769..29949a8 100644 --- a/src/monocoque/monocoque-gui.c +++ b/src/monocoque/monocoque-gui.c @@ -91,7 +91,10 @@ int monocoque_initialize(int argc, char** argv) } p = NULL; - p = malloc(sizeof(Parameters)); + // calloc, not malloc: freeparams() walks every pointer in this struct + // on the early-exit paths, before getParameters has set them. See the + // same change in monocoque-cli.c. + p = calloc(1, sizeof(Parameters)); p->config_dirpath = NULL; p->config_filepath = NULL; p->log_filename_str = NULL; diff --git a/src/monocoque/monocoque.c b/src/monocoque/monocoque.c index b75447c..e27bfef 100644 --- a/src/monocoque/monocoque.c +++ b/src/monocoque/monocoque.c @@ -84,8 +84,16 @@ int main(int argc, char** argv) return 0; } Parameters* p = NULL; - p = malloc(sizeof(Parameters)); - MonocoqueSettings* ms = malloc(sizeof(MonocoqueSettings));; + // calloc, not malloc: a --help or --version run jumps straight to + // cleanup_final before a single field is set, and the cleanup path frees + // every pointer in both structs. Uninitialised heap made that a free() of + // whatever junk was there -- confirmed on Debian forky, where the + // released .deb aborted with `free(): invalid pointer` in + // monocoquesettingsfree() on `monocoque --help`. Older glibc happened not + // to notice, which is the only reason this looked fine on stable and + // Fedora. + p = calloc(1, sizeof(Parameters)); + MonocoqueSettings* ms = calloc(1, sizeof(MonocoqueSettings)); ConfigError ppe = getParameters(argc, argv, p); if (ppe == E_SUCCESS_AND_EXIT || ppe == E_SOMETHING_BAD) 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/a.out b/tests/a.out deleted file mode 100755 index 209cdd7..0000000 Binary files a/tests/a.out and /dev/null differ 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/tests/producer b/tests/producer deleted file mode 100755 index 60b37bc..0000000 Binary files a/tests/producer and /dev/null differ diff --git a/tests/test.bin b/tests/test.bin deleted file mode 100644 index 75a5854..0000000 Binary files a/tests/test.bin and /dev/null differ 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..a92e83b 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: libyder2.0t64,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/debian-stable/control b/tools/distro/debian/dpkg/debian-stable/control index 195384d..b20b9d2 100644 --- a/tools/distro/debian/dpkg/debian-stable/control +++ b/tools/distro/debian/dpkg/debian-stable/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,libglu1-mesa,libcurl4t64 +Depends: libyder2.0t64,libpulse0,libconfig11,libargtable2-0,liblua5.4-0,libuv1t64,libserialport0,libxml2,libxdg-basedir1,libhidapi-hidraw0,libgtk-3-0t64,libglu1-mesa,libcurl4t64 diff --git a/tools/distro/debian/dpkg/ubuntu-latest/control b/tools/distro/debian/dpkg/ubuntu-latest/control index c5eff6e..2418e1c 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: libyder2.0t64,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..2675f1b 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 @@ -33,9 +40,20 @@ make %install mkdir -p $RPM_BUILD_ROOT/usr/bin +mkdir -p $RPM_BUILD_ROOT/usr/share/monocoque cp $RPM_BUILD_DIR/monocoque/build/monocoque $RPM_BUILD_ROOT/usr/bin/monocoque cp $RPM_BUILD_DIR/monocoque/build/gmonocoque $RPM_BUILD_ROOT/usr/bin/gmonocoque +# simd is statically linked against the vendored yder/orcania (see the CI job), +# so it adds no Requires: Fedora packages neither library. +cp $RPM_BUILD_DIR/monocoque/build/simd $RPM_BUILD_ROOT/usr/bin/simd +# Without ~/.config/simd/simd.config simd still maps telemetry but disables its +# Automatic Bridge Mode, so nothing launches the Windows bridge under Proton. +# Shipped as an example because a package must not write into $HOME. +cp $RPM_BUILD_DIR/monocoque/src/monocoque/simulatorapi/simapi/simd/conf/simd.config \ + $RPM_BUILD_ROOT/usr/share/monocoque/simd.config %files /usr/bin/monocoque /usr/bin/gmonocoque +/usr/bin/simd +/usr/share/monocoque/simd.config 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