Files
goldenChart/scripts/generate-desktop-latest-json.mjs
T
2026-06-17 10:12:21 +09:00

149 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
/**
* dist-desktop/updates → latest.json (Tauri updater v2)
*
* Usage:
* node scripts/generate-desktop-latest-json.mjs \
* --version 0.1.0 \
* --dir dist-desktop/updates \
* --base-url https://stock.exdev.co.kr/desktop/updates \
* --notes "Release notes"
*/
import fs from 'node:fs';
import path from 'node:path';
function arg(name, fallback) {
const i = process.argv.indexOf(name);
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
}
const version = arg('--version', '0.1.0');
const dir = path.resolve(arg('--dir', 'dist-desktop/updates'));
const rawBaseUrl = arg('--base-url', 'https://stock.exdev.co.kr/desktop/updates');
const baseUrl = rawBaseUrl
.replace(/\/$/, '')
.replace(/^http:\/\//i, 'https://')
.replace(/^https:\/\/exdev\.co\.kr/i, 'https://stock.exdev.co.kr');
const notes = arg('--notes', `GoldenChart Desktop ${version}`);
if (!fs.existsSync(dir)) {
console.error('Directory not found:', dir);
process.exit(1);
}
const files = fs.readdirSync(dir);
function versionFromName(name) {
const m = name.match(/(\d+\.\d+\.\d+)/);
return m ? m[1] : '0.0.0';
}
function cmpSemver(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d !== 0) return d;
}
return 0;
}
/** @type {Record<string, { url: string; signature: string; bundleVersion: string }>} */
const platforms = {};
/** Windows updater bundle 우선순위 (낮을수록 preferred) — setup.exe 수동 설치용은 제외 */
function windowsBundlePriority(name) {
const lower = name.toLowerCase();
if (lower.endsWith('.nsis.zip')) return 0;
if (lower.endsWith('.msi.zip')) return 1;
if (lower.endsWith('-setup.exe') || (lower.includes('setup') && lower.endsWith('.exe'))) return 9;
return 5;
}
function addPlatform(platformKey, bundleName) {
const bundlePath = path.join(dir, bundleName);
if (!fs.existsSync(bundlePath)) return;
const sigPath = `${bundlePath}.sig`;
let signature = '';
if (fs.existsSync(sigPath)) {
signature = fs.readFileSync(sigPath, 'utf8').trim();
} else {
console.warn(`[warn] missing signature: ${sigPath}`);
return;
}
const bundleVersion = versionFromName(bundleName);
const effectiveVersion = bundleVersion === '0.0.0' ? version : bundleVersion;
const existing = platforms[platformKey];
if (existing) {
const cmp = cmpSemver(effectiveVersion, existing.bundleVersion);
if (cmp < 0) return;
if (cmp === 0 && platformKey === 'windows-x86_64') {
const prevPri = windowsBundlePriority(path.basename(existing.url.split('/').pop() ?? ''));
const nextPri = windowsBundlePriority(bundleName);
if (nextPri > prevPri) return;
} else if (cmp === 0 && platformKey !== 'windows-x86_64') {
return;
}
}
platforms[platformKey] = {
url: `${baseUrl}/${encodeURIComponent(bundleName)}`,
signature,
bundleVersion: effectiveVersion,
};
}
for (const f of files) {
if (f.endsWith('.sig')) continue;
if (f.endsWith('.dmg')) continue; // 설치용 dmg — updater manifest 대상 아님
const lower = f.toLowerCase();
if (lower.includes('aarch64') && (lower.endsWith('.tar.gz') || lower.endsWith('.app.tar.gz'))) {
addPlatform('darwin-aarch64', f);
} else if ((lower.includes('x64') || lower.includes('x86_64')) && lower.endsWith('.tar.gz')) {
addPlatform('darwin-x86_64', f);
} else if (lower.includes('aarch64') && lower.endsWith('.app.tar.gz')) {
addPlatform('darwin-aarch64', f);
} else if (lower.endsWith('.nsis.zip') || (lower.includes('setup') && lower.endsWith('.zip'))) {
addPlatform('windows-x86_64', f);
} else if (lower.endsWith('-setup.exe') || (lower.includes('setup') && lower.endsWith('.exe'))) {
addPlatform('windows-x86_64', f);
} else if (lower.endsWith('.app.tar.gz')) {
addPlatform('darwin-aarch64', f);
}
}
const bundleMaxVersion = Object.values(platforms).reduce(
(max, p) => (cmpSemver(p.bundleVersion, max) > 0 ? p.bundleVersion : max),
'0.0.0',
);
// manifest 버전 = 빌드 버전(--version). 번들 파일명에 버전 없으면 CLI 버전 사용.
// dmg 파일명으로 올리지 않음 — tar.gz 내용과 불일치하면 업데이트 무한 루프.
let manifestVersion = version;
if (bundleMaxVersion !== '0.0.0' && cmpSemver(bundleMaxVersion, version) < 0) {
console.warn(`[warn] bundle version ${bundleMaxVersion} < build ${version} — using build version`);
}
const manifest = {
version: manifestVersion,
notes,
pub_date: new Date().toISOString(),
platforms: Object.fromEntries(
Object.entries(platforms).map(([k, v]) => [k, { url: v.url, signature: v.signature }]),
),
};
const outPath = path.join(dir, 'latest.json');
const body = `${JSON.stringify(manifest, null, 2)}\n`;
if (body.length < 8) {
console.error('[error] refusing to write empty latest.json');
process.exit(3);
}
const tmpPath = `${outPath}.tmp`;
fs.writeFileSync(tmpPath, body, 'utf8');
fs.renameSync(tmpPath, outPath);
console.log('Wrote', outPath);
console.log('Platforms:', Object.keys(platforms).join(', ') || '(none — sign updater bundles first)');
if (Object.keys(platforms).length === 0) {
process.exit(2);
}