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 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 19:18:29 +00:00
1 parent 90fde44c32
commit 7e0618d64b
29 files changed
+1593 -267

No files matched your search

+65
View File
@@ -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/<n>x<n>/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
}
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Build and package SolveCraft for FreeBSD:
#
# $DIST/solvecraft-<version>-freebsd-x86_64.tar.gz a /usr/local-style tree:
# solvecraft-<version>-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-<version>-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"
+73
View File
@@ -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" "$@"
@@ -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;
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- AppStream metadata (software centres, Flathub). Template: package.sh substitutes @VERSION@
and @DATE@ from Cargo.toml and the build date. -->
<component type="desktop-application">
<id>ai.storyteller.solvecraft</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<name>SolveCraft</name>
<summary>Parametric 3D CAD in the style of Fusion</summary>
<developer id="io.github.bherbruck">
<name>SolveCraft contributors</name>
</developer>
<description>
<p>
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.
</p>
<ul>
<li>Sketches with a constraint solver and parameter-driven dimensions</li>
<li>Extrude, revolve, sweep, loft, fillet, chamfer, shell, holes and patterns</li>
<li>Components and joints, sheet metal, and a plastic-enclosure subset</li>
<li>STEP and IGES import and export; 3MF, STL and OBJ</li>
<li>Every action is a command, scriptable from a CLI, a JSON control channel and an MCP server</li>
</ul>
</description>
<launchable type="desktop-id">ai.storyteller.solvecraft.desktop</launchable>
<icon type="stock">ai.storyteller.solvecraft</icon>
<url type="homepage">https://bherbruck.github.io/solvecraft/</url>
<url type="bugtracker">https://github.com/bherbruck/solvecraft/issues</url>
<url type="help">https://bherbruck.github.io/solvecraft/</url>
<categories>
<category>Graphics</category>
<category>3DGraphics</category>
<category>Engineering</category>
</categories>
<keywords>
<keyword>cad</keyword>
<keyword>parametric</keyword>
<keyword>step</keyword>
</keywords>
<screenshots>
<screenshot type="default">
<caption>A sample part with its sketches and feature timeline</caption>
<image>https://raw.githubusercontent.com/bherbruck/solvecraft/main/docs/screenshots/sample-plate.png</image>
</screenshot>
</screenshots>
<requires>
<display_length compare="ge">760</display_length>
</requires>
<supports>
<control>keyboard</control>
<control>pointing</control>
</supports>
<provides>
<binary>solvecraft</binary>
<binary>solvecraft-cli</binary>
<mediatype>application/x-solvecraft</mediatype>
</provides>
<content_rating type="oars-1.1"/>
<releases>
<release version="@VERSION@" date="@DATE@"/>
</releases>
</component>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- shared-mime-info definitions for types the system database may not have.
Installed to /usr/share/mime/packages/ (update-mime-database runs on install). -->
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-solvecraft">
<comment>SolveCraft design</comment>
<icon name="ai.storyteller.solvecraft"/>
<sub-class-of type="application/json"/>
<glob pattern="*.solvecraft"/>
</mime-type>
</mime-info>
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Repackage a SolveCraft Linux tarball as a single-file Flatpak bundle:
#
# $DIST/solvecraft-<version>-linux-<arch>.flatpak
#
# Usage: packaging/linux/flatpak-bundle.sh [--no-test] [TARBALL]
#
# TARBALL defaults to $DIST/solvecraft-<version>-linux-<arch>.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"
@@ -0,0 +1,43 @@
# Flatpak manifest for the single-file bundle attached to every GitHub release
# (solvecraft-<version>-linux-<arch>.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-<version>-linux-<arch>.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
+74
View File
@@ -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
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# Build and package SolveCraft for Linux (<arch> is x86_64 or aarch64):
#
# $DIST/solvecraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
# $DIST/solvecraft-<version>-linux-<arch>.AppImage.zsync delta updates (needs zsyncmake)
# $DIST/solvecraft-<version>-linux-<arch>.deb Debian, Ubuntu, Mint, Pop!_OS, ...
# $DIST/solvecraft-<version>-linux-<arch>.rpm Fedora, openSUSE, RHEL, ...
# $DIST/solvecraft-<version>-linux-<arch>.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 <https://github.com/bherbruck/solvecraft>}"
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"
+13
View File
@@ -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
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Template: packaging/macos/package.sh substitutes @VERSION@ (full, e.g. 0.2.0-rc.1) and
@SHORT_VERSION@ (numeric X.Y.Z). Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0). -->
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>SolveCraft</string>
<key>CFBundleName</key>
<string>SolveCraft</string>
<key>CFBundleExecutable</key>
<string>SolveCraft</string>
<key>CFBundleIdentifier</key>
<string>ai.storyteller.solvecraft</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>@SHORT_VERSION@</string>
<key>CFBundleVersion</key>
<string>@SHORT_VERSION@</string>
<key>SolveCraftVersion</key>
<string>@VERSION@</string>
<key>CFBundleIconFile</key>
<string>SolveCraft</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.graphics-design</string>
<!-- Must match MACOSX_DEPLOYMENT_TARGET in package.sh. -->
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>SolveCraft design</string>
<key>CFBundleTypeExtensions</key>
<array><string>solvecraft</string></array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>STEP, IGES and mesh files</string>
<key>CFBundleTypeExtensions</key>
<array><string>step</string><string>stp</string><string>igs</string><string>iges</string><string>3mf</string><string>stl</string></array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
</dict>
</plist>
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# Build and package SolveCraft for macOS:
#
# $DIST/solvecraft-<version>-macos-<arch>.dmg SolveCraft.app on a drag-to-Applications DMG
# $DIST/solvecraft-cli-<version>-macos-<arch>.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"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Build the browser version and zip it: $DIST/solvecraft-web-<version>.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-<version>/ 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"
+133
View File
@@ -0,0 +1,133 @@
<#
.SYNOPSIS
Build and package SolveCraft for Windows.
.DESCRIPTION
Produces, in $env:DIST (default: dist/release):
solvecraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
solvecraft-<version>-windows-<arch>-portable.zip solvecraft.exe + solvecraft-cli.exe + licences
SolveCraft-<version>-windows-<arch>.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
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Overrides for the setup wizard's WixUI strings, so its pages name the version being installed
([SolvecraftVersion] is the full version, pre-release tag included, set in solvecraft.wxs).
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
-->
<WixLocalization Culture="en-US" xmlns="http://wixtoolset.org/schemas/v4/wxl">
<String Id="WelcomeDlgTitle" Value="{\WixUI_Font_Bigger}Welcome to the [ProductName] [SolvecraftVersion] Setup Wizard" />
<String Id="WelcomeDlgDescription" Value="The Setup Wizard will install [ProductName] version [SolvecraftVersion] on your computer. Click Next to continue or Cancel to exit the Setup Wizard." />
<String Id="VerifyReadyDlgInstallTitle" Value="{\WixUI_Font_Title}Ready to install [ProductName] [SolvecraftVersion]" />
<String Id="ExitDialogTitle" Value="{\WixUI_Font_Bigger}Completed the [ProductName] [SolvecraftVersion] Setup Wizard" />
</WixLocalization>
+121
View File
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SolveCraft MSI (WiX Toolset v5, plus the UI and Util extensions). Built by package.ps1:
wix build solvecraft.wxs -arch x64 -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext
-culture en-US -loc solvecraft.en-us.wxl -d Version=1.2.3 -d DisplayVersion=1.2.3-rc.1
-d BinDir=... -d IconPath=... -o out.msi
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolveCraft by
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
SolveCraft" box. Installs a Start Menu shortcut, solvecraft-cli, the licence files (including
THIRD-PARTY-LICENSES.txt), makes SolveCraft the owner of `.solvecraft` and offers it under
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package
Name="SolveCraft"
Manufacturer="SolveCraft contributors"
Version="$(var.Version)"
UpgradeCode="6A145FD6-D285-428D-AE84-A69570CDF4DB"
Scope="perMachine"
Compressed="yes"
Language="1033">
<SummaryInformation Description="SolveCraft parametric 3D CAD" Manufacturer="SolveCraft contributors" />
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other. -->
<MajorUpgrade AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of SolveCraft is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<!-- Advertised shortcuts to an .exe need an .exe icon identifier (ICE50). -->
<Icon Id="SolvecraftIcon.exe" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="SolvecraftIcon.exe" />
<Property Id="ARPURLINFOABOUT" Value="https://bherbruck.github.io/solvecraft/" />
<Property Id="ARPURLUPDATEINFO" Value="https://github.com/bherbruck/solvecraft/releases" />
<Property Id="ARPHELPLINK" Value="https://github.com/bherbruck/solvecraft/issues" />
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="SolveCraft" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<!-- An upgrade installs where the user put the previous version (written by SolvecraftApp). -->
<Property Id="INSTALLFOLDER">
<RegistrySearch Id="PreviousInstallFolder" Root="HKLM" Key="Software\SolveCraft" Name="InstallDir" Type="raw" />
</Property>
<Feature Id="Main" Title="SolveCraft" Level="1">
<ComponentGroupRef Id="SolvecraftFiles" />
</Feature>
<!-- WixUI_InstallDir without its licence page (MIT OR Apache-2.0: nothing to accept). -->
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<UI>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2" Condition="NOT Installed" />
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2" />
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchSolvecraft"
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
</UI>
<Property Id="SolvecraftVersion" Value="$(var.DisplayVersion)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="SolveCraft $(var.DisplayVersion) is installed. You can start it any time from the Start menu." />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch SolveCraft" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
<Property Id="WixShellExecTarget" Value="[#SolvecraftExe]" />
<!-- Runs in the wizard's own (unelevated) process, so the app starts as the user. -->
<CustomAction Id="LaunchSolvecraft" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)" DllEntry="WixShellExec" Impersonate="yes" Return="ignore" />
</Package>
<Fragment>
<ComponentGroup Id="SolvecraftFiles" Directory="INSTALLFOLDER">
<Component Id="SolvecraftApp" Guid="290BEF27-C200-4069-AA43-7DE6A39CDB0A">
<File Id="SolvecraftExe" Source="$(var.BinDir)\solvecraft.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="SolveCraft"
Description="SolveCraft parametric 3D CAD" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="SolvecraftIcon.exe" IconIndex="0" />
</File>
<!-- .solvecraft: SolveCraft's own format; SolveCraft is its default app. -->
<ProgId Id="Solvecraft.Design" Description="SolveCraft Design" Icon="SolvecraftExe" IconIndex="0">
<Extension Id="solvecraft" ContentType="application/x-solvecraft">
<Verb Id="open" Command="Open" TargetFile="SolvecraftExe" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
<!-- STEP, IGES and meshes: offered under "Open with", never forced as the default. -->
<ProgId Id="Solvecraft.Model" Description="3D model" Icon="SolvecraftExe" IconIndex="0" />
<RegistryValue Root="HKLM" Key="Software\Classes\Solvecraft.Model\shell\open\command" Value="&quot;[#SolvecraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.step\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.stp\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.igs\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.iges\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.3mf\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.stl\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
<!-- "Open with" app list and the Run dialog (Win+R "solvecraft"). -->
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe" Name="FriendlyAppName" Value="SolveCraft" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\shell\open\command" Value="&quot;[#SolvecraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".solvecraft" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".step" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".stp" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".igs" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".iges" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".3mf" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".stl" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\solvecraft.exe" Value="[#SolvecraftExe]" Type="string" />
<RegistryValue Root="HKLM" Key="Software\SolveCraft" Name="InstallDir" Value="[INSTALLFOLDER]" Type="string" />
</Component>
<Component Id="SolvecraftCli">
<File Id="SolvecraftCliExe" Source="$(var.BinDir)\solvecraft-cli.exe" KeyPath="yes" />
</Component>
<Component Id="SolvecraftLicences">
<File Id="LicenseMit" Source="$(var.BinDir)\LICENSE-MIT" KeyPath="yes" />
<File Id="LicenseApache" Source="$(var.BinDir)\LICENSE-APACHE" />
<File Id="Notice" Source="$(var.BinDir)\NOTICE" />
<File Id="ThirdParty" Source="$(var.BinDir)\THIRD-PARTY-LICENSES.txt" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>