Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/release-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ jobs:
fi
tar -czf "dist/${{ matrix.archive }}" -C dist stringcast

- name: Prepare macOS DMG
if: runner.os == 'macOS'
run: packaging/macos/build_dmg.sh dist/macos/Stringcast.app dist/stringcast-macos.dmg

- name: Prepare Windows artifact
if: runner.os == 'Windows'
shell: pwsh
Expand All @@ -82,3 +86,11 @@ jobs:
name: ${{ matrix.archive }}
path: dist/${{ matrix.archive }}
if-no-files-found: error

- name: Upload macOS DMG artifact
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: stringcast-macos.dmg
path: dist/stringcast-macos.dmg
if-no-files-found: error
17 changes: 13 additions & 4 deletions docs/MACOS_APP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ The app is written to:
dist/macos/Stringcast.app
```

Build a drag-to-Applications DMG:

```bash
packaging/macos/build_dmg.sh
```

The DMG is written to:

```text
dist/stringcast-macos.dmg
```

Open it:

```bash
Expand Down Expand Up @@ -78,13 +90,10 @@ The app does not block startup on this permission check. If permissions are miss
## Current Limitations

- The app is unsigned.
- There is no custom app icon yet.
- Logs open in Finder rather than an in-app viewer.
- Packaging does not create a DMG or installer yet.
- The DMG is unsigned and not notarized.

## Next Packaging Steps

- Add an icon.
- Add log/status display.
- Add code signing and notarization.
- Produce a DMG for end users.
15 changes: 12 additions & 3 deletions docs/RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
# Release Artifacts

Stringcast can be distributed as downloadable binaries from GitHub Actions. This lets users run the Rust app without installing Cargo.
Stringcast can be distributed as downloadable artifacts from GitHub Actions. This lets users run the Rust app without installing Cargo.

The current artifacts are raw CLI binaries, not full desktop installers. A macOS app wrapper and richer desktop packaging are planned separately.
The macOS build includes both a tarball and a drag-to-Applications DMG. Linux and Windows currently ship archive artifacts.

## Build Artifacts

The `Release Builds` workflow creates:

```text
stringcast-macos.dmg
stringcast-macos.tar.gz
stringcast-linux-x86_64.tar.gz
stringcast-windows-x86_64.zip
```

Each archive includes:
Each platform archive includes:

- `stringcast` or `stringcast.exe`
- `Stringcast.app` in the macOS archive
Expand Down Expand Up @@ -54,6 +55,14 @@ gh run download <run-id> -D ./artifacts

## macOS Smoke Test

For the DMG:

```bash
open stringcast-macos.dmg
```

Then drag `Stringcast.app` to Applications, or launch it directly from the mounted volume for a quick test.

Unpack:

```bash
Expand Down
53 changes: 36 additions & 17 deletions packaging/macos/build_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ if ! command -v sips >/dev/null 2>&1; then
exit 1
fi

if ! command -v iconutil >/dev/null 2>&1; then
echo "missing iconutil; cannot generate app icon" >&2
if ! command -v python3 >/dev/null 2>&1; then
echo "missing python3; cannot generate app icon" >&2
exit 1
fi

Expand All @@ -65,21 +65,40 @@ xcrun --sdk macosx swiftc \
-o "$MACOS_DIR/StringcastMenu"

if [ -f "$ICON_SOURCE" ]; then
ICONSET_DIR="$OUT_DIR/StringcastIcon.iconset"
rm -rf "$ICONSET_DIR"
mkdir -p "$ICONSET_DIR"
sips -z 16 16 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_16x16.png" >/dev/null
sips -z 32 32 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_16x16@2x.png" >/dev/null
sips -z 32 32 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_32x32.png" >/dev/null
sips -z 64 64 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_32x32@2x.png" >/dev/null
sips -z 128 128 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_128x128.png" >/dev/null
sips -z 256 256 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_128x128@2x.png" >/dev/null
sips -z 256 256 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_256x256.png" >/dev/null
sips -z 512 512 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_256x256@2x.png" >/dev/null
sips -z 512 512 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_512x512.png" >/dev/null
sips -z 1024 1024 "$ICON_SOURCE" --out "$ICONSET_DIR/icon_512x512@2x.png" >/dev/null
iconutil -c icns "$ICONSET_DIR" -o "$RESOURCES_DIR/StringcastIcon.icns"
rm -rf "$ICONSET_DIR"
ICON_WORK_DIR="$OUT_DIR/StringcastIcon.work"
rm -rf "$ICON_WORK_DIR"
mkdir -p "$ICON_WORK_DIR"
sips -Z 1024 "$ICON_SOURCE" --out "$ICON_WORK_DIR/icon-scaled.png" >/dev/null
sips --padToHeightWidth 1024 1024 --padColor FFFFFF "$ICON_WORK_DIR/icon-scaled.png" --out "$ICON_WORK_DIR/icon-1024.png" >/dev/null 2>&1
for size in 16 32 64 128 256 512 1024; do
sips -z "$size" "$size" "$ICON_WORK_DIR/icon-1024.png" --out "$ICON_WORK_DIR/icon_${size}.png" >/dev/null
done
python3 - "$RESOURCES_DIR/StringcastIcon.icns" "$ICON_WORK_DIR" <<'PY'
import struct
import sys
from pathlib import Path

output = Path(sys.argv[1])
source_dir = Path(sys.argv[2])
entries = [
(b"icp4", "icon_16.png"),
(b"icp5", "icon_32.png"),
(b"icp6", "icon_64.png"),
(b"ic07", "icon_128.png"),
(b"ic08", "icon_256.png"),
(b"ic09", "icon_512.png"),
(b"ic10", "icon_1024.png"),
]

chunks = []
for icon_type, filename in entries:
data = (source_dir / filename).read_bytes()
chunks.append(icon_type + struct.pack(">I", len(data) + 8) + data)

payload = b"".join(chunks)
output.write_bytes(b"icns" + struct.pack(">I", len(payload) + 8) + payload)
PY
rm -rf "$ICON_WORK_DIR"
else
echo "warning: icon source not found: $ICON_SOURCE" >&2
echo " save the app icon PNG there, or set STRINGCAST_ICON=/path/to/icon.png" >&2
Expand Down
54 changes: 54 additions & 0 deletions packaging/macos/build_dmg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/sh
set -eu

if [ "${1:-}" = "--help" ]; then
cat <<'USAGE'
usage: packaging/macos/build_dmg.sh [app-path] [output-dmg-path] [volume-name]

Builds an unsigned macOS DMG with Stringcast.app and an Applications shortcut.

Defaults:
app-path: dist/macos/Stringcast.app
output-dmg-path: dist/stringcast-macos.dmg
volume-name: Stringcast
USAGE
exit 0
fi

APP_PATH=${1:-dist/macos/Stringcast.app}
DMG_PATH=${2:-dist/stringcast-macos.dmg}
VOLUME_NAME=${3:-Stringcast}

if [ "$(uname -s)" != "Darwin" ]; then
echo "macOS DMGs can only be built on macOS" >&2
exit 1
fi

if [ ! -d "$APP_PATH" ]; then
echo "missing app bundle: $APP_PATH" >&2
echo "run: cargo build --release && packaging/macos/build_app.sh" >&2
exit 1
fi

if ! command -v hdiutil >/dev/null 2>&1; then
echo "missing hdiutil; cannot generate DMG" >&2
exit 1
fi

DMG_DIR=$(dirname "$DMG_PATH")
mkdir -p "$DMG_DIR"

STAGING_DIR=$(mktemp -d "${TMPDIR:-/tmp}/stringcast-dmg.XXXXXX")
trap 'rm -rf "$STAGING_DIR"' EXIT INT TERM

cp -R "$APP_PATH" "$STAGING_DIR/Stringcast.app"
ln -s /Applications "$STAGING_DIR/Applications"

hdiutil create \
-volname "$VOLUME_NAME" \
-srcfolder "$STAGING_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"

echo "Built $DMG_PATH"
Loading