server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物
This commit is contained in:
@@ -135,6 +135,11 @@ func (s *Server) handleSolve(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) handlePair(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if !s.auth(r) {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -143,11 +148,18 @@ func (s *Server) handlePair(w http.ResponseWriter, r *http.Request) {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": "请求格式不合法"})
|
||||
return
|
||||
}
|
||||
if req.Code == "" {
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": "配对码必填"})
|
||||
return
|
||||
}
|
||||
if s.pair == nil {
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": "配对服务未启用"})
|
||||
return
|
||||
}
|
||||
if err := s.pair(req.Code, req.Name); err != nil {
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": err.Error()})
|
||||
return
|
||||
|
||||
@@ -2,11 +2,14 @@ package pairing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -69,11 +72,27 @@ func AutoPair(server, email, password, deviceName, pubKeyB64 string) (uint64, er
|
||||
|
||||
// PairWithCode 凭配对码注册设备,返回服务器分配的设备 ID
|
||||
func PairWithCode(server, pairCode, deviceName, osName, version, pubKeyB64 string) (uint64, error) {
|
||||
pairCode = strings.ToUpper(strings.TrimSpace(pairCode))
|
||||
deviceName = strings.TrimSpace(deviceName)
|
||||
if deviceName == "" {
|
||||
deviceName = "macOS Agent"
|
||||
}
|
||||
pubKeyB64 = strings.TrimSpace(pubKeyB64)
|
||||
if pubKeyB64 == "" {
|
||||
return 0, fmt.Errorf("本机设备公钥为空,请重启客户端后重试")
|
||||
}
|
||||
pubKey, keyErr := base64.StdEncoding.DecodeString(pubKeyB64)
|
||||
if keyErr != nil || len(pubKey) != ed25519.PublicKeySize {
|
||||
return 0, fmt.Errorf("本机设备公钥格式错误,请重启客户端后重试")
|
||||
}
|
||||
code, raw, err := httpJSON("POST", server+"/api/v1/agent/pair", "", ginH{
|
||||
"code": pairCode, "deviceName": deviceName, "os": osName, "version": version, "publicKey": pubKeyB64,
|
||||
})
|
||||
if err != nil || code != 200 {
|
||||
return 0, fmt.Errorf("pair failed: %s", string(raw))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("配对请求失败:%w", err)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return 0, fmt.Errorf("%s", responseMessage(raw, "配对失败"))
|
||||
}
|
||||
id, _ := strconv.ParseUint(field(raw, "deviceId"), 10, 64)
|
||||
if id == 0 {
|
||||
@@ -81,3 +100,17 @@ func PairWithCode(server, pairCode, deviceName, osName, version, pubKeyB64 strin
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func responseMessage(raw []byte, fallback string) string {
|
||||
var envelope struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &envelope); err == nil && strings.TrimSpace(envelope.Message) != "" {
|
||||
return envelope.Message
|
||||
}
|
||||
if text := strings.TrimSpace(string(raw)); text != "" {
|
||||
return text
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package pairing
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPairWithCodeSendsNormalizedPayload(t *testing.T) {
|
||||
pub, _, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/agent/pair" || r.Method != http.MethodPost {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var body struct {
|
||||
Code string `json:"code"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Code != "AB23CD" || body.DeviceName != "macOS Agent" {
|
||||
t.Fatalf("payload was not normalized: %+v", body)
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(body.PublicKey)
|
||||
if err != nil || len(decoded) != ed25519.PublicKeySize {
|
||||
t.Fatalf("invalid public key: %q", body.PublicKey)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"code":0,"message":"ok","data":{"deviceId":42}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
id, err := PairWithCode(server.URL, " ab23cd ", "", "core", "0.3.0", base64.StdEncoding.EncodeToString(pub))
|
||||
if err != nil || id != 42 {
|
||||
t.Fatalf("PairWithCode() = %d, %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairWithCodeRejectsMissingPublicKey(t *testing.T) {
|
||||
_, err := PairWithCode("http://127.0.0.1:1", "AB23CD", "Mac", "core", "0.3.0", "")
|
||||
if err == nil || !strings.Contains(err.Error(), "公钥") {
|
||||
t.Fatalf("expected actionable public key error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairWithCodePreservesServerMessage(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"code":1001,"data":null,"message":"设备公钥格式不合法"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
pub, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
_, err := PairWithCode(server.URL, "AB23CD", "Mac", "core", "0.3.0", base64.StdEncoding.EncodeToString(pub))
|
||||
if err == nil || !strings.Contains(err.Error(), "设备公钥格式不合法") {
|
||||
t.Fatalf("expected server message, got %v", err)
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -9,8 +9,9 @@ import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
@@ -102,6 +103,16 @@ func main() {
|
||||
if id.DeviceID > 0 {
|
||||
return errors.New("设备已配对")
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
name = cfg.Name
|
||||
}
|
||||
if name == "" {
|
||||
name, _ = os.Hostname()
|
||||
}
|
||||
if name == "" {
|
||||
name = "macOS Agent"
|
||||
}
|
||||
did, err := pairing.PairWithCode(cfg.Server, code, name, "core", "0.2.0", id.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// 生成 icons/app.png(256x256 纯蓝方块图标)
|
||||
// 生成 icons/app.png(512x512 纯蓝方块图标,兼容 macOS DMG)
|
||||
const zlib = require("zlib");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const W = 256, H = 256;
|
||||
const W = 512, H = 512;
|
||||
const row = Buffer.alloc(1 + W * 4);
|
||||
row[0] = 0;
|
||||
for (let x = 0; x < W; x++) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 938 B |
Binary file not shown.
|
Before Width: | Height: | Size: 916 B |
+30
-7
@@ -1,4 +1,4 @@
|
||||
const { app, BrowserWindow, Tray, Menu, ipcMain, shell, nativeImage } = require('electron');
|
||||
const { app, BrowserWindow, Tray, Menu, Notification, ipcMain, shell, nativeImage } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -17,6 +17,7 @@ let localJsonTimer = null;
|
||||
let quitting = false;
|
||||
|
||||
const dataDir = path.join(app.getPath('userData'), 'data');
|
||||
const localJsonPath = path.join(dataDir, 'local.json');
|
||||
const coreBin = isWin ? 'agent-core.exe' : 'agent-core';
|
||||
const coreExe = app.isPackaged
|
||||
? path.join(process.resourcesPath, coreBin)
|
||||
@@ -30,7 +31,14 @@ function startCore() {
|
||||
}
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
try { coreProc?.kill(); } catch (e) { /* ignore */ }
|
||||
corePort = 0;
|
||||
coreToken = '';
|
||||
try { fs.rmSync(localJsonPath, { force: true }); } catch (e) { /* ignore */ }
|
||||
coreProc = spawn(coreExe, ['-dir', dataDir], { windowsHide: true });
|
||||
coreProc.on('error', (err) => {
|
||||
console.error('[core] process error: ' + err.message);
|
||||
win?.webContents.send('core-error', '核心程序启动失败:' + err.message);
|
||||
});
|
||||
coreProc.on('exit', (code) => {
|
||||
coreProc = null;
|
||||
win?.webContents.send('core-exit', code);
|
||||
@@ -42,7 +50,7 @@ function pollLocalJson() {
|
||||
if (localJsonTimer) clearInterval(localJsonTimer);
|
||||
localJsonTimer = setInterval(() => {
|
||||
try {
|
||||
const info = JSON.parse(fs.readFileSync(path.join(dataDir, 'local.json'), 'utf8'));
|
||||
const info = JSON.parse(fs.readFileSync(localJsonPath, 'utf8'));
|
||||
if (info.port && info.token) {
|
||||
corePort = info.port;
|
||||
coreToken = info.token;
|
||||
@@ -91,20 +99,31 @@ ipcMain.handle('core:save-config', (e, cfg) => { writeConfig(cfg); startCore();
|
||||
ipcMain.handle('core:restart', () => { startCore(); return true; });
|
||||
ipcMain.handle('app:open-data-dir', () => shell.openPath(dataDir));
|
||||
ipcMain.handle('app:open-web', (e, url) => shell.openExternal(url));
|
||||
ipcMain.handle('app:notify', (e, title, body) => {
|
||||
if (Notification.isSupported()) {
|
||||
new Notification({ title: String(title || 'EveryPublish'), body: String(body || '') }).show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle('app:set-autostart', (e, on) => {
|
||||
app.setLoginItemSettings({ openAtLogin: !!on });
|
||||
app.setLoginItemSettings({ openAtLogin: !!on, openAsHidden: true, args: ['--minimized'] });
|
||||
return app.getLoginItemSettings().openAtLogin;
|
||||
});
|
||||
ipcMain.handle('app:get-autostart', () => app.getLoginItemSettings().openAtLogin);
|
||||
ipcMain.handle('app:quit', () => { quitting = true; app.quit(); });
|
||||
|
||||
function readConfig() {
|
||||
try { return JSON.parse(fs.readFileSync(path.join(dataDir, 'config.json'), 'utf8')); }
|
||||
catch (e) { return { server: '', fakeAll: false }; }
|
||||
const defaults = {
|
||||
server: 'http://127.0.0.1:8090',
|
||||
webUrl: 'http://127.0.0.1:3003',
|
||||
fakeAll: false,
|
||||
};
|
||||
try { return { ...defaults, ...JSON.parse(fs.readFileSync(path.join(dataDir, 'config.json'), 'utf8')) }; }
|
||||
catch (e) { return defaults; }
|
||||
}
|
||||
function writeConfig(cfg) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify(cfg, null, 2));
|
||||
fs.writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify({ ...readConfig(), ...cfg }, null, 2), { mode: 0o600 });
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
@@ -172,6 +191,10 @@ async function runSmoke() {
|
||||
}
|
||||
}
|
||||
|
||||
app.on('before-quit', () => { quitting = true; try { coreProc?.kill(); } catch (e) { /* ignore */ } });
|
||||
app.on('before-quit', () => {
|
||||
quitting = true;
|
||||
if (localJsonTimer) clearInterval(localJsonTimer);
|
||||
try { coreProc?.kill(); } catch (e) { /* ignore */ }
|
||||
});
|
||||
app.on('window-all-closed', () => { /* 驻留托盘不退出 */ });
|
||||
app.on('activate', showWin);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "everypublish-client",
|
||||
"version": "0.2.0",
|
||||
"description": "EveryPublish Windows 客户端(Electron 壳 + Go agent-core)",
|
||||
"description": "EveryPublish 历史 Electron 调试壳(非 macOS 正式交付)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
@@ -17,12 +17,12 @@
|
||||
"productName": "EveryPublish",
|
||||
"directories": { "output": "release" },
|
||||
"files": ["main.js", "preload.js", "renderer/**/*", "icons/**/*"],
|
||||
"extraResources": [
|
||||
{ "from": "../dist/agent-core.exe", "to": "agent-core.exe" }
|
||||
],
|
||||
"win": {
|
||||
"target": [{ "target": "nsis", "arch": ["x64"] }],
|
||||
"icon": "icons/app.ico",
|
||||
"extraResources": [
|
||||
{ "from": "../dist/agent-core.exe", "to": "agent-core.exe" }
|
||||
],
|
||||
"signAndEditExecutable": false
|
||||
},
|
||||
"nsis": {
|
||||
|
||||
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
restart: () => ipcRenderer.invoke('core:restart'),
|
||||
openDataDir: () => ipcRenderer.invoke('app:open-data-dir'),
|
||||
openWeb: (url) => ipcRenderer.invoke('app:open-web', url),
|
||||
notify: (title, body) => ipcRenderer.invoke('app:notify', title, body),
|
||||
setAutoStart: (on) => ipcRenderer.invoke('app:set-autostart', on),
|
||||
getAutoStart: () => ipcRenderer.invoke('app:get-autostart'),
|
||||
quit: () => ipcRenderer.invoke('app:quit'),
|
||||
|
||||
@@ -13,6 +13,7 @@ document.querySelectorAll('.tab').forEach((tab) => {
|
||||
|
||||
// ---- 状态页 ----
|
||||
let currentServer = '';
|
||||
let currentWeb = 'http://127.0.0.1:3003';
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const s = await window.api.status();
|
||||
@@ -33,7 +34,7 @@ setInterval(refreshStatus, 3000);
|
||||
refreshStatus();
|
||||
|
||||
$('btn-open-web').addEventListener('click', () => {
|
||||
const url = currentServer || 'http://127.0.0.1:8090';
|
||||
const url = currentWeb || currentServer || 'http://127.0.0.1:3003';
|
||||
window.api.openWeb(url);
|
||||
});
|
||||
$('btn-open-data').addEventListener('click', () => window.api.openDataDir());
|
||||
@@ -66,35 +67,42 @@ $('btn-pair').addEventListener('click', async () => {
|
||||
// ---- 挑战页 ----
|
||||
let selectedChallenge = null;
|
||||
let pollTimer = null;
|
||||
let knownChallengeIds = new Set();
|
||||
const KIND_LABEL = { qr: '扫码', sms: '短信验证码', confirm: 'APP 确认', captcha: '图形验证码', pending: '等待设备' };
|
||||
|
||||
async function refreshChallenges() {
|
||||
try {
|
||||
const r = await window.api.challenges();
|
||||
const list = (r && r.list) || [];
|
||||
const fresh = list.filter((ch) => ch.challengeId && !knownChallengeIds.has(ch.challengeId));
|
||||
if (fresh.length && knownChallengeIds.size > 0 && window.api.notify) {
|
||||
window.api.notify('EveryPublish 需要处理挑战', fresh.map((ch) => ch.platform || '平台').join('、') + ' 有新的扫码或验证码任务');
|
||||
}
|
||||
knownChallengeIds = new Set(list.map((ch) => ch.challengeId).filter(Boolean));
|
||||
const badge = $('ch-badge');
|
||||
if (list.length > 0) { badge.textContent = list.length; badge.classList.remove('hidden'); }
|
||||
else badge.classList.add('hidden');
|
||||
const box = $('ch-list');
|
||||
box.innerHTML = '';
|
||||
box.replaceChildren();
|
||||
list.forEach((ch) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'item' + (selectedChallenge && selectedChallenge.challengeId === ch.challengeId ? ' selected' : '');
|
||||
const inner = [
|
||||
'<span><b>',
|
||||
ch.platform || '?',
|
||||
'</b> · ',
|
||||
KIND_LABEL[ch.kind] || ch.kind,
|
||||
'</span><span class="p">',
|
||||
ch.prompt || '',
|
||||
'</span>',
|
||||
].join('');
|
||||
div.innerHTML = inner;
|
||||
const label = document.createElement('span');
|
||||
const platform = document.createElement('b');
|
||||
platform.textContent = ch.platform || '?';
|
||||
label.append(platform, document.createTextNode(' · ' + (KIND_LABEL[ch.kind] || ch.kind)));
|
||||
const prompt = document.createElement('span');
|
||||
prompt.className = 'p';
|
||||
prompt.textContent = ch.prompt || '';
|
||||
div.append(label, prompt);
|
||||
div.addEventListener('click', () => selectChallenge(ch));
|
||||
box.appendChild(div);
|
||||
});
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<p class="hint">暂无待处理挑战</p>';
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'hint';
|
||||
empty.textContent = '暂无待处理挑战';
|
||||
box.appendChild(empty);
|
||||
}
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
@@ -149,7 +157,9 @@ startPolling();
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const cfg = await window.api.getConfig();
|
||||
$('set-server').value = (cfg && cfg.server) || '';
|
||||
$('set-server').value = (cfg && cfg.server) || 'http://127.0.0.1:8090';
|
||||
$('set-web').value = (cfg && cfg.webUrl) || 'http://127.0.0.1:3003';
|
||||
currentWeb = $('set-web').value;
|
||||
$('set-fake').checked = !!(cfg && cfg.fakeAll);
|
||||
$('set-autostart').checked = await window.api.getAutoStart();
|
||||
} catch (e) { /* 忽略 */ }
|
||||
@@ -158,12 +168,15 @@ loadSettings();
|
||||
|
||||
$('btn-save').addEventListener('click', async () => {
|
||||
const server = $('set-server').value.trim();
|
||||
const webUrl = $('set-web').value.trim();
|
||||
const res = $('set-result');
|
||||
if (!server) { res.textContent = '请填写服务器地址(内网部署填 http://服务器IP:8090)'; res.className = 'msg err'; return; }
|
||||
const cfg = { server: server, fakeAll: $('set-fake').checked };
|
||||
if (!webUrl) { res.textContent = '请填写网站地址'; res.className = 'msg err'; return; }
|
||||
const cfg = { server: server, webUrl: webUrl, fakeAll: $('set-fake').checked };
|
||||
await window.api.saveConfig(cfg);
|
||||
await window.api.setAutoStart($('set-autostart').checked);
|
||||
currentServer = server;
|
||||
currentWeb = webUrl;
|
||||
res.textContent = '已保存,核心进程已重启';
|
||||
res.className = 'msg ok';
|
||||
});
|
||||
|
||||
@@ -72,6 +72,8 @@
|
||||
<h2>设置</h2>
|
||||
<label>服务器地址(内网部署时填 http://服务器IP:8090)</label>
|
||||
<input id="set-server" placeholder="http://192.168.x.x:8090" />
|
||||
<label>网站地址(本地开发默认 http://127.0.0.1:3003)</label>
|
||||
<input id="set-web" placeholder="http://127.0.0.1:3003" />
|
||||
<label class="switch-row">
|
||||
<input type="checkbox" id="set-fake" />
|
||||
<span>模拟执行(无真实账号的安装联调)</span>
|
||||
|
||||
Vendored
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
|
||||
CORE_DIR="$ROOT_DIR/client/core"
|
||||
DIST_DIR="$ROOT_DIR/client/dist"
|
||||
RELEASE_DIR="$ROOT_DIR/client/macos/release"
|
||||
APP_DIR="$RELEASE_DIR/EveryPublish.app"
|
||||
ARCH=${MAC_ARCH:-arm64}
|
||||
SIGN_IDENTITY=${MAC_SIGN_IDENTITY:--}
|
||||
|
||||
case "$ARCH" in
|
||||
arm64) GOARCH=arm64; SWIFT_ARCH=arm64 ;;
|
||||
x64) GOARCH=amd64; SWIFT_ARCH=x86_64 ;;
|
||||
*) echo "Unsupported MAC_ARCH: $ARCH (use arm64 or x64)" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$DIST_DIR" "$RELEASE_DIR"
|
||||
(cd "$CORE_DIR" && GOOS=darwin GOARCH="$GOARCH" CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o "$DIST_DIR/agent-core" .)
|
||||
chmod 755 "$DIST_DIR/agent-core"
|
||||
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
|
||||
swiftc "$SCRIPT_DIR/EveryPublishMacOSApp.swift" \
|
||||
-target "$SWIFT_ARCH-apple-macosx13.0" \
|
||||
-O \
|
||||
-parse-as-library \
|
||||
-framework SwiftUI \
|
||||
-framework AppKit \
|
||||
-framework UserNotifications \
|
||||
-o "$APP_DIR/Contents/MacOS/EveryPublish"
|
||||
cp "$DIST_DIR/agent-core" "$APP_DIR/Contents/Resources/agent-core"
|
||||
chmod 755 "$APP_DIR/Contents/MacOS/EveryPublish" "$APP_DIR/Contents/Resources/agent-core"
|
||||
|
||||
cat > "$APP_DIR/Contents/Info.plist" <<PLIST
|
||||
<?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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key><string>EveryPublish</string>
|
||||
<key>CFBundleExecutable</key><string>EveryPublish</string>
|
||||
<key>CFBundleIdentifier</key><string>com.everypublish.macos</string>
|
||||
<key>CFBundleName</key><string>EveryPublish</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.3.0</string>
|
||||
<key>CFBundleVersion</key><string>0.3.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>13.0</string>
|
||||
<key>LSUIElement</key><false/>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
if [ "$SIGN_IDENTITY" = "-" ]; then
|
||||
DMG_SUFFIX="-local"
|
||||
echo "Signing app ad hoc for local testing (set MAC_SIGN_IDENTITY for distribution)"
|
||||
codesign --force --deep --sign - "$APP_DIR"
|
||||
else
|
||||
DMG_SUFFIX=""
|
||||
echo "Signing app with $SIGN_IDENTITY"
|
||||
codesign --force --deep --options runtime --timestamp --sign "$SIGN_IDENTITY" "$APP_DIR"
|
||||
fi
|
||||
codesign --verify --deep --strict "$APP_DIR"
|
||||
|
||||
if command -v hdiutil >/dev/null 2>&1; then
|
||||
STAGE_DIR=$(mktemp -d "$RELEASE_DIR/dmg-stage.XXXXXX")
|
||||
trap 'rm -rf "$STAGE_DIR"' EXIT
|
||||
cp -R "$APP_DIR" "$STAGE_DIR/EveryPublish.app"
|
||||
ln -s /Applications "$STAGE_DIR/Applications"
|
||||
rm -f "$RELEASE_DIR/EveryPublish-0.3.0-$ARCH.dmg" "$RELEASE_DIR/EveryPublish-0.3.0-$ARCH-local.dmg"
|
||||
hdiutil create -volname EveryPublish -srcfolder "$STAGE_DIR" -ov -format UDZO "$RELEASE_DIR/EveryPublish-0.3.0-$ARCH$DMG_SUFFIX.dmg" >/dev/null
|
||||
fi
|
||||
|
||||
file "$APP_DIR/Contents/MacOS/EveryPublish" "$APP_DIR/Contents/Resources/agent-core"
|
||||
echo "Built $APP_DIR"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
APP_SOURCE="$SCRIPT_DIR/release/EveryPublish.app"
|
||||
APP_DEST="${HOME}/Applications/EveryPublish.app"
|
||||
|
||||
if [ ! -d "$APP_SOURCE" ]; then
|
||||
echo "Build the app first: ./build-macos-app.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${HOME}/Applications"
|
||||
rm -rf "$APP_DEST"
|
||||
cp -R "$APP_SOURCE" "$APP_DEST"
|
||||
xattr -dr com.apple.quarantine "$APP_DEST" 2>/dev/null || true
|
||||
open "$APP_DEST"
|
||||
echo "Installed local test app at $APP_DEST"
|
||||
Reference in New Issue
Block a user