|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { execFileSync } from 'child_process'; |
| 7 | +import { createHash } from 'crypto'; |
| 8 | +import * as fs from 'fs'; |
| 9 | +import * as path from 'path'; |
| 10 | +import { fetchCoreLibraries, getStandardArtifacts, type IFoundryDependencyVersions, requiredCoreLibraryNames, supportsCoreLibraryTarget, VSCODE_NUGET_FEED } from '../../dictation-runtime/nuget.ts'; |
| 11 | + |
| 12 | +const repositoryRoot = path.resolve(import.meta.dirname, '../../..'); |
| 13 | +const packageName = 'foundry-local-sdk'; |
| 14 | +const credentialTokenEnvironmentVariable = 'VSS_NUGET_ACCESSTOKEN'; |
| 15 | +const expectedInstallerUtilsHash = '0831c932b10389283e805f88a204b0f6a5a8053f2ee520e56a0f0adf1352aa8b'; |
| 16 | + |
| 17 | +type RootPackageJson = { |
| 18 | + dependencies?: Record<string, string>; |
| 19 | + allowScripts?: Record<string, boolean>; |
| 20 | +}; |
| 21 | + |
| 22 | +type FoundryPackageJson = { |
| 23 | + version?: string; |
| 24 | + scripts?: Record<string, string>; |
| 25 | +}; |
| 26 | + |
| 27 | +function readJson<T>(filePath: string): T { |
| 28 | + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; |
| 29 | +} |
| 30 | + |
| 31 | +function getPinnedPackage(root: string): { packageJsonPath: string; packageJson: RootPackageJson; allowScripts: Record<string, boolean>; allowScriptsKey: string } { |
| 32 | + const packageJsonPath = path.join(root, 'package.json'); |
| 33 | + const packageJson = readJson<RootPackageJson>(packageJsonPath); |
| 34 | + const allowScripts = packageJson.allowScripts; |
| 35 | + const version = packageJson.dependencies?.[packageName]; |
| 36 | + const allowScriptsKey = version ? `${packageName}@${version}` : undefined; |
| 37 | + |
| 38 | + if (!allowScripts || !version || !allowScriptsKey || allowScripts[allowScriptsKey] !== true) { |
| 39 | + throw new Error(`Expected an approved, pinned ${packageName} install script in package.json`); |
| 40 | + } |
| 41 | + |
| 42 | + return { packageJsonPath, packageJson, allowScripts, allowScriptsKey }; |
| 43 | +} |
| 44 | + |
| 45 | +export function disableFoundryLocalInstall(root = repositoryRoot): void { |
| 46 | + const { packageJsonPath, packageJson, allowScripts, allowScriptsKey } = getPinnedPackage(root); |
| 47 | + allowScripts[allowScriptsKey] = false; |
| 48 | + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, undefined, 2)}\n`); |
| 49 | + console.log(`Disabled ${allowScriptsKey} install script for this CI job`); |
| 50 | +} |
| 51 | + |
| 52 | +function validateInstallerUtils(installerUtilsPath: string): void { |
| 53 | + const contents = fs.readFileSync(installerUtilsPath); |
| 54 | + const actualHash = createHash('sha256').update(contents).digest('hex'); |
| 55 | + |
| 56 | + if (actualHash !== expectedInstallerUtilsHash) { |
| 57 | + throw new Error(`Unexpected ${packageName} installer utility hash ${actualHash}`); |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +function runLifecycleScript(packageRoot: string, relativeScriptPath: string): void { |
| 62 | + execFileSync(process.execPath, [path.join(packageRoot, relativeScriptPath)], { |
| 63 | + cwd: packageRoot, |
| 64 | + stdio: 'inherit' |
| 65 | + }); |
| 66 | +} |
| 67 | + |
| 68 | +export async function installFoundryLocal(root = repositoryRoot): Promise<void> { |
| 69 | + if (!process.env[credentialTokenEnvironmentVariable]) { |
| 70 | + throw new Error(`${credentialTokenEnvironmentVariable} was not set by NuGetAuthenticate`); |
| 71 | + } |
| 72 | + |
| 73 | + const rootPackageJson = readJson<RootPackageJson>(path.join(root, 'package.json')); |
| 74 | + const version = rootPackageJson.dependencies?.[packageName]; |
| 75 | + const allowScriptsKey = version ? `${packageName}@${version}` : undefined; |
| 76 | + if (!version || !allowScriptsKey || rootPackageJson.allowScripts?.[allowScriptsKey] !== false) { |
| 77 | + throw new Error(`Expected the pinned ${packageName} install script to be disabled before installation`); |
| 78 | + } |
| 79 | + |
| 80 | + const packageRoot = path.join(root, 'node_modules', packageName); |
| 81 | + const packageJson = readJson<FoundryPackageJson>(path.join(packageRoot, 'package.json')); |
| 82 | + if (packageJson.version !== version) { |
| 83 | + throw new Error(`Expected ${packageName}@${version}, found ${packageJson.version ?? 'an unknown version'}`); |
| 84 | + } |
| 85 | + if (packageJson.scripts?.preinstall !== 'node script/preinstall.cjs' || packageJson.scripts.install !== 'node script/install-standard.cjs') { |
| 86 | + throw new Error(`Unexpected ${packageName}@${version} lifecycle scripts`); |
| 87 | + } |
| 88 | + |
| 89 | + validateInstallerUtils(path.join(packageRoot, 'script', 'install-utils.cjs')); |
| 90 | + runLifecycleScript(packageRoot, 'script/preinstall.cjs'); |
| 91 | + |
| 92 | + const target = `${process.platform}-${process.arch}`; |
| 93 | + if (!supportsCoreLibraryTarget(target)) { |
| 94 | + console.warn(`[foundry-local] Unsupported platform: ${target}. Skipping.`); |
| 95 | + return; |
| 96 | + } |
| 97 | + |
| 98 | + const dependencies = readJson<IFoundryDependencyVersions>(path.join(packageRoot, 'deps_versions.json')); |
| 99 | + const artifacts = getStandardArtifacts(target, dependencies); |
| 100 | + const binDir = path.join(packageRoot, 'foundry-local-core', target); |
| 101 | + await fetchCoreLibraries(target, artifacts, binDir, { feeds: [VSCODE_NUGET_FEED], skipIfPresent: true }); |
| 102 | + |
| 103 | + const missingFiles = requiredCoreLibraryNames(target).filter(file => !fs.existsSync(path.join(binDir, file))); |
| 104 | + if (missingFiles.length > 0) { |
| 105 | + throw new Error(`[foundry-local] Missing required native libraries for ${target}: ${missingFiles.join(', ')}`); |
| 106 | + } |
| 107 | + |
| 108 | + const coreVersion = dependencies['foundry-local-core'].nuget; |
| 109 | + const platformPackageJson = { |
| 110 | + name: `@foundry-local-core/${target}`, |
| 111 | + version: coreVersion, |
| 112 | + description: `Native binaries for Foundry Local SDK (${target})`, |
| 113 | + private: true, |
| 114 | + }; |
| 115 | + fs.writeFileSync(path.join(binDir, 'package.json'), JSON.stringify(platformPackageJson, undefined, 2)); |
| 116 | + console.log('[foundry-local] Installation complete.'); |
| 117 | +} |
| 118 | + |
| 119 | +if (import.meta.filename === process.argv[1]) { |
| 120 | + await installFoundryLocal(); |
| 121 | +} |
0 commit comments