mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-01 16:51:53 +03:00
fix(release): package TUI compliance materials
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""Create one self-contained, licensed native TUI release archive."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
_SUPPORTED_TARGETS = {
|
||||
"darwin-arm64",
|
||||
"darwin-x64",
|
||||
"linux-arm64",
|
||||
"linux-x64",
|
||||
"win32-x64",
|
||||
}
|
||||
|
||||
|
||||
def _sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _source_archive(root: Path) -> bytes:
|
||||
included = [
|
||||
"README.md",
|
||||
"RELINKING.md",
|
||||
"SOURCE_OFFER.md",
|
||||
"package.json",
|
||||
"bun.lock",
|
||||
"tsconfig.json",
|
||||
"src",
|
||||
"scripts/build.ts",
|
||||
"scripts/prepare-target.ts",
|
||||
"scripts/release-notices.ts",
|
||||
"scripts/package-release.py",
|
||||
"licenses",
|
||||
]
|
||||
output = io.BytesIO()
|
||||
with tarfile.open(fileobj=output, mode="w:gz", format=tarfile.PAX_FORMAT) as archive:
|
||||
for relative in included:
|
||||
path = root / relative
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(path)
|
||||
archive.add(path, arcname=Path("nanobot-tui-source") / relative)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("usage: package-release.py <target>")
|
||||
target = sys.argv[1]
|
||||
if target not in _SUPPORTED_TARGETS:
|
||||
raise SystemExit(f"unsupported target: {target}")
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
project_root = root.parent
|
||||
extension = ".exe" if target.startswith("win32-") else ""
|
||||
asset = f"nanobot-tui-{target}{extension}"
|
||||
dist = root / "dist"
|
||||
|
||||
files = {
|
||||
asset: (dist / asset).read_bytes(),
|
||||
"THIRD_PARTY_NOTICES.txt": (dist / f"{asset}.THIRD_PARTY_NOTICES.txt").read_bytes(),
|
||||
"RELINKING.md": (root / "RELINKING.md").read_bytes(),
|
||||
"SOURCE_OFFER.md": (root / "SOURCE_OFFER.md").read_bytes(),
|
||||
"LICENSE": (project_root / "LICENSE").read_bytes(),
|
||||
"BUN-1.3.13-LICENSE.md": (root / "licenses" / "BUN-1.3.13-LICENSE.md").read_bytes(),
|
||||
"LGPL-2.0.txt": (root / "licenses" / "LGPL-2.0.txt").read_bytes(),
|
||||
"LGPL-2.1.txt": (root / "licenses" / "LGPL-2.1.txt").read_bytes(),
|
||||
"nanobot-tui-source.tar.gz": _source_archive(root),
|
||||
}
|
||||
manifest = "".join(f"{_sha256(content)} {name}\n" for name, content in files.items()).encode()
|
||||
files["MANIFEST.sha256"] = manifest
|
||||
|
||||
output = dist / f"{asset}.zip"
|
||||
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
||||
for name, content in files.items():
|
||||
archive.writestr(name, content)
|
||||
digest = _sha256(output.read_bytes())
|
||||
output.with_name(f"{output.name}.sha256").write_text(
|
||||
f"{digest} {output.name}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,137 @@
|
||||
import { readdir, readFile, writeFile } from "node:fs/promises"
|
||||
import { basename, join } from "node:path"
|
||||
|
||||
const target = process.argv[2]
|
||||
if (!target) throw new Error("target is required")
|
||||
const supportedTargets = new Set([
|
||||
"darwin-arm64",
|
||||
"darwin-x64",
|
||||
"linux-arm64",
|
||||
"linux-x64",
|
||||
"win32-x64",
|
||||
])
|
||||
if (!supportedTargets.has(target)) throw new Error(`unsupported target: ${target}`)
|
||||
const nativePackagesByTarget: Record<string, string[]> = {
|
||||
"darwin-arm64": ["@opentui/core-darwin-arm64"],
|
||||
"darwin-x64": ["@opentui/core-darwin-x64"],
|
||||
"linux-arm64": ["@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl"],
|
||||
"linux-x64": ["@opentui/core-linux-x64", "@opentui/core-linux-x64-musl"],
|
||||
"win32-x64": ["@opentui/core-win32-x64"],
|
||||
}
|
||||
|
||||
const root = join(import.meta.dir, "..")
|
||||
const projectRoot = join(root, "..")
|
||||
const extension = target.startsWith("win32-") ? ".exe" : ""
|
||||
const asset = `nanobot-tui-${target}${extension}`
|
||||
const output = join(root, "dist", `${asset}.THIRD_PARTY_NOTICES.txt`)
|
||||
|
||||
type PackageNotice = {
|
||||
name: string
|
||||
version: string
|
||||
license: string
|
||||
files: Array<{ name: string; content: string }>
|
||||
}
|
||||
|
||||
type PackageManifest = {
|
||||
name?: unknown
|
||||
version?: unknown
|
||||
license?: unknown
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
async function runtimePackageRoots(root: string): Promise<string[]> {
|
||||
const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8")) as PackageManifest
|
||||
const nodeModules = join(root, "node_modules")
|
||||
const devDependencies = new Set(Object.keys(manifest.devDependencies ?? {}))
|
||||
const pending = Object.keys(manifest.dependencies ?? {}).map((name) => ({ name, optional: false }))
|
||||
const visited = new Set<string>()
|
||||
const roots: string[] = []
|
||||
|
||||
while (pending.length) {
|
||||
const next = pending.shift()
|
||||
if (!next || visited.has(next.name)) continue
|
||||
const { name, optional } = next
|
||||
const path = join(nodeModules, name)
|
||||
if (!(await Bun.file(join(path, "package.json")).exists())) {
|
||||
if (optional) continue
|
||||
throw new Error(`required runtime package is missing: ${name}`)
|
||||
}
|
||||
const dependency = JSON.parse(await readFile(join(path, "package.json"), "utf8")) as PackageManifest
|
||||
visited.add(name)
|
||||
roots.push(path)
|
||||
pending.push(
|
||||
...Object.keys(dependency.dependencies ?? {}).map((dependencyName) => ({
|
||||
name: dependencyName,
|
||||
optional: false,
|
||||
})),
|
||||
...Object.keys(dependency.optionalDependencies ?? {}).map((dependencyName) => ({
|
||||
name: dependencyName,
|
||||
optional: true,
|
||||
})),
|
||||
...Object.keys(dependency.peerDependencies ?? {})
|
||||
.filter((peer) => !devDependencies.has(peer))
|
||||
.map((peer) => ({ name: peer, optional: true })),
|
||||
)
|
||||
}
|
||||
return roots.sort()
|
||||
}
|
||||
|
||||
async function readPackageNotice(path: string): Promise<PackageNotice> {
|
||||
const manifest = JSON.parse(await readFile(join(path, "package.json"), "utf8")) as PackageManifest
|
||||
const entries = await readdir(path, { withFileTypes: true })
|
||||
const licenseNames = entries
|
||||
.filter((entry) => entry.isFile() && /^(licen[cs]e|copying|notice)([._-].*)?$/i.test(entry.name))
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
if (!licenseNames.length) throw new Error(`${manifest.name ?? basename(path)} has no license file`)
|
||||
return {
|
||||
name: String(manifest.name ?? basename(path)),
|
||||
version: String(manifest.version ?? "unknown"),
|
||||
license: String(manifest.license ?? "see included license"),
|
||||
files: await Promise.all(
|
||||
licenseNames.map(async (name) => ({ name, content: await readFile(join(path, name), "utf8") })),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const notices = await Promise.all(
|
||||
(await runtimePackageRoots(root)).map(readPackageNotice),
|
||||
)
|
||||
const packagedNames = new Set(notices.map((notice) => notice.name))
|
||||
for (const name of nativePackagesByTarget[target] ?? []) {
|
||||
if (!packagedNames.has(name)) throw new Error(`target runtime package is missing: ${name}`)
|
||||
}
|
||||
const sections = [
|
||||
"nanobot native TUI third-party notices",
|
||||
"",
|
||||
`Target: ${target}`,
|
||||
"Runtime: Bun 1.3.13",
|
||||
"The release archive also contains SOURCE_OFFER.md, RELINKING.md, and the complete TUI application source.",
|
||||
"",
|
||||
"===== nanobot project license =====",
|
||||
"",
|
||||
await readFile(join(projectRoot, "LICENSE"), "utf8"),
|
||||
"",
|
||||
"===== Bun 1.3.13 runtime license and linked-library notice =====",
|
||||
"",
|
||||
await readFile(join(root, "licenses", "BUN-1.3.13-LICENSE.md"), "utf8"),
|
||||
"",
|
||||
"===== GNU Lesser General Public License 2.0 =====",
|
||||
"",
|
||||
await readFile(join(root, "licenses", "LGPL-2.0.txt"), "utf8"),
|
||||
"",
|
||||
"===== GNU Lesser General Public License 2.1 =====",
|
||||
"",
|
||||
await readFile(join(root, "licenses", "LGPL-2.1.txt"), "utf8"),
|
||||
]
|
||||
|
||||
for (const notice of notices.sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
sections.push("", `===== ${notice.name} ${notice.version} (${notice.license}) =====`)
|
||||
for (const file of notice.files) sections.push("", `--- ${file.name} ---`, "", file.content)
|
||||
}
|
||||
|
||||
await writeFile(output, `${sections.join("\n").trimEnd()}\n`, "utf8")
|
||||
console.log(output)
|
||||
Reference in New Issue
Block a user