From 7e0618d64b3dc70de64b5565ae436a9204074cea Mon Sep 17 00:00:00 2001 From: bherbruck Date: Fri, 9 Oct 2026 19:04:27 +0000 Subject: [PATCH] Release pipeline after PhotoCraft's: AppImage/deb/rpm/Flatpak/tar.gz for Linux x86_64 and aarch64, FreeBSD, a macOS dmg and CLI zip, Windows x64/x86/arm64 MSI and portable zip (plus the plain x64 exe), web zip, SHA256SUMS.txt; version 0.1.2 Adapted from storytold/photocraft's release.yml and packaging/ (Apache-2.0; ATTRIBUTION.md and NOTICE say so). Packages install the licence files, THIRD-PARTY-LICENSES.txt included. The app icon is drawn by `solvecraft-cli icon` (a blue block with a bore, rendered on a transparent background; .ico too), so no image is committed. Notes: a short header plus GitHub's generated What's Changed. A dry run (Run workflow, publish off) keeps the files as the release-assets artifact. packaging-lint.yml checks the workflows, scripts and metadata. Co-Authored-By: Claude Opus 5.5 --- .github/release-notes.md | 90 +--- .github/release/package-linux.sh | 38 -- .github/release/package-macos.sh | 44 -- .github/workflows/packaging-lint.yml | 53 +++ .github/workflows/release.yml | 388 +++++++++++++----- ATTRIBUTION.md | 1 + Cargo.lock | 26 +- Cargo.toml | 2 +- NOTICE | 2 + apps/solvecraft-cli/src/main.rs | 66 +++ crates/render/src/lib.rs | 2 +- crates/render/src/raster.rs | 28 +- packaging/env.sh | 65 +++ packaging/freebsd/package.sh | 70 ++++ packaging/linux/AppRun | 73 ++++ .../linux/ai.storyteller.solvecraft.desktop | 14 + .../ai.storyteller.solvecraft.metainfo.xml.in | 64 +++ .../linux/ai.storyteller.solvecraft.mime.xml | 11 + packaging/linux/flatpak-bundle.sh | 72 ++++ .../ai.storyteller.solvecraft.bundle.yml | 43 ++ packaging/linux/nfpm.yaml | 74 ++++ packaging/linux/package.sh | 139 +++++++ packaging/linux/postinst.sh | 13 + packaging/macos/Info.plist.in | 64 +++ packaging/macos/package.sh | 116 ++++++ packaging/web/package.sh | 36 ++ packaging/windows/package.ps1 | 133 ++++++ packaging/windows/solvecraft.en-us.wxl | 12 + packaging/windows/solvecraft.wxs | 121 ++++++ 29 files changed, 1593 insertions(+), 267 deletions(-) delete mode 100755 .github/release/package-linux.sh delete mode 100755 .github/release/package-macos.sh create mode 100644 .github/workflows/packaging-lint.yml create mode 100644 packaging/env.sh create mode 100755 packaging/freebsd/package.sh create mode 100755 packaging/linux/AppRun create mode 100644 packaging/linux/ai.storyteller.solvecraft.desktop create mode 100644 packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in create mode 100644 packaging/linux/ai.storyteller.solvecraft.mime.xml create mode 100755 packaging/linux/flatpak-bundle.sh create mode 100644 packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml create mode 100644 packaging/linux/nfpm.yaml create mode 100755 packaging/linux/package.sh create mode 100755 packaging/linux/postinst.sh create mode 100644 packaging/macos/Info.plist.in create mode 100755 packaging/macos/package.sh create mode 100755 packaging/web/package.sh create mode 100644 packaging/windows/package.ps1 create mode 100644 packaging/windows/solvecraft.en-us.wxl create mode 100644 packaging/windows/solvecraft.wxs diff --git a/.github/release-notes.md b/.github/release-notes.md index 450ad293..57cb892f 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -1,73 +1,25 @@ -SolveCraft is an open-source parametric 3D CAD application in pure Rust: sketch on a plane, -constrain and dimension the sketch, turn profiles into solids on a feature timeline, and change a -parameter to watch the timeline rebuild. It is early, and we'd like your bug reports. +SolveCraft is open-source parametric 3D CAD in Rust: sketch, constrain, extrude, and change a +parameter to watch the timeline rebuild. [Documentation](https://bherbruck.github.io/solvecraft/) +· [try it in the browser](https://bherbruck.github.io/solvecraft/app/) · +[contributing](https://github.com/bherbruck/solvecraft/blob/main/CONTRIBUTING.md) · +[issues](https://github.com/bherbruck/solvecraft/issues). AI agents can model with it over MCP: +`solvecraft-cli mcp` ([how](https://bherbruck.github.io/solvecraft/guide/mcp.html)). -## Links +**Which file?** +- **Windows:** `SolveCraft-{version}-windows-x64.exe` runs as is. Or install the `.msi`, or unzip the + `-portable.zip` (x64, x86 and arm64). +- **macOS 11+** (Apple silicon and Intel): open the `.dmg` and drag SolveCraft onto Applications. + The CLI is in `solvecraft-cli-{version}-macos-universal.zip`. +- **Linux** (x86_64 and aarch64, glibc 2.35+): the `.AppImage` (`chmod +x`, then run it), the + `.deb` or `.rpm`, the `.flatpak` (`flatpak install --user `), or the `.tar.gz`. +- **FreeBSD:** `tar -xzf solvecraft-{version}-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local`. +- **Web:** `solvecraft-web-{version}.zip` is a static site to host yourself. +- `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing`. -- **Documentation:** https://bherbruck.github.io/solvecraft/ -- **Try it in the browser:** https://bherbruck.github.io/solvecraft/app/ (`?sample` opens a sample part) -- **Contributing:** [CONTRIBUTING.md](https://github.com/bherbruck/solvecraft/blob/main/CONTRIBUTING.md) -- **Bugs and ideas:** [issues](https://github.com/bherbruck/solvecraft/issues) - -## What works - -- Sketches with SolveCraft's own constraint solver: lines, rectangles, circles, arcs, polygons, - slots, splines, text; constraints and parameter-driven dimensions; automatic profiles. -- Solid features on a parametric timeline that rebuilds incrementally: extrude, revolve, sweep, - loft, fillet and chamfer, shell, draft, holes and threads, patterns, mirror, combine, split, - primitives. Faces and edges are referenced by persistent names, so upstream edits don't - scramble downstream features. -- Parameters with unit-aware expressions, and configurations. -- Components, joints, motion studies, sheet metal with flat patterns, and a plastic-enclosure - subset (bosses, lips, snap fits). -- Files: `.solvecraft` designs; STEP (AP203/214/242) and IGES import and export; 3MF, STL and OBJ; - DXF. Atomic saves, autosave and crash recovery. -- 76 of 76 reference parts built in Fusion rebuild in SolveCraft with the same volume, area and - topology. - -## Built for AI agents - -Everything the UI does is a command with JSON parameters, so an agent can do it too. -`solvecraft-cli mcp` is a Model Context Protocol server: headless, or bridged to the running app -(`solvecraft --control 7878`, then `solvecraft-cli mcp --connect 127.0.0.1:7878`) so you can -watch the agent model. In Claude Code: `claude mcp add solvecraft -- solvecraft-cli mcp`. The -demo at the top of the [README](https://github.com/bherbruck/solvecraft#readme) shows an agent -building an enclosure through the UI and MCP. - -## Downloads - -| File | What | -|---|---| -| `SolveCraft-{tag}-windows-x86_64.exe` | Windows 10/11: the app, ready to run | -| `solvecraft-cli-{tag}-windows-x86_64.exe` | Windows: the command-line tool and MCP server | -| `SolveCraft-{tag}-macos-universal.dmg` | macOS 11 or newer, Apple silicon and Intel: open it and drag SolveCraft onto Applications | -| `solvecraft-cli-{tag}-macos-universal` | macOS: the command-line tool and MCP server | -| `SolveCraft-{tag}-x86_64.AppImage` | Linux x86_64 (glibc 2.35 or newer): the app as one file | -| `solvecraft-{tag}-linux-x86_64` | Linux: the app as a plain binary | -| `solvecraft-cli-{tag}-linux-x86_64` | Linux: the command-line tool and MCP server | -| `solvecraft-{tag}-web.zip` | the web app, to host yourself (any static server; HTTPS or localhost for saving files) | -| `SHA256SUMS` | checksums: `sha256sum -c SHA256SUMS` | - -On Linux and macOS a downloaded program isn't executable yet: `chmod +x` it first -(`chmod +x SolveCraft-{tag}-x86_64.AppImage && ./SolveCraft-{tag}-x86_64.AppImage`). The licences -of SolveCraft and everything it is built from are inside each program: Help ▸ About ▸ Licences, -or `solvecraft-cli licences`. - -### The binaries are not signed - -- **macOS:** the first time, right-click (or Control-click) `SolveCraft.app` in Applications and - choose **Open**, then **Open** again in the dialog. If macOS says the app is damaged, run - `xattr -dr com.apple.quarantine /Applications/SolveCraft.app`. For the CLI: - `xattr -d com.apple.quarantine solvecraft-cli-{tag}-macos-universal`. -- **Windows:** SmartScreen may say "Windows protected your PC". Click **More info**, then - **Run anyway**. - -## Known limitations - -SolveCraft is early. Some fillet, chamfer and boolean configurations still fail (with an error, -never a crash); surfaces, drawings, CAM, mesh tools and simulation are out of scope for now; the -model is Z-up only, so STEP files from Y-up tools open lying on their backs. The full list is in +The builds aren't signed yet. On macOS, right-click SolveCraft in Applications and choose +**Open** the first time (or run `xattr -dr com.apple.quarantine /Applications/SolveCraft.app`). +On Windows, SmartScreen may say "Windows protected your PC": **More info**, then **Run anyway**. +Licences are built in (Help ▸ About ▸ Licences, `solvecraft-cli licences`); the packages also +install `THIRD-PARTY-LICENSES.txt`. Known limitations: [ROADMAP.md](https://github.com/bherbruck/solvecraft/blob/main/ROADMAP.md#known-limitations). -SolveCraft is MIT OR Apache-2.0. It is a clean-room implementation and is not affiliated with -Autodesk; Fusion is a trademark of Autodesk, Inc. diff --git a/.github/release/package-linux.sh b/.github/release/package-linux.sh deleted file mode 100755 index 641edfc4..00000000 --- a/.github/release/package-linux.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -# Package the Linux release build: the app and the CLI as plain binaries, and an AppImage of the -# app. Usage: .github/release/package-linux.sh (from the repository root, after -# `cargo build --release -p solvecraft -p solvecraft-cli`). Writes out/. -set -euo pipefail -tag=${1:?usage: package-linux.sh } -bin=${CARGO_TARGET_DIR:-target}/release -rm -rf stage out && mkdir -p stage out -cp "$bin/solvecraft" "out/solvecraft-$tag-linux-x86_64" -cp "$bin/solvecraft-cli" "out/solvecraft-cli-$tag-linux-x86_64" - -# AppImage. The icon is rendered by SolveCraft itself from the bracket example, so no image file -# is committed. -app=stage/SolveCraft.AppDir -mkdir -p "$app/usr/bin" -cp "$bin/solvecraft" "$bin/solvecraft-cli" "$app/usr/bin/" -"$bin/solvecraft-cli" snapshot examples/bracket.json --out "$app/solvecraft.png" --width 256 --height 256 >/dev/null -cat > "$app/solvecraft.desktop" <<'DESKTOP' -[Desktop Entry] -Type=Application -Name=SolveCraft -Comment=Parametric 3D CAD -Exec=solvecraft %f -Icon=solvecraft -Categories=Graphics;Engineering; -MimeType=model/step; -DESKTOP -cat > "$app/AppRun" <<'APPRUN' -#!/bin/sh -here="$(dirname "$(readlink -f "$0")")" -exec "$here/usr/bin/solvecraft" "$@" -APPRUN -chmod +x "$app/AppRun" -tool=stage/appimagetool -curl -sSfL -o "$tool" https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -chmod +x "$tool" -APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 "$tool" --no-appstream "$app" "out/SolveCraft-$tag-x86_64.AppImage" -ls -l out diff --git a/.github/release/package-macos.sh b/.github/release/package-macos.sh deleted file mode 100755 index 1d4c9a35..00000000 --- a/.github/release/package-macos.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# Package the macOS release build: a universal (arm64 + x86_64) SolveCraft.app in a .dmg (with an -# Applications link to drag it onto), and the universal CLI as a plain binary. -# Usage: .github/release/package-macos.sh (from the repository root, after building both -# aarch64-apple-darwin and x86_64-apple-darwin). Writes out/. Neither is signed. -set -euo pipefail -tag=${1:?usage: package-macos.sh } -version=${tag#v} -t=${CARGO_TARGET_DIR:-target} -rm -rf stage out && mkdir -p stage/dmg out -app="stage/dmg/SolveCraft.app" -mkdir -p "$app/Contents/MacOS" -lipo -create -output "$app/Contents/MacOS/solvecraft" "$t/aarch64-apple-darwin/release/solvecraft" "$t/x86_64-apple-darwin/release/solvecraft" -lipo -create -output "out/solvecraft-cli-$tag-macos-universal" "$t/aarch64-apple-darwin/release/solvecraft-cli" "$t/x86_64-apple-darwin/release/solvecraft-cli" -cat > "$app/Contents/Info.plist" < - - - - CFBundleNameSolveCraft - CFBundleDisplayNameSolveCraft - CFBundleIdentifierio.github.bherbruck.solvecraft - CFBundleExecutablesolvecraft - CFBundlePackageTypeAPPL - CFBundleVersion$version - CFBundleShortVersionString$version - LSMinimumSystemVersion11.0 - NSHighResolutionCapable - - -PLIST -lipo -info "$app/Contents/MacOS/solvecraft" -ln -s /Applications stage/dmg/Applications -hdiutil create -volname "SolveCraft $tag" -srcfolder stage/dmg -ov -format UDZO "out/SolveCraft-$tag-macos-universal.dmg" -# Check the image: mount it and look at what a user will see. -mnt=$(mktemp -d) -hdiutil attach -readonly -nobrowse -mountpoint "$mnt" "out/SolveCraft-$tag-macos-universal.dmg" -ls -la "$mnt" "$mnt/SolveCraft.app/Contents" "$mnt/SolveCraft.app/Contents/MacOS" -test -L "$mnt/Applications" && test "$(readlink "$mnt/Applications")" = /Applications -plutil -lint "$mnt/SolveCraft.app/Contents/Info.plist" -lipo "$mnt/SolveCraft.app/Contents/MacOS/solvecraft" -verify_arch arm64 x86_64 -"$mnt/SolveCraft.app/Contents/MacOS/solvecraft" --version -hdiutil detach "$mnt" -ls -l out diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml new file mode 100644 index 00000000..8a53275d --- /dev/null +++ b/.github/workflows/packaging-lint.yml @@ -0,0 +1,53 @@ +# Quick static checks for the release pipeline (seconds, no Rust build): workflow syntax, the +# packaging shell and PowerShell scripts, and the Linux desktop/AppStream/Flatpak metadata. +# Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +name: Packaging lint + +on: + push: + branches: [main] + paths: [".github/workflows/**", "packaging/**"] + pull_request: + paths: [".github/workflows/**", "packaging/**"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: packaging-lint-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + env: + ACTIONLINT_VERSION: 1.7.12 + steps: + - uses: actions/checkout@v4 + - name: Install tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends shellcheck desktop-file-utils appstream libxml2-utils python3-yaml + curl -fsSL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ + | tar -xz -C "$RUNNER_TEMP" actionlint + - name: actionlint + run: '"$RUNNER_TEMP/actionlint" -color' + - name: shellcheck + run: shellcheck packaging/*.sh packaging/*/*.sh packaging/linux/AppRun + - name: PowerShell + shell: pwsh + run: | + foreach ($f in Get-ChildItem packaging/windows/*.ps1) { + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$errors) | Out-Null + if ($errors) { $errors | ForEach-Object { Write-Output "::error file=$($f.Name)::$_" }; exit 1 } + Write-Output "ok $($f.Name)" + } + - name: XML, desktop entry, AppStream, Flatpak manifest + run: | + xmllint --noout packaging/windows/solvecraft.wxs packaging/windows/solvecraft.en-us.wxl packaging/macos/*.plist* packaging/linux/*.xml* + desktop-file-validate packaging/linux/ai.storyteller.solvecraft.desktop + sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml" + appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml" + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solvecraft' and m['command'] == 'solvecraft'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81cd0bef..43375dc3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,18 @@ -# Release builds: push a tag `v*` (or run it by hand with a tag name) and this builds the desktop -# app and the CLI for Linux, Windows and macOS plus the web app, and attaches them to a GitHub -# release with SHA256SUMS (a pre-release when the tag has a suffix, such as v0.2.0-rc1). Every -# download is a plain program (Windows .exe, Linux binary and AppImage, a macOS .dmg); the licences -# are built into the programs (Help ▸ About ▸ Licences, `solvecraft-cli licences`). Only the web -# app, being many files, is a zip. Notes: .github/release-notes.md, with {tag} replaced. A dry run: -# Actions ▸ Release ▸ Run workflow with "publish" off keeps the assets as the release-assets artifact. +# Release pipeline, after PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +# +# Push a tag `v` (matching [workspace.package] version in Cargo.toml) and this builds and +# packages SolveCraft for every platform and publishes the GitHub release "SolveCraft v" +# with SHA256SUMS.txt, .github/release-notes.md and GitHub's generated "What's Changed". A +# version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet. +# +# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .tar.gz packaging/linux +# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd +# macOS universal .dmg, and the CLI as a .zip packaging/macos +# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows +# Web a .zip of the static site packaging/web +# +# A dry run: Actions > Release > Run workflow (publish off) builds everything and keeps it as +# the release-assets artifact instead of releasing. name: Release on: @@ -12,144 +20,324 @@ on: tags: ["v*"] workflow_dispatch: inputs: - tag: - description: "Tag to release (must exist), e.g. v0.1.0; empty: the branch this runs on" - required: false publish: - description: "Create the GitHub release (off: a dry run that keeps everything as the release-assets artifact)" + description: "Publish the release (off: a dry run; the files become the release-assets artifact)" type: boolean - default: true + default: false permissions: - contents: write + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false env: CARGO_TERM_COLOR: always - TAG: ${{ inputs.tag || github.ref_name }} - TRUNK_VERSION: v0.21.14 + CARGO_INCREMENTAL: 0 jobs: - linux: - runs-on: ubuntu-22.04 # an older glibc, so the binaries run on older distributions - timeout-minutes: 60 + version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.v.outputs.version }} + date: ${{ steps.v.outputs.date }} + prerelease: ${{ steps.v.outputs.prerelease }} steps: - uses: actions/checkout@v4 - with: - ref: ${{ env.TAG }} - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: cargo build --release --locked -p solvecraft -p solvecraft-cli - - name: Package - run: .github/release/package-linux.sh "$TAG" - - uses: actions/upload-artifact@v4 - with: - name: linux - path: out/* - - windows: - runs-on: windows-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ env.TAG }} - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: cargo build --release --locked -p solvecraft -p solvecraft-cli - - name: Package - shell: pwsh + - id: v + name: Resolve version run: | - New-Item -ItemType Directory -Force -Path out | Out-Null - Copy-Item target/release/solvecraft.exe "out/SolveCraft-$env:TAG-windows-x86_64.exe" - Copy-Item target/release/solvecraft-cli.exe "out/solvecraft-cli-$env:TAG-windows-x86_64.exe" + . packaging/env.sh + if [ "$GITHUB_REF_TYPE" = tag ] && [ "${GITHUB_REF_NAME#v}" != "$VERSION" ]; then + echo "::error::tag $GITHUB_REF_NAME doesn't match the Cargo.toml version $VERSION" + exit 1 + fi + case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac + { echo "version=$VERSION"; echo "date=$(date -u +%Y-%m-%d)"; echo "prerelease=$pre"; } >>"$GITHUB_OUTPUT" + echo "### SolveCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + + # The app icon in every size, drawn by solvecraft-cli (no image files are committed), for the + # Windows jobs: the ARM64 build is cross-compiled and can't run there to draw its own. + icons: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: release-icons + - run: cargo build --release --locked -p solvecraft-cli + - run: | + . packaging/env.sh + make_icons target/release/solvecraft-cli icons - uses: actions/upload-artifact@v4 with: - name: windows - path: out/* + name: icons + path: icons + if-no-files-found: error + + linux: + name: Linux ${{ matrix.arch }} + needs: version + # The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35. + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { arch: x86_64, runner: ubuntu-22.04, deb: amd64 } + - { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 } + env: + SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} + SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + NFPM_VERSION: 2.47.0 + steps: + - uses: actions/checkout@v4 + - name: Install packaging tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends desktop-file-utils appstream zsync rpm + curl -fsSL -o "$RUNNER_TEMP/nfpm.deb" \ + "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${{ matrix.deb }}.deb" + sudo dpkg -i "$RUNNER_TEMP/nfpm.deb" + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: release-linux-${{ matrix.arch }} + - name: Build and package (AppImage, deb, rpm, tar.gz) + run: packaging/linux/package.sh + - name: Check the packages + run: | + dpkg-deb --info dist/release/*.deb + dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/solvecraft$|usr/bin/solvecraft-cli$|metainfo|applications|THIRD-PARTY-LICENSES.txt' + rpm -qip dist/release/*.rpm + rpm -qlp dist/release/*.rpm | grep -E '/usr/bin/solvecraft$|THIRD-PARTY-LICENSES.txt' + tar -tzf dist/release/*.tar.gz | grep -E '/bin/solvecraft$|THIRD-PARTY-LICENSES.txt' + ldd target/release/solvecraft + for appimage in dist/release/*.AppImage; do + chmod +x "$appimage" + APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version + "$appimage" --appimage-updateinformation | grep -F 'gh-releases-zsync|' + test -s "$appimage.zsync" + done + - uses: actions/upload-artifact@v4 + with: + name: release-linux-${{ matrix.arch }} + path: dist/release/* + if-no-files-found: error + + flatpak: + name: Flatpak ${{ matrix.arch }} + needs: [version, linux] + # Repackages the Linux job's tarball (the same binaries as the AppImage/deb/rpm); no Rust + # build here. 24.04 for a newer flatpak-builder than 22.04 ships. + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { arch: x86_64, runner: ubuntu-24.04 } + - { arch: aarch64, runner: ubuntu-24.04-arm } + env: + SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} + SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: release-linux-${{ matrix.arch }} + path: dist/linux + - name: Install flatpak-builder + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends flatpak flatpak-builder librsvg2-common + # Ubuntu 24.04's AppArmor blocks the unprivileged user namespaces bubblewrap needs. + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Build the Flatpak bundle + run: packaging/linux/flatpak-bundle.sh "dist/linux/solvecraft-$SOLVECRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz" + - uses: actions/upload-artifact@v4 + with: + name: release-flatpak-${{ matrix.arch }} + path: dist/release/*.flatpak + if-no-files-found: error + + freebsd: + name: FreeBSD x86_64 + needs: version + # GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM. + runs-on: ubuntu-latest + timeout-minutes: 180 + env: + SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} + SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - name: Build and package in a FreeBSD VM (tar.gz) + # vmactions/freebsd-vm v1.5.9, pinned by commit (it runs our build). + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc + with: + release: "14.3" + usesh: true + cpu: 4 + mem: 12288 + # Passed into the VM by name; never templated into the script below. + envs: "SOLVECRAFT_VERSION SOLVECRAFT_BUILD_DATE" + prepare: | + pkg install -y bash git curl pkgconf libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable + run: | + set -e + . "$HOME/.cargo/env" + git config --global --add safe.directory "$PWD" + # Build outside the checkout so only dist/ is copied back out of the VM. 4 jobs: more + # runs the VM out of memory. + export CARGO_TARGET_DIR=/var/tmp/solvecraft-target CARGO_BUILD_JOBS=4 + bash packaging/freebsd/package.sh + tar -tzf dist/release/solvecraft-*-freebsd-x86_64.tar.gz \ + | grep -E '/bin/solvecraft$|/bin/solvecraft-cli$|/share/applications/ai\.storyteller\.solvecraft\.desktop$|/share/doc/solvecraft/THIRD-PARTY-LICENSES\.txt$' + - uses: actions/upload-artifact@v4 + with: + name: release-freebsd-x86_64 + path: dist/release/* + if-no-files-found: error macos: - # One universal build: arm64 natively, x86_64 cross-compiled on the same runner, joined by lipo. + name: macOS universal + needs: version runs-on: macos-14 - timeout-minutes: 90 + env: + SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} + SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 - with: - ref: ${{ env.TAG }} - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin, x86_64-apple-darwin - uses: Swatinem/rust-cache@v2 - - run: | - cargo build --release --locked -p solvecraft -p solvecraft-cli --target aarch64-apple-darwin - cargo build --release --locked -p solvecraft -p solvecraft-cli --target x86_64-apple-darwin - - name: Package - run: .github/release/package-macos.sh "$TAG" + with: + key: release-macos-universal + - name: Build and package (dmg, CLI zip; the dmg is mounted and checked) + run: packaging/macos/package.sh --arch universal - uses: actions/upload-artifact@v4 with: - name: macos - path: out/* + name: release-macos + path: dist/release/* + if-no-files-found: error - web: - runs-on: ubuntu-latest - timeout-minutes: 60 + windows: + name: Windows ${{ matrix.arch }} + needs: [version, icons] + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - { arch: x64, target: x86_64-pc-windows-msvc } + - { arch: x86, target: i686-pc-windows-msvc } + # Windows on ARM: cross-compiled on the x64 runner. + - { arch: arm64, target: aarch64-pc-windows-msvc } + env: + SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: - ref: ${{ env.TAG }} + name: icons + path: icons + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + key: release-windows-${{ matrix.arch }} + - name: Install WiX + shell: pwsh + run: | + dotnet tool install --global wix --version 5.0.2 + wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2 + - name: Build and package (msi, portable zip) + shell: pwsh + env: + SOLVECRAFT_ICONS: ${{ github.workspace }}/icons + run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} + - uses: actions/upload-artifact@v4 + with: + name: release-windows-${{ matrix.arch }} + path: dist/release/* + if-no-files-found: error + + web: + name: Web (wasm) + needs: version + runs-on: ubuntu-latest + env: + SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} + TRUNK_VERSION: 0.21.14 + steps: + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 + with: + key: release-web - name: Install trunk run: | - mkdir -p "$HOME/.local/bin" - curl -sSfL "https://github.com/trunk-rs/trunk/releases/download/${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C "$HOME/.local/bin" - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - - name: Build (relative public URL, so it can be served from any path) - working-directory: apps/solvecraft-web - run: trunk build --release - - name: Package - run: | - name="solvecraft-$TAG-web" - mkdir -p out && cp -r dist/web "$name" - cp LICENSE-MIT LICENSE-APACHE NOTICE THIRD-PARTY-LICENSES.txt "$name/" - zip -qr "out/$name.zip" "$name" + curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \ + | tar -xz -C "$HOME/.cargo/bin" trunk + - name: Build and zip + run: packaging/web/package.sh - uses: actions/upload-artifact@v4 with: - name: web - path: out/* + name: release-web + path: dist/release/* + if-no-files-found: error release: - needs: [linux, windows, macos, web] + name: GitHub Release + needs: [version, linux, flatpak, freebsd, macos, windows, web] runs-on: ubuntu-latest + permissions: + contents: write + env: + VERSION: ${{ needs.version.outputs.version }} + PRERELEASE: ${{ needs.version.outputs.prerelease }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} steps: - uses: actions/checkout@v4 - with: - ref: ${{ env.TAG }} - uses: actions/download-artifact@v4 with: - path: assets + pattern: release-* + path: artifacts merge-multiple: true - - name: Checksums - working-directory: assets - run: sha256sum * > SHA256SUMS && cat SHA256SUMS - - name: Notes - run: sed "s/{tag}/$TAG/g" .github/release-notes.md > notes.md - - if: ${{ github.event_name == 'push' || inputs.publish }} - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ env.TAG }} - name: SolveCraft ${{ env.TAG }} - body_path: notes.md - generate_release_notes: true - # A tag with a suffix (v0.2.0-rc1) is a pre-release; v1.2.3 is a normal release. - prerelease: ${{ contains(env.TAG, '-') }} - files: assets/* - # A dry run (workflow_dispatch with publish off): the assets and notes as an artifact. - - if: ${{ github.event_name == 'workflow_dispatch' && !inputs.publish }} - run: cp notes.md assets/release-notes.md - - if: ${{ github.event_name == 'workflow_dispatch' && !inputs.publish }} + - name: Checksums and notes + run: | + (cd artifacts && sha256sum -- * | tee SHA256SUMS.txt) + sed "s/{version}/$VERSION/g" .github/release-notes.md >notes.md + - name: Publish the release + if: github.event_name == 'push' || inputs.publish + working-directory: artifacts + run: | + tag="v$VERSION" + pre=() + if [ "$PRERELEASE" = true ]; then pre=(--prerelease); else pre=(--latest); fi + if gh release view "$tag" >/dev/null 2>&1; then + echo "::error::Release $tag already exists. Bump the version before tagging." + exit 1 + fi + gh release create "$tag" --title "SolveCraft v$VERSION" --verify-tag --notes-file ../notes.md --generate-notes "${pre[@]}" -- * + { + echo "Published **SolveCraft v$VERSION** at \`${GITHUB_SHA::9}\`:" + echo + sed 's/^/ /' SHA256SUMS.txt + } >>"$GITHUB_STEP_SUMMARY" + - name: Keep the files (dry run) + if: github.event_name == 'workflow_dispatch' && !inputs.publish + run: cp notes.md artifacts/release-notes.md + - if: github.event_name == 'workflow_dispatch' && !inputs.publish uses: actions/upload-artifact@v4 with: name: release-assets - path: assets/* + path: artifacts/* diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 8eb04386..4880cd91 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -40,6 +40,7 @@ Generated-in-code assets are original and have no file to list: | crates/io/src/threemf.rs (bounded zip part reading and writing) | GridCraft `crates/xlsx/src/package.rs` | MIT OR Apache-2.0 | | crates/ui-egui/src/titlebar.rs (title bar drag, caption buttons, resize zones, their tests) and the window setup in apps/solvecraft/src/main.rs | VectorCraft `crates/ui-egui/src/titlebar.rs`, `apps/vectorcraft/src/main.rs` | MIT OR Apache-2.0 | | LICENSE-MIT, LICENSE-APACHE (licence text) | CADCraft | — | +| .github/workflows/release.yml, .github/workflows/packaging-lint.yml and packaging/ (release pipeline: Linux AppImage/deb/rpm/Flatpak/tar.gz, FreeBSD, macOS dmg, Windows MSI/portable zip, web zip; AppRun, nfpm, Flatpak, WiX and Info.plist templates) | PhotoCraft (https://github.com/storytold/photocraft) `.github/workflows/{release,packaging-lint}.yml`, `packaging/` | Apache-2.0 | ## Vendored third-party code diff --git a/Cargo.lock b/Cargo.lock index 3c6bb9af..da9f3962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3059,7 +3059,7 @@ dependencies = [ [[package]] name = "solvecraft" -version = "0.1.1" +version = "0.1.2" dependencies = [ "eframe", "egui", @@ -3072,7 +3072,7 @@ dependencies = [ [[package]] name = "solvecraft-cli" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde_json", "solvecraft-engine", @@ -3081,7 +3081,7 @@ dependencies = [ [[package]] name = "solvecraft-doc" -version = "0.1.1" +version = "0.1.2" dependencies = [ "log", "serde", @@ -3095,7 +3095,7 @@ dependencies = [ [[package]] name = "solvecraft-engine" -version = "0.1.1" +version = "0.1.2" dependencies = [ "log", "serde", @@ -3111,7 +3111,7 @@ dependencies = [ [[package]] name = "solvecraft-geom" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde", "serde_json", @@ -3119,7 +3119,7 @@ dependencies = [ [[package]] name = "solvecraft-io" -version = "0.1.1" +version = "0.1.2" dependencies = [ "quick-xml 0.38.4", "solvecraft-doc", @@ -3131,7 +3131,7 @@ dependencies = [ [[package]] name = "solvecraft-kernel" -version = "0.1.1" +version = "0.1.2" dependencies = [ "log", "serde", @@ -3146,7 +3146,7 @@ dependencies = [ [[package]] name = "solvecraft-mcp" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde_json", "solvecraft-engine", @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "solvecraft-render" -version = "0.1.1" +version = "0.1.2" dependencies = [ "image", "serde", @@ -3163,7 +3163,7 @@ dependencies = [ [[package]] name = "solvecraft-sketch" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde", "serde_json", @@ -3174,7 +3174,7 @@ dependencies = [ [[package]] name = "solvecraft-ui-egui" -version = "0.1.1" +version = "0.1.2" dependencies = [ "egui", "egui-wgpu", @@ -3188,7 +3188,7 @@ dependencies = [ [[package]] name = "solvecraft-web" -version = "0.1.1" +version = "0.1.2" dependencies = [ "eframe", "egui", @@ -4487,7 +4487,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xtask" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index fa3be764..6da43de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"] exclude = ["vendor"] [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.90" diff --git a/NOTICE b/NOTICE index 1639a695..a6d011f6 100644 --- a/NOTICE +++ b/NOTICE @@ -16,6 +16,8 @@ vendor/truck-shapeops/LICENSE. Copyright (c) RICOS Co. Ltd. Some code is adapted from sibling projects in the storytold family (CADCraft, GridCraft), which are licensed MIT OR Apache-2.0; ATTRIBUTION.md lists the files. +The release packaging (.github/workflows/release.yml, packaging/) is adapted from PhotoCraft +(https://github.com/storytold/photocraft), licensed under the Apache License 2.0. None of the bundled assets come from third parties: every icon is drawn in code. The per-file list is in ATTRIBUTION.md. diff --git a/apps/solvecraft-cli/src/main.rs b/apps/solvecraft-cli/src/main.rs index 09b94105..674f4925 100644 --- a/apps/solvecraft-cli/src/main.rs +++ b/apps/solvecraft-cli/src/main.rs @@ -11,6 +11,7 @@ //! solvecraft-cli step-corpus ... import part.step and compare with measure.json //! solvecraft-cli mcp [--in design|script] [--connect HOST:PORT] MCP server on stdio (docs/mcp.md) //! solvecraft-cli licences SolveCraft's licence and the third-party licences +//! solvecraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file) //! ``` #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] @@ -37,6 +38,7 @@ const USAGE: &str = "usage: solvecraft-cli step-corpus ... [--json] solvecraft-cli mcp [--in design.solvecraft|script.json] [--connect 127.0.0.1:PORT] solvecraft-cli licences + solvecraft-cli icon --out icon.png [--size 256] | --out icon.ico "; fn main() -> ExitCode { @@ -57,6 +59,7 @@ fn main() -> ExitCode { Some("oracle") => oracle::run(&args[1..]), Some("step-corpus") => oracle::step_corpus(&args[1..]), Some("mcp") => cmd_mcp(&args[1..]), + Some("icon") => cmd_icon(&args[1..]), Some("licences" | "licenses") => { // Written, not print!ed: a closed pipe (`| head`) is not a crash. use std::io::Write as _; @@ -204,6 +207,69 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> { Ok(()) } +/// The app icon: a blue block with a bore, drawn by the CPU renderer on a transparent background +/// (so packages need no committed image). `.ico` holds the Windows sizes as PNGs. +fn cmd_icon(args: &[String]) -> Result<(), String> { + let out = flag(args, "--out").ok_or("icon needs --out FILE.png or FILE.ico")?; + let size: usize = flag(args, "--size").and_then(|x| x.parse().ok()).unwrap_or(256).clamp(8, 2048); + let bytes = if out.to_ascii_lowercase().ends_with(".ico") { + let pngs: Vec<(usize, Vec)> = + [16, 20, 24, 32, 40, 48, 64, 128, 256].into_iter().map(|s| icon_png(s).map(|p| (s, p))).collect::>()?; + ico(&pngs)? + } else { + icon_png(size)? + }; + std::fs::write(out, &bytes).map_err(|e| format!("{out}: {e}"))?; + println!("{}", pretty(&json!({"path": out, "bytes": bytes.len()}))); + Ok(()) +} + +fn icon_png(size: usize) -> Result, String> { + use solvecraft_engine::render::{Rgb, Scene, SceneLine, SceneMesh, png, render_transparent}; + let mut s = Session::default(); + for (cmd, p) in [ + ("solid.box", json!({"length": 20, "width": 20, "height": 20, "center": [0, 0, 0]})), + ("solid.cylinder", json!({"diameter": 9, "height": 30, "base": [0, 0, -15], "operation": "cut"})), + ] { + s.execute(cmd, &p).map_err(|e| format!("icon: {e}"))?; + } + let mut cam = solvecraft_engine::view::home_camera(&s); + cam.distance *= 0.95; + let mut scene = Scene { radius: 60.0, ..Default::default() }; + let px = size as f64 / 256.0; + for body in &s.world_state().bodies { + let mesh = body.mesh(); + scene.meshes.push(SceneMesh { mesh: std::sync::Arc::clone(&mesh), color: Rgb(90, 160, 240) }); + for (i, e) in mesh.edges.iter().enumerate() { + if !mesh.seams.get(i).copied().unwrap_or(false) { + scene.lines.push(SceneLine { points: e.clone(), color: Rgb(24, 52, 96), width: (3.0 * px).max(0.8), on_top: false }); + } + } + } + let c = render_transparent(&mut scene, &cam, size, size); + png(c.w, c.h, c.rgba).ok_or_else(|| "icon: PNG encoding failed".to_string()) +} + +/// A Windows icon file holding PNG images (Vista and later read these). +fn ico(images: &[(usize, Vec)]) -> Result, String> { + let n = u16::try_from(images.len()).map_err(|_| "too many icon sizes")?; + let mut out = Vec::new(); + out.extend_from_slice(&[0, 0, 1, 0]); + out.extend_from_slice(&n.to_le_bytes()); + let mut offset = 6 + 16 * images.len(); + for (size, data) in images { + let dim = if *size >= 256 { 0 } else { *size as u8 }; + out.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]); + out.extend_from_slice(&u32::try_from(data.len()).map_err(|_| "icon too large")?.to_le_bytes()); + out.extend_from_slice(&u32::try_from(offset).map_err(|_| "icon too large")?.to_le_bytes()); + offset += data.len(); + } + for (_, data) in images { + out.extend_from_slice(data); + } + Ok(out) +} + fn cmd_exec(args: &[String]) -> Result<(), String> { let id = args.first().ok_or_else(|| USAGE.to_string())?; let p: Value = match args.get(1) { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 54be069c..19d3e4e4 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -8,4 +8,4 @@ mod camera; mod raster; pub use camera::{Camera, CameraAnim, Mat4, Quat, StandardView, ease_in_out}; -pub use raster::{Canvas, Rgb, Scene, SceneLine, SceneMesh, render, render_png}; +pub use raster::{Canvas, Rgb, Scene, SceneLine, SceneMesh, png, render, render_png, render_transparent}; diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index e41ac70f..1a260700 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -200,12 +200,38 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas { /// Render to PNG bytes. pub fn render_png(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Option> { let c = render(scene, cam, w, h); - let img = image::RgbaImage::from_raw(c.w as u32, c.h as u32, c.rgba)?; + png(c.w, c.h, c.rgba) +} + +/// RGBA pixels (`w × h`) to PNG bytes. +pub fn png(w: usize, h: usize, rgba: Vec) -> Option> { + let img = image::RgbaImage::from_raw(u32::try_from(w).ok()?, u32::try_from(h).ok()?, rgba)?; let mut buf = std::io::Cursor::new(Vec::new()); img.write_to(&mut buf, image::ImageFormat::Png).ok()?; Some(buf.into_inner()) } +/// Render on a transparent background: the scene drawn over black and over white, and the +/// alpha recovered from the difference (exact for the anti-aliased edges). +pub fn render_transparent(scene: &mut Scene, cam: &Camera, w: usize, h: usize) -> Canvas { + let keep = scene.background; + scene.background = Some((Rgb(0, 0, 0), Rgb(0, 0, 0))); + let black = render(scene, cam, w, h); + scene.background = Some((Rgb(255, 255, 255), Rgb(255, 255, 255))); + let mut out = render(scene, cam, w, h); + scene.background = keep; + for (o, b) in out.rgba.as_chunks_mut::<4>().0.iter_mut().zip(black.rgba.as_chunks::<4>().0) { + // white - black = 255 (1 - alpha) in every channel; average them. + let d: u32 = (0..3).map(|k| u32::from(o[k].saturating_sub(b[k]))).sum(); + let a = 255.0 - d as f64 / 3.0; + for k in 0..3 { + o[k] = if a > 0.5 { (f64::from(b[k]) * 255.0 / a).round().clamp(0.0, 255.0) as u8 } else { 0 }; + } + o[3] = a.round().clamp(0.0, 255.0) as u8; + } + out +} + #[cfg(test)] mod tests { use super::*; diff --git a/packaging/env.sh b/packaging/env.sh new file mode 100644 index 00000000..4cbc9643 --- /dev/null +++ b/packaging/env.sh @@ -0,0 +1,65 @@ +# shellcheck shell=bash +# Shared setup for the packaging scripts. Source it: `. "$(dirname "$0")/../env.sh"`. +# Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +# +# Exports: +# ROOT workspace root +# VERSION [workspace.package] version from Cargo.toml (override: SOLVECRAFT_VERSION) +# DIST output directory for release artifacts (default: $ROOT/dist/release) +# SOLVECRAFT_BUILD_DATE UTC build date, YYYY-MM-DD +# CARGO_TARGET_DIR cargo's target dir (default: $ROOT/target) + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export ROOT + +# The version lives in one place: `[workspace.package] version` in the root Cargo.toml. +workspace_version() { + awk ' + /^\[/ { in_pkg = ($0 == "[workspace.package]") ; next } + in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit } + ' "$ROOT/Cargo.toml" +} + +VERSION="${SOLVECRAFT_VERSION:-$(workspace_version)}" +if [ -z "$VERSION" ]; then + echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2 + exit 1 +fi +export VERSION + +DIST="${DIST:-$ROOT/dist/release}" +mkdir -p "$DIST" +DIST="$(cd "$DIST" && pwd)" +export DIST + +export SOLVECRAFT_BUILD_DATE="${SOLVECRAFT_BUILD_DATE:-$(date -u +%Y-%m-%d)}" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" +APP_ID=ai.storyteller.solvecraft +export APP_ID + +# Emit a GitHub Actions warning (plain stderr outside Actions). +warn() { + if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi +} + +# Copy the licence, notice and readme files into a package directory. The programs also carry +# them (Help > About > Licences, `solvecraft-cli licences`). +copy_docs() { + local dest="$1" f + for f in README.md LICENSE-MIT LICENSE-APACHE NOTICE THIRD-PARTY-LICENSES.txt; do + if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi + done +} + +# The app icon in every size a package needs, drawn by the CLI (no image files are committed): +# $1 = the solvecraft-cli to run, $2 = output dir. Writes hicolor/x/apps/$APP_ID.png, +# solvecraft.ico and solvecraft-1024.png. +make_icons() { + local cli="$1" out="$2" s + for s in 16 24 32 48 64 128 256 512; do + mkdir -p "$out/hicolor/${s}x${s}/apps" + "$cli" icon --size "$s" --out "$out/hicolor/${s}x${s}/apps/$APP_ID.png" >/dev/null + done + "$cli" icon --out "$out/solvecraft.ico" >/dev/null + "$cli" icon --size 1024 --out "$out/solvecraft-1024.png" >/dev/null +} diff --git a/packaging/freebsd/package.sh b/packaging/freebsd/package.sh new file mode 100755 index 00000000..e7e6ee5c --- /dev/null +++ b/packaging/freebsd/package.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Build and package SolveCraft for FreeBSD: +# +# $DIST/solvecraft--freebsd-x86_64.tar.gz a /usr/local-style tree: +# solvecraft--freebsd-x86_64/{bin, share/applications, share/icons, share/mime, +# share/metainfo, share/doc/solvecraft} +# +# Install by copying the tree's contents into /usr/local: +# tar -xzf solvecraft--freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local +# +# Usage: packaging/freebsd/package.sh [--skip-build] +# Needs: bash, cargo, and the packages installed by release.yml's FreeBSD job. +# Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +LINUX="$ROOT/packaging/linux" + +SKIP_BUILD=0 +while [ $# -gt 0 ]; do + case "$1" in + --skip-build) SKIP_BUILD=1; shift ;; + -h | --help) sed -n '2,13p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +# FreeBSD's uname says amd64; release file names use x86_64 like the Linux ones. +case "$(uname -m)" in + amd64 | x86_64) ARCH=x86_64 ;; + arm64 | aarch64) ARCH=aarch64 ;; + *) echo "unsupported architecture $(uname -m)" >&2; exit 2 ;; +esac +BASENAME="solvecraft-$VERSION-freebsd-$ARCH" +echo "==> SolveCraft $VERSION for FreeBSD $ARCH" + +# The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory. +export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}" +if [ "$SKIP_BUILD" = 0 ]; then + (cd "$ROOT" && cargo build --release --locked -p solvecraft -p solvecraft-cli) +fi +WORK="$CARGO_TARGET_DIR/freebsd-package" +STAGE="$WORK/$BASENAME" +BIN="$CARGO_TARGET_DIR/release" +rm -rf "$WORK" + +# FreeBSD's install(1) has no -D: create the directories first. +mkdir -p "$STAGE/bin" "$STAGE/share/applications" "$STAGE/share/mime/packages" \ + "$STAGE/share/metainfo" "$STAGE/share/icons" "$STAGE/share/doc/solvecraft" +install -m 755 "$BIN/solvecraft" "$BIN/solvecraft-cli" "$STAGE/bin/" +strip "$STAGE/bin/solvecraft" "$STAGE/bin/solvecraft-cli" 2>/dev/null || true +# The desktop entry, MIME type, metainfo and icons are the freedesktop files Linux ships. +install -m 644 "$LINUX/$APP_ID.desktop" "$STAGE/share/applications/$APP_ID.desktop" +install -m 644 "$LINUX/$APP_ID.mime.xml" "$STAGE/share/mime/packages/$APP_ID.xml" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SOLVECRAFT_BUILD_DATE/g" \ + "$LINUX/$APP_ID.metainfo.xml.in" >"$STAGE/share/metainfo/$APP_ID.metainfo.xml" +make_icons "$STAGE/bin/solvecraft-cli" "$WORK/icons" +cp -R "$WORK/icons/hicolor" "$STAGE/share/icons/" +copy_docs "$STAGE/share/doc/solvecraft" + +for f in bin/solvecraft bin/solvecraft-cli "share/applications/$APP_ID.desktop" \ + "share/icons/hicolor/256x256/apps/$APP_ID.png" share/doc/solvecraft/LICENSE-MIT \ + share/doc/solvecraft/LICENSE-APACHE share/doc/solvecraft/THIRD-PARTY-LICENSES.txt; do + if [ ! -e "$STAGE/$f" ]; then echo "error: $f is missing from the package" >&2; exit 1; fi +done + +tar -C "$WORK" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME" +echo "wrote $DIST/$BASENAME.tar.gz" +"$STAGE/bin/solvecraft-cli" --version +echo "==> done" diff --git a/packaging/linux/AppRun b/packaging/linux/AppRun new file mode 100755 index 00000000..4b1b815c --- /dev/null +++ b/packaging/linux/AppRun @@ -0,0 +1,73 @@ +#!/bin/sh +# SolveCraft AppImage entry point (packaging/linux/AppRun; installed by package.sh). +# Adapted from PhotoCraft's AppRun (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +# +# On Wayland the compositor resolves a window's dock icon by taking the window's +# app_id (ai.storyteller.solvecraft) and looking up an installed .desktop file. +# An AppImage installs nothing, so every AppImage run would otherwise show the +# generic "Wayland" icon. Best-effort desktop integration: on first run +# (and whenever the AppImage's path changes) install the .desktop entry and the +# hicolor icons into $XDG_DATA_HOME, then launch. Every failure here must leave +# the launch untouched. Opt out with SOLVECRAFT_NO_DESKTOP_INTEGRATION=1. + +set -u + +APPDIR="$(dirname "$(readlink -f "$0")")" +DESK_ID=ai.storyteller.solvecraft + +integrate() { + case "${SOLVECRAFT_NO_DESKTOP_INTEGRATION:-}" in + 1 | true | yes) return 0 ;; + esac + # The AppImage runtime exports $APPIMAGE; without it (an extracted AppDir) there + # is no path to register and nothing to do. + appimage="${APPIMAGE:-}" + [ -n "$appimage" ] && [ -f "$appimage" ] || return 0 + appimage="$(readlink -f "$appimage")" || return 0 + # Exec= is parsed by the desktop: a path holding a quote, backquote, dollar, backslash, + # percent or a line break would need escaping (or could add lines), so skip integration. + case "$appimage" in + *[\"\`\$\\%]* | *' +'*) return 0 ;; + esac + home="${HOME:-}" + [ -n "$home" ] || return 0 + data="${XDG_DATA_HOME:-$home/.local/share}" + src_desktop="$APPDIR/usr/share/applications/$DESK_ID.desktop" + [ -f "$src_desktop" ] || return 0 + + # Rewrite Exec to this AppImage (the binary is not on the host PATH) and drop + # TryExec, which would hide the entry; everything else is shipped as packaged. + apps="$data/applications" + mkdir -p "$apps" 2>/dev/null || return 0 + tmp="$apps/.$DESK_ID.tmp.$$" + esc="$(printf '%s' "$appimage" | sed 's/[\\&|]/\\&/g')" + if sed -e "s|^Exec=.*|Exec=\"$esc\" %F|" -e '/^TryExec=/d' "$src_desktop" 2>/dev/null >"$tmp"; then + if cmp -s "$tmp" "$apps/$DESK_ID.desktop"; then + rm -f "$tmp" + else + if mv -f "$tmp" "$apps/$DESK_ID.desktop" 2>/dev/null; then + command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1 + else + rm -f "$tmp" + fi + fi + else + rm -f "$tmp" + return 0 + fi + + # Icons: per-file compare so an unchanged install costs a handful of cmp calls. + src_icons="$APPDIR/usr/share/icons/hicolor" + [ -d "$src_icons" ] || return 0 + icons="$data/icons/hicolor" + (cd "$src_icons" && find . -type f) | while IFS= read -r rel; do + mkdir -p "$icons/$(dirname "$rel")" 2>/dev/null || continue + if ! cmp -s "$src_icons/$rel" "$icons/$rel"; then + cp "$src_icons/$rel" "$icons/$rel" 2>/dev/null || true + fi + done +} + +integrate +exec "$APPDIR/usr/bin/solvecraft" "$@" diff --git a/packaging/linux/ai.storyteller.solvecraft.desktop b/packaging/linux/ai.storyteller.solvecraft.desktop new file mode 100644 index 00000000..0cdcfe18 --- /dev/null +++ b/packaging/linux/ai.storyteller.solvecraft.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Type=Application +Name=SolveCraft +GenericName=3D CAD +Comment=Parametric 3D CAD: sketch, constrain, extrude, and change a parameter to rebuild +Exec=solvecraft %f +TryExec=solvecraft +Icon=ai.storyteller.solvecraft +Terminal=false +StartupNotify=true +StartupWMClass=solvecraft +Categories=Graphics;3DGraphics;Engineering; +Keywords=cad;3d;parametric;sketch;step;stl;modelling;modeling; +MimeType=application/x-solvecraft;model/step;model/iges;model/stl;model/3mf; diff --git a/packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in b/packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in new file mode 100644 index 00000000..a2d76e1a --- /dev/null +++ b/packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in @@ -0,0 +1,64 @@ + + + + ai.storyteller.solvecraft + CC0-1.0 + MIT OR Apache-2.0 + SolveCraft + Parametric 3D CAD in the style of Fusion + + SolveCraft contributors + + +

+ SolveCraft is an open-source parametric 3D CAD application written in Rust. Sketch on a + plane, constrain and dimension the sketch, turn profiles into solids on a feature + timeline, and change a parameter to watch the timeline rebuild. +

+
    +
  • Sketches with a constraint solver and parameter-driven dimensions
  • +
  • Extrude, revolve, sweep, loft, fillet, chamfer, shell, holes and patterns
  • +
  • Components and joints, sheet metal, and a plastic-enclosure subset
  • +
  • STEP and IGES import and export; 3MF, STL and OBJ
  • +
  • Every action is a command, scriptable from a CLI, a JSON control channel and an MCP server
  • +
+
+ ai.storyteller.solvecraft.desktop + ai.storyteller.solvecraft + https://bherbruck.github.io/solvecraft/ + https://github.com/bherbruck/solvecraft/issues + https://bherbruck.github.io/solvecraft/ + + Graphics + 3DGraphics + Engineering + + + cad + parametric + step + + + + A sample part with its sketches and feature timeline + https://raw.githubusercontent.com/bherbruck/solvecraft/main/docs/screenshots/sample-plate.png + + + + 760 + + + keyboard + pointing + + + solvecraft + solvecraft-cli + application/x-solvecraft + + + + + +
diff --git a/packaging/linux/ai.storyteller.solvecraft.mime.xml b/packaging/linux/ai.storyteller.solvecraft.mime.xml new file mode 100644 index 00000000..2e3df1a8 --- /dev/null +++ b/packaging/linux/ai.storyteller.solvecraft.mime.xml @@ -0,0 +1,11 @@ + + + + + SolveCraft design + + + + + diff --git a/packaging/linux/flatpak-bundle.sh b/packaging/linux/flatpak-bundle.sh new file mode 100755 index 00000000..639bedc5 --- /dev/null +++ b/packaging/linux/flatpak-bundle.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Repackage a SolveCraft Linux tarball as a single-file Flatpak bundle: +# +# $DIST/solvecraft--linux-.flatpak +# +# Usage: packaging/linux/flatpak-bundle.sh [--no-test] [TARBALL] +# +# TARBALL defaults to $DIST/solvecraft--linux-.tar.gz from package.sh. The bundle +# is built for the host architecture (x86_64 or aarch64), which must match the tarball's. +# Needs flatpak, flatpak-builder and the SVG pixbuf loader (librsvg2-common) for the host's +# `appstreamcli compose`; the freedesktop runtime and SDK named in the manifest are +# installed per-user from Flathub. Unless --no-test, the bundle is then installed per-user and +# `solvecraft-cli --version` is run inside the sandbox as a smoke test. +# Manifest: packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml. +# Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" +FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo + +TEST=1 +TARBALL="" +while [ $# -gt 0 ]; do + case "$1" in + --no-test) TEST=0; shift ;; + -h | --help) sed -n '2,14p' "$0"; exit 0 ;; + -*) echo "unknown argument: $1" >&2; exit 2 ;; + *) TARBALL="$1"; shift ;; + esac +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) ;; + aarch64 | arm64) ARCH=aarch64 ;; + *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; +esac +BASENAME="solvecraft-$VERSION-linux-$ARCH" +TARBALL="${TARBALL:-$DIST/$BASENAME.tar.gz}" +[ -f "$TARBALL" ] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; } +for tool in flatpak flatpak-builder; do + command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; } +done + +echo "==> SolveCraft $VERSION Flatpak bundle for $ARCH from $(basename "$TARBALL")" + +WORK="$CARGO_TARGET_DIR/flatpak-bundle" +rm -rf "$WORK" +mkdir -p "$WORK/stage" +tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1 +[ -x "$WORK/stage/bin/solvecraft" ] || { echo "error: $TARBALL has no bin/solvecraft" >&2; exit 1; } +cp "$HERE/flatpak/$APP_ID.bundle.yml" "$WORK/$APP_ID.yml" + +flatpak remote-add --user --if-not-exists flathub "$FLATHUB" +# --disable-rofiles-fuse: no FUSE in containers and CI runners. +flatpak-builder --user --install-deps-from=flathub --disable-rofiles-fuse --force-clean \ + --default-branch=stable --repo="$WORK/repo" --state-dir="$WORK/state" "$WORK/build" "$WORK/$APP_ID.yml" + +OUT="$DIST/$BASENAME.flatpak" +# --runtime-repo lets `flatpak install` fetch the freedesktop runtime from Flathub if missing. +flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable +echo "wrote $OUT" + +if [ "$TEST" = 1 ]; then + flatpak install --user -y --noninteractive --reinstall "$OUT" + flatpak info --user "$APP_ID" + flatpak run --command=solvecraft-cli "$APP_ID" --version + flatpak run --command=sh "$APP_ID" -c 'ls /app/share/applications /app/share/metainfo /app/share/mime/packages /app/share/icons/hicolor/256x256/apps /app/share/doc/solvecraft' +fi +echo "==> done" +ls -lh "$OUT" diff --git a/packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml b/packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml new file mode 100644 index 00000000..789eba4b --- /dev/null +++ b/packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml @@ -0,0 +1,43 @@ +# Flatpak manifest for the single-file bundle attached to every GitHub release +# (solvecraft--linux-.flatpak). Build it with packaging/linux/flatpak-bundle.sh. +# Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +# +# It repackages the release's own Linux tarball (solvecraft--linux-.tar.gz, built +# on Ubuntu 22.04 by package.sh) instead of compiling Rust inside flatpak-builder: no network +# during the build, and the same binaries as the AppImage/deb/rpm. The binaries need glibc >= 2.35 +# and load X11, Wayland, xkbcommon and Vulkan at runtime; the freedesktop runtime has all of +# them, and the GPU driver comes from its org.freedesktop.Platform.GL extension. +id: ai.storyteller.solvecraft +runtime: org.freedesktop.Platform +runtime-version: '26.08' +sdk: org.freedesktop.Sdk +command: solvecraft +finish-args: + # Display: Wayland first; X11 only when no Wayland compositor is running. + - --socket=wayland + - --socket=fallback-x11 + # X11 shared memory (MIT-SHM). Without it the X11 fallback is slow or fails. + - --share=ipc + # GPU access for the wgpu viewport (Vulkan, or GL as a fallback). + - --device=dri + # Open/Save dialogs go through the file-chooser portal (rfd's xdg-portal backend), so picked + # files work anywhere without broad filesystem access. Documents is granted so files passed by + # path (the command line, recent designs) work in the usual place. + - --filesystem=xdg-documents + # Deliberately not granted: network (the optional --control server listens on 127.0.0.1 in + # the sandbox; `flatpak override --user --share=network ai.storyteller.solvecraft` exposes it to + # host agents) and home/host filesystem access. +modules: + - name: solvecraft + buildsystem: simple + build-commands: + - install -Dm755 bin/solvecraft /app/bin/solvecraft + - install -Dm755 bin/solvecraft-cli /app/bin/solvecraft-cli + - install -Dm644 share/applications/ai.storyteller.solvecraft.desktop /app/share/applications/ai.storyteller.solvecraft.desktop + - install -Dm644 share/mime/packages/ai.storyteller.solvecraft.xml /app/share/mime/packages/ai.storyteller.solvecraft.xml + # Already has the release's version and date substituted by package.sh. + - install -Dm644 share/metainfo/ai.storyteller.solvecraft.metainfo.xml /app/share/metainfo/ai.storyteller.solvecraft.metainfo.xml + - mkdir -p /app/share/icons /app/share/doc && cp -R share/icons/hicolor /app/share/icons/ && cp -R share/doc/solvecraft /app/share/doc/ + sources: + - type: dir + path: stage diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml new file mode 100644 index 00000000..309bc28e --- /dev/null +++ b/packaging/linux/nfpm.yaml @@ -0,0 +1,74 @@ +# .deb and .rpm packages (nfpm: https://nfpm.goreleaser.com). Run by package.sh, which stages +# the file tree, substitutes its path for ${STAGE} (nfpm doesn't expand env vars in `src`) and +# exports VERSION and NFPM_ARCH. Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0). +name: solvecraft +arch: ${NFPM_ARCH} +platform: linux +version: ${VERSION} +# semver: 0.2.0-rc.1 becomes 0.2.0~rc.1 in deb/rpm, so pre-releases sort before the release. +version_schema: semver +release: 1 +section: graphics +priority: optional +maintainer: ${SOLVECRAFT_MAINTAINER} +vendor: SolveCraft contributors +homepage: https://bherbruck.github.io/solvecraft/ +license: MIT OR Apache-2.0 +description: |- + Open-source parametric 3D CAD in the style of Fusion + Sketches with a constraint solver, a parametric feature timeline, components and + joints, sheet metal; STEP, IGES, 3MF, STL and OBJ. Includes solvecraft-cli, a + headless command runner and MCP server for AI agents. + +# The binaries link only glibc and libgcc_s directly. Windowing (X11/Wayland, xkbcommon) and the +# GPU (Vulkan, EGL/GL) are loaded at runtime with dlopen, so they aren't ELF NEEDED entries and +# the package tools can't find them: list them here. +overrides: + deb: + depends: + - libc6 (>= 2.35) + - libgcc-s1 + - libxkbcommon0 + - libxkbcommon-x11-0 + - libwayland-client0 + - libx11-6 + - libx11-xcb1 + - libxcb1 + - libxcursor1 + - libxi6 + - libvulkan1 | libegl1 + recommends: + - mesa-vulkan-drivers + - libegl-mesa0 + - xdg-desktop-portal + rpm: + # Soname provides rather than package names, so the same requirements resolve on Fedora/RHEL + # and openSUSE. + depends: + - libxkbcommon.so.0()(64bit) + - libxkbcommon-x11.so.0()(64bit) + - libwayland-client.so.0()(64bit) + - libX11.so.6()(64bit) + - libX11-xcb.so.1()(64bit) + - libxcb.so.1()(64bit) + - libXcursor.so.1()(64bit) + - libXi.so.6()(64bit) + - (libvulkan.so.1()(64bit) or libEGL.so.1()(64bit)) + recommends: + - mesa-vulkan-drivers + - xdg-desktop-portal + +contents: + - src: ${STAGE}/usr/bin/solvecraft + dst: /usr/bin/solvecraft + file_info: { mode: 0755 } + - src: ${STAGE}/usr/bin/solvecraft-cli + dst: /usr/bin/solvecraft-cli + file_info: { mode: 0755 } + - src: ${STAGE}/usr/share/ + dst: /usr/share/ + type: tree + +scripts: + postinstall: packaging/linux/postinst.sh + postremove: packaging/linux/postinst.sh diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh new file mode 100755 index 00000000..da0e7037 --- /dev/null +++ b/packaging/linux/package.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Build and package SolveCraft for Linux ( is x86_64 or aarch64): +# +# $DIST/solvecraft--linux-.AppImage any distro with glibc >= the build host's +# $DIST/solvecraft--linux-.AppImage.zsync delta updates (needs zsyncmake) +# $DIST/solvecraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... +# $DIST/solvecraft--linux-.rpm Fedora, openSUSE, RHEL, ... +# $DIST/solvecraft--linux-.tar.gz plain FHS-style tree (bin/, share/) +# +# Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"] +# +# Needs: cargo; nfpm for deb/rpm (https://nfpm.goreleaser.com); appimagetool for the AppImage +# (downloaded into $CARGO_TARGET_DIR if missing). Build on an old distro (CI: Ubuntu 22.04, +# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli, +# zsyncmake (the zsync package) for the AppImage's .zsync. +# Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" + +SKIP_BUILD=0 +FORMATS="appimage deb rpm tar" +while [ $# -gt 0 ]; do + case "$1" in + --skip-build) SKIP_BUILD=1; shift ;; + --formats) FORMATS="$2"; shift 2 ;; + -h | --help) sed -n '2,16p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) DEB_ARCH=amd64 ;; + aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;; + *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; +esac +export SOLVECRAFT_MAINTAINER="${SOLVECRAFT_MAINTAINER:-SolveCraft contributors }" +BASENAME="solvecraft-$VERSION-linux-$ARCH" + +echo "==> SolveCraft $VERSION for Linux $ARCH ($FORMATS)" + +if [ "$SKIP_BUILD" = 0 ]; then + (cd "$ROOT" && cargo build --release --locked -p solvecraft -p solvecraft-cli) +fi +BIN="$CARGO_TARGET_DIR/release" +WORK="$CARGO_TARGET_DIR/linux-package" +STAGE="$WORK/root" +rm -rf "$WORK" + +# ---- stage an FHS tree (shared by every format) ------------------------------------------------- +install -Dm755 "$BIN/solvecraft" "$STAGE/usr/bin/solvecraft" +install -Dm755 "$BIN/solvecraft-cli" "$STAGE/usr/bin/solvecraft-cli" +strip "$STAGE/usr/bin/solvecraft" "$STAGE/usr/bin/solvecraft-cli" 2>/dev/null || true +install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop" +install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml" +mkdir -p "$STAGE/usr/share/metainfo" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SOLVECRAFT_BUILD_DATE/g" \ + "$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" +make_icons "$STAGE/usr/bin/solvecraft-cli" "$WORK/icons" +mkdir -p "$STAGE/usr/share/icons" +cp -R "$WORK/icons/hicolor" "$STAGE/usr/share/icons/" +mkdir -p "$STAGE/usr/share/doc/solvecraft" +copy_docs "$STAGE/usr/share/doc/solvecraft" + +if command -v desktop-file-validate >/dev/null; then + desktop-file-validate "$STAGE/usr/share/applications/$APP_ID.desktop" +fi +if command -v appstreamcli >/dev/null; then + appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" +fi + +has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } + +# ---- .tar.gz ------------------------------------------------------------------------------------ +if has tar; then + mkdir -p "$WORK/tar" + cp -R "$STAGE/usr" "$WORK/tar/$BASENAME" + tar -C "$WORK/tar" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME" + echo "wrote $DIST/$BASENAME.tar.gz" +fi + +# ---- .deb / .rpm -------------------------------------------------------------------------------- +if has deb || has rpm; then + command -v nfpm >/dev/null || { echo "error: nfpm not found (https://nfpm.goreleaser.com/install/)" >&2; exit 1; } + export VERSION + export NFPM_ARCH="$DEB_ARCH" + # nfpm expands env vars in fields like `version` and `arch`, but not in `contents[].src`. + sed "s|\${STAGE}|$STAGE|g" "$HERE/nfpm.yaml" >"$WORK/nfpm.yaml" + for fmt in deb rpm; do + if has "$fmt"; then (cd "$ROOT" && nfpm package -f "$WORK/nfpm.yaml" -p "$fmt" -t "$DIST/$BASENAME.$fmt"); fi + done +fi + +# ---- AppImage ----------------------------------------------------------------------------------- +if has appimage; then + APPDIR="$WORK/SolveCraft.AppDir" + cp -R "$STAGE" "$APPDIR" + mv "$APPDIR/usr/share/doc" "$WORK/doc-unused" + # AppRun is a script, not a symlink: it installs the .desktop entry and icons into + # $XDG_DATA_HOME before launching, so Wayland compositors can resolve the window's app_id to + # our icon. + install -Dm755 "$HERE/AppRun" "$APPDIR/AppRun" + cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop" + cp "$WORK/icons/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" + ln -s "$APP_ID.png" "$APPDIR/.DirIcon" + + TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}" + if [ -z "$TOOL" ]; then + TOOL="$CARGO_TARGET_DIR/appimagetool-$ARCH.AppImage" + if [ ! -x "$TOOL" ]; then + curl -fsSL -o "$TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage" + chmod +x "$TOOL" + fi + fi + OUT="$DIST/$BASENAME.AppImage" + APPDIR="$(cd "$APPDIR" && pwd)" + TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")" + rm -f "$OUT.zsync" + # Update information: AppImageUpdate and the like fetch only the changed blocks of a newer + # release through the .zsync published next to each AppImage. `latest` is the newest published + # release that is not a pre-release. + REPO="${GITHUB_REPOSITORY:-bherbruck/solvecraft}" + UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|solvecraft-*-linux-$ARCH.AppImage.zsync" + # Extract-and-run: works without FUSE (containers, CI). With zsyncmake on the host appimagetool + # also writes the .zsync, into its working directory, hence the cd. + (cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT") + echo "wrote $OUT" + if [ -s "$OUT.zsync" ]; then + echo "wrote $OUT.zsync" + else + warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it" + fi +fi + +"$STAGE/usr/bin/solvecraft-cli" --version +echo "==> done" +ls -lh "$DIST" diff --git a/packaging/linux/postinst.sh b/packaging/linux/postinst.sh new file mode 100755 index 00000000..775e4df9 --- /dev/null +++ b/packaging/linux/postinst.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# deb/rpm post-install and post-remove: refresh the desktop, MIME and icon caches. +# Every tool is optional; missing ones are skipped. +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi +if command -v update-mime-database >/dev/null 2>&1; then + update-mime-database /usr/share/mime || true +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +fi +exit 0 diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in new file mode 100644 index 00000000..c67eadd6 --- /dev/null +++ b/packaging/macos/Info.plist.in @@ -0,0 +1,64 @@ + + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SolveCraft + CFBundleName + SolveCraft + CFBundleExecutable + SolveCraft + CFBundleIdentifier + ai.storyteller.solvecraft + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + @SHORT_VERSION@ + CFBundleVersion + @SHORT_VERSION@ + SolveCraftVersion + @VERSION@ + CFBundleIconFile + SolveCraft + LSApplicationCategoryType + public.app-category.graphics-design + + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + NSSupportsAutomaticGraphicsSwitching + + CFBundleDocumentTypes + + + CFBundleTypeName + SolveCraft design + CFBundleTypeExtensions + solvecraft + CFBundleTypeRole + Editor + LSHandlerRank + Owner + + + CFBundleTypeName + STEP, IGES and mesh files + CFBundleTypeExtensions + stepstpigsiges3mfstl + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + + diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh new file mode 100755 index 00000000..abd8e03b --- /dev/null +++ b/packaging/macos/package.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# Build and package SolveCraft for macOS: +# +# $DIST/solvecraft--macos-.dmg SolveCraft.app on a drag-to-Applications DMG +# $DIST/solvecraft-cli--macos-.zip the headless CLI with the licence files +# +# Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build] +# +# The app is ad-hoc signed (not notarized): the first launch needs right-click > Open. +# Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/macos" + +ARCH=universal +SKIP_BUILD=0 +while [ $# -gt 0 ]; do + case "$1" in + --arch) ARCH="$2"; shift 2 ;; + --skip-build) SKIP_BUILD=1; shift ;; + -h | --help) sed -n '2,10p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done +case "$ARCH" in + universal) TARGETS=(aarch64-apple-darwin x86_64-apple-darwin) ;; + aarch64) TARGETS=(aarch64-apple-darwin) ;; + x86_64) TARGETS=(x86_64-apple-darwin) ;; + *) echo "unknown --arch $ARCH" >&2; exit 2 ;; +esac + +# Keep in sync with LSMinimumSystemVersion in Info.plist.in. +export MACOSX_DEPLOYMENT_TARGET=11.0 +SHORT_VERSION="${VERSION%%-*}" +WORK="$CARGO_TARGET_DIR/macos-package" +APP="$WORK/SolveCraft.app" +DMG="$DIST/solvecraft-$VERSION-macos-$ARCH.dmg" +CLI_ZIP="$DIST/solvecraft-cli-$VERSION-macos-$ARCH.zip" + +echo "==> SolveCraft $VERSION for macOS ($ARCH)" +if [ "$SKIP_BUILD" = 0 ]; then + args=() + for t in "${TARGETS[@]}"; do args+=(--target "$t"); done + (cd "$ROOT" && cargo build --release --locked -p solvecraft -p solvecraft-cli "${args[@]}") +fi + +rm -rf "$WORK" +mkdir -p "$WORK/bin" +for bin in solvecraft solvecraft-cli; do + inputs=() + for t in "${TARGETS[@]}"; do inputs+=("$CARGO_TARGET_DIR/$t/release/$bin"); done + lipo -create -output "$WORK/bin/$bin" "${inputs[@]}" + lipo -info "$WORK/bin/$bin" +done + +# ---- SolveCraft.app ---------------------------------------------------------------------------- +echo "==> assembling $APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" +cp "$WORK/bin/solvecraft" "$APP/Contents/MacOS/SolveCraft" +# The icon, drawn by the CLI (no image files are committed). +SET="$WORK/SolveCraft.iconset" +mkdir -p "$SET" +for s in 16 32 128 256 512; do + "$WORK/bin/solvecraft-cli" icon --size "$s" --out "$SET/icon_${s}x${s}.png" >/dev/null + "$WORK/bin/solvecraft-cli" icon --size $((s * 2)) --out "$SET/icon_${s}x${s}@2x.png" >/dev/null +done +iconutil -c icns -o "$APP/Contents/Resources/SolveCraft.icns" "$SET" +mkdir -p "$APP/Contents/Resources/Licenses" +copy_docs "$APP/Contents/Resources/Licenses" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" "$HERE/Info.plist.in" >"$APP/Contents/Info.plist" +plutil -lint "$APP/Contents/Info.plist" +printf 'APPL????' >"$APP/Contents/PkgInfo" +# Ad-hoc signature over the whole bundle (Apple silicon refuses unsigned code). +codesign --force --sign - --timestamp=none "$APP" +codesign --verify --strict --deep --verbose=2 "$APP" + +# ---- DMG --------------------------------------------------------------------------------------- +echo "==> building $DMG" +STAGE="$WORK/dmg" +mkdir -p "$STAGE" +ditto "$APP" "$STAGE/SolveCraft.app" +ln -s /Applications "$STAGE/Applications" +rm -f "$DMG" "$WORK/raw.dmg" +# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, +# which is flaky on CI runners ("Resource busy"). +hdiutil makehybrid -hfs -hfs-volume-name "SolveCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" +rm -f "$WORK/raw.dmg" + +# Check the image as a user gets it: mount it and look inside. +MNT="$(mktemp -d)" +hdiutil attach -readonly -nobrowse -mountpoint "$MNT" "$DMG" +ls -la "$MNT" "$MNT/SolveCraft.app/Contents" "$MNT/SolveCraft.app/Contents/MacOS" "$MNT/SolveCraft.app/Contents/Resources" +test -L "$MNT/Applications" && test "$(readlink "$MNT/Applications")" = /Applications +plutil -lint "$MNT/SolveCraft.app/Contents/Info.plist" +case "$ARCH" in universal) WANT=(arm64 x86_64) ;; aarch64) WANT=(arm64) ;; *) WANT=(x86_64) ;; esac +lipo "$MNT/SolveCraft.app/Contents/MacOS/SolveCraft" -verify_arch "${WANT[@]}" +test -s "$MNT/SolveCraft.app/Contents/Resources/Licenses/THIRD-PARTY-LICENSES.txt" +codesign --verify --strict --deep "$MNT/SolveCraft.app" +"$MNT/SolveCraft.app/Contents/MacOS/SolveCraft" --version +hdiutil detach "$MNT" + +# ---- CLI --------------------------------------------------------------------------------------- +echo "==> building $CLI_ZIP" +CLI_DIR="$WORK/solvecraft-cli-$VERSION-macos-$ARCH" +mkdir -p "$CLI_DIR" +cp "$WORK/bin/solvecraft-cli" "$CLI_DIR/" +codesign --force --sign - --timestamp=none --identifier ai.storyteller.solvecraft-cli "$CLI_DIR/solvecraft-cli" +copy_docs "$CLI_DIR" +rm -f "$CLI_ZIP" +ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP" + +"$WORK/bin/solvecraft-cli" --version +echo "==> done" +ls -lh "$DMG" "$CLI_ZIP" diff --git a/packaging/web/package.sh b/packaging/web/package.sh new file mode 100755 index 00000000..35f5f9ed --- /dev/null +++ b/packaging/web/package.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Build the browser version and zip it: $DIST/solvecraft-web-.zip +# +# Usage: packaging/web/package.sh [--skip-build] +# +# Needs: trunk and the wasm32-unknown-unknown target. The zip holds a self-contained static site +# in solvecraft-web-/ that works from any URL path (public_url = "./" in Trunk.toml). +# Saving designs needs HTTPS or localhost (the browser's private file storage). +# Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" + +if [ "${1:-}" != "--skip-build" ]; then + command -v trunk >/dev/null || { echo "error: trunk not found (cargo install trunk --locked)" >&2; exit 1; } + (cd "$ROOT/apps/solvecraft-web" && trunk build --release) +fi + +SITE="$ROOT/dist/web" +[ -f "$SITE/index.html" ] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; } +# Paths must be relative so the site works under any prefix. +if grep -Eq '(src|href)="/[^/]' "$SITE/index.html"; then + echo "error: $SITE/index.html has root-absolute URLs; it would break when served from a sub-path" >&2 + exit 1 +fi + +NAME="solvecraft-web-$VERSION" +WORK="$CARGO_TARGET_DIR/web-package" +rm -rf "$WORK" +mkdir -p "$WORK/$NAME" +cp -R "$SITE/." "$WORK/$NAME/" +copy_docs "$WORK/$NAME" +rm -f "$DIST/$NAME.zip" +(cd "$WORK" && zip -qr9 "$DIST/$NAME.zip" "$NAME") +echo "wrote $DIST/$NAME.zip" +ls -lh "$DIST/$NAME.zip" diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 new file mode 100644 index 00000000..91737a8f --- /dev/null +++ b/packaging/windows/package.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Build and package SolveCraft for Windows. + +.DESCRIPTION + Produces, in $env:DIST (default: dist/release): + solvecraft--windows-.msi per-machine installer (WiX v5) + solvecraft--windows--portable.zip solvecraft.exe + solvecraft-cli.exe + licences + SolveCraft--windows-.exe the app alone (x64 only: a plain download) + + The binaries link the C runtime statically (+crt-static), so nothing else needs installing. + The icon comes from $env:SOLVECRAFT_ICONS (solvecraft.ico, made by `solvecraft-cli icon`), or + is drawn by the CLI just built when this machine can run it. Nothing is signed. + + Needs: Rust (MSVC toolchain + the target) and WiX v5: + dotnet tool install --global wix --version 5.0.2 + wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2 + + Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). + +.EXAMPLE + pwsh packaging/windows/package.ps1 -Arch x64 + pwsh packaging/windows/package.ps1 -Arch arm64 # cross-compiled; needs the MSVC ARM64 build tools +#> +param( + [ValidateSet('x64', 'x86', 'arm64')] [string] $Arch = 'x64', + [switch] $SkipBuild +) +$ErrorActionPreference = 'Stop' +$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path + +function Invoke-Native([string] $What, [scriptblock] $Block) { + Write-Output "==> $What" + & $Block + if ($LASTEXITCODE -ne 0) { throw "$What failed with exit code $LASTEXITCODE" } +} + +# The version lives in one place: [workspace.package] version in the root Cargo.toml. +$Version = $env:SOLVECRAFT_VERSION +if (-not $Version) { + $inPkg = $false + foreach ($line in Get-Content (Join-Path $Root 'Cargo.toml')) { + if ($line -match '^\s*\[') { $inPkg = ($line.Trim() -eq '[workspace.package]'); continue } + if ($inPkg -and $line -match '^\s*version\s*=\s*"([^"]+)"') { $Version = $Matches[1]; break } + } +} +if (-not $Version) { throw 'could not read [workspace.package] version from Cargo.toml' } +# MSI ProductVersion is numeric (major.minor.build); pre-release tags are dropped there. +$MsiVersion = ($Version -split '-')[0] + +$Target = switch ($Arch) { 'x64' { 'x86_64-pc-windows-msvc' } 'x86' { 'i686-pc-windows-msvc' } 'arm64' { 'aarch64-pc-windows-msvc' } } +$Dist = if ($env:DIST) { $env:DIST } else { Join-Path $Root 'dist\release' } +$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root 'target' } +New-Item -ItemType Directory -Force -Path $Dist | Out-Null + +Write-Output "SolveCraft $Version for Windows $Arch ($Target)" + +if (-not $SkipBuild) { + # Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and + # proc-macros are unaffected. + $flagVar = 'CARGO_TARGET_' + ($Target.ToUpper() -replace '-', '_') + '_RUSTFLAGS' + [Environment]::SetEnvironmentVariable($flagVar, '-C target-feature=+crt-static') + Invoke-Native "cargo build ($Target)" { cargo build --release --locked -p solvecraft -p solvecraft-cli --target $Target } +} +$Bin = Join-Path $TargetDir "$Target\release" + +# Check both binaries' PE headers: Machine must match -Arch; Subsystem 2 = GUI (the app opens no +# console window), 3 = console (the CLI's output reaches the terminal). +function Get-PeHeader([string] $Path) { + $bytes = [System.IO.File]::ReadAllBytes($Path) + $pe = [BitConverter]::ToInt32($bytes, 0x3C) + return @{ Machine = [BitConverter]::ToUInt16($bytes, $pe + 4); Subsystem = [BitConverter]::ToUInt16($bytes, $pe + 0x5C) } +} +$Machine = switch ($Arch) { 'x64' { 0x8664 } 'x86' { 0x14C } 'arm64' { 0xAA64 } } +foreach ($check in @(@('solvecraft.exe', 2), @('solvecraft-cli.exe', 3))) { + $h = Get-PeHeader (Join-Path $Bin $check[0]) + if ($h.Machine -ne $Machine) { throw "$($check[0]) is for machine 0x$('{0:X}' -f $h.Machine), expected 0x$('{0:X}' -f $Machine) ($Arch)" } + if ($h.Subsystem -ne $check[1]) { throw "$($check[0]) has PE subsystem $($h.Subsystem), expected $($check[1])" } + Write-Output "ok $($check[0]): $Arch, PE subsystem $($h.Subsystem)" +} +$Stage = Join-Path $TargetDir "windows-package\$Arch" +Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Stage | Out-Null +Copy-Item (Join-Path $Bin 'solvecraft.exe'), (Join-Path $Bin 'solvecraft-cli.exe') $Stage +foreach ($f in 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE', 'THIRD-PARTY-LICENSES.txt') { Copy-Item (Join-Path $Root $f) $Stage } + +$HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() +$CanRun = ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') +$Icon = if ($env:SOLVECRAFT_ICONS) { Join-Path $env:SOLVECRAFT_ICONS 'solvecraft.ico' } else { Join-Path $Stage 'solvecraft.ico' } +if (-not (Test-Path $Icon)) { + if (-not $CanRun) { throw "no icon: set SOLVECRAFT_ICONS to a folder with solvecraft.ico (an $Arch CLI can't run on this $HostArch machine)" } + Invoke-Native 'draw the icon' { & (Join-Path $Stage 'solvecraft-cli.exe') icon --out $Icon } +} + +# ---- MSI --------------------------------------------------------------------------------------- +$Msi = Join-Path $Dist "solvecraft-$Version-windows-$Arch.msi" +Invoke-Native 'wix build' { + wix build (Join-Path $PSScriptRoot 'solvecraft.wxs') -arch $Arch ` + -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext ` + -culture en-US -loc (Join-Path $PSScriptRoot 'solvecraft.en-us.wxl') ` + -d "Version=$MsiVersion" -d "DisplayVersion=$Version" -d "BinDir=$Stage" -d "IconPath=$Icon" ` + -o $Msi +} +Invoke-Native 'MSI shortcut icon validation (ICE50)' { + wix msi validate $Msi -ice ICE50 -intermediateFolder (Join-Path $Stage 'msi-validation') +} +# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets. +Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb')) + +# ---- portable zip ------------------------------------------------------------------------------ +$Portable = Join-Path $TargetDir "windows-package\solvecraft-$Version-windows-$Arch-portable" +Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Portable | Out-Null +Copy-Item (Join-Path $Stage '*.exe') $Portable +foreach ($f in 'README.md', 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE', 'THIRD-PARTY-LICENSES.txt') { Copy-Item (Join-Path $Root $f) $Portable } +$Zip = Join-Path $Dist "solvecraft-$Version-windows-$Arch-portable.zip" +Remove-Item -Force $Zip -ErrorAction SilentlyContinue +Compress-Archive -Path $Portable -DestinationPath $Zip + +# ---- the app as a plain download (x64) --------------------------------------------------------- +$Outputs = @($Msi, $Zip) +if ($Arch -eq 'x64') { + $Exe = Join-Path $Dist "SolveCraft-$Version-windows-$Arch.exe" + Copy-Item (Join-Path $Stage 'solvecraft.exe') $Exe + $Outputs += $Exe +} + +if ($CanRun) { + Invoke-Native 'solvecraft-cli --version' { & (Join-Path $Stage 'solvecraft-cli.exe') --version } +} else { + Write-Output "skipping solvecraft-cli --version: an $Arch build doesn't run on this $HostArch machine" +} +Get-Item $Outputs | Format-Table Name, Length diff --git a/packaging/windows/solvecraft.en-us.wxl b/packaging/windows/solvecraft.en-us.wxl new file mode 100644 index 00000000..f1cd7d43 --- /dev/null +++ b/packaging/windows/solvecraft.en-us.wxl @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packaging/windows/solvecraft.wxs b/packaging/windows/solvecraft.wxs new file mode 100644 index 00000000..1f406ee9 --- /dev/null +++ b/packaging/windows/solvecraft.wxs @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +