diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml new file mode 100644 index 00000000..addead6b --- /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, +# packaging shell/PowerShell scripts, and the Linux desktop/AppStream/Flatpak metadata. +name: Packaging lint + +on: + push: + branches: [main, release] + paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"] + pull_request: + paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"] + 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 + - 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/vectorcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* + desktop-file-validate packaging/linux/ai.storyteller.vectorcraft.desktop + sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.vectorcraft.metainfo.xml" + appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.vectorcraft.metainfo.xml" + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.vectorcraft.yml')); assert m['id'] == 'ai.storyteller.vectorcraft' and m['command'] == 'vectorcraft'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..70ebb9c6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,285 @@ +# Release pipeline: every push to `release` builds signed installers for every platform and +# creates or updates the draft GitHub Release "VectorCraft v". See docs/releasing.md. +# +# All jobs run in the `release` environment (restricted to the `release` branch), which holds +# the signing secrets. Every secret is optional: missing ones produce unsigned artifacts and a +# warning, never a failed build. +name: Release + +on: + push: + branches: [release] + workflow_dispatch: + inputs: + version: + description: "Version override for a test run, e.g. 0.2.0-rc.1 (empty: Cargo.toml)" + type: string + default: "" + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + # Never cancel a run mid-notarization or mid-upload; a newer push waits its turn. + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 + VECTORCRAFT_BUILD_SHA: ${{ github.sha }} + INPUT_VERSION: ${{ inputs.version }} + +jobs: + version: + name: version + runs-on: ubuntu-latest + environment: release + outputs: + version: ${{ steps.v.outputs.version }} + date: ${{ steps.v.outputs.date }} + prerelease: ${{ steps.v.outputs.prerelease }} + steps: + - uses: actions/checkout@v4 + - id: v + name: Resolve version + run: | + . packaging/env.sh + v="${INPUT_VERSION#v}" + v="${v:-$VERSION}" + if ! printf '%s' "$v" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then + echo "::error::'$v' is not a version like 1.2.3 or 1.2.3-rc.1" + exit 1 + fi + case "$v" in *-*) pre=true ;; *) pre=false ;; esac + { + echo "version=$v" + echo "date=$(date -u +%Y-%m-%d)" + echo "prerelease=$pre" + } >>"$GITHUB_OUTPUT" + echo "### VectorCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + + macos: + name: macOS universal + needs: version + runs-on: macos-15 + environment: release + env: + VECTORCRAFT_VERSION: ${{ needs.version.outputs.version }} + VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin, x86_64-apple-darwin + - uses: Swatinem/rust-cache@v2 + with: + key: release-macos-universal + - name: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$VECTORCRAFT_VERSION" + - name: Import signing certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: packaging/macos/import-cert.sh + - name: Build, sign, notarize + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: packaging/macos/package.sh --arch universal + - uses: actions/upload-artifact@v4 + with: + name: release-macos + path: dist/release/* + if-no-files-found: error + - name: Remove temporary keychain + if: always() + run: | + if [ -n "${MACOS_KEYCHAIN:-}" ]; then security delete-keychain "$MACOS_KEYCHAIN" || true; fi + + windows: + name: Windows ${{ matrix.arch }} + needs: version + runs-on: windows-latest + environment: release + strategy: + fail-fast: false + matrix: + include: + - { arch: x64, target: x86_64-pc-windows-msvc } + - { arch: x86, target: i686-pc-windows-msvc } + env: + VECTORCRAFT_VERSION: ${{ needs.version.outputs.version }} + VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + key: release-windows-${{ matrix.arch }} + - name: Apply version override + if: inputs.version != '' + shell: bash + run: cargo xtask version set "$VECTORCRAFT_VERSION" + - name: Install WiX + shell: pwsh + run: dotnet tool install --global wix --version 5.0.2 + - name: Build, sign, package + shell: pwsh + env: + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} + AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} + AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} + 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 + + linux: + name: Linux ${{ matrix.arch }} + needs: version + # The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35 (Ubuntu 22.04+, + # Debian 12+, Fedora 36+, RHEL 10). See docs/releasing.md. + runs-on: ${{ matrix.runner }} + environment: release + 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: + VECTORCRAFT_VERSION: ${{ needs.version.outputs.version }} + VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + NFPM_VERSION: 2.47.0 + steps: + - uses: actions/checkout@v4 + - name: Install build and packaging tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev \ + desktop-file-utils appstream python3-yaml + 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: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$VECTORCRAFT_VERSION" + - 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/vectorcraft$|metainfo|applications' + ldd target/release/vectorcraft + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.vectorcraft.yml')); assert m['id'] == 'ai.storyteller.vectorcraft'" + for appimage in dist/release/*.AppImage; do + chmod +x "$appimage" + APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version + done + - uses: actions/upload-artifact@v4 + with: + name: release-linux-${{ matrix.arch }} + path: dist/release/* + if-no-files-found: error + + web: + name: Web (wasm) + needs: version + runs-on: ubuntu-latest + environment: release + env: + VECTORCRAFT_VERSION: ${{ needs.version.outputs.version }} + VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + 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: | + 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 + trunk --version + - name: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$VECTORCRAFT_VERSION" + - name: Build and zip + run: packaging/web/package.sh + - uses: actions/upload-artifact@v4 + with: + name: release-web + path: dist/release/* + if-no-files-found: error + + release: + name: Draft GitHub Release + needs: [version, macos, windows, linux, web] + runs-on: ubuntu-latest + environment: release + 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/download-artifact@v4 + with: + pattern: release-* + path: artifacts + merge-multiple: true + - name: Checksums + working-directory: artifacts + run: | + sha256sum -- * | tee SHA256SUMS.txt + - name: Create or update the draft release + working-directory: artifacts + run: | + tag="v$VERSION" + title="VectorCraft v$VERSION" + pre=() + if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi + if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then + if [ "$draft" != "true" ]; then + echo "::error::Release $tag is already published. Bump the version (cargo xtask version set) before pushing to release." + exit 1 + fi + echo "Updating draft $tag: replacing its assets" + gh release view "$tag" --json assets --jq '.assets[].name' | while read -r asset; do + gh release delete-asset "$tag" "$asset" --yes + done + gh release edit "$tag" --draft --title "$title" --target "$GITHUB_SHA" --prerelease="$PRERELEASE" + gh release upload "$tag" --clobber -- * + else + echo "Creating draft $tag" + gh release create "$tag" --draft --title "$title" --target "$GITHUB_SHA" --generate-notes "${pre[@]}" -- * + fi + { + echo "Draft release **$title** at \`${GITHUB_SHA::9}\`:" + echo + sed 's/^/ /' SHA256SUMS.txt + } >>"$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index 111e8e5d..d4076fe6 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,8 @@

> [!NOTE] -> **ArtCraft is a community of artists from all walks of life.** Painters, photographers, -> filmmakers, illustrators, designers, animators, hobbyists, and people who picked up a pencil -> last week. If you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).** +> **ArtCraft is a community of artists from all walks of life.** Digital, generative, music, +> games — if you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).**

A look around · diff --git a/packaging/.shellcheckrc b/packaging/.shellcheckrc new file mode 100644 index 00000000..36c19d06 --- /dev/null +++ b/packaging/.shellcheckrc @@ -0,0 +1,3 @@ +# shellcheck config for the packaging scripts (CI: .github/workflows/packaging-lint.yml). +source-path=SCRIPTDIR +external-sources=true diff --git a/packaging/env.sh b/packaging/env.sh new file mode 100644 index 00000000..c70004ff --- /dev/null +++ b/packaging/env.sh @@ -0,0 +1,58 @@ +# shellcheck shell=bash +# Shared setup for the packaging scripts. Source it: `. "$(dirname "$0")/../env.sh"`. +# +# Exports: +# ROOT workspace root +# VERSION [workspace.package] version from Cargo.toml (override: VECTORCRAFT_VERSION) +# DIST output directory for release artifacts (default: $ROOT/dist/release) +# VECTORCRAFT_BUILD_SHA git commit baked into the binaries (see crates/engine/src/build_info.rs) +# VECTORCRAFT_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 exactly one place: `[workspace.package] version` in the root Cargo.toml. +# (`cargo xtask version` prints the same thing; awk avoids compiling xtask here.) +workspace_version() { + awk ' + /^\[/ { in_pkg = ($0 == "[workspace.package]") ; next } + in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit } + ' "$ROOT/Cargo.toml" +} + +VERSION="${VECTORCRAFT_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" +export DIST + +if [ -z "${VECTORCRAFT_BUILD_SHA:-}" ]; then + VECTORCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)" +fi +export VECTORCRAFT_BUILD_SHA +export VECTORCRAFT_BUILD_DATE="${VECTORCRAFT_BUILD_DATE:-$(date -u +%Y-%m-%d)}" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" + +# Emit a GitHub Actions warning (plain stderr outside Actions). +warn() { + if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi +} + +# Copy licence and readme files that exist into a package directory. +copy_docs() { + local dest="$1" f + for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do + if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi + done +} + +# Portable SHA-256 of a file (prints just the hash). +sha256() { + if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi +} diff --git a/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in b/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in new file mode 100644 index 00000000..ade490d3 --- /dev/null +++ b/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in @@ -0,0 +1,61 @@ + + + + ai.storyteller.vectorcraft + CC0-1.0 + MIT OR Apache-2.0 + VectorCraft +

Draw and edit vector illustrations, SVG and PDF + + Learning Machines LLC + + +

+ VectorCraft is a vector graphics editor. Draw and edit vector illustrations, SVG and PDF +

+
+ ai.storyteller.vectorcraft.desktop + ai.storyteller.vectorcraft + + https://getartcraft.com/apps/vectorcraft + https://github.com/storytold/vectorcraft/issues + https://github.com/storytold/vectorcraft + https://discord.gg/artcraft + + Graphics + RasterGraphics + Photography + + + photo + image editor + psd + layers + + + + Editing The Great Wave off Kanagawa with type layers and adjustment layers + https://raw.githubusercontent.com/storytold/vectorcraft/main/docs/images/vectorcraft-demo.jpg + + + + #9fd0ff + #1f64ba + + + 760 + + + keyboard + pointing + tablet + + + vectorcraft + + + + + + diff --git a/packaging/linux/ai.storyteller.vectorcraft.mime.xml b/packaging/linux/ai.storyteller.vectorcraft.mime.xml new file mode 100644 index 00000000..be4dcc22 --- /dev/null +++ b/packaging/linux/ai.storyteller.vectorcraft.mime.xml @@ -0,0 +1,3 @@ + + + diff --git a/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml b/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml new file mode 100644 index 00000000..11038201 --- /dev/null +++ b/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml @@ -0,0 +1,56 @@ +# Flatpak manifest for VectorCraft, ready for a Flathub submission. +# +# Flatpak builds run offline, so the crate sources must be vendored first. From the repo root: +# +# curl -fsSLO https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py +# python3 flatpak-cargo-generator.py Cargo.lock -o packaging/linux/flatpak/cargo-sources.json +# flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.vectorcraft.yml +# flatpak run ai.storyteller.vectorcraft +# +# For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. +id: ai.storyteller.vectorcraft +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable +command: vectorcraft +finish-args: + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --device=dri + # Open/Save dialogs use the file-chooser portal (rfd), and drag-and-drop uses the document + # portal, so broad filesystem access isn't needed. Pictures is granted for opening files by + # path from the command line. + - --filesystem=xdg-pictures +build-options: + append-path: /usr/lib/sdk/rust-stable/bin +modules: + - name: vectorcraft + buildsystem: simple + build-options: + env: + CARGO_HOME: /run/build/vectorcraft/cargo + CARGO_NET_OFFLINE: 'true' + build-commands: + - cargo --offline build --release --locked -p vectorcraft + - install -Dm755 target/release/vectorcraft /app/bin/vectorcraft + - install -Dm644 packaging/linux/ai.storyteller.vectorcraft.desktop /app/share/applications/ai.storyteller.vectorcraft.desktop + - install -Dm644 packaging/linux/ai.storyteller.vectorcraft.mime.xml /app/share/mime/packages/ai.storyteller.vectorcraft.xml + - mkdir -p /app/share/icons && cp -R assets/app-icon/hicolor /app/share/icons/ + - >- + version=$(awk '/^\[/{p=($0=="[workspace.package]");next} p&&$1=="version"{gsub(/[" ]/,"",$3);print $3;exit}' Cargo.toml); + install -d /app/share/metainfo; + sed -e "s/@VERSION@/$version/" -e "s/@DATE@/$(date -u +%Y-%m-%d)/" + packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.vectorcraft.metainfo.xml + sources: + - type: dir + path: ../../.. + skip: + - target + - dist + - corpus + - build-flatpak + - .flatpak-builder + - cargo-sources.json diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml new file mode 100644 index 00000000..572ed6fa --- /dev/null +++ b/packaging/linux/nfpm.yaml @@ -0,0 +1,70 @@ +# .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. +# +# nfpm package -f packaging/linux/nfpm.yaml -p deb -t dist/release/ +name: vectorcraft +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: ${VECTORCRAFT_MAINTAINER} +vendor: Learning Machines LLC +homepage: https://getartcraft.com/apps/vectorcraft +license: MIT OR Apache-2.0 +description: |- + Open-source, native image editor that works the way Photoshop users expect. + Layers, masks, adjustment layers, layer styles, type and brushes; opens and + saves layered PSD/PSB files; 8/16/32-bit documents; GPU compositing (wgpu). + Includes vectorcraft-cli, a headless converter and command runner. + +# The binaries link only glibc and libgcc_s directly. Windowing (X11/Wayland, xkbcommon) and the +# GPU (Vulkan, EGL/GL) are loaded at runtime, so they are dependencies/recommendations here +# rather than ELF NEEDED entries. +overrides: + deb: + depends: + - libc6 (>= 2.35) + - libgcc-s1 + - libxkbcommon0 + - libvulkan1 | libegl1 + recommends: + - libwayland-client0 + - libx11-6 + - libx11-xcb1 + - libxcursor1 + - libxi6 + - libxrandr2 + - mesa-vulkan-drivers + - xdg-desktop-portal + rpm: + depends: + - libxkbcommon + - (vulkan-loader or libglvnd-egl) + recommends: + - libwayland-client + - libX11 + - libXcursor + - libXi + - libXrandr + - mesa-vulkan-drivers + - xdg-desktop-portal + +contents: + - src: ${STAGE}/usr/bin/vectorcraft + dst: /usr/bin/vectorcraft + file_info: { mode: 0755 } + - src: ${STAGE}/usr/bin/vectorcraft-cli + dst: /usr/bin/vectorcraft-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 100644 index 00000000..1644d6a7 --- /dev/null +++ b/packaging/linux/package.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Build and package VectorCraft for Linux ( is x86_64 or aarch64): +# +# $DIST/vectorcraft--linux-.AppImage any distro with glibc >= the build host's +# $DIST/vectorcraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... +# $DIST/vectorcraft--linux-.rpm Fedora, openSUSE, RHEL, ... +# $DIST/vectorcraft--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. +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" +APP_ID=ai.storyteller.vectorcraft + +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,15p' "$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 VECTORCRAFT_MAINTAINER="${VECTORCRAFT_MAINTAINER:-VectorCraft maintainers }" +BASENAME="vectorcraft-$VERSION-linux-$ARCH" + +echo "==> VectorCraft $VERSION for Linux $ARCH ($FORMATS)" + +if [ "$SKIP_BUILD" = 0 ]; then + (cd "$ROOT" && cargo build --release --locked -p vectorcraft -p vectorcraft-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/vectorcraft" "$STAGE/usr/bin/vectorcraft" +install -Dm755 "$BIN/vectorcraft-cli" "$STAGE/usr/bin/vectorcraft-cli" +strip "$STAGE/usr/bin/vectorcraft" "$STAGE/usr/bin/vectorcraft-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@/$VECTORCRAFT_BUILD_DATE/g" \ + "$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" +mkdir -p "$STAGE/usr/share/icons" +cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/" +mkdir -p "$STAGE/usr/share/doc/vectorcraft" +copy_docs "$STAGE/usr/share/doc/vectorcraft" + +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/VectorCraft.AppDir" + cp -R "$STAGE" "$APPDIR" + mv "$APPDIR/usr/share/doc" "$WORK/doc-unused" + ln -s usr/bin/vectorcraft "$APPDIR/AppRun" + cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop" + cp "$ROOT/assets/app-icon/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" + # Extract-and-run: works without FUSE (containers, CI). The output embeds the static runtime, + # so users don't need libfuse2 either. + ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream "$APPDIR" "$OUT" + echo "wrote $OUT" +fi + +"$STAGE/usr/bin/vectorcraft-cli" --version +echo "==> done" +ls -lh "$DIST" diff --git a/packaging/linux/postinst.sh b/packaging/linux/postinst.sh new file mode 100644 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..95819c78 --- /dev/null +++ b/packaging/macos/Info.plist.in @@ -0,0 +1,149 @@ + + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + VectorCraft + CFBundleName + VectorCraft + CFBundleExecutable + VectorCraft + CFBundleIdentifier + ai.storyteller.vectorcraft + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + @SHORT_VERSION@ + CFBundleVersion + @SHORT_VERSION@ + VectorCraftVersion + @VERSION@ + VectorCraftBuildCommit + @BUILD_SHA@ + CFBundleIconFile + VectorCraft + LSApplicationCategoryType + public.app-category.graphics-design + + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + NSSupportsAutomaticGraphicsSwitching + + NSRequiresAquaSystemAppearance + + NSHumanReadableCopyright + Copyright © the VectorCraft authors. MIT OR Apache-2.0. + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + ai.storyteller.vectorcraft.document + UTTypeDescription + VectorCraft Document + UTTypeConformsTo + + public.data + public.image + + UTTypeIconFile + VectorCraft + UTTypeTagSpecification + + public.filename-extension + pcraft + public.mime-type + application/x-vectorcraft + + + + + UTImportedTypeDeclarations + + + UTTypeIdentifier + com.adobe.photoshop-large-image + UTTypeDescription + Photoshop Large Document + UTTypeConformsTo + + public.data + public.image + + UTTypeTagSpecification + + public.filename-extension + psb + + + + UTTypeIdentifier + ai.storyteller.vectorcraft.qoi + UTTypeDescription + QOI Image + UTTypeConformsTo + + public.data + public.image + + UTTypeTagSpecification + + public.filename-extension + qoi + + + + + + + CFBundleTypeName + Photoshop Document + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + LSItemContentTypes + + com.adobe.photoshop-image + com.adobe.photoshop-large-image + + + + CFBundleTypeName + Image + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + LSItemContentTypes + + public.png + public.jpeg + public.tiff + org.webmproject.webp + com.compuserve.gif + com.microsoft.bmp + com.truevision.tga-image + com.microsoft.ico + public.pbm + com.ilm.openexr-image + public.radiance + public.avif + ai.storyteller.vectorcraft.qoi + + + + + diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist new file mode 100644 index 00000000..4dff2c82 --- /dev/null +++ b/packaging/macos/entitlements.plist @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packaging/macos/import-cert.sh b/packaging/macos/import-cert.sh new file mode 100644 index 00000000..3940a64a --- /dev/null +++ b/packaging/macos/import-cert.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# CI only: import the Developer ID certificate into a temporary keychain so codesign can use it. +# +# Inputs (env): APPLE_CERTIFICATE (base64 .p12), APPLE_CERTIFICATE_PASSWORD, KEYCHAIN_PASSWORD +# (optional; random if unset), RUNNER_TEMP (or TMPDIR). +# Outputs: MACOS_SIGN_IDENTITY (SHA-1 of the identity) and MACOS_KEYCHAIN, appended to +# $GITHUB_ENV when running in Actions, otherwise printed. +# +# Missing certificate: prints a warning and exits 0, so package.sh falls back to ad-hoc signing. +set -euo pipefail +umask 077 +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" + +if [ -z "${APPLE_CERTIFICATE:-}" ]; then + warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized" + exit 0 +fi + +TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +KEYCHAIN="$TMP/vectorcraft-signing.keychain-db" +CERT="$TMP/vectorcraft-signing.p12" +KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}" + +trap 'rm -f "$CERT"' EXIT +printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT" +security create-keychain -p "$KC_PASS" "$KEYCHAIN" +security set-keychain-settings -lut 21600 "$KEYCHAIN" +security unlock-keychain -p "$KC_PASS" "$KEYCHAIN" +security import "$CERT" -P "${APPLE_CERTIFICATE_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security -t cert -f pkcs12 -k "$KEYCHAIN" +rm -f "$CERT" +# Let codesign use the key without a UI prompt. +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KEYCHAIN" >/dev/null +# Put it on the search list (keeping the login keychain) so codesign and notarytool find it. +# shellcheck disable=SC2046 +security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + +IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')" +if [ -z "$IDENTITY" ]; then + security find-identity -v -p codesigning "$KEYCHAIN" >&2 + echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2 + exit 1 +fi +echo "Imported signing identity $IDENTITY" +if [ -n "${GITHUB_ENV:-}" ]; then + { echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV" +else + echo "MACOS_SIGN_IDENTITY=$IDENTITY" + echo "MACOS_KEYCHAIN=$KEYCHAIN" +fi diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh new file mode 100644 index 00000000..6c07e5c9 --- /dev/null +++ b/packaging/macos/package.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# Build, sign and (optionally) notarize the macOS release artifacts: +# +# $DIST/vectorcraft--macos-.dmg VectorCraft.app on a drag-to-Applications DMG +# $DIST/vectorcraft-cli--macos-.zip the headless CLI +# +# Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build] +# +# Signing (env): +# MACOS_SIGN_IDENTITY codesign identity (name or SHA-1). Default "-" = ad-hoc (local testing; +# Gatekeeper will reject the result on other Macs). In CI, import-cert.sh sets it. +# MACOS_KEYCHAIN keychain holding the identity (optional) +# Notarization (env; all three needed, and a real identity): +# APPLE_ID, APPLE_PASSWORD (app-specific password), APPLE_TEAM_ID +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,16p' "$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 +IDENTITY="${MACOS_SIGN_IDENTITY:--}" +SHORT_VERSION="${VERSION%%-*}" +WORK="$CARGO_TARGET_DIR/macos-package" +APP="$WORK/VectorCraft.app" +DMG="$DIST/vectorcraft-$VERSION-macos-$ARCH.dmg" +CLI_ZIP="$DIST/vectorcraft-cli-$VERSION-macos-$ARCH.zip" + +NOTARIZE=0 +if [ "$IDENTITY" = "-" ]; then + warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized" +elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then + NOTARIZE=1 +else + warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized" +fi + +echo "==> VectorCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" + +# ---- build ------------------------------------------------------------------------------------- +if [ "$SKIP_BUILD" = 0 ]; then + args=() + for t in "${TARGETS[@]}"; do args+=(--target "$t"); done + (cd "$ROOT" && cargo build --release --locked -p vectorcraft -p vectorcraft-cli "${args[@]}") +fi + +rm -rf "$WORK" +mkdir -p "$WORK/bin" +for bin in vectorcraft vectorcraft-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 + +# ---- signing helpers --------------------------------------------------------------------------- +sign() { + # Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped. + local ts=(--timestamp) + [ "$IDENTITY" = "-" ] && ts=(--timestamp=none) + local kc=() + [ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN") + codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@" +} + +notarize() { + local file="$1" out id status + echo "==> notarizing $(basename "$file") (this can take a few minutes)" + out="$(xcrun notarytool submit "$file" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" --wait --timeout 1h --output-format json)" || true + echo "$out" + id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)" + status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)" + if [ "$status" != "Accepted" ]; then + if [ -n "$id" ]; then + xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true + fi + echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2 + exit 1 + fi +} + +# ---- VectorCraft.app ---------------------------------------------------------------------------- +echo "==> assembling $APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" +# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile). +cp "$WORK/bin/vectorcraft" "$APP/Contents/MacOS/VectorCraft" +cp "$ROOT/assets/app-icon/vectorcraft.icns" "$APP/Contents/Resources/VectorCraft.icns" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ + -e "s/@BUILD_SHA@/${VECTORCRAFT_BUILD_SHA:-unknown}/g" \ + "$HERE/Info.plist.in" >"$APP/Contents/Info.plist" +plutil -lint "$APP/Contents/Info.plist" +printf 'APPL????' >"$APP/Contents/PkgInfo" + +# Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). +# Today the only nested code is the main executable; frameworks/helpers would be signed here too. +sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/VectorCraft" +sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP" +codesign --verify --strict --deep --verbose=2 "$APP" + +if [ "$NOTARIZE" = 1 ]; then + ditto -c -k --keepParent "$APP" "$WORK/VectorCraft-notarize.zip" + notarize "$WORK/VectorCraft-notarize.zip" + xcrun stapler staple "$APP" + xcrun stapler validate "$APP" + spctl --assess --type execute -vvv "$APP" +fi + +# ---- DMG --------------------------------------------------------------------------------------- +echo "==> building $DMG" +STAGE="$WORK/dmg" +mkdir -p "$STAGE" +ditto "$APP" "$STAGE/VectorCraft.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") and hangs in sandboxed sessions. +hdiutil makehybrid -hfs -hfs-volume-name "VectorCraft $VERSION" -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" +sign "$DMG" +codesign --verify --strict --verbose=2 "$DMG" +if [ "$NOTARIZE" = 1 ]; then + notarize "$DMG" + xcrun stapler staple "$DMG" + xcrun stapler validate "$DMG" + spctl --assess --type open --context context:primary-signature -vvv "$DMG" +fi + +# ---- CLI --------------------------------------------------------------------------------------- +echo "==> building $CLI_ZIP" +CLI_DIR="$WORK/vectorcraft-cli-$VERSION-macos-$ARCH" +mkdir -p "$CLI_DIR" +cp "$WORK/bin/vectorcraft-cli" "$CLI_DIR/" +copy_docs "$CLI_DIR" +sign --options runtime "$CLI_DIR/vectorcraft-cli" +codesign --verify --strict --verbose=2 "$CLI_DIR/vectorcraft-cli" +rm -f "$CLI_ZIP" +ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP" +# A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online. +if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi + +"$WORK/bin/vectorcraft-cli" --version +echo "==> done" +ls -lh "$DMG" "$CLI_ZIP" diff --git a/packaging/web/.htaccess b/packaging/web/.htaccess new file mode 100644 index 00000000..7b7e6e18 --- /dev/null +++ b/packaging/web/.htaccess @@ -0,0 +1,16 @@ +# Apache (see HOSTING.md). Needs mod_mime; mod_deflate and mod_headers are optional. +AddType application/wasm .wasm +AddType text/javascript .js + + + AddOutputFilterByType DEFLATE application/wasm text/javascript text/html + + + + + Header set Cache-Control "public, max-age=31536000, immutable" + + + Header set Cache-Control "no-cache" + + diff --git a/packaging/web/README.md b/packaging/web/README.md new file mode 100644 index 00000000..b39ec0e9 --- /dev/null +++ b/packaging/web/README.md @@ -0,0 +1,89 @@ +# Hosting VectorCraft for the web + +`vectorcraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a +static site in `vectorcraft-web-/`: + +| File | What it is | +|---|---| +| `index.html` | The page. It loads everything through relative URLs. | +| `vectorcraft-web-.js` | wasm-bindgen glue (generated, ES module) | +| `vectorcraft-web-_bg.wasm` | The app, about 13 MB, or 5 MB with compression | +| `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | + +There is no server-side code. Upload the folder's contents anywhere that serves static files. + +## Any path works + +All URLs in `index.html` are relative (`public_url = "./"` in `apps/vectorcraft-web/Trunk.toml`), +so the site works at a domain root (`https://example.com/`), under a prefix +(`https://example.com/tools/vectorcraft/`) and from a CDN bucket. The asset names carry a content +hash, so they can be cached forever. Only `index.html` needs revalidation. + +## Required server settings + +- **MIME type:** serve `.wasm` as `application/wasm`. Browsers refuse to stream-compile it under + any other type, and the app then loads slowly or not at all. Serve `.js` as `text/javascript`. + Most hosts already do both. For nginx, check that `mime.types` has `application/wasm wasm;`. +- **Compression:** turn on gzip or Brotli for `.wasm`, `.js` and `.html`. That takes the + download from about 13 MB to about 5 MB. You can also precompress (`brotli -k *.wasm`) and let + the server send `Content-Encoding: br`. +- **Caching:** `Cache-Control: public, max-age=31536000, immutable` on the hashed `.wasm` and + `.js` files, and `no-cache` on `index.html`. +- **HTTPS:** WebGPU (and the clipboard) only work in a secure context, which means `https://` + or `http://localhost`. Over plain HTTP elsewhere, the app falls back to WebGL2. +- **No special isolation headers:** VectorCraft doesn't use `SharedArrayBuffer`, so it doesn't + need `Cross-Origin-Opener-Policy` or `Cross-Origin-Embedder-Policy`. If your site already sends + COEP `require-corp`, also send `Cross-Origin-Resource-Policy: same-origin` (or `cross-origin` + when the files live on a CDN) on the app's files. + +nginx example: + +```nginx +location /vectorcraft/ { + types { application/wasm wasm; text/javascript js; text/html html; } + gzip on; + gzip_types application/wasm text/javascript text/html; + location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } + location ~* index\.html$ { add_header Cache-Control "no-cache"; } +} +``` + +Local test: `python3 -m http.server 8765` inside the folder, then open http://localhost:8765/. + +## Embedding in a page (iframe) + +```html + +``` + +- The app fills the iframe and follows its size, so size the iframe and not the app. +- Keyboard shortcuts go to the iframe after the user clicks into it, as with any embedded app. +- **Cross-origin embeds** work. Preferences are kept in the iframe's `localStorage`. Browsers + that partition or block third-party storage may forget them between visits, and the app + then starts with defaults. +- **Sandboxed iframes** need at least + `sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"`. Without + `allow-same-origin` there's no storage. Without `allow-downloads`, Save and Export (browser + downloads) are blocked. +- Don't send `X-Frame-Options: DENY` or a `frame-ancestors` CSP that excludes the embedding page. + +## Renderer selection and fallback flags + +VectorCraft renders with wgpu. It uses **WebGPU** when the browser has it and falls back to +**WebGL2** on its own. URL query flags override this, and they work on the iframe `src` too: + +| Flag | Effect | +|---|---| +| *(none)* | WebGPU if available, otherwise WebGL2 | +| `?webgl` | Force the WebGL2 backend (useful when a WebGPU driver misbehaves) | +| `?cpu` | Force the CPU canvas path (slowest, most compatible) | + +For example: `