Files
compiler-explorer/lib/options-handler.ts
Mats ea29a6e9d6 Separate list and load actions for source API (#7090)
This is the first PR in API consistency with more to come:

1. The source API urls have been properly defined with
`/source/:source/list` and `/source/:source/load/:language/:filename`
- These used to be two API endpoints mushed together into one `handle()`
function. They are now separate
- While it may appear from the tests that this is an API breakage,
there's currently no way to have the source API spit out responses for
stuff like `/source/moose/load/Grunkle`.
2. The frontend code calling the list api now has shared types
3. Code has been migrated to `/lib/handlers/api/source.ts`
- I've moved code here, because I would like to separate the stuff
that's under the API and the stuff that isn't (e.g., healthcheck)
- The source endpoint is currently not under /api, but I believe it
makes sense to move it.
4. GitHub is terrible at showing the diff, but the `browser.ts` file has
been removed, since it's never actually used. Our frontend only calls
`/source/builtin/...` and it never had any entries. Besides, the type
used on the frontend for the locally stored stuff is different...
2024-11-27 13:54:47 +09:00

503 lines
22 KiB
TypeScript
Executable File

// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import https from 'https';
import path from 'path';
import fs from 'fs-extra';
import semverParser from 'semver';
import _ from 'underscore';
import {AppDefaultArguments} from '../app.js';
import {CompilerInfo} from '../types/compiler.interfaces.js';
import type {LanguageKey} from '../types/languages.interfaces.js';
import type {Source} from '../types/source.interfaces.js';
import type {ToolTypeKey} from '../types/tool.interfaces.js';
import {logger} from './logger.js';
import type {PropertyGetter, PropertyValue} from './properties.interfaces.js';
import {CompilerProps} from './properties.js';
import {BaseTool, getToolTypeByKey} from './tooling/index.js';
import {asSafeVer, getHash, splitArguments, splitIntoArray} from './utils.js';
// TODO: Figure out if same as libraries.interfaces.ts?
export type VersionInfo = {
name?: string;
version: string;
staticliblink: string[];
alias: string[];
dependencies: string[];
path: string[];
libpath: string[];
liblink: string[];
lookupname?: PropertyValue;
lookupversion?: PropertyValue;
options: string[];
hidden: boolean;
packagedheaders?: boolean;
$order?: number;
};
export type OptionsHandlerLibrary = {
name: string;
url: string;
description: string;
staticliblink: string[];
liblink: string[];
dependencies: string[];
versions: Record<string, VersionInfo>;
examples: string[];
options: string[];
packagedheaders?: boolean;
};
// TODO: Is this the same as Options in static/options.interfaces.ts?
export type ClientOptionsType = {
sharingEnabled: boolean;
githubEnabled: boolean;
showSponsors: boolean;
urlShortenService: string;
defaultSource: string;
compilers: CompilerInfo[];
libs: Record<string, Record<string, OptionsHandlerLibrary>>;
remoteLibs: Record<any, any>;
tools: Record<any, any>;
defaultLibs: Record<LanguageKey, string>;
defaultCompiler: Record<LanguageKey, string>;
compileOptions: Record<LanguageKey, string>;
supportsBinary: Record<LanguageKey, boolean>;
supportsBinaryObject: Record<LanguageKey, boolean>;
supportsExecute: boolean;
supportsLibraryCodeFilter: boolean;
languages: Record<string, any>;
sources: {
name: string;
urlpart: string;
}[];
sentryDsn: string;
sentryEnvironment: string | number | true;
release: string;
gitReleaseCommit: string;
cookieDomainRe: string;
localStoragePrefix: PropertyValue;
cvCompilerCountMax: number;
defaultFontScale: number;
doCache: boolean;
thirdPartyIntegrationEnabled: boolean;
statusTrackingEnabled: boolean;
compilerVersionsUrl?: string;
policies: {
cookies: {
enabled: boolean;
key: string;
};
privacy: {
enabled: boolean;
key: string;
};
};
motdUrl: string;
pageloadUrl: string;
};
/***
* Handles the setup of the options object passed on each page request
*/
export class ClientOptionsHandler {
compilerProps: CompilerProps['get'];
ceProps: PropertyGetter;
supportsBinary: Record<LanguageKey, boolean>;
supportsBinaryObject: Record<LanguageKey, boolean>;
supportsExecutePerLanguage: Record<LanguageKey, boolean>;
supportsExecute: boolean;
supportsLibraryCodeFilterPerLanguage: Record<LanguageKey, boolean>;
supportsLibraryCodeFilter: boolean;
remoteLibs: Record<any, any>;
options: ClientOptionsType;
optionsJSON: string;
optionsHash: string;
/***
*
* @param {Object[]} fileSources - Files to show in the Load/Save pane
* @param {string} fileSources.name - UI display name of the file
* @param {string} fileSources.urlpart - Relative url path to fetch the file from
* @param {CompilerProps} compilerProps
* @param {Object} defArgs - Compiler Explorer arguments
*/
constructor(fileSources: Source[], compilerProps: CompilerProps, defArgs: AppDefaultArguments) {
this.compilerProps = compilerProps.get.bind(compilerProps);
this.ceProps = compilerProps.ceProps;
const ceProps = compilerProps.ceProps;
const sources = _.sortBy(
fileSources.map(source => {
return {name: source.name, urlpart: source.urlpart};
}),
'name',
);
/***
* @type {CELanguages}
*/
const languages = compilerProps.languages;
this.supportsBinary = this.compilerProps(languages, 'supportsBinary', true, res => !!res);
this.supportsBinaryObject = this.compilerProps(languages, 'supportsBinaryObject', true, res => !!res);
this.supportsExecutePerLanguage = this.compilerProps(languages, 'supportsExecute', true, res => !!res);
this.supportsExecute = Object.values(this.supportsExecutePerLanguage).some(Boolean);
this.supportsLibraryCodeFilterPerLanguage = this.compilerProps(languages, 'supportsLibraryCodeFilter', false);
this.supportsLibraryCodeFilter = Object.values(this.supportsLibraryCodeFilterPerLanguage).some(Boolean);
const libs = this.parseLibraries(this.compilerProps<string>(languages, 'libs'));
const tools = this.parseTools(this.compilerProps<string>(languages, 'tools'));
this.remoteLibs = {};
const cookiePolicyEnabled = !!ceProps('cookiePolicyEnabled');
const privacyPolicyEnabled = !!ceProps('privacyPolicyEnabled');
const cookieDomainRe = ceProps('cookieDomainRe', '');
this.options = {
sharingEnabled: ceProps('clientSharingEnabled', true),
githubEnabled: ceProps('clientGitHubRibbonEnabled', true),
showSponsors: ceProps('showSponsors', false),
urlShortenService: ceProps('urlShortenService', 'default'),
defaultSource: ceProps('defaultSource', ''),
compilers: [],
libs: libs,
remoteLibs: {},
tools: tools,
defaultLibs: this.compilerProps(languages, 'defaultLibs', ''),
defaultCompiler: this.compilerProps(languages, 'defaultCompiler', ''),
compileOptions: this.compilerProps(languages, 'defaultOptions', ''),
supportsBinary: this.supportsBinary,
supportsBinaryObject: this.supportsBinaryObject,
supportsExecute: this.supportsExecute,
supportsLibraryCodeFilter: this.supportsLibraryCodeFilter,
languages: languages,
sources: sources,
sentryDsn: ceProps('sentryDsn', ''),
sentryEnvironment: ceProps('sentryEnvironment') || defArgs.env[0],
release: defArgs.releaseBuildNumber || defArgs.gitReleaseName,
gitReleaseCommit: defArgs.gitReleaseName || '',
cookieDomainRe,
localStoragePrefix: ceProps('localStoragePrefix'),
cvCompilerCountMax: ceProps('cvCompilerCountMax', 6),
defaultFontScale: ceProps('defaultFontScale', 14),
doCache: defArgs.doCache,
thirdPartyIntegrationEnabled: ceProps('thirdPartyIntegrationEnabled', true),
compilerVersionsUrl: ceProps<string | undefined>('compilerVersionsUrl', undefined),
statusTrackingEnabled: ceProps('statusTrackingEnabled', true),
policies: {
cookies: {
enabled: cookiePolicyEnabled,
key: 'cookie_status',
},
privacy: {
enabled: privacyPolicyEnabled,
key: 'privacy_status',
},
},
motdUrl: ceProps('motdUrl', ''),
pageloadUrl: ceProps('pageloadUrl', ''),
};
// Will be immediately replaced with actual values
this.optionsJSON = '';
this.optionsHash = '';
this._updateOptionsHash();
}
parseTools(baseTools: Record<string, string>) {
const tools: Record<string, Record<string, BaseTool>> = {};
for (const [lang, forLang] of Object.entries(baseTools)) {
if (lang && forLang) {
tools[lang] = {};
for (const tool of forLang.split(':')) {
const toolBaseName = `tools.${tool}`;
const className = this.compilerProps<string>(lang, toolBaseName + '.class');
const Tool = getToolTypeByKey(className);
const toolPath = this.compilerProps<string>(lang, toolBaseName + '.exe');
if (fs.existsSync(toolPath)) {
tools[lang][tool] = new Tool(
{
id: tool,
name: this.compilerProps<string>(lang, toolBaseName + '.name'),
type: this.compilerProps<string>(lang, toolBaseName + '.type') as ToolTypeKey,
exe: toolPath,
exclude: splitIntoArray(this.compilerProps<string>(lang, toolBaseName + '.exclude')),
includeKey: this.compilerProps<string>(lang, toolBaseName + '.includeKey'),
options: splitArguments(this.compilerProps<string>(lang, toolBaseName + '.options')),
args: this.compilerProps<string>(lang, toolBaseName + '.args'),
languageId: this.compilerProps<string>(
lang,
toolBaseName + '.languageId',
) as LanguageKey,
stdinHint: this.compilerProps<string>(lang, toolBaseName + '.stdinHint'),
monacoStdin: this.compilerProps<string>(lang, toolBaseName + '.monacoStdin'),
icon: this.compilerProps<string>(lang, toolBaseName + '.icon'),
darkIcon: this.compilerProps<string>(lang, toolBaseName + '.darkIcon'),
compilerLanguage: lang as LanguageKey,
},
{
ceProps: this.ceProps,
compilerProps: (propname: string) => this.compilerProps(lang, propname),
},
);
} else {
logger.warn(`Unable to stat ${toolBaseName} tool binary`);
}
}
}
}
return tools;
}
parseLibraries(baseLibs: Record<string, string>) {
// Record language -> {Record lib name -> lib}
const libraries: Record<string, Record<string, OptionsHandlerLibrary>> = {};
for (const [lang, forLang] of Object.entries(baseLibs)) {
if (lang && forLang) {
libraries[lang] = {};
for (const lib of forLang.split(':')) {
const libBaseName = `libs.${lib}`;
libraries[lang][lib] = {
name: this.compilerProps<string>(lang, libBaseName + '.name'),
url: this.compilerProps<string>(lang, libBaseName + '.url'),
description: this.compilerProps<string>(lang, libBaseName + '.description'),
staticliblink: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.staticliblink')),
liblink: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.liblink')),
dependencies: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.dependencies')),
versions: {},
examples: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.examples')),
options: splitArguments(this.compilerProps(lang, libBaseName + '.options', '')),
packagedheaders: this.compilerProps<boolean>(lang, libBaseName + '.packagedheaders', false),
};
const listedVersions = `${this.compilerProps(lang, libBaseName + '.versions')}`;
if (listedVersions) {
for (const version of listedVersions.split(':')) {
const libVersionName = libBaseName + `.versions.${version}`;
const versionObject: VersionInfo = {
version: this.compilerProps<string>(lang, libVersionName + '.version'),
staticliblink: splitIntoArray(
this.compilerProps<string>(lang, libVersionName + '.staticliblink'),
libraries[lang][lib].staticliblink,
),
alias: splitIntoArray(this.compilerProps<string>(lang, libVersionName + '.alias')),
dependencies: splitIntoArray(
this.compilerProps<string>(lang, libVersionName + '.dependencies'),
libraries[lang][lib].dependencies,
),
path: [],
libpath: [],
liblink: splitIntoArray(
this.compilerProps<string>(lang, libVersionName + '.liblink'),
libraries[lang][lib].liblink,
),
// Library options might get overridden later
options: libraries[lang][lib].options,
hidden: this.compilerProps(lang, libVersionName + '.hidden', false),
packagedheaders: libraries[lang][lib].packagedheaders,
};
const lookupversion = this.compilerProps(lang, libVersionName + '.lookupversion');
if (lookupversion) {
versionObject.lookupversion = lookupversion;
}
const lookupname = this.compilerProps(lang, libVersionName + '.lookupname');
if (lookupname) {
versionObject.lookupname = lookupname;
}
const includes = this.compilerProps<string>(lang, libVersionName + '.path');
if (includes) {
versionObject.path = includes.split(path.delimiter);
} else if (version !== 'autodetect') {
logger.warn(`Library ${lib} ${version} (${lang}) has no include paths`);
}
const libpath = this.compilerProps<string>(lang, libVersionName + '.libpath');
if (libpath) {
versionObject.libpath = libpath.split(path.delimiter);
}
const options = this.compilerProps<string>(lang, libVersionName + '.options');
if (options !== undefined) {
versionObject.options = splitArguments(options);
}
versionObject.packagedheaders = this.compilerProps<boolean>(
lang,
libVersionName + '.packagedheaders',
libraries[lang][lib].packagedheaders,
);
libraries[lang][lib].versions[version] = versionObject;
}
} else {
logger.warn(`No versions found for ${lib} library`);
}
}
}
}
for (const langGroup of Object.values(libraries)) {
for (const libGroup of Object.values(langGroup)) {
const versions = Object.values(libGroup.versions);
versions.sort((a, b) => semverParser.compare(asSafeVer(a.version), asSafeVer(b.version), true));
let order = 0;
// Set $order to index on array. As group is an array, iteration order is guaranteed.
for (const lib of versions) {
lib['$order'] = order++;
}
}
}
return libraries;
}
getRemoteId(remoteUrl: string, language: LanguageKey) {
const url = new URL(remoteUrl);
return url.host.replaceAll('.', '_') + '_' + language;
}
libArrayToObject(libsArr: any[]) {
const libs: Record<string, any> = {};
for (const lib of libsArr) {
libs[lib.id] = lib;
const versions = lib.versions;
lib.versions = {};
for (const version of versions) {
lib.versions[version.id] = version;
}
}
return libs;
}
async getRemoteLibraries(language: LanguageKey, remoteUrl: string) {
const remoteId = this.getRemoteId(remoteUrl, language);
if (!this.remoteLibs[remoteId]) {
return new Promise(resolve => {
const url = remoteUrl + '/api/libraries/' + language;
logger.info(`Fetching remote libraries from ${url}`);
let fullData = '';
https.get(url, res => {
res.on('data', data => {
fullData += data;
});
res.on('end', () => {
try {
const libsArr = JSON.parse(fullData);
this.remoteLibs[remoteId] = this.libArrayToObject(libsArr);
} catch (e) {
logger.error('Error while fetching remote libraries, but continuing.', e);
this.remoteLibs[remoteId] = {};
}
resolve(this.remoteLibs[remoteId]);
});
});
});
}
return this.remoteLibs[remoteId];
}
async fetchRemoteLibrariesIfNeeded(language: LanguageKey, target: string) {
await this.getRemoteLibraries(language, target);
}
async setCompilers(compilers: CompilerInfo[]) {
const forbiddenKeys = new Set([
'exe',
'versionFlag',
'versionRe',
'compilerType',
'demangler',
'objdumper',
'postProcess',
'demanglerType',
'isSemVer',
]);
const copiedCompilers = JSON.parse(JSON.stringify(compilers)) as CompilerInfo[];
const semverGroups: Record<string, Partial<CompilerInfo>[]> = {};
// Reset the supportsExecute flag in case critical compilers change
for (const key of Object.keys(this.options.languages)) {
this.options.languages[key].supportsExecute = false;
}
for (const [compilersKey, compiler] of copiedCompilers.entries()) {
if (compiler.supportsExecute) {
this.options.languages[compiler.lang].supportsExecute = true;
}
if (compiler.isSemVer) {
if (!semverGroups[compiler.group]) semverGroups[compiler.group] = [];
// Desired index which will keep the array in order
semverGroups[compiler.group].push(compiler);
}
if (compiler.remote) {
await this.fetchRemoteLibrariesIfNeeded(compiler.lang, compiler.remote.target);
}
for (const propKey of Object.keys(compiler)) {
if (forbiddenKeys.has(propKey)) {
delete copiedCompilers[compilersKey][propKey as keyof CompilerInfo];
}
}
}
for (const group of Object.values(semverGroups)) {
group.sort((a, b) => semverParser.compare(asSafeVer(a.semver), asSafeVer(b.semver), true));
let order = 0;
// Set $order to -index on array. As group is an array, iteration order is guaranteed.
for (const compiler of group) {
compiler['$order'] = -order++;
}
}
this.options.compilers = copiedCompilers;
this.options.remoteLibs = this.remoteLibs;
this._updateOptionsHash();
}
_updateOptionsHash() {
this.optionsJSON = JSON.stringify(this.options);
this.optionsHash = getHash(this.options, 'Options Hash V1');
logger.info(`OPTIONS HASH: ${this.optionsHash}`);
}
get() {
return this.options;
}
getJSON() {
return this.optionsJSON;
}
getHash() {
return this.optionsHash;
}
}