Release pipeline: signed macOS universal, Windows x64/x86, Linux, FreeBSD and web builds; MTEXT editor

Workflows and packaging adapted from the craftrules playbook (release environment with the twelve
signing secrets, new WiX upgrade code, .dxf/.dwg file associations), app icon set (hicolor, icns,
ico), docs/releasing.md. MTEXT now opens a text editor window for its contents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-05 23:50:51 -04:00
1 parent 3e2155e587
commit 4c74c5591c
42 files changed
+1714 -5

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
# 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
- 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
CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked
+57
View File
@@ -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/cadcraft.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 '<Icon Id="[^"]*"' packaging/windows/cadcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error::Icon Id must end in .ico or .exe"; exit 1
fi
desktop-file-validate packaging/linux/ai.storyteller.cadcraft.desktop
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.cadcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.cadcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.cadcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.cadcraft.yml')); assert m['id'] == 'ai.storyteller.cadcraft' and m['command'] == 'cadcraft'"
+290
View File
@@ -0,0 +1,290 @@
# Release pipeline: every push to `release` builds signed installers for every platform and
# creates or updates the draft GitHub Release "CADCraft v<version>". 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
CADCRAFT_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 "### CADCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
macos:
name: macOS universal
needs: version
runs-on: macos-15
environment: release
env:
CADCRAFT_VERSION: ${{ needs.version.outputs.version }}
CADCRAFT_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 "$CADCRAFT_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 }
# Windows on ARM: cross-compiled on the x64 runner, so signing and WiX run as above.
# .github/workflows/windows-arm64.yml installs and runs it on ARM64 hardware.
- { arch: arm64, target: aarch64-pc-windows-msvc }
env:
CADCRAFT_VERSION: ${{ needs.version.outputs.version }}
CADCRAFT_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 "$CADCRAFT_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:
CADCRAFT_VERSION: ${{ needs.version.outputs.version }}
CADCRAFT_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 "$CADCRAFT_VERSION"
- name: Build and package (AppImage, deb, rpm, tar.gz)
env:
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/cadcraft$|metainfo|applications'
ldd target/release/cadcraft
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.cadcraft.yml')); assert m['id'] == 'ai.storyteller.cadcraft'"
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:
CADCRAFT_VERSION: ${{ needs.version.outputs.version }}
CADCRAFT_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 "$CADCRAFT_VERSION"
- name: Build and zip
env:
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="CADCraft 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"
+10 -5
View File
@@ -23,13 +23,18 @@ Generated-in-code assets are original and have no file to list:
| Asset | Author | Source | Licence | Notes |
|---|---|---|---|---|
| `assets/app-icon/cadcraft-16.png` | CADCraft contributors | generated (provisional drafting-triangle icon) | MIT OR Apache-2.0 | Provisional until the mascot icon is drawn |
| `assets/app-icon/cadcraft-32.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/cadcraft-48.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/cadcraft.icns` | CADCraft contributors | generated (packaging/icons.sh) | MIT OR Apache-2.0 | macOS icon |
| `assets/app-icon/cadcraft.ico` | CADCraft contributors | generated (packaging/icons.sh) | MIT OR Apache-2.0 | Windows icon |
| `assets/app-icon/cadcraft-64.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/cadcraft-128.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/cadcraft-256.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/cadcraft-512.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/cadcraft-1024.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | |
| `assets/app-icon/cadcraft-macos-512.png` | CADCraft contributors | generated (macOS margin variant) | MIT OR Apache-2.0 | |
| `docs/images/ui-bracket.png` | CADCraft contributors | screenshot of CADCraft itself (sample drawing generated in code) | MIT OR Apache-2.0 | Original; no Autodesk UI |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+59
View File
@@ -9,6 +9,7 @@ use crate::icons::{self, Icon};
use crate::theme::Tokens;
pub fn show(app: &mut CadApp, ctx: &egui::Context) {
mtext_editor(app, ctx);
let Some(d) = app.ui.dialog.clone() else { return };
let mut open = true;
match d.as_str() {
@@ -226,3 +227,61 @@ fn blocks(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
}
});
}
/// The multiline text editor shown while MTEXT asks for its contents.
fn mtext_editor(app: &mut CadApp, ctx: &egui::Context) {
let active = app.session.running.as_ref().is_some_and(|r| r.id == "mtext") && app.session.current_prompt().is_some_and(|p| p.accept.text && !p.accept.point);
let id = egui::Id::new("mtext_editor_buffer");
if !active {
ctx.data_mut(|d| d.remove::<String>(id));
return;
}
let mut buf = ctx.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_default();
let mut submit = None;
let mut cancel = false;
egui::Window::new("Text Editor").collapsible(false).resizable(true).default_size(vec2(460.0, 220.0)).show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Style: Standard");
ui.separator();
let h = app.session.doc().map(|d| d.header.f64("TEXTSIZE", 0.2)).unwrap_or(0.2);
ui.label(format!("Height: {h:.4}"));
ui.separator();
if ui.button("B").on_hover_text("Bold").clicked() {
buf.push_str("{\\fArial|b1;}");
}
if ui.button("⅟").on_hover_text("Stack (type 1/2 then select)").clicked() {
buf.push_str("\\S1/2;");
}
if ui.button("°").on_hover_text("Degree").clicked() {
buf.push_str("%%d");
}
if ui.button("±").on_hover_text("Plus/minus").clicked() {
buf.push_str("%%p");
}
if ui.button("⌀").on_hover_text("Diameter").clicked() {
buf.push_str("%%c");
}
});
let r = ui.add(egui::TextEdit::multiline(&mut buf).desired_rows(6).desired_width(f32::INFINITY).hint_text("Type text; Enter starts a new paragraph"));
if !r.has_focus() && buf.is_empty() {
r.request_focus();
}
ui.horizontal(|ui| {
if ui.button("OK").clicked() || (r.has_focus() && ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Enter))) {
submit = Some(buf.replace('\n', "\\P"));
}
if ui.button("Cancel").clicked() {
cancel = true;
}
ui.label(RichText::new("⌘↩ to finish").small());
});
});
ctx.data_mut(|d| d.insert_temp(id, buf));
if let Some(t) = submit {
ctx.data_mut(|d| d.remove::<String>(id));
let _ = app.session.input(cadcraft_engine::Input::Text(t));
} else if cancel {
ctx.data_mut(|d| d.remove::<String>(id));
app.session.cancel();
}
}
+10
View File
@@ -0,0 +1,10 @@
# Releasing CADCraft
The canonical recipe is craftrules' [`release/playbook.md`](https://github.com/storytold/craftrules/blob/main/release/playbook.md); this file covers only what's CADCraft-specific.
- **Version:** `[workspace.package] version` in the root `Cargo.toml` is the single source. `cargo xtask version` prints it; `cargo xtask version set X.Y.Z[-pre]` changes it. The app shows it in About, `cadcraft --version` and `cadcraft-cli --version`; release builds also embed the commit (`CADCRAFT_BUILD_SHA`).
- **Cut a release:** bump the version on `main`, then push `main` to `release` (`git push origin main:release`). Every push to `release` runs `.github/workflows/release.yml` and creates or updates the draft GitHub Release `CADCraft v<version>`. A `workflow_dispatch` run with `version: X.Y.Z-rc.N` makes a test build.
- **Artifacts:** macOS universal `.dmg` (Developer ID signed, notarized, stapled) and CLI `.zip`; Windows x64 and x86 `.msi` + portable `.zip` (Azure Trusted Signing); Linux x86_64 `.AppImage`, `.deb`, `.rpm`, `.tar.gz` (+ Flatpak manifest in `packaging/linux/flatpak/`); FreeBSD build check (`freebsd.yml`); web `.zip` (wasm + `index.html`, see `packaging/web/README.md`); `SHA256SUMS.txt`.
- **Secrets:** the twelve signing secrets live in the `release` environment (deployable only from the `release` branch, which only org admins and release-managers can push). All are optional: a missing one produces unsigned artifacts with a warning.
- **File types:** `.dxf` and `.dwg` (macOS document types, Windows "Open with", Linux MIME types).
- **Local packaging test:** `packaging/macos/package.sh` (ad-hoc signed DMG), `packaging/linux/package.sh`, `packaging/web/package.sh`.
+3
View File
@@ -0,0 +1,3 @@
# shellcheck config for the packaging scripts (CI: .github/workflows/packaging-lint.yml).
source-path=SCRIPTDIR
external-sources=true
+69
View File
@@ -0,0 +1,69 @@
# 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: CADCRAFT_VERSION)
# DIST output directory for release artifacts (default: $ROOT/dist/release)
# CADCRAFT_BUILD_SHA git commit baked into the binaries (see crates/engine/src/build_info.rs)
# CADCRAFT_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="${CADCRAFT_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 "${CADCRAFT_BUILD_SHA:-}" ]; then
CADCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)"
fi
export CADCRAFT_BUILD_SHA
export CADCRAFT_BUILD_DATE="${CADCRAFT_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-<family-dir>.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
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Regenerate the derived app icons from assets/app-icon/cadcraft-1024.png (macOS: sips, iconutil).
# The outputs are committed, so builds and packaging never need these tools.
#
# packaging/icons.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIR="$ROOT/assets/app-icon"
SRC="$DIR/cadcraft-1024.png"
ID="ai.storyteller.cadcraft"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
command -v sips >/dev/null || { echo "error: sips not found (macOS only)" >&2; exit 1; }
resize() { sips -s format png -z "$2" "$2" "$1" --out "$3" >/dev/null; }
for s in 16 24 32 48 64 128 256 512; do
mkdir -p "$DIR/hicolor/${s}x${s}/apps"
resize "$SRC" "$s" "$DIR/hicolor/${s}x${s}/apps/$ID.png"
done
resize "$SRC" 64 "$DIR/cadcraft-64.png"
resize "$SRC" 256 "$DIR/cadcraft-256.png"
ICO_PNGS=()
for s in 16 20 24 32 40 48 64 128 256; do
resize "$SRC" "$s" "$TMP/ico-$s.png"
ICO_PNGS+=("$TMP/ico-$s.png")
done
(cd "$ROOT" && cargo run -q -p xtask -- ico "$DIR/cadcraft.ico" "${ICO_PNGS[@]}")
# macOS icons sit on Apple's grid: the macOS-margin master is cadcraft-macos-512.png.
SET="$TMP/cadcraft.iconset"
mkdir -p "$SET"
for s in 16 32 128 256; do
resize "$DIR/cadcraft-macos-512.png" "$s" "$SET/icon_${s}x${s}.png"
resize "$DIR/cadcraft-macos-512.png" $((s * 2)) "$SET/icon_${s}x${s}@2x.png"
done
resize "$DIR/cadcraft-macos-512.png" 512 "$SET/icon_512x512.png"
iconutil -c icns -o "$DIR/cadcraft.icns" "$SET"
echo "icons written to $DIR"
@@ -0,0 +1,14 @@
[Desktop Entry]
Type=Application
Name=CADCraft
GenericName=Computer-Aided Design
Comment=Draft, dimension and plot 2D drawings; open DXF and DWG files
Exec=cadcraft %F
TryExec=cadcraft
Icon=ai.storyteller.cadcraft
Terminal=false
StartupNotify=true
StartupWMClass=ai.storyteller.cadcraft
Categories=Graphics;Engineering;VectorGraphics;
MimeType=image/vnd.dxf;image/vnd.dwg;application/x-dxf;application/acad;
Keywords=cad;drafting;dxf;dwg;drawing;engineering;architecture;mechanical;
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- AppStream metadata (software centres, Flathub). Template: package.sh and the Flatpak
manifest substitute @VERSION@ and @DATE@ from Cargo.toml and the build date. -->
<component type="desktop-application">
<id>ai.storyteller.cadcraft</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<name>CADCraft</name>
<summary>Draft, dimension and plot 2D drawings</summary>
<developer id="ai.storyteller">
<name>Learning Machines LLC</name>
</developer>
<description>
<p>
CADCraft is an open-source computer-aided design and drafting application with a familiar
command line, object snaps, layers, dimensions, hatches and blocks. It opens and saves DXF
and DWG drawings, exports SVG and PNG, and can be driven by AI agents over MCP.
</p>
</description>
<launchable type="desktop-id">ai.storyteller.cadcraft.desktop</launchable>
<icon type="stock">ai.storyteller.cadcraft</icon>
<!-- Only url types known to appstream 0.15 (Ubuntu 22.04, where CI validates): no vcs-browser. -->
<url type="homepage">https://getartcraft.com/apps/cadcraft</url>
<url type="bugtracker">https://github.com/storytold/cadcraft/issues</url>
<url type="help">https://github.com/storytold/cadcraft</url>
<url type="contact">https://discord.gg/artcraft</url>
<categories>
<category>Graphics</category>
<category>Engineering</category>
</categories>
<keywords>
<keyword>CAD</keyword>
<keyword>drafting</keyword>
<keyword>DXF</keyword>
<keyword>DWG</keyword>
</keywords>
<screenshots>
<screenshot type="default">
<caption>A mechanical part drawing with dimensions, hatching and layers</caption>
<image>https://raw.githubusercontent.com/storytold/cadcraft/main/docs/images/ui-bracket.png</image>
</screenshot>
</screenshots>
<branding>
<color type="primary" scheme_preference="light">#9ee3f2</color>
<color type="primary" scheme_preference="dark">#0b6f88</color>
</branding>
<requires>
<display_length compare="ge">760</display_length>
</requires>
<supports>
<control>keyboard</control>
<control>pointing</control>
<control>tablet</control>
</supports>
<provides>
<binary>cadcraft</binary>
</provides>
<content_rating type="oars-1.1"/>
<releases>
<release version="@VERSION@" date="@DATE@"/>
</releases>
</component>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="image/vnd.dxf">
<comment>DXF drawing</comment>
<glob pattern="*.dxf"/>
</mime-type>
<mime-type type="image/vnd.dwg">
<comment>DWG drawing</comment>
<magic priority="50"><match type="string" value="AC10" offset="0"/></magic>
<glob pattern="*.dwg"/>
</mime-type>
</mime-info>
@@ -0,0 +1,56 @@
# Flatpak manifest for CADCraft, 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.cadcraft.yml
# flatpak run ai.storyteller.cadcraft
#
# For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit.
id: ai.storyteller.cadcraft
runtime: org.freedesktop.Platform
runtime-version: '25.08'
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: cadcraft
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: cadcraft
buildsystem: simple
build-options:
env:
CARGO_HOME: /run/build/cadcraft/cargo
CARGO_NET_OFFLINE: 'true'
build-commands:
- cargo --offline build --release --locked -p cadcraft
- install -Dm755 target/release/cadcraft /app/bin/cadcraft
- install -Dm644 packaging/linux/ai.storyteller.cadcraft.desktop /app/share/applications/ai.storyteller.cadcraft.desktop
- install -Dm644 packaging/linux/ai.storyteller.cadcraft.mime.xml /app/share/mime/packages/ai.storyteller.cadcraft.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.cadcraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.cadcraft.metainfo.xml
sources:
- type: dir
path: ../../..
skip:
- target
- dist
- corpus
- build-flatpak
- .flatpak-builder
- cargo-sources.json
+70
View File
@@ -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: cadcraft
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: ${CADCRAFT_MAINTAINER}
vendor: Learning Machines LLC
homepage: https://getartcraft.com/apps/cadcraft
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 cadcraft-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/cadcraft
dst: /usr/bin/cadcraft
file_info: { mode: 0755 }
- src: ${STAGE}/usr/bin/cadcraft-cli
dst: /usr/bin/cadcraft-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
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Build and package CADCraft for Linux (<arch> is x86_64 or aarch64):
#
# $DIST/cadcraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
# $DIST/cadcraft-<version>-linux-<arch>.deb Debian, Ubuntu, Mint, Pop!_OS, ...
# $DIST/cadcraft-<version>-linux-<arch>.rpm Fedora, openSUSE, RHEL, ...
# $DIST/cadcraft-<version>-linux-<arch>.tar.gz plain FHS-style tree (bin/, share/)
#
# Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"]
#
# Needs: cargo; nfpm for deb/rpm (https://nfpm.goreleaser.com); appimagetool for the AppImage
# (downloaded into $CARGO_TARGET_DIR if missing). Build on an old distro (CI: Ubuntu 22.04,
# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli.
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
HERE="$ROOT/packaging/linux"
APP_ID=ai.storyteller.cadcraft
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 CADCRAFT_MAINTAINER="${CADCRAFT_MAINTAINER:-CADCraft maintainers <[email protected]>}"
BASENAME="cadcraft-$VERSION-linux-$ARCH"
echo "==> CADCraft $VERSION for Linux $ARCH ($FORMATS)"
if [ "$SKIP_BUILD" = 0 ]; then
(cd "$ROOT" && cargo build --release --locked -p cadcraft -p cadcraft-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/cadcraft" "$STAGE/usr/bin/cadcraft"
install -Dm755 "$BIN/cadcraft-cli" "$STAGE/usr/bin/cadcraft-cli"
strip "$STAGE/usr/bin/cadcraft" "$STAGE/usr/bin/cadcraft-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@/$CADCRAFT_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/cadcraft"
copy_docs "$STAGE/usr/share/doc/cadcraft"
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/CADCraft.AppDir"
cp -R "$STAGE" "$APPDIR"
mv "$APPDIR/usr/share/doc" "$WORK/doc-unused"
ln -s usr/bin/cadcraft "$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/cadcraft-cli" --version
echo "==> done"
ls -lh "$DIST"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# deb/rpm post-install and post-remove: refresh the desktop, MIME and icon caches.
# Every tool is optional; missing ones are skipped.
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v update-mime-database >/dev/null 2>&1; then
update-mime-database /usr/share/mime || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
fi
exit 0
+111
View File
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Template: packaging/macos/package.sh substitutes @VERSION@ (full, e.g. 0.2.0-rc.1),
@SHORT_VERSION@ (numeric X.Y.Z) and @BUILD_SHA@. -->
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>CADCraft</string>
<key>CFBundleName</key>
<string>CADCraft</string>
<key>CFBundleExecutable</key>
<string>CADCraft</string>
<key>CFBundleIdentifier</key>
<string>ai.storyteller.cadcraft</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>@SHORT_VERSION@</string>
<key>CFBundleVersion</key>
<string>@SHORT_VERSION@</string>
<key>CADCraftVersion</key>
<string>@VERSION@</string>
<key>CADCraftBuildCommit</key>
<string>@BUILD_SHA@</string>
<key>CFBundleIconFile</key>
<string>CADCraft</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.graphics-design</string>
<!-- Must match MACOSX_DEPLOYMENT_TARGET in package.sh. Big Sur is the first release on
Apple silicon and has the Metal feature set wgpu targets. -->
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © the CADCraft authors. MIT OR Apache-2.0.</string>
<!-- Drawing files CADCraft opens (declared here in case macOS has no type for them). -->
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>ai.storyteller.cadcraft.dxf</string>
<key>UTTypeDescription</key>
<string>Drawing Exchange Format</string>
<key>UTTypeConformsTo</key>
<array><string>public.data</string></array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array><string>dxf</string></array>
<key>public.mime-type</key>
<array><string>image/vnd.dxf</string></array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>ai.storyteller.cadcraft.dwg</string>
<key>UTTypeDescription</key>
<string>Drawing</string>
<key>UTTypeConformsTo</key>
<array><string>public.data</string></array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array><string>dwg</string></array>
<key>public.mime-type</key>
<array><string>image/vnd.dwg</string></array>
</dict>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Drawing Exchange Format</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array><string>ai.storyteller.cadcraft.dxf</string></array>
<key>CFBundleTypeIconFile</key>
<string>CADCraft</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Drawing</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array><string>ai.storyteller.cadcraft.dwg</string></array>
<key>CFBundleTypeIconFile</key>
<string>CADCraft</string>
</dict>
</array>
</dict>
</plist>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Hardened-runtime entitlements for Designcraft. Deliberately empty: the app is pure Rust,
loads no unsigned code, needs no JIT, and is not sandboxed (it opens files by path, from the
command line and drag-and-drop). Add exceptions here only with a reason in a comment. -->
<plist version="1.0">
<dict>
</dict>
</plist>
+50
View File
@@ -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/cadcraft-signing.keychain-db"
CERT="$TMP/cadcraft-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
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# Build, sign and (optionally) notarize the macOS release artifacts:
#
# $DIST/cadcraft-<version>-macos-<arch>.dmg CADCraft.app on a drag-to-Applications DMG
# $DIST/cadcraft-cli-<version>-macos-<arch>.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/CADCraft.app"
DMG="$DIST/cadcraft-$VERSION-macos-$ARCH.dmg"
CLI_ZIP="$DIST/cadcraft-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 "==> CADCraft $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 cadcraft -p cadcraft-cli "${args[@]}")
fi
rm -rf "$WORK"
mkdir -p "$WORK/bin"
for bin in cadcraft cadcraft-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
}
# ---- CADCraft.app ----------------------------------------------------------------------------
echo "==> assembling $APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile).
cp "$WORK/bin/cadcraft" "$APP/Contents/MacOS/CADCraft"
cp "$ROOT/assets/app-icon/cadcraft.icns" "$APP/Contents/Resources/CADCraft.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@/${CADCRAFT_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/CADCraft"
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/CADCraft-notarize.zip"
notarize "$WORK/CADCraft-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/CADCraft.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 "CADCraft $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/cadcraft-cli-$VERSION-macos-$ARCH"
mkdir -p "$CLI_DIR"
cp "$WORK/bin/cadcraft-cli" "$CLI_DIR/"
copy_docs "$CLI_DIR"
sign --options runtime "$CLI_DIR/cadcraft-cli"
codesign --verify --strict --verbose=2 "$CLI_DIR/cadcraft-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/cadcraft-cli" --version
echo "==> done"
ls -lh "$DMG" "$CLI_ZIP"
+16
View File
@@ -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
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE application/wasm text/javascript text/html
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(wasm|js)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "^index\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
+89
View File
@@ -0,0 +1,89 @@
# Hosting CADCraft for the web
`cadcraft-web-<version>.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a
static site in `cadcraft-web-<version>/`:
| File | What it is |
|---|---|
| `index.html` | The page. It loads everything through relative URLs. |
| `cadcraft-web-<hash>.js` | wasm-bindgen glue (generated, ES module) |
| `cadcraft-web-<hash>_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/cadcraft-web/Trunk.toml`),
so the site works at a domain root (`https://example.com/`), under a prefix
(`https://example.com/tools/cadcraft/`) 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:** CADCraft 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 /cadcraft/ {
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
<iframe
src="https://example.com/cadcraft/"
title="CADCraft image editor"
style="width: 100%; height: 720px; border: 0;"
allow="fullscreen; clipboard-read; clipboard-write"
allowfullscreen>
</iframe>
```
- 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
CADCraft 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: `<iframe src="https://example.com/cadcraft/?webgl" ...>`.
A browser with neither WebGPU nor WebGL2 gets a message in place of the app.
+13
View File
@@ -0,0 +1,13 @@
# Netlify / Cloudflare Pages header rules (see HOSTING.md). Other hosts: set the same headers.
/*
X-Content-Type-Options: nosniff
/*.wasm
Content-Type: application/wasm
Cache-Control: public, max-age=31536000, immutable
/*.js
Cache-Control: public, max-age=31536000, immutable
/index.html
Cache-Control: no-cache
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Build the browser version and zip it: $DIST/cadcraft-web-<version>.zip
#
# Usage: packaging/web/package.sh [--skip-build]
#
# Needs: trunk (brew install trunk / cargo install trunk --locked) and the wasm32-unknown-unknown
# target. The zip holds a self-contained static site in cadcraft-web-<version>/ that works
# from any URL path and inside an <iframe>. Hosting notes: packaging/web/README.md.
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
HERE="$ROOT/packaging/web"
if [ "${1:-}" != "--skip-build" ]; then
command -v trunk >/dev/null || { echo "error: trunk not found (cargo install trunk --locked)" >&2; exit 1; }
(cd "$ROOT/apps/cadcraft-web" && trunk build --release)
fi
SITE="$ROOT/dist/web"
[ -f "$SITE/index.html" ] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
# Paths must be relative so the site works under any prefix (public_url = "./" in Trunk.toml).
if grep -Eq '(src|href)="/[^/]' "$SITE/index.html"; then
echo "error: $SITE/index.html has root-absolute URLs; it would break when served from a sub-path" >&2
exit 1
fi
NAME="cadcraft-web-$VERSION"
WORK="$CARGO_TARGET_DIR/web-package"
rm -rf "$WORK"
mkdir -p "$WORK/$NAME"
cp -R "$SITE/." "$WORK/$NAME/"
# Sample server configs (MIME type, caching, compression); harmless where unused.
cp "$HERE/_headers" "$HERE/.htaccess" "$WORK/$NAME/"
cp "$HERE/README.md" "$WORK/$NAME/HOSTING.md"
copy_docs "$WORK/$NAME"
rm -f "$DIST/$NAME.zip"
(cd "$WORK" && zip -qr9 "$DIST/$NAME.zip" "$NAME")
echo "wrote $DIST/$NAME.zip"
ls -lh "$DIST/$NAME.zip"
+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
CADCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
wix build cadcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
Per-machine install into Program Files, a Start Menu shortcut, and CADCraft offered under
"Open with" for .dxf and .dwg drawings (without taking over the user's default app).
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
Name="CADCraft"
Manufacturer="Learning Machines LLC"
Version="$(var.Version)"
UpgradeCode="119D7D76-0167-4A9E-8F37-09DA0235F68E"
Scope="perMachine"
Compressed="yes"
Language="1033">
<SummaryInformation Description="CADCraft computer-aided design" Manufacturer="Learning Machines LLC" />
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other
(MSI versions are numeric, so 1.2.3-rc.1 and 1.2.3-rc.2 are both 1.2.3). -->
<MajorUpgrade AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of CADCraft is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<Icon Id="CadcraftIcon.ico" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="CadcraftIcon.ico" />
<Property Id="ARPURLINFOABOUT" Value="https://getartcraft.com/apps/cadcraft" />
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/cadcraft" />
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="CADCraft" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<Feature Id="Main" Title="CADCraft" Level="1">
<ComponentGroupRef Id="CadcraftFiles" />
</Feature>
</Package>
<Fragment>
<ComponentGroup Id="CadcraftFiles" Directory="INSTALLFOLDER">
<Component Id="CadcraftApp" Guid="2AC18056-48DE-4079-AB79-4FDEF4FBB075">
<File Id="CadcraftExe" Source="$(var.BinDir)\cadcraft.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="CADCraft"
Description="CADCraft computer-aided design" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="CadcraftIcon.ico" IconIndex="0" />
</File>
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\cadcraft.exe" Value="[#CadcraftExe]" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\cadcraft.exe\shell\open\command" Value="&quot;[#CadcraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.dxf\OpenWithProgids" Name="CADCraft.Drawing" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.dwg\OpenWithProgids" Name="CADCraft.Drawing" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\CADCraft.Drawing\shell\open\command" Value="&quot;[#CadcraftExe]&quot; &quot;%1&quot;" Type="string" />
</Component>
<Component Id="CadcraftCli">
<File Id="CadcraftCliExe" Source="$(var.BinDir)\cadcraft-cli.exe" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
+131
View File
@@ -0,0 +1,131 @@
<#
.SYNOPSIS
Build, sign and package CADCraft for Windows.
.DESCRIPTION
Produces, in $env:DIST (default: dist/release):
cadcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
cadcraft-<version>-windows-<arch>-portable.zip cadcraft.exe + cadcraft-cli.exe
The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable
zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a
warning when no signing secrets are set).
Needs: Rust (MSVC toolchain + the target), the Windows SDK (rc.exe, signtool.exe),
and WiX v5: dotnet tool install --global wix --version 5.0.2
.EXAMPLE
pwsh packaging/windows/package.ps1 -Arch x64
pwsh packaging/windows/package.ps1 -Arch x86 -SkipBuild
pwsh packaging/windows/package.ps1 -Arch arm64 # cross-compiled; needs the MSVC ARM64 build tools
#>
param(
[ValidateSet('x64', 'x86', 'arm64')] [string] $Arch = 'x64',
[switch] $SkipBuild
)
$ErrorActionPreference = 'Stop'
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
function Invoke-Native([string] $What, [scriptblock] $Block) {
Write-Output "==> $What"
& $Block
if ($LASTEXITCODE -ne 0) { throw "$What failed with exit code $LASTEXITCODE" }
}
# The version lives in one place: [workspace.package] version in the root Cargo.toml.
$Version = $env:CADCRAFT_VERSION
if (-not $Version) {
$inPkg = $false
foreach ($line in Get-Content (Join-Path $Root 'Cargo.toml')) {
if ($line -match '^\s*\[') { $inPkg = ($line.Trim() -eq '[workspace.package]'); continue }
if ($inPkg -and $line -match '^\s*version\s*=\s*"([^"]+)"') { $Version = $Matches[1]; break }
}
}
if (-not $Version) { throw 'could not read [workspace.package] version from Cargo.toml' }
# MSI ProductVersion is numeric (major.minor.build); pre-release tags are dropped there.
$MsiVersion = ($Version -split '-')[0]
$Target = switch ($Arch) { 'x64' { 'x86_64-pc-windows-msvc' } 'x86' { 'i686-pc-windows-msvc' } 'arm64' { 'aarch64-pc-windows-msvc' } }
$Dist = if ($env:DIST) { $env:DIST } else { Join-Path $Root 'dist\release' }
$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root 'target' }
New-Item -ItemType Directory -Force -Path $Dist | Out-Null
if (-not $env:CADCRAFT_BUILD_SHA) { $env:CADCRAFT_BUILD_SHA = (git -C $Root rev-parse HEAD 2>$null) }
if (-not $env:CADCRAFT_BUILD_DATE) { $env:CADCRAFT_BUILD_DATE = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') }
Write-Output "CADCraft $Version for Windows $Arch ($Target)"
if (-not $SkipBuild) {
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
# proc-macros are unaffected.
$flagVar = 'CARGO_TARGET_' + ($Target.ToUpper() -replace '-', '_') + '_RUSTFLAGS'
[Environment]::SetEnvironmentVariable($flagVar, '-C target-feature=+crt-static')
# Fail the build (rather than warn) if the icon/VERSIONINFO can't be embedded.
$env:CADCRAFT_REQUIRE_WINRES = '1'
Invoke-Native "cargo build ($Target)" { cargo build --release --locked -p cadcraft -p cadcraft-cli --target $Target }
}
$Bin = Join-Path $TargetDir "$Target\release"
# Check both binaries' PE headers before packaging. Machine (COFF header) must match -Arch, so an
# x64 build can never ship labelled arm64. Subsystem (optional header): 2 = Windows GUI, 3 = console.
# The app must be GUI (no console window opens with it); the CLI must stay console so its output
# reaches the terminal.
function Get-PeHeader([string] $Path) {
$bytes = [System.IO.File]::ReadAllBytes($Path)
$pe = [BitConverter]::ToInt32($bytes, 0x3C)
return @{ Machine = [BitConverter]::ToUInt16($bytes, $pe + 4); Subsystem = [BitConverter]::ToUInt16($bytes, $pe + 0x5C) }
}
$Machine = switch ($Arch) { 'x64' { 0x8664 } 'x86' { 0x14C } 'arm64' { 0xAA64 } }
foreach ($check in @(@('cadcraft.exe', 2), @('cadcraft-cli.exe', 3))) {
$h = Get-PeHeader (Join-Path $Bin $check[0])
if ($h.Machine -ne $Machine) { throw "$($check[0]) is for machine 0x$('{0:X}' -f $h.Machine), expected 0x$('{0:X}' -f $Machine) ($Arch)" }
if ($h.Subsystem -ne $check[1]) { throw "$($check[0]) has PE subsystem $($h.Subsystem), expected $($check[1])" }
Write-Output "ok $($check[0]): $Arch, PE subsystem $($h.Subsystem)"
}
$Stage = Join-Path $TargetDir "windows-package\$Arch"
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
Copy-Item (Join-Path $Bin 'cadcraft.exe'), (Join-Path $Bin 'cadcraft-cli.exe') $Stage
& (Join-Path $PSScriptRoot 'sign.ps1') (Join-Path $Stage 'cadcraft.exe') (Join-Path $Stage 'cadcraft-cli.exe')
# ---- MSI ---------------------------------------------------------------------------------------
$Msi = Join-Path $Dist "cadcraft-$Version-windows-$Arch.msi"
Invoke-Native 'wix build' {
wix build (Join-Path $PSScriptRoot 'cadcraft.wxs') -arch $Arch `
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\cadcraft.ico')" `
-o $Msi
}
# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets.
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb'))
& (Join-Path $PSScriptRoot 'sign.ps1') $Msi
# ---- portable zip ------------------------------------------------------------------------------
$Portable = Join-Path $TargetDir "windows-package\cadcraft-$Version-windows-$Arch-portable"
Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Portable | Out-Null
Copy-Item (Join-Path $Stage '*.exe') $Portable
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
$p = Join-Path $Root $f
if (Test-Path $p) { Copy-Item $p $Portable }
}
# Builds made with craft-fonts (CRAFT_FONTS_DIR) embed its fonts: ship their licences.
if ($env:CRAFT_FONTS_DIR) {
foreach ($ofl in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue) {
Copy-Item $ofl.FullName (Join-Path $Portable "OFL-$($ofl.Directory.Name).txt")
}
}
$Zip = Join-Path $Dist "cadcraft-$Version-windows-$Arch-portable.zip"
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
Compress-Archive -Path $Portable -DestinationPath $Zip
# Smoke-test the CLI when this machine can run it. An ARM64 build made on an x64 runner can't run
# here; .github/workflows/windows-arm64.yml installs and runs it on ARM64 instead.
$HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
if ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') {
Invoke-Native 'cadcraft-cli --version' { & (Join-Path $Stage 'cadcraft-cli.exe') --version }
} else {
Write-Output "skipping cadcraft-cli --version: an $Arch build doesn't run on this $HostArch machine"
}
Get-Item $Msi, $Zip | Format-Table Name, Length
+95
View File
@@ -0,0 +1,95 @@
<#
.SYNOPSIS
Authenticode-sign files with signtool, using whichever signing material is configured.
.DESCRIPTION
Tries, in order:
1. A code-signing certificate file: WINDOWS_CERTIFICATE (base64 .pfx), WINDOWS_CERTIFICATE_PASSWORD
2. Azure Trusted Signing: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET,
AZURE_SIGNING_ENDPOINT (e.g. https://eus.codesigning.azure.net),
AZURE_SIGNING_ACCOUNT, AZURE_CERT_PROFILE
With neither, it prints a warning and leaves the files unsigned (exit code 0), so test builds
still produce installers.
This is the one place to adapt when the signing setup changes (an HSM/cloud key, a different
timestamp server, ...). Optional overrides: WINDOWS_TIMESTAMP_URL, SIGNTOOL (path to signtool.exe).
.EXAMPLE
pwsh packaging/windows/sign.ps1 dist/cadcraft.exe dist/Designcraft.msi
#>
param(
[Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
[string[]] $Files
)
$ErrorActionPreference = 'Stop'
function Write-Warn([string] $Message) {
if ($env:GITHUB_ACTIONS) { Write-Output "::warning::$Message" } else { Write-Warning $Message }
}
function Find-SignTool {
if ($env:SIGNTOOL) { return $env:SIGNTOOL }
$onPath = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($onPath) { return $onPath.Source }
$kits = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
$found = Get-ChildItem -Path $kits -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
Sort-Object FullName -Descending | Select-Object -First 1
if (-not $found) { throw 'signtool.exe not found (install the Windows SDK)' }
return $found.FullName
}
function Invoke-SignTool([string[]] $Arguments) {
& $script:SignTool @Arguments
if ($LASTEXITCODE -ne 0) { throw "signtool $($Arguments[0]) failed with exit code $LASTEXITCODE" }
}
$Files = $Files | ForEach-Object { (Resolve-Path $_).Path }
$haveCert = [bool]$env:WINDOWS_CERTIFICATE
$azureVars = 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', 'AZURE_SIGNING_ENDPOINT', 'AZURE_SIGNING_ACCOUNT', 'AZURE_CERT_PROFILE'
$haveAzure = -not ($azureVars | Where-Object { -not [Environment]::GetEnvironmentVariable($_) })
if (-not $haveCert -and -not $haveAzure) {
Write-Warn "Windows signing secrets not set (WINDOWS_CERTIFICATE or AZURE_*): leaving unsigned: $($Files -join ', ')"
exit 0
}
$script:SignTool = Find-SignTool
$common = @('sign', '/v', '/fd', 'SHA256', '/td', 'SHA256', '/d', 'CADCraft', '/du', 'https://github.com/storytold/cadcraft')
$tmp = Join-Path ([IO.Path]::GetTempPath()) "cadcraft-sign-$PID"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
if ($haveCert) {
Write-Output "Signing with WINDOWS_CERTIFICATE: $($Files -join ', ')"
$pfx = Join-Path $tmp 'cert.pfx'
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE))
$ts = if ($env:WINDOWS_TIMESTAMP_URL) { $env:WINDOWS_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
$signArgs = $common + @('/tr', $ts, '/f', $pfx)
if ($env:WINDOWS_CERTIFICATE_PASSWORD) { $signArgs += @('/p', $env:WINDOWS_CERTIFICATE_PASSWORD) }
Invoke-SignTool ($signArgs + $Files)
}
else {
Write-Output "Signing with Azure Trusted Signing ($env:AZURE_SIGNING_ACCOUNT / $env:AZURE_CERT_PROFILE): $($Files -join ', ')"
# The dlib authenticates with DefaultAzureCredential, which reads AZURE_TENANT_ID,
# AZURE_CLIENT_ID and AZURE_CLIENT_SECRET from the environment.
$pkg = Join-Path $tmp 'trusted-signing.zip'
Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client' -OutFile $pkg
Expand-Archive -Path $pkg -DestinationPath (Join-Path $tmp 'client') -Force
$dlib = Get-ChildItem -Path (Join-Path $tmp 'client') -Recurse -Filter Azure.CodeSigning.Dlib.dll |
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1
if (-not $dlib) { throw 'Azure.CodeSigning.Dlib.dll not found in Microsoft.Trusted.Signing.Client' }
$metadata = Join-Path $tmp 'metadata.json'
@{
Endpoint = $env:AZURE_SIGNING_ENDPOINT
CodeSigningAccountName = $env:AZURE_SIGNING_ACCOUNT
CertificateProfileName = $env:AZURE_CERT_PROFILE
} | ConvertTo-Json | Set-Content -Path $metadata -Encoding utf8
$ts = if ($env:WINDOWS_TIMESTAMP_URL) { $env:WINDOWS_TIMESTAMP_URL } else { 'http://timestamp.acs.microsoft.com' }
Invoke-SignTool ($common + @('/tr', $ts, '/dlib', $dlib.FullName, '/dmdf', $metadata) + $Files)
}
Invoke-SignTool (@('verify', '/pa', '/v') + $Files)
}
finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}