From fec92080bd8a2b5f67898644e898d7f4d6fa1862 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:45:42 -0700 Subject: [PATCH 01/17] chore: bump foundry-local-sdk to 2.x --- .../product-build-alpine-node-modules.yml | 14 +- .../alpine/product-build-alpine.yml | 11 +- .../common/disableFoundryLocalInstall.ts | 8 -- .../azure-pipelines/common/foundry-local.yml | 27 ---- .../common/foundryLocalInstall.ts | 121 ------------------ .../product-build-darwin-node-modules.yml | 14 +- .../steps/product-build-darwin-compile.yml | 19 +-- .../product-build-linux-node-modules.yml | 14 +- .../steps/product-build-linux-compile.yml | 19 +-- .../product-quality-checks.yml | 11 +- .../web/product-build-web-node-modules.yml | 14 +- .../azure-pipelines/web/product-build-web.yml | 11 +- .../product-build-win32-node-modules.yml | 14 +- .../azure-pipelines/win32/sdl-scan-win32.yml | 31 ++++- .../steps/product-build-win32-compile.yml | 19 +-- build/npm/postinstall.ts | 52 ++------ package-lock.json | 31 +++-- package.json | 4 +- 18 files changed, 119 insertions(+), 315 deletions(-) delete mode 100644 build/azure-pipelines/common/disableFoundryLocalInstall.ts delete mode 100644 build/azure-pipelines/common/foundry-local.yml delete mode 100644 build/azure-pipelines/common/foundryLocalInstall.ts diff --git a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml index 9e92897ba3e8dd..cf0d66a10c81cc 100644 --- a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml +++ b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml @@ -61,10 +61,9 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare - onlyOnNodeModulesCacheMiss: true + - task: NuGetAuthenticate@1 + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Setup NuGet Authentication - task: Docker@1 inputs: @@ -131,17 +130,14 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" VSCODE_NPMRC_PATH: $(NPMRC_PATH) displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - onlyOnNodeModulesCacheMiss: true - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 23fc9a1ad30d01..b6fb412e56e5d5 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -106,9 +106,8 @@ jobs: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication - task: Docker@1 inputs: @@ -175,16 +174,14 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" VSCODE_NPMRC_PATH: $(NPMRC_PATH) displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/common/disableFoundryLocalInstall.ts b/build/azure-pipelines/common/disableFoundryLocalInstall.ts deleted file mode 100644 index f63cdaeea0aaad..00000000000000 --- a/build/azure-pipelines/common/disableFoundryLocalInstall.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { disableFoundryLocalInstall } from './foundryLocalInstall.ts'; - -disableFoundryLocalInstall(); diff --git a/build/azure-pipelines/common/foundry-local.yml b/build/azure-pipelines/common/foundry-local.yml deleted file mode 100644 index fd9d88e8907546..00000000000000 --- a/build/azure-pipelines/common/foundry-local.yml +++ /dev/null @@ -1,27 +0,0 @@ -parameters: - - name: phase - type: string - values: - - prepare - - install - - name: onlyOnNodeModulesCacheMiss - type: boolean - default: false - -steps: - - ${{ if eq(parameters.phase, 'prepare') }}: - - task: NuGetAuthenticate@1 - ${{ if eq(parameters.onlyOnNodeModulesCacheMiss, true) }}: - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NuGet Authentication - - - script: node build/azure-pipelines/common/disableFoundryLocalInstall.ts - ${{ if eq(parameters.onlyOnNodeModulesCacheMiss, true) }}: - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Disable Foundry Local Native Install - - - ${{ if eq(parameters.phase, 'install') }}: - - script: node build/azure-pipelines/common/foundryLocalInstall.ts - ${{ if eq(parameters.onlyOnNodeModulesCacheMiss, true) }}: - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Install Foundry Local Native Dependencies diff --git a/build/azure-pipelines/common/foundryLocalInstall.ts b/build/azure-pipelines/common/foundryLocalInstall.ts deleted file mode 100644 index ff80bc58bf5020..00000000000000 --- a/build/azure-pipelines/common/foundryLocalInstall.ts +++ /dev/null @@ -1,121 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { execFileSync } from 'child_process'; -import { createHash } from 'crypto'; -import * as fs from 'fs'; -import * as path from 'path'; -import { fetchCoreLibraries, getStandardArtifacts, type IFoundryDependencyVersions, requiredCoreLibraryNames, supportsCoreLibraryTarget, VSCODE_NUGET_FEED } from '../../dictation-runtime/nuget.ts'; - -const repositoryRoot = path.resolve(import.meta.dirname, '../../..'); -const packageName = 'foundry-local-sdk'; -const credentialTokenEnvironmentVariable = 'VSS_NUGET_ACCESSTOKEN'; -const expectedInstallerUtilsHash = '0831c932b10389283e805f88a204b0f6a5a8053f2ee520e56a0f0adf1352aa8b'; - -type RootPackageJson = { - dependencies?: Record; - allowScripts?: Record; -}; - -type FoundryPackageJson = { - version?: string; - scripts?: Record; -}; - -function readJson(filePath: string): T { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; -} - -function getPinnedPackage(root: string): { packageJsonPath: string; packageJson: RootPackageJson; allowScripts: Record; allowScriptsKey: string } { - const packageJsonPath = path.join(root, 'package.json'); - const packageJson = readJson(packageJsonPath); - const allowScripts = packageJson.allowScripts; - const version = packageJson.dependencies?.[packageName]; - const allowScriptsKey = version ? `${packageName}@${version}` : undefined; - - if (!allowScripts || !version || !allowScriptsKey || allowScripts[allowScriptsKey] !== true) { - throw new Error(`Expected an approved, pinned ${packageName} install script in package.json`); - } - - return { packageJsonPath, packageJson, allowScripts, allowScriptsKey }; -} - -export function disableFoundryLocalInstall(root = repositoryRoot): void { - const { packageJsonPath, packageJson, allowScripts, allowScriptsKey } = getPinnedPackage(root); - allowScripts[allowScriptsKey] = false; - fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, undefined, 2)}\n`); - console.log(`Disabled ${allowScriptsKey} install script for this CI job`); -} - -function validateInstallerUtils(installerUtilsPath: string): void { - const contents = fs.readFileSync(installerUtilsPath); - const actualHash = createHash('sha256').update(contents).digest('hex'); - - if (actualHash !== expectedInstallerUtilsHash) { - throw new Error(`Unexpected ${packageName} installer utility hash ${actualHash}`); - } -} - -function runLifecycleScript(packageRoot: string, relativeScriptPath: string): void { - execFileSync(process.execPath, [path.join(packageRoot, relativeScriptPath)], { - cwd: packageRoot, - stdio: 'inherit' - }); -} - -export async function installFoundryLocal(root = repositoryRoot): Promise { - if (!process.env[credentialTokenEnvironmentVariable]) { - throw new Error(`${credentialTokenEnvironmentVariable} was not set by NuGetAuthenticate`); - } - - const rootPackageJson = readJson(path.join(root, 'package.json')); - const version = rootPackageJson.dependencies?.[packageName]; - const allowScriptsKey = version ? `${packageName}@${version}` : undefined; - if (!version || !allowScriptsKey || rootPackageJson.allowScripts?.[allowScriptsKey] !== false) { - throw new Error(`Expected the pinned ${packageName} install script to be disabled before installation`); - } - - const packageRoot = path.join(root, 'node_modules', packageName); - const packageJson = readJson(path.join(packageRoot, 'package.json')); - if (packageJson.version !== version) { - throw new Error(`Expected ${packageName}@${version}, found ${packageJson.version ?? 'an unknown version'}`); - } - if (packageJson.scripts?.preinstall !== 'node script/preinstall.cjs' || packageJson.scripts.install !== 'node script/install-standard.cjs') { - throw new Error(`Unexpected ${packageName}@${version} lifecycle scripts`); - } - - validateInstallerUtils(path.join(packageRoot, 'script', 'install-utils.cjs')); - runLifecycleScript(packageRoot, 'script/preinstall.cjs'); - - const target = `${process.platform}-${process.arch}`; - if (!supportsCoreLibraryTarget(target)) { - console.warn(`[foundry-local] Unsupported platform: ${target}. Skipping.`); - return; - } - - const dependencies = readJson(path.join(packageRoot, 'deps_versions.json')); - const artifacts = getStandardArtifacts(target, dependencies); - const binDir = path.join(packageRoot, 'foundry-local-core', target); - await fetchCoreLibraries(target, artifacts, binDir, { feeds: [VSCODE_NUGET_FEED], skipIfPresent: true }); - - const missingFiles = requiredCoreLibraryNames(target).filter(file => !fs.existsSync(path.join(binDir, file))); - if (missingFiles.length > 0) { - throw new Error(`[foundry-local] Missing required native libraries for ${target}: ${missingFiles.join(', ')}`); - } - - const coreVersion = dependencies['foundry-local-core'].nuget; - const platformPackageJson = { - name: `@foundry-local-core/${target}`, - version: coreVersion, - description: `Native binaries for Foundry Local SDK (${target})`, - private: true, - }; - fs.writeFileSync(path.join(binDir, 'package.json'), JSON.stringify(platformPackageJson, undefined, 2)); - console.log('[foundry-local] Installation complete.'); -} - -if (import.meta.filename === process.argv[1]) { - await installFoundryLocal(); -} diff --git a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml index 3317e43816e990..eaeb4a83fae3c6 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml @@ -75,10 +75,9 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Setup PyPI Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare - onlyOnNodeModulesCacheMiss: true + - task: NuGetAuthenticate@1 + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Setup NuGet Authentication - script: | set -e @@ -99,6 +98,8 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen @@ -107,11 +108,6 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - onlyOnNodeModulesCacheMiss: true - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index 4293575b5c4e5b..313985ef46e6c6 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -30,10 +30,6 @@ steps: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if or(eq(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, true)) }}: - - script: node build/azure-pipelines/common/disableFoundryLocalInstall.ts - displayName: Disable Foundry Local Native Install - - template: ../../distro/download-distro.yml@self - task: AzureKeyVault@2 @@ -87,9 +83,8 @@ steps: displayName: Setup NPM Authentication - ${{ if and(eq(parameters.VSCODE_CIBUILD, false), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, false)) }}: - - template: ../../common/foundry-local.yml@self - parameters: - phase: prepare + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication - task: PipAuthenticate@1 inputs: @@ -115,6 +110,11 @@ steps: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + ${{ if or(eq(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, true)) }}: + FOUNDRY_LOCAL_SKIP_INSTALL: "1" + ${{ else }}: + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen @@ -123,11 +123,6 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if and(eq(parameters.VSCODE_CIBUILD, false), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, false)) }}: - - template: ../../common/foundry-local.yml@self - parameters: - phase: install - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/linux/product-build-linux-node-modules.yml b/build/azure-pipelines/linux/product-build-linux-node-modules.yml index ea09134e1a863d..176562a639441c 100644 --- a/build/azure-pipelines/linux/product-build-linux-node-modules.yml +++ b/build/azure-pipelines/linux/product-build-linux-node-modules.yml @@ -82,10 +82,9 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare - onlyOnNodeModulesCacheMiss: true + - task: NuGetAuthenticate@1 + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Setup NuGet Authentication - script: | set -e @@ -144,14 +143,11 @@ jobs: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 ONNXRUNTIME_NODE_INSTALL: skip GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - onlyOnNodeModulesCacheMiss: true - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index 2dc72da7c766c0..272454913a3753 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -38,10 +38,6 @@ steps: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if or(eq(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, true)) }}: - - script: node build/azure-pipelines/common/disableFoundryLocalInstall.ts - displayName: Disable Foundry Local Native Install - - template: ../../distro/download-distro.yml@self - task: AzureKeyVault@2 @@ -107,9 +103,8 @@ steps: displayName: Setup NPM Authentication - ${{ if and(eq(parameters.VSCODE_CIBUILD, false), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, false)) }}: - - template: ../../common/foundry-local.yml@self - parameters: - phase: prepare + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication - script: | set -e @@ -167,14 +162,14 @@ steps: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 ONNXRUNTIME_NODE_INSTALL: skip GITHUB_TOKEN: "$(github-token-code-oss)" + ${{ if or(eq(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, true)) }}: + FOUNDRY_LOCAL_SKIP_INSTALL: "1" + ${{ else }}: + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if and(eq(parameters.VSCODE_CIBUILD, false), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, false)) }}: - - template: ../../common/foundry-local.yml@self - parameters: - phase: install - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/product-quality-checks.yml b/build/azure-pipelines/product-quality-checks.yml index 59df292c8fb031..ec11d009794db4 100644 --- a/build/azure-pipelines/product-quality-checks.yml +++ b/build/azure-pipelines/product-quality-checks.yml @@ -56,9 +56,8 @@ jobs: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ./common/foundry-local.yml@self - parameters: - phase: prepare + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication - script: | set -e @@ -106,13 +105,11 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ./common/foundry-local.yml@self - parameters: - phase: install - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/web/product-build-web-node-modules.yml b/build/azure-pipelines/web/product-build-web-node-modules.yml index 0a9e076eb35d67..6ad5e69eceff4c 100644 --- a/build/azure-pipelines/web/product-build-web-node-modules.yml +++ b/build/azure-pipelines/web/product-build-web-node-modules.yml @@ -54,10 +54,9 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare - onlyOnNodeModulesCacheMiss: true + - task: NuGetAuthenticate@1 + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Setup NuGet Authentication - script: | set -e @@ -85,14 +84,11 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - onlyOnNodeModulesCacheMiss: true - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 46b4afffc73f2d..24234d819d7107 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -68,9 +68,8 @@ jobs: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication - script: | set -e @@ -98,13 +97,11 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - - script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/win32/product-build-win32-node-modules.yml b/build/azure-pipelines/win32/product-build-win32-node-modules.yml index 528a580afe3e4b..2cc86618cee49b 100644 --- a/build/azure-pipelines/win32/product-build-win32-node-modules.yml +++ b/build/azure-pipelines/win32/product-build-win32-node-modules.yml @@ -71,10 +71,9 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare - onlyOnNodeModulesCacheMiss: true + - task: NuGetAuthenticate@1 + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Setup NuGet Authentication - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -86,15 +85,12 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" retryCountOnTaskFailure: 5 displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - onlyOnNodeModulesCacheMiss: true - - powershell: node build/azure-pipelines/common/checkNativeOptionalDeps.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index 4ecd6ae9c36c3e..84e49ebbc6b806 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -26,6 +26,31 @@ steps: KeyVaultName: vscode-oss-build-secrets SecretsFilter: "github-token-code-oss" + - powershell: node build/setup-npm-registry.ts $env:NPM_REGISTRY + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Registry + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + # Set the private NPM registry to the global npmrc file + # so that authentication works for subfolders like build/, remote/, extensions/ etc + # which does not have their own .npmrc file + exec { npm config set registry "$env:NPM_REGISTRY" } + $NpmrcPath = (npm config get userconfig) + echo "##vso[task.setvariable variable=NPMRC_PATH]$NpmrcPath" + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM + + - task: npmAuthenticate@0 + inputs: + workingFile: $(NPMRC_PATH) + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Authentication + + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication + - pwsh: | $includes = @' { @@ -108,14 +133,12 @@ steps: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" retryCountOnTaskFailure: 5 displayName: Install dependencies condition: and(succeeded(), ne(variables.APISCAN_NODE_MODULES_RESTORED, 'true')) - - template: ../common/foundry-local.yml@self - parameters: - phase: install - - powershell: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index da881043c5f1b2..42677197ffba1a 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -32,10 +32,6 @@ steps: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if or(eq(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, true)) }}: - - script: node build/azure-pipelines/common/disableFoundryLocalInstall.ts - displayName: Disable Foundry Local Native Install - - task: UsePythonVersion@0 inputs: versionSpec: "3.x" @@ -93,9 +89,8 @@ steps: displayName: Setup NPM Authentication - ${{ if and(eq(parameters.VSCODE_CIBUILD, false), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, false)) }}: - - template: ../../common/foundry-local.yml@self - parameters: - phase: prepare + - task: NuGetAuthenticate@1 + displayName: Setup NuGet Authentication - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -111,15 +106,15 @@ steps: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" + ${{ if or(eq(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, true)) }}: + FOUNDRY_LOCAL_SKIP_INSTALL: "1" + ${{ else }}: + FOUNDRY_LOCAL_NUGET_MODE: dotnet + FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" retryCountOnTaskFailure: 5 displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if and(eq(parameters.VSCODE_CIBUILD, false), eq(parameters.VSCODE_SKIP_FOUNDRY_LOCAL_INSTALL, false)) }}: - - template: ../../common/foundry-local.yml@self - parameters: - phase: install - - powershell: node build/azure-pipelines/common/checkNativeOptionalDeps.ts displayName: Verify native optional dependency binaries diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index a2f2adde45cc6b..bd22fc3faf9c28 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -338,51 +338,19 @@ async function main() { log('.', `Created ${claudeSkillsLinkType} .claude/skills -> .agents/skills`); } - // foundry-local-sdk (on-device chat dictation) resolves its prebuilt N-API - // addon and native core libraries from fixed, package-relative paths. We do - // not ship that native payload (its addon requires a newer glibc than our - // minimum supported Linux distros); it is downloaded on demand at runtime - // into a per-user cache. Patch the SDK loader so it honors the - // `VSCODE_FOUNDRY_LOCAL_NATIVE_DIR` env var (pointing at that cache) for both - // the addon and the core libraries, falling back to the original - // package-relative logic so dev-from-source still works. Idempotent. + // Temporary: patch @github/copilot-sdk session.js to fix ESM import + // (missing .js extension on vscode-jsonrpc/node). Fixed upstream in v0.1.32. + // TODO: Remove once @github/copilot-sdk is updated to >=0.1.32 for (const dir of ['', 'remote']) { - const coreInteropFile = path.join(root, dir, 'node_modules', 'foundry-local-sdk', 'dist', 'detail', 'coreInterop.js'); - if (!fs.existsSync(coreInteropFile)) { - continue; - } - const content = fs.readFileSync(coreInteropFile, 'utf8'); - // Apply the addon and core patches independently. They previously shared - // a single `VSCODE_FOUNDRY_LOCAL_NATIVE_DIR` presence check, so if only - // one SDK needle changed the file was left half-patched and every later - // run skipped it entirely — and since packaging removes both native - // fallbacks, a missing half makes shipped dictation unusable. Use a - // distinct marker per half and apply whichever is absent. - const addonMarker = '// VSCODE_PATCH:foundry-addon-native-dir'; - const coreMarker = '// VSCODE_PATCH:foundry-core-native-dir'; - const addonNeedle = ` const platformKey = \`\${platform}-\${arch}\`;\n // The prebuilt addon ships inside the SDK package under prebuilds//\n const sdkRoot = path.resolve(__dirname, '..', '..');`; - const addonReplacement = ` const platformKey = \`\${platform}-\${arch}\`;\n ${addonMarker}: prefer the on-demand native runtime cache when present.\n const overrideDir = process.env.VSCODE_FOUNDRY_LOCAL_NATIVE_DIR;\n if (overrideDir) {\n const overridePath = path.join(overrideDir, 'prebuilds', platformKey, 'foundry_local_napi.node');\n if (fs.existsSync(overridePath)) {\n return require(overridePath);\n }\n }\n // The prebuilt addon ships inside the SDK package under prebuilds//\n const sdkRoot = path.resolve(__dirname, '..', '..');`; - const coreNeedle = ` const platformKey = \`\${platform}-\${arch}\`;\n // Resolve the native binary directory at foundry-local-core/,`; - const coreReplacement = ` const platformKey = \`\${platform}-\${arch}\`;\n ${coreMarker}: prefer the on-demand native runtime cache when present.\n const overrideDir = process.env.VSCODE_FOUNDRY_LOCAL_NATIVE_DIR;\n if (overrideDir) {\n const overrideExt = CoreInterop._getLibraryExtension();\n const overrideCorePath = path.join(overrideDir, 'foundry-local-core', platformKey, \`Microsoft.AI.Foundry.Local.Core\${overrideExt}\`);\n if (fs.existsSync(overrideCorePath)) {\n config.params['FoundryLocalCorePath'] = overrideCorePath;\n return overrideCorePath;\n }\n }\n // Resolve the native binary directory at foundry-local-core/,`; - let patched = content; - if (!patched.includes(addonMarker)) { - if (patched.includes(addonNeedle)) { - patched = patched.replace(addonNeedle, addonReplacement); - } else { - log(dir || '.', 'WARNING: foundry-local-sdk coreInterop.js loadAddon shape changed; skipped addon override patch'); + const sessionFile = path.join(root, dir, 'node_modules', '@github', 'copilot-sdk', 'dist', 'session.js'); + if (fs.existsSync(sessionFile)) { + const content = fs.readFileSync(sessionFile, 'utf8'); + const patched = content.replace(/from "vscode-jsonrpc\/node"/g, 'from "vscode-jsonrpc/node.js"'); + if (content !== patched) { + fs.writeFileSync(sessionFile, patched); + log(dir || '.', 'Patched @github/copilot-sdk session.js (vscode-jsonrpc ESM import fix)'); } } - if (!patched.includes(coreMarker)) { - if (patched.includes(coreNeedle)) { - patched = patched.replace(coreNeedle, coreReplacement); - } else { - log(dir || '.', 'WARNING: foundry-local-sdk coreInterop.js _resolveDefaultCorePath shape changed; skipped core override patch'); - } - } - if (content !== patched) { - fs.writeFileSync(coreInteropFile, patched); - log(dir || '.', 'Patched foundry-local-sdk coreInterop.js (on-demand native runtime override)'); - } } } diff --git a/package-lock.json b/package-lock.json index ab78309d1da034..615559586b5aa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "@xterm/xterm": "^6.1.0-beta.304", "chrome-remote-interface": "^0.33.0", "detect-libc": "^2.1.2", - "foundry-local-sdk": "1.2.3", + "foundry-local-sdk": "2.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -5561,12 +5561,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", - "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", + "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/agent-base": { @@ -10168,13 +10168,26 @@ } }, "node_modules/foundry-local-sdk": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/foundry-local-sdk/-/foundry-local-sdk-1.2.3.tgz", - "integrity": "sha512-1iy5Aixi+p3GiGbIZN3rIuKsLYmcXWjqEDPMNFie2g9ufoTq58OZf+tdYdI4AKA7kNsteO9HWHnzDoPqdoxw5A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/foundry-local-sdk/-/foundry-local-sdk-2.0.1.tgz", + "integrity": "sha512-5TwBhKypAGEeezfvluiNlWPVIBX82MW9riqerlROKEzODRYxmzo13fKQ2qEDkVBbioV2PWP4LFFzUqk/NrKjcw==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "adm-zip": "^0.5.16" + "adm-zip": "^0.6.0", + "node-addon-api": "^8.2.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/foundry-local-sdk/node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" } }, "node_modules/fragment-cache": { diff --git a/package.json b/package.json index fd2a5ba330e00a..a7d63f6e8bc37d 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "@xterm/xterm": "^6.1.0-beta.304", "chrome-remote-interface": "^0.33.0", "detect-libc": "^2.1.2", - "foundry-local-sdk": "1.2.3", + "foundry-local-sdk": "2.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -293,7 +293,7 @@ "allowScripts": { "bufferutil@4.1.0": true, "cpu-features": false, - "foundry-local-sdk@1.2.3": true, + "foundry-local-sdk@2.0.1": true, "kerberos@2.1.1": true, "koffi@3.2.1": false, "native-keymap@3.3.9": true, From 7d8858031b666e04ecb173ad447deee6822d541b Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:10:31 -0700 Subject: [PATCH 02/17] Apply PR feedback --- .../common/dictation-runtime-produce.yml | 6 +- build/dictation-runtime/common.ts | 25 ++-- build/dictation-runtime/nuget.ts | 71 +++++++---- build/dictation-runtime/package.ts | 103 +++++++++------- build/gulpfile.vscode.ts | 15 +-- build/lib/test/dictationRuntime.test.ts | 53 +++++++++ .../common/localTranscription.ts | 15 +-- .../node/foundryLocalRuntime.ts | 111 +++++++++--------- .../node/localTranscriptionService.ts | 42 +++---- .../test/node/foundryLocalRuntime.test.ts | 53 +++++---- .../localTranscriptionService.ts | 2 +- 11 files changed, 290 insertions(+), 206 deletions(-) create mode 100644 build/lib/test/dictationRuntime.test.ts diff --git a/build/azure-pipelines/common/dictation-runtime-produce.yml b/build/azure-pipelines/common/dictation-runtime-produce.yml index ba30ab03719f46..acd3d1d4d45b73 100644 --- a/build/azure-pipelines/common/dictation-runtime-produce.yml +++ b/build/azure-pipelines/common/dictation-runtime-produce.yml @@ -16,10 +16,10 @@ parameters: # real publish runs (VSCODE_PUBLISH=true). darwin-x64 stamps but uploads # nothing (the darwin-arm64 job publishes the shared Apple Silicon payload). # -# The core libraries are fetched from NuGet for the target's explicit RID (see +# The ONNX libraries are fetched from NuGet for the target's explicit RID (see # build/dictation-runtime/nuget.ts), so ARM64 targets build fine on x64 pools; -# the prebuilt addon is copied from the already-restored node_modules. No npm -# registry auth is needed. +# both addons and the Foundry Local library are copied from the already-restored +# node_modules. No npm registry auth is needed. # # Steps: # 1. AzureCLI@2 to fetch the SPN credentials for the CDN storage account — diff --git a/build/dictation-runtime/common.ts b/build/dictation-runtime/common.ts index 857618f3cf7801..36ed2955064e4e 100644 --- a/build/dictation-runtime/common.ts +++ b/build/dictation-runtime/common.ts @@ -11,9 +11,9 @@ * 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 + * by on-device dictation: two prebuilt N-API addons plus the Foundry Local / + * 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`). @@ -38,6 +38,11 @@ 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'; +/** Resolve the SDK root through its ESM-only exported entry point. */ +export function resolveSdkPackageRoot(): string { + return path.resolve(path.dirname(fileURLToPath(import.meta.resolve(SDK_PACKAGE_NAME))), '..'); +} + /** * 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 @@ -103,7 +108,7 @@ export const SUPPORTED_TARGETS: ReadonlySet = new Set([ * 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. + * real `alpine` platform return `undefined`: the native libraries are glibc-linked. */ export function getRuntimeTargetForBuild(vscodePlatform: string, arch: string): string | undefined { if (vscodePlatform === 'alpine' || arch === 'alpine' || arch === 'musl') { @@ -127,13 +132,13 @@ export function getRuntimeTargetForBuild(vscodePlatform: string, arch: string): * 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 + * - non-publish product builds: packaging keeps the SDK's addons but strips + * its shared libraries (`getFoundryLocalExcludeFilter` in + * `gulpfile.vscode.ts`), so a packaged build with no stamp would have no + * loadable runtime. 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. + * (idempotently) by publish runs. Local dev-from-source (which never runs + * `produce.ts`) uses the complete `node_modules` payload. * * Returns `false` for platforms/arches that can never host dictation (armhf, * Alpine/musl, web) so their `product.json` stays clean. diff --git a/build/dictation-runtime/nuget.ts b/build/dictation-runtime/nuget.ts index 06693879567d30..01af174a434b49 100644 --- a/build/dictation-runtime/nuget.ts +++ b/build/dictation-runtime/nuget.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ /** - * Fetches the Foundry Local native core libraries (Foundry Local Core + - * onnxruntime + onnxruntime-genai) from NuGet for an EXPLICIT RID, so a single + * Fetches the Foundry Local native dependencies (onnxruntime + + * onnxruntime-genai) from NuGet for an EXPLICIT RID, so a single * build agent can assemble a tarball for any target regardless of its own * `process.platform`/`process.arch`. * @@ -17,7 +17,7 @@ * tarballs; extracting `runtimes//native/*` from the same `.nupkg` files * for an explicit RID is host-independent and fixes that. * - * Only the "standard" artifact set is supported (the three packages selected by + * Only the "standard" artifact set is supported (the two packages selected by * `package.ts`); the SDK installer's WinML override / `includeFiles` / * `removeFiles` paths are intentionally not ported. */ @@ -28,7 +28,7 @@ import * as fs from 'fs'; import * as https from 'https'; import * as os from 'os'; import * as path from 'path'; -import { SDK_PACKAGE_NAME } from './common.ts'; +import { resolveSdkPackageRoot } from './common.ts'; const SCRIPT = 'nuget.ts'; const VSCODE_FEED_PREFIX = 'https://pkgs.dev.azure.com/monacotools/'; @@ -44,8 +44,7 @@ export const VSCODE_NUGET_FEED = 'https://pkgs.dev.azure.com/monacotools/Monaco/ * `.nupkg` archives. */ function loadAdmZip(): any { - const sdkRequire = createRequire(import.meta.url); - const fromSdk = createRequire(sdkRequire.resolve(`${SDK_PACKAGE_NAME}/package.json`)); + const fromSdk = createRequire(path.join(resolveSdkPackageRoot(), 'package.json')); return fromSdk('adm-zip'); } @@ -89,46 +88,68 @@ export interface INugetArtifact { } export interface IFoundryDependencyVersions { - readonly 'foundry-local-core': { readonly nuget: string }; readonly onnxruntime: { readonly version: string }; readonly 'onnxruntime-genai': { readonly version: string }; } -export interface IFetchCoreLibrariesOptions { +export interface IFetchDependencyLibrariesOptions { readonly feeds?: readonly string[]; readonly skipIfPresent?: boolean; } -export function supportsCoreLibraryTarget(target: string): boolean { +export function supportsDependencyLibraryTarget(target: string): boolean { return Object.hasOwn(RID_BY_TARGET, target); } -export function getStandardArtifacts(target: string, dependencies: IFoundryDependencyVersions): readonly INugetArtifact[] { - const ortPackageName = target === 'linux-x64' ? 'Microsoft.ML.OnnxRuntime.Gpu.Linux' : 'Microsoft.ML.OnnxRuntime.Foundry'; +export function getStandardArtifacts(dependencies: IFoundryDependencyVersions): readonly INugetArtifact[] { return [ - { name: 'Microsoft.AI.Foundry.Local.Core', version: dependencies['foundry-local-core'].nuget }, - { name: ortPackageName, version: dependencies.onnxruntime.version }, + { name: 'Microsoft.ML.OnnxRuntime', version: dependencies.onnxruntime.version }, { name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: dependencies['onnxruntime-genai'].version }, ]; } -export function requiredCoreLibraryNames(target: string): readonly string[] { +export function requiredDependencyLibraryNames(target: string, dependencies: IFoundryDependencyVersions): readonly string[] { const isWin = target.startsWith('win32-'); - const ext = isWin ? '.dll' : target.startsWith('darwin-') ? '.dylib' : '.so'; + const isDarwin = target.startsWith('darwin-'); + const ext = isWin ? '.dll' : isDarwin ? '.dylib' : '.so'; const prefix = isWin ? '' : 'lib'; + const onnxRuntime = isWin + ? 'onnxruntime.dll' + : isDarwin + ? `libonnxruntime.${dependencies.onnxruntime.version.split('.')[0]}.dylib` + : 'libonnxruntime.so.1'; return [ - `Microsoft.AI.Foundry.Local.Core${ext}`, - `${prefix}onnxruntime${ext}`, + onnxRuntime, `${prefix}onnxruntime-genai${ext}`, ]; } +export function normalizeOrtLibraryName(binDir: string, target: string, version: string): void { + let unversioned: string; + let versioned: string; + if (target.startsWith('linux-')) { + unversioned = path.join(binDir, 'libonnxruntime.so'); + versioned = path.join(binDir, 'libonnxruntime.so.1'); + } else if (target.startsWith('darwin-')) { + unversioned = path.join(binDir, 'libonnxruntime.dylib'); + versioned = path.join(binDir, `libonnxruntime.${version.split('.')[0]}.dylib`); + } else { + return; + } + if (!fs.existsSync(versioned) && fs.existsSync(unversioned)) { + fs.renameSync(unversioned, versioned); + } + if (target.startsWith('darwin-') && fs.existsSync(versioned) && !fs.existsSync(unversioned)) { + fs.symlinkSync(path.basename(versioned), unversioned); + } +} + /** * Download each `artifact` `.nupkg` for `target`'s RID and extract its native * shared libraries into `binDir`. Throws if a package can't be fetched from any * feed; callers verify the resulting library set separately. */ -export async function fetchCoreLibraries(target: string, artifacts: readonly INugetArtifact[], binDir: string, options?: IFetchCoreLibrariesOptions): Promise { +export async function fetchDependencyLibraries(target: string, artifacts: readonly INugetArtifact[], binDir: string, options?: IFetchDependencyLibrariesOptions): Promise { const rid = RID_BY_TARGET[target]; if (!rid) { throw new Error(`[${SCRIPT}] No NuGet RID mapping for target '${target}'.`); @@ -163,7 +184,7 @@ async function installPackage( skipIfPresent: boolean, ): Promise { if (skipIfPresent) { - const expectedFile = expectedCoreLibraryName(target, artifact.name); + const expectedFile = expectedDependencyLibraryName(target, artifact.name); if (expectedFile && fs.existsSync(path.join(binDir, expectedFile))) { console.log(`[${SCRIPT}] ${artifact.name}: already present, skipping download.`); return; @@ -206,16 +227,14 @@ async function installPackage( throw new Error(`[${SCRIPT}] Failed to download ${artifact.name} ${artifact.version} from any feed (${feedHosts}): ${lastError instanceof Error ? lastError.message : lastError}`); } -function expectedCoreLibraryName(target: string, packageName: string): string | undefined { - const [foundryCore, onnxRuntime, onnxRuntimeGenAI] = requiredCoreLibraryNames(target); - if (packageName.includes('Foundry.Local.Core')) { - return foundryCore; - } +function expectedDependencyLibraryName(target: string, packageName: string): string | undefined { + const isWin = target.startsWith('win32-'); + const isDarwin = target.startsWith('darwin-'); if (packageName.includes('OnnxRuntimeGenAI')) { - return onnxRuntimeGenAI; + return `${isWin ? '' : 'lib'}onnxruntime-genai${isWin ? '.dll' : isDarwin ? '.dylib' : '.so'}`; } if (packageName.includes('OnnxRuntime')) { - return onnxRuntime; + return isWin ? 'onnxruntime.dll' : isDarwin ? 'libonnxruntime.dylib' : 'libonnxruntime.so'; } return undefined; } diff --git a/build/dictation-runtime/package.ts b/build/dictation-runtime/package.ts index 22ecfcab261608..04d97d09b4062c 100644 --- a/build/dictation-runtime/package.ts +++ b/build/dictation-runtime/package.ts @@ -5,7 +5,7 @@ /** * Builds one per-target tarball of the Foundry Local native runtime (the - * prebuilt N-API addon + the Foundry Local Core / onnxruntime / onnxruntime-genai + * prebuilt N-API addons + the Foundry Local / onnxruntime / onnxruntime-genai * shared libraries). Callable as both a Node library (`buildOne(...)`) and a thin * CLI (bottom of this file). * @@ -13,32 +13,28 @@ * "Dictation runtime: build + upload" pipeline step; the CLI form is for local * one-off builds and requires `VSS_NUGET_ACCESSTOKEN` for the VS Code NuGet feed. * - * The addon is copied from the pinned `foundry-local-sdk` package's `prebuilds/` - * (which ships every target), and the core libraries are fetched from NuGet for - * the requested target's RID via `fetchCoreLibraries` (NOT the SDK's host-locked - * installer), so ANY build host can produce ANY target's tarball. This is what - * lets VS Code's ARM64 desktop builds — which run on x64 pools — publish their - * `linux-arm64`/`win32-arm64` runtimes. + * The addons and Foundry Local library are copied from the pinned + * `foundry-local-sdk` package's `prebuilds/` (which ships every target), and the + * ONNX libraries are fetched from NuGet for the requested target's RID via + * `fetchDependencyLibraries`, so ANY build host can produce ANY target's + * tarball. * * The produced tarball's internal layout mirrors the runtime cache layout so the * runtime extraction is a plain untar: * - * prebuilds//foundry_local_napi.node - * foundry-local-core// + * prebuilds// */ -import { createRequire } from 'module'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as tar from 'tar'; -import { getRuntimeVersion, parseFlags, SDK_PACKAGE_NAME, sha256OfFile, SUPPORTED_TARGETS } from './common.ts'; -import { fetchCoreLibraries, getStandardArtifacts, type IFoundryDependencyVersions, requiredCoreLibraryNames } from './nuget.ts'; +import { getRuntimeVersion, parseFlags, resolveSdkPackageRoot, SDK_PACKAGE_NAME, sha256OfFile, SUPPORTED_TARGETS } from './common.ts'; +import { fetchDependencyLibraries, getStandardArtifacts, type IFoundryDependencyVersions, normalizeOrtLibraryName, requiredDependencyLibraryNames } from './nuget.ts'; const SCRIPT = 'package.ts'; -/** Resolve `foundry-local-sdk` subpaths from the repo-root `node_modules`. */ -const sdkRequire = createRequire(import.meta.url); +const SDK_ROOT = resolveSdkPackageRoot(); export interface IBuildResult { readonly tgzPath: string; @@ -53,10 +49,9 @@ export interface IBuildArgs { } /** - * Build one runtime tarball for `args.target`. Copies the prebuilt addon from - * the installed SDK, fetches the matching core libraries via the SDK's NuGet - * installer, and tars both into a single gzipped tarball. Returns the produced - * `.tgz` path and its sha256. + * Build one runtime tarball for `args.target`. Copies the SDK's prebuilt native + * files, fetches the matching ONNX libraries from NuGet, and tars them into a + * single gzipped tarball. Returns the produced `.tgz` path and its sha256. */ export async function buildOne(args: IBuildArgs): Promise { if (!SUPPORTED_TARGETS.has(args.target)) { @@ -68,8 +63,8 @@ export async function buildOne(args: IBuildArgs): Promise { try { console.log(`[${SCRIPT}] Building ${SDK_PACKAGE_NAME}@${version} native runtime for ${args.target} in ${stagingDir}`); - await stageAddon(stagingDir, args.target); - await stageCoreLibraries(stagingDir, args.target); + await stageSdkNativeFiles(stagingDir, args.target); + await stageDependencyLibraries(stagingDir, args.target); fs.mkdirSync(args.outDir, { recursive: true }); const tgzPath = path.join(args.outDir, `${args.target}.tgz`); @@ -86,41 +81,61 @@ export async function buildOne(args: IBuildArgs): Promise { } /** - * Copy the prebuilt N-API addon for `target` out of the installed - * `foundry-local-sdk` package (`prebuilds//foundry_local_napi.node`) - * into the staging tree. + * Copy the native files shipped in the SDK's prebuild directory for `target`. + * This includes both Node-API addons and the Foundry Local shared library. */ -async function stageAddon(stagingDir: string, target: string): Promise { - const sdkRoot = path.dirname(sdkRequire.resolve(`${SDK_PACKAGE_NAME}/package.json`)); - const addonSrc = path.join(sdkRoot, 'prebuilds', target, 'foundry_local_napi.node'); - if (!fs.existsSync(addonSrc)) { - throw new Error(`[${SCRIPT}] Prebuilt addon not found for ${target} at ${addonSrc}. Is ${SDK_PACKAGE_NAME} installed?`); +async function stageSdkNativeFiles(stagingDir: string, target: string): Promise { + const sourceDir = path.join(SDK_ROOT, 'prebuilds', target); + if (!fs.existsSync(sourceDir)) { + throw new Error(`[${SCRIPT}] Prebuild directory not found for ${target} at ${sourceDir}. Is ${SDK_PACKAGE_NAME} installed?`); + } + const targetDir = path.join(stagingDir, 'prebuilds', target); + fs.mkdirSync(targetDir, { recursive: true }); + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { + if (entry.isFile() && isNativeFile(entry.name)) { + fs.copyFileSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name)); + } + } + for (const name of requiredSdkNativeFileNames(target)) { + if (!fs.existsSync(path.join(targetDir, name))) { + throw new Error(`[${SCRIPT}] SDK native file '${name}' not found for ${target} in ${sourceDir}.`); + } } - const addonDest = path.join(stagingDir, 'prebuilds', target, 'foundry_local_napi.node'); - fs.mkdirSync(path.dirname(addonDest), { recursive: true }); - fs.copyFileSync(addonSrc, addonDest); } /** - * Fetch the Foundry Local core libraries for `target` into the staging tree from - * NuGet, for that target's explicit RID (see `fetchCoreLibraries`). Replicates - * the standard variant's artifact selection, including the linux-x64 GPU ONNX - * Runtime package. Host-independent — `target` need not match the build host. + * Fetch the ONNX libraries for `target` into the same flat prebuild directory, + * using that target's explicit RID. Host-independent — `target` need not match + * the build host. */ -async function stageCoreLibraries(stagingDir: string, target: string): Promise { - const dependencies = sdkRequire(`${SDK_PACKAGE_NAME}/deps_versions.json`) as IFoundryDependencyVersions; - const artifacts = getStandardArtifacts(target, dependencies); +async function stageDependencyLibraries(stagingDir: string, target: string): Promise { + const dependencies = JSON.parse(fs.readFileSync(path.join(SDK_ROOT, 'deps_versions.json'), 'utf8')) as IFoundryDependencyVersions; + const artifacts = getStandardArtifacts(dependencies); - const coreDir = path.join(stagingDir, 'foundry-local-core', target); - await fetchCoreLibraries(target, artifacts, coreDir); + const targetDir = path.join(stagingDir, 'prebuilds', target); + await fetchDependencyLibraries(target, artifacts, targetDir, { skipIfPresent: true }); + normalizeOrtLibraryName(targetDir, target, dependencies.onnxruntime.version); - for (const name of requiredCoreLibraryNames(target)) { - if (!fs.existsSync(path.join(coreDir, name))) { - throw new Error(`[${SCRIPT}] Core library '${name}' missing after install for ${target} — refusing to build an incomplete tarball.`); + for (const name of requiredDependencyLibraryNames(target, dependencies)) { + if (!fs.existsSync(path.join(targetDir, name))) { + throw new Error(`[${SCRIPT}] Dependency library '${name}' missing after install for ${target} — refusing to build an incomplete tarball.`); } } } +function requiredSdkNativeFileNames(target: string): readonly string[] { + const foundryLocalLibrary = target.startsWith('win32-') + ? 'foundry_local.dll' + : target.startsWith('darwin-') + ? 'libfoundry_local.dylib' + : 'libfoundry_local.so'; + return ['foundry_local_node.node', 'foundry_local_preload.node', foundryLocalLibrary]; +} + +function isNativeFile(name: string): boolean { + return name.endsWith('.node') || name.endsWith('.dll') || name.includes('.dylib') || name.includes('.so'); +} + /** * Build the gzipped tar via node-tar so the output is consistent regardless of * which host's system tar would otherwise be used. `portable`/`mtime` strip @@ -135,7 +150,7 @@ async function buildTarball(stagingDir: string, outTgz: string): Promise { portable: true, mtime: new Date(0), }, - ['prebuilds', 'foundry-local-core'], + ['prebuilds'], ); } diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 8d648346d5c9a6..cfa70e11b359ff 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -115,19 +115,16 @@ function computeChecksum(filename: string): string { return hash; } -// foundry-local-sdk (on-device chat dictation) ships a prebuilt N-API addon -// (`foundry_local_napi.node`) inside its tarball, and its native core libraries -// are fetched per-RID into `foundry-local-core/-/` at install -// time. The addon requires a newer glibc than VS Code's minimum supported Linux -// distros, so we deliberately do NOT ship any of this native payload: it is -// downloaded on demand at runtime, only on supported platforms, into a per-user -// cache (see `src/vs/platform/localTranscription/node/foundryLocalRuntime.ts`). -// Exclude every prebuilt addon and core library from the package here. +// foundry-local-sdk (on-device chat dictation) loads two N-API addons from its +// package, while configureNativeLoader redirects its shared libraries to the +// per-user runtime cache. Keep the addons but exclude the shared libraries, +// which require a newer glibc than VS Code's minimum supported Linux distros. function getFoundryLocalExcludeFilter(): string[] { return [ '**', '!**/foundry-local-sdk/prebuilds/**', - '!**/foundry-local-sdk/foundry-local-core/**', + '**/foundry-local-sdk/prebuilds/**/foundry_local_node.node', + '**/foundry-local-sdk/prebuilds/**/foundry_local_preload.node', ]; } diff --git a/build/lib/test/dictationRuntime.test.ts b/build/lib/test/dictationRuntime.test.ts new file mode 100644 index 00000000000000..65e168c1386930 --- /dev/null +++ b/build/lib/test/dictationRuntime.test.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { suite, test } from 'node:test'; +import { + getStandardArtifacts, + type IFoundryDependencyVersions, + normalizeOrtLibraryName, + requiredDependencyLibraryNames, +} from '../../dictation-runtime/nuget.ts'; + +const dependencies: IFoundryDependencyVersions = { + onnxruntime: { version: '1.28.0' }, + 'onnxruntime-genai': { version: '0.15.2' }, +}; + +suite('dictation runtime', () => { + test('uses the Foundry Local 2.x dependency artifact set', () => { + assert.deepStrictEqual(getStandardArtifacts(dependencies), [ + { name: 'Microsoft.ML.OnnxRuntime', version: '1.28.0' }, + { name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: '0.15.2' }, + ]); + }); + + test('requires the platform-specific 2.x dependency names', () => { + assert.deepStrictEqual({ + linux: requiredDependencyLibraryNames('linux-x64', dependencies), + darwin: requiredDependencyLibraryNames('darwin-arm64', dependencies), + win32: requiredDependencyLibraryNames('win32-arm64', dependencies), + }, { + linux: ['libonnxruntime.so.1', 'libonnxruntime-genai.so'], + darwin: ['libonnxruntime.1.dylib', 'libonnxruntime-genai.dylib'], + win32: ['onnxruntime.dll', 'onnxruntime-genai.dll'], + }); + }); + + test('normalizes the Linux ONNX Runtime soname', () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'dictation-runtime-test-')); + try { + fs.writeFileSync(path.join(directory, 'libonnxruntime.so'), 'runtime'); + normalizeOrtLibraryName(directory, 'linux-x64', dependencies.onnxruntime.version); + assert.deepStrictEqual(fs.readdirSync(directory), ['libonnxruntime.so.1']); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); +}); diff --git a/src/vs/platform/localTranscription/common/localTranscription.ts b/src/vs/platform/localTranscription/common/localTranscription.ts index 2678c9159558b3..133c68e13f3a79 100644 --- a/src/vs/platform/localTranscription/common/localTranscription.ts +++ b/src/vs/platform/localTranscription/common/localTranscription.ts @@ -115,10 +115,10 @@ export interface ILocalTranscriptionService { * * `proxyUrl`/`noProxy` bridge VS Code's `http.proxy`/`http.noProxy` settings * into this utility process: when set, they are applied as the standard proxy - * environment variables before any download, so all provisioning legs — the - * addon tarball and NuGet core libraries (our own fetches) and the native - * Foundry Local *model* download — route through the proxy. When they are - * omitted, the process's inherited OS environment proxy vars still apply. + * environment variables before any download, so both the native runtime CDN + * download and the Foundry Local *model* download route through the proxy. + * When they are omitted, the process's inherited OS environment proxy vars + * still apply. * * `proxyStrictSSL === false` (VS Code's `http.proxyStrictSSL`) disables TLS * certificate verification for the JavaScript download legs. `proxyAuthorization` @@ -130,9 +130,10 @@ export interface ILocalTranscriptionService { * * `runtimeUrlTemplate`/`runtimeVersion` come from `product.dictationRuntime` * (stamped by `build/dictation-runtime/produce.ts`). When set, the native - * runtime (Foundry Local addon + core libraries) is downloaded from VS Code's - * CDN for this host's target. When omitted (local dev builds), the runtime - * falls back to the SDK's own `node_modules` payload and nothing is downloaded. + * runtime (Foundry Local addons + shared libraries) is downloaded from VS + * Code's CDN for this host's target. When omitted (local dev builds), the + * runtime falls back to the SDK's own `node_modules` payload and nothing is + * downloaded. */ start(options: { readonly cacheDir: string; readonly model?: string; readonly language?: string; readonly proxyUrl?: string; readonly noProxy?: string; readonly proxyStrictSSL?: boolean; readonly proxyAuthorization?: string; readonly runtimeUrlTemplate?: string; readonly runtimeVersion?: string }): Promise; diff --git a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts index 5a268ab7e508c3..0bc3f2e772c450 100644 --- a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts +++ b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts @@ -14,24 +14,23 @@ import { CancellationError } from '../../../base/common/errors.js'; * On-demand provisioning of the Foundry Local native runtime used by on-device * dictation. * - * `foundry-local-sdk` ships a prebuilt N-API addon (`foundry_local_napi.node`) - * and native core libraries (Foundry Local Core + ONNX Runtime + ONNX Runtime - * GenAI). The addon requires a newer glibc than VS Code's minimum supported - * Linux distros, so we deliberately do NOT bundle any of this native payload - * with the product (see `build/gulpfile.vscode.ts`). Instead we republish a - * per-target tarball of the addon + core libraries to VS Code's CDN at build - * time (see `build/dictation-runtime/`) and download it here, at runtime, only - * on supported platforms, into a per-user writable cache — keeping the shipped - * package's glibc floor intact and avoiding any runtime dependency on the npm - * registry or NuGet. + * `foundry-local-sdk` ships two prebuilt N-API addons (`foundry_local_node.node` + * and `foundry_local_preload.node`) and native libraries (Foundry Local + ONNX + * Runtime + ONNX Runtime GenAI). The shared libraries require a newer glibc than + * VS Code's minimum supported Linux distros, so we bundle only the addons with + * the product (see `build/gulpfile.vscode.ts`). We republish the complete + * per-target native directory to VS Code's CDN at build time (see + * `build/dictation-runtime/`) and download it here, at runtime, into a per-user + * writable cache. This keeps the shipped package's glibc floor intact and + * avoids any runtime dependency on the npm registry or NuGet. * - * The SDK loader (`dist/detail/coreInterop.js`) is patched during - * `postinstall` to honor `VSCODE_FOUNDRY_LOCAL_NATIVE_DIR`, pointing it at the - * cache directory this module populates. The tarball's internal layout mirrors - * the SDK's own package layout so the patched resolution is a trivial path join: + * The tarball's internal layout mirrors the SDK's own package layout: * - * //prebuilds//foundry_local_napi.node - * //foundry-local-core// + * //prebuilds// + * + * The SDK keeps its addons in the packaged npm module and is configured through + * `configureNativeLoader`/`FoundryLocalConfig.libraryPath` to preload the shared + * libraries from this cache directory. * * NOTE: the single CDN download leg honors the standard proxy environment * variables (`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY`, with `NO_PROXY`). VS Code's @@ -45,7 +44,7 @@ import { CancellationError } from '../../../base/common/errors.js'; /** * Platforms (`-`) for which Foundry Local ships - * a native addon + core libraries. Mirrors the SDK installer's RID map. + * native addons and libraries. Mirrors the SDK installer's RID map. */ export const FOUNDRY_LOCAL_SUPPORTED_PLATFORMS: ReadonlySet = new Set([ 'darwin-arm64', @@ -84,10 +83,10 @@ const inFlight = new Map>(); const DOWNLOAD_INACTIVITY_TIMEOUT_MS = 60_000; /** - * Ensure the Foundry Local native runtime (addon + core libraries) is present + * Ensure the Foundry Local native runtime (addons + shared libraries) is present * in ``, downloading the per-target CDN tarball if necessary. Returns - * the versioned override directory to set as `VSCODE_FOUNDRY_LOCAL_NATIVE_DIR` - * before loading the SDK. + * the concrete cached `prebuilds/` directory to use as the SDK's + * `libraryPath` before constructing a manager. * * Idempotent: once a version is fully provisioned a per-platform `.complete` * marker is written and subsequent calls return immediately (after verifying the @@ -100,15 +99,16 @@ export async function ensureFoundryLocalRuntime(cacheRoot: string, download: IFo } const overrideDir = join(cacheRoot, download.version); + const libraryPath = foundryPrebuildDir(overrideDir, platformKey); // A single in-flight provisioning per override dir; late joiners share it. - const existing = inFlight.get(overrideDir); + const existing = inFlight.get(libraryPath); if (existing) { return existing; } const promise = doEnsure(overrideDir, platformKey, download, token, onProgress) - .finally(() => inFlight.delete(overrideDir)); - inFlight.set(overrideDir, promise); + .finally(() => inFlight.delete(libraryPath)); + inFlight.set(libraryPath, promise); return promise; } @@ -120,7 +120,7 @@ async function doEnsure(overrideDir: string, platformKey: string, download: IFou // marker never short-circuits this arch's provisioning and a stale/partially // deleted cache is repaired rather than trusted. if (isRuntimeProvisioned(overrideDir, platformKey)) { - return overrideDir; + return foundryPrebuildDir(overrideDir, platformKey); } // Fail fast (before any download) when the host can't actually load the @@ -131,7 +131,7 @@ async function doEnsure(overrideDir: string, platformKey: string, download: IFou onProgress?.('Downloading dictation runtime…'); await provisionRuntime(overrideDir, platformKey, download.urlTemplate, download.version, token); - return overrideDir; + return foundryPrebuildDir(overrideDir, platformKey); } /** @@ -142,8 +142,7 @@ async function doEnsure(overrideDir: string, platformKey: string, download: IFou * host should use `ensureFoundryLocalRuntime`, which gates and de-dupes. */ export async function provisionRuntime(overrideDir: string, platformKey: string, urlTemplate: string, version: string, token: CancellationToken): Promise { - const addonPath = foundryAddonPath(overrideDir, platformKey); - const coreDir = foundryCoreDir(overrideDir, platformKey); + const targetDir = foundryPrebuildDir(overrideDir, platformKey); // The cache is shared by the utility processes of every open VS Code window, // so provision into a process-unique staging dir and atomically promote each @@ -152,24 +151,25 @@ export async function provisionRuntime(overrideDir: string, platformKey: string, // the published copy and the loser accepts it as success. const url = format2(urlTemplate, { target: platformKey }); const staging = join(overrideDir, `.staging-${process.pid}-${randomSuffix()}`); - const stagingAddon = join(staging, 'prebuilds', platformKey, 'foundry_local_napi.node'); - const stagingCore = join(staging, 'foundry-local-core', platformKey); + const stagingTarget = foundryPrebuildDir(staging, platformKey); try { await downloadAndExtractTarball(url, staging, token); throwIfCancelled(token); - if (!fs.existsSync(stagingAddon) || !hasAllCoreLibraries(stagingCore)) { + if (!hasAllRuntimeFiles(stagingTarget, platformKey)) { throw new Error(`Foundry Local native runtime download from ${url} completed but expected files are missing.`); } - await promoteDir(dirname(stagingAddon), dirname(addonPath)); - await promoteDir(stagingCore, coreDir); + if (fs.existsSync(targetDir) && !hasAllRuntimeFiles(targetDir, platformKey)) { + await fs.promises.rm(targetDir, { recursive: true, force: true }); + } + await promoteDir(stagingTarget, targetDir); } finally { await fs.promises.rm(staging, { recursive: true, force: true }).catch(() => { /* best effort */ }); } // Verify the published payload — ours or a concurrent winner's — is complete. - if (!fs.existsSync(addonPath) || !hasAllCoreLibraries(coreDir)) { + if (!hasAllRuntimeFiles(targetDir, platformKey)) { throw new Error('Foundry Local native runtime is incomplete after provisioning.'); } @@ -181,26 +181,20 @@ function foundryMarkerPath(overrideDir: string, platformKey: string): string { return join(overrideDir, `.complete-${platformKey}`); } -/** Path of the prebuilt N-API addon inside a versioned override dir. */ -function foundryAddonPath(overrideDir: string, platformKey: string): string { - return join(overrideDir, 'prebuilds', platformKey, 'foundry_local_napi.node'); -} - -/** Directory of the native core libraries inside a versioned override dir. */ -function foundryCoreDir(overrideDir: string, platformKey: string): string { - return join(overrideDir, 'foundry-local-core', platformKey); +/** Directory containing the target's addons and shared libraries. */ +function foundryPrebuildDir(overrideDir: string, platformKey: string): string { + return join(overrideDir, 'prebuilds', platformKey); } /** * Whether `` holds a complete, verified runtime for `platformKey`: - * the per-platform marker AND the actual addon + all core libraries. A marker + * the per-platform marker AND all expected addons and shared libraries. A marker * alone is insufficient (it can belong to a different architecture, or the * payload can be partially deleted). Exported for tests. */ export function isRuntimeProvisioned(overrideDir: string, platformKey: string): boolean { return fs.existsSync(foundryMarkerPath(overrideDir, platformKey)) - && fs.existsSync(foundryAddonPath(overrideDir, platformKey)) - && hasAllCoreLibraries(foundryCoreDir(overrideDir, platformKey)); + && hasAllRuntimeFiles(foundryPrebuildDir(overrideDir, platformKey), platformKey); } /** @@ -265,10 +259,9 @@ function detectGlibcVersion(): [number, number] | undefined { /** * Download the per-target runtime tarball from `url` and extract it into - * `stagingDir`, which then contains `prebuilds//foundry_local_napi.node` - * and `foundry-local-core//` (the tarball's layout - * mirrors the cache layout). The tarball is published to VS Code's CDN by - * `build/dictation-runtime/`. + * `stagingDir`, which then contains + * `prebuilds//`. The tarball is published + * to VS Code's CDN by `build/dictation-runtime/`. */ async function downloadAndExtractTarball(url: string, stagingDir: string, token: CancellationToken): Promise { await fs.promises.mkdir(stagingDir, { recursive: true }); @@ -286,20 +279,24 @@ async function downloadAndExtractTarball(url: string, stagingDir: string, token: } } -/** The core library filenames required for the current platform. Exported for tests. */ -export function requiredCoreLibraryNames(): string[] { - const ext = process.platform === 'win32' ? '.dll' : process.platform === 'darwin' ? '.dylib' : '.so'; - const prefix = process.platform === 'win32' ? '' : 'lib'; +/** The native files required for a target's complete runtime. Exported for tests. */ +export function requiredRuntimeFileNames(platformKey: string): string[] { + const isWin = platformKey.startsWith('win32-'); + const isDarwin = platformKey.startsWith('darwin-'); + const ext = isWin ? '.dll' : isDarwin ? '.dylib' : '.so'; + const prefix = isWin ? '' : 'lib'; return [ - `Microsoft.AI.Foundry.Local.Core${ext}`, - `${prefix}onnxruntime${ext}`, + 'foundry_local_node.node', + 'foundry_local_preload.node', + `${prefix}foundry_local${ext}`, + isWin ? 'onnxruntime.dll' : isDarwin ? 'libonnxruntime.1.dylib' : 'libonnxruntime.so.1', `${prefix}onnxruntime-genai${ext}`, ]; } -/** Whether all required core libraries already exist in `coreDir`. */ -function hasAllCoreLibraries(coreDir: string): boolean { - return requiredCoreLibraryNames().every(name => fs.existsSync(join(coreDir, name))); +/** Whether all required runtime files already exist in `targetDir`. */ +function hasAllRuntimeFiles(targetDir: string, platformKey: string): boolean { + return requiredRuntimeFileNames(platformKey).every(name => fs.existsSync(join(targetDir, name))); } /** diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index e4763e20545534..f9733b3a6db872 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -72,11 +72,10 @@ function runtimeCacheDir(modelCacheDir: string): string { } /** - * Foundry Local JS SDK. It is an ESM package that loads a native addon - * (`foundry_local_napi.node`) plus the Foundry Local Core / onnxruntime / - * onnxruntime-genai shared libraries. Import it lazily so forking the utility - * process stays cheap; the model itself is only downloaded/loaded when dictation - * first runs. + * Foundry Local JS SDK. It is an ESM package that loads two native addons plus + * the Foundry Local / onnxruntime / onnxruntime-genai shared libraries. Import + * it lazily so forking the utility process stays cheap; the model itself is only + * downloaded/loaded when dictation first runs. */ type FoundryLocal = typeof import('foundry-local-sdk'); type FoundryLocalManager = import('foundry-local-sdk').FoundryLocalManager; @@ -527,24 +526,25 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans // The model cache state is unknown until the catalog is queried. this._setStatus({ state: LocalTranscriptionModelState.Loading }); - // Ensure the Foundry Local native runtime (N-API addon + core - // libraries) is available before loading the SDK. We do not ship - // it — the addon requires a newer glibc than our minimum supported - // Linux distros — so in packaged builds it is downloaded on demand - // from VS Code's CDN (per `product.dictationRuntime`) into a - // per-user cache and the SDK loader is pointed at it via env var. + // Ensure the Foundry Local shared libraries are available before + // loading the SDK. Packaged builds keep the two addons but download + // the shared libraries on demand from VS Code's CDN into a per-user + // cache and point the SDK loader at it via libraryPath. // This is a no-op once cached. In dev builds (no product config) - // the SDK resolves its addon + core libs from node_modules, so we + // the SDK resolves its addons + shared libraries from node_modules, so we // skip provisioning and leave the loader on its default path. + let nativeLibraryPath: string | undefined; if (this._runtimeDownload) { - const nativeDir = await ensureFoundryLocalRuntime(runtimeCacheDir(cacheDir), this._runtimeDownload, cts.token); - process.env.VSCODE_FOUNDRY_LOCAL_NATIVE_DIR = nativeDir; + nativeLibraryPath = await ensureFoundryLocalRuntime(runtimeCacheDir(cacheDir), this._runtimeDownload, cts.token); } if (!this._sdk) { this._sdk = await import('foundry-local-sdk'); } if (!this._manager) { + if (nativeLibraryPath) { + this._sdk.configureNativeLoader({ libraryPath: nativeLibraryPath }); + } // Store downloaded model files under VS Code's cache dir so // subsequent sessions load without re-downloading ("model // management"). `createAsync` avoids blocking the event loop @@ -553,6 +553,7 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans appName: FOUNDRY_APP_NAME, modelCacheDir: cacheDir, logLevel: 'warn', + ...(nativeLibraryPath ? { libraryPath: nativeLibraryPath } : {}), }); } @@ -571,16 +572,9 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans // download UI appears immediately rather than waiting for the // SDK's first progress callback. this._setStatus({ state: LocalTranscriptionModelState.Downloading, progress: 0 }); - // Bridge VS Code cancellation to the AbortSignal the SDK expects. - const ac = new AbortController(); - const sub = cts.token.onCancellationRequested(() => ac.abort()); - try { - await model.download((percent: number) => { - this._setStatus({ state: LocalTranscriptionModelState.Downloading, progress: Math.min(1, Math.max(0, percent / 100)) }); - }, ac.signal); - } finally { - sub.dispose(); - } + await model.download((percent: number) => { + this._setStatus({ state: LocalTranscriptionModelState.Downloading, progress: Math.min(1, Math.max(0, percent / 100)) }); + }); } // model.load() has no AbortSignal; check cancellation before starting it. diff --git a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts index 10253beb4b878d..48eba7332423d2 100644 --- a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts +++ b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts @@ -20,7 +20,7 @@ import { isRuntimeProvisioned, promoteDir, provisionRuntime, - requiredCoreLibraryNames, + requiredRuntimeFileNames, resolveProxyUrl, } from '../../node/foundryLocalRuntime.js'; @@ -39,12 +39,10 @@ flakySuite('FoundryLocalRuntime', () => { teardown(() => Promises.rm(testDir)); function writePayload(overrideDir: string, key: string): void { - fs.mkdirSync(join(overrideDir, 'prebuilds', key), { recursive: true }); - fs.writeFileSync(join(overrideDir, 'prebuilds', key, 'foundry_local_napi.node'), 'addon'); - const coreDir = join(overrideDir, 'foundry-local-core', key); - fs.mkdirSync(coreDir, { recursive: true }); - for (const name of requiredCoreLibraryNames()) { - fs.writeFileSync(join(coreDir, name), 'lib'); + const targetDir = join(overrideDir, 'prebuilds', key); + fs.mkdirSync(targetDir, { recursive: true }); + for (const name of requiredRuntimeFileNames(key)) { + fs.writeFileSync(join(targetDir, name), 'native'); } } @@ -73,10 +71,17 @@ flakySuite('FoundryLocalRuntime', () => { assert.strictEqual(isRuntimeProvisioned(testDir, platformKey), false); }); - test('isRuntimeProvisioned: false when one core library is missing (partial cache)', () => { + test('isRuntimeProvisioned: false when one shared library is missing (partial cache)', () => { writePayload(testDir, platformKey); writeMarker(testDir, platformKey); - fs.rmSync(join(testDir, 'foundry-local-core', platformKey, requiredCoreLibraryNames()[0])); + fs.rmSync(join(testDir, 'prebuilds', platformKey, 'libonnxruntime-genai.so')); + assert.strictEqual(isRuntimeProvisioned(testDir, platformKey), false); + }); + + test('isRuntimeProvisioned: false when the preload addon is missing (partial cache)', () => { + writePayload(testDir, platformKey); + writeMarker(testDir, platformKey); + fs.rmSync(join(testDir, 'prebuilds', platformKey, 'foundry_local_preload.node')); assert.strictEqual(isRuntimeProvisioned(testDir, platformKey), false); }); @@ -149,21 +154,20 @@ flakySuite('FoundryLocalRuntime', () => { /** * Build a runtime tarball fixture (`.tgz`) whose internal layout - * matches what `provisionRuntime` extracts and verifies. Set `omitCoreLib` - * to leave one required core library out (an incomplete/corrupt payload). + * matches what `provisionRuntime` extracts and verifies. Set + * `omitRuntimeFile` to leave one required file out. */ - async function makeTarball(key: string, opts?: { omitCoreLib?: boolean }): Promise { + async function makeTarball(key: string, opts?: { omitRuntimeFile?: string }): Promise { const src = join(testDir, `src-${key}`); - fs.mkdirSync(join(src, 'prebuilds', key), { recursive: true }); - fs.writeFileSync(join(src, 'prebuilds', key, 'foundry_local_napi.node'), 'addon'); - const coreDir = join(src, 'foundry-local-core', key); - fs.mkdirSync(coreDir, { recursive: true }); - const libs = requiredCoreLibraryNames(); - for (const name of opts?.omitCoreLib ? libs.slice(1) : libs) { - fs.writeFileSync(join(coreDir, name), 'lib'); + const targetDir = join(src, 'prebuilds', key); + fs.mkdirSync(targetDir, { recursive: true }); + for (const name of requiredRuntimeFileNames(key)) { + if (name !== opts?.omitRuntimeFile) { + fs.writeFileSync(join(targetDir, name), 'native'); + } } const tgz = join(testDir, `${key}.tgz`); - await tar.c({ file: tgz, cwd: src, gzip: true }, ['prebuilds', 'foundry-local-core']); + await tar.c({ file: tgz, cwd: src, gzip: true }, ['prebuilds']); return tgz; } @@ -205,9 +209,8 @@ flakySuite('FoundryLocalRuntime', () => { // {target} was substituted with the platform key in the request URL. assert.deepStrictEqual(server.requested, [`${platformKey}.tgz`]); // Payload extracted into the cache layout + completion marker written. - assert.strictEqual(fs.existsSync(join(overrideDir, 'prebuilds', platformKey, 'foundry_local_napi.node')), true); - for (const name of requiredCoreLibraryNames()) { - assert.strictEqual(fs.existsSync(join(overrideDir, 'foundry-local-core', platformKey, name)), true); + for (const name of requiredRuntimeFileNames(platformKey)) { + assert.strictEqual(fs.existsSync(join(overrideDir, 'prebuilds', platformKey, name)), true); } assert.strictEqual(isRuntimeProvisioned(overrideDir, platformKey), true); } finally { @@ -229,8 +232,8 @@ flakySuite('FoundryLocalRuntime', () => { } }); - test('provisionRuntime: rejects an incomplete payload (missing core library) and writes no marker', async () => { - const tgz = await makeTarball(platformKey, { omitCoreLib: true }); + test('provisionRuntime: rejects an incomplete payload and writes no marker', async () => { + const tgz = await makeTarball(platformKey, { omitRuntimeFile: 'foundry_local_preload.node' }); const server = await startServer({ [`${platformKey}.tgz`]: tgz }); try { const overrideDir = join(testDir, '1.2.3'); diff --git a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts index c15ef4b2e181f6..d6f8e5dd7a66c5 100644 --- a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts +++ b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts @@ -60,7 +60,7 @@ export class LocalTranscriptionService { type: 'localTranscription', name: 'local-transcription', // The on-device dictation runtime is downloaded from our CDN and its - // native addon (foundry_local_napi.node) is signed by a third party, + // native addons are signed by a third party, // so on macOS it must load in the plugin helper (library validation // disabled) to avoid a Team ID mismatch dlopen failure. allowLoadingUnsignedLibraries: true From 7dcee43b7843c885053ae344f6fba216a69b6dcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:17:48 +0000 Subject: [PATCH 03/17] Register FoundryLocalManager for disposal via this._register Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .../localTranscription/node/localTranscriptionService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index f9733b3a6db872..612acb20d91170 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -549,12 +549,12 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans // subsequent sessions load without re-downloading ("model // management"). `createAsync` avoids blocking the event loop // during native init. - this._manager = await this._sdk.FoundryLocalManager.createAsync({ + this._manager = this._register(await this._sdk.FoundryLocalManager.createAsync({ appName: FOUNDRY_APP_NAME, modelCacheDir: cacheDir, logLevel: 'warn', ...(nativeLibraryPath ? { libraryPath: nativeLibraryPath } : {}), - }); + })); } const model = await withTimeout( From e6067b6d4496a6b18fbad73a0b0534233eccce43 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:36:49 -0700 Subject: [PATCH 04/17] fix: missing platform filter in excludes --- build/gulpfile.vscode.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index cfa70e11b359ff..9f35769ccf42f9 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -29,7 +29,7 @@ import { checkApiProposalNamesTask, copyCodiconsTask } from './lib/compilation.t import { ensureCopilotPlatformPackage, getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getCopilotRuntimeVersion, getCopilotTgrepExcludeFilter, getMxcExcludeFilter, getRipgrepExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; import { ensureOSProxyResolverPlatformPackage, getOSProxyResolverExcludeFilter, getOSProxyResolverPlatformFiles } from './lib/osProxyResolver.ts'; import { readAgentSdkResults } from './agent-sdk/common.ts'; -import { readDictationRuntimeResults } from './dictation-runtime/common.ts'; +import { getRuntimeTargetForBuild, readDictationRuntimeResults } from './dictation-runtime/common.ts'; import { promisify } from 'util'; import globCallback from 'glob'; import rceditCallback from 'rcedit'; @@ -117,14 +117,18 @@ function computeChecksum(filename: string): string { // foundry-local-sdk (on-device chat dictation) loads two N-API addons from its // package, while configureNativeLoader redirects its shared libraries to the -// per-user runtime cache. Keep the addons but exclude the shared libraries, -// which require a newer glibc than VS Code's minimum supported Linux distros. -function getFoundryLocalExcludeFilter(): string[] { +// per-user runtime cache. Keep the target platform's addons but exclude the +// shared libraries, which require a newer glibc than VS Code's minimum supported +// Linux distros. +function getFoundryLocalExcludeFilter(platform: string, arch: string): string[] { + const target = getRuntimeTargetForBuild(platform, arch); return [ '**', '!**/foundry-local-sdk/prebuilds/**', - '**/foundry-local-sdk/prebuilds/**/foundry_local_node.node', - '**/foundry-local-sdk/prebuilds/**/foundry_local_preload.node', + ...(target ? [ + `**/foundry-local-sdk/prebuilds/${target}/foundry_local_node.node`, + `**/foundry-local-sdk/prebuilds/${target}/foundry_local_preload.node`, + ] : []), ]; } @@ -255,7 +259,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(filter(getCopilotTgrepExcludeFilter(platform, arch))) .pipe(filter(getRipgrepExcludeFilter(platform, arch))) .pipe(filter(getMxcExcludeFilter(arch))) - .pipe(filter(getFoundryLocalExcludeFilter())) + .pipe(filter(getFoundryLocalExcludeFilter(platform, arch))) .pipe(filter(getOSProxyResolverExcludeFilter(platform, arch))) .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) From 2bbc7ca582302d9a25d9844ab3726f5190760c04 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:13:42 -0700 Subject: [PATCH 05/17] Apply cancellation feedback --- .../node/localTranscriptionService.ts | 7 +- .../localTranscriptionService.ts | 83 ++++++++++++++----- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index 612acb20d91170..1679cb467a8ae7 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -35,9 +35,8 @@ const FOUNDRY_APP_NAME = 'vscode-dictation'; * unresolved forever — and since `start`/`pushAudio`/`stop`/`cancel` all await * these calls (directly or via the serialized append chain / stream consumer), * the renderer would wait indefinitely with no error ever surfacing to the - * user. Model *download* already reports progress and is not included here; - * a stalled download is instead caught by its own inactivity timeout inside - * `ensureFoundryLocalRuntime`. + * user. Model download reports progress but exposes no `AbortSignal`; + * cancellation is enforced by terminating this utility process. */ const MODEL_CATALOG_TIMEOUT_MS = 30_000; const MODEL_LOAD_TIMEOUT_MS = 120_000; @@ -244,7 +243,7 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans private _loadedModelId: string | undefined; /** In-flight (or resolved) model download+load for the selected model. */ private _modelPromise: Promise | undefined; - /** Cancellation source for the in-flight model download/load; aborts it when cancelled. */ + /** Cancellation source for in-flight model preparation. */ private _modelPrepareCts: CancellationTokenSource | undefined; /** diff --git a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts index d6f8e5dd7a66c5..52bcff42eadc69 100644 --- a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts +++ b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts @@ -5,11 +5,13 @@ import { getDelayedChannel, IChannel, ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { arch, platform } from '../../../../base/common/process.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { ILocalTranscriptionService, localTranscriptionChannelName } from '../../../../platform/localTranscription/common/localTranscription.js'; -import { IUtilityProcessWorkerWorkbenchService } from '../../utilityProcess/electron-browser/utilityProcessWorkerWorkbenchService.js'; +import { ILocalTranscriptionModelStatus, ILocalTranscriptionResult, ILocalTranscriptionService, localTranscriptionChannelName } from '../../../../platform/localTranscription/common/localTranscription.js'; +import { IUtilityProcessWorker, IUtilityProcessWorkerWorkbenchService } from '../../utilityProcess/electron-browser/utilityProcessWorkerWorkbenchService.js'; /** * Platform/architecture combinations for which the Foundry Local native runtime @@ -37,12 +39,20 @@ function isOnDeviceTranscriptionSupported(): boolean { * onnxruntime-genai). The worker is spun up lazily on first use and torn down * with the window. */ -export class LocalTranscriptionService { +export class LocalTranscriptionService extends Disposable { declare readonly _serviceBrand: undefined; readonly isSupported = isOnDeviceTranscriptionSupported(); + private readonly _onDidChangeModelStatus = this._register(new Emitter()); + readonly onDidChangeModelStatus = this._onDidChangeModelStatus.event; + + private readonly _onDidTranscribe = this._register(new Emitter()); + readonly onDidTranscribe = this._onDidTranscribe.event; + + private readonly _worker = this._register(new MutableDisposable()); + private readonly _workerEvents = this._register(new DisposableStore()); private _channel: IChannel | undefined; private _proxy: ILocalTranscriptionService | undefined; @@ -50,27 +60,45 @@ export class LocalTranscriptionService { @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService private readonly productService: IProductService, - ) { } + ) { + super(); + } private _getChannel(): IChannel { if (!this._channel) { - this._channel = getDelayedChannel((async () => { - const { client } = await this.utilityProcessWorkerWorkbenchService.createWorker({ - moduleId: 'vs/platform/localTranscription/node/localTranscriptionMain', - type: 'localTranscription', - name: 'local-transcription', - // The on-device dictation runtime is downloaded from our CDN and its - // native addons are signed by a third party, - // so on macOS it must load in the plugin helper (library validation - // disabled) to avoid a Team ID mismatch dlopen failure. - allowLoadingUnsignedLibraries: true - }); - return client.getChannel(localTranscriptionChannelName); - })()); + this._channel = getDelayedChannel(this._createWorkerChannel()); } return this._channel; } + private async _createWorkerChannel(): Promise { + const worker = await this.utilityProcessWorkerWorkbenchService.createWorker({ + moduleId: 'vs/platform/localTranscription/node/localTranscriptionMain', + type: 'localTranscription', + name: 'local-transcription', + // The on-device dictation runtime is downloaded from our CDN and its + // native addons are signed by a third party, + // so on macOS it must load in the plugin helper (library validation + // disabled) to avoid a Team ID mismatch dlopen failure. + allowLoadingUnsignedLibraries: true + }); + this._worker.value = worker; + + const channel = worker.client.getChannel(localTranscriptionChannelName); + const proxy = ProxyChannel.toService(channel, { disableMarshalling: true }); + this._workerEvents.clear(); + this._workerEvents.add(proxy.onDidChangeModelStatus(status => this._onDidChangeModelStatus.fire(status))); + this._workerEvents.add(proxy.onDidTranscribe(result => this._onDidTranscribe.fire(result))); + + void worker.onDidTerminate.then(() => { + if (this._worker.value === worker) { + this._resetWorker(); + } + }); + + return channel; + } + private _getProxy(): ILocalTranscriptionService { if (!this._proxy) { this._proxy = ProxyChannel.toService(this._getChannel(), { disableMarshalling: true }); @@ -78,9 +106,6 @@ export class LocalTranscriptionService { return this._proxy; } - get onDidChangeModelStatus() { return this._getProxy().onDidChangeModelStatus; } - get onDidTranscribe() { return this._getProxy().onDidTranscribe; } - getModelStatus() { return this._getProxy().getModelStatus(); } importModel(options: Parameters[0]) { return this._getProxy().importModel(options); } start(options: { cacheDir: string; model?: string; language?: string }) { @@ -100,7 +125,23 @@ export class LocalTranscriptionService { } pushAudio(chunk: Parameters[0]) { return this._getProxy().pushAudio(chunk); } stop() { return this._getProxy().stop(); } - cancel() { return this._getProxy().cancel(); } + async cancel(): Promise { + if (!this._proxy) { + return; + } + try { + await this._proxy.cancel(); + } finally { + this._resetWorker(); + } + } + + private _resetWorker(): void { + this._channel = undefined; + this._proxy = undefined; + this._workerEvents.clear(); + this._worker.clear(); + } /** * Read VS Code's `http.proxy`/`http.noProxy`/`http.proxyStrictSSL`/ From f6868d58f6c9f582921bd94e1e4426a5ddb8f2de Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:56:45 -0700 Subject: [PATCH 06/17] Apply runtime files feedback --- build/dictation-runtime/package.ts | 37 +++++++++---------- .../node/foundryLocalRuntime.ts | 32 ++++++++-------- .../test/node/foundryLocalRuntime.test.ts | 17 ++++++--- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/build/dictation-runtime/package.ts b/build/dictation-runtime/package.ts index 04d97d09b4062c..a85112a385b947 100644 --- a/build/dictation-runtime/package.ts +++ b/build/dictation-runtime/package.ts @@ -5,24 +5,23 @@ /** * Builds one per-target tarball of the Foundry Local native runtime (the - * prebuilt N-API addons + the Foundry Local / onnxruntime / onnxruntime-genai - * shared libraries). Callable as both a Node library (`buildOne(...)`) and a thin + * Foundry Local / onnxruntime / onnxruntime-genai shared libraries). Callable + * as both a Node library (`buildOne(...)`) and a thin * CLI (bottom of this file). * * The library form is what `produce.ts` calls during the per-platform * "Dictation runtime: build + upload" pipeline step; the CLI form is for local * one-off builds and requires `VSS_NUGET_ACCESSTOKEN` for the VS Code NuGet feed. * - * The addons and Foundry Local library are copied from the pinned - * `foundry-local-sdk` package's `prebuilds/` (which ships every target), and the - * ONNX libraries are fetched from NuGet for the requested target's RID via - * `fetchDependencyLibraries`, so ANY build host can produce ANY target's - * tarball. + * The Foundry Local library is copied from the pinned `foundry-local-sdk` + * package's `prebuilds/` (which ships every target), and the ONNX libraries are + * fetched from NuGet for the requested target's RID via + * `fetchDependencyLibraries`, so ANY build host can produce ANY target's tarball. * * The produced tarball's internal layout mirrors the runtime cache layout so the * runtime extraction is a plain untar: * - * prebuilds// + * prebuilds// */ import * as fs from 'fs'; @@ -63,7 +62,7 @@ export async function buildOne(args: IBuildArgs): Promise { try { console.log(`[${SCRIPT}] Building ${SDK_PACKAGE_NAME}@${version} native runtime for ${args.target} in ${stagingDir}`); - await stageSdkNativeFiles(stagingDir, args.target); + await stageSdkSharedLibraries(stagingDir, args.target); await stageDependencyLibraries(stagingDir, args.target); fs.mkdirSync(args.outDir, { recursive: true }); @@ -81,10 +80,10 @@ export async function buildOne(args: IBuildArgs): Promise { } /** - * Copy the native files shipped in the SDK's prebuild directory for `target`. - * This includes both Node-API addons and the Foundry Local shared library. + * Copy the Foundry Local shared library shipped in the SDK's prebuild directory + * for `target`. The Node-API addons are bundled with the product. */ -async function stageSdkNativeFiles(stagingDir: string, target: string): Promise { +async function stageSdkSharedLibraries(stagingDir: string, target: string): Promise { const sourceDir = path.join(SDK_ROOT, 'prebuilds', target); if (!fs.existsSync(sourceDir)) { throw new Error(`[${SCRIPT}] Prebuild directory not found for ${target} at ${sourceDir}. Is ${SDK_PACKAGE_NAME} installed?`); @@ -92,13 +91,13 @@ async function stageSdkNativeFiles(stagingDir: string, target: string): Promise< const targetDir = path.join(stagingDir, 'prebuilds', target); fs.mkdirSync(targetDir, { recursive: true }); for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { - if (entry.isFile() && isNativeFile(entry.name)) { + if (entry.isFile() && isSharedLibrary(entry.name)) { fs.copyFileSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name)); } } - for (const name of requiredSdkNativeFileNames(target)) { + for (const name of requiredSdkSharedLibraryNames(target)) { if (!fs.existsSync(path.join(targetDir, name))) { - throw new Error(`[${SCRIPT}] SDK native file '${name}' not found for ${target} in ${sourceDir}.`); + throw new Error(`[${SCRIPT}] SDK shared library '${name}' not found for ${target} in ${sourceDir}.`); } } } @@ -123,17 +122,17 @@ async function stageDependencyLibraries(stagingDir: string, target: string): Pro } } -function requiredSdkNativeFileNames(target: string): readonly string[] { +function requiredSdkSharedLibraryNames(target: string): readonly string[] { const foundryLocalLibrary = target.startsWith('win32-') ? 'foundry_local.dll' : target.startsWith('darwin-') ? 'libfoundry_local.dylib' : 'libfoundry_local.so'; - return ['foundry_local_node.node', 'foundry_local_preload.node', foundryLocalLibrary]; + return [foundryLocalLibrary]; } -function isNativeFile(name: string): boolean { - return name.endsWith('.node') || name.endsWith('.dll') || name.includes('.dylib') || name.includes('.so'); +function isSharedLibrary(name: string): boolean { + return name.endsWith('.dll') || name.includes('.dylib') || name.includes('.so'); } /** diff --git a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts index 0bc3f2e772c450..f9d9e06c0d546c 100644 --- a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts +++ b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts @@ -18,15 +18,15 @@ import { CancellationError } from '../../../base/common/errors.js'; * and `foundry_local_preload.node`) and native libraries (Foundry Local + ONNX * Runtime + ONNX Runtime GenAI). The shared libraries require a newer glibc than * VS Code's minimum supported Linux distros, so we bundle only the addons with - * the product (see `build/gulpfile.vscode.ts`). We republish the complete - * per-target native directory to VS Code's CDN at build time (see - * `build/dictation-runtime/`) and download it here, at runtime, into a per-user - * writable cache. This keeps the shipped package's glibc floor intact and - * avoids any runtime dependency on the npm registry or NuGet. + * the product (see `build/gulpfile.vscode.ts`). We republish the per-target + * shared libraries to VS Code's CDN at build time (see + * `build/dictation-runtime/`) and download them here, at runtime, into a + * per-user writable cache. This keeps the shipped package's glibc floor intact + * and avoids any runtime dependency on the npm registry or NuGet. * * The tarball's internal layout mirrors the SDK's own package layout: * - * //prebuilds// + * //prebuilds// * * The SDK keeps its addons in the packaged npm module and is configured through * `configureNativeLoader`/`FoundryLocalConfig.libraryPath` to preload the shared @@ -83,10 +83,10 @@ const inFlight = new Map>(); const DOWNLOAD_INACTIVITY_TIMEOUT_MS = 60_000; /** - * Ensure the Foundry Local native runtime (addons + shared libraries) is present - * in ``, downloading the per-target CDN tarball if necessary. Returns - * the concrete cached `prebuilds/` directory to use as the SDK's - * `libraryPath` before constructing a manager. + * Ensure the Foundry Local shared libraries are present in ``, + * downloading the per-target CDN tarball if necessary. Returns the concrete + * cached `prebuilds/` directory to use as the SDK's `libraryPath` + * before constructing a manager. * * Idempotent: once a version is fully provisioned a per-platform `.complete` * marker is written and subsequent calls return immediately (after verifying the @@ -181,16 +181,16 @@ function foundryMarkerPath(overrideDir: string, platformKey: string): string { return join(overrideDir, `.complete-${platformKey}`); } -/** Directory containing the target's addons and shared libraries. */ +/** Directory containing the target's shared libraries. */ function foundryPrebuildDir(overrideDir: string, platformKey: string): string { return join(overrideDir, 'prebuilds', platformKey); } /** * Whether `` holds a complete, verified runtime for `platformKey`: - * the per-platform marker AND all expected addons and shared libraries. A marker - * alone is insufficient (it can belong to a different architecture, or the - * payload can be partially deleted). Exported for tests. + * the per-platform marker AND all expected shared libraries. A marker alone is + * insufficient (it can belong to a different architecture, or the payload can + * be partially deleted). Exported for tests. */ export function isRuntimeProvisioned(overrideDir: string, platformKey: string): boolean { return fs.existsSync(foundryMarkerPath(overrideDir, platformKey)) @@ -260,7 +260,7 @@ function detectGlibcVersion(): [number, number] | undefined { /** * Download the per-target runtime tarball from `url` and extract it into * `stagingDir`, which then contains - * `prebuilds//`. The tarball is published + * `prebuilds//`. The tarball is published * to VS Code's CDN by `build/dictation-runtime/`. */ async function downloadAndExtractTarball(url: string, stagingDir: string, token: CancellationToken): Promise { @@ -286,8 +286,6 @@ export function requiredRuntimeFileNames(platformKey: string): string[] { const ext = isWin ? '.dll' : isDarwin ? '.dylib' : '.so'; const prefix = isWin ? '' : 'lib'; return [ - 'foundry_local_node.node', - 'foundry_local_preload.node', `${prefix}foundry_local${ext}`, isWin ? 'onnxruntime.dll' : isDarwin ? 'libonnxruntime.1.dylib' : 'libonnxruntime.so.1', `${prefix}onnxruntime-genai${ext}`, diff --git a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts index 48eba7332423d2..ff3381c58c9da8 100644 --- a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts +++ b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts @@ -78,11 +78,16 @@ flakySuite('FoundryLocalRuntime', () => { assert.strictEqual(isRuntimeProvisioned(testDir, platformKey), false); }); - test('isRuntimeProvisioned: false when the preload addon is missing (partial cache)', () => { - writePayload(testDir, platformKey); - writeMarker(testDir, platformKey); - fs.rmSync(join(testDir, 'prebuilds', platformKey, 'foundry_local_preload.node')); - assert.strictEqual(isRuntimeProvisioned(testDir, platformKey), false); + test('requiredRuntimeFileNames: includes only shared libraries', () => { + assert.deepStrictEqual({ + linux: requiredRuntimeFileNames('linux-x64'), + darwin: requiredRuntimeFileNames('darwin-arm64'), + win32: requiredRuntimeFileNames('win32-x64'), + }, { + linux: ['libfoundry_local.so', 'libonnxruntime.so.1', 'libonnxruntime-genai.so'], + darwin: ['libfoundry_local.dylib', 'libonnxruntime.1.dylib', 'libonnxruntime-genai.dylib'], + win32: ['foundry_local.dll', 'onnxruntime.dll', 'onnxruntime-genai.dll'], + }); }); test('isRuntimeProvisioned: a different arch marker does not satisfy this arch', () => { @@ -233,7 +238,7 @@ flakySuite('FoundryLocalRuntime', () => { }); test('provisionRuntime: rejects an incomplete payload and writes no marker', async () => { - const tgz = await makeTarball(platformKey, { omitRuntimeFile: 'foundry_local_preload.node' }); + const tgz = await makeTarball(platformKey, { omitRuntimeFile: 'libfoundry_local.so' }); const server = await startServer({ [`${platformKey}.tgz`]: tgz }); try { const overrideDir = join(testDir, '1.2.3'); From 2187f64aa47418a195b9dc0cf32abc8f84e75a08 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:36:49 -0700 Subject: [PATCH 07/17] Apply worker teardown feedback --- .../localTranscriptionService.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts index 52bcff42eadc69..3c758ab0174662 100644 --- a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts +++ b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts @@ -7,6 +7,7 @@ import { getDelayedChannel, IChannel, ProxyChannel } from '../../../../base/part import { arch, platform } from '../../../../base/common/process.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../base/common/errors.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; @@ -55,6 +56,7 @@ export class LocalTranscriptionService extends Disposable { private readonly _workerEvents = this._register(new DisposableStore()); private _channel: IChannel | undefined; private _proxy: ILocalTranscriptionService | undefined; + private _workerGeneration = 0; constructor( @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, @@ -66,12 +68,12 @@ export class LocalTranscriptionService extends Disposable { private _getChannel(): IChannel { if (!this._channel) { - this._channel = getDelayedChannel(this._createWorkerChannel()); + this._channel = getDelayedChannel(this._createWorkerChannel(this._workerGeneration)); } return this._channel; } - private async _createWorkerChannel(): Promise { + private async _createWorkerChannel(generation: number): Promise { const worker = await this.utilityProcessWorkerWorkbenchService.createWorker({ moduleId: 'vs/platform/localTranscription/node/localTranscriptionMain', type: 'localTranscription', @@ -82,6 +84,10 @@ export class LocalTranscriptionService extends Disposable { // disabled) to avoid a Team ID mismatch dlopen failure. allowLoadingUnsignedLibraries: true }); + if (generation !== this._workerGeneration || this._store.isDisposed) { + worker.dispose(); + throw new CancellationError(); + } this._worker.value = worker; const channel = worker.client.getChannel(localTranscriptionChannelName); @@ -126,21 +132,36 @@ export class LocalTranscriptionService extends Disposable { pushAudio(chunk: Parameters[0]) { return this._getProxy().pushAudio(chunk); } stop() { return this._getProxy().stop(); } async cancel(): Promise { - if (!this._proxy) { + const proxy = this._proxy; + if (!proxy) { + return; + } + const worker = this._detachWorker(); + if (!worker) { return; } try { - await this._proxy.cancel(); + await proxy.cancel(); } finally { - this._resetWorker(); + worker.dispose(); } } - private _resetWorker(): void { + private _detachWorker(): IUtilityProcessWorker | undefined { + this._workerGeneration++; this._channel = undefined; this._proxy = undefined; this._workerEvents.clear(); - this._worker.clear(); + return this._worker.clearAndLeak(); + } + + private _resetWorker(): void { + this._detachWorker()?.dispose(); + } + + override dispose(): void { + this._workerGeneration++; + super.dispose(); } /** From c5de65ca8e9f86b03eb773c40b76ee5ecc469fa6 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:37:57 -0700 Subject: [PATCH 08/17] Apply atomic publish feedback --- .../node/foundryLocalRuntime.ts | 98 ++++++++++++++++++- .../test/node/foundryLocalRuntime.test.ts | 23 +++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts index f9d9e06c0d546c..c9acd11f8a8308 100644 --- a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts +++ b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as os from 'os'; +import { timeout } from '../../../base/common/async.js'; import { dirname, join } from '../../../base/common/path.js'; import { format2 } from '../../../base/common/strings.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; @@ -81,6 +82,8 @@ const inFlight = new Map>(); /** Abort a download after this long without any connection/response progress. */ const DOWNLOAD_INACTIVITY_TIMEOUT_MS = 60_000; +const PUBLISH_LOCK_RETRY_MS = 100; +const INVALID_PUBLISH_LOCK_STALE_MS = 30_000; /** * Ensure the Foundry Local shared libraries are present in ``, @@ -160,10 +163,7 @@ export async function provisionRuntime(overrideDir: string, platformKey: string, throw new Error(`Foundry Local native runtime download from ${url} completed but expected files are missing.`); } - if (fs.existsSync(targetDir) && !hasAllRuntimeFiles(targetDir, platformKey)) { - await fs.promises.rm(targetDir, { recursive: true, force: true }); - } - await promoteDir(stagingTarget, targetDir); + await publishRuntime(stagingTarget, overrideDir, platformKey, token); } finally { await fs.promises.rm(staging, { recursive: true, force: true }).catch(() => { /* best effort */ }); } @@ -176,6 +176,96 @@ export async function provisionRuntime(overrideDir: string, platformKey: string, await fs.promises.writeFile(foundryMarkerPath(overrideDir, platformKey), `${version}\n`).catch(() => { /* best effort marker */ }); } +/** Publish a staged runtime without replacing a complete concurrent winner. */ +export async function publishRuntime(stagingTarget: string, overrideDir: string, platformKey: string, token: CancellationToken): Promise { + const lock = await acquireRuntimePublishLock(overrideDir, platformKey, token); + try { + throwIfCancelled(token); + const targetDir = foundryPrebuildDir(overrideDir, platformKey); + if (hasAllRuntimeFiles(targetDir, platformKey)) { + return; + } + await fs.promises.rm(targetDir, { recursive: true, force: true }); + await promoteDir(stagingTarget, targetDir); + } finally { + await releaseRuntimePublishLock(lock); + } +} + +interface IRuntimePublishLock { + readonly path: string; + readonly handle: fs.promises.FileHandle; +} + +async function acquireRuntimePublishLock(overrideDir: string, platformKey: string, token: CancellationToken): Promise { + await fs.promises.mkdir(overrideDir, { recursive: true }); + const lockPath = join(overrideDir, `.publish-${platformKey}.lock`); + while (true) { + throwIfCancelled(token); + try { + const handle = await fs.promises.open(lockPath, 'wx'); + try { + await handle.writeFile(`${process.pid}\n`); + return { path: lockPath, handle }; + } catch (err) { + await handle.close(); + await removeFileIfExists(lockPath); + throw err; + } + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'EEXIST') { + throw err; + } + } + + if (await isStaleRuntimePublishLock(lockPath)) { + await removeFileIfExists(lockPath); + continue; + } + await timeout(PUBLISH_LOCK_RETRY_MS); + } +} + +async function releaseRuntimePublishLock(lock: IRuntimePublishLock): Promise { + await lock.handle.close(); + await removeFileIfExists(lock.path); +} + +async function removeFileIfExists(path: string): Promise { + try { + await fs.promises.unlink(path); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err; + } + } +} + +async function isStaleRuntimePublishLock(lockPath: string): Promise { + try { + const pid = Number.parseInt(await fs.promises.readFile(lockPath, 'utf8'), 10); + if (Number.isSafeInteger(pid) && pid > 0) { + return !isPidAlive(pid); + } + const stat = await fs.promises.stat(lockPath); + return Date.now() - stat.mtimeMs >= INVALID_PUBLISH_LOCK_STALE_MS; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } + throw err; + } +} + +function isPidAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === 'EPERM'; + } +} + /** Path of the per-platform completion marker inside a versioned override dir. */ function foundryMarkerPath(overrideDir: string, platformKey: string): string { return join(overrideDir, `.complete-${platformKey}`); diff --git a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts index ff3381c58c9da8..c5295d745609b4 100644 --- a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts +++ b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts @@ -18,6 +18,7 @@ import { foundryLocalPlatformKey, isFoundryLocalRuntimeSupported, isRuntimeProvisioned, + publishRuntime, promoteDir, provisionRuntime, requiredRuntimeFileNames, @@ -125,6 +126,28 @@ flakySuite('FoundryLocalRuntime', () => { assert.strictEqual(fs.readFileSync(join(to, 'file'), 'utf8'), 'winner'); }); + test('publishRuntime: keeps a complete runtime published by another process', async () => { + const targetDir = join(testDir, 'prebuilds', platformKey); + writePayload(testDir, platformKey); + fs.writeFileSync(join(targetDir, 'winner'), 'winner'); + + const stagingTarget = join(testDir, 'staging', 'prebuilds', platformKey); + fs.mkdirSync(stagingTarget, { recursive: true }); + for (const name of requiredRuntimeFileNames(platformKey)) { + fs.writeFileSync(join(stagingTarget, name), 'loser'); + } + + await publishRuntime(stagingTarget, testDir, platformKey, CancellationToken.None); + + assert.deepStrictEqual({ + winner: fs.readFileSync(join(targetDir, 'winner'), 'utf8'), + stagingStillExists: fs.existsSync(stagingTarget), + }, { + winner: 'winner', + stagingStillExists: true, + }); + }); + test('resolveProxyUrl: honors scheme-specific vars, ALL_PROXY fallback, and NO_PROXY', () => { const actual = { none: resolveProxyUrl('https://api.nuget.org/', {}), From 87e26ea149340af365562e647aeca074da2e7859 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:13:37 -0700 Subject: [PATCH 09/17] WIP --- build/darwin/create-universal-app.ts | 2 +- build/linux/debian/dep-lists.ts | 3 +++ build/linux/rpm/dep-lists.ts | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 0aad2a24cfef47..c905147dba1331 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -145,7 +145,7 @@ async function main(buildDir?: string) { // them as arch-unique. Paths here are ASAR-internal (top level, no `node_modules` // prefix). Over-covering is harmless: the allowlist is only consulted for files // that are actually unique to one arch. - singleArchFiles: '{**/@github/copilot-darwin-*,**/@github/copilot-darwin-*/**,**/@github/copilot-sdk-darwin-*,**/@github/copilot-sdk-darwin-*/**,**/@github/copilot/prebuilds/darwin-*,**/@github/copilot/prebuilds/darwin-*/**,**/@github/copilot/tgrep/bin/darwin-*,**/@github/copilot/tgrep/bin/darwin-*/**,**/@github/copilot/sdk/tgrep/bin/darwin-*,**/@github/copilot/sdk/tgrep/bin/darwin-*/**,**/@github/copilot/sdk/prebuilds/darwin-*,**/@github/copilot/sdk/prebuilds/darwin-*/**,**/@github/copilot/sdk/ripgrep/bin/darwin-*,**/@github/copilot/sdk/ripgrep/bin/darwin-*/**,**/@vscode/ripgrep-universal/bin/darwin-*,**/@vscode/ripgrep-universal/bin/darwin-*/**,**/@vscode/os-proxy-resolver-darwin-*,**/@vscode/os-proxy-resolver-darwin-*/**,**/@microsoft/mxc-sdk/bin/*,**/@microsoft/mxc-sdk/bin/*/**}', + singleArchFiles: '{**/@github/copilot-darwin-*,**/@github/copilot-darwin-*/**,**/@github/copilot-sdk-darwin-*,**/@github/copilot-sdk-darwin-*/**,**/@github/copilot/prebuilds/darwin-*,**/@github/copilot/prebuilds/darwin-*/**,**/@github/copilot/tgrep/bin/darwin-*,**/@github/copilot/tgrep/bin/darwin-*/**,**/@github/copilot/sdk/tgrep/bin/darwin-*,**/@github/copilot/sdk/tgrep/bin/darwin-*/**,**/@github/copilot/sdk/prebuilds/darwin-*,**/@github/copilot/sdk/prebuilds/darwin-*/**,**/@github/copilot/sdk/ripgrep/bin/darwin-*,**/@github/copilot/sdk/ripgrep/bin/darwin-*/**,**/@vscode/ripgrep-universal/bin/darwin-*,**/@vscode/ripgrep-universal/bin/darwin-*/**,**/@vscode/os-proxy-resolver-darwin-*,**/@vscode/os-proxy-resolver-darwin-*/**,**/@microsoft/mxc-sdk/bin/*,**/@microsoft/mxc-sdk/bin/*/**,**/foundry-local-sdk/prebuilds,**/foundry-local-sdk/prebuilds/darwin-arm64,**/foundry-local-sdk/prebuilds/darwin-arm64/**}', x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/**,**/node_modules/@github/copilot-sdk-darwin-*/**,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot-darwin-*/**,**/node_modules.asar.unpacked/@github/copilot-sdk-darwin-*/**,**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/prebuilds/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/sdk/tgrep/bin/darwin-*/*,**/extensions/copilot/node_modules/@github/copilot/tgrep/bin/darwin-*/*,**/node_modules/@vscode/ripgrep-universal/bin/darwin-*/*,**/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/darwin-*/*,**/node_modules/@vscode/os-proxy-resolver-darwin-*/**,**/node_modules.asar.unpacked/@vscode/os-proxy-resolver-darwin-*/**,**/node_modules/@microsoft/mxc-sdk/bin/**,**/node_modules.asar.unpacked/@microsoft/mxc-sdk/bin/**}', filesToSkipComparison: (file: string) => { for (const expected of filesToSkip) { diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index eb8b42624b2852..9aafe657066aa0 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -29,6 +29,7 @@ export const referenceGeneratedDepsByArch = { 'libatk-bridge2.0-0 (>= 2.5.3)', 'libatk1.0-0 (>= 2.11.90)', 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.14)', 'libc6 (>= 2.15)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', @@ -50,6 +51,8 @@ export const referenceGeneratedDepsByArch = { 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', 'libpango-1.0-0 (>= 1.14.0)', + 'libstdc++6 (>= 5)', + 'libstdc++6 (>= 9)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 34958017f0970d..5462330095a5a8 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -256,6 +256,8 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', + 'libc.so.6(GLIBC_2.32)(64bit)', + 'libc.so.6(GLIBC_2.34)(64bit)', 'libcairo.so.2()(64bit)', 'libcups.so.2()(64bit)', 'libcurl.so.4()(64bit)', @@ -264,6 +266,7 @@ export const referenceGeneratedDepsByArch = { 'libdl.so.2()(64bit)', 'libdl.so.2(GLIBC_2.17)(64bit)', 'libexpat.so.1()(64bit)', + 'libfoundry_local.so()(64bit)', 'libgbm.so.1()(64bit)', 'libgcc_s.so.1()(64bit)', 'libgcc_s.so.1(GCC_3.0)(64bit)', @@ -315,6 +318,7 @@ export const referenceGeneratedDepsByArch = { 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.26)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.31)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', 'libudev.so.1()(64bit)', @@ -326,7 +330,6 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', - 'rtld(GNU_HASH)', - 'xdg-utils' + 'rtld(GNU_HASH)' ] }; From ef2c03ba9b094984e6800a8c9ce09bda4800b79b Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:56:37 -0700 Subject: [PATCH 10/17] Shift all files to cache --- .../common/dictation-runtime-produce.yml | 2 +- build/dictation-runtime/package.ts | 40 ++++++++++--------- build/gulpfile.vscode.ts | 19 +++------ build/npm/postinstall.ts | 34 ++++++++++++++++ .../node/foundryLocalRuntime.ts | 23 ++++++----- .../node/localTranscriptionService.ts | 12 +++--- .../test/node/foundryLocalRuntime.test.ts | 8 ++-- 7 files changed, 85 insertions(+), 53 deletions(-) diff --git a/build/azure-pipelines/common/dictation-runtime-produce.yml b/build/azure-pipelines/common/dictation-runtime-produce.yml index acd3d1d4d45b73..2b91af532a1baf 100644 --- a/build/azure-pipelines/common/dictation-runtime-produce.yml +++ b/build/azure-pipelines/common/dictation-runtime-produce.yml @@ -18,7 +18,7 @@ parameters: # # The ONNX libraries are fetched from NuGet for the target's explicit RID (see # build/dictation-runtime/nuget.ts), so ARM64 targets build fine on x64 pools; -# both addons and the Foundry Local library are copied from the already-restored +# the addons and Foundry Local library are copied from the already-restored # node_modules. No npm registry auth is needed. # # Steps: diff --git a/build/dictation-runtime/package.ts b/build/dictation-runtime/package.ts index a85112a385b947..92991a79b5783f 100644 --- a/build/dictation-runtime/package.ts +++ b/build/dictation-runtime/package.ts @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ /** - * Builds one per-target tarball of the Foundry Local native runtime (the - * Foundry Local / onnxruntime / onnxruntime-genai shared libraries). Callable - * as both a Node library (`buildOne(...)`) and a thin + * Builds one per-target tarball of the Foundry Local native runtime (the N-API + * addons and Foundry Local / onnxruntime / onnxruntime-genai shared libraries). + * Callable as both a Node library (`buildOne(...)`) and a thin * CLI (bottom of this file). * * The library form is what `produce.ts` calls during the per-platform * "Dictation runtime: build + upload" pipeline step; the CLI form is for local * one-off builds and requires `VSS_NUGET_ACCESSTOKEN` for the VS Code NuGet feed. * - * The Foundry Local library is copied from the pinned `foundry-local-sdk` - * package's `prebuilds/` (which ships every target), and the ONNX libraries are - * fetched from NuGet for the requested target's RID via + * The Foundry Local addons and library are copied from the pinned + * `foundry-local-sdk` package's `prebuilds/` (which ships every target), and + * the ONNX libraries are fetched from NuGet for the requested target's RID via * `fetchDependencyLibraries`, so ANY build host can produce ANY target's tarball. * * The produced tarball's internal layout mirrors the runtime cache layout so the * runtime extraction is a plain untar: * - * prebuilds// + * prebuilds// */ import * as fs from 'fs'; @@ -62,7 +62,7 @@ export async function buildOne(args: IBuildArgs): Promise { try { console.log(`[${SCRIPT}] Building ${SDK_PACKAGE_NAME}@${version} native runtime for ${args.target} in ${stagingDir}`); - await stageSdkSharedLibraries(stagingDir, args.target); + await stageSdkNativeFiles(stagingDir, args.target); await stageDependencyLibraries(stagingDir, args.target); fs.mkdirSync(args.outDir, { recursive: true }); @@ -80,10 +80,10 @@ export async function buildOne(args: IBuildArgs): Promise { } /** - * Copy the Foundry Local shared library shipped in the SDK's prebuild directory - * for `target`. The Node-API addons are bundled with the product. + * Copy the Foundry Local addons and shared libraries shipped in the SDK's + * prebuild directory for `target`. */ -async function stageSdkSharedLibraries(stagingDir: string, target: string): Promise { +async function stageSdkNativeFiles(stagingDir: string, target: string): Promise { const sourceDir = path.join(SDK_ROOT, 'prebuilds', target); if (!fs.existsSync(sourceDir)) { throw new Error(`[${SCRIPT}] Prebuild directory not found for ${target} at ${sourceDir}. Is ${SDK_PACKAGE_NAME} installed?`); @@ -91,13 +91,13 @@ async function stageSdkSharedLibraries(stagingDir: string, target: string): Prom const targetDir = path.join(stagingDir, 'prebuilds', target); fs.mkdirSync(targetDir, { recursive: true }); for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { - if (entry.isFile() && isSharedLibrary(entry.name)) { + if (entry.isFile() && isNativeRuntimeFile(entry.name)) { fs.copyFileSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name)); } } - for (const name of requiredSdkSharedLibraryNames(target)) { + for (const name of requiredSdkNativeFileNames(target)) { if (!fs.existsSync(path.join(targetDir, name))) { - throw new Error(`[${SCRIPT}] SDK shared library '${name}' not found for ${target} in ${sourceDir}.`); + throw new Error(`[${SCRIPT}] SDK native file '${name}' not found for ${target} in ${sourceDir}.`); } } } @@ -122,17 +122,21 @@ async function stageDependencyLibraries(stagingDir: string, target: string): Pro } } -function requiredSdkSharedLibraryNames(target: string): readonly string[] { +function requiredSdkNativeFileNames(target: string): readonly string[] { const foundryLocalLibrary = target.startsWith('win32-') ? 'foundry_local.dll' : target.startsWith('darwin-') ? 'libfoundry_local.dylib' : 'libfoundry_local.so'; - return [foundryLocalLibrary]; + return [ + 'foundry_local_node.node', + 'foundry_local_preload.node', + foundryLocalLibrary, + ]; } -function isSharedLibrary(name: string): boolean { - return name.endsWith('.dll') || name.includes('.dylib') || name.includes('.so'); +function isNativeRuntimeFile(name: string): boolean { + return name.endsWith('.node') || name.endsWith('.dll') || name.includes('.dylib') || name.includes('.so'); } /** diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 9f35769ccf42f9..03c4f880f47db5 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -29,7 +29,7 @@ import { checkApiProposalNamesTask, copyCodiconsTask } from './lib/compilation.t import { ensureCopilotPlatformPackage, getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getCopilotRuntimeVersion, getCopilotTgrepExcludeFilter, getMxcExcludeFilter, getRipgrepExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; import { ensureOSProxyResolverPlatformPackage, getOSProxyResolverExcludeFilter, getOSProxyResolverPlatformFiles } from './lib/osProxyResolver.ts'; import { readAgentSdkResults } from './agent-sdk/common.ts'; -import { getRuntimeTargetForBuild, readDictationRuntimeResults } from './dictation-runtime/common.ts'; +import { readDictationRuntimeResults } from './dictation-runtime/common.ts'; import { promisify } from 'util'; import globCallback from 'glob'; import rceditCallback from 'rcedit'; @@ -115,20 +115,13 @@ function computeChecksum(filename: string): string { return hash; } -// foundry-local-sdk (on-device chat dictation) loads two N-API addons from its -// package, while configureNativeLoader redirects its shared libraries to the -// per-user runtime cache. Keep the target platform's addons but exclude the -// shared libraries, which require a newer glibc than VS Code's minimum supported -// Linux distros. -function getFoundryLocalExcludeFilter(platform: string, arch: string): string[] { - const target = getRuntimeTargetForBuild(platform, arch); +// foundry-local-sdk (on-device chat dictation) loads its N-API addons and shared +// libraries from the per-user runtime cache. Exclude the package's native files, +// which may require a newer glibc than VS Code's supported maximum. +function getFoundryLocalExcludeFilter(): string[] { return [ '**', '!**/foundry-local-sdk/prebuilds/**', - ...(target ? [ - `**/foundry-local-sdk/prebuilds/${target}/foundry_local_node.node`, - `**/foundry-local-sdk/prebuilds/${target}/foundry_local_preload.node`, - ] : []), ]; } @@ -259,7 +252,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(filter(getCopilotTgrepExcludeFilter(platform, arch))) .pipe(filter(getRipgrepExcludeFilter(platform, arch))) .pipe(filter(getMxcExcludeFilter(arch))) - .pipe(filter(getFoundryLocalExcludeFilter(platform, arch))) + .pipe(filter(getFoundryLocalExcludeFilter())) .pipe(filter(getOSProxyResolverExcludeFilter(platform, arch))) .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index bd22fc3faf9c28..cae349bf71a6df 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -338,6 +338,40 @@ async function main() { log('.', `Created ${claudeSkillsLinkType} .claude/skills -> .agents/skills`); } + // foundry-local-sdk's libraryPath redirects its shared libraries but not its + // two N-API addons. Packaged builds provision all native files together, so + // patch the lazy loader to resolve the addons from libraryPath as well. + for (const dir of ['', 'remote']) { + const nativeLoaderFile = path.join(root, dir, 'node_modules', 'foundry-local-sdk', 'dist', 'detail', 'native.js'); + if (!fs.existsSync(nativeLoaderFile)) { + continue; + } + const content = fs.readFileSync(nativeLoaderFile, 'utf8'); + const marker = '// VSCODE_PATCH:foundry-addons-from-library-path'; + if (content.includes(marker)) { + continue; + } + const replacements: readonly [string, string][] = [ + [ + `const addonPath = resolve(prebuildDir, "foundry_local_node.node");\nconst preloadAddonPath = resolve(prebuildDir, "foundry_local_preload.node");`, + `${marker}\nlet addonPath = resolve(prebuildDir, "foundry_local_node.node");\nlet preloadAddonPath = resolve(prebuildDir, "foundry_local_preload.node");`, + ], + [ + `if (!existsSync(fullPath)) {\n throw new Error(\`libraryPath does not contain \${expected}: \${libraryPath}\`);\n }`, + `if (!existsSync(fullPath)) {\n throw new Error(\`libraryPath does not contain \${expected}: \${libraryPath}\`);\n }\n const configuredAddonPath = resolve(libraryPath, "foundry_local_node.node");\n const configuredPreloadAddonPath = resolve(libraryPath, "foundry_local_preload.node");\n if (!existsSync(configuredAddonPath) || !existsSync(configuredPreloadAddonPath)) {\n throw new Error(\`libraryPath does not contain both Foundry Local addons: \${libraryPath}\`);\n }\n addonPath = configuredAddonPath;\n preloadAddonPath = configuredPreloadAddonPath;`, + ], + ]; + let patched = content; + for (const [needle, replacement] of replacements) { + if (!patched.includes(needle)) { + throw new Error(`Unexpected foundry-local-sdk native loader shape in ${nativeLoaderFile}`); + } + patched = patched.replace(needle, replacement); + } + fs.writeFileSync(nativeLoaderFile, patched); + log(dir || '.', 'Patched foundry-local-sdk native loader (addons from libraryPath)'); + } + // Temporary: patch @github/copilot-sdk session.js to fix ESM import // (missing .js extension on vscode-jsonrpc/node). Fixed upstream in v0.1.32. // TODO: Remove once @github/copilot-sdk is updated to >=0.1.32 diff --git a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts index c9acd11f8a8308..73825f82f86008 100644 --- a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts +++ b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts @@ -17,21 +17,20 @@ import { CancellationError } from '../../../base/common/errors.js'; * * `foundry-local-sdk` ships two prebuilt N-API addons (`foundry_local_node.node` * and `foundry_local_preload.node`) and native libraries (Foundry Local + ONNX - * Runtime + ONNX Runtime GenAI). The shared libraries require a newer glibc than - * VS Code's minimum supported Linux distros, so we bundle only the addons with - * the product (see `build/gulpfile.vscode.ts`). We republish the per-target - * shared libraries to VS Code's CDN at build time (see - * `build/dictation-runtime/`) and download them here, at runtime, into a - * per-user writable cache. This keeps the shipped package's glibc floor intact - * and avoids any runtime dependency on the npm registry or NuGet. + * Runtime + ONNX Runtime GenAI). These native files may require a newer glibc + * than VS Code supports, so we republish the per-target payload to VS Code's CDN + * at build time (see `build/dictation-runtime/`) and download it here, at + * runtime, into a per-user writable cache. This keeps the shipped package's + * glibc floor intact and avoids any runtime dependency on the npm registry or + * NuGet. * * The tarball's internal layout mirrors the SDK's own package layout: * * //prebuilds// * - * The SDK keeps its addons in the packaged npm module and is configured through - * `configureNativeLoader`/`FoundryLocalConfig.libraryPath` to preload the shared - * libraries from this cache directory. + * The SDK loader is patched at install time so + * `configureNativeLoader`/`FoundryLocalConfig.libraryPath` loads both addons and + * shared libraries from this cache directory. * * NOTE: the single CDN download leg honors the standard proxy environment * variables (`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY`, with `NO_PROXY`). VS Code's @@ -350,7 +349,7 @@ function detectGlibcVersion(): [number, number] | undefined { /** * Download the per-target runtime tarball from `url` and extract it into * `stagingDir`, which then contains - * `prebuilds//`. The tarball is published + * `prebuilds//`. The tarball is published * to VS Code's CDN by `build/dictation-runtime/`. */ async function downloadAndExtractTarball(url: string, stagingDir: string, token: CancellationToken): Promise { @@ -376,6 +375,8 @@ export function requiredRuntimeFileNames(platformKey: string): string[] { const ext = isWin ? '.dll' : isDarwin ? '.dylib' : '.so'; const prefix = isWin ? '' : 'lib'; return [ + 'foundry_local_node.node', + 'foundry_local_preload.node', `${prefix}foundry_local${ext}`, isWin ? 'onnxruntime.dll' : isDarwin ? 'libonnxruntime.1.dylib' : 'libonnxruntime.so.1', `${prefix}onnxruntime-genai${ext}`, diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index 1679cb467a8ae7..ada2e51e7b73c4 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -525,13 +525,13 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans // The model cache state is unknown until the catalog is queried. this._setStatus({ state: LocalTranscriptionModelState.Loading }); - // Ensure the Foundry Local shared libraries are available before - // loading the SDK. Packaged builds keep the two addons but download - // the shared libraries on demand from VS Code's CDN into a per-user - // cache and point the SDK loader at it via libraryPath. + // Ensure the Foundry Local native files are available before + // loading the SDK. Packaged builds download the addons and shared + // libraries on demand from VS Code's CDN into a per-user cache and + // point the SDK loader at it via libraryPath. // This is a no-op once cached. In dev builds (no product config) - // the SDK resolves its addons + shared libraries from node_modules, so we - // skip provisioning and leave the loader on its default path. + // the SDK resolves its native files from node_modules, so we skip + // provisioning and leave the loader on its default path. let nativeLibraryPath: string | undefined; if (this._runtimeDownload) { nativeLibraryPath = await ensureFoundryLocalRuntime(runtimeCacheDir(cacheDir), this._runtimeDownload, cts.token); diff --git a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts index c5295d745609b4..2d895b08834f67 100644 --- a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts +++ b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts @@ -79,15 +79,15 @@ flakySuite('FoundryLocalRuntime', () => { assert.strictEqual(isRuntimeProvisioned(testDir, platformKey), false); }); - test('requiredRuntimeFileNames: includes only shared libraries', () => { + test('requiredRuntimeFileNames: includes addons and shared libraries', () => { assert.deepStrictEqual({ linux: requiredRuntimeFileNames('linux-x64'), darwin: requiredRuntimeFileNames('darwin-arm64'), win32: requiredRuntimeFileNames('win32-x64'), }, { - linux: ['libfoundry_local.so', 'libonnxruntime.so.1', 'libonnxruntime-genai.so'], - darwin: ['libfoundry_local.dylib', 'libonnxruntime.1.dylib', 'libonnxruntime-genai.dylib'], - win32: ['foundry_local.dll', 'onnxruntime.dll', 'onnxruntime-genai.dll'], + linux: ['foundry_local_node.node', 'foundry_local_preload.node', 'libfoundry_local.so', 'libonnxruntime.so.1', 'libonnxruntime-genai.so'], + darwin: ['foundry_local_node.node', 'foundry_local_preload.node', 'libfoundry_local.dylib', 'libonnxruntime.1.dylib', 'libonnxruntime-genai.dylib'], + win32: ['foundry_local_node.node', 'foundry_local_preload.node', 'foundry_local.dll', 'onnxruntime.dll', 'onnxruntime-genai.dll'], }); }); From 39aab943e4a4daa4424a096920600fa62c89e0e9 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:20:12 -0700 Subject: [PATCH 11/17] Apply local model review feedback --- build/dictation-runtime/common.ts | 14 +++++++------- build/dictation-runtime/nuget.ts | 4 ---- build/dictation-runtime/package.ts | 4 +++- build/npm/postinstall.ts | 15 --------------- .../node/foundryLocalRuntime.ts | 5 +++-- .../node/localTranscriptionService.ts | 11 ++++------- .../test/node/foundryLocalRuntime.test.ts | 2 +- .../electron-browser/localTranscriptionService.ts | 9 ++++----- 8 files changed, 22 insertions(+), 42 deletions(-) diff --git a/build/dictation-runtime/common.ts b/build/dictation-runtime/common.ts index 36ed2955064e4e..86719d70e511c3 100644 --- a/build/dictation-runtime/common.ts +++ b/build/dictation-runtime/common.ts @@ -132,13 +132,13 @@ export function getRuntimeTargetForBuild(vscodePlatform: string, arch: string): * 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 keeps the SDK's addons but strips - * its shared libraries (`getFoundryLocalExcludeFilter` in - * `gulpfile.vscode.ts`), so a packaged build with no stamp would have no - * loadable runtime. Stamping regardless of `VSCODE_PUBLISH` gives the - * packaged app a usable CDN source; the payload for that version is uploaded - * (idempotently) by publish runs. Local dev-from-source (which never runs - * `produce.ts`) uses the complete `node_modules` payload. + * - non-publish product builds: packaging strips the SDK's native payload + * (`getFoundryLocalExcludeFilter` in `gulpfile.vscode.ts`), so a packaged + * build with no stamp would have no loadable runtime. Stamping regardless + * of `VSCODE_PUBLISH` gives the packaged app a usable CDN source; the payload + * for that version is uploaded (idempotently) by publish runs. Local + * dev-from-source (which never runs `produce.ts`) uses the complete + * `node_modules` payload. * * Returns `false` for platforms/arches that can never host dictation (armhf, * Alpine/musl, web) so their `product.json` stays clean. diff --git a/build/dictation-runtime/nuget.ts b/build/dictation-runtime/nuget.ts index 01af174a434b49..e1e8071d619994 100644 --- a/build/dictation-runtime/nuget.ts +++ b/build/dictation-runtime/nuget.ts @@ -97,10 +97,6 @@ export interface IFetchDependencyLibrariesOptions { readonly skipIfPresent?: boolean; } -export function supportsDependencyLibraryTarget(target: string): boolean { - return Object.hasOwn(RID_BY_TARGET, target); -} - export function getStandardArtifacts(dependencies: IFoundryDependencyVersions): readonly INugetArtifact[] { return [ { name: 'Microsoft.ML.OnnxRuntime', version: dependencies.onnxruntime.version }, diff --git a/build/dictation-runtime/package.ts b/build/dictation-runtime/package.ts index 92991a79b5783f..f03a2053431e53 100644 --- a/build/dictation-runtime/package.ts +++ b/build/dictation-runtime/package.ts @@ -123,7 +123,8 @@ async function stageDependencyLibraries(stagingDir: string, target: string): Pro } function requiredSdkNativeFileNames(target: string): readonly string[] { - const foundryLocalLibrary = target.startsWith('win32-') + const isWindows = target.startsWith('win32-'); + const foundryLocalLibrary = isWindows ? 'foundry_local.dll' : target.startsWith('darwin-') ? 'libfoundry_local.dylib' @@ -132,6 +133,7 @@ function requiredSdkNativeFileNames(target: string): readonly string[] { 'foundry_local_node.node', 'foundry_local_preload.node', foundryLocalLibrary, + ...(isWindows ? ['Microsoft.Windows.AI.MachineLearning.dll'] : []), ]; } diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index cae349bf71a6df..8d24db7d43d6ed 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -371,21 +371,6 @@ async function main() { fs.writeFileSync(nativeLoaderFile, patched); log(dir || '.', 'Patched foundry-local-sdk native loader (addons from libraryPath)'); } - - // Temporary: patch @github/copilot-sdk session.js to fix ESM import - // (missing .js extension on vscode-jsonrpc/node). Fixed upstream in v0.1.32. - // TODO: Remove once @github/copilot-sdk is updated to >=0.1.32 - for (const dir of ['', 'remote']) { - const sessionFile = path.join(root, dir, 'node_modules', '@github', 'copilot-sdk', 'dist', 'session.js'); - if (fs.existsSync(sessionFile)) { - const content = fs.readFileSync(sessionFile, 'utf8'); - const patched = content.replace(/from "vscode-jsonrpc\/node"/g, 'from "vscode-jsonrpc/node.js"'); - if (content !== patched) { - fs.writeFileSync(sessionFile, patched); - log(dir || '.', 'Patched @github/copilot-sdk session.js (vscode-jsonrpc ESM import fix)'); - } - } - } } main().catch(err => { diff --git a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts index 73825f82f86008..dcc727726fa405 100644 --- a/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts +++ b/src/vs/platform/localTranscription/node/foundryLocalRuntime.ts @@ -26,7 +26,7 @@ import { CancellationError } from '../../../base/common/errors.js'; * * The tarball's internal layout mirrors the SDK's own package layout: * - * //prebuilds// + * //prebuilds// * * The SDK loader is patched at install time so * `configureNativeLoader`/`FoundryLocalConfig.libraryPath` loads both addons and @@ -85,7 +85,7 @@ const PUBLISH_LOCK_RETRY_MS = 100; const INVALID_PUBLISH_LOCK_STALE_MS = 30_000; /** - * Ensure the Foundry Local shared libraries are present in ``, + * Ensure the Foundry Local native files are present in ``, * downloading the per-target CDN tarball if necessary. Returns the concrete * cached `prebuilds/` directory to use as the SDK's `libraryPath` * before constructing a manager. @@ -378,6 +378,7 @@ export function requiredRuntimeFileNames(platformKey: string): string[] { 'foundry_local_node.node', 'foundry_local_preload.node', `${prefix}foundry_local${ext}`, + ...(isWin ? ['Microsoft.Windows.AI.MachineLearning.dll'] : []), isWin ? 'onnxruntime.dll' : isDarwin ? 'libonnxruntime.1.dylib' : 'libonnxruntime.so.1', `${prefix}onnxruntime-genai${ext}`, ]; diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index ada2e51e7b73c4..df1b5e02d48e6f 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -61,10 +61,10 @@ function withTimeout(promise: Promise, ms: number, message: string): Promi } /** - * Directory holding the on-demand Foundry Local native runtime (addon + core - * libraries). Derived as a sibling of the model cache dir so both live under VS - * Code's cache home; kept separate from model files since it is versioned by SDK - * version and provisioned independently. + * Directory holding the on-demand Foundry Local addons and shared libraries. + * Derived as a sibling of the model cache dir so both live under VS Code's cache + * home; kept separate from model files since it is versioned by SDK version and + * provisioned independently. */ function runtimeCacheDir(modelCacheDir: string): string { return join(dirname(modelCacheDir), 'chatDictationRuntime'); @@ -541,9 +541,6 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans this._sdk = await import('foundry-local-sdk'); } if (!this._manager) { - if (nativeLibraryPath) { - this._sdk.configureNativeLoader({ libraryPath: nativeLibraryPath }); - } // Store downloaded model files under VS Code's cache dir so // subsequent sessions load without re-downloading ("model // management"). `createAsync` avoids blocking the event loop diff --git a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts index 2d895b08834f67..1b33d383a8551e 100644 --- a/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts +++ b/src/vs/platform/localTranscription/test/node/foundryLocalRuntime.test.ts @@ -87,7 +87,7 @@ flakySuite('FoundryLocalRuntime', () => { }, { linux: ['foundry_local_node.node', 'foundry_local_preload.node', 'libfoundry_local.so', 'libonnxruntime.so.1', 'libonnxruntime-genai.so'], darwin: ['foundry_local_node.node', 'foundry_local_preload.node', 'libfoundry_local.dylib', 'libonnxruntime.1.dylib', 'libonnxruntime-genai.dylib'], - win32: ['foundry_local_node.node', 'foundry_local_preload.node', 'foundry_local.dll', 'onnxruntime.dll', 'onnxruntime-genai.dll'], + win32: ['foundry_local_node.node', 'foundry_local_preload.node', 'foundry_local.dll', 'Microsoft.Windows.AI.MachineLearning.dll', 'onnxruntime.dll', 'onnxruntime-genai.dll'], }); }); diff --git a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts index 3c758ab0174662..d3eadfd81b0e50 100644 --- a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts +++ b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts @@ -16,11 +16,10 @@ import { IUtilityProcessWorker, IUtilityProcessWorkerWorkbenchService } from '.. /** * Platform/architecture combinations for which the Foundry Local native runtime - * ships a prebuilt addon and core libraries, and packaging keeps them (see the - * Foundry Local bundling in build/gulpfile.vscode.ts). On anything else (e.g. - * darwin/x64, linux/armhf) the native runtime is absent, so on-device - * transcription cannot run and the feature must report itself unsupported rather - * than showing a mic that fails on use. + * is published to the CDN. On anything else (e.g. darwin/x64, linux/armhf) the + * native runtime is unavailable, so on-device transcription cannot run and the + * feature must report itself unsupported rather than showing a mic that fails + * on use. */ const SUPPORTED_TARGETS = new Set([ 'darwin-arm64', From c0ac479fcf3456cae40d7dce3b113afd3457f2ba Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:27:27 -0700 Subject: [PATCH 12/17] Undo dep bumps --- build/linux/debian/dep-lists.ts | 3 --- build/linux/rpm/dep-lists.ts | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 9aafe657066aa0..eb8b42624b2852 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -29,7 +29,6 @@ export const referenceGeneratedDepsByArch = { 'libatk-bridge2.0-0 (>= 2.5.3)', 'libatk1.0-0 (>= 2.11.90)', 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.14)', 'libc6 (>= 2.15)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', @@ -51,8 +50,6 @@ export const referenceGeneratedDepsByArch = { 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 9)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 5462330095a5a8..34958017f0970d 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -256,8 +256,6 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', - 'libc.so.6(GLIBC_2.32)(64bit)', - 'libc.so.6(GLIBC_2.34)(64bit)', 'libcairo.so.2()(64bit)', 'libcups.so.2()(64bit)', 'libcurl.so.4()(64bit)', @@ -266,7 +264,6 @@ export const referenceGeneratedDepsByArch = { 'libdl.so.2()(64bit)', 'libdl.so.2(GLIBC_2.17)(64bit)', 'libexpat.so.1()(64bit)', - 'libfoundry_local.so()(64bit)', 'libgbm.so.1()(64bit)', 'libgcc_s.so.1()(64bit)', 'libgcc_s.so.1(GCC_3.0)(64bit)', @@ -318,7 +315,6 @@ export const referenceGeneratedDepsByArch = { 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.26)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.31)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', 'libudev.so.1()(64bit)', @@ -330,6 +326,7 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', - 'rtld(GNU_HASH)' + 'rtld(GNU_HASH)', + 'xdg-utils' ] }; From c21261631a7e5e07bf7f2700e81d1c6950922c51 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:38:21 -0700 Subject: [PATCH 13/17] Apply redundant library download feedback --- build/dictation-runtime/nuget.ts | 39 +++++++++++++++---------- build/lib/test/dictationRuntime.test.ts | 17 +++++++++++ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/build/dictation-runtime/nuget.ts b/build/dictation-runtime/nuget.ts index e1e8071d619994..f4a9fbad1e49cb 100644 --- a/build/dictation-runtime/nuget.ts +++ b/build/dictation-runtime/nuget.ts @@ -105,19 +105,28 @@ export function getStandardArtifacts(dependencies: IFoundryDependencyVersions): } export function requiredDependencyLibraryNames(target: string, dependencies: IFoundryDependencyVersions): readonly string[] { + return [ + onnxRuntimeLibraryName(target, dependencies.onnxruntime.version), + onnxRuntimeGenAiLibraryName(target), + ]; +} + +function onnxRuntimeLibraryName(target: string, version: string): string { const isWin = target.startsWith('win32-'); const isDarwin = target.startsWith('darwin-'); - const ext = isWin ? '.dll' : isDarwin ? '.dylib' : '.so'; - const prefix = isWin ? '' : 'lib'; - const onnxRuntime = isWin + return isWin ? 'onnxruntime.dll' : isDarwin - ? `libonnxruntime.${dependencies.onnxruntime.version.split('.')[0]}.dylib` + ? `libonnxruntime.${version.split('.')[0]}.dylib` : 'libonnxruntime.so.1'; - return [ - onnxRuntime, - `${prefix}onnxruntime-genai${ext}`, - ]; +} + +function onnxRuntimeGenAiLibraryName(target: string): string { + return target.startsWith('win32-') + ? 'onnxruntime-genai.dll' + : target.startsWith('darwin-') + ? 'libonnxruntime-genai.dylib' + : 'libonnxruntime-genai.so'; } export function normalizeOrtLibraryName(binDir: string, target: string, version: string): void { @@ -180,7 +189,7 @@ async function installPackage( skipIfPresent: boolean, ): Promise { if (skipIfPresent) { - const expectedFile = expectedDependencyLibraryName(target, artifact.name); + const expectedFile = expectedDependencyLibraryName(target, artifact); if (expectedFile && fs.existsSync(path.join(binDir, expectedFile))) { console.log(`[${SCRIPT}] ${artifact.name}: already present, skipping download.`); return; @@ -223,14 +232,12 @@ async function installPackage( throw new Error(`[${SCRIPT}] Failed to download ${artifact.name} ${artifact.version} from any feed (${feedHosts}): ${lastError instanceof Error ? lastError.message : lastError}`); } -function expectedDependencyLibraryName(target: string, packageName: string): string | undefined { - const isWin = target.startsWith('win32-'); - const isDarwin = target.startsWith('darwin-'); - if (packageName.includes('OnnxRuntimeGenAI')) { - return `${isWin ? '' : 'lib'}onnxruntime-genai${isWin ? '.dll' : isDarwin ? '.dylib' : '.so'}`; +function expectedDependencyLibraryName(target: string, artifact: INugetArtifact): string | undefined { + if (artifact.name.includes('OnnxRuntimeGenAI')) { + return onnxRuntimeGenAiLibraryName(target); } - if (packageName.includes('OnnxRuntime')) { - return isWin ? 'onnxruntime.dll' : isDarwin ? 'libonnxruntime.dylib' : 'libonnxruntime.so'; + if (artifact.name.includes('OnnxRuntime')) { + return onnxRuntimeLibraryName(target, artifact.version); } return undefined; } diff --git a/build/lib/test/dictationRuntime.test.ts b/build/lib/test/dictationRuntime.test.ts index 65e168c1386930..55a55d59f0e2ff 100644 --- a/build/lib/test/dictationRuntime.test.ts +++ b/build/lib/test/dictationRuntime.test.ts @@ -9,6 +9,7 @@ import * as os from 'os'; import * as path from 'path'; import { suite, test } from 'node:test'; import { + fetchDependencyLibraries, getStandardArtifacts, type IFoundryDependencyVersions, normalizeOrtLibraryName, @@ -50,4 +51,20 @@ suite('dictation runtime', () => { fs.rmSync(directory, { recursive: true, force: true }); } }); + + test('skips downloads when normalized dependency libraries are present', async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'dictation-runtime-test-')); + try { + for (const target of ['linux-x64', 'darwin-arm64']) { + const targetDirectory = path.join(directory, target); + fs.mkdirSync(targetDirectory); + for (const name of requiredDependencyLibraryNames(target, dependencies)) { + fs.writeFileSync(path.join(targetDirectory, name), 'runtime'); + } + await fetchDependencyLibraries(target, getStandardArtifacts(dependencies), targetDirectory, { feeds: [], skipIfPresent: true }); + } + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); }); From e858e96c87104e7420a72bdbd1d55ebfa1a83921 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:44:45 -0700 Subject: [PATCH 14/17] Reduce cross-compile noise --- .../common/dictation-runtime-produce.yml | 10 ++++++++-- .../darwin/product-build-darwin-node-modules.yml | 7 +------ .../linux/product-build-linux-node-modules.yml | 7 +------ .../win32/product-build-win32-node-modules.yml | 7 +------ 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/build/azure-pipelines/common/dictation-runtime-produce.yml b/build/azure-pipelines/common/dictation-runtime-produce.yml index 2b91af532a1baf..8da8c626a07b64 100644 --- a/build/azure-pipelines/common/dictation-runtime-produce.yml +++ b/build/azure-pipelines/common/dictation-runtime-produce.yml @@ -22,11 +22,17 @@ parameters: # node_modules. No npm registry auth is needed. # # Steps: -# 1. AzureCLI@2 to fetch the SPN credentials for the CDN storage account — +# 1. NuGetAuthenticate@1 for the target-specific ONNX downloads — skipped on +# non-publish runs because no payload is produced. +# 2. AzureCLI@2 to fetch the SPN credentials for the CDN storage account — # skipped on non-publish runs (no CDN write to auth for). -# 2. Run `node build/dictation-runtime/produce.ts`. Always stamps; uploads +# 3. Run `node build/dictation-runtime/produce.ts`. Always stamps; uploads # only on publish. steps: + - task: NuGetAuthenticate@1 + displayName: "Dictation runtime: authenticate NuGet" + condition: and(succeeded(), eq(lower(variables['VSCODE_PUBLISH']), 'true')) + - task: AzureCLI@2 displayName: "Dictation runtime: fetch CDN credentials" condition: and(succeeded(), eq(lower(variables['VSCODE_PUBLISH']), 'true')) diff --git a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml index eaeb4a83fae3c6..2749d2dd177b6b 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml @@ -75,10 +75,6 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Setup PyPI Authentication - - task: NuGetAuthenticate@1 - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Setup NuGet Authentication - - script: | set -e c++ --version @@ -98,8 +94,7 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" - FOUNDRY_LOCAL_NUGET_MODE: dotnet - FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" + FOUNDRY_LOCAL_SKIP_INSTALL: "1" # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen diff --git a/build/azure-pipelines/linux/product-build-linux-node-modules.yml b/build/azure-pipelines/linux/product-build-linux-node-modules.yml index 176562a639441c..1019ad95cf64cd 100644 --- a/build/azure-pipelines/linux/product-build-linux-node-modules.yml +++ b/build/azure-pipelines/linux/product-build-linux-node-modules.yml @@ -82,10 +82,6 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - task: NuGetAuthenticate@1 - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Setup NuGet Authentication - - script: | set -e @@ -143,8 +139,7 @@ jobs: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 ONNXRUNTIME_NODE_INSTALL: skip GITHUB_TOKEN: "$(github-token-code-oss)" - FOUNDRY_LOCAL_NUGET_MODE: dotnet - FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" + FOUNDRY_LOCAL_SKIP_INSTALL: "1" displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/win32/product-build-win32-node-modules.yml b/build/azure-pipelines/win32/product-build-win32-node-modules.yml index 2cc86618cee49b..6669ab2ecd3679 100644 --- a/build/azure-pipelines/win32/product-build-win32-node-modules.yml +++ b/build/azure-pipelines/win32/product-build-win32-node-modules.yml @@ -71,10 +71,6 @@ jobs: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - task: NuGetAuthenticate@1 - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Setup NuGet Authentication - - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -85,8 +81,7 @@ jobs: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-token-code-oss)" - FOUNDRY_LOCAL_NUGET_MODE: dotnet - FOUNDRY_LOCAL_NUGET_FEEDS: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/index.json" + FOUNDRY_LOCAL_SKIP_INSTALL: "1" retryCountOnTaskFailure: 5 displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) From aa60809cda031ea73612d727276203d207d1ef2f Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:00:51 -0700 Subject: [PATCH 15/17] fix: outdated ref --- build/azure-pipelines/win32/sdl-scan-win32.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index 84e49ebbc6b806..e318b55e8026cf 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -119,10 +119,6 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - template: ../common/foundry-local.yml@self - parameters: - phase: prepare - - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" From f0716acbcf2a3aa1eb2bc4dca641513b85123407 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 23 Sep 2026 08:06:09 -0700 Subject: [PATCH 16/17] Work around outdated upload --- build/agent-sdk/README.md | 3 ++- build/agent-sdk/upload.ts | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/build/agent-sdk/README.md b/build/agent-sdk/README.md index 7e84f6f5e23242..360f6933d2111f 100644 --- a/build/agent-sdk/README.md +++ b/build/agent-sdk/README.md @@ -106,7 +106,8 @@ gulp graph. As its own pipeline step: node-tar+gzip with reproducible flags. Has a thin CLI at bottom. - `upload.ts` — `uploadOne(...)`. HEAD-then-decide: absent → upload; matching sha → skip (idempotent re-runs); different / no-metadata sha - → fail loud, refusing to overwrite content-addressed history. Thin CLI. + → fail loud, refusing to overwrite content-addressed history, except for + explicitly allowlisted development versions. Thin CLI. - `produce.ts` — pipeline-step entry. For one `(vscode-platform, arch)`, iterates the SDKs in parallel, calls `buildOne` + `uploadOne` for each that applies, writes results to `AGENT_SDK_RESULTS_FILE`, and emits diff --git a/build/agent-sdk/upload.ts b/build/agent-sdk/upload.ts index fb15dd3b6ac004..5b45b19529e0a0 100644 --- a/build/agent-sdk/upload.ts +++ b/build/agent-sdk/upload.ts @@ -14,8 +14,8 @@ * - Absent → upload. * - Present with matching sha256 (in `metadata.sha256`) → skip. * - Present with different / no sha256 metadata → fail loud, refusing to - * overwrite content-addressed history. Recovery: delete the blob in the - * Azure Portal and re-run. + * overwrite content-addressed history, except for explicitly allowlisted + * development versions. */ import * as fs from 'fs'; @@ -25,6 +25,7 @@ import { BlobServiceClient } from '@azure/storage-blob'; import { buildCdnUrl, getAgentMeta, parseFlags, type Sdk, sha256OfFile } from './common.ts'; const SCRIPT = 'upload.ts'; +const OVERWRITABLE_DEVELOPMENT_VERSIONS = new Set(['2.0.1']); export interface IUploadArgs { readonly sdk: Sdk; @@ -77,13 +78,16 @@ export async function uploadOne(args: IUploadArgs): Promise { console.log(`[${SCRIPT}] blob already present with matching sha256 — skipping upload (idempotent).`); return { url: buildCdnUrl(args.sdk, args.sdkVersion, args.sdkTarget), sha256 }; } - throw new Error( - `[${SCRIPT}] Blob already present with ${remoteSha ? 'DIFFERENT' : 'NO'} sha256 metadata — refusing to overwrite content-addressed history.\n` + - ` remote: ${remoteSha ?? ''}\n` + - ` local: ${sha256}\n` + - `If the local build is what should ship, delete the remote blob in Azure Portal and re-run. ` + - `Otherwise: investigate why the same ${getAgentMeta(args.sdk).name}@${args.sdkVersion} produced different bytes.`, - ); + if (!OVERWRITABLE_DEVELOPMENT_VERSIONS.has(args.sdkVersion)) { + throw new Error( + `[${SCRIPT}] Blob already present with ${remoteSha ? 'DIFFERENT' : 'NO'} sha256 metadata — refusing to overwrite content-addressed history.\n` + + ` remote: ${remoteSha ?? ''}\n` + + ` local: ${sha256}\n` + + `If the local build is what should ship, delete the remote blob in Azure Portal and re-run. ` + + `Otherwise: investigate why the same ${getAgentMeta(args.sdk).name}@${args.sdkVersion} produced different bytes.`, + ); + } + console.log(`[${SCRIPT}] overwriting ${getAgentMeta(args.sdk).name}@${args.sdkVersion} because it is an allowlisted development version.`); } console.log(`[${SCRIPT}] uploading ${fs.statSync(args.tgzPath).size} bytes…`); From 1686d8b9cafd7ae04e7b9409864213cde1e6fa3d Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 23 Sep 2026 08:24:42 -0700 Subject: [PATCH 17/17] Revert "Work around outdated upload" This reverts commit f0716acbcf2a3aa1eb2bc4dca641513b85123407. --- build/agent-sdk/README.md | 3 +-- build/agent-sdk/upload.ts | 22 +++++++++------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/build/agent-sdk/README.md b/build/agent-sdk/README.md index 360f6933d2111f..7e84f6f5e23242 100644 --- a/build/agent-sdk/README.md +++ b/build/agent-sdk/README.md @@ -106,8 +106,7 @@ gulp graph. As its own pipeline step: node-tar+gzip with reproducible flags. Has a thin CLI at bottom. - `upload.ts` — `uploadOne(...)`. HEAD-then-decide: absent → upload; matching sha → skip (idempotent re-runs); different / no-metadata sha - → fail loud, refusing to overwrite content-addressed history, except for - explicitly allowlisted development versions. Thin CLI. + → fail loud, refusing to overwrite content-addressed history. Thin CLI. - `produce.ts` — pipeline-step entry. For one `(vscode-platform, arch)`, iterates the SDKs in parallel, calls `buildOne` + `uploadOne` for each that applies, writes results to `AGENT_SDK_RESULTS_FILE`, and emits diff --git a/build/agent-sdk/upload.ts b/build/agent-sdk/upload.ts index 5b45b19529e0a0..fb15dd3b6ac004 100644 --- a/build/agent-sdk/upload.ts +++ b/build/agent-sdk/upload.ts @@ -14,8 +14,8 @@ * - Absent → upload. * - Present with matching sha256 (in `metadata.sha256`) → skip. * - Present with different / no sha256 metadata → fail loud, refusing to - * overwrite content-addressed history, except for explicitly allowlisted - * development versions. + * overwrite content-addressed history. Recovery: delete the blob in the + * Azure Portal and re-run. */ import * as fs from 'fs'; @@ -25,7 +25,6 @@ import { BlobServiceClient } from '@azure/storage-blob'; import { buildCdnUrl, getAgentMeta, parseFlags, type Sdk, sha256OfFile } from './common.ts'; const SCRIPT = 'upload.ts'; -const OVERWRITABLE_DEVELOPMENT_VERSIONS = new Set(['2.0.1']); export interface IUploadArgs { readonly sdk: Sdk; @@ -78,16 +77,13 @@ export async function uploadOne(args: IUploadArgs): Promise { console.log(`[${SCRIPT}] blob already present with matching sha256 — skipping upload (idempotent).`); return { url: buildCdnUrl(args.sdk, args.sdkVersion, args.sdkTarget), sha256 }; } - if (!OVERWRITABLE_DEVELOPMENT_VERSIONS.has(args.sdkVersion)) { - throw new Error( - `[${SCRIPT}] Blob already present with ${remoteSha ? 'DIFFERENT' : 'NO'} sha256 metadata — refusing to overwrite content-addressed history.\n` + - ` remote: ${remoteSha ?? ''}\n` + - ` local: ${sha256}\n` + - `If the local build is what should ship, delete the remote blob in Azure Portal and re-run. ` + - `Otherwise: investigate why the same ${getAgentMeta(args.sdk).name}@${args.sdkVersion} produced different bytes.`, - ); - } - console.log(`[${SCRIPT}] overwriting ${getAgentMeta(args.sdk).name}@${args.sdkVersion} because it is an allowlisted development version.`); + throw new Error( + `[${SCRIPT}] Blob already present with ${remoteSha ? 'DIFFERENT' : 'NO'} sha256 metadata — refusing to overwrite content-addressed history.\n` + + ` remote: ${remoteSha ?? ''}\n` + + ` local: ${sha256}\n` + + `If the local build is what should ship, delete the remote blob in Azure Portal and re-run. ` + + `Otherwise: investigate why the same ${getAgentMeta(args.sdk).name}@${args.sdkVersion} produced different bytes.`, + ); } console.log(`[${SCRIPT}] uploading ${fs.statSync(args.tgzPath).size} bytes…`);