mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
- latest biome, and fix its configuration - fixes "static" content to be globally configured too (instead of per-line) - fixes issues: - imports fixed up - `Date.now()` vs `+new Date()` - some unused things `_` prefixed After discussion with the team, turned off the unused parameter warning.
438 lines
17 KiB
TypeScript
438 lines
17 KiB
TypeScript
// Copyright (c) 2022, 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 {EventEmitter} from 'golden-layout';
|
|
import $ from 'jquery';
|
|
import {LRUCache} from 'lru-cache';
|
|
import _ from 'underscore';
|
|
import {ResultLine} from '../types/resultline/resultline.interfaces.js';
|
|
import {options} from './options.js';
|
|
|
|
import jqXHR = JQuery.jqXHR;
|
|
import ErrorTextStatus = JQuery.Ajax.ErrorTextStatus;
|
|
|
|
import {CompilationResult, FiledataPair} from '../types/compilation/compilation.interfaces.js';
|
|
import {CompilerInfo} from '../types/compiler.interfaces.js';
|
|
import {CompilationStatus} from './compiler-service.interfaces.js';
|
|
import {IncludeDownloads, SourceAndFiles} from './download-service.js';
|
|
import {SentryCapture} from './sentry.js';
|
|
import {SiteSettings} from './settings.js';
|
|
|
|
const ASCII_COLORS_RE = new RegExp(/\x1B\[[\d;]*m(.\[K)?/g);
|
|
|
|
export class CompilerService {
|
|
private readonly base = window.httpRoot;
|
|
private allowStoreCodeDebug: boolean;
|
|
private cache: LRUCache<string, CompilationResult>;
|
|
private readonly compilersByLang: Record<string, Record<string, CompilerInfo>>;
|
|
|
|
constructor(eventHub: EventEmitter) {
|
|
this.allowStoreCodeDebug = true;
|
|
this.cache = new LRUCache({
|
|
maxSize: 200 * 1024,
|
|
sizeCalculation: n => JSON.stringify(n).length,
|
|
});
|
|
|
|
this.compilersByLang = {};
|
|
|
|
for (const compiler of options.compilers) {
|
|
if (!(compiler.lang in this.compilersByLang)) this.compilersByLang[compiler.lang] = {};
|
|
this.compilersByLang[compiler.lang][compiler.id] = compiler;
|
|
}
|
|
|
|
eventHub.on(
|
|
'settingsChange',
|
|
(newSettings: SiteSettings) => (this.allowStoreCodeDebug = newSettings.allowStoreCodeDebug),
|
|
);
|
|
}
|
|
|
|
private getDefaultCompilerForLang(langId: string) {
|
|
return options.defaultCompiler[langId];
|
|
}
|
|
|
|
public processFromLangAndCompiler(
|
|
langId: string | null,
|
|
compilerId: string,
|
|
): {langId: string | null; compiler: CompilerInfo | null} | null {
|
|
try {
|
|
if (langId) {
|
|
if (!compilerId) {
|
|
compilerId = this.getDefaultCompilerForLang(langId);
|
|
}
|
|
|
|
const foundCompiler = this.findCompiler(langId, compilerId);
|
|
if (!foundCompiler) {
|
|
const compilers = Object.values(this.getCompilersForLang(langId) ?? {});
|
|
if (compilers.length > 0) {
|
|
return {
|
|
compiler: compilers[0],
|
|
langId: langId,
|
|
};
|
|
}
|
|
return {
|
|
// There were no compilers, so return null, the selection will show up empty
|
|
compiler: null,
|
|
langId: langId,
|
|
};
|
|
}
|
|
return {
|
|
compiler: foundCompiler,
|
|
langId: langId,
|
|
};
|
|
}
|
|
if (compilerId) {
|
|
const matchingCompilers = Object.values(options.languages).map(lang => {
|
|
const compiler = this.findCompiler(lang.id, compilerId);
|
|
if (compiler) {
|
|
return {
|
|
langId: lang.id,
|
|
compiler: compiler,
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
// Ensure that if no compiler is present, we return null instead of undefined
|
|
return matchingCompilers.find(compiler => compiler !== null) ?? null;
|
|
}
|
|
const languages = Object.values(options.languages);
|
|
if (languages.length > 0) {
|
|
const firstLang = languages[0];
|
|
return this.processFromLangAndCompiler(firstLang.id, compilerId);
|
|
}
|
|
// TODO: What now? No languages loaded
|
|
return null;
|
|
} catch (e) {
|
|
SentryCapture(e, 'processFromLangAndCompiler');
|
|
}
|
|
// TODO: What now? Found no compilers!
|
|
return {
|
|
langId: langId,
|
|
compiler: null,
|
|
};
|
|
}
|
|
|
|
public getGroupsInUse(langId: string): {value: string; label: string}[] {
|
|
return _.uniq(Object.values(this.getCompilersForLang(langId) ?? {}), false, compiler => compiler.group)
|
|
.map(compiler => {
|
|
return {value: compiler.group, label: compiler.groupName || compiler.group};
|
|
})
|
|
.sort((a, b) =>
|
|
a.label.localeCompare(b.label, undefined /* Ignore language */, {
|
|
sensitivity: 'base',
|
|
}),
|
|
);
|
|
}
|
|
|
|
getCompilersForLang(langId: string): Record<string, CompilerInfo> | undefined {
|
|
return langId in this.compilersByLang ? this.compilersByLang[langId] : undefined;
|
|
}
|
|
|
|
private findCompilerInList(compilers: Record<string, CompilerInfo>, compilerId: string) {
|
|
if (compilerId in compilers) {
|
|
return compilers[compilerId];
|
|
}
|
|
for (const id in compilers) {
|
|
if (compilers[id].alias.includes(compilerId)) {
|
|
return compilers[id];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
findCompiler(langId: string, compilerId: string): CompilerInfo | null {
|
|
if (!compilerId) return null;
|
|
const compilers = this.getCompilersForLang(langId) ?? {};
|
|
return this.findCompilerInList(compilers, compilerId);
|
|
}
|
|
|
|
private static handleRequestError(
|
|
request: any,
|
|
reject: (reason?: any) => void,
|
|
xhr: jqXHR,
|
|
textStatus: ErrorTextStatus,
|
|
errorThrown: string,
|
|
) {
|
|
let error = errorThrown;
|
|
if (!error) {
|
|
switch (textStatus) {
|
|
case 'timeout':
|
|
error = 'Request timed out';
|
|
break;
|
|
case 'abort':
|
|
error = 'Request was aborted';
|
|
break;
|
|
case 'error':
|
|
switch (xhr.status) {
|
|
case 500:
|
|
error = 'Request failed: internal server error';
|
|
break;
|
|
case 504:
|
|
error = 'Request failed: gateway timeout';
|
|
break;
|
|
default:
|
|
error = 'Request failed: HTTP error code ' + xhr.status;
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
error = 'Error sending request';
|
|
break;
|
|
}
|
|
}
|
|
const requestError = new Error(error);
|
|
// Attach request context to the error for debugging
|
|
(requestError as any).request = request;
|
|
reject(requestError);
|
|
}
|
|
|
|
private getBaseUrl() {
|
|
return window.location.origin + this.base;
|
|
}
|
|
|
|
public async submit(request: Record<string, any>) {
|
|
request.allowStoreCodeDebug = this.allowStoreCodeDebug;
|
|
const jsonRequest = JSON.stringify(request);
|
|
if (options.doCache && !request.bypassCache) {
|
|
const cachedResult = this.cache.get(jsonRequest);
|
|
if (cachedResult) {
|
|
return {
|
|
request: request,
|
|
result: cachedResult,
|
|
localCacheHit: true,
|
|
};
|
|
}
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const compilerId = encodeURIComponent(request.compiler);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: `${this.getBaseUrl()}api/compiler/${compilerId}/compile`,
|
|
dataType: 'json',
|
|
contentType: 'application/json',
|
|
data: jsonRequest,
|
|
success: result => {
|
|
if (result?.okToCache && options.doCache) {
|
|
this.cache.set(jsonRequest, result);
|
|
}
|
|
resolve({
|
|
request: request,
|
|
result: result,
|
|
localCacheHit: false,
|
|
});
|
|
},
|
|
error: (jqXHR, textStatus, errorThrown) => {
|
|
CompilerService.handleRequestError(request, reject, jqXHR, textStatus, errorThrown);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
public submitCMake(request: Record<string, any>) {
|
|
request.allowStoreCodeDebug = this.allowStoreCodeDebug;
|
|
const jsonRequest = JSON.stringify(request);
|
|
if (options.doCache && !request.bypassCache) {
|
|
const cachedResult = this.cache.get(jsonRequest);
|
|
if (cachedResult) {
|
|
return Promise.resolve({
|
|
request: request,
|
|
result: cachedResult,
|
|
localCacheHit: true,
|
|
});
|
|
}
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const compilerId = encodeURIComponent(request.compiler);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: `${this.getBaseUrl()}api/compiler/${compilerId}/cmake`,
|
|
dataType: 'json',
|
|
contentType: 'application/json',
|
|
data: jsonRequest,
|
|
success: result => {
|
|
if (result?.okToCache && options.doCache) {
|
|
this.cache.set(jsonRequest, result);
|
|
}
|
|
resolve({
|
|
request: request,
|
|
result: result,
|
|
localCacheHit: false,
|
|
});
|
|
},
|
|
error: (jqXHR, textStatus, errorThrown) => {
|
|
CompilerService.handleRequestError(request, reject, jqXHR, textStatus, errorThrown);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
public requestPopularArguments(compilerId: string, usedOptions: string) {
|
|
return new Promise((resolve, reject) => {
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: `${this.getBaseUrl()}api/popularArguments/${compilerId}`,
|
|
dataType: 'json',
|
|
data: JSON.stringify({
|
|
usedOptions: usedOptions,
|
|
presplit: false,
|
|
}),
|
|
success: result => {
|
|
resolve({
|
|
request: compilerId,
|
|
result: result,
|
|
localCacheHit: false,
|
|
});
|
|
},
|
|
error: (jqXHR, textStatus, errorThrown) => {
|
|
CompilerService.handleRequestError(compilerId, reject, jqXHR, textStatus, errorThrown);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
public async expandToFiles(source: string): Promise<SourceAndFiles> {
|
|
const includesOrEmbeds = new IncludeDownloads();
|
|
|
|
const includeOrEmbedFind = /^\s*#\s*(include|embed)\s*["<](https?:\/\/[^">]+)[">]/;
|
|
const lines = source.split('\n');
|
|
for (const idx in lines) {
|
|
const line = lines[idx];
|
|
const match = line.match(includeOrEmbedFind);
|
|
if (match) {
|
|
const download = includesOrEmbeds.include(match[2]);
|
|
lines[idx] = line.replace(includeOrEmbedFind, `#${match[1]} "${download.filename}"`);
|
|
}
|
|
}
|
|
|
|
const files: FiledataPair[] = await includesOrEmbeds.allDownloadsAsFileDataPairs();
|
|
|
|
return {
|
|
source: lines.join('\n'),
|
|
files: files,
|
|
};
|
|
}
|
|
|
|
public static getSelectizerOrder() {
|
|
return [{field: '$order'}, {field: '$score'}, {field: 'name'}];
|
|
}
|
|
|
|
public static doesCompilationResultHaveWarnings(result: CompilationResult) {
|
|
// TODO: Types probably need to be updated here
|
|
|
|
const stdout = result.stdout ?? [];
|
|
|
|
const stderr = result.stderr ?? [];
|
|
// TODO: Pass what compiler did this and check if it it's actually skippable
|
|
// Right now we're ignoring outputs that match the input filename
|
|
// Compiler & Executor are capable of giving us the info, but conformance view is not
|
|
if (stdout.length === 1 && stderr.length === 0 && result.inputFilename) {
|
|
// This code is a special case for MSVC which writes the filename to stdout
|
|
// MSVC will use back-slashes, Wine will use forward slashes
|
|
// We could also move this calculation to the server at some point
|
|
const lastSlashPos = _.findLastIndex(result.inputFilename, ch => ch === '\\' || ch === '/');
|
|
return result.inputFilename.substring(lastSlashPos + 1) !== stdout[0].text;
|
|
}
|
|
return stdout.length > 0 || stderr.length > 0;
|
|
}
|
|
|
|
public static calculateStatusIcon(result: CompilationResult): CompilationStatus {
|
|
let code = 1;
|
|
if (result.code !== 0) {
|
|
code = 3;
|
|
} else if (CompilerService.doesCompilationResultHaveWarnings(result)) {
|
|
code = 2;
|
|
}
|
|
return {code: code, compilerOut: result.code};
|
|
}
|
|
|
|
private static getAriaLabel(status: CompilationStatus) {
|
|
// Compiling...
|
|
if (status.code === 4) return 'Compiling';
|
|
if (status.compilerOut === 0) {
|
|
// StdErr.length > 0
|
|
if (status.code === 3) return 'Compilation succeeded with errors';
|
|
// StdOut.length > 0
|
|
if (status.code === 2) return 'Compilation succeeded with warnings';
|
|
return 'Compilation succeeded';
|
|
}
|
|
// StdErr.length > 0
|
|
if (status.code === 3) return 'Compilation failed with errors';
|
|
// StdOut.length > 0
|
|
if (status.code === 2) return 'Compilation failed with warnings';
|
|
return 'Compilation failed';
|
|
}
|
|
|
|
private static getColor(status: CompilationStatus) {
|
|
// Compiling...
|
|
if (status.code === 4) return '#888888';
|
|
if (status.compilerOut === 0) {
|
|
// StdErr.length > 0
|
|
if (status.code === 3) return '#FF6645';
|
|
// StdOut.length > 0
|
|
if (status.code === 2) return '#FF6500';
|
|
return '#12BB12';
|
|
}
|
|
// StdErr.length > 0
|
|
if (status.code === 3) return '#FF1212';
|
|
// StdOut.length > 0
|
|
if (status.code === 2) return '#BB8700';
|
|
return '#FF6645';
|
|
}
|
|
|
|
public static handleCompilationStatus(
|
|
statusLabel: JQuery | null,
|
|
statusIcon: JQuery | null,
|
|
status: CompilationStatus,
|
|
) {
|
|
if (statusLabel != null) {
|
|
statusLabel.toggleClass('error', status.code === 3).toggleClass('warning', status.code === 2);
|
|
}
|
|
|
|
if (statusIcon != null) {
|
|
statusIcon
|
|
.removeClass()
|
|
.addClass('status-icon fas')
|
|
.css('color', CompilerService.getColor(status))
|
|
.toggle(status.code !== 0)
|
|
.attr('aria-label', CompilerService.getAriaLabel(status))
|
|
.toggleClass('fa-spinner fa-spin', status.code === 4)
|
|
.toggleClass('fa-times-circle', status.code === 3)
|
|
.toggleClass('fa-check-circle', status.code === 1 || status.code === 2);
|
|
}
|
|
}
|
|
|
|
public static handleOutputButtonTitle(element: JQuery, result: CompilationResult) {
|
|
const stdout = result.stdout ?? [];
|
|
|
|
const stderr = result.stderr ?? [];
|
|
|
|
function filterAsciiColors(line: ResultLine) {
|
|
return line.text.replace(ASCII_COLORS_RE, '');
|
|
}
|
|
|
|
const output = stdout.map(filterAsciiColors).concat(stderr.map(filterAsciiColors)).join('\n');
|
|
|
|
element.prop('title', output);
|
|
}
|
|
}
|