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
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
arch: universal
artifact_name: preview-macos-universal
artifact_suffix: macos-universal
app_path: dist/mac-universal/Hush.app
- runner: windows-latest
platform: windows
arch: x64
Expand Down Expand Up @@ -93,7 +94,11 @@ jobs:

- name: Verify native module (macOS)
if: matrix.platform == 'macos'
run: node scripts/verify-native-module.mjs darwin ${{ matrix.arch }}
run: node scripts/verify-native-module.mjs darwin ${{ matrix.arch }} ${{ matrix.app_path }}

- name: Verify macOS signing
if: matrix.platform == 'macos'
run: npm run verify:macos-signing -- ${{ matrix.app_path }}

- name: Verify native module (Windows)
if: matrix.platform == 'windows'
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/release-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ jobs:
run: npx electron-builder --mac --universal --publish never

- name: Verify native module
run: node scripts/verify-native-module.mjs darwin universal
run: node scripts/verify-native-module.mjs darwin universal dist/mac-universal/Hush.app

- name: Verify macOS signing
run: npm run verify:macos-signing -- dist/mac-universal/Hush.app

- name: Inspect macOS build metadata
shell: bash
Expand Down
Binary file added build/dmg-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/dmg-background@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions build/dmg-readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Hush for macOS

English
-------
Move Hush.app to the Applications folder.

If macOS says the app cannot be opened or is damaged, run:

sudo xattr -rd com.apple.quarantine /Applications/Hush.app

Then open Hush again from the Applications folder.

简体中文
--------
将 Hush.app 移动到“应用程序”文件夹。

如果 macOS 提示应用无法打开或已损坏,请运行:

sudo xattr -rd com.apple.quarantine /Applications/Hush.app

然后从“应用程序”文件夹重新打开 Hush。
2 changes: 2 additions & 0 deletions build/entitlements.mac.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
20 changes: 20 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,30 @@ mac:
- dmg
- zip
artifactName: ${name}-macos-${arch}-${version}.${ext}
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: false
dmg:
artifactName: ${name}-macos-${arch}-${version}.${ext}
background: build/dmg-background.png
iconSize: 80
iconTextSize: 12
window:
width: 540
height: 380
contents:
- x: 132
y: 154
type: file
- x: 408
y: 154
type: link
path: /Applications
- x: 270
y: 280
type: file
path: build/dmg-readme.txt
name: Read Me.txt
linux:
target:
- AppImage
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"build:dmg-background": "electron scripts/render-dmg-background.mjs",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win --x64",
Expand All @@ -25,7 +26,8 @@
"build:mac:x64": "npm run build && electron-builder --mac --x64",
"build:mac:universal": "npm run build && electron-builder --mac --universal",
"build:linux": "npm run build && electron-builder --linux",
"build:publish": "npm run build && electron-builder --publish always"
"build:publish": "npm run build && electron-builder --publish always",
"verify:macos-signing": "node scripts/verify-macos-signing.mjs"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
Expand Down
Binary file added resources/dreamcreator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/xiadown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
192 changes: 192 additions & 0 deletions scripts/render-dmg-background.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { app, BrowserWindow } from 'electron'

const WIDTH = 540
const HEIGHT = 380
const OUTPUT_DIR = join(process.cwd(), 'build')

app.commandLine.appendSwitch('disable-gpu')
app.commandLine.appendSwitch('force-device-scale-factor', '1')

app
.whenReady()
.then(async () => {
await mkdir(OUTPUT_DIR, { recursive: true })
await renderBackground()
})
.then(() => app.quit())
.catch((error) => {
console.error(error)
app.exit(1)
})

async function renderBackground() {
const window = new BrowserWindow({
width: WIDTH,
height: HEIGHT,
show: false,
frame: false,
transparent: false,
resizable: false,
webPreferences: {
offscreen: true,
sandbox: true
}
})

await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(createHtml())}`)
await window.webContents.executeJavaScript('document.fonts.ready')

const image = await window.webContents.capturePage()
const imageSize = image.getSize()
const retinaImage =
imageSize.width >= WIDTH * 2 && imageSize.height >= HEIGHT * 2
? image
: image.resize({ width: WIDTH * 2, height: HEIGHT * 2, quality: 'best' })

await writeFile(
join(OUTPUT_DIR, 'dmg-background.png'),
retinaImage.resize({ width: WIDTH, height: HEIGHT, quality: 'best' }).toPNG()
)
await writeFile(join(OUTPUT_DIR, 'dmg-background@2x.png'), retinaImage.toPNG())
window.destroy()
}

function createHtml() {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
* {
box-sizing: border-box;
}

html,
body {
width: ${WIDTH}px;
height: ${HEIGHT}px;
margin: 0;
overflow: hidden;
background: #f7f8f5;
}

body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
color: #1f241f;
}

.canvas {
position: relative;
width: ${WIDTH}px;
height: ${HEIGHT}px;
overflow: hidden;
background:
radial-gradient(circle at 50% 0%, rgba(234, 88, 12, 0.12), transparent 42%),
linear-gradient(180deg, #fff7ed 0%, #ffedd5 64%, #fff7ed 100%);
}

.canvas::before {
content: "";
position: absolute;
inset: 16px;
border: 1px solid rgba(194, 65, 12, 0.12);
border-radius: 8px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.46), rgba(255, 247, 237, 0.1));
}

.title {
position: absolute;
top: 32px;
left: 0;
width: 100%;
text-align: center;
font-size: 28px;
font-weight: 800;
line-height: 1.1;
color: #431407;
}

.subtitle {
position: absolute;
top: 70px;
left: 0;
width: 100%;
text-align: center;
color: #7c2d12;
font-size: 13px;
font-weight: 650;
}

.drop-zone {
position: absolute;
top: 106px;
width: 96px;
height: 96px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.48);
border: 1px solid rgba(194, 65, 12, 0.12);
box-shadow: 0 18px 56px rgba(154, 52, 18, 0.12);
}

.drop-zone.app {
left: 84px;
}

.drop-zone.applications {
right: 84px;
}

.arrow {
position: absolute;
top: 152px;
left: 228px;
width: 84px;
height: 3px;
border-radius: 999px;
background: #ea580c;
box-shadow: 0 0 0 5px rgba(234, 88, 12, 0.1);
}

.arrow::after {
content: "";
position: absolute;
top: -6px;
right: -1px;
width: 15px;
height: 15px;
border-top: 3px solid #ea580c;
border-right: 3px solid #ea580c;
transform: rotate(45deg);
}

.instruction {
position: absolute;
left: 56px;
right: 56px;
top: 226px;
text-align: center;
color: #431407;
font-size: 16px;
font-weight: 750;
line-height: 1.35;
}

</style>
</head>
<body>
<main class="canvas">
<div class="title">Install Hush</div>
<div class="subtitle">Drag Hush into Applications</div>
<div class="drop-zone app"></div>
<div class="arrow"></div>
<div class="drop-zone applications"></div>
<div class="instruction">Move Hush.app to the Applications folder.</div>
</main>
</body>
</html>`
}
81 changes: 81 additions & 0 deletions scripts/verify-macos-signing.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { existsSync } from 'node:fs'
import { isAbsolute, join } from 'node:path'
import { spawnSync } from 'node:child_process'

const [, , appPathArg = 'dist/mac-universal/Hush.app'] = process.argv
const appPath = isAbsolute(appPathArg) ? appPathArg : join(process.cwd(), appPathArg)

if (process.platform !== 'darwin') {
console.error('macOS signing verification can only run on darwin')
process.exit(2)
}

if (!existsSync(appPath)) {
console.error(`App bundle not found: ${appPath}`)
process.exit(1)
}

run('codesign', ['--verify', '--deep', '--strict', '--verbose=4', appPath], { capture: true })

const entitlements = run('codesign', ['-d', '--entitlements', ':-', appPath], { capture: true })
const requiredEntitlements = [
'com.apple.security.cs.allow-jit',
'com.apple.security.cs.allow-unsigned-executable-memory',
'com.apple.security.cs.disable-library-validation'
]

for (const entitlement of requiredEntitlements) {
if (!entitlements.includes(`<key>${entitlement}</key>`)) {
console.error(`Missing required entitlement: ${entitlement}`)
process.exit(1)
}
}

const infoPlistPath = join(appPath, 'Contents', 'Info.plist')
const executableName = run(
'plutil',
['-extract', 'CFBundleExecutable', 'raw', '-o', '-', infoPlistPath],
{
capture: true
}
).trim()
const executablePath = join(appPath, 'Contents', 'MacOS', executableName)
const electronVersion = run(
executablePath,
['-e', 'process.stdout.write(process.versions.electron || "")'],
{
capture: true,
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
}
).trim()

if (!electronVersion) {
console.error('Electron dyld smoke test did not return an Electron version')
process.exit(1)
}

console.log(`macOS signing verification passed for ${appPathArg} (Electron ${electronVersion})`)

function run(command, args, options = {}) {
const result = spawnSync(command, args, {
encoding: 'utf8',
env: options.env ?? process.env
})

if (result.status !== 0) {
if (result.stdout) process.stdout.write(result.stdout)
if (result.stderr) process.stderr.write(result.stderr)
console.error(`${command} ${args.join(' ')} failed`)
process.exit(result.status ?? 1)
}

if (options.capture) {
return result.stdout
}

if (result.stdout) process.stdout.write(result.stdout)
if (result.stderr) process.stderr.write(result.stderr)
return result.stdout
}
Loading
Loading