mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-09-11 15:54:09 -04:00
A CMake build runs cmake and then the build tool, but nothing in the UI
said what those steps actually were: the compiler pane's "All compilation
options" popover reports the per-file compile line, and the build system's
own invocations were invisible. When a project build fails in the configure
step, there was no way to see what was run.
Each build step already carried its arguments and exit code, so this adds
what was missing and puts it behind a button:
- `BuildStep.command`: the binary. `doBuildstepAndAddToResult` received
it but only ever stored the arguments, so `--build .` could not be
shown as a cmake invocation.
- `BuildStep.env`: what the step actually ran with. LD_LIBRARY_PATH is
taken from `ldPath` rather than `env`, because exec() overwrites the
inherited value with it before spawning -- reading `env` reports the
server's own, which is empty. PATH and HOME are left out as host state
rather than anything about the build, and empty values are dropped.
Both go through masking, which needed a new entry point. Env values hold
several paths in one string, and `-L/tmp/<prefix>X -Wl,-rpath,...` puts a
temp dir at the end of a token; maskRootdirKeepingAppPrefix() is built for
argv, where neither happens, so it masked one path in LDFLAGS and left
three. maskRootdirsInText() is global and treats a bare temp dir as a path.
The argv entry point is untouched.
The steps are shown in a modal rather than a popover: a command line plus
its environment is more than a popover holds, and a popover that dismisses
on outside mouseup is awkward to copy from. Step status comes from the exit
code alone -- calculateStatusIcon() reads any output as a warning, which
fits a compiler but would paint every successful cmake step orange.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168db5wLwZMU9TtcbN9ZD6v
265 lines
10 KiB
TypeScript
265 lines
10 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.
|
|
|
|
export function isString(x: any): x is string {
|
|
return typeof x === 'string' || x instanceof String;
|
|
}
|
|
|
|
// Object.keys is typed as returning :string[] for some reason
|
|
// This util is for cases where the key is a union of a few possible keys and we
|
|
// want the resulting array properly typed.
|
|
export function keys<K extends string | number | symbol>(o: Partial<Record<K, any>>): K[] {
|
|
return Object.keys(o) as K[];
|
|
}
|
|
|
|
export function unique<V>(arr: V[]): V[] {
|
|
return [...new Set(arr)];
|
|
}
|
|
|
|
export function intersection<V>(a: V[], b: V[]): V[] {
|
|
const B = new Set(b);
|
|
return [...a].filter(item => B.has(item));
|
|
}
|
|
|
|
// arr.filter(x => x !== null) returns a (T | null)[] even though it is a T[]
|
|
// Apparently the idiomatic solution is arr.filter((x): x is T => x !== null), but this is shorter (and the type
|
|
// predicate also isn't type checked so it doesn't seem safe to me)
|
|
export function remove<U, V extends U>(arr: U[], v: V) {
|
|
return arr.filter(item => item !== v) as Exclude<U, V extends null | undefined ? V : never>[];
|
|
}
|
|
|
|
// https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAMwK4DsDGMCWEVysAWwgDdgAeAVQBo4A1OUGYFAEwGc4KA+ACgEMoUAFycA2gF0axEbQCUcAN4AoOKrzAYSKLgFQAdAkwAbJlB6YmBOAF4ucC4TgBCa9bjF5fDgFEQaI0gs5NR0DCBMrBwoSEZGcAA+cKhBhijALHAA-KEiaaRQXBIA3EoAvkpKQf4CwHBoOGzwuiI8jVCYKADmCXDRsbLFFfUojXAgNupEpPyCNH1GsiUA9EtqcAB6mUMN8ACeE-hTwDNQNABECBAQZ4tKK2ubFUA
|
|
|
|
// For Array.prototype.sort
|
|
export function basic_comparator<T>(a: T, b: T) {
|
|
if (a < b) {
|
|
return -1;
|
|
}
|
|
if (a > b) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// https://stackoverflow.com/questions/41253310/typescript-retrieve-element-type-information-from-array-type
|
|
export type ElementType<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer T)[] ? T : never;
|
|
|
|
const EscapeMap = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'`': '`',
|
|
};
|
|
const EscapeRE = new RegExp(`(?:${Object.keys(EscapeMap).join('|')})`, 'g');
|
|
|
|
export function escapeHTML(text: string) {
|
|
return text.replace(EscapeRE, str => EscapeMap[str as keyof typeof EscapeMap]);
|
|
}
|
|
|
|
function splitIntoChunks(s: string, chunkSize: number): string[] {
|
|
const chunks: string[] = [];
|
|
const isNegative = s.slice(0, 1) === '-';
|
|
if (isNegative) {
|
|
s = s.slice(1);
|
|
}
|
|
const firstChunkLength = s.length % chunkSize;
|
|
if (firstChunkLength !== 0) {
|
|
chunks.push(s.slice(0, firstChunkLength));
|
|
}
|
|
for (let i = firstChunkLength; i < s.length; i += chunkSize) {
|
|
chunks.push(s.slice(i, i + chunkSize));
|
|
}
|
|
if (isNegative) {
|
|
chunks[0] = '-' + (chunks[0] ?? '');
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
export function addDigitSeparator(n: string, digitSeparator: string, chunkSize: number): string {
|
|
return splitIntoChunks(n, chunkSize).join(digitSeparator);
|
|
}
|
|
|
|
class ArgumentParser {
|
|
private rest: string;
|
|
private position: number;
|
|
private inDoubleQuotes: boolean;
|
|
private readonly results: string[];
|
|
|
|
constructor(str: string) {
|
|
this.rest = str.trim();
|
|
this.position = 0;
|
|
this.inDoubleQuotes = false;
|
|
this.results = [];
|
|
}
|
|
|
|
private addResult(result: string) {
|
|
if (this.results[this.position] === undefined) {
|
|
this.results[this.position] = result;
|
|
} else {
|
|
this.results[this.position] += result;
|
|
}
|
|
}
|
|
|
|
private handleEscapeCharacter() {
|
|
if (this.rest.length < 2) {
|
|
this.addResult('\\');
|
|
} else {
|
|
if (this.rest.charAt(1) !== '\n') {
|
|
this.addResult(this.rest.charAt(1));
|
|
}
|
|
}
|
|
this.rest = this.rest.slice(2);
|
|
}
|
|
|
|
private handleSingleQuotes() {
|
|
let quotePos = this.rest.indexOf("'", 1);
|
|
if (quotePos === -1) quotePos = this.rest.length;
|
|
this.addResult(this.rest.slice(1, quotePos));
|
|
this.rest = this.rest.slice(quotePos + 1);
|
|
}
|
|
|
|
private handleDoubleQuotes() {
|
|
const match = /["\\]/.exec(this.rest);
|
|
const matchEnd = match ? match.index : this.rest.length;
|
|
|
|
this.addResult(this.rest.slice(0, matchEnd));
|
|
|
|
if (match && match[0] === '\\') {
|
|
this.rest = this.rest.slice(matchEnd);
|
|
this.handleEscapeCharacter();
|
|
} else {
|
|
this.rest = this.rest.slice(matchEnd + 1);
|
|
this.inDoubleQuotes = false;
|
|
}
|
|
}
|
|
|
|
private handleWhitespace() {
|
|
const wsMatch = /^\s+/.exec(this.rest);
|
|
if (wsMatch) {
|
|
this.rest = this.rest.slice(wsMatch[0].length);
|
|
++this.position;
|
|
}
|
|
}
|
|
|
|
private handleNonDoubleQuote(nextMatch: RegExpExecArray) {
|
|
this.addResult(this.rest.slice(0, nextMatch.index));
|
|
this.rest = this.rest.slice(nextMatch.index);
|
|
|
|
switch (nextMatch[0]) {
|
|
case '\\':
|
|
this.handleEscapeCharacter();
|
|
break;
|
|
case "'":
|
|
this.handleSingleQuotes();
|
|
break;
|
|
case '"':
|
|
this.rest = this.rest.slice(1);
|
|
this.inDoubleQuotes = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
public exec(): string[] {
|
|
while (this.rest) {
|
|
if (this.inDoubleQuotes) {
|
|
this.handleDoubleQuotes();
|
|
continue;
|
|
}
|
|
|
|
this.handleWhitespace();
|
|
|
|
const nextMatch = /[\s'"\\]/.exec(this.rest);
|
|
if (!nextMatch) {
|
|
this.addResult(this.rest);
|
|
this.rest = '';
|
|
continue;
|
|
}
|
|
|
|
this.handleNonDoubleQuote(nextMatch);
|
|
}
|
|
return this.results;
|
|
}
|
|
}
|
|
|
|
export function splitArguments(str = ''): string[] {
|
|
return new ArgumentParser(str).exec();
|
|
}
|
|
|
|
export function capitaliseFirst(str: string): string {
|
|
if (str.length === 0) return str;
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
export const ce_temp_prefix = 'compiler-explorer-compiler';
|
|
|
|
// Matches everything up to and through a CE temp dir, `.../<ce_temp_prefix><suffix>/`.
|
|
// We key off the ce_temp_prefix marker, NOT the live os.tmpdir(): the temp root varies
|
|
// by host/config (macOS /var vs /private/var, an execution.tempDirRoot elsewhere) and
|
|
// the path being masked may have been recorded under a different tmpdir than this one.
|
|
// The marker is the only invariant — it's the same constant we create the dir with.
|
|
// `(?:[A-Za-z]:)?` + `/` handles Windows too; `[^/\s]+` confines the match to one
|
|
// non-empty path token so an embedded `-I/tmp/<prefix>XXX/inc` still masks to
|
|
// `-I/app/inc` (real temp paths never have empty `//` segments).
|
|
// A user path that itself contains a `<ce_temp_prefix>...` segment would be masked too,
|
|
// but that only affects displayed output (never what's compiled/executed) and needs a
|
|
// deliberately odd dir name, so it's not worth a costlier scheme to prevent.
|
|
const TEMPDIR_RE = new RegExp(`(?:[A-Za-z]:)?/(?:[^/\\s]+/)*${ce_temp_prefix}[\\w.-]*/`);
|
|
|
|
/**
|
|
* Rewrites a CE temp dir down to `/app/`, and stops there. That is the path the compiler
|
|
* is really given, and what `__FILE__` and friends expand to, so anything that shows the
|
|
* user a command line has to keep the prefix. Use maskRootdir() for output lines instead.
|
|
*/
|
|
export function maskRootdirKeepingAppPrefix(filepath: string): string {
|
|
// TODO: this falsy guard is load-bearing against runtime `undefined` that the types
|
|
// don't catch — not declared `string | undefined` callers (TS would reject those),
|
|
// but type holes: a JSON.parse(...) result typed `any` (base-compiler cleanup) and a
|
|
// Record<number,string> index that's really `undefined` when the key is missing
|
|
// (noUncheckedIndexedAccess is off). Tighten those two sites, then this can go.
|
|
if (!filepath) return filepath;
|
|
// TEMPDIR_RE has a repeated path-segment group that backtracks on long input, and this
|
|
// runs on every output line. Gate it behind a cheap linear substring check: the regex
|
|
// cannot match without the marker anyway.
|
|
return filepath.includes(ce_temp_prefix) ? filepath.replace(TEMPDIR_RE, '/app/') : filepath;
|
|
}
|
|
|
|
// As TEMPDIR_RE, but global, and the trailing slash is optional so a bare temp dir counts as a path. Both
|
|
// differences only matter for free-form text: an argv entry holds one path and the temp dir is always the
|
|
// prefix of a file within it, neither of which holds for something like `-L/tmp/<prefix>X -Wl,-rpath,...`.
|
|
const TEMPDIR_TEXT_RE = new RegExp(`(?:[A-Za-z]:)?/(?:[^/\\s]+/)*${ce_temp_prefix}[\\w.-]*(/?)`, 'g');
|
|
|
|
/**
|
|
* Rewrites every CE temp dir in a string that may hold several paths — an environment value such as
|
|
* LDFLAGS, rather than a single argument. Use maskRootdirKeepingAppPrefix() for one path on its own.
|
|
*/
|
|
export function maskRootdirsInText(text: string): string {
|
|
if (!text) return text;
|
|
// Same cheap guard as above: the regex cannot match without the marker, and it backtracks on long input.
|
|
return text.includes(ce_temp_prefix) ? text.replace(TEMPDIR_TEXT_RE, '/app$1') : text;
|
|
}
|