vcpkg-tool/vcpkg-artifacts/session.ts

264 строки
9.3 KiB
TypeScript

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { strict } from 'assert';
import { createHash } from 'crypto';
import { MetadataFile } from './amf/metadata-file';
import { Artifact, InstalledArtifact } from './artifacts/artifact';
import { configurationName, defaultConfig } from './constants';
import { FileSystem } from './fs/filesystem';
import { HttpsFileSystem } from './fs/http-filesystem';
import { LocalFileSystem } from './fs/local-filesystem';
import { UnifiedFileSystem } from './fs/unified-filesystem';
import { VsixLocalFilesystem } from './fs/vsix-local-filesystem';
import { i } from './i18n';
import { installGit } from './installers/git';
import { installNuGet } from './installers/nuget';
import { installUnTar } from './installers/untar';
import { installUnZip } from './installers/unzip';
import { InstallEvents, InstallOptions } from './interfaces/events';
import { Installer } from './interfaces/metadata/installers/Installer';
import { RegistryDatabase, RegistryResolver } from './registries/registries';
import { Channels, Stopwatch } from './util/channels';
import { Uri } from './util/uri';
/** The definition for an installer tool function */
type InstallerTool<T extends Installer = any> = (
session: Session,
name: string,
version: string,
targetLocation: Uri,
install: T,
events: Partial<InstallEvents>,
options: Partial<InstallOptions>
) => Promise<void>
export type Context = { [key: string]: Array<string> | undefined; } & {
readonly os: string;
readonly arch: string;
readonly windows: boolean;
readonly osx: boolean;
readonly linux: boolean;
readonly freebsd: boolean;
readonly x64: boolean;
readonly x86: boolean;
readonly arm: boolean;
readonly arm64: boolean;
}
export type SessionSettings = {
readonly vcpkgCommand?: string;
readonly homeFolder: string;
readonly vcpkgArtifactsRoot?: string;
readonly vcpkgDownloads?: string;
readonly vcpkgRegistriesCache?: string;
readonly telemetryFile?: string;
readonly nextPreviousEnvironment?: string;
readonly globalConfig?: string;
}
interface ArtifactEntry {
registryUri: string;
id: string;
version: string;
}
function hexsha(content: string) {
return createHash('sha256').update(content, 'ascii').digest('hex');
}
function formatArtifactEntry(entry: ArtifactEntry): string {
// we hash all the things to remove PII
return `${hexsha(entry.registryUri)}:${hexsha(entry.id)}:${hexsha(entry.version)}`;
}
/**
* The Session class is used to hold a reference to the
* message channels,
* the filesystems,
* and any other 'global' data that should be kept.
*
*/
export class Session {
/** @internal */
readonly stopwatch = new Stopwatch();
readonly fileSystem: FileSystem;
readonly channels: Channels;
readonly homeFolder: Uri;
readonly nextPreviousEnvironment: Uri;
readonly installFolder: Uri;
readonly registryFolder: Uri;
readonly telemetryFile: Uri | undefined;
get vcpkgCommand() { return this.settings.vcpkgCommand; }
readonly globalConfig: Uri;
readonly downloads: Uri;
currentDirectory: Uri;
configuration?: MetadataFile;
/** register installer functions here */
private installers = new Map<string, InstallerTool>([
['nuget', installNuGet],
['unzip', installUnZip],
['untar', installUnTar],
['git', installGit]
]);
readonly registryDatabase = new RegistryDatabase();
readonly globalRegistryResolver = new RegistryResolver(this.registryDatabase);
processVcpkgArg(argSetting: string | undefined, defaultName: string): Uri {
return argSetting ? this.fileSystem.file(argSetting) : this.homeFolder.join(defaultName);
}
constructor(currentDirectory: string, public readonly context: Context, public readonly settings: SessionSettings) {
this.fileSystem = new UnifiedFileSystem(this).
register('file', new LocalFileSystem(this)).
register('vsix', new VsixLocalFilesystem(this)).
register('https', new HttpsFileSystem(this)
);
this.channels = new Channels(this);
if (settings.telemetryFile) {
this.telemetryFile = this.fileSystem.file(settings.telemetryFile);
}
this.homeFolder = this.fileSystem.file(settings.homeFolder);
this.downloads = this.processVcpkgArg(settings.vcpkgDownloads, 'downloads');
this.globalConfig = this.processVcpkgArg(settings.globalConfig, configurationName);
this.registryFolder = this.processVcpkgArg(settings.vcpkgRegistriesCache, 'registries').join('artifact');
this.installFolder = this.processVcpkgArg(settings.vcpkgArtifactsRoot, 'artifacts');
this.nextPreviousEnvironment = this.processVcpkgArg(settings.nextPreviousEnvironment, `previous-environment-${Date.now().toFixed()}.json`);
this.currentDirectory = this.fileSystem.file(currentDirectory);
}
parseLocation(location: string): Uri {
// Drive letter, absolute Unix path, or drive-relative windows path, treat as a file
if (/^[A-Za-z]:/.exec(location) || location.startsWith('/') || location.startsWith('\\')) {
return this.fileSystem.file(location);
}
// Otherwise, it's a URI
return this.fileSystem.parseUri(location);
}
async saveConfig() {
await this.configuration?.save(this.globalConfig);
}
async init() {
// load global configuration
if (!await this.fileSystem.isDirectory(this.homeFolder)) {
// let's create the folder
try {
await this.fileSystem.createDirectory(this.homeFolder);
} catch (error: any) {
// if this throws, let it
this.channels.debug(error?.message);
}
// check if it got made, because at an absolute minimum, we need a folder, so failing this is catastrophic.
strict.ok(await this.fileSystem.isDirectory(this.homeFolder), i`Fatal: The root folder '${this.homeFolder.fsPath}' cannot be created`);
}
if (!await this.fileSystem.isFile(this.globalConfig)) {
try {
await this.globalConfig.writeUTF8(defaultConfig);
} catch {
// if this throws, let it
}
// check if it got made, because at an absolute minimum, we need the config file, so failing this is catastrophic.
strict.ok(await this.fileSystem.isFile(this.globalConfig), i`Fatal: The global configuration file '${this.globalConfig.fsPath}' cannot be created`);
}
// got past the checks, let's load the configuration.
this.configuration = await MetadataFile.parseMetadata(this.globalConfig.fsPath, this.globalConfig, this);
this.channels.debug(`Loaded global configuration file '${this.globalConfig.fsPath}'`);
// load the registries
for (const [name, regDef] of this.configuration.registries) {
const loc = regDef.location.get(0);
if (loc) {
const uri = this.parseLocation(loc);
const reg = await this.registryDatabase.loadRegistry(this, uri);
this.globalRegistryResolver.add(uri, name);
if (reg) {
this.channels.debug(`Loaded global manifest ${name} => ${uri.formatted}`);
}
}
}
return this;
}
async findProjectProfile(startLocation = this.currentDirectory): Promise<Uri | undefined> {
let location = startLocation;
const path = location.join(configurationName);
if (await this.fileSystem.isFile(path)) {
return path;
}
location = location.join('..');
return (location.toString() === startLocation.toString()) ? undefined : this.findProjectProfile(location);
}
async getInstalledArtifacts() {
const result = new Array<{ folder: Uri, id: string, artifact: Artifact }>();
if (! await this.installFolder.exists()) {
return result;
}
for (const [folder, stat] of await this.installFolder.readDirectory(undefined, { recursive: true })) {
try {
const artifactJsonPath = folder.join('artifact.json');
const metadata = await MetadataFile.parseMetadata(artifactJsonPath.fsPath, artifactJsonPath, this);
result.push({
folder,
id: metadata.id,
artifact: await new InstalledArtifact(this, metadata)
});
} catch {
// not a valid install.
}
}
return result;
}
/** returns an installer function (or undefined) for a given installerkind */
artifactInstaller(installInfo: Installer) {
return this.installers.get(installInfo.installerKind);
}
async openManifest(filename: string, uri: Uri): Promise<MetadataFile> {
return await MetadataFile.parseConfiguration(filename, await uri.readUTF8(), this);
}
readonly #acquiredArtifacts: Array<ArtifactEntry> = [];
readonly #activatedArtifacts: Array<ArtifactEntry> = [];
trackAcquire(registryUri: string, id: string, version: string) {
this.#acquiredArtifacts.push({ registryUri: registryUri, id: id, version: version });
}
trackActivate(registryUri: string, id: string, version: string) {
this.#activatedArtifacts.push({ registryUri: registryUri, id: id, version: version });
}
writeTelemetry(): Promise<any> {
const acquiredArtifacts = this.#acquiredArtifacts.map(formatArtifactEntry).join(',');
const activatedArtifacts = this.#activatedArtifacts.map(formatArtifactEntry).join(',');
const telemetryFile = this.telemetryFile;
if (telemetryFile) {
return telemetryFile.writeUTF8(JSON.stringify({
'acquired-artifacts': acquiredArtifacts,
'activated-artifacts': activatedArtifacts
}));
}
return Promise.resolve(undefined);
}
}