feat(release): make the signing-free install work with nothing but Windows
Some checks failed
deploy-site / deploy (push) Failing after 3m26s
Some checks failed
deploy-site / deploy (push) Failing after 3m26s
The manual install path still needed 7-Zip, which the target machine does not have, so "installable without a certificate" was not yet true. The channel now also publishes the app as byte-split zip parts, and the install script joins them and extracts with the built-in Windows Expand-Archive after verifying every part and the joined archive. Version 1.3.1 republishes the channel from a single build, because a version's artifacts can only match one build and published volumes are never overwritten.
This commit is contained in:
parent
c35c6f3e95
commit
0411f389d9
30 changed files with 234 additions and 118 deletions
|
|
@ -11,6 +11,9 @@
|
|||
// apps/desktop/release/<version>/portable.json (볼륨 인덱스: 이름/크기/sha256)
|
||||
// bucket/d3ro-voice.json (Scoop 매니페스트, 커밋 대상)
|
||||
//
|
||||
// zip 분할 부품(수동 설치용)도 함께 만든다 — Windows 내장 Expand-Archive로 해제할 수 있어
|
||||
// 사용자에게 7-Zip 설치를 요구하지 않는다. 7z 볼륨은 Scoop 전용으로 더 작다(162MiB vs 243MiB).
|
||||
//
|
||||
// 사용:
|
||||
// npm run build --workspace=@d3ro/desktop
|
||||
// node scripts/ci/build-portable.mjs # 7z 분할 볼륨
|
||||
|
|
@ -18,7 +21,15 @@
|
|||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
|
@ -34,9 +45,12 @@ const FEED = 'https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice'
|
|||
const PORTABLE_VERSION_PATH = `${FEED}/portable-${version}`
|
||||
const PORTABLE_LATEST_PATH = `${FEED}/portable-latest`
|
||||
const ARCHIVE_BASE = `D3RO-Voice-${version}-x64-portable`
|
||||
/** Cloudflare 본문 한도(100MiB)보다 여유를 둔 볼륨 크기 */
|
||||
/** Cloudflare 본문 한도(100MiB)보다 여유를 둔 7z 볼륨 크기 (Scoop 경로) */
|
||||
const VOLUME_SIZE = '95m'
|
||||
const MAX_VOLUME_BYTES = 95 * 1024 * 1024
|
||||
/** 수동 설치 스크립트용 zip 분할 부품 크기 */
|
||||
const ZIP_PART_SIZE = '90m'
|
||||
const MAX_ZIP_PART_BYTES = 95 * 1024 * 1024
|
||||
|
||||
const wantZip = process.argv.includes('--zip')
|
||||
const releaseDir = join(desktopDir, 'release', version)
|
||||
|
|
@ -225,6 +239,73 @@ writeFileSync(
|
|||
'utf8',
|
||||
)
|
||||
|
||||
// ── 수동 설치용 zip 분할 부품 ─────────────────────────────
|
||||
const zipTarget = join(releaseDir, `${ARCHIVE_BASE}.zip`)
|
||||
console.log(`[portable] 수동 설치용 zip 생성 (분할 ${ZIP_PART_SIZE})`)
|
||||
const zipBuild = spawnSync(
|
||||
'npx',
|
||||
[
|
||||
'electron-builder',
|
||||
'--win',
|
||||
'zip',
|
||||
'--x64',
|
||||
'--config',
|
||||
'electron-builder.yml',
|
||||
'--publish',
|
||||
'never',
|
||||
'-c.win.forceCodeSigning=false',
|
||||
'-c.npmRebuild=false',
|
||||
],
|
||||
{ cwd: desktopDir, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||
)
|
||||
if (zipBuild.status !== 0) {
|
||||
console.error(`[portable] zip 빌드 실패 (exit ${zipBuild.status ?? 'null'})`)
|
||||
process.exit(zipBuild.status ?? 1)
|
||||
}
|
||||
|
||||
const producedZip = readdirSync(releaseDir).find((name) =>
|
||||
name.endsWith('.zip') && name.includes(version) && !name.includes('.part'),
|
||||
)
|
||||
if (!producedZip) {
|
||||
console.error('[portable] zip 산출물을 찾을 수 없습니다.')
|
||||
process.exit(1)
|
||||
}
|
||||
if (join(releaseDir, producedZip) !== zipTarget) {
|
||||
renameSync(join(releaseDir, producedZip), zipTarget)
|
||||
}
|
||||
|
||||
// zip을 90MiB 단위로 바이트 분할한다 (사용자가 이어 붙여 Expand-Archive로 해제)
|
||||
const zipBytes = readFileSync(zipTarget)
|
||||
const partSize = 90 * 1024 * 1024
|
||||
const zipParts = []
|
||||
for (let offset = 0, index = 1; offset < zipBytes.length; offset += partSize, index += 1) {
|
||||
const slice = zipBytes.subarray(offset, Math.min(offset + partSize, zipBytes.length))
|
||||
const name = `${ARCHIVE_BASE}.zip.${String(index).padStart(3, '0')}`
|
||||
if (slice.length > MAX_ZIP_PART_BYTES) {
|
||||
console.error(`[portable] zip 부품이 너무 큽니다: ${name}`)
|
||||
process.exit(1)
|
||||
}
|
||||
writeFileSync(join(releaseDir, name), slice)
|
||||
zipParts.push({
|
||||
name,
|
||||
size: slice.length,
|
||||
sha256: createHash('sha256').update(slice).digest('hex'),
|
||||
url: `${PORTABLE_VERSION_PATH}/${name}`,
|
||||
})
|
||||
}
|
||||
const zipSha256 = createHash('sha256').update(zipBytes).digest('hex')
|
||||
|
||||
// 인덱스에 zip 부품 정보를 추가한다 (설치 스크립트가 사용)
|
||||
const indexJson = JSON.parse(readFileSync(join(releaseDir, 'portable.json'), 'utf8'))
|
||||
indexJson.zipArchive = `${ARCHIVE_BASE}.zip`
|
||||
indexJson.zipSize = zipBytes.length
|
||||
indexJson.zipSha256 = zipSha256
|
||||
indexJson.zipParts = zipParts
|
||||
writeFileSync(
|
||||
join(releaseDir, 'portable.json'),
|
||||
`${JSON.stringify(indexJson, null, 2)}\n`,
|
||||
'utf8',
|
||||
)
|
||||
console.log(
|
||||
[
|
||||
'[portable] 완료',
|
||||
|
|
@ -232,6 +313,7 @@ console.log(
|
|||
...volumeEntries.map(
|
||||
(entry) => ` ${entry.name} (${(entry.size / 1048576).toFixed(1)}MiB)`,
|
||||
),
|
||||
` zip : ${zipParts.length}개 부품 / 합계 ${(zipBytes.length / 1048576).toFixed(1)}MiB`,
|
||||
` 인덱스 : ${join(releaseDir, 'portable.json')}`,
|
||||
` scoop : ${join(root, 'bucket', 'd3ro-voice.json')}`,
|
||||
` 게시 : node scripts/ci/publish-portable-release.mjs`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue