diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 0000000..abb8f21 --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,284 @@ +name: Desktop Release + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + inputs: + platform: + description: "Desktop platform to build" + required: true + default: all + type: choice + options: + - all + - macos + - linux + - windows + publish: + description: "Upload artifacts to the GitHub Release" + required: true + default: true + type: boolean + +permissions: + contents: write + packages: read + +concurrency: + group: desktop-release-${{ github.ref }} + cancel-in-progress: true + +jobs: + release-meta: + name: Resolve release metadata + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.meta.outputs.tag }} + version: ${{ steps.meta.outputs.version }} + platform: ${{ steps.meta.outputs.platform }} + should_publish: ${{ steps.meta.outputs.should_publish }} + checkout_ref: ${{ steps.meta.outputs.checkout_ref }} + steps: + - name: Checkout selected ref + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Resolve tag and version + id: meta + shell: bash + run: | + set -euo pipefail + + if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then + platform="all" + should_publish="true" + else + platform="${{ github.event.inputs.platform }}" + if [[ "${{ github.event.inputs.publish }}" == "true" ]]; then + should_publish="true" + else + should_publish="false" + fi + fi + + checkout_ref="${GITHUB_REF_NAME}" + + if [[ "$should_publish" == "true" ]]; then + if [[ ! "$checkout_ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::publish=true requires ref to look like vX.Y.Z or vX.Y.Z-suffix; got '$checkout_ref'." + exit 1 + fi + tag="$checkout_ref" + version="${checkout_ref#v}" + else + tag="" + version="$(node -p "require('./package.json').version")" + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "platform=$platform" >> "$GITHUB_OUTPUT" + echo "should_publish=$should_publish" >> "$GITHUB_OUTPUT" + echo "checkout_ref=$checkout_ref" >> "$GITHUB_OUTPUT" + + create-release: + name: Create GitHub release + needs: release-meta + if: needs.release-meta.outputs.should_publish == 'true' + runs-on: ubuntu-latest + steps: + - name: Ensure GitHub release exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + tag="${{ needs.release-meta.outputs.tag }}" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Release $tag already exists." + else + gh release create "$tag" --repo "${{ github.repository }}" --title "$tag" --generate-notes + fi + + publish-macos: + name: Publish macOS (${{ matrix.arch }}) + needs: [release-meta, create-release] + if: | + always() && + (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') && + (needs.release-meta.outputs.platform == 'all' || needs.release-meta.outputs.platform == 'macos') + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + arch: [x64, arm64] + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.release-meta.outputs.checkout_ref }} + + - uses: actions/setup-go@v6 + with: + go-version-file: daemon/go.mod + cache-dependency-path: daemon/go.sum + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Set desktop package version + run: node electron/scripts/set-desktop-version.cjs "${{ needs.release-meta.outputs.version }}" + + - name: Materialize notarization API key + shell: bash + env: + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + run: | + set -euo pipefail + key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" + printf '%s' "$APPLE_API_KEY_BASE64" | base64 -D > "$key_path" + chmod 600 "$key_path" + echo "APPLE_API_KEY=$key_path" >> "$GITHUB_ENV" + + - name: Build desktop release + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: npm run build:desktop -- --mac --${{ matrix.arch }} --publish never + + - name: Upload desktop artifacts to release + if: needs.release-meta.outputs.should_publish == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find release -maxdepth 1 -type f -print0 | sort -z) + if (( ${#files[@]} == 0 )); then + echo "::error::No release artifacts found in release" + exit 1 + fi + gh release upload "${{ needs.release-meta.outputs.tag }}" "${files[@]}" --clobber --repo "${{ github.repository }}" + + publish-linux: + name: Publish Linux + needs: [release-meta, create-release] + if: | + always() && + (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') && + (needs.release-meta.outputs.platform == 'all' || needs.release-meta.outputs.platform == 'linux') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.release-meta.outputs.checkout_ref }} + + - uses: actions/setup-go@v6 + with: + go-version-file: daemon/go.mod + cache-dependency-path: daemon/go.sum + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Set desktop package version + run: node electron/scripts/set-desktop-version.cjs "${{ needs.release-meta.outputs.version }}" + + - name: Build desktop release + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: npm run build:desktop -- --linux --x64 --publish never + + - name: Upload desktop artifacts to release + if: needs.release-meta.outputs.should_publish == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find release -maxdepth 1 -type f -print0 | sort -z) + if (( ${#files[@]} == 0 )); then + echo "::error::No release artifacts found in release" + exit 1 + fi + gh release upload "${{ needs.release-meta.outputs.tag }}" "${files[@]}" --clobber --repo "${{ github.repository }}" + + publish-windows: + name: Publish Windows (${{ matrix.arch }}) + needs: [release-meta, create-release] + if: | + always() && + (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') && + (needs.release-meta.outputs.platform == 'all' || needs.release-meta.outputs.platform == 'windows') + runs-on: windows-2025-vs2026 + strategy: + fail-fast: false + matrix: + arch: [x64, arm64] + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.release-meta.outputs.checkout_ref }} + + - uses: actions/setup-go@v6 + with: + go-version-file: daemon/go.mod + cache-dependency-path: daemon/go.sum + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Set desktop package version + run: node electron/scripts/set-desktop-version.cjs "${{ needs.release-meta.outputs.version }}" + + - name: Build desktop release + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: npm run build:desktop -- --win --${{ matrix.arch }} --publish never + + - name: Upload desktop artifacts to release + if: needs.release-meta.outputs.should_publish == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find release -maxdepth 1 -type f -print0 | sort -z) + if (( ${#files[@]} == 0 )); then + echo "::error::No release artifacts found in release" + exit 1 + fi + gh release upload "${{ needs.release-meta.outputs.tag }}" "${files[@]}" --clobber --repo "${{ github.repository }}" diff --git a/.gitignore b/.gitignore index 5001552..6a44fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ docs/ # Local scratch /tmp/ + +# Local signing / notarization credentials +/electron/cred/ diff --git a/electron/build/background.png b/electron/build/background.png new file mode 100644 index 0000000..067f3d0 Binary files /dev/null and b/electron/build/background.png differ diff --git a/electron/build/background@2x.png b/electron/build/background@2x.png new file mode 100644 index 0000000..85d1f4f Binary files /dev/null and b/electron/build/background@2x.png differ diff --git a/electron/electron-builder.yml b/electron/electron-builder.yml new file mode 100644 index 0000000..c40da4e --- /dev/null +++ b/electron/electron-builder.yml @@ -0,0 +1,81 @@ +appId: com.crew44.desktop +productName: Crew44 +copyright: Copyright © 2026 Crew44 +asar: true +directories: + output: release + buildResources: electron/build +files: + - package.json + - electron/**/* + - dist/**/* + - "!**/.DS_Store" + - "!**/*.map" +extraResources: + - from: bin/ + to: bin/ + filter: + - crew44-daemon* +publish: + provider: github + owner: getcrew44 + repo: crew44 + releaseType: release +mac: + category: public.app-category.developer-tools + icon: electron/assets/crew44.icns + target: + - dmg + - zip + artifactName: Crew44-${version}-mac-${arch}.${ext} + hardenedRuntime: true + notarize: true + gatekeeperAssess: false + strictVerify: false + entitlements: electron/build/entitlements.mac.plist + entitlementsInherit: electron/build/entitlements.mac.plist + electronLanguages: + - en + - en_GB + - zh_CN + - zh_TW +dmg: + artifactName: Crew44-${version}-mac-${arch}.${ext} + background: background.png + icon: electron/assets/crew44.icns + title: "${productName} ${version}" + iconSize: 96 + iconTextSize: 14 + window: + x: 160 + y: 120 + width: 793 + height: 496 + contents: + - x: 220 + y: 296 + type: file + - x: 573 + y: 296 + type: link + path: /Applications +win: + icon: electron/assets/crew44.ico + artifactName: Crew44-Setup-${version}-${arch}.${ext} + target: + - target: nsis + arch: + - x64 + - arm64 +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + shortcutName: Crew44 +linux: + category: Development + artifactName: Crew44-${version}-linux-${arch}.${ext} + target: + - AppImage + - deb +npmRebuild: false diff --git a/electron/scripts/build-daemon.cjs b/electron/scripts/build-daemon.cjs index c3f32d6..cc8b5b3 100644 --- a/electron/scripts/build-daemon.cjs +++ b/electron/scripts/build-daemon.cjs @@ -8,12 +8,17 @@ const path = require('path'); const repoRoot = path.resolve(__dirname, '..', '..'); const target = process.argv[2]; +const targetArch = process.argv[3]; const goosByTarget = { mac: 'darwin', win: 'windows', linux: 'linux' }; const goos = target ? goosByTarget[target] : null; if (target && !goos) { console.error(`Unknown target "${target}". Expected one of: mac, win, linux.`); process.exit(1); } +if (targetArch && !['amd64', 'arm64'].includes(targetArch)) { + console.error(`Unknown GOARCH "${targetArch}". Expected one of: amd64, arm64.`); + process.exit(1); +} const hostGoos = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'darwin' : 'linux'; const effectiveGoos = goos || hostGoos; @@ -26,7 +31,7 @@ fs.mkdirSync(outDir, { recursive: true }); const env = { ...process.env }; if (goos) { env.GOOS = goos; - env.GOARCH = process.env.GOARCH || 'amd64'; + env.GOARCH = targetArch || process.env.GOARCH || 'amd64'; env.CGO_ENABLED = '0'; } diff --git a/electron/scripts/build-desktop.cjs b/electron/scripts/build-desktop.cjs new file mode 100644 index 0000000..c18fcb8 --- /dev/null +++ b/electron/scripts/build-desktop.cjs @@ -0,0 +1,143 @@ +#!/usr/bin/env node +const { execFileSync, spawnSync } = require('child_process'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..', '..'); +const builderConfigPath = path.join(repoRoot, 'electron', 'electron-builder.yml'); +const daemonBuildScript = path.join(repoRoot, 'electron', 'scripts', 'build-daemon.cjs'); + +const PLATFORM_FLAGS = { + mac: '--mac', + win: '--win', + linux: '--linux', +}; + +const PLATFORM_TO_GO = { + mac: 'mac', + win: 'win', + linux: 'linux', +}; + +const ARCH_TO_GO = { + x64: 'amd64', + arm64: 'arm64', +}; + +function hostPlatform() { + if (process.platform === 'darwin') return 'mac'; + if (process.platform === 'win32') return 'win'; + return 'linux'; +} + +function hostArch() { + if (process.arch === 'arm64') return 'arm64'; + return 'x64'; +} + +function parseArgs(argv) { + const options = { + platforms: [], + archs: [], + passthrough: [], + }; + + for (const arg of argv) { + if (arg === '--') continue; + if (arg === '--mac') { + options.platforms.push('mac'); + continue; + } + if (arg === '--win') { + options.platforms.push('win'); + continue; + } + if (arg === '--linux') { + options.platforms.push('linux'); + continue; + } + if (arg === '--x64') { + options.archs.push('x64'); + continue; + } + if (arg === '--arm64') { + options.archs.push('arm64'); + continue; + } + options.passthrough.push(arg); + } + + if (options.platforms.length === 0) options.platforms.push(hostPlatform()); + if (options.archs.length === 0) options.archs.push(hostArch()); + return options; +} + +function unique(values) { + return [...new Set(values)]; +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: repoRoot, + stdio: 'inherit', + shell: process.platform === 'win32', + ...options, + }); + + if (result.error) { + console.error(`[build-desktop] failed to spawn ${command}: ${result.error.message}`); + process.exit(1); + } + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +function buildRenderer() { + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + run(npmCmd, ['exec', '--', 'vite', 'build']); +} + +function buildDaemon(platform, arch) { + execFileSync('node', [daemonBuildScript, PLATFORM_TO_GO[platform], ARCH_TO_GO[arch]], { + cwd: repoRoot, + stdio: 'inherit', + }); +} + +function builderArgsForTarget(platform, arch, passthrough, useScopedOutputDir) { + const args = [ + 'exec', + '--', + 'electron-builder', + '--config', + builderConfigPath, + PLATFORM_FLAGS[platform], + `--${arch}`, + ...passthrough, + ]; + + if (useScopedOutputDir) { + args.push(`-c.directories.output=release/${platform}-${arch}`); + } + + return args; +} + +function main() { + const parsed = parseArgs(process.argv.slice(2)); + const platforms = unique(parsed.platforms); + const archs = unique(parsed.archs); + const targets = platforms.flatMap(platform => archs.map(arch => ({ platform, arch }))); + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + + buildRenderer(); + + const useScopedOutputDir = targets.length > 1; + for (const target of targets) { + console.log(`[build-desktop] packaging ${target.platform}/${target.arch}`); + buildDaemon(target.platform, target.arch); + run(npmCmd, builderArgsForTarget(target.platform, target.arch, parsed.passthrough, useScopedOutputDir)); + } +} + +main(); diff --git a/electron/scripts/dist-mac.cjs b/electron/scripts/dist-mac.cjs deleted file mode 100644 index 6eda1d9..0000000 --- a/electron/scripts/dist-mac.cjs +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env node -// Signs the .app produced by `electron-builder --dir --mac`, wraps it in -// a signed DMG via macOS-builtin `hdiutil`, and (when SKIP_NOTARIZE isn't -// set) notarizes + staples it via `xcrun notarytool` using a keychain -// profile. -// -// Why not let electron-builder do all this: -// - electron-builder's @electron/osx-sign signs ~2x as many files as -// this script, doubling per-file TSA timestamp round trips. Painful -// on China networks (and slow even on US ones). -// - electron-builder's dmg-builder downloads dmgbuild bundles from -// GitHub-hosted CDNs, which are unreliable from China. hdiutil ships -// with macOS; no network needed. -// - notarytool with a keychain profile is what worked in the bespoke -// dist.cjs flow before the electron-builder migration. -// -// Flags: -// --skip-sign Don't sign anything. Implies --skip-notarize. -// --skip-notarize Sign but don't notarize. Useful for local builds -// when you don't want to wait on Apple's notary. -// -// Env: -// APPLE_SIGNING_IDENTITY (or CSC_NAME / CSC_IDENTITY) -// Developer ID Application identity. Find with -// `security find-identity -p codesigning -v`. -// NOTARY_PROFILE Keychain profile name for xcrun notarytool. -// Defaults to `crew44-notarize`. Create one with: -// `xcrun notarytool store-credentials NOTARY_PROFILE` -const fs = require('fs'); -const path = require('path'); -const { spawnSync } = require('child_process'); - -const args = new Set(process.argv.slice(2)); -const SKIP_SIGN = args.has('--skip-sign'); -const SKIP_NOTARIZE = args.has('--skip-notarize') || SKIP_SIGN; - -const IDENTITY = process.env.APPLE_SIGNING_IDENTITY || process.env.CSC_IDENTITY || process.env.CSC_NAME; -const NOTARY_PROFILE = process.env.NOTARY_PROFILE || 'crew44-notarize'; - -if (!SKIP_SIGN && !IDENTITY) { - console.error('APPLE_SIGNING_IDENTITY (or CSC_IDENTITY / CSC_NAME) must be set to a Developer ID Application identity.'); - console.error(' Find yours with: security find-identity -p codesigning -v'); - console.error(' Or pass --skip-sign to produce an unsigned DMG.'); - process.exit(1); -} - -const repoRoot = path.resolve(__dirname, '..', '..'); -const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); -const productName = pkg.productName || pkg.name; -const version = pkg.version; -const releaseDir = path.join(repoRoot, 'release'); -const appRoot = path.join(releaseDir, 'mac-arm64'); -const targetApp = path.join(appRoot, `${productName}.app`); -const entitlements = path.join(repoRoot, 'electron', 'build', 'entitlements.mac.plist'); - -if (!fs.existsSync(targetApp)) { - console.error(`Expected .app at ${targetApp}`); - console.error('Run electron-builder --dir --mac first (the npm scripts do this for you).'); - process.exit(1); -} - -function run(cmd, argv, opts = {}) { - const result = spawnSync(cmd, argv, { stdio: 'inherit', ...opts }); - if (result.status !== 0) { - throw new Error(`${cmd} ${argv.join(' ')} exited with status ${result.status}`); - } - return result; -} - -function sign(target, { entitlements = null } = {}) { - const argv = ['--force', '--timestamp', '--options', 'runtime', '--sign', IDENTITY]; - if (entitlements) argv.push('--entitlements', entitlements); - argv.push(target); - run('codesign', argv); -} - -const MACHO_MAGIC = new Set([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca]); - -function isMachO(filePath) { - try { - const fd = fs.openSync(filePath, 'r'); - const buf = Buffer.alloc(4); - const bytesRead = fs.readSync(fd, buf, 0, 4, 0); - fs.closeSync(fd); - if (bytesRead < 4) return false; - return MACHO_MAGIC.has(buf.readUInt32BE(0)); - } catch { - return false; - } -} - -function signTree(dir, entitlements) { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (entry.isSymbolicLink()) continue; - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - signTree(full, entitlements); - if (full.endsWith('.app')) { - sign(full, { entitlements }); - } else if (full.endsWith('.framework')) { - sign(full); - } - } else if (entry.isFile() && isMachO(full)) { - sign(full); - } - } -} - -function signApp() { - const contents = path.join(targetApp, 'Contents'); - console.log(`Signing nested binaries and bundles under ${contents}`); - signTree(contents, entitlements); - - console.log(`Signing outer app ${targetApp}`); - sign(targetApp, { entitlements }); - - console.log('Verifying signature'); - run('codesign', ['--verify', '--deep', '--strict', '--verbose=2', targetApp]); -} - -function makeDmg() { - const stagingDir = path.join(appRoot, 'dmg-staging'); - const dmgPath = path.join(releaseDir, `${productName}-${version}-arm64.dmg`); - - fs.rmSync(stagingDir, { recursive: true, force: true }); - fs.mkdirSync(stagingDir, { recursive: true }); - - const stagedApp = path.join(stagingDir, path.basename(targetApp)); - fs.cpSync(targetApp, stagedApp, { recursive: true, verbatimSymlinks: true }); - fs.symlinkSync('/Applications', path.join(stagingDir, 'Applications')); - - fs.rmSync(dmgPath, { force: true }); - console.log(`Creating ${dmgPath}`); - run('hdiutil', [ - 'create', - '-volname', `${productName} ${version}`, - '-srcfolder', stagingDir, - '-ov', - '-format', 'UDZO', - dmgPath, - ]); - - fs.rmSync(stagingDir, { recursive: true, force: true }); - return dmgPath; -} - -function notarize(dmgPath) { - console.log(`Submitting ${dmgPath} to notarytool (profile ${NOTARY_PROFILE})`); - const result = spawnSync('xcrun', [ - 'notarytool', 'submit', dmgPath, - '--keychain-profile', NOTARY_PROFILE, - '--wait', - '--output-format', 'json', - ], { encoding: 'utf8' }); - - process.stdout.write(result.stdout || ''); - process.stderr.write(result.stderr || ''); - - // notarytool emits JSON on stdout on success and Swift error text on - // stderr on transport failures (S3 upload timeout from poor networks, - // expired credentials, Apple service outage). Don't try to JSON-parse - // the latter — surface it as-is so the failure mode is legible. - const stdout = (result.stdout || '').trim(); - if (!stdout.startsWith('{')) { - console.error('\nnotarytool did not return JSON. Common causes:'); - console.error(' - Network timeout uploading the DMG to Apple/AWS S3'); - console.error(' - Keychain profile missing or expired (re-create with'); - console.error(` \`xcrun notarytool store-credentials ${NOTARY_PROFILE}\`)`); - console.error(' - Apple notary service outage'); - console.error(`\nThe signed DMG is still at: ${dmgPath}`); - console.error('Retry with: NOTARY_PROFILE=' + NOTARY_PROFILE + ' npm run dist:mac'); - console.error('Or skip notarization for now: npm run dist:mac:unsigned'); - process.exit(1); - } - - let parsed; - try { - parsed = JSON.parse(stdout); - } catch (err) { - throw new Error(`Could not parse notarytool output: ${err.message}`); - } - - if (parsed.status !== 'Accepted') { - console.error(`\nNotarization status: ${parsed.status}`); - console.error(`Submission id: ${parsed.id}`); - console.error(`Fetch the failure log with:`); - console.error(` xcrun notarytool log ${parsed.id} --keychain-profile ${NOTARY_PROFILE}`); - process.exit(1); - } - - console.log(`Stapling ${dmgPath}`); - run('xcrun', ['stapler', 'staple', dmgPath]); - run('xcrun', ['stapler', 'validate', dmgPath]); -} - -if (!SKIP_SIGN) signApp(); - -const dmgPath = makeDmg(); -console.log(`Wrote ${dmgPath}`); - -if (!SKIP_SIGN) { - sign(dmgPath); -} - -if (!SKIP_NOTARIZE) notarize(dmgPath); - -console.log(`\nDone. Distributable: ${dmgPath}`); diff --git a/electron/scripts/set-desktop-version.cjs b/electron/scripts/set-desktop-version.cjs new file mode 100644 index 0000000..daa074e --- /dev/null +++ b/electron/scripts/set-desktop-version.cjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const version = process.argv[2]; +if (!version) { + console.error('Usage: node electron/scripts/set-desktop-version.cjs '); + process.exit(1); +} + +const packageJsonPath = path.join(__dirname, '..', '..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +packageJson.version = version; +fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); diff --git a/package.json b/package.json index 911951d..6e4e22c 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,11 @@ "build:daemon:mac": "node electron/scripts/build-daemon.cjs mac", "build:daemon:win": "node electron/scripts/build-daemon.cjs win", "build:daemon:linux": "node electron/scripts/build-daemon.cjs linux", - "dist:mac": "npm run build:daemon:mac && vite build && electron-builder --dir --mac -c.mac.identity=null && node electron/scripts/dist-mac.cjs", - "dist:mac:unsigned": "npm run build:daemon:mac && vite build && electron-builder --dir --mac -c.mac.identity=null && node electron/scripts/dist-mac.cjs --skip-sign", - "dist:win": "npm run build:daemon:win && vite build && electron-builder --win", - "dist:linux": "npm run build:daemon:linux && vite build && electron-builder --linux", + "build:desktop": "node electron/scripts/build-desktop.cjs", + "dist:mac": "npm run build:desktop -- --mac --arm64 --publish never", + "dist:mac:unsigned": "npm run build:desktop -- --mac --arm64 --publish never -c.mac.identity=null -c.mac.notarize=false -c.mac.hardenedRuntime=false", + "dist:win": "npm run build:desktop -- --win --x64 --publish never", + "dist:linux": "npm run build:desktop -- --linux --x64 --publish never", "web:dev": "node daemon/scripts/web-dev.cjs", "mobile:start": "npm --workspace=@crew44/mobile run start --", "mobile:android": "npm --workspace=@crew44/mobile run android --", @@ -37,66 +38,11 @@ "test:mobile-pwa": "npm --workspace=@crew44/mobile-pwa run test", "test:web": "vitest run", "test:mobile": "npm --workspace=@crew44/mobile run test", - "desktop:smoke:linux": "npm run build:daemon:linux && vite build && electron-builder --dir --linux --x64", - "desktop:smoke:win": "npm run build:daemon:win && vite build && electron-builder --dir --win --x64", + "desktop:smoke:linux": "npm run build:desktop -- --linux --x64 --dir --publish never", + "desktop:smoke:win": "npm run build:desktop -- --win --x64 --dir --publish never", "test": "cd daemon && go test ./... && cd .. && vitest run && npm run test:mobile-pwa && npm run test:mobile", "clean": "rm -rf bin dist release .electron-app crew44-server crew44-daemon" }, - "build": { - "appId": "com.crew44.desktop", - "productName": "Crew44", - "copyright": "Copyright © 2026 Crew44", - "asar": true, - "directories": { - "output": "release", - "buildResources": "electron/build" - }, - "files": [ - "package.json", - "electron/**/*", - "dist/**/*", - "!**/.DS_Store", - "!**/*.map" - ], - "extraResources": [ - { - "from": "bin/", - "to": "bin/", - "filter": [ - "crew44-daemon*" - ] - } - ], - "mac": { - "category": "public.app-category.developer-tools", - "icon": "electron/assets/crew44.icns" - }, - "win": { - "icon": "electron/assets/crew44.ico", - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ] - }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "artifactName": "${productName}-Setup-${version}-${arch}.${ext}", - "shortcutName": "Crew44" - }, - "linux": { - "category": "Development", - "target": [ - "AppImage", - "deb" - ] - } - }, "dependencies": { "katex": "^0.17.0", "qrcode.react": "^4.2.0",