diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..ce0e08b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,10 @@
+APP_NAME=Openscreen
+BUNDLE_ID=com.siddharthvaddem.openscreen
+
+APPLE_ID=
+TEAM_ID=
+SIGN_IDENTITY="Developer ID Application: Samir Patil ()"
+CSC_NAME="Samir Patil ()"
+
+NOTARY_PROFILE=OpenScreen-notary
+APPLE_APP_SPECIFIC_PASSWORD=
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5638ffc..f42a92d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -3,6 +3,16 @@ name: Build Electron App
on:
workflow_dispatch:
+ inputs:
+ arch:
+ description: 'Architecture to build'
+ required: true
+ default: 'both'
+ type: choice
+ options:
+ - arm64
+ - x64
+ - both
jobs:
build-windows:
@@ -36,38 +46,180 @@ jobs:
build-macos:
runs-on: macos-latest
+ strategy:
+ matrix:
+ arch: ${{ github.event.inputs.arch == 'both' && fromJSON('["arm64", "x64"]') || fromJSON(format('["{0}"]', github.event.inputs.arch)) }}
+
steps:
+ # ─── Checkout ─────────────────────────────────────────────
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
+ # ─── Setup Node.js ────────────────────────────────────────
- name: Setup Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
- node-version: '22'
+ node-version: 22
+ cache: npm
+ # ─── Setup Python (needed by some native deps) ────────────
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.11'
+ # ─── Install Dependencies ─────────────────────────────────
- name: Install dependencies
run: npm ci
- - name: Install app dependencies
- run: npx electron-builder install-app-deps
-
- - name: Build macOS app
- run: npm run build:mac
+ # ─── Import Code Signing Certificate ──────────────────────
+ # This is the KEY step that makes CI signing work.
+ # We create a temporary keychain, import the .p12 cert into it,
+ # and set it as the default so codesign can find it.
+ - name: Import code signing certificate
env:
+ MAC_CERTIFICATE_P12: ${{ secrets.MAC_CERTIFICATE_P12 }}
+ MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
+ run: |
+ # Create a temporary keychain
+ KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain-db
+ KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
+
+ # Create and configure keychain
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+ # Decode and import certificate
+ echo "$MAC_CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
+ security import $RUNNER_TEMP/certificate.p12 \
+ -k "$KEYCHAIN_PATH" \
+ -P "$MAC_CERTIFICATE_PASSWORD" \
+ -T /usr/bin/codesign \
+ -T /usr/bin/security
+
+ # Allow codesign to access the keychain without UI prompt
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+ # Add to keychain search path (makes it the default)
+ security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
+
+ # Verify the identity is available
+ security find-identity -v -p codesigning "$KEYCHAIN_PATH"
+
+ # Clean up the .p12 file
+ rm -f $RUNNER_TEMP/certificate.p12
+
+ # ─── Build Vite + Electron ────────────────────────────────
+ - name: Build Vite + Electron
+ run: npx tsc && npx vite build
+
+ # ─── Package with electron-builder ────────────────────────
+ # electron-builder handles deep codesigning the .app bundle
+ # "notarize: false" in electron-builder.json5 prevents it from
+ # trying its own notarization flow
+ - name: Package .app bundle
+ run: npx electron-builder --mac --${{ matrix.arch }} --dir
+ env:
+ CSC_NAME: "Samir Patil (N26FZ4GW28)"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Upload macOS build
+ # ─── Read version from package.json ───────────────────────
+ - name: Get version
+ id: version
+ run: echo "version=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
+
+ # ─── Locate the .app bundle ───────────────────────────────
+ - name: Find .app bundle
+ id: find_app
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ echo "=== Release directory contents ==="
+ ls -laR "release/${VERSION}/" || echo "release/${VERSION}/ not found"
+ echo "=== Searching for .app bundle ==="
+ APP_BUNDLE=$(find "release/${VERSION}" -maxdepth 4 -name "*.app" -type d | head -n1)
+ if [ -z "$APP_BUNDLE" ]; then
+ echo "::error::No .app bundle found in release/${VERSION}/"
+ exit 1
+ fi
+ echo "app_bundle=$APP_BUNDLE" >> $GITHUB_OUTPUT
+ echo "Found: $APP_BUNDLE"
+
+ # ─── Verify .app signature ────────────────────────────────
+ - name: Verify .app code signature
+ run: codesign --verify --deep --strict "${{ steps.find_app.outputs.app_bundle }}"
+
+ # ─── Create DMG ───────────────────────────────────────────
+ - name: Create DMG
+ id: dmg
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ ARCH="${{ matrix.arch }}"
+ DMG_NAME="Openscreen-Mac-${ARCH}-${VERSION}.dmg"
+ RELEASE_DIR="release/${VERSION}"
+ DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}"
+ STAGING="${RELEASE_DIR}/dmg-staging"
+
+ mkdir -p "$STAGING"
+ cp -R "${{ steps.find_app.outputs.app_bundle }}" "$STAGING/"
+ ln -s /Applications "$STAGING/Applications"
+
+ hdiutil create \
+ -srcfolder "$STAGING" \
+ -volname "Openscreen" \
+ -fs HFS+ \
+ -fsargs "-c c=64,a=16,e=16" \
+ -format UDBZ \
+ "$DMG_OUTPUT"
+
+ rm -rf "$STAGING"
+
+ echo "dmg_path=$DMG_OUTPUT" >> $GITHUB_OUTPUT
+ echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
+
+ # ─── Sign DMG ─────────────────────────────────────────────
+ - name: Sign DMG
+ run: |
+ codesign --force \
+ --sign "Developer ID Application: Samir Patil (N26FZ4GW28)" \
+ --timestamp \
+ "${{ steps.dmg.outputs.dmg_path }}"
+
+ # ─── Notarize DMG ────────────────────────────────────────
+ # On CI we can't use keychain profiles for notarytool, so we
+ # pass credentials directly via env vars / flags
+ - name: Notarize DMG
+ run: |
+ xcrun notarytool submit "${{ steps.dmg.outputs.dmg_path }}" \
+ --apple-id "${{ secrets.APPLE_ID }}" \
+ --team-id "${{ secrets.APPLE_TEAM_ID }}" \
+ --password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" \
+ --wait
+ timeout-minutes: 15
+
+ # ─── Staple ───────────────────────────────────────────────
+ - name: Staple notarization ticket
+ run: xcrun stapler staple "${{ steps.dmg.outputs.dmg_path }}"
+
+ # ─── Validate ─────────────────────────────────────────────
+ - name: Validate stapled DMG
+ run: |
+ xcrun stapler validate "${{ steps.dmg.outputs.dmg_path }}"
+ spctl -a -vv -t install "${{ steps.dmg.outputs.dmg_path }}"
+
+ # ─── Upload Artifact ──────────────────────────────────────
+ - name: Upload notarized DMG
uses: actions/upload-artifact@v4
with:
- name: macos-installer
- path: release/**/*.dmg
+ name: openscreen-mac-${{ matrix.arch }}
+ path: ${{ steps.dmg.outputs.dmg_path }}
retention-days: 30
+ # ─── Cleanup Keychain ─────────────────────────────────────
+ - name: Cleanup keychain
+ if: always()
+ run: security delete-keychain $RUNNER_TEMP/build.keychain-db || true
+
build-linux:
runs-on: ubuntu-latest
steps:
diff --git a/.gitignore b/.gitignore
index 9393ef6..1f895bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ dist
dist-electron
dist-ssr
*.local
+.env
# Editor directories and files
.vscode/*
diff --git a/electron-builder.json5 b/electron-builder.json5
index 40fce0a..18498df 100644
--- a/electron-builder.json5
+++ b/electron-builder.json5
@@ -20,16 +20,18 @@
"!CONTRIBUTING.md",
"!LICENSE"
],
- "extraResources": [
- {
- "from": "public/wallpapers",
- "to": "assets/wallpapers"
- }
- ],
- "publish": [{"provider": "github"}],
-
- "mac": {
- "hardenedRuntime": false,
+ "extraResources": [
+ {
+ "from": "public/wallpapers",
+ "to": "assets/wallpapers"
+ }
+ ],
+
+ "mac": {
+ "notarize": false,
+ "hardenedRuntime": true,
+ "entitlements": "macos.entitlements",
+ "entitlementsInherit": "macos.entitlements",
"target": [
{
"target": "dmg",
@@ -38,13 +40,13 @@
],
"icon": "icons/icons/mac/icon.icns",
"artifactName": "${productName}-Mac-${arch}-${version}-Installer.${ext}",
- "extendInfo": {
- "NSAudioCaptureUsageDescription": "OpenScreen needs audio capture permission to record system audio.",
- "NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.",
- "NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.",
- "NSCameraUseContinuityCameraDeviceType": true,
- "com.apple.security.device.audio-input": true
- }
+ "extendInfo": {
+ "NSAudioCaptureUsageDescription": "OpenScreen needs audio capture permission to record system audio.",
+ "NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.",
+ "NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.",
+ "NSCameraUseContinuityCameraDeviceType": true,
+ "com.apple.security.device.audio-input": true
+ }
},
"linux": {
"target": [
@@ -54,14 +56,14 @@
"artifactName": "${productName}-Linux-${version}.${ext}",
"category": "AudioVideo"
},
- "win": {
- "target": [
- "nsis"
- ],
- "icon": "icons/icons/win/icon.ico"
- },
- "nsis": {
- "oneClick": false,
- "allowToChangeInstallationDirectory": true
- }
-}
+ "win": {
+ "target": [
+ "nsis"
+ ],
+ "icon": "icons/icons/win/icon.ico"
+ },
+ "nsis": {
+ "oneClick": false,
+ "allowToChangeInstallationDirectory": true
+ }
+}
diff --git a/macos.entitlements b/macos.entitlements
new file mode 100644
index 0000000..5c6ddcf
--- /dev/null
+++ b/macos.entitlements
@@ -0,0 +1,25 @@
+
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
+ com.apple.security.cs.disable-library-validation
+
+
+
+ com.apple.security.device.audio-input
+
+
+
+ com.apple.security.device.camera
+
+
+
diff --git a/scripts/build_macos.sh b/scripts/build_macos.sh
new file mode 100755
index 0000000..bd35710
--- /dev/null
+++ b/scripts/build_macos.sh
@@ -0,0 +1,216 @@
+#!/bin/bash
+#
+# OpenScreen macOS Build Script
+# Produces: release//OpenScreen-Mac--.dmg
+#
+# Usage: chmod +x scripts/build_macos.sh && ./scripts/build_macos.sh
+#
+
+set -euo pipefail
+
+# ── Load .env ─────────────────────────────────────────────────────────
+PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+ENV_FILE="${PROJECT_ROOT}/.env"
+
+if [ -f "$ENV_FILE" ]; then
+ set -a
+ source "$ENV_FILE"
+ set +a
+else
+ echo "ERROR: .env file not found at ${ENV_FILE}"
+ echo "Create one with APP_NAME, SIGN_IDENTITY, NOTARY_PROFILE, etc."
+ exit 1
+fi
+
+# ── Config ────────────────────────────────────────────────────────────
+VERSION=$(node -p "require('${PROJECT_ROOT}/package.json').version")
+RELEASE_DIR="${PROJECT_ROOT}/release/${VERSION}"
+ENTITLEMENTS="${PROJECT_ROOT}/macos.entitlements"
+ARCHS=("arm64" "x64")
+
+# ── Colors ────────────────────────────────────────────────────────────
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+BOLD='\033[1m'
+
+print_step() { echo -e "\n${CYAN}${BOLD}▸ $1${NC}"; }
+print_ok() { echo -e "${GREEN}✓ $1${NC}"; }
+print_warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
+print_err() { echo -e "${RED}✗ $1${NC}"; }
+
+# ── Preflight ─────────────────────────────────────────────────────────
+echo -e "\n${BOLD}╔══════════════════════════════════════════╗${NC}"
+echo -e "${BOLD}║ ${APP_NAME} macOS Build Script v${VERSION} ║${NC}"
+echo -e "${BOLD}╚══════════════════════════════════════════╝${NC}"
+
+print_step "Checking prerequisites..."
+
+if [[ "$(uname)" != "Darwin" ]]; then
+ print_err "This script must be run on macOS."
+ exit 1
+fi
+print_ok "Running on macOS ($(uname -m))"
+
+if ! command -v node &> /dev/null; then
+ print_err "Node.js not found. Please install Node.js first."
+ exit 1
+fi
+print_ok "Node.js found: $(node -v)"
+
+if ! command -v npm &> /dev/null; then
+ print_err "npm not found."
+ exit 1
+fi
+print_ok "npm found: $(npm -v)"
+
+# Check signing identity
+if ! security find-identity -v -p codesigning | grep -q "$SIGN_IDENTITY"; then
+ print_err "Signing identity not found: ${SIGN_IDENTITY}"
+ print_err "Run 'security find-identity -v -p codesigning' to see available identities."
+ exit 1
+fi
+print_ok "Signing identity found: ${SIGN_IDENTITY}"
+
+# Check notary profile
+if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" &> /dev/null; then
+ print_err "Notary profile '${NOTARY_PROFILE}' not found in keychain."
+ print_err "Run: xcrun notarytool store-credentials \"${NOTARY_PROFILE}\" --apple-id \"${APPLE_ID}\" --team-id \"${TEAM_ID}\""
+ exit 1
+fi
+print_ok "Notary profile found: ${NOTARY_PROFILE}"
+
+# Check entitlements
+if [ ! -f "$ENTITLEMENTS" ]; then
+ print_err "Entitlements file not found: ${ENTITLEMENTS}"
+ exit 1
+fi
+print_ok "Entitlements file found"
+
+# ── Clean ─────────────────────────────────────────────────────────────
+cd "$PROJECT_ROOT"
+
+print_step "Cleaning previous build artifacts..."
+rm -rf dist dist-electron "${RELEASE_DIR}"
+print_ok "Clean complete"
+
+# ── Install Dependencies ─────────────────────────────────────────────
+print_step "Installing dependencies..."
+npm ci
+print_ok "Dependencies installed"
+
+# ── Build Vite + Electron ────────────────────────────────────────────
+print_step "Building Vite + Electron... (this may take a minute)"
+npx tsc && npx vite build
+print_ok "Vite + Electron build complete"
+
+# ── Package, Sign, Notarize per Architecture ─────────────────────────
+for ARCH in "${ARCHS[@]}"; do
+ echo ""
+ echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${BOLD} Building for: ${ARCH}${NC}"
+ echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+
+ # ── Package with electron-builder ─────────────────────────────
+ print_step "[${ARCH}] Packaging with electron-builder..."
+
+ # Build .app only (--dir), electron-builder handles codesigning
+ # with hardenedRuntime + entitlements from electron-builder.json5
+ CSC_NAME="$CSC_NAME" npx electron-builder --mac --${ARCH} --dir
+
+ # Find the .app bundle
+ APP_BUNDLE=$(find "${RELEASE_DIR}" -maxdepth 2 -name "*.app" -type d | grep -i "${ARCH}\|mac" | head -n1)
+ if [ -z "$APP_BUNDLE" ]; then
+ # Fallback: find any .app in the output
+ APP_BUNDLE=$(find "${RELEASE_DIR}" -maxdepth 2 -name "*.app" -type d | head -n1)
+ fi
+
+ if [ -z "$APP_BUNDLE" ]; then
+ print_err "[${ARCH}] Could not find .app bundle in ${RELEASE_DIR}"
+ exit 1
+ fi
+ print_ok "[${ARCH}] App bundle: $(basename "$APP_BUNDLE")"
+
+ # ── Verify codesign on .app ───────────────────────────────────
+ print_step "[${ARCH}] Verifying .app code signature..."
+ codesign --verify --deep --strict "$APP_BUNDLE" 2>&1 || print_warn "[${ARCH}] Deep verify had warnings (may be expected pre-notarization)"
+ print_ok "[${ARCH}] .app signature verified"
+
+ # ── Create DMG ────────────────────────────────────────────────
+ DMG_NAME="${APP_NAME}-Mac-${ARCH}-${VERSION}.dmg"
+ DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}"
+ DMG_STAGING="${RELEASE_DIR}/dmg-staging-${ARCH}"
+
+ print_step "[${ARCH}] Creating DMG..."
+
+ rm -f "$DMG_OUTPUT"
+ rm -rf "$DMG_STAGING"
+
+ # Stage: app + Applications shortcut for drag-to-install
+ mkdir -p "$DMG_STAGING"
+ cp -R "$APP_BUNDLE" "$DMG_STAGING/"
+ ln -s /Applications "$DMG_STAGING/Applications"
+
+ hdiutil create \
+ -srcfolder "$DMG_STAGING" \
+ -volname "${APP_NAME}" \
+ -fs HFS+ \
+ -fsargs "-c c=64,a=16,e=16" \
+ -format UDBZ \
+ "$DMG_OUTPUT"
+
+ print_ok "[${ARCH}] DMG created: ${DMG_NAME}"
+ rm -rf "$DMG_STAGING"
+
+ # ── Sign DMG ──────────────────────────────────────────────────
+ print_step "[${ARCH}] Signing DMG..."
+ codesign --force --sign "$SIGN_IDENTITY" --timestamp "$DMG_OUTPUT"
+ print_ok "[${ARCH}] DMG signed"
+
+ # ── Notarize DMG ──────────────────────────────────────────────
+ print_step "[${ARCH}] Notarizing DMG with Apple... (this may take several minutes)"
+ xcrun notarytool submit "$DMG_OUTPUT" \
+ --keychain-profile "$NOTARY_PROFILE" \
+ --wait
+ print_ok "[${ARCH}] DMG notarized"
+
+ # ── Staple ────────────────────────────────────────────────────
+ print_step "[${ARCH}] Stapling notarization ticket..."
+ xcrun stapler staple "$DMG_OUTPUT"
+ print_ok "[${ARCH}] Ticket stapled"
+
+ # ── Validate ──────────────────────────────────────────────────
+ print_step "[${ARCH}] Validating stapled DMG..."
+ xcrun stapler validate "$DMG_OUTPUT"
+ print_ok "[${ARCH}] Validation passed"
+
+done
+
+# ── Clean up unpacked dirs (keep only DMGs) ───────────────────────────
+print_step "Cleaning up intermediate directories..."
+find "${RELEASE_DIR}" -maxdepth 1 -type d ! -name "$(basename "$RELEASE_DIR")" -exec rm -rf {} + 2>/dev/null || true
+print_ok "Cleanup complete"
+
+# ── Done ──────────────────────────────────────────────────────────────
+echo ""
+echo -e "${GREEN}${BOLD}════════════════════════════════════════════${NC}"
+echo -e "${GREEN}${BOLD} Build & Notarization Complete!${NC}"
+echo -e "${GREEN}${BOLD}════════════════════════════════════════════${NC}"
+echo ""
+
+for ARCH in "${ARCHS[@]}"; do
+ DMG_NAME="${APP_NAME}-Mac-${ARCH}-${VERSION}.dmg"
+ DMG_PATH="${RELEASE_DIR}/${DMG_NAME}"
+ if [ -f "$DMG_PATH" ]; then
+ DMG_SIZE=$(du -h "$DMG_PATH" | cut -f1)
+ echo -e " 📦 ${BOLD}${ARCH}:${NC} ${DMG_PATH}"
+ echo -e " 📏 ${BOLD}Size:${NC} ${DMG_SIZE}"
+ echo ""
+ fi
+done
+
+echo -e " ${GREEN}All DMGs are fully signed, notarized, and stapled!${NC}"
+echo -e " ${GREEN}Ready for distribution outside the Mac App Store.${NC}"
+echo ""