diff --git a/.github/workflows/repository-hygiene.yml b/.github/workflows/repository-hygiene.yml index 86cfeb2..f60fb16 100644 --- a/.github/workflows/repository-hygiene.yml +++ b/.github/workflows/repository-hygiene.yml @@ -168,10 +168,12 @@ jobs: # installed here too. - name: Install dependencies run: npm ci - - name: Install Linux native capture dependencies + - name: Install Linux packaging and native capture dependencies if: matrix.target == 'linux-amd64' run: >- sudo apt-get update && sudo apt-get install -y + zip + unzip libportal-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev diff --git a/README.md b/README.md index b4b958e..63ebecc 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Piik uses icon controls by default; select **EN** in the header to show labels. The App offers a local room, a temporary public link, or a connection to your own site. Keep the App running while sharing through it. -Download an archive beginning with **`piik-app`**. The filename also identifies the platform: +Download a ZIP beginning with **`piik-app`**. The filename also identifies the platform: | Filename contains | Platform | | --- | --- | diff --git a/README.zh-CN.md b/README.zh-CN.md index 971d275..1741e5c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -64,7 +64,7 @@ Piik 是一个免费开源的屏幕共享工具,适合游戏围观、一起看 Piik 默认使用图示按钮;点击顶部的**中**可显示文字。 客户端提供本地房间、临时公网邀请和连接已有站点三种模式,分享期间需保持 App 运行。 -下载以 **`piik-app`** 开头的压缩包,再按文件名中的平台标识选择: +下载以 **`piik-app`** 开头的 ZIP 压缩包,再按文件名中的平台标识选择: | 文件名包含 | 适用平台 | | --- | --- | diff --git a/docs/deployment.md b/docs/deployment.md index 2c9270c..f730ec1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -92,7 +92,7 @@ node scripts/package-client-candidate.mjs \ The wrapper downloads the pinned public-link sidecar, verifies its digest, builds the target App and available capture process, executes every packaged -runtime, and emits one `tar.gz` plus its SHA-256 file. +runtime from the extracted ZIP, and emits one `.zip` plus its SHA-256 file. Retain the descriptor and successful deployment output as release metadata. Do not create a follow-up source commit solely to duplicate their revision, asset, diff --git a/docs/operations/website.md b/docs/operations/website.md index 0752c70..ef4439c 100644 --- a/docs/operations/website.md +++ b/docs/operations/website.md @@ -74,7 +74,7 @@ ranges for seeking the soundtrack. Keep asset links relative so both a custom domain and GitHub's `/Piik/` project path work. Downloads are static links to the latest published GitHub packages, with explicit -Windows x64, macOS Apple silicon and Linux x64 cards. `PIIK_WEBSITE_RELEASE_DATA` +Windows x64, macOS Apple silicon and Linux x64 ZIP cards. `PIIK_WEBSITE_RELEASE_DATA` can point the build at a temporary JSON containing the GitHub release and the same-tag Gitee release with its attachment list. CI supplies this input; the builder validates version, source identity and platform URLs. A missing/incomplete diff --git a/scripts/archive-tool.mjs b/scripts/archive-tool.mjs index 11e0c14..ddfc3e9 100644 --- a/scripts/archive-tool.mjs +++ b/scripts/archive-tool.mjs @@ -1,5 +1,6 @@ +import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; export function tarExecutable() { if (process.platform !== "win32") return "tar"; @@ -8,3 +9,24 @@ export function tarExecutable() { const systemTar = join(systemRoot, "System32", "tar.exe"); return existsSync(systemTar) ? systemTar : "tar"; } + +export function createZip(source, archive) { + archive = resolve(archive); + if (process.platform === "linux") { + execFileSync("zip", ["-qry", archive, "."], { cwd: source, stdio: "pipe" }); + } else { + execFileSync(tarExecutable(), ["--format=zip", "-cf", archive, "."], { + cwd: source, stdio: "pipe", windowsHide: true, + }); + } +} + +export function extractZip(archive, destination) { + if (process.platform === "linux") { + execFileSync("unzip", ["-q", resolve(archive), "-d", destination], { stdio: "pipe" }); + } else { + execFileSync(tarExecutable(), ["-xpf", resolve(archive), "-C", destination], { + stdio: "pipe", windowsHide: true, + }); + } +} diff --git a/scripts/build-website.mjs b/scripts/build-website.mjs index cdea1c9..5abedb0 100644 --- a/scripts/build-website.mjs +++ b/scripts/build-website.mjs @@ -41,8 +41,8 @@ if (process.env.PIIK_WEBSITE_RELEASE_DATA) { const release = websiteDownloads(JSON.parse(await readFile(process.env.PIIK_WEBSITE_RELEASE_DATA, "utf8"))); homepage = homepage.replace(/
[\s\S]*?<\/p>/, `
${release.version}` + - `Download for your system, then extract the archive.` + - `选择对应系统,下载后解压。` + + `Download the ZIP for your system, then extract it.` + + `选择对应系统,下载 ZIP 后解压。` + `Release notes版本说明
`); homepage = homepage.replace(/]*data-download="([^"]+)"[^>]*data-provider="([^"]+)"[^>]*)>([\s\S]*?)<\/a\s*>/g, (original, attributes, target, provider, content) => { @@ -50,8 +50,8 @@ if (process.env.PIIK_WEBSITE_RELEASE_DATA) { const href = provider === 'github' ? item?.url : item?.mirrorURL; if (!href) return original; const label = provider === 'github' - ? { en: 'Download from GitHub', zh: 'GitHub 下载' } - : { en: 'Download from Gitee', zh: 'Gitee 备用下载' }; + ? { en: 'Download ZIP · GitHub', zh: '下载 ZIP · GitHub' } + : { en: 'Gitee alternative (ZIP)', zh: 'Gitee 备用下载(ZIP)' }; return `` + content .replace(/[\s\S]*?<\/span\s*>/, `${label.en}`) .replace(/[\s\S]*?<\/span\s*>/, `${label.zh}`) + ''; diff --git a/scripts/package-client-candidate.mjs b/scripts/package-client-candidate.mjs index 1121d69..8687377 100644 --- a/scripts/package-client-candidate.mjs +++ b/scripts/package-client-candidate.mjs @@ -21,7 +21,7 @@ import { pipeline } from "node:stream/promises"; import { fileURLToPath } from "node:url"; import { clientPackageTarget, CLOUDFLARED_VERSION } from "./client-package-targets.mjs"; -import { tarExecutable } from "./archive-tool.mjs"; +import { createZip, extractZip, tarExecutable } from "./archive-tool.mjs"; import { resetBuildWorkspace } from "./build-workspace.mjs"; function fail(message) { @@ -366,16 +366,19 @@ try { ]; if (capture) assembleArguments.push("--capture", capture); run(process.execPath, assembleArguments, repositoryRoot); - await verifyPackage(packageRoot, target, revision, temporaryRoot); mkdirSync(outputRoot, { recursive: false, mode: 0o700 }); const shortRevision = revision.slice(0, 7); - const archiveName = `piik-app-${target.id}-${shortRevision}.tar.gz`; + const archiveName = `piik-app-${target.id}-${shortRevision}.zip`; const archive = join(outputRoot, archiveName); - run(tarExecutable(), ["-czf", archive, "-C", packageRoot, "."], repositoryRoot); + createZip(packageRoot, archive); + const extractedRoot = join(temporaryRoot, "extracted"); + mkdirSync(extractedRoot); + extractZip(archive, extractedRoot); + await verifyPackage(extractedRoot, target, revision, temporaryRoot); const digest = sha256(archive); writeFileSync(join(outputRoot, `${archiveName}.sha256`), `${digest} ${archiveName}\n`, "ascii"); - writeFileSync(join(outputRoot, archiveName.replace(/\.tar\.gz$/, ".release.json")), + writeFileSync(join(outputRoot, archiveName.replace(/\.zip$/, ".release.json")), `${JSON.stringify({ schema: 2, version, revision, target: target.id, artifact: archiveName, artifactSha256: digest }, null, 2)}\n`); process.stdout.write(`${JSON.stringify({ diff --git a/site/downloads.ts b/site/downloads.ts index c530504..d2e063e 100644 --- a/site/downloads.ts +++ b/site/downloads.ts @@ -21,7 +21,7 @@ export function websiteDownloads(data: { github: unknown; mirror?: unknown }) { version: release.version, notes: release.url, packages: targets.map(target => { - const name = `piik-app-${target}-${release.revision!.slice(0, 7)}.tar.gz`; + const name = `piik-app-${target}-${release.revision!.slice(0, 7)}.zip`; const url = `https://github.com/TNTcraftHIM/Piik/releases/download/${release.version}/${name}`; const matches = primaryAssets.filter(asset => asset.name === name); const asset = matches[0]; diff --git a/tests/archive-tool.test.ts b/tests/archive-tool.test.ts new file mode 100644 index 0000000..62dfd42 --- /dev/null +++ b/tests/archive-tool.test.ts @@ -0,0 +1,49 @@ +import { execFileSync } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { createZip, extractZip } from "../scripts/archive-tool.mjs"; + +describe("App ZIP distribution", () => { + it("round-trips hidden files, spaced paths and executable permissions", () => { + const root = mkdtempSync(join(tmpdir(), "piik-archive-")); + try { + const source = join(root, "assembled app"); + const extracted = join(root, "extracted app"); + const archive = join(root, "Piik App.zip"); + const files = { + "piik-app": "#!/bin/sh\nexit 0\n", + "Piik App.app/Contents/MacOS/launcher": "#!/bin/sh\nexit 0\n", + "runtime/.native/画面.txt": "Native capture\n", + ".release-metadata": "Hidden metadata\n", + }; + for (const [name, content] of Object.entries(files)) { + const path = join(source, name); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content); + } + const executables = ["piik-app", "Piik App.app/Contents/MacOS/launcher"]; + for (const name of executables) chmodSync(join(source, name), 0o755); + createZip(source, archive); + expect(readFileSync(archive).subarray(0, 4)).toEqual(Buffer.from([0x50, 0x4b, 0x03, 0x04])); + mkdirSync(extracted); + extractZip(archive, extracted); + expect(readdirSync(extracted, { recursive: true }).sort()).toEqual( + readdirSync(source, { recursive: true }).sort(), + ); + for (const [name, content] of Object.entries(files)) { + expect(readFileSync(join(extracted, name), "utf8")).toBe(content); + } + if (process.platform !== "win32") { + for (const name of executables) { + expect(statSync(join(extracted, name)).mode & 0o777).toBe(0o755); + execFileSync(join(extracted, name), [], { cwd: extracted }); + } + } + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/publish-release.test.ts b/tests/publish-release.test.ts index de7700a..f86b6f3 100644 --- a/tests/publish-release.test.ts +++ b/tests/publish-release.test.ts @@ -14,7 +14,7 @@ const directory = mkdtempSync(join(tmpdir(), "piik-publisher-")); const version = "v1.0.1", revision = "a".repeat(40); const hash = (value: string | Buffer) => createHash("sha256").update(value).digest("hex"); for (const target of ["server", "windows-amd64", "linux-amd64", "darwin-arm64"]) { - const artifact = `${target}.tar.gz`; + const artifact = `${target}.${target === "server" ? "tar.gz" : "zip"}`; writeFileSync(join(directory, artifact), target); writeFileSync(join(directory, `${target}.release.json`), JSON.stringify({ schema: 2, version, revision, artifact, artifactSha256: hash(target), diff --git a/tests/release-automation.test.ts b/tests/release-automation.test.ts index acd8592..a022229 100644 --- a/tests/release-automation.test.ts +++ b/tests/release-automation.test.ts @@ -160,7 +160,7 @@ describe("release automation", () => { const run = () => spawnSync(process.execPath, [publisher, root, version, revision, "--dry-run"], { encoding: "utf8" }); try { for (const target of ["server", "windows-amd64", "linux-amd64", "darwin-arm64"]) { - const artifact = `${target}.tar.gz`; + const artifact = `${target}.${target === "server" ? "tar.gz" : "zip"}`; writeFileSync(join(root, artifact), target); const descriptor = { schema: 2, version, revision, artifact, artifactSha256: sha(target), ...(target === "server" ? { manifest: "server.manifest.tsv", manifestSha256: sha("manifest") } : { target }) }; @@ -176,7 +176,7 @@ describe("release automation", () => { writeFileSync(path, JSON.stringify({ ...JSON.parse(original), revision: "b".repeat(40) })); expect(run().stderr).toContain("Release identity mismatch"); writeFileSync(path, original); - writeFileSync(join(root, "windows-amd64.tar.gz"), "changed bytes"); + writeFileSync(join(root, "windows-amd64.zip"), "changed bytes"); expect(run().stderr).toContain("checksum mismatch"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/website-downloads.test.ts b/tests/website-downloads.test.ts index 053729d..ea48a70 100644 --- a/tests/website-downloads.test.ts +++ b/tests/website-downloads.test.ts @@ -7,7 +7,7 @@ const github = { tag_name: version, target_commitish: revision, draft: false, prerelease: false, html_url: `https://github.com/TNTcraftHIM/Piik/releases/tag/${version}`, assets: ["windows-amd64", "darwin-arm64", "linux-amd64"].map(target => { - const name = `piik-app-${target}-${revision.slice(0, 7)}.tar.gz`; + const name = `piik-app-${target}-${revision.slice(0, 7)}.zip`; return { name, state: "uploaded", size: 1024, browser_download_url: `https://github.com/TNTcraftHIM/Piik/releases/download/${version}/${name}` }; }), @@ -43,7 +43,7 @@ describe("website package downloads", () => { { ...mirror, tag_name: "v1.1.0" }, { ...mirror, assets: [] }]) { expect(websiteDownloads({ github, mirror: changed }).packages.every(item => item.mirrorURL === null)).toBe(true); } - for (const change of [{ size: 2048 }, { browser_download_url: "https://evil.example/package.tar.gz" }]) { + for (const change of [{ size: 2048 }, { browser_download_url: "https://evil.example/package.zip" }]) { const result = websiteDownloads({ github, mirror: { ...mirror, assets: [{ ...mirror.assets[0], ...change }, ...mirror.assets.slice(1)] } }); expect(result.packages[0]!.mirrorURL).toBeNull();