mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-03 01:13:12 +08:00
226 lines
9.7 KiB
TypeScript
226 lines
9.7 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Shared helpers for the per-platform dictation-runtime build pipeline. Called
|
|
* from `package.ts`, `upload.ts`, and `produce.ts`, plus the gulpfiles'
|
|
* `packageTask` (via `readDictationRuntimeResults`) so each VS Code build can
|
|
* stamp its own `product.dictationRuntime` into the per-platform `product.json`
|
|
* at packaging time.
|
|
*
|
|
* This mirrors `build/agent-sdk/` but for the Foundry Local native runtime used
|
|
* by on-device dictation: a prebuilt N-API addon (`foundry_local_napi.node`)
|
|
* plus the Foundry Local Core / onnxruntime / onnxruntime-genai shared
|
|
* libraries. Rather than downloading those from npm + NuGet at runtime, each
|
|
* platform build job produces its own tarball and uploads it to
|
|
* `main.vscode-cdn.net`; the runtime downloads the single content-addressed
|
|
* tarball for its target (see `foundryLocalRuntime.ts`).
|
|
*
|
|
* There is exactly ONE runtime (unlike the multi-SDK agent-sdk pipeline), so
|
|
* `product.dictationRuntime` is a single `{version, urlTemplate}` object rather
|
|
* than a map keyed by id. The version is the pinned `foundry-local-sdk`
|
|
* dependency in the repo-root `package.json` — the same package the runtime
|
|
* loads — so the shipped types and the CDN payload stay in lockstep.
|
|
*/
|
|
|
|
import * as crypto from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const THIS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
/** Repo-root `package.json`, the source of truth for the pinned SDK version. */
|
|
const ROOT_PACKAGE_JSON = path.join(THIS_DIR, '..', '..', 'package.json');
|
|
|
|
/** The npm package whose native payload we republish to the CDN. */
|
|
export const SDK_PACKAGE_NAME = 'foundry-local-sdk';
|
|
|
|
/**
|
|
* Path segment under the CDN URL and the conceptual id of the runtime. Kept as a
|
|
* named constant (rather than inlined) so the blob layout, the url builders, and
|
|
* `upload.ts`'s filename parsing agree on one string.
|
|
*/
|
|
export const RUNTIME_ID = 'foundry-local';
|
|
|
|
interface IRootPackageJson {
|
|
readonly dependencies?: Readonly<Record<string, string>>;
|
|
}
|
|
|
|
let _versionCache: string | undefined;
|
|
|
|
/**
|
|
* The exact pinned `foundry-local-sdk` version from the repo-root
|
|
* `package.json`. Rejects `^`/`~` ranges — a range would let the build resolve
|
|
* a different version than the runtime types were compiled against, and the
|
|
* content-addressed CDN upload would then diverge across runs.
|
|
*/
|
|
export function getRuntimeVersion(): string {
|
|
if (_versionCache) {
|
|
return _versionCache;
|
|
}
|
|
const json = JSON.parse(fs.readFileSync(ROOT_PACKAGE_JSON, 'utf8')) as IRootPackageJson;
|
|
const version = json.dependencies?.[SDK_PACKAGE_NAME];
|
|
if (!version) {
|
|
throw new Error(`Expected a '${SDK_PACKAGE_NAME}' entry in ${ROOT_PACKAGE_JSON} dependencies.`);
|
|
}
|
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
throw new Error(`Refusing to use ${SDK_PACKAGE_NAME}@${version} from ${ROOT_PACKAGE_JSON}: must be an exact version (no ^ or ~ ranges).`);
|
|
}
|
|
_versionCache = version;
|
|
return version;
|
|
}
|
|
|
|
/** Strict subset of VS Code build platforms the dictation runtime can target. */
|
|
export type VscodeBuildPlatform = 'darwin' | 'linux' | 'win32';
|
|
|
|
/** Runtime whitelist mirroring `VscodeBuildPlatform`, for CLI guards. */
|
|
export const KNOWN_VSCODE_PLATFORMS: ReadonlySet<string> = new Set<VscodeBuildPlatform>([
|
|
'darwin', 'linux', 'win32',
|
|
]);
|
|
|
|
/**
|
|
* The set of `<platform>-<arch>` targets Foundry Local ships a native runtime
|
|
* for. Mirrors `FOUNDRY_LOCAL_SUPPORTED_PLATFORMS` in
|
|
* `src/vs/platform/localTranscription/node/foundryLocalRuntime.ts` and the
|
|
* `SUPPORTED_TARGETS` runtime gate — keep the three in sync.
|
|
*/
|
|
export const SUPPORTED_TARGETS: ReadonlySet<string> = new Set([
|
|
'darwin-arm64',
|
|
'linux-x64',
|
|
'linux-arm64',
|
|
'win32-x64',
|
|
'win32-arm64',
|
|
]);
|
|
|
|
/**
|
|
* Resolves the runtime target for a particular VS Code build, or `undefined`
|
|
* when that `(platform, arch)` has no Foundry Local runtime (e.g. `darwin-x64`,
|
|
* Alpine/musl, armhf, web). The macOS build is Universal but Foundry Local only
|
|
* ships `darwin-arm64`, so `darwin-x64` returns `undefined` — the runtime gate
|
|
* reports on-device dictation unsupported there and never downloads.
|
|
*
|
|
* The legacy Alpine x64 encoding (`{platform: 'linux', arch: 'alpine'}`) and any
|
|
* real `alpine` platform return `undefined`: the core libraries are glibc-linked.
|
|
*/
|
|
export function getRuntimeTargetForBuild(vscodePlatform: string, arch: string): string | undefined {
|
|
if (vscodePlatform === 'alpine' || arch === 'alpine' || arch === 'musl') {
|
|
return undefined;
|
|
}
|
|
const target = `${vscodePlatform}-${arch}`;
|
|
return SUPPORTED_TARGETS.has(target) ? target : undefined;
|
|
}
|
|
|
|
/**
|
|
* Whether a given VS Code build should stamp `product.dictationRuntime`, EVEN IF
|
|
* this build's own `(platform, arch)` has no CDN payload of its own.
|
|
*
|
|
* The stamp — `{version, urlTemplate}` — is target-agnostic (the `{target}`
|
|
* placeholder is resolved by the runtime per launch), so it must be present on
|
|
* every product whose app can host on-device dictation, whether or not this
|
|
* particular job produced/uploaded the payload:
|
|
*
|
|
* - `darwin-x64`: the macOS app is Universal. Its x64 and arm64 slices are
|
|
* merged into one bundle whose single `product.json` must carry the runtime
|
|
* config so dictation works when the Universal app runs natively on Apple
|
|
* Silicon — even though only the `darwin-arm64` job builds/uploads the
|
|
* payload. So `darwin-x64` stamps but does not produce.
|
|
* - non-publish product builds: packaging always strips the SDK's native
|
|
* payload (`getFoundryLocalExcludeFilter` in `gulpfile.vscode.ts`), so a
|
|
* packaged build with no stamp would have NEITHER a CDN location NOR a
|
|
* `node_modules` fallback. Stamping regardless of `VSCODE_PUBLISH` gives the
|
|
* packaged app a usable CDN source; the payload for that version is uploaded
|
|
* (idempotently) by publish runs. Only local dev-from-source (which never
|
|
* runs `produce.ts`) keeps the `node_modules` payload.
|
|
*
|
|
* Returns `false` for platforms/arches that can never host dictation (armhf,
|
|
* Alpine/musl, web) so their `product.json` stays clean.
|
|
*/
|
|
export function shouldStampRuntime(vscodePlatform: string, arch: string): boolean {
|
|
if (getRuntimeTargetForBuild(vscodePlatform, arch)) {
|
|
return true;
|
|
}
|
|
// The Universal macOS x64 slice must match its arm64 sibling's stamp.
|
|
return vscodePlatform === 'darwin' && arch === 'x64';
|
|
}
|
|
|
|
/**
|
|
* The CDN URL `product.dictationRuntime.urlTemplate` resolves to for a concrete
|
|
* target. Content-addressed under
|
|
* `dictation-runtime/<RUNTIME_ID>/<version>/<target>.tgz`. Matches the upload
|
|
* path written by `upload.ts`.
|
|
*/
|
|
export function buildCdnUrl(version: string, target: string): string {
|
|
return `https://main.vscode-cdn.net/dictation-runtime/${RUNTIME_ID}/${version}/${target}.tgz`;
|
|
}
|
|
|
|
/**
|
|
* The `format2`-style URL template stamped into
|
|
* `product.dictationRuntime.urlTemplate`. The runtime substitutes `{target}`
|
|
* per launch via `foundryLocalPlatformKey()`; every platform job emits the same
|
|
* template so a macOS Universal bundle can share one `product.json`.
|
|
*/
|
|
export function buildCdnUrlTemplate(version: string): string {
|
|
return `https://main.vscode-cdn.net/dictation-runtime/${RUNTIME_ID}/${version}/{target}.tgz`;
|
|
}
|
|
|
|
/** Streams `filePath` into a sha256 hasher without buffering the whole file. */
|
|
export function sha256OfFile(filePath: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const hash = crypto.createHash('sha256');
|
|
const stream = fs.createReadStream(filePath);
|
|
stream.on('error', reject);
|
|
stream.on('data', chunk => hash.update(chunk));
|
|
stream.on('end', () => resolve(hash.digest('hex')));
|
|
});
|
|
}
|
|
|
|
/** Parses `--key=value` CLI flags into a Map. */
|
|
export function parseFlags(argv: readonly string[]): Map<string, string> {
|
|
const flags = new Map<string, string>();
|
|
for (const arg of argv) {
|
|
const m = /^--([a-zA-Z-]+)=(.+)$/.exec(arg);
|
|
if (m) {
|
|
flags.set(m[1], m[2]);
|
|
}
|
|
}
|
|
return flags;
|
|
}
|
|
|
|
/**
|
|
* The `product.dictationRuntime` entry, written by `produce.ts` and read by the
|
|
* gulpfiles' `packageTask`. Shape matches `IDictationRuntimeProductConfig` in
|
|
* `src/vs/base/common/product.ts`.
|
|
*/
|
|
export interface IDictationRuntimeResult {
|
|
readonly version: string;
|
|
readonly urlTemplate: string;
|
|
}
|
|
|
|
/**
|
|
* Reads the per-platform dictation-runtime results file written by `produce.ts`.
|
|
* Returns `undefined` when `DICTATION_RUNTIME_RESULTS_FILE` is unset, the file
|
|
* doesn't exist, or the build can't host dictation. In a pipeline build that CAN
|
|
* host dictation this is always present (publish or not); it is absent for local
|
|
* dev-from-source (which never runs `produce.ts`) — that path ships product.json
|
|
* without `dictationRuntime` and the runtime falls back to the SDK's own
|
|
* `node_modules` payload.
|
|
*/
|
|
export function readDictationRuntimeResults(): IDictationRuntimeResult | undefined {
|
|
const filePath = process.env.DICTATION_RUNTIME_RESULTS_FILE;
|
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
return undefined;
|
|
}
|
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
|
|
if (typeof parsed !== 'object' || parsed === null) {
|
|
throw new Error(`DICTATION_RUNTIME_RESULTS_FILE at ${filePath} is not a JSON object`);
|
|
}
|
|
const result = parsed as Partial<IDictationRuntimeResult>;
|
|
if (typeof result.version !== 'string' || typeof result.urlTemplate !== 'string') {
|
|
return undefined;
|
|
}
|
|
return { version: result.version, urlTemplate: result.urlTemplate };
|
|
}
|