From 0b394ad433812c130595d296e6ad12f8231d9d6b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 6 Oct 2026 00:45:34 -0400 Subject: [PATCH] Web build and release CI: slidecraft-web, packaging, release workflow - apps/slidecraft-web: SlideApp in the browser via eframe's web runner (trunk; WebGPU with WebGL2 fallback). Opens the sample deck (?blank for empty, ?show to present, ?webgl to force WebGL2). Open/Insert use the browser file picker through Services::open_async + inbox; Save/Export download; dropped files are read asynchronously. - ui-egui: native-only synchronous drop handling (wasm uses the inbox), real clock on wasm (js_sys::Date::now), About shows build commit/date when SLIDECRAFT_BUILD_SHA/DATE are set. - .github/workflows/release.yml (push to `release` / workflow_dispatch): macOS universal signed+notarized dmg, Windows x64/x86 signed msi + portable zip, Linux AppImage/deb/rpm/tar.gz (x86_64, aarch64), Flatpak bundle, FreeBSD tar.gz (vmactions), web zip; draft GitHub Release "SlideCraft v" from [workspace.package] version. Same secret names as DesignCraft. - freebsd.yml and packaging-lint.yml CI, ported from DesignCraft. - packaging/: macOS (Info.plist, entitlements, cert import), Windows (WiX with new UpgradeCode, .slidecraft ProgId, Open-with for pptx), Linux (.desktop, AppStream, MIME, nfpm, Flatpak), FreeBSD and web scripts; shared setup in packaging/common.sh. - apps/slidecraft/build.rs embeds icon + VERSIONINFO on Windows; slidecraft.icns/.ico generated from the committed PNGs by packaging/icons.sh. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/freebsd.yml | 56 +++ .github/workflows/packaging-lint.yml | 57 +++ .github/workflows/release.yml | 414 ++++++++++++++++++ .gitignore | 3 + ATTRIBUTION.md | 1 + Cargo.lock | 40 +- ROADMAP.md | 7 +- apps/slidecraft-web/Cargo.toml | 30 ++ apps/slidecraft-web/Trunk.toml | 10 + apps/slidecraft-web/index.html | 20 + apps/slidecraft-web/src/main.rs | 31 ++ apps/slidecraft-web/src/web.rs | 176 ++++++++ apps/slidecraft/Cargo.toml | 4 + apps/slidecraft/build.rs | 30 ++ assets/app-icon/slidecraft.icns | Bin 0 -> 110107 bytes assets/app-icon/slidecraft.ico | Bin 0 -> 22252 bytes crates/ui-egui/Cargo.toml | 3 + crates/ui-egui/src/dialogs.rs | 17 +- crates/ui-egui/src/lib.rs | 6 +- packaging/.shellcheckrc | 3 + packaging/common.sh | 70 +++ packaging/freebsd/package.sh | 44 ++ packaging/icons.sh | 48 ++ .../linux/ai.storyteller.slidecraft.desktop | 14 + .../ai.storyteller.slidecraft.metainfo.xml.in | 59 +++ .../linux/ai.storyteller.slidecraft.mime.xml | 9 + .../flatpak/ai.storyteller.slidecraft.yml | 56 +++ packaging/linux/nfpm.yaml | 71 +++ packaging/linux/package.sh | 120 +++++ packaging/linux/postinst.sh | 13 + packaging/macos/Info.plist.in | 95 ++++ packaging/macos/entitlements.plist | 9 + packaging/macos/import-cert.sh | 50 +++ packaging/macos/package.sh | 164 +++++++ packaging/web/.htaccess | 16 + packaging/web/README.md | 88 ++++ packaging/web/_headers | 13 + packaging/web/package.sh | 39 ++ packaging/windows/package.ps1 | 131 ++++++ packaging/windows/sign.ps1 | 95 ++++ packaging/windows/slidecraft.wxs | 80 ++++ 41 files changed, 2185 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/freebsd.yml create mode 100644 .github/workflows/packaging-lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 apps/slidecraft-web/Cargo.toml create mode 100644 apps/slidecraft-web/Trunk.toml create mode 100644 apps/slidecraft-web/index.html create mode 100644 apps/slidecraft-web/src/main.rs create mode 100644 apps/slidecraft-web/src/web.rs create mode 100644 apps/slidecraft/build.rs create mode 100644 assets/app-icon/slidecraft.icns create mode 100644 assets/app-icon/slidecraft.ico create mode 100644 packaging/.shellcheckrc create mode 100644 packaging/common.sh create mode 100755 packaging/freebsd/package.sh create mode 100755 packaging/icons.sh create mode 100644 packaging/linux/ai.storyteller.slidecraft.desktop create mode 100644 packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in create mode 100644 packaging/linux/ai.storyteller.slidecraft.mime.xml create mode 100644 packaging/linux/flatpak/ai.storyteller.slidecraft.yml create mode 100644 packaging/linux/nfpm.yaml create mode 100755 packaging/linux/package.sh create mode 100755 packaging/linux/postinst.sh create mode 100644 packaging/macos/Info.plist.in create mode 100644 packaging/macos/entitlements.plist create mode 100755 packaging/macos/import-cert.sh create mode 100755 packaging/macos/package.sh create mode 100644 packaging/web/.htaccess create mode 100644 packaging/web/README.md create mode 100644 packaging/web/_headers create mode 100755 packaging/web/package.sh create mode 100644 packaging/windows/package.ps1 create mode 100644 packaging/windows/sign.ps1 create mode 100644 packaging/windows/slidecraft.wxs diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml new file mode 100644 index 0000000..7b66f33 --- /dev/null +++ b/.github/workflows/freebsd.yml @@ -0,0 +1,56 @@ +# Builds and tests the workspace on FreeBSD, inside a FreeBSD VM on a Linux runner +# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter. +name: FreeBSD + +on: + push: + branches: [main] + pull_request: + paths: + - Cargo.toml + - Cargo.lock + - "**/Cargo.toml" + - "crates/fonts/**" + - .github/workflows/freebsd.yml + workflow_dispatch: + +concurrency: + group: freebsd-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + freebsd: + name: FreeBSD 14 (build + test) + runs-on: ubuntu-latest + timeout-minutes: 300 + steps: + - uses: actions/checkout@v4 + # The Japanese font tests run against craft-fonts (an optional build input; they skip without it). + - name: Check out craft-fonts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + repository: storytold/craft-fonts + ref: 43913056ce82a3fdad0ef69f6b88d1dc922a8104 + path: craft-fonts + - name: Build and test in a FreeBSD VM + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 + with: + release: "14.3" + usesh: true + cpu: 4 + mem: 12288 + # Nothing to bring back; copying the target dir out of the VM fails on large builds. + copyback: false + prepare: | + pkg install -y git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib + curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + run: | + . "$HOME/.cargo/env" + # The checkout is synced in under another owner; let tests that call git read it. + git config --global --add safe.directory "*" + rustc --version + CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked + CRAFT_FONTS_DIR="$PWD/craft-fonts" CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml new file mode 100644 index 0000000..d7362dc --- /dev/null +++ b/.github/workflows/packaging-lint.yml @@ -0,0 +1,57 @@ +# 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/slidecraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* + # Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon. + if grep -oE '"$RUNNER_TEMP/ai.storyteller.slidecraft.metainfo.xml" + appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.slidecraft.metainfo.xml" + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.slidecraft.yml')); assert m['id'] == 'ai.storyteller.slidecraft' and m['command'] == 'slidecraft'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b660831 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,414 @@ +# Release pipeline: every push to `release` builds installers for every platform and creates or +# updates the draft GitHub Release "SlideCraft v" (tag v). Recipe: +# craftrules release/playbook.md. +# +# The version has one source: `[workspace.package] version` in Cargo.toml (`cargo xtask version`, +# `cargo xtask version set X.Y.Z`). The app shows it in About and `--version`; the release name +# and tag use it too. workflow_dispatch can override it for a test build (applied to the build). +# +# 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 + SLIDECRAFT_BUILD_SHA: ${{ github.sha }} + INPUT_VERSION: ${{ inputs.version }} + # Optional font input (storytold/craft-fonts docs/integration.md); releases always embed it. + # This commit adds fonts/latin-manifest.txt (the Latin presentation fonts). + CRAFT_FONTS_REF: 43913056ce82a3fdad0ef69f6b88d1dc922a8104 + +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/common.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 "### SlideCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + + macos: + name: macOS universal + needs: version + runs-on: macos-15 + environment: release + env: + SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - name: Check out craft-fonts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + repository: storytold/craft-fonts + ref: ${{ env.CRAFT_FONTS_REF }} + path: craft-fonts + - 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 "$SLIDECRAFT_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: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" + 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: + SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - name: Check out craft-fonts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + repository: storytold/craft-fonts + ref: ${{ env.CRAFT_FONTS_REF }} + path: craft-fonts + - 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 "$SLIDECRAFT_VERSION" + - name: Install WiX + shell: pwsh + run: dotnet tool install --global wix --version 5.0.2 + - name: Build, sign, package + shell: pwsh + env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" + 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). + 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: + SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + NFPM_VERSION: 2.47.0 + steps: + - uses: actions/checkout@v4 + - name: Check out craft-fonts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + repository: storytold/craft-fonts + ref: ${{ env.CRAFT_FONTS_REF }} + path: craft-fonts + - 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 "$SLIDECRAFT_VERSION" + - name: Build and package (AppImage, deb, rpm, tar.gz) + env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" + 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/slidecraft$|metainfo|applications' + ldd target/release/slidecraft + 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 + + flatpak: + name: Linux Flatpak + needs: version + runs-on: ubuntu-latest + environment: release + # Best effort: Flathub builds from source itself (playbook §7); the bundle is a convenience. + continue-on-error: true + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08 + options: --privileged + env: + SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + - name: Apply version override + if: inputs.version != '' + run: | + # No cargo in this container: the same edit `cargo xtask version set` makes. + sed -i "0,/^version = \".*\"/s//version = \"$SLIDECRAFT_VERSION\"/" Cargo.toml + grep -m1 '^version' Cargo.toml + - name: Vendor the crate sources (flatpak builds run offline) + run: | + dnf install -y python3-pip + pip3 install --quiet aiohttp tomlkit + curl -fsSL -o "$RUNNER_TEMP/flatpak-cargo-generator.py" \ + https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py + python3 "$RUNNER_TEMP/flatpak-cargo-generator.py" Cargo.lock -o packaging/linux/flatpak/cargo-sources.json + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + manifest-path: packaging/linux/flatpak/ai.storyteller.slidecraft.yml + bundle: slidecraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak + upload-artifact: false + cache-key: flatpak-builder-${{ github.sha }} + - uses: actions/upload-artifact@v4 + with: + name: release-flatpak + path: slidecraft-*.flatpak + if-no-files-found: error + + freebsd: + name: FreeBSD x86_64 + needs: version + runs-on: ubuntu-latest + environment: release + timeout-minutes: 240 + env: + SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + VERSION_OVERRIDE: ${{ inputs.version }} + steps: + - uses: actions/checkout@v4 + - name: Check out craft-fonts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + repository: storytold/craft-fonts + ref: ${{ env.CRAFT_FONTS_REF }} + path: craft-fonts + - name: Build and package in a FreeBSD VM + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 + with: + release: "14.3" + usesh: true + cpu: 4 + mem: 12288 + envs: SLIDECRAFT_VERSION SLIDECRAFT_BUILD_DATE SLIDECRAFT_BUILD_SHA VERSION_OVERRIDE + # Copies the workspace (with dist/release) back; the target dir lives outside it so the + # copy stays small. + copyback: true + prepare: | + pkg install -y bash git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader fontconfig freetype2 + curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + run: | + . "$HOME/.cargo/env" + git config --global --add safe.directory "*" + export CARGO_TARGET_DIR="$HOME/target" CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 + if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$SLIDECRAFT_VERSION"; fi + bash packaging/freebsd/package.sh + - uses: actions/upload-artifact@v4 + with: + name: release-freebsd + path: dist/release/* + if-no-files-found: error + + web: + name: Web (wasm) + needs: version + runs-on: ubuntu-latest + environment: release + env: + SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + TRUNK_VERSION: 0.21.14 + steps: + - uses: actions/checkout@v4 + - name: Check out craft-fonts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + repository: storytold/craft-fonts + ref: ${{ env.CRAFT_FONTS_REF }} + path: craft-fonts + - 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 "$SLIDECRAFT_VERSION" + - name: Build and zip + env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" + 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, flatpak, freebsd, web] + # The installers are required; the Flatpak bundle and FreeBSD tarball are attached when they + # built. + if: >- + !cancelled() && needs.version.result == 'success' && needs.macos.result == 'success' && + needs.windows.result == 'success' && needs.linux.result == 'success' && needs.web.result == 'success' + 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="SlideCraft 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/.gitignore b/.gitignore index 8b89730..07d2ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ plan/ /.claude/ /craft-fonts/ packaging/env.sh +/packaging/linux/flatpak/cargo-sources.json +/build-flatpak/ +/.flatpak-builder/ diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 5c90950..f5f91aa 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -18,6 +18,7 @@ deck and UI icons are original work of the SlideCraft contributors (drawn or gen | `docs/brand/artcraft-mark-black.svg`, `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Mark for light backgrounds | | `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `standards/license-files` | — (licence text) | Terms for the ArtCraft marks | | `assets/app-icon/slidecraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.slidecraft.png` | SlideCraft contributors | rendered by SlideCraft from `assets/app-icon/icon-source.slidecraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn | +| `assets/app-icon/slidecraft.icns`, `assets/app-icon/slidecraft.ico` | SlideCraft contributors | generated from the PNGs above by `packaging/icons.sh` (sips/iconutil, `cargo xtask ico`) | MIT OR Apache-2.0 | macOS and Windows app icons | | `assets/app-icon/icon-source.slidecraft` | SlideCraft contributors | made with `slidecraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon | ## Bundled through dependencies diff --git a/Cargo.lock b/Cargo.lock index 4417fbc..d405d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3129,6 +3129,7 @@ dependencies = [ "slidecraft-engine", "slidecraft-ui-egui", "winit", + "winresource", ] [[package]] @@ -3247,12 +3248,9 @@ dependencies = [ name = "slidecraft-pptx" version = "0.1.0" dependencies = [ - "image", "log", "quick-xml 0.38.4", - "serde_json", "slidecraft-color", - "slidecraft-engine", "slidecraft-geom", "slidecraft-model", "thiserror 2.0.21", @@ -3293,6 +3291,7 @@ version = "0.1.0" dependencies = [ "egui", "egui_kittest", + "js-sys", "log", "serde", "serde_json", @@ -3306,6 +3305,23 @@ dependencies = [ "slidecraft-text", ] +[[package]] +name = "slidecraft-web" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "js-sys", + "log", + "rfd", + "serde_json", + "slidecraft-engine", + "slidecraft-ui-egui", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "slotmap" version = "1.1.1" @@ -3579,10 +3595,12 @@ version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ + "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -3616,6 +3634,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tracing" version = "0.1.44" @@ -4577,6 +4601,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/ROADMAP.md b/ROADMAP.md index 553ac35..dcbd509 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,8 +31,13 @@ format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and av palette, light/dark. - Automation: ~200 commands, every one reachable from `slidecraft-cli`, the app's JSON control channel and the MCP server (headless or connected to the running app). +- Web: `apps/slidecraft-web` runs the same UI in the browser (trunk; WebGPU with WebGL2 fallback), + opening the sample deck; Open/Insert use the browser file picker, Save/Export download. +- Release CI: pushes to `release` build a draft GitHub Release (macOS universal dmg, Windows + x64/x86 msi + zip, Linux AppImage/deb/rpm/tar.gz + Flatpak, FreeBSD tar.gz, web zip); signing + secrets live in the `release` environment. Version: `cargo xtask version`. -**In progress:** PPTX import/export · web (WASM) build · release packaging for all platforms. +**In progress:** PPTX import/export · first signed release run. **Next (in order):** vector PDF artwork · audio/video playback · Format Shape pane depth · connectors with glue, freeform and edit points · merge shapes · Animation Pane polish · native macOS menu bar · diff --git a/apps/slidecraft-web/Cargo.toml b/apps/slidecraft-web/Cargo.toml new file mode 100644 index 0000000..16b8546 --- /dev/null +++ b/apps/slidecraft-web/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "slidecraft-web" +description = "SlideCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true + +[[bin]] +name = "slidecraft-web" +path = "src/main.rs" + +[dependencies] +slidecraft-engine = { workspace = true } +slidecraft-ui-egui = { workspace = true } +egui = { workspace = true } +log = { workspace = true } +serde_json = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +# `wgpu` pulls egui-wgpu's defaults: WebGPU plus the WebGL2 fallback. +eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] } +rfd = { workspace = true } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration"] } diff --git a/apps/slidecraft-web/Trunk.toml b/apps/slidecraft-web/Trunk.toml new file mode 100644 index 0000000..c6e5c00 --- /dev/null +++ b/apps/slidecraft-web/Trunk.toml @@ -0,0 +1,10 @@ +# Build with: trunk build --release (from apps/slidecraft-web) +[build] +target = "index.html" +dist = "../../dist/web" +public_url = "./" +filehash = true + +[serve] +addresses = ["127.0.0.1"] +port = 8768 diff --git a/apps/slidecraft-web/index.html b/apps/slidecraft-web/index.html new file mode 100644 index 0000000..4f2d54d --- /dev/null +++ b/apps/slidecraft-web/index.html @@ -0,0 +1,20 @@ + + + + + + SlideCraft + + + + + + +
Loading SlideCraft…
+ + diff --git a/apps/slidecraft-web/src/main.rs b/apps/slidecraft-web/src/main.rs new file mode 100644 index 0000000..82f564c --- /dev/null +++ b/apps/slidecraft-web/src/main.rs @@ -0,0 +1,31 @@ +//! SlideCraft in the browser. +//! +//! Runs the same [`slidecraft_ui_egui::SlideApp`] as the desktop app through eframe's web +//! runner (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` +//! from this directory (output in `dist/web`). +//! +//! Differences from the desktop app: +//! - no TCP control channel (browsers can't listen on sockets); +//! - File → Open and Insert → Pictures/Audio/Video use the browser file picker; bytes arrive +//! asynchronously through `Services::inbox` (presentations open, media is inserted); +//! - Save and Export trigger a browser download; +//! - dropped files are read asynchronously by `web::WebShell` and delivered through the inbox; +//! - preferences are not persisted. +//! +//! URL query flags: `?webgl` forces the WebGL2 backend instead of WebGPU; `?blank` starts with an +//! empty presentation instead of the sample deck; `?show` starts the slide show. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +#![forbid(unsafe_code)] + +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(target_arch = "wasm32")] +fn main() { + web::start(); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() { + eprintln!("slidecraft-web only runs in the browser: build it with `trunk build --release` in apps/slidecraft-web"); +} diff --git a/apps/slidecraft-web/src/web.rs b/apps/slidecraft-web/src/web.rs new file mode 100644 index 0000000..0bb14b7 --- /dev/null +++ b/apps/slidecraft-web/src/web.rs @@ -0,0 +1,176 @@ +//! The browser shell: web `Services`, drag-and-drop, and the eframe web runner. + +use slidecraft_engine::Session; +use slidecraft_ui_egui::{Inbox, Services, SlideApp}; +use wasm_bindgen::JsCast as _; + +const PRESENTATION_EXTS: &[&str] = &["slidecraft", "pptx", "potx", "ppsx", "txt", "md"]; +const PICTURE_EXTS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"]; +const AUDIO_EXTS: &[&str] = &["wav", "mp3", "m4a", "aac", "flac", "ogg", "opus", "aif", "aiff"]; +const VIDEO_EXTS: &[&str] = &["mp4", "m4v", "mov", "webm"]; +const CANVAS_ID: &str = "slidecraft_canvas"; +const LOADING_ID: &str = "slidecraft_loading"; + +pub fn start() { + eframe::WebLogger::init(log::LevelFilter::Info).ok(); + wasm_bindgen_futures::spawn_local(async { + let Some(document) = web_sys::window().and_then(|w| w.document()) else { + log::error!("no document"); + return; + }; + let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::().ok()) else { + log::error!("missing "); + return; + }; + let mut options = eframe::WebOptions::default(); + if query().contains("webgl") + && let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup + { + create.instance_descriptor.backends = eframe::wgpu::Backends::GL; + } + let result = eframe::WebRunner::new() + .start( + canvas, + options, + Box::new(move |cc| { + if let Some(rs) = &cc.wgpu_render_state { + log::info!("slidecraft-web {}: wgpu backend {:?}", env!("CARGO_PKG_VERSION"), rs.adapter.get_info().backend); + } + let inbox: Inbox = Inbox::default(); + let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone())); + let q = query(); + if !q.contains("blank") { + if let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) { + log::error!("sample deck: {e}"); + } + } else if let Err(e) = app.run("file.new", serde_json::json!({})) { + log::error!("new presentation: {e}"); + } + if q.contains("show") { + app.start_show(0, false); + } + Ok(Box::new(WebShell { app, inbox })) + }), + ) + .await; + if let Some(el) = document.get_element_by_id(LOADING_ID) { + match result { + Ok(()) => el.remove(), + Err(e) => el.set_inner_html(&format!("

SlideCraft failed to start: {e:?}

A browser with WebGPU or WebGL2 is required.

")), + } + } + }); +} + +fn query() -> String { + web_sys::window().and_then(|w| w.location().search().ok()).unwrap_or_default() +} + +/// Wraps the app to read dropped files asynchronously (browsers can't read them synchronously) +/// and feed them through the inbox. +struct WebShell { + app: SlideApp, + inbox: Inbox, +} + +impl eframe::App for WebShell { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let dropped = ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files)); + for f in dropped { + let inbox = self.inbox.clone(); + let ctx = ctx.clone(); + wasm_bindgen_futures::spawn_local(async move { + let name = f.path().file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "dropped".into()); + match f.bytes_async().await { + Ok(bytes) => { + inbox.lock().unwrap_or_else(|e| e.into_inner()).push((name, bytes)); + ctx.request_repaint(); + } + Err(e) => log::error!("couldn't read dropped file {name}: {e}"), + } + }); + } + self.app.logic(ctx); + } + + fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) { + self.app.raw_input_hook(raw); + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + self.app.ui(ui); + } +} + +/// Web services: no file paths, so `pick_open`/`pick_save`/`read` stay `None` and the UI falls +/// back to `open_async` (browser picker → inbox) and `download`. +fn services(inbox: Inbox, ctx: egui::Context) -> Services { + let open_inbox = inbox.clone(); + Services { + open_async: Some(Box::new(move |purpose: &str| { + let inbox = open_inbox.clone(); + let ctx = ctx.clone(); + let dialog = match purpose { + "picture" => rfd::AsyncFileDialog::new().add_filter("Pictures", PICTURE_EXTS), + "audio" => rfd::AsyncFileDialog::new().add_filter("Audio", AUDIO_EXTS), + "video" => rfd::AsyncFileDialog::new().add_filter("Video", VIDEO_EXTS), + _ => rfd::AsyncFileDialog::new().add_filter("Presentations", PRESENTATION_EXTS), + }; + wasm_bindgen_futures::spawn_local(async move { + let Some(file) = dialog.pick_file().await else { + return; + }; + let bytes = file.read().await; + inbox.lock().unwrap_or_else(|e| e.into_inner()).push((file.file_name(), bytes)); + ctx.request_repaint(); + }); + })), + write: Some(Box::new(|name: &str, bytes: &[u8]| download(name, bytes))), + download: Some(Box::new(|name: &str, bytes: &[u8]| { + if let Err(e) = download(name, bytes) { + log::error!("download of {name} failed: {e}"); + } + })), + inbox: Some(inbox), + ..Default::default() + } +} + +/// Trigger a browser download of `bytes` named after the last component of `path`. +fn download(path: &str, bytes: &[u8]) -> Result<(), String> { + let js = |e: wasm_bindgen::JsValue| format!("{e:?}"); + let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "slidecraft".into()); + let window = web_sys::window().ok_or("no window")?; + let document = window.document().ok_or("no document")?; + let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes)); + let opts = web_sys::BlobPropertyBag::new(); + opts.set_type(mime_for(&name)); + let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &opts).map_err(js)?; + let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(js)?; + let a: web_sys::HtmlAnchorElement = document.create_element("a").map_err(js)?.dyn_into().map_err(|_| "not an anchor")?; + a.set_href(&url); + a.set_download(&name); + a.style().set_property("display", "none").map_err(js)?; + let body = document.body().ok_or("no body")?; + body.append_child(&a).map_err(js)?; + a.click(); + a.remove(); + // Revoke after the click has been dispatched; the download keeps its own reference. + let revoke = wasm_bindgen::closure::Closure::once_into_js(move || { + web_sys::Url::revoke_object_url(&url).ok(); + }); + window.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000).map_err(js)?; + Ok(()) +} + +fn mime_for(name: &str) -> &'static str { + match name.rsplit('.').next().map(str::to_ascii_lowercase).as_deref() { + Some("png") => "image/png", + Some("jpg" | "jpeg") => "image/jpeg", + Some("pdf") => "application/pdf", + Some("txt") => "text/plain", + Some("pptx") => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + Some("slidecraft") => "application/zip", + _ => "application/octet-stream", + } +} diff --git a/apps/slidecraft/Cargo.toml b/apps/slidecraft/Cargo.toml index bdb937c..cf518b1 100644 --- a/apps/slidecraft/Cargo.toml +++ b/apps/slidecraft/Cargo.toml @@ -18,6 +18,10 @@ egui = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +[build-dependencies] +# Icon + VERSIONINFO for slidecraft.exe; build.rs only uses it when the target is Windows. +winresource = "0.1" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rfd = { workspace = true } arboard = { workspace = true, features = ["image-data"] } diff --git a/apps/slidecraft/build.rs b/apps/slidecraft/build.rs new file mode 100644 index 0000000..40c647a --- /dev/null +++ b/apps/slidecraft/build.rs @@ -0,0 +1,30 @@ +//! Windows only: embed the app icon and version info (VERSIONINFO) into `slidecraft.exe`, so it +//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. +//! +//! On every other target this does nothing. A missing resource compiler is a warning, so a +//! cross-compile from macOS or Linux still links, unless `SLIDECRAFT_REQUIRE_WINRES=1` turns it +//! into an error (for release builds). + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../../assets/app-icon/slidecraft.ico"); + println!("cargo:rerun-if-env-changed=SLIDECRAFT_REQUIRE_WINRES"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + let mut res = winresource::WindowsResource::new(); + res.set_icon("../../assets/app-icon/slidecraft.ico") + .set("ProductName", "SlideCraft") + .set("FileDescription", "SlideCraft presentations") + .set("CompanyName", "Learning Machines LLC") + .set("LegalCopyright", "Copyright (c) the SlideCraft authors. MIT OR Apache-2.0.") + .set("OriginalFilename", "slidecraft.exe") + .set("InternalName", "slidecraft"); + if let Err(e) = res.compile() { + if std::env::var_os("SLIDECRAFT_REQUIRE_WINRES").is_some() { + eprintln!("embedding Windows resources failed: {e}"); + std::process::exit(1); + } + println!("cargo:warning=slidecraft.exe built without icon/version resources: {e}"); + } +} diff --git a/assets/app-icon/slidecraft.icns b/assets/app-icon/slidecraft.icns new file mode 100644 index 0000000000000000000000000000000000000000..1ab212cacdbdadfced53109cea5f0f2723502697 GIT binary patch literal 110107 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%0DDCp(Q0q3t}=3$oqCCFLDd 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 z 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%0AaGQ*B2qfJe-*H83 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 V_`b>lUyywTwg2Kg_&EBz^ncgW%l!ZV literal 0 HcmV?d00001 diff --git a/assets/app-icon/slidecraft.ico b/assets/app-icon/slidecraft.ico new file mode 100644 index 0000000000000000000000000000000000000000..769cd6c9452551bca040ecd2c06614da478262f8 GIT binary patch literal 22252 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 H+~_|5_;mM# literal 0 HcmV?d00001 diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 8684384..854f945 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -22,5 +22,8 @@ serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" + [dev-dependencies] egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index b13b7ab..6f20dfc 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -738,7 +738,7 @@ pub fn about(_app: &mut SlideApp, ui: &mut Ui) { let (r, _) = ui.allocate_exact_size(vec2(64.0, 64.0), Sense::hover()); paint_logo(ui.painter(), r); ui.label(egui::RichText::new("SlideCraft").font(theme::bold(22.0))); - ui.label(egui::RichText::new(format!("Version {}", env!("CARGO_PKG_VERSION"))).color(t.text_dim)); + ui.label(egui::RichText::new(version_line()).color(t.text_dim)); ui.add_space(6.0); ui.label("Presentations and slide shows, rebuilt from scratch in pure Rust."); ui.label("An open-source, clean-room project. Not affiliated with Microsoft."); @@ -751,6 +751,21 @@ pub fn about(_app: &mut SlideApp, ui: &mut Ui) { }); } +/// "Version X.Y.Z" plus, for release builds, the short commit and build date. The version comes +/// from `[workspace.package] version` (the single source of truth, `cargo xtask version`); the +/// release workflow sets `SLIDECRAFT_BUILD_SHA` / `SLIDECRAFT_BUILD_DATE` at build time. +pub fn version_line() -> String { + let mut s = format!("Version {}", env!("CARGO_PKG_VERSION")); + let sha = option_env!("SLIDECRAFT_BUILD_SHA").map(|s| s.get(..9).unwrap_or(s)).filter(|s| !s.is_empty()); + let date = option_env!("SLIDECRAFT_BUILD_DATE").filter(|s| !s.is_empty()); + match (sha, date) { + (Some(sha), Some(date)) => s.push_str(&format!(" ({sha}, {date})")), + (Some(x), None) | (None, Some(x)) => s.push_str(&format!(" ({x})")), + (None, None) => {} + } + s +} + /// SlideCraft's mark, drawn in code: a slide card with a play triangle in our orange. pub fn paint_logo(p: &egui::Painter, r: Rect) { let orange = Color32::from_rgb(0xF2, 0x6B, 0x1D); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f65fcd6..5446ffd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -498,6 +498,9 @@ impl SlideApp { } self.collect_screenshots(ctx); self.issue_screenshots(ctx); + // On the web, dropped files can only be read asynchronously: the host reads them and + // delivers them through `Services::inbox`. + #[cfg(not(target_arch = "wasm32"))] for f in ctx.input(|i| i.raw.dropped_files.clone()) { let name = f.path().file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "dropped file".into()); match f.bytes() { @@ -749,6 +752,5 @@ pub fn now_ms() -> f64 { #[cfg(target_arch = "wasm32")] fn js_now() -> f64 { - // egui's input time is good enough for UI timing on the web. - 0.0 + js_sys::Date::now() } diff --git a/packaging/.shellcheckrc b/packaging/.shellcheckrc new file mode 100644 index 0000000..36c19d0 --- /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/common.sh b/packaging/common.sh new file mode 100644 index 0000000..6a0c3d8 --- /dev/null +++ b/packaging/common.sh @@ -0,0 +1,70 @@ +# shellcheck shell=bash +# Shared setup for the packaging scripts. Source it: `. "$(dirname "$0")/../common.sh"`. +# (Not env.sh: .gitignore reserves packaging/env.sh for local, uncommitted settings.) +# +# Exports: +# ROOT workspace root +# VERSION [workspace.package] version from Cargo.toml (override: SLIDECRAFT_VERSION) +# DIST output directory for release artifacts (default: $ROOT/dist/release) +# SLIDECRAFT_BUILD_SHA git commit shown in the About dialog (crates/ui-egui/src/dialogs.rs version_line) +# SLIDECRAFT_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="${SLIDECRAFT_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 "${SLIDECRAFT_BUILD_SHA:-}" ]; then + SLIDECRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)" +fi +export SLIDECRAFT_BUILD_SHA +export SLIDECRAFT_BUILD_DATE="${SLIDECRAFT_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 + copy_font_licences "$dest" +} + +# Builds made with craft-fonts (CRAFT_FONTS_DIR, set by the release workflow) embed its fonts, so +# the package carries their licences as OFL-.txt. Nothing without CRAFT_FONTS_DIR. +copy_font_licences() { + local dest="$1" f + [ -n "${CRAFT_FONTS_DIR:-}" ] || return 0 + for f in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do + if [ -f "$f" ]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; 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/freebsd/package.sh b/packaging/freebsd/package.sh new file mode 100755 index 0000000..fc6beaf --- /dev/null +++ b/packaging/freebsd/package.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Build and package SlideCraft for FreeBSD: +# +# $DIST/slidecraft--freebsd-.tar.gz bin/, share/ (desktop entry, icons, AppStream) +# +# Usage: packaging/freebsd/package.sh [--skip-build] +# +# Run on FreeBSD (CI: a FreeBSD 14 VM via vmactions/freebsd-vm). Runtime needs: libxkbcommon, +# wayland or libX11, and mesa (Vulkan or EGL): `pkg install libxkbcommon libX11 mesa-libs vulkan-loader`. +set -euo pipefail +# shellcheck source=../common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../common.sh" +LINUX="$ROOT/packaging/linux" +APP_ID=ai.storyteller.slidecraft + +if [ "${1:-}" != "--skip-build" ]; then + (cd "$ROOT" && cargo build --release --locked -p slidecraft -p slidecraft-cli) +fi + +ARCH="$(uname -m)" +case "$ARCH" in + amd64) ARCH=x86_64 ;; + arm64) ARCH=aarch64 ;; +esac +BIN="$CARGO_TARGET_DIR/release" +NAME="slidecraft-$VERSION-freebsd-$ARCH" +WORK="$CARGO_TARGET_DIR/freebsd-package" +STAGE="$WORK/$NAME" +rm -rf "$WORK" +mkdir -p "$STAGE/bin" "$STAGE/share/applications" "$STAGE/share/metainfo" "$STAGE/share/mime/packages" "$STAGE/share/icons" "$STAGE/share/doc/slidecraft" + +install -m 755 "$BIN/slidecraft" "$BIN/slidecraft-cli" "$STAGE/bin/" +strip "$STAGE/bin/slidecraft" "$STAGE/bin/slidecraft-cli" 2>/dev/null || true +install -m 644 "$LINUX/$APP_ID.desktop" "$STAGE/share/applications/" +install -m 644 "$LINUX/$APP_ID.mime.xml" "$STAGE/share/mime/packages/$APP_ID.xml" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SLIDECRAFT_BUILD_DATE/g" \ + "$LINUX/$APP_ID.metainfo.xml.in" >"$STAGE/share/metainfo/$APP_ID.metainfo.xml" +cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/" +copy_docs "$STAGE/share/doc/slidecraft" + +tar -C "$WORK" -czf "$DIST/$NAME.tar.gz" "$NAME" +"$STAGE/bin/slidecraft-cli" --version +echo "wrote $DIST/$NAME.tar.gz" +ls -lh "$DIST/$NAME.tar.gz" diff --git a/packaging/icons.sh b/packaging/icons.sh new file mode 100755 index 0000000..c49610f --- /dev/null +++ b/packaging/icons.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Regenerate the platform app icons from the committed PNGs in assets/app-icon/: +# +# assets/app-icon/slidecraft.ico Windows (16–256 px, from hicolor/*) +# assets/app-icon/slidecraft.icns macOS (16–1024 px, from slidecraft-1024.png; needs sips + iconutil) +# +# The PNGs themselves are rendered by SlideCraft from assets/app-icon/icon-source.slidecraft. +# The outputs are committed, so builds and packaging never need these tools; package scripts +# call this only when an output is missing. +# +# packaging/icons.sh [ico] [icns] (default: both) +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIR="$ROOT/assets/app-icon" +ID="ai.storyteller.slidecraft" +SRC="$DIR/slidecraft-1024.png" +WANT="${*:-ico icns}" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +case " $WANT " in + *" ico "*) + ICO_PNGS=() + for s in 16 24 32 48 64 128 256; do + ICO_PNGS+=("$DIR/hicolor/${s}x${s}/apps/$ID.png") + done + (cd "$ROOT" && cargo run -q -p xtask -- ico "$DIR/slidecraft.ico" "${ICO_PNGS[@]}") + echo "wrote $DIR/slidecraft.ico" + ;; +esac + +case " $WANT " in + *" icns "*) + if command -v iconutil >/dev/null && command -v sips >/dev/null; then + SET="$TMP/slidecraft.iconset" + mkdir -p "$SET" + for s in 16 32 128 256 512; do + sips -z "$s" "$s" "$SRC" --out "$SET/icon_${s}x${s}.png" >/dev/null + d=$((s * 2)) + sips -z "$d" "$d" "$SRC" --out "$SET/icon_${s}x${s}@2x.png" >/dev/null + done + iconutil -c icns -o "$DIR/slidecraft.icns" "$SET" + echo "wrote $DIR/slidecraft.icns" + else + echo "warning: sips/iconutil not found (macOS only); slidecraft.icns not regenerated" >&2 + fi + ;; +esac diff --git a/packaging/linux/ai.storyteller.slidecraft.desktop b/packaging/linux/ai.storyteller.slidecraft.desktop new file mode 100644 index 0000000..099d43b --- /dev/null +++ b/packaging/linux/ai.storyteller.slidecraft.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Type=Application +Name=SlideCraft +GenericName=Presentation +Comment=Create and present slide decks +Exec=slidecraft %F +TryExec=slidecraft +Icon=ai.storyteller.slidecraft +Terminal=false +StartupNotify=true +StartupWMClass=ai.storyteller.slidecraft +Categories=Office;Presentation; +MimeType=application/x-slidecraft;application/vnd.openxmlformats-officedocument.presentationml.presentation;application/vnd.openxmlformats-officedocument.presentationml.template;application/vnd.openxmlformats-officedocument.presentationml.slideshow; +Keywords=presentation;slides;slide show;deck;pptx;keynote;powerpoint; diff --git a/packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in b/packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in new file mode 100644 index 0000000..843e7db --- /dev/null +++ b/packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in @@ -0,0 +1,59 @@ + + + + ai.storyteller.slidecraft + CC0-1.0 + MIT OR Apache-2.0 + SlideCraft +

Create and present slide decks + + Learning Machines LLC + + +

+ SlideCraft builds presentations and slide shows: themes, layouts, shapes, pictures, + tables, charts, transitions and animations. It opens and saves PowerPoint (.pptx) files + and presents them full screen with a presenter view. +

+

+ Every action is also a command that the command line and AI agents (through MCP) can drive. +

+
+ ai.storyteller.slidecraft.desktop + ai.storyteller.slidecraft + + https://getartcraft.com/apps/slidecraft + https://github.com/storytold/slidecraft/issues + https://github.com/storytold/slidecraft + https://discord.gg/artcraft + + Office + Presentation + + + presentation + slides + slide show + pptx + + + #ffc9a8 + #b8471f + + + 760 + + + keyboard + pointing + + + slidecraft + application/x-slidecraft + + + + + + diff --git a/packaging/linux/ai.storyteller.slidecraft.mime.xml b/packaging/linux/ai.storyteller.slidecraft.mime.xml new file mode 100644 index 0000000..49530e4 --- /dev/null +++ b/packaging/linux/ai.storyteller.slidecraft.mime.xml @@ -0,0 +1,9 @@ + + + + SlideCraft presentation + + + + + diff --git a/packaging/linux/flatpak/ai.storyteller.slidecraft.yml b/packaging/linux/flatpak/ai.storyteller.slidecraft.yml new file mode 100644 index 0000000..14dd78d --- /dev/null +++ b/packaging/linux/flatpak/ai.storyteller.slidecraft.yml @@ -0,0 +1,56 @@ +# Flatpak manifest for SlideCraft, 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.slidecraft.yml +# flatpak run ai.storyteller.slidecraft +# +# For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. +id: ai.storyteller.slidecraft +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable +command: slidecraft +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. Documents is granted for opening decks by + # path from the command line. + - --filesystem=xdg-documents +build-options: + append-path: /usr/lib/sdk/rust-stable/bin +modules: + - name: slidecraft + buildsystem: simple + build-options: + env: + CARGO_HOME: /run/build/slidecraft/cargo + CARGO_NET_OFFLINE: 'true' + build-commands: + - cargo --offline build --release --locked -p slidecraft + - install -Dm755 target/release/slidecraft /app/bin/slidecraft + - install -Dm644 packaging/linux/ai.storyteller.slidecraft.desktop /app/share/applications/ai.storyteller.slidecraft.desktop + - install -Dm644 packaging/linux/ai.storyteller.slidecraft.mime.xml /app/share/mime/packages/ai.storyteller.slidecraft.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.slidecraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.slidecraft.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 0000000..5e04c86 --- /dev/null +++ b/packaging/linux/nfpm.yaml @@ -0,0 +1,71 @@ +# .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: slidecraft +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: editors +priority: optional +maintainer: ${SLIDECRAFT_MAINTAINER} +vendor: Learning Machines LLC +homepage: https://getartcraft.com/apps/slidecraft +license: MIT OR Apache-2.0 +description: |- + Open-source, native presentation app that works the way PowerPoint users + expect. Themes, layouts, shapes, pictures, tables, charts, transitions and + animations; opens and saves PowerPoint (.pptx) files; full-screen slide show + with presenter view; GPU rendering (wgpu). + Includes slidecraft-cli, a headless renderer 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/slidecraft + dst: /usr/bin/slidecraft + file_info: { mode: 0755 } + - src: ${STAGE}/usr/bin/slidecraft-cli + dst: /usr/bin/slidecraft-cli + file_info: { mode: 0755 } + - src: ${STAGE}/usr/share/ + dst: /usr/share/ + type: tree + +scripts: + postinstall: packaging/linux/postinst.sh + postremove: packaging/linux/postinst.sh diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh new file mode 100755 index 0000000..d0560d1 --- /dev/null +++ b/packaging/linux/package.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Build and package SlideCraft for Linux ( is x86_64 or aarch64): +# +# $DIST/slidecraft--linux-.AppImage any distro with glibc >= the build host's +# $DIST/slidecraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... +# $DIST/slidecraft--linux-.rpm Fedora, openSUSE, RHEL, ... +# $DIST/slidecraft--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=../common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../common.sh" +HERE="$ROOT/packaging/linux" +APP_ID=ai.storyteller.slidecraft + +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 SLIDECRAFT_MAINTAINER="${SLIDECRAFT_MAINTAINER:-SlideCraft maintainers }" +BASENAME="slidecraft-$VERSION-linux-$ARCH" + +echo "==> SlideCraft $VERSION for Linux $ARCH ($FORMATS)" + +if [ "$SKIP_BUILD" = 0 ]; then + (cd "$ROOT" && cargo build --release --locked -p slidecraft -p slidecraft-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/slidecraft" "$STAGE/usr/bin/slidecraft" +install -Dm755 "$BIN/slidecraft-cli" "$STAGE/usr/bin/slidecraft-cli" +strip "$STAGE/usr/bin/slidecraft" "$STAGE/usr/bin/slidecraft-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@/$SLIDECRAFT_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/slidecraft" +copy_docs "$STAGE/usr/share/doc/slidecraft" + +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/SlideCraft.AppDir" + cp -R "$STAGE" "$APPDIR" + mv "$APPDIR/usr/share/doc" "$WORK/doc-unused" + ln -s usr/bin/slidecraft "$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/slidecraft-cli" --version +echo "==> done" +ls -lh "$DIST" diff --git a/packaging/linux/postinst.sh b/packaging/linux/postinst.sh new file mode 100755 index 0000000..775e4df --- /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 0000000..347fe72 --- /dev/null +++ b/packaging/macos/Info.plist.in @@ -0,0 +1,95 @@ + + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SlideCraft + CFBundleName + SlideCraft + CFBundleExecutable + SlideCraft + CFBundleIdentifier + ai.storyteller.slidecraft + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + @SHORT_VERSION@ + CFBundleVersion + @SHORT_VERSION@ + SlideCraftVersion + @VERSION@ + SlideCraftBuildCommit + @BUILD_SHA@ + CFBundleIconFile + SlideCraft + LSApplicationCategoryType + public.app-category.productivity + + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + NSSupportsAutomaticGraphicsSwitching + + NSRequiresAquaSystemAppearance + + NSHumanReadableCopyright + Copyright © the SlideCraft authors. MIT OR Apache-2.0. + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + ai.storyteller.slidecraft.presentation + UTTypeDescription + SlideCraft Presentation + UTTypeConformsTo + + public.data + public.presentation + + UTTypeIconFile + SlideCraft + UTTypeTagSpecification + + public.filename-extension + slidecraft + public.mime-type + application/x-slidecraft + + + + + UTImportedTypeDeclarations + + + UTTypeIdentifier + org.openxmlformats.presentationml.presentation + UTTypeDescription + Open XML Presentation + UTTypeConformsTo + + public.data + public.presentation + public.zip-archive + + UTTypeTagSpecification + + public.filename-extension + pptx + + + + + + diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist new file mode 100644 index 0000000..e3c22ff --- /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 100755 index 0000000..5be3ebf --- /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=../common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../common.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/slidecraft-signing.keychain-db" +CERT="$TMP/slidecraft-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 100755 index 0000000..b7da66a --- /dev/null +++ b/packaging/macos/package.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Build, sign and (optionally) notarize the macOS release artifacts: +# +# $DIST/slidecraft--macos-.dmg SlideCraft.app on a drag-to-Applications DMG +# $DIST/slidecraft-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=../common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../common.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/SlideCraft.app" +DMG="$DIST/slidecraft-$VERSION-macos-$ARCH.dmg" +CLI_ZIP="$DIST/slidecraft-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 "==> SlideCraft $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 slidecraft -p slidecraft-cli "${args[@]}") +fi + +rm -rf "$WORK" +mkdir -p "$WORK/bin" +for bin in slidecraft slidecraft-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 +} + +# ---- SlideCraft.app ---------------------------------------------------------------------------- +echo "==> assembling $APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" +# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile). +cp "$WORK/bin/slidecraft" "$APP/Contents/MacOS/SlideCraft" +cp "$ROOT/assets/app-icon/slidecraft.icns" "$APP/Contents/Resources/SlideCraft.icns" +# Licences of the embedded craft-fonts fonts (only when built with CRAFT_FONTS_DIR). +copy_font_licences "$APP/Contents/Resources" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ + -e "s/@BUILD_SHA@/${SLIDECRAFT_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/SlideCraft" +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/SlideCraft-notarize.zip" + notarize "$WORK/SlideCraft-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/SlideCraft.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 "SlideCraft $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/slidecraft-cli-$VERSION-macos-$ARCH" +mkdir -p "$CLI_DIR" +cp "$WORK/bin/slidecraft-cli" "$CLI_DIR/" +copy_docs "$CLI_DIR" +sign --options runtime "$CLI_DIR/slidecraft-cli" +codesign --verify --strict --verbose=2 "$CLI_DIR/slidecraft-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/slidecraft-cli" --version +echo "==> done" +ls -lh "$DMG" "$CLI_ZIP" diff --git a/packaging/web/.htaccess b/packaging/web/.htaccess new file mode 100644 index 0000000..7b7e6e1 --- /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 0000000..4b272bf --- /dev/null +++ b/packaging/web/README.md @@ -0,0 +1,88 @@ +# Hosting SlideCraft for the web + +`slidecraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a +static site in `slidecraft-web-/`: + +| File | What it is | +|---|---| +| `index.html` | The page. It loads everything through relative URLs. | +| `slidecraft-web-.js` | wasm-bindgen glue (generated, ES module) | +| `slidecraft-web-_bg.wasm` | The app, size is printed by `packaging/web/package.sh` | +| `_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/slidecraft-web/Trunk.toml`), +so the site works at a domain root (`https://example.com/`), under a prefix +(`https://example.com/tools/slidecraft/`) 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 shrinks the + download by roughly 60%. 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:** SlideCraft 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 /slidecraft/ { + 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. The web build keeps no preferences between visits; it starts + with the sample deck (or `?blank`) every time. +- **Sandboxed iframes** need at least + `sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"`. 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 + +SlideCraft 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) | +| `?blank` | Start with an empty presentation instead of the sample deck | +| `?show` | Start the slide show immediately (kiosk/embedded decks) | + +For example: `