| @@ -0,0 +1,137 @@ |
| | 1 | import { createHash } from "node:crypto"; |
| | 2 | import { spawn } from "node:child_process"; |
| | 3 | import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; |
| | 4 | import { join, relative, resolve } from "node:path"; |
| | 5 | |
| | 6 | import { buildDistributionPackage } from "./artifact"; |
| | 7 | import { repoRoot } from "./metadata"; |
| | 8 | import { buildPortableDistribution, portableTargetIds } from "./portable"; |
| | 9 | |
| | 10 | const defaultReleaseDir = resolve(repoRoot, "apps/executor/dist/release"); |
| | 11 | |
| | 12 | type CommandInput = { |
| | 13 | command: string; |
| | 14 | args: ReadonlyArray<string>; |
| | 15 | cwd: string; |
| | 16 | }; |
| | 17 | |
| | 18 | type CommandOutput = { |
| | 19 | stdout: string; |
| | 20 | stderr: string; |
| | 21 | }; |
| | 22 | |
| | 23 | type PackResult = { |
| | 24 | filename?: string; |
| | 25 | }; |
| | 26 | |
| | 27 | const runCommand = async (input: CommandInput): Promise<CommandOutput> => { |
| | 28 | const child = spawn(input.command, [...input.args], { |
| | 29 | cwd: input.cwd, |
| | 30 | env: process.env, |
| | 31 | stdio: ["ignore", "pipe", "pipe"], |
| | 32 | }); |
| | 33 | |
| | 34 | let stdout = ""; |
| | 35 | let stderr = ""; |
| | 36 | |
| | 37 | child.stdout?.setEncoding("utf8"); |
| | 38 | child.stdout?.on("data", (chunk) => { |
| | 39 | stdout += chunk; |
| | 40 | }); |
| | 41 | |
| | 42 | child.stderr?.setEncoding("utf8"); |
| | 43 | child.stderr?.on("data", (chunk) => { |
| | 44 | stderr += chunk; |
| | 45 | }); |
| | 46 | |
| | 47 | const exitCode = await new Promise<number>((resolveExitCode, reject) => { |
| | 48 | child.once("error", reject); |
| | 49 | child.once("close", (code) => { |
| | 50 | resolveExitCode(code ?? -1); |
| | 51 | }); |
| | 52 | }); |
| | 53 | |
| | 54 | if (exitCode !== 0) { |
| | 55 | throw new Error( |
| | 56 | [ |
| | 57 | `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, |
| | 58 | stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null, |
| | 59 | stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null, |
| | 60 | ] |
| | 61 | .filter((part) => part !== null) |
| | 62 | .join("\n\n"), |
| | 63 | ); |
| | 64 | } |
| | 65 | |
| | 66 | return { |
| | 67 | stdout: stdout.trim(), |
| | 68 | stderr: stderr.trim(), |
| | 69 | }; |
| | 70 | }; |
| | 71 | |
| | 72 | const computeSha256 = (contents: Uint8Array): string => |
| | 73 | createHash("sha256").update(contents).digest("hex"); |
| | 74 | |
| | 75 | const createChecksumsFile = async ( |
| | 76 | releaseDir: string, |
| | 77 | assetPaths: ReadonlyArray<string>, |
| | 78 | ): Promise<string> => { |
| | 79 | const entries = await Promise.all( |
| | 80 | [...assetPaths] |
| | 81 | .sort((left, right) => left.localeCompare(right)) |
| | 82 | .map(async (assetPath) => { |
| | 83 | const contents = await readFile(assetPath); |
| | 84 | const relativePath = relative(releaseDir, assetPath).replaceAll("\\", "/"); |
| | 85 | return `${computeSha256(contents)} ${relativePath}`; |
| | 86 | }), |
| | 87 | ); |
| | 88 | |
| | 89 | const checksumsPath = join(releaseDir, "checksums.txt"); |
| | 90 | await writeFile(checksumsPath, `${entries.join("\n")}\n`); |
| | 91 | return checksumsPath; |
| | 92 | }; |
| | 93 | |
| | 94 | const packDistributionPackage = async (releaseDir: string): Promise<string> => { |
| | 95 | const output = await runCommand({ |
| | 96 | command: "npm", |
| | 97 | args: ["pack", "./apps/executor/dist/npm", "--pack-destination", releaseDir, "--json"], |
| | 98 | cwd: repoRoot, |
| | 99 | }); |
| | 100 | const [result] = JSON.parse(output.stdout) as ReadonlyArray<PackResult>; |
| | 101 | const filename = result?.filename; |
| | 102 | |
| | 103 | if (!filename) { |
| | 104 | throw new Error(`npm pack did not report an output filename. stdout:\n${output.stdout}`); |
| | 105 | } |
| | 106 | |
| | 107 | return join(releaseDir, filename); |
| | 108 | }; |
| | 109 | |
| | 110 | const main = async () => { |
| | 111 | const releaseDir = defaultReleaseDir; |
| | 112 | const portableDir = join(releaseDir, "portable"); |
| | 113 | |
| | 114 | await rm(releaseDir, { recursive: true, force: true }); |
| | 115 | await mkdir(releaseDir, { recursive: true }); |
| | 116 | |
| | 117 | await buildDistributionPackage(); |
| | 118 | const packageArchivePath = await packDistributionPackage(releaseDir); |
| | 119 | const portableArtifact = await buildPortableDistribution({ |
| | 120 | outputDir: portableDir, |
| | 121 | buildWeb: false, |
| | 122 | targets: portableTargetIds, |
| | 123 | }); |
| | 124 | const portableArchivePaths = portableArtifact.artifacts |
| | 125 | .map((artifact) => artifact.archivePath) |
| | 126 | .filter((archivePath): archivePath is string => archivePath !== null); |
| | 127 | const checksumsPath = await createChecksumsFile(releaseDir, [ |
| | 128 | packageArchivePath, |
| | 129 | ...portableArchivePaths, |
| | 130 | ]); |
| | 131 | |
| | 132 | for (const assetPath of [packageArchivePath, ...portableArchivePaths, checksumsPath]) { |
| | 133 | process.stdout.write(`${assetPath}\n`); |
| | 134 | } |
| | 135 | }; |
| | 136 | |
| | 137 | await main(); |