|
1 | | -import { createHash } from "node:crypto"; |
2 | | -import { readdir, readFile, mkdir, rm, writeFile, chmod, stat } from "node:fs/promises"; |
3 | | -import { execFile } from "node:child_process"; |
4 | | -import { promisify } from "node:util"; |
| 1 | +import { cp, mkdir, readFile, rm } from "node:fs/promises"; |
5 | 2 | import path from "node:path"; |
6 | 3 |
|
7 | | -const exec = promisify(execFile); |
8 | 4 | const appRoot = path.resolve(new URL("..", import.meta.url).pathname); |
9 | 5 | const root = path.resolve(appRoot, "../.."); |
10 | | -if (process.platform !== "linux") throw new Error("VS Code App packaging currently supports Linux build runners only"); |
11 | | - |
12 | | -/** |
13 | | - * Force a umask-independent mode set on the staged tree. |
14 | | - * |
15 | | - * `cp -a` preserves whatever modes tar materialized, and the build host's umask |
16 | | - * narrows them (0664/0775 under umask 0002, 0644/0755 under umask 022). Those |
17 | | - * bits end up in the final archive's tar headers, so the digest — and with it |
18 | | - * the `sha256` declared in canvas-extension.json — used to vary per build host. |
19 | | - */ |
20 | | -async function normalizeModes(dir) { |
21 | | - for (const entry of await readdir(dir, { withFileTypes: true })) { |
22 | | - const target = path.join(dir, entry.name); |
23 | | - if (entry.isSymbolicLink()) continue; |
24 | | - if (entry.isDirectory()) { |
25 | | - await chmod(target, 0o755); |
26 | | - await normalizeModes(target); |
27 | | - } else if (entry.isFile()) { |
28 | | - const { mode } = await stat(target); |
29 | | - await chmod(target, mode & 0o111 ? 0o755 : 0o644); |
30 | | - } |
31 | | - } |
32 | | -} |
33 | 6 | const output = path.resolve(process.env.OUTPUT_DIR ?? path.join(root, "dist")); |
34 | | -const version = "1.109.5"; |
35 | | -const releases = { |
36 | | - "linux-amd64": ["x64", "b433bf4f0227321a7014d8460d10a8f958adc0f45aa79bd889e84e65e8f88363"], |
37 | | - "linux-arm64": ["arm64", "36d9c14036489b63de84ebace837fcacf7e60e669a0dc715802c5443684ea4dc"], |
38 | | -}; |
39 | | -const work = path.join(output, ".download"); |
40 | 7 | const packageRoot = path.join(output, "apps", "vscode"); |
41 | | -const manifest = JSON.parse(await readFile(path.join(appRoot, "canvas-extension.json"), "utf8")); |
42 | | -await rm(work, { recursive: true, force: true }); |
43 | | -await rm(packageRoot, { recursive: true, force: true }); |
44 | | -await mkdir(work, { recursive: true }); |
45 | | -await mkdir(path.join(packageRoot, "backend"), { recursive: true }); |
46 | | -for (const [platform, [upstreamArch, expected]] of Object.entries(releases)) { |
47 | | - const source = path.join(work, `${platform}.tar.gz`); |
48 | | - const archive = `openvscode-server-v${version}-linux-${upstreamArch}.tar.gz`; |
49 | | - const url = `https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v${version}/${archive}`; |
50 | | - await exec("curl", ["--fail", "--location", "--retry", "3", "--output", source, url]); |
51 | | - const actual = createHash("sha256").update(await readFile(source)).digest("hex"); |
52 | | - if (actual !== expected) throw new Error(`${platform}: upstream checksum mismatch (${actual})`); |
53 | | - const unpacked = path.join(work, `${platform}-unpacked`); |
54 | | - const normalized = path.join(work, `${platform}-normalized`); |
55 | | - await mkdir(unpacked, { recursive: true }); |
56 | | - await exec("tar", ["-xzf", source, "-C", unpacked]); |
57 | | - await mkdir(normalized, { recursive: true }); |
58 | | - const [top] = await readdir(unpacked); |
59 | | - await exec("cp", ["-a", `${path.join(unpacked, top)}/.`, normalized]); |
60 | | - await writeFile(path.join(normalized, "bin", "openvscode-bootstrap"), "#!/bin/sh\nset -eu\ndata_dir=\"\"\nfor candidate do data_dir=\"$candidate\"; done\nmkdir -p \"$data_dir\" \"$data_dir/home\" \"$data_dir/workspace\"\nexport HOME=\"$data_dir/home\"\nexec \"$(dirname \"$0\")/openvscode-server\" \"$@\"\n"); |
61 | | - await exec("chmod", ["0755", path.join(normalized, "bin", "openvscode-bootstrap")]); |
62 | | - await normalizeModes(normalized); |
63 | | - const destination = path.join(packageRoot, "backend", `openvscode-${platform}.tar.gz`); |
64 | | - await exec("tar", ["--sort=name", "--mtime=UTC 1970-01-01", "--owner=0", "--group=0", "--numeric-owner", "-czf", destination, "-C", normalized, "."]); |
65 | | - const produced = createHash("sha256").update(await readFile(destination)).digest("hex"); |
66 | | - const artifact = manifest?.backend?.artifacts?.[platform]; |
67 | | - if (!artifact?.sha256) { |
68 | | - throw new Error(`canvas-extension.json declares no backend.artifacts.${platform}.sha256`); |
| 8 | +const manifest = JSON.parse( |
| 9 | + await readFile(path.join(appRoot, "canvas-extension.json"), "utf8"), |
| 10 | +); |
| 11 | + |
| 12 | +for (const [platform, artifact] of Object.entries(manifest.backend.artifacts)) { |
| 13 | + const url = new URL(artifact.url); |
| 14 | + if (url.protocol !== "https:" || !url.pathname.endsWith(".tar.gz")) { |
| 15 | + throw new Error(`${platform}: invalid HTTPS artifact URL`); |
69 | 16 | } |
70 | | - if (path.basename(destination) !== path.basename(artifact.path)) { |
71 | | - throw new Error(`${platform}: built ${path.basename(destination)} but canvas-extension.json declares ${artifact.path}`); |
| 17 | + if (!/^[0-9a-f]{64}$/.test(artifact.sha256)) { |
| 18 | + throw new Error(`${platform}: invalid SHA-256`); |
72 | 19 | } |
73 | | - if (produced !== artifact.sha256) { |
74 | | - throw new Error( |
75 | | - `${platform}: built archive digest ${produced} does not match the sha256 declared for backend.artifacts.${platform} in canvas-extension.json (${artifact.sha256}); update the manifest with the reproducible digest` |
76 | | - ); |
| 20 | + if (!Number.isInteger(artifact.strip_components) || artifact.strip_components < 0) { |
| 21 | + throw new Error(`${platform}: invalid strip_components`); |
77 | 22 | } |
78 | | - console.log(`${platform}: ${produced}`); |
79 | 23 | } |
80 | | -await exec("cp", [path.join(appRoot, "canvas-extension.json"), path.join(packageRoot, "canvas-extension.json")]); |
81 | | -await exec("cp", [path.join(appRoot, "extension.js"), path.join(packageRoot, "extension.js")]); |
82 | | -await exec("cp", [path.join(appRoot, "README.md"), path.join(packageRoot, "README.md")]); |
83 | | -await rm(work, { recursive: true, force: true }); |
84 | | -console.log(`Wrote ${packageRoot}`); |
| 24 | + |
| 25 | +await rm(packageRoot, { recursive: true, force: true }); |
| 26 | +await mkdir(packageRoot, { recursive: true }); |
| 27 | +for (const filename of ["canvas-extension.json", "extension.js", "README.md"]) { |
| 28 | + await cp(path.join(appRoot, filename), path.join(packageRoot, filename)); |
| 29 | +} |
| 30 | +console.log(`Wrote source-installable App package to ${packageRoot}`); |
0 commit comments