diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 000000000..76fdcebc5 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,169 @@ +name: Build Snap Package + +on: + # workflow_run only works if this workflow file lives on the DEFAULT branch. + # Triggered as soon as "Release" finishes (which itself triggers on + # `release: published` + workflow_dispatch, not on every push to main). + workflow_run: + workflows: ["Release"] + types: [completed] + workflow_dispatch: + inputs: + version: + description: 'Existing release tag to build against (test)' + required: true + default: 'v0.9.0' + +# Minimal default; the build-snap job raises this to `contents: write` itself, +# because it attaches the snap as a release asset. +permissions: + contents: read + +# One global group, and deliberately NOT cancel-in-progress: two releases in +# quick succession would both publish to the Snap Store's `stable` channel, and +# a Snap release cannot be retracted. +concurrency: + group: snap + cancel-in-progress: false + +jobs: + build-snap: + timeout-minutes: 75 + runs-on: ubuntu-22.04 + # `types: [completed]` also fires when the release workflow FAILED or was + # cancelled; without this gate a failed release would still build a snap. + # No extra head_branch guard is needed here: "Release" only triggers on + # `release: published` + workflow_dispatch, never on an ordinary push to + # main, so there is no spurious-trigger scenario. + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' + permissions: + contents: write + env: + # Job-level so the gated publish step can reference it from `if:`. + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + + steps: + - uses: actions/checkout@v5 + with: + # On workflow_run, checkout defaults to the DEFAULT BRANCH rather than + # the tag/commit the release workflow built — wrong, because + # snap/snapcraft.yaml must come from the built commit. Hence the + # explicit head_sha of that workflow; for workflow_dispatch (no + # workflow_run event) it falls back to github.ref, i.e. the branch the + # manual dispatch was started from. + ref: ${{ github.event.workflow_run.head_sha || github.ref }} + + - name: Get version + id: get-version + env: + # Untrusted input goes through env, NEVER directly as ${} in the shell. + # Do not set GITHUB_* names here: they are reserved and already present. + EVENT_NAME: ${{ github.event_name }} + DISPATCH_VERSION: ${{ github.event.inputs.version }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + VERSION="$DISPATCH_VERSION" + elif [ "$EVENT_NAME" = "workflow_run" ]; then + # For a `release`-triggered workflow_run, head_branch is not + # guaranteed to be the tag name (it can be the default branch), so + # fall back to looking up the latest published release instead of + # trusting head_branch. + if printf '%s' "$HEAD_BRANCH" | grep -qE '^v[0-9]'; then + VERSION="$HEAD_BRANCH" + else + VERSION="$(gh release list --exclude-drafts --limit 1 --json tagName --jq '.[0].tagName // empty')" + fi + else + VERSION="" + fi + if [ -z "$VERSION" ]; then + echo "::error::Could not determine version (event=$EVENT_NAME, head_branch=$HEAD_BRANCH)" + exit 1 + fi + # `version` and `version_number` are fed into a `sed` REPLACEMENT + # PATTERN below, and a dispatch input is free text — a '/' or '&' would + # rewrite that pattern, and a newline would write a second line into + # $GITHUB_OUTPUT (output injection). Whitelist: the `case` covers the + # entire value (including newlines), the regex then checks structure. + case "$VERSION" in + *[!v0-9.]*) + echo "::error::Version contains characters other than 'v', digits and dots." >&2 + exit 1 + ;; + esac + if ! printf '%s' "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Version '$VERSION' is not of the form vX.Y.Z." >&2 + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "version_number=${VERSION#v}" >> "$GITHUB_OUTPUT" + + # We do NOT rebuild the Rust app: the snap repackages the AppImage from the + # published GitHub release (--appimage-extract rather than dpkg-deb -x, + # since this project does not produce a .deb). + - name: Wait for release AppImage and download it + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.get-version.outputs.version }} + run: | + echo "Waiting for AppImage asset of release $VERSION..." + found="" + for i in $(seq 1 10); do + if gh release view "$VERSION" --json assets --jq '.assets[].name' 2>/dev/null | grep -q '\.AppImage$'; then + found=1 + echo "AppImage asset found" + break + fi + echo "no AppImage asset yet... (attempt $i/10)" + sleep 15 + done + if [ -z "$found" ]; then + echo "::error::AppImage asset for $VERSION did not appear in time" + exit 1 + fi + gh release download "$VERSION" -p '*.AppImage' --clobber --dir . + appimage=$(ls *.AppImage | head -1) + cp "$appimage" open-cad-studio.AppImage + echo "Using $appimage -> open-cad-studio.AppImage" + + - name: Set version in snapcraft.yaml + env: + VERSION_NUMBER: ${{ steps.get-version.outputs.version_number }} + run: | + sed -i "s/^version: .*/version: '$VERSION_NUMBER'/" snap/snapcraft.yaml + grep '^version:' snap/snapcraft.yaml + + - name: Build snap + uses: snapcore/action-build@v1 + id: snapcraft + + # Real release (via workflow_run): attach the snap as a release asset. + - name: Upload snap to GitHub release + if: github.event_name == 'workflow_run' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.get-version.outputs.version }} + SNAP: ${{ steps.snapcraft.outputs.snap }} + run: | + gh release upload "$VERSION" "$SNAP" --clobber + + # Manual test run: snap as a CI artifact only, do NOT touch the release. + - name: Upload snap as workflow artifact + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: open-cad-studio-snap + path: ${{ steps.snapcraft.outputs.snap }} + if-no-files-found: error + + # Publishing live to the Snap Store: only on a real release, and only once + # the store credentials secret exists (an owner step) — and only once the + # name `open-cad-studio` has been registered on snapcraft.io. + - name: Publish to Snap Store + if: github.event_name == 'workflow_run' && env.SNAPCRAFT_STORE_CREDENTIALS != '' + uses: snapcore/action-publish@v1 + with: + snap: ${{ steps.snapcraft.outputs.snap }} + release: stable diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 000000000..01b7685ce --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,83 @@ +name: open-cad-studio +base: core22 +# Overwritten in CI with the release version number (see .github/workflows/snap.yml). +version: '0.0.0' +title: Open CAD Studio +summary: 2D drafting and 3D modeling with native DWG/DXF support +description: | + Open CAD Studio (OCS) is a CAD application for 2D drafting and 3D + modeling, built in Rust. Reads and writes DWG and DXF files natively + (R13 through R2018). + + File formats: + - DWG and DXF read/write (R13-R2018) + - STL export, STEP AP203 export, OBJ import + - PDF export (plot layouts) + - WBLOCK, and XREF (attach/reload/auto-resolve external references) + + 2D drafting: + - Lines, polylines, rectangles, polygons, circles, arcs, ellipses, + splines + - Hatch fills with pattern, scale, and angle editing + - Offset, trim, extend, fillet, break, stretch, lengthen + - Array, mirror, move, copy, rotate, scale, explode + - Double-click text editing, and area/perimeter/centroid measurement + + 3D modeling: + - Solid primitives (box, sphere, cylinder), extrude, revolve, loft, + sweep, and 3D array + - ACIS tessellation for rendering 3DSOLID/REGION/BODY entities + + Open CAD Studio is open source, part of the OpenAEC Foundation family + of applications, and rendered with wgpu for GPU-accelerated 2D/3D + drawing. + +grade: stable +confinement: strict + +apps: + open-cad-studio: + command: usr/bin/OpenCADStudio + desktop: usr/share/applications/io.github.HakanSeven12.OpenCadStudio.desktop + plugs: + - home # open/save/export drawings in the home directory + - removable-media # same, on external drives / USB + - network # license / Patreon check + - opengl # GPU access for wgpu (Vulkan/GL) + - wayland + - x11 + - desktop + - desktop-legacy + +parts: + open-cad-studio: + plugin: nil + # The CI step copies the published release AppImage to the project root as + # `open-cad-studio.AppImage`; we unpack that instead of rebuilding from + # source. `--appimage-extract` yields a ready-made AppDir. + override-build: | + set -eu + chmod +x "${CRAFT_PROJECT_DIR}/open-cad-studio.AppImage" + "${CRAFT_PROJECT_DIR}/open-cad-studio.AppImage" --appimage-extract + mkdir -p "${CRAFT_PART_INSTALL}/usr" + cp -r squashfs-root/usr/. "${CRAFT_PART_INSTALL}/usr/" + # OCS uses winit (X11/Wayland) + wgpu (Vulkan with a GL fallback), not + # GTK/WebKit — so deliberately NO `extensions: [gnome]`. + # libvulkan1/mesa-vulkan-drivers are a fallback; real hardware acceleration + # goes through the `opengl` plug to the host driver. + stage-packages: + - libvulkan1 + - mesa-vulkan-drivers + - libgl1 + - libx11-6 + - libxcursor1 + - libxi6 + - libxrandr2 + - libxinerama1 + - libxkbcommon0 + - libxkbcommon-x11-0 + - libwayland-client0 + - libwayland-cursor0 + - libwayland-egl1 + - libfontconfig1 + - libfreetype6