Repository: compiler-explorer/compiler-explorer
Pull request #6467 (2024-05-11 04:59:23)

Detect local label assignments as directives on MIPS (#6392)
For obscure reasons (see [comment here](https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/config/mips/mips.h;h=9d965966f2fa9b16f8320b628a6885666aa6cf8a;hb=refs/heads/trunk#l2948)) GCC MIPS inserts DWARF debug labels with the format `$LXX = .` instead of `$LXX:`. So this updates the regexes in that scenario using the existing MIPS detection, to explicitly consider that specific pattern as a directive instead of an assignment. That way they are filtered correctly in the assembly output with the "Directives" option like on all other architectures.

This shouldn't need any additional tests, just updating the existing ones to say those labels should now be filtered. Currently it causes test failures though, I couldn't find any testing docs or if there is an automatic way to regenerate the test files? If there is a recommended way, I can update them.

partouf: I would like to some caution in terms of the tests failing:
* I don't think https://github.com/compiler-explorer/compiler-explorer/actions/runs/9041257536/job/24878097174?pr=6467#step:5:121 it would be appropriate to hide the label here (all filters disabled), the labels are used for address and size calculations stored in data
* same here https://github.com/compiler-explorer/compiler-explorer/actions/runs/9041257536/job/24878097174?pr=6467#step:5:204

The rest of the failing tests it seems Ok to hide the labels.

We've had a bit of shuffling around as the tests are concerned, but you can run `npx vitest test/filter-tests.ts --update` (according to https://github.com/compiler-explorer/compiler-explorer/pull/6366)

jf2048: Good point :smiley: I managed to fix those tests and find a much simpler solution. Instead of messing about with directives I made it so `symbol = .` is now detected as a label in all cases. Note that will also cause those lines to be filtered if someone inserts that as inline assembly, but that should be consistent with how unused `symbol:` lines are handled too if those are placed in inline assembly.

Patch:
diff --git a/app.ts b/app.ts
index b8bad7711..92ba44195 100755
--- a/app.ts
+++ b/app.ts
@@ -576,6 +576,7 @@ async function main() {
         compileHandler,
         clientOptionsHandler,
         storageHandler,
+        compilationEnvironment,
         ceProps,
         opts,
         defArgs,
diff --git a/etc/config/ada.amazon.properties b/etc/config/ada.amazon.properties
index a0d42681a..56529bdc5 100644
--- a/etc/config/ada.amazon.properties
+++ b/etc/config/ada.amazon.properties
@@ -85,7 +85,7 @@ compiler.gnatloongarch641410.demangler=/opt/compiler-explorer/loongarch64/gcc-14
 
 ################################
 # GNAT for sparc-leon
-group.gnatsparcleons.compilers=gnatsparcleon1310:gnatsparcleon1320:gnatsparcleon1330
+group.gnatsparcleons.compilers=gnatsparcleon1310:gnatsparcleon1320:gnatsparcleon1330:gnatsparcleon1410
 group.gnatsparcleons.groupName=SPARC LEON GNAT
 group.gnatsparcleons.baseName=sparc leon gnat
 
@@ -104,6 +104,11 @@ compiler.gnatsparcleon1330.semver=13.3.0
 compiler.gnatsparcleon1330.objdumper=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
 compiler.gnatsparcleon1330.demangler=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
 
+compiler.gnatsparcleon1410.exe=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-gnatmake
+compiler.gnatsparcleon1410.semver=14.1.0
+compiler.gnatsparcleon1410.objdumper=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
+compiler.gnatsparcleon1410.demangler=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
+
 ################################
 # GNAT for sparc
 group.gnatsparcs.compilers=gnatsparc1220:gnatsparc1230:gnatsparc1310:gnatsparc1320:gnatsparc1330:gnatsparc1410
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index b50e55709..bafce754e 100644
--- a/etc/config/c++.amazon.properties
+++ b/etc/config/c++.amazon.properties
@@ -35,26 +35,35 @@ compiler.g346.exe=/opt/compiler-explorer/gcc-renovated-3.4.6/bin/g++
 compiler.g346.semver=3.4.6
 compiler.g404.exe=/opt/compiler-explorer/gcc-renovated-4.0.4/bin/g++
 compiler.g404.semver=4.0.4
+compiler.g404.supportsBinary=false
+compiler.g404.supportsExecute=false
 compiler.g412.exe=/opt/compiler-explorer/gcc-4.1.2/bin/g++
 compiler.g412.semver=4.1.2
 compiler.g412.supportsBinary=false
+compiler.g412.supportsExecute=false
 compiler.g447.exe=/opt/compiler-explorer/gcc-4.4.7/bin/g++
 compiler.g447.alias=/usr/bin/g++-4.4
 compiler.g447.semver=4.4.7
 compiler.g447.supportsBinary=false
+compiler.g447.supportsExecute=false
 compiler.g453.exe=/opt/compiler-explorer/gcc-4.5.3/bin/g++
 compiler.g453.alias=/usr/bin/g++-4.5
 compiler.g453.semver=4.5.3
+compiler.g453.supportsExecute=false
 compiler.g453.supportsBinary=false
 compiler.g464.exe=/opt/compiler-explorer/gcc-4.6.4/bin/g++
 compiler.g464.alias=/usr/bin/g++-4.6
 compiler.g464.semver=4.6.4
+compiler.g464.supportsBinary=false
+compiler.g464.supportsExecute=false
 compiler.g471.exe=/opt/compiler-explorer/gcc-4.7.1/bin/g++
 compiler.g471.semver=4.7.1
 compiler.g471.supportsBinary=false
+compiler.g471.supportsExecute=false
 compiler.g472.exe=/opt/compiler-explorer/gcc-4.7.2/bin/g++
 compiler.g472.semver=4.7.2
 compiler.g472.supportsBinary=false
+compiler.g472.supportsExecute=false
 compiler.g473.exe=/opt/compiler-explorer/gcc-4.7.3/bin/g++
 compiler.g473.semver=4.7.3
 compiler.g473.alias=/usr/bin/g++-4.7
@@ -1491,7 +1500,7 @@ compiler.sparc64g1410.demangler=/opt/compiler-explorer/sparc64/gcc-14.1.0/sparc6
 group.sparcleon.compilers=&gccsparcleon
 
 # GCC for SPARC-LEON
-group.gccsparcleon.compilers=sparcleong1220:sparcleong1220-1:sparcleong1230:sparcleong1310:sparcleong1320:sparcleong1330
+group.gccsparcleon.compilers=sparcleong1220:sparcleong1220-1:sparcleong1230:sparcleong1310:sparcleong1320:sparcleong1330:sparcleong1410
 group.gccsparcleon.baseName=SPARC LEON gcc
 group.gccsparcleon.groupName=SPARC LEON GCC
 group.gccsparcleon.isSemVer=true
@@ -1523,6 +1532,11 @@ compiler.sparcleong1330.semver=13.3.0
 compiler.sparcleong1330.objdumper=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
 compiler.sparcleong1330.demangler=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
 
+compiler.sparcleong1410.exe=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-g++
+compiler.sparcleong1410.semver=14.1.0
+compiler.sparcleong1410.objdumper=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
+compiler.sparcleong1410.demangler=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
+
 compiler.sparcleong1220-1.exe=/opt/compiler-explorer/sparc-leon/gcc-12.2.0-1/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-g++
 compiler.sparcleong1220-1.semver=12.2.0
 compiler.sparcleong1220-1.objdumper=/opt/compiler-explorer/sparc-leon/gcc-12.2.0-1/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
diff --git a/etc/config/c.amazon.properties b/etc/config/c.amazon.properties
index 2ce868941..a76c59816 100644
--- a/etc/config/c.amazon.properties
+++ b/etc/config/c.amazon.properties
@@ -28,21 +28,37 @@ group.cgcc86.compilerCategories=gcc
 
 compiler.cg346.exe=/opt/compiler-explorer/gcc-renovated-3.4.6/bin/gcc
 compiler.cg346.semver=3.4.6
+compiler.cg346.supportsBinary=false
+compiler.cg346.supportsExecute=false
 compiler.cg404.exe=/opt/compiler-explorer/gcc-renovated-4.0.4/bin/gcc
 compiler.cg404.semver=4.0.4
+compiler.cg404.supportsBinary=false
+compiler.cg404.supportsExecute=false
 compiler.cg412.exe=/opt/compiler-explorer/gcc-4.1.2/bin/gcc
 compiler.cg412.semver=4.1.2
+compiler.cg412.supportsBinary=false
+compiler.cg412.supportsExecute=false
 compiler.cg447.exe=/opt/compiler-explorer/gcc-4.4.7/bin/gcc
 compiler.cg447.semver=4.4.7
+compiler.cg447.supportsBinary=false
+compiler.cg447.supportsExecute=false
 compiler.cg453.exe=/opt/compiler-explorer/gcc-4.5.3/bin/gcc
 compiler.cg453.semver=4.5.3
+compiler.cg453.supportsBinary=false
+compiler.cg453.supportsExecute=false
 compiler.cg464.exe=/opt/compiler-explorer/gcc-4.6.4/bin/gcc
 compiler.cg464.alias=/usr/bin/gcc-4.6
 compiler.cg464.semver=4.6.4
+compiler.cg464.supportsBinary=false
+compiler.cg464.supportsExecute=false
 compiler.cg471.exe=/opt/compiler-explorer/gcc-4.7.1/bin/gcc
 compiler.cg471.semver=4.7.1
+compiler.cg471.supportsBinary=false
+compiler.cg471.supportsExecute=false
 compiler.cg472.exe=/opt/compiler-explorer/gcc-4.7.2/bin/gcc
 compiler.cg472.semver=4.7.2
+compiler.cg472.supportsBinary=false
+compiler.cg472.supportsExecute=false
 compiler.cg473.exe=/opt/compiler-explorer/gcc-4.7.3/bin/gcc
 compiler.cg473.semver=4.7.3
 compiler.cg474.exe=/opt/compiler-explorer/gcc-4.7.4/bin/gcc
@@ -1176,7 +1192,7 @@ compiler.csparc64g1410.demangler=/opt/compiler-explorer/sparc64/gcc-14.1.0/sparc
 group.csparcleon.compilers=&cgccsparcleon
 
 # GCC for SPARC-LEON
-group.cgccsparcleon.compilers=csparcleong1220:csparcleong1220-1:csparcleong1230:csparcleong1310:csparcleong1320:csparcleong1330
+group.cgccsparcleon.compilers=csparcleong1220:csparcleong1220-1:csparcleong1230:csparcleong1310:csparcleong1320:csparcleong1330:csparcleong1410
 group.cgccsparcleon.baseName=SPARC LEON gcc
 group.cgccsparcleon.groupName=SPARC LEON GCC
 group.cgccsparcleon.isSemVer=true
@@ -1208,6 +1224,11 @@ compiler.csparcleong1330.semver=13.3.0
 compiler.csparcleong1330.objdumper=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
 compiler.csparcleong1330.demangler=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
 
+compiler.csparcleong1410.exe=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-gcc
+compiler.csparcleong1410.semver=14.1.0
+compiler.csparcleong1410.objdumper=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
+compiler.csparcleong1410.demangler=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
+
 compiler.csparcleong1220-1.exe=/opt/compiler-explorer/sparc-leon/gcc-12.2.0-1/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-gcc
 compiler.csparcleong1220-1.semver=12.2.0
 compiler.csparcleong1220-1.objdumper=/opt/compiler-explorer/sparc-leon/gcc-12.2.0-1/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
diff --git a/etc/config/compiler-explorer.defaults.properties b/etc/config/compiler-explorer.defaults.properties
index 5b21122c7..249be9508 100644
--- a/etc/config/compiler-explorer.defaults.properties
+++ b/etc/config/compiler-explorer.defaults.properties
@@ -61,3 +61,6 @@ delayCleanupTemp=false
 
 thirdPartyIntegrationEnabled=true
 statusTrackingEnabled=true
+
+# adds endpoint '/localexecution/:hash' for testing of remote execution
+localexecutionEndpoint=false
diff --git a/etc/config/csharp.defaults.properties b/etc/config/csharp.defaults.properties
index b95857689..9ef85774d 100644
--- a/etc/config/csharp.defaults.properties
+++ b/etc/config/csharp.defaults.properties
@@ -3,6 +3,7 @@ supportsBinary=true
 needsMulti=false
 compilerType=csharp
 defaultCompiler=dotnet707csharp
+executionEnvironmentClass=local-dotnet
 
 group.csharp.compilers=dotnettrunkcsharp:dotnet707csharp:dotnet703csharp:dotnet701csharp:dotnet6018csharp:dotnet6014csharp:dotnet6011csharp
 
diff --git a/etc/config/fortran.amazon.properties b/etc/config/fortran.amazon.properties
index 6190f70af..7b78bbae8 100644
--- a/etc/config/fortran.amazon.properties
+++ b/etc/config/fortran.amazon.properties
@@ -335,7 +335,7 @@ compiler.fsparc64g1410.demangler=/opt/compiler-explorer/sparc64/gcc-14.1.0/sparc
 
 ###############################
 # GCC for SPARC LEON
-group.gccsparcleon.compilers=fsparcleong1220:fsparcleong1220-1:fsparcleong1230:fsparcleong1310:fsparcleong1320:fsparcleong1330
+group.gccsparcleon.compilers=fsparcleong1220:fsparcleong1220-1:fsparcleong1230:fsparcleong1310:fsparcleong1320:fsparcleong1330:fsparcleong1410
 group.gccsparcleon.groupName=SPARC LEON gfortran
 group.gccsparcleon.baseName=SPARC LEON gfortran
 
@@ -371,6 +371,11 @@ compiler.fsparcleong1330.semver=13.3.0
 compiler.fsparcleong1330.objdumper=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
 compiler.fsparcleong1330.demangler=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
 
+compiler.fsparcleong1410.exe=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-gfortran
+compiler.fsparcleong1410.semver=14.1.0
+compiler.fsparcleong1410.objdumper=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
+compiler.fsparcleong1410.demangler=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
+
 ###############################
 # GCC for LOONGARCH64
 group.gccloongarch64.compilers=floongarch64g1220:floongarch64g1230:floongarch64g1310:floongarch64g1320:floongarch64g1410:floongarch64g1330
diff --git a/etc/config/fsharp.defaults.properties b/etc/config/fsharp.defaults.properties
index bfed0718d..76281fff2 100644
--- a/etc/config/fsharp.defaults.properties
+++ b/etc/config/fsharp.defaults.properties
@@ -3,6 +3,7 @@ supportsBinary=true
 needsMulti=false
 compilerType=fsharp
 defaultCompiler=dotnet707fsharp
+executionEnvironmentClass=local-dotnet
 
 group.fsharp.compilers=dotnettrunkfsharp:dotnet707fsharp:dotnet703fsharp:dotnet701fsharp:dotnet6018fsharp:dotnet6014fsharp:dotnet6011fsharp
 
diff --git a/etc/config/objc.amazon.properties b/etc/config/objc.amazon.properties
index 46db5a770..75cd63f12 100644
--- a/etc/config/objc.amazon.properties
+++ b/etc/config/objc.amazon.properties
@@ -157,7 +157,7 @@ compiler.objcsparc64g1410.demangler=/opt/compiler-explorer/sparc64/gcc-14.1.0/sp
 group.objcsparcleon.compilers=&objcgccsparcleon
 
 # GCC for SPARC-LEON
-group.objcgccsparcleon.compilers=objcsparcleong1220:objcsparcleong1220-1:objcsparcleong1230:objcsparcleong1310:objcsparcleong1320:objcsparcleong1330
+group.objcgccsparcleon.compilers=objcsparcleong1220:objcsparcleong1220-1:objcsparcleong1230:objcsparcleong1310:objcsparcleong1320:objcsparcleong1330:objcsparcleong1410
 group.objcgccsparcleon.supportsBinary=true
 group.objcgccsparcleon.supportsExecute=false
 group.objcgccsparcleon.baseName=SPARC LEON gcc
@@ -191,6 +191,11 @@ compiler.objcsparcleong1330.semver=13.3.0
 compiler.objcsparcleong1330.objdumper=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
 compiler.objcsparcleong1330.demangler=/opt/compiler-explorer/sparc-leon/gcc-13.3.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
 
+compiler.objcsparcleong1410.exe=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-gcc
+compiler.objcsparcleong1410.semver=14.1.0
+compiler.objcsparcleong1410.objdumper=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
+compiler.objcsparcleong1410.demangler=/opt/compiler-explorer/sparc-leon/gcc-14.1.0/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-c++filt
+
 compiler.objcsparcleong1220-1.exe=/opt/compiler-explorer/sparc-leon/gcc-12.2.0-1/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-gcc
 compiler.objcsparcleong1220-1.semver=12.2.0
 compiler.objcsparcleong1220-1.objdumper=/opt/compiler-explorer/sparc-leon/gcc-12.2.0-1/sparc-leon-linux-uclibc/bin/sparc-leon-linux-uclibc-objdump
diff --git a/etc/config/vb.defaults.properties b/etc/config/vb.defaults.properties
index 046f11bbc..2db923c4b 100644
--- a/etc/config/vb.defaults.properties
+++ b/etc/config/vb.defaults.properties
@@ -3,6 +3,7 @@ supportsBinary=true
 needsMulti=false
 compilerType=vb
 defaultCompiler=dotnet707vb
+executionEnvironmentClass=local-dotnet
 
 group.vb.compilers=dotnettrunkvb:dotnet707vb:dotnet703vb:dotnet701vb:dotnet6018vb:dotnet6014vb:dotnet6011vb
 
diff --git a/lib/artifact-utils.ts b/lib/artifact-utils.ts
new file mode 100644
index 000000000..06585837c
--- /dev/null
+++ b/lib/artifact-utils.ts
@@ -0,0 +1,84 @@
+// Copyright (c) 2024, 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 path from 'path';
+
+import fs from 'fs-extra';
+
+import {BufferOkFunc, BuildResult, CompilationResult} from '../types/compilation/compilation.interfaces.js';
+import {Artifact, ArtifactType} from '../types/tool.interfaces.js';
+
+import {HeaptrackWrapper} from './runtime-tools/heaptrack-wrapper.js';
+import * as utils from './utils.js';
+
+export async function addArtifactToResult(
+    result: CompilationResult,
+    filepath: string,
+    customType?: string,
+    customTitle?: string,
+    checkFunc?: BufferOkFunc,
+): Promise<void> {
+    const file_buffer = await fs.readFile(filepath);
+
+    if (checkFunc && !checkFunc(file_buffer)) return;
+
+    const artifact: Artifact = {
+        content: file_buffer.toString('base64'),
+        type: customType || 'application/octet-stream',
+        name: path.basename(filepath),
+        title: customTitle || path.basename(filepath),
+    };
+
+    if (!result.artifacts) result.artifacts = [];
+
+    result.artifacts.push(artifact);
+}
+
+export async function addHeaptrackResults(result: CompilationResult, dirPath?: string): Promise<void> {
+    let dirPathToUse: string = '';
+    if (dirPath) {
+        dirPathToUse = dirPath;
+    } else if (result.buildResult && result.buildResult.dirPath) {
+        dirPathToUse = result.buildResult.dirPath;
+    }
+
+    if (dirPathToUse === '') return;
+
+    const flamegraphFilepath = path.join(dirPathToUse, HeaptrackWrapper.FlamegraphFilename);
+    if (await utils.fileExists(flamegraphFilepath)) {
+        await addArtifactToResult(result, flamegraphFilepath, ArtifactType.heaptracktxt, 'Heaptrack results');
+    }
+}
+
+export function moveArtifactsIntoResult(movefrom: BuildResult, moveto: CompilationResult): CompilationResult {
+    if (movefrom.artifacts && movefrom.artifacts.length > 0) {
+        if (!moveto.artifacts) {
+            moveto.artifacts = [];
+        }
+        moveto.artifacts.push(...movefrom.artifacts);
+        delete movefrom.artifacts;
+    }
+
+    return moveto;
+}
diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts
index d05c76bbd..41019e2f4 100644
--- a/lib/base-compiler.ts
+++ b/lib/base-compiler.ts
@@ -34,7 +34,6 @@ import _ from 'underscore';
 import {unique} from '../shared/common-utils.js';
 import {ParsedAsmResultLine} from '../types/asmresult/asmresult.interfaces.js';
 import {
-    BufferOkFunc,
     BuildResult,
     BuildStep,
     BypassCache,
@@ -64,8 +63,6 @@ import type {
 import type {CompilerInfo, ICompiler, PreliminaryCompilerInfo} from '../types/compiler.interfaces.js';
 import {
     BasicExecutionResult,
-    ConfiguredRuntimeTool,
-    ConfiguredRuntimeTools,
     ExecutableExecutionOptions,
     RuntimeToolType,
     UnprocessedExecResult,
@@ -75,8 +72,9 @@ import {InstructionSet} from '../types/instructionsets.js';
 import type {Language} from '../types/languages.interfaces.js';
 import type {Library, LibraryVersion, SelectedLibraryVersion} from '../types/libraries/libraries.interfaces.js';
 import type {ResultLine} from '../types/resultline/resultline.interfaces.js';
-import {type Artifact, ArtifactType, type ToolResult, type ToolTypeKey} from '../types/tool.interfaces.js';
+import {type ToolResult, type ToolTypeKey} from '../types/tool.interfaces.js';
 
+import {moveArtifactsIntoResult} from './artifact-utils.js';
 import {unwrap} from './assert.js';
 import type {BuildEnvDownloadInfo} from './buildenvsetup/buildenv.interfaces.js';
 import {BuildEnvSetupBase, getBuildEnvTypeByKey} from './buildenvsetup/index.js';
@@ -87,6 +85,8 @@ import {ClangCParser, ClangParser, GCCCParser, GCCParser, ICCParser} from './com
 import {BaseDemangler, getDemanglerTypeByKey} from './demangler/index.js';
 import {LLVMIRDemangler} from './demangler/llvm.js';
 import * as exec from './exec.js';
+import {IExecutionEnvironment} from './execution/execution-env.interfaces.js';
+import {getExecutionEnvironmentByKey} from './execution/index.js';
 import {ExternalParserBase} from './external-parsers/base.js';
 import {getExternalParserByKey} from './external-parsers/index.js';
 import {InstructionSets} from './instructionsets.js';
@@ -196,6 +196,7 @@ export class BaseCompiler implements ICompiler {
         help: 'Time spent on objdump and parsing of objdumps',
         labelNames: [],
     });
+    protected executionEnvironmentClass: any;
 
     constructor(compilerInfo: PreliminaryCompilerInfo & {disabledFilters?: string[]}, env: CompilationEnvironment) {
         // Information about our compiler
@@ -208,7 +209,7 @@ export class BaseCompiler implements ICompiler {
         this.compileFilename = `example${this.lang.extensions[0]}`;
         this.env = env;
         // Partial application of compilerProps with the proper language id applied to it
-        this.compilerProps = _.partial(this.env.compilerProps as any, this.lang.id);
+        this.compilerProps = this.env.getCompilerPropsForLanguage(this.lang.id);
         this.compiler.supportsIntel = !!this.compiler.intelAsm;
 
         this.alwaysResetLdPath = this.env.ceProps('alwaysResetLdPath');
@@ -217,6 +218,9 @@ export class BaseCompiler implements ICompiler {
         this.stubText = this.compilerProps('stubText', '');
         this.compilerWrapper = this.compilerProps('compiler-wrapper');
 
+        const executionEnvironmentClassStr = this.compilerProps<string>('executionEnvironmentClass', 'local');
+        this.executionEnvironmentClass = getExecutionEnvironmentByKey(executionEnvironmentClassStr);
+
         if (!this.compiler.options) this.compiler.options = '';
         if (!this.compiler.optArg) this.compiler.optArg = '';
         if (!this.compiler.supportsOptOutput) this.compiler.supportsOptOutput = false;
@@ -637,47 +641,6 @@ export class BaseCompiler implements ICompiler {
         return result;
     }
 
-    processExecutionResult(input: UnprocessedExecResult, inputFilename?: string): BasicExecutionResult {
-        const start = performance.now();
-        const stdout = utils.parseOutput(input.stdout, inputFilename);
-        const stderr = utils.parseOutput(input.stderr, inputFilename);
-        const end = performance.now();
-        return {
-            ...input,
-            stdout,
-            stderr,
-            processExecutionResultTime: end - start,
-        };
-    }
-
-    processUserExecutableExecutionResult(
-        input: UnprocessedExecResult,
-        stdErrlineParseOptions: utils.LineParseOptions,
-    ): BasicExecutionResult {
-        const start = performance.now();
-        const stdout = utils.parseOutput(input.stdout, undefined, undefined, []);
-        const stderr = utils.parseOutput(input.stderr, undefined, undefined, stdErrlineParseOptions);
-        const end = performance.now();
-        return {
-            ...input,
-            stdout,
-            stderr,
-            processExecutionResultTime: end - start,
-        };
-    }
-
-    getEmptyExecutionResult(): BasicExecutionResult {
-        return {
-            code: -1,
-            okToCache: false,
-            filenameTransform: x => x,
-            stdout: [],
-            stderr: [],
-            execTime: '',
-            timedOut: false,
-        };
-    }
-
     transformToCompilationResult(input: UnprocessedExecResult, inputFilename): CompilationResult {
         const transformedInput = input.filenameTransform(inputFilename);
 
@@ -688,99 +651,6 @@ export class BaseCompiler implements ICompiler {
         };
     }
 
-    protected setEnvironmentVariablesFromRuntime(
-        configuredTools: ConfiguredRuntimeTools,
-        execOptions: ExecutionOptions,
-    ) {
-        for (const runtime of configuredTools) {
-            if (runtime.name === RuntimeToolType.env) {
-                for (const env of runtime.options) {
-                    if (!execOptions.env) execOptions.env = {};
-
-                    execOptions.env[env.name] = env.value;
-                }
-            }
-        }
-    }
-
-    protected async execBinaryMaybeWrapped(
-        executable: string,
-        args: string[],
-        execOptions: ExecutionOptions,
-        executeParameters: ExecutableExecutionOptions,
-        homeDir: string,
-    ): Promise<BasicExecutionResult> {
-        let runWithHeaptrack: ConfiguredRuntimeTool | undefined = undefined;
-
-        if (!execOptions.env) execOptions.env = {};
-
-        if (executeParameters.runtimeTools) {
-            this.setEnvironmentVariablesFromRuntime(executeParameters.runtimeTools, execOptions);
-
-            for (const runtime of executeParameters.runtimeTools) {
-                if (runtime.name === RuntimeToolType.heaptrack) {
-                    runWithHeaptrack = runtime;
-                }
-            }
-        }
-
-        if (runWithHeaptrack && HeaptrackWrapper.isSupported(this.env)) {
-            const wrapper = new HeaptrackWrapper(
-                homeDir,
-                exec.sandbox,
-                this.exec,
-                runWithHeaptrack.options,
-                this.env.ceProps,
-                this.sandboxType,
-            );
-            const execResult: UnprocessedExecResult = await wrapper.exec(executable, args, execOptions);
-            return this.processUserExecutableExecutionResult(execResult, [utils.LineParseOption.AtFileLine]);
-        } else {
-            const execResult: UnprocessedExecResult = await exec.sandbox(executable, args, execOptions);
-            return this.processUserExecutableExecutionResult(execResult, []);
-        }
-    }
-
-    async execBinary(
-        executable: string,
-        maxSize: number,
-        executeParameters: ExecutableExecutionOptions,
-        homeDir: string,
-    ): Promise<BasicExecutionResult> {
-        // We might want to save this in the compilation environment once execution is made available
-        const timeoutMs = this.env.ceProps('binaryExecTimeoutMs', 2000);
-        try {
-            const execOptions: ExecutionOptions = {
-                maxOutput: maxSize,
-                timeoutMs: timeoutMs,
-                ldPath: _.union(this.compiler.ldPath, executeParameters.ldPath),
-                input: executeParameters.stdin,
-                env: executeParameters.env,
-                customCwd: homeDir,
-                appHome: homeDir,
-            };
-
-            return this.execBinaryMaybeWrapped(
-                executable,
-                executeParameters.args,
-                execOptions,
-                executeParameters,
-                homeDir,
-            );
-        } catch (err: UnprocessedExecResult | any) {
-            if (err.code && err.stderr) {
-                return this.processExecutionResult(err);
-            } else {
-                return {
-                    ...this.getEmptyExecutionResult(),
-                    stdout: err.stdout ? utils.parseOutput(err.stdout) : [],
-                    stderr: err.stderr ? utils.parseOutput(err.stderr) : [],
-                    code: err.code === undefined ? -1 : err.code,
-                };
-            }
-        }
-    }
-
     protected filename(fn) {
         return fn;
     }
@@ -1864,29 +1734,6 @@ export class BaseCompiler implements ICompiler {
         }
     }
 
-    async addArtifactToResult(
-        result: CompilationResult,
-        filepath: string,
-        customType?: string,
-        customTitle?: string,
-        checkFunc?: BufferOkFunc,
-    ) {
-        const file_buffer = await fs.readFile(filepath);
-
-        if (checkFunc && !checkFunc(file_buffer)) return;
-
-        const artifact: Artifact = {
-            content: file_buffer.toString('base64'),
-            type: customType || 'application/octet-stream',
-            name: path.basename(filepath),
-            title: customTitle || path.basename(filepath),
-        };
-
-        if (!result.artifacts) result.artifacts = [];
-
-        result.artifacts.push(artifact);
-    }
-
     protected async writeMultipleFiles(files: any[], dirPath: string) {
         const filesToWrite: Promise<void>[] = [];
 
@@ -1989,7 +1836,7 @@ export class BaseCompiler implements ICompiler {
             if (buildResults) return buildResults;
         }
 
-        let compilationResult;
+        let compilationResult: BuildResult;
         try {
             compilationResult = await this.buildExecutableInFolder(key, dirPath);
             if (compilationResult.code !== 0) {
@@ -1999,6 +1846,12 @@ export class BaseCompiler implements ICompiler {
             return this.handleUserError(e, dirPath);
         }
 
+        compilationResult.preparedLdPaths = _.union(
+            this.compiler.ldPath,
+            this.getSharedLibraryPathsAsLdLibraryPathsForExecution(key.libraries, dirPath),
+        );
+        compilationResult.defaultExecOptions = this.getDefaultExecOptions();
+
         await this.storePackageWithExecutable(key, dirPath, compilationResult);
 
         if (!compilationResult.dirPath) {
@@ -2048,7 +1901,8 @@ export class BaseCompiler implements ICompiler {
         try {
             await fs.writeFile(path.join(dirPath, compilationResultFilename), JSON.stringify(compilationResult));
             await this.packager.package(dirPath, packagedFile);
-            await this.env.executablePut(key, packagedFile);
+            const hash = await this.env.executablePut(key, packagedFile);
+            logger.debug('Storing ' + hash);
         } catch (err) {
             logger.error('Caught an error trying to put to cache: ', {err});
         } finally {
@@ -2056,27 +1910,26 @@ export class BaseCompiler implements ICompiler {
         }
     }
 
-    runExecutable(executable: string, executeParameters: ExecutableExecutionOptions, homeDir) {
-        const maxExecOutputSize = this.env.ceProps('max-executable-output-size', 32 * 1024);
+    protected processExecutionResult(input: UnprocessedExecResult, inputFilename?: string): BasicExecutionResult {
+        return utils.processExecutionResult(input, inputFilename);
+    }
 
+    async runExecutable(
+        executable: string,
+        executeParameters: ExecutableExecutionOptions,
+        homeDir,
+    ): Promise<BasicExecutionResult> {
         const execOptionsCopy: ExecutableExecutionOptions = JSON.parse(
             JSON.stringify(executeParameters),
         ) as ExecutableExecutionOptions;
 
-        // Hardcoded fix for #2339. Ideally I'd have a config option for this, but for now this is plenty good enough.
-        execOptionsCopy.env = {
-            ASAN_OPTIONS: 'color=always',
-            UBSAN_OPTIONS: 'color=always',
-            MSAN_OPTIONS: 'color=always',
-            LSAN_OPTIONS: 'color=always',
-            ...executeParameters.env,
-        };
-
         if (this.compiler.executionWrapper) {
             execOptionsCopy.args = [...this.compiler.executionWrapperArgs, executable, ...execOptionsCopy.args];
             executable = this.compiler.executionWrapper;
         }
-        return this.execBinary(executable, maxExecOutputSize, execOptionsCopy, homeDir);
+
+        const execEnv: IExecutionEnvironment = new this.executionEnvironmentClass(this.env);
+        return execEnv.execBinary(executable, execOptionsCopy, homeDir);
     }
 
     protected fixExecuteParametersForInterpreting(executeParameters, outputFilename, key) {
@@ -2160,46 +2013,23 @@ export class BaseCompiler implements ICompiler {
             };
         }
 
-        executeParameters.ldPath = this.getSharedLibraryPathsAsLdLibraryPathsForExecution(
-            key.libraries,
-            buildResult.dirPath,
-        );
+        if (buildResult.preparedLdPaths) {
+            executeParameters.ldPath = buildResult.preparedLdPaths;
+        } else {
+            executeParameters.ldPath = this.getSharedLibraryPathsAsLdLibraryPathsForExecution(
+                key.libraries,
+                buildResult.dirPath,
+            );
+        }
+
         const result = await this.runExecutable(buildResult.executableFilename, executeParameters, buildResult.dirPath);
-        return this.moveArtifactsIntoResult(buildResult, {
+        return moveArtifactsIntoResult(buildResult, {
             ...result,
             didExecute: true,
             buildResult: buildResult,
         });
     }
 
-    moveArtifactsIntoResult(movefrom: BuildResult, moveto: CompilationResult): CompilationResult {
-        if (movefrom.artifacts && movefrom.artifacts.length > 0) {
-            if (!moveto.artifacts) {
-                moveto.artifacts = [];
-            }
-            moveto.artifacts.push(...movefrom.artifacts);
-            delete movefrom.artifacts;
-        }
-
-        return moveto;
-    }
-
-    async addHeaptrackResults(result: CompilationResult, dirPath?: string) {
-        let dirPathToUse: string = '';
-        if (dirPath) {
-            dirPathToUse = dirPath;
-        } else if (result.buildResult && result.buildResult.dirPath) {
-            dirPathToUse = result.buildResult.dirPath;
-        }
-
-        if (dirPathToUse === '') return;
-
-        const flamegraphFilepath = path.join(dirPathToUse, HeaptrackWrapper.FlamegraphFilename);
-        if (await utils.fileExists(flamegraphFilepath)) {
-            await this.addArtifactToResult(result, flamegraphFilepath, ArtifactType.heaptracktxt, 'Heaptrack results');
-        }
-    }
-
     async handleExecution(
         key,
         executeParameters: ExecutableExecutionOptions,
@@ -2216,14 +2046,6 @@ export class BaseCompiler implements ICompiler {
 
         const result = await this.doExecution(key, executeParameters, bypassCache);
 
-        if (executeParameters.runtimeTools) {
-            for (const runtime of executeParameters.runtimeTools) {
-                if (runtime.name === RuntimeToolType.heaptrack) {
-                    await this.addHeaptrackResults(result);
-                }
-            }
-        }
-
         if (!bypassExecutionCache(bypassCache)) {
             await this.env.cachePut(execKey, result, undefined);
         }
@@ -2523,11 +2345,12 @@ export class BaseCompiler implements ICompiler {
         return this.processExecutionResult(result);
     }
 
-    handleUserError(error, dirPath) {
+    handleUserError(error, dirPath): CompilationResult {
         return {
             dirPath,
             okToCache: false,
             code: -1,
+            timedOut: false,
             asm: [{text: `<${error.message}>`}],
             stdout: [],
             stderr: [{text: `<${error.message}>`}],
@@ -2611,7 +2434,7 @@ export class BaseCompiler implements ICompiler {
         }
     }
 
-    async cmake(files, key, bypassCache: BypassCache) {
+    async cmake(files, key, bypassCache: BypassCache): Promise<CompilationResult> {
         // key = {source, options, backendOptions, filters, bypassCache, tools, executeParameters, libraries};
 
         if (!this.compiler.supportsBinary) {
@@ -2651,11 +2474,11 @@ export class BaseCompiler implements ICompiler {
 
         const outputFilename = this.getExecutableFilename(path.join(dirPath, 'build'), this.outputFilebase, key);
 
-        let fullResult = bypassExecutionCache(bypassCache)
+        let fullResult: CompilationResult = bypassExecutionCache(bypassCache)
             ? null
             : await this.loadPackageWithExecutable(cacheKey, dirPath);
         if (fullResult) {
-            fullResult.fetchedFromCache = true;
+            fullResult.retreivedFromCache = true;
 
             delete fullResult.inputFilename;
             delete fullResult.executableFilename;
@@ -2677,6 +2500,10 @@ export class BaseCompiler implements ICompiler {
             const makeExecParams = this.createCmakeExecParams(execParams, dirPath, libsAndOptions, toolchainPath);
 
             fullResult = {
+                code: 0,
+                timedOut: false,
+                stdout: [],
+                stderr: [],
                 buildsteps: [],
                 inputFilename: writeSummary.inputFilename,
             };
@@ -2703,6 +2530,9 @@ export class BaseCompiler implements ICompiler {
             if (cmakeStepResult.code !== 0) {
                 fullResult.result = {
                     dirPath,
+                    timedOut: false,
+                    stdout: [],
+                    stderr: [],
                     okToCache: false,
                     code: cmakeStepResult.code,
                     asm: [{text: '<Build failed>'}],
@@ -2722,6 +2552,9 @@ export class BaseCompiler implements ICompiler {
             if (makeStepResult.code !== 0) {
                 fullResult.result = {
                     dirPath,
+                    timedOut: false,
+                    stdout: [],
+                    stderr: [],
                     okToCache: false,
                     code: makeStepResult.code,
                     asm: [{text: '<Build failed>'}],
@@ -2731,7 +2564,12 @@ export class BaseCompiler implements ICompiler {
 
             fullResult.result = {
                 dirPath,
+                code: 0,
+                timedOut: false,
+                stdout: [],
+                stderr: [],
                 okToCache: true,
+                compilationOptions: this.getUsedEnvironmentVariableFlags(makeExecParams),
             };
 
             if (!key.backendOptions.skipAsm) {
@@ -2743,29 +2581,21 @@ export class BaseCompiler implements ICompiler {
                 fullResult.result = asmResult;
             }
 
-            fullResult.result.compilationOptions = this.getUsedEnvironmentVariableFlags(makeExecParams);
-
             fullResult.code = 0;
-            _.each(fullResult.buildsteps, function (step) {
-                fullResult.code += step.code;
-            });
+            if (fullResult.buildsteps) {
+                _.each(fullResult.buildsteps, function (step) {
+                    fullResult.code += step.code;
+                });
+            }
 
             await this.storePackageWithExecutable(cacheKey, dirPath, fullResult);
         }
 
-        fullResult.result.dirPath = dirPath;
+        if (fullResult.result) fullResult.result.dirPath = dirPath;
 
         if (this.compiler.supportsExecute && doExecute) {
             fullResult.execResult = await this.runExecutable(outputFilename, executeOptions, dirPath);
             fullResult.didExecute = true;
-
-            if (executeOptions.runtimeTools) {
-                for (const runtime of executeOptions.runtimeTools) {
-                    if (runtime.name === RuntimeToolType.heaptrack) {
-                        await this.addHeaptrackResults(fullResult, dirPath);
-                    }
-                }
-            }
         }
 
         const optOutput = undefined;
@@ -2785,7 +2615,7 @@ export class BaseCompiler implements ICompiler {
             path.join(dirPath, 'build'),
         );
 
-        delete fullResult.result.dirPath;
+        if (fullResult.result) delete fullResult.result.dirPath;
 
         this.cleanupResult(fullResult);
 
@@ -2833,10 +2663,10 @@ export class BaseCompiler implements ICompiler {
     }
 
     async compile(
-        source,
-        options,
-        backendOptions,
-        filters,
+        source: string,
+        options: string[],
+        backendOptions: Record<string, any>,
+        filters: ParseFiltersAndOutputOptions,
         bypassCache: BypassCache,
         tools,
         executeParameters,
diff --git a/lib/compilation-env.ts b/lib/compilation-env.ts
index 40b90e57a..9b16ab0b2 100644
--- a/lib/compilation-env.ts
+++ b/lib/compilation-env.ts
@@ -41,6 +41,8 @@ import type {PropertyGetter} from './properties.interfaces.js';
 import {CompilerProps} from './properties.js';
 import {createStatsNoter, IStatsNoter} from './stats.js';
 
+type PropFunc = (string, any?) => any;
+
 export class CompilationEnvironment {
     ceProps: PropertyGetter;
     compilationQueue: CompilationQueue;
@@ -156,7 +158,7 @@ export class CompilationEnvironment {
         return this.compilerCache.put(key, JSON.stringify(result), creator);
     }
 
-    async executableGet(object: CacheableValue, destinationFolder: string) {
+    async executableGet(object: CacheableValue, destinationFolder: string): Promise<string | null> {
         const key = BaseCache.hash(object) + '_exec';
         const result = await this.executableCache.get(key);
         if (!result.hit) return null;
@@ -165,9 +167,10 @@ export class CompilationEnvironment {
         return filepath;
     }
 
-    executablePut(object: CacheableValue, filepath: string) {
+    async executablePut(object: CacheableValue, filepath: string): Promise<string> {
         const key = BaseCache.hash(object) + '_exec';
-        return this.executableCache.put(key, fs.readFileSync(filepath));
+        await this.executableCache.put(key, fs.readFileSync(filepath));
+        return key;
     }
 
     enqueue<T>(job: Job<T>, options?: EnqueueOptions) {
@@ -177,4 +180,8 @@ export class CompilationEnvironment {
     findBadOptions(options: string[]) {
         return options.filter(option => !this.okOptions.test(option) || this.badOptions.test(option));
     }
+
+    getCompilerPropsForLanguage(languageId: string): PropFunc {
+        return _.partial(this.compilerProps as any, languageId);
+    }
 }
diff --git a/lib/compilers/beebasm.ts b/lib/compilers/beebasm.ts
index b1d5a2aea..de7edcb75 100644
--- a/lib/compilers/beebasm.ts
+++ b/lib/compilers/beebasm.ts
@@ -29,6 +29,7 @@ import fs from 'fs-extra';
 import type {ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import {ArtifactType} from '../../types/tool.interfaces.js';
+import {addArtifactToResult} from '../artifact-utils.js';
 import {BaseCompiler} from '../base-compiler.js';
 import {AsmParserBeebAsm} from '../parsers/asm-parser-beebasm.js';
 import * as utils from '../utils.js';
@@ -84,7 +85,7 @@ export class BeebAsmCompiler extends BaseCompiler {
         if (result.code === 0 && options.includes('-v')) {
             const diskfile = path.join(dirPath, 'disk.ssd');
             if (await utils.fileExists(diskfile)) {
-                await this.addArtifactToResult(result, diskfile, ArtifactType.bbcdiskimage);
+                await addArtifactToResult(result, diskfile, ArtifactType.bbcdiskimage);
 
                 if (!hasBootOption) {
                     if (!result.hints) result.hints = [];
diff --git a/lib/compilers/carbon.ts b/lib/compilers/carbon.ts
index 65a69efd7..6e090cebe 100644
--- a/lib/compilers/carbon.ts
+++ b/lib/compilers/carbon.ts
@@ -84,6 +84,7 @@ export class CarbonCompiler extends BaseCompiler {
                 stderr: [],
                 code: code,
                 didExecute: true,
+                timedOut: false,
                 buildResult: {
                     code: 0,
                     timedOut: false,
diff --git a/lib/compilers/cc65.ts b/lib/compilers/cc65.ts
index 64102fcd6..b53eae372 100644
--- a/lib/compilers/cc65.ts
+++ b/lib/compilers/cc65.ts
@@ -31,6 +31,7 @@ import type {CompilationResult} from '../../types/compilation/compilation.interf
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {ArtifactType} from '../../types/tool.interfaces.js';
+import {addArtifactToResult} from '../artifact-utils.js';
 import {BaseCompiler} from '../base-compiler.js';
 import {CC65AsmParser} from '../parsers/asm-parser-cc65.js';
 import * as utils from '../utils.js';
@@ -118,14 +119,14 @@ export class Cc65Compiler extends BaseCompiler {
         const dirPath = path.dirname(outputFilename);
         const nesFile = path.join(dirPath, 'example.nes');
         if (await utils.fileExists(nesFile)) {
-            await this.addArtifactToResult(res, nesFile, ArtifactType.nesrom);
+            await addArtifactToResult(res, nesFile, ArtifactType.nesrom);
         }
 
         if (result.compilationOptions?.includes('c64') && (await utils.fileExists(outputFilename))) {
             if (outputFilename.endsWith('.prg')) {
-                await this.addArtifactToResult(res, outputFilename, ArtifactType.c64prg);
+                await addArtifactToResult(res, outputFilename, ArtifactType.c64prg);
             } else {
-                await this.addArtifactToResult(
+                await addArtifactToResult(
                     res,
                     outputFilename,
                     ArtifactType.c64prg,
diff --git a/lib/compilers/clang.ts b/lib/compilers/clang.ts
index ffb83d1c6..e69f298f7 100644
--- a/lib/compilers/clang.ts
+++ b/lib/compilers/clang.ts
@@ -37,6 +37,7 @@ import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ExecutableExecutionOptions, UnprocessedExecResult} from '../../types/execution/execution.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {ArtifactType} from '../../types/tool.interfaces.js';
+import {addArtifactToResult} from '../artifact-utils.js';
 import {BaseCompiler} from '../base-compiler.js';
 import {CompilationEnvironment} from '../compilation-env.js';
 import {AmdgpuAsmParser} from '../parsers/asm-parser-amdgpu.js';
@@ -108,15 +109,9 @@ export class ClangCompiler extends BaseCompiler {
         }
 
         if (jsonFilepath) {
-            this.addArtifactToResult(
-                result,
-                jsonFilepath,
-                ArtifactType.timetrace,
-                'Trace events JSON',
-                (buffer: Buffer) => {
-                    return buffer.toString('utf8').startsWith('{"traceEvents":[');
-                },
-            );
+            addArtifactToResult(result, jsonFilepath, ArtifactType.timetrace, 'Trace events JSON', (buffer: Buffer) => {
+                return buffer.toString('utf8').startsWith('{"traceEvents":[');
+            });
         }
     }
 
diff --git a/lib/compilers/dotnet.ts b/lib/compilers/dotnet.ts
index e6494938d..cfe284304 100644
--- a/lib/compilers/dotnet.ts
+++ b/lib/compilers/dotnet.ts
@@ -25,20 +25,15 @@
 import path from 'path';
 
 import fs from 'fs-extra';
-import _ from 'underscore';
 
 import type {CompilationResult, ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
-import {
-    type BasicExecutionResult,
-    type ExecutableExecutionOptions,
-} from '../../types/execution/execution.interfaces.js';
+import {ExecutableExecutionOptions} from '../../types/execution/execution.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {BaseCompiler} from '../base-compiler.js';
+import {AssemblyName, DotnetExtraConfiguration} from '../execution/dotnet-execution-env.js';
+import {IExecutionEnvironment} from '../execution/execution-env.interfaces.js';
 import {DotNetAsmParser} from '../parsers/asm-parser-dotnet.js';
-import * as utils from '../utils.js';
-
-const AssemblyName = 'CompilerExplorer';
 
 class DotNetCompiler extends BaseCompiler {
     private readonly sdkBaseDir: string;
@@ -429,43 +424,6 @@ class DotNetCompiler extends BaseCompiler {
         return this.compilerOptions;
     }
 
-    override async execBinary(
-        executable: string,
-        maxSize: number,
-        executeParameters: ExecutableExecutionOptions,
-        homeDir: string,
-    ): Promise<BasicExecutionResult> {
-        const programDir = path.dirname(executable);
-        const programOutputPath = path.join(programDir, 'bin', this.buildConfig, this.targetFramework);
-        const programDllPath = path.join(programOutputPath, `${AssemblyName}.dll`);
-        const execOptions = this.getDefaultExecOptions();
-        execOptions.maxOutput = maxSize;
-        execOptions.timeoutMs = this.env.ceProps('binaryExecTimeoutMs', 2000);
-        execOptions.ldPath = _.union(this.compiler.ldPath, executeParameters.ldPath);
-        execOptions.customCwd = homeDir;
-        execOptions.appHome = homeDir;
-        execOptions.env = executeParameters.env;
-        execOptions.env.DOTNET_EnableWriteXorExecute = '0';
-        execOptions.env.DOTNET_CLI_HOME = programDir;
-        execOptions.env.CORE_ROOT = this.clrBuildDir;
-        execOptions.input = executeParameters.stdin;
-        const execArgs = ['-p', 'System.Runtime.TieredCompilation=false', programDllPath, ...executeParameters.args];
-        try {
-            return this.execBinaryMaybeWrapped(this.corerunPath, execArgs, execOptions, executeParameters, homeDir);
-        } catch (err: any) {
-            if (err.code && err.stderr) {
-                return this.processExecutionResult(err);
-            } else {
-                return {
-                    ...this.getEmptyExecutionResult(),
-                    stdout: err.stdout ? utils.parseOutput(err.stdout) : [],
-                    stderr: err.stderr ? utils.parseOutput(err.stderr) : [],
-                    code: err.code === undefined ? -1 : err.code,
-                };
-            }
-        }
-    }
-
     async getRuntimeVersion() {
         const versionFilePath = `${this.clrBuildDir}/version.txt`;
         if (fs.existsSync(versionFilePath)) {
@@ -532,6 +490,28 @@ class DotNetCompiler extends BaseCompiler {
 
         return result;
     }
+
+    override runExecutable(executable: string, executeParameters: ExecutableExecutionOptions, homeDir) {
+        const execOptionsCopy: ExecutableExecutionOptions = JSON.parse(
+            JSON.stringify(executeParameters),
+        ) as ExecutableExecutionOptions;
+
+        if (this.compiler.executionWrapper) {
+            execOptionsCopy.args = [...this.compiler.executionWrapperArgs, executable, ...execOptionsCopy.args];
+            executable = this.compiler.executionWrapper;
+        }
+
+        const extraConfiguration: DotnetExtraConfiguration = {
+            buildConfig: this.buildConfig,
+            clrBuildDir: this.clrBuildDir,
+            langVersion: this.langVersion,
+            targetFramework: this.targetFramework,
+            corerunPath: this.corerunPath,
+        };
+
+        const execEnv: IExecutionEnvironment = new this.executionEnvironmentClass(this.env);
+        return execEnv.execBinary(executable, execOptionsCopy, homeDir, extraConfiguration);
+    }
 }
 
 export class CSharpCompiler extends DotNetCompiler {
diff --git a/lib/compilers/llvm-mos.ts b/lib/compilers/llvm-mos.ts
index 026a0fe45..6d412155e 100644
--- a/lib/compilers/llvm-mos.ts
+++ b/lib/compilers/llvm-mos.ts
@@ -28,6 +28,7 @@ import type {CompilationResult} from '../../types/compilation/compilation.interf
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {ArtifactType} from '../../types/tool.interfaces.js';
+import {addArtifactToResult} from '../artifact-utils.js';
 import * as utils from '../utils.js';
 
 import {ClangCompiler} from './clang.js';
@@ -88,7 +89,7 @@ export class LLVMMOSCompiler extends ClangCompiler {
             }
 
             if (await utils.fileExists(nesFile)) {
-                await this.addArtifactToResult(res, nesFile, ArtifactType.nesrom);
+                await addArtifactToResult(res, nesFile, ArtifactType.nesrom);
             }
         } else if (this.compiler.exe.includes('c64')) {
             let prgFile = outputFilename;
@@ -97,7 +98,7 @@ export class LLVMMOSCompiler extends ClangCompiler {
             }
 
             if (await utils.fileExists(prgFile)) {
-                await this.addArtifactToResult(res, prgFile, ArtifactType.c64prg);
+                await addArtifactToResult(res, prgFile, ArtifactType.c64prg);
             }
         }
 
diff --git a/lib/compilers/madpascal.ts b/lib/compilers/madpascal.ts
index 249ac3041..8ab3171d0 100644
--- a/lib/compilers/madpascal.ts
+++ b/lib/compilers/madpascal.ts
@@ -30,6 +30,7 @@ import {CompilationResult, ExecutionOptions} from '../../types/compilation/compi
 import {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {ArtifactType} from '../../types/tool.interfaces.js';
+import {addArtifactToResult} from '../artifact-utils.js';
 import {BaseCompiler, c_value_placeholder} from '../base-compiler.js';
 import {CompilationEnvironment} from '../compilation-env.js';
 import {MadsAsmParser} from '../parsers/asm-parser-mads.js';
@@ -144,7 +145,7 @@ export class MadPascalCompiler extends BaseCompiler {
             if (assemblerResult.code === 0 && this.isTargettingC64(options)) {
                 const diskfile = path.join(tmpDir, 'output.obx');
                 if (await utils.fileExists(diskfile)) {
-                    await this.addArtifactToResult(result, diskfile, ArtifactType.c64prg, 'output.prg');
+                    await addArtifactToResult(result, diskfile, ArtifactType.c64prg, 'output.prg');
                 }
             }
 
diff --git a/lib/compilers/z88dk.ts b/lib/compilers/z88dk.ts
index 7997859d5..cdd8f0c73 100644
--- a/lib/compilers/z88dk.ts
+++ b/lib/compilers/z88dk.ts
@@ -28,6 +28,7 @@ import type {ExecutionOptions} from '../../types/compilation/compilation.interfa
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
 import {ArtifactType} from '../../types/tool.interfaces.js';
+import {addArtifactToResult} from '../artifact-utils.js';
 import {BaseCompiler} from '../base-compiler.js';
 import {logger} from '../logger.js';
 import {AsmParserZ88dk} from '../parsers/asm-parser-z88dk.js';
@@ -182,12 +183,12 @@ export class z88dkCompiler extends BaseCompiler {
         if (result.code === 0 && filters.binary) {
             const tapeFilepath = path.join(result.dirPath, this.getTapefilename());
             if (await utils.fileExists(tapeFilepath)) {
-                await this.addArtifactToResult(result, tapeFilepath, ArtifactType.zxtape);
+                await addArtifactToResult(result, tapeFilepath, ArtifactType.zxtape);
             }
 
             const smsFilepath = path.join(result.dirPath, this.getSmsfilename());
             if (await utils.fileExists(smsFilepath)) {
-                await this.addArtifactToResult(result, smsFilepath, ArtifactType.smsrom);
+                await addArtifactToResult(result, smsFilepath, ArtifactType.smsrom);
             }
         }
 
diff --git a/lib/execution/_all.ts b/lib/execution/_all.ts
new file mode 100644
index 000000000..3c44a7538
--- /dev/null
+++ b/lib/execution/_all.ts
@@ -0,0 +1,26 @@
+// Copyright (c) 2024, 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 {LocalExecutionEnvironment} from './base-execution-env.js';
+export {DotnetExecutionEnvironment} from './dotnet-execution-env.js';
diff --git a/lib/execution/base-execution-env.ts b/lib/execution/base-execution-env.ts
new file mode 100644
index 000000000..1b4b11435
--- /dev/null
+++ b/lib/execution/base-execution-env.ts
@@ -0,0 +1,311 @@
+// Copyright (c) 2024, 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 os from 'os';
+import path from 'path';
+
+import fs from 'fs-extra';
+import temp from 'temp';
+
+import {BuildResult, ExecutionOptions, ExecutionParams} from '../../types/compilation/compilation.interfaces.js';
+import {
+    BasicExecutionResult,
+    ConfiguredRuntimeTool,
+    ConfiguredRuntimeTools,
+    ExecutableExecutionOptions,
+    RuntimeToolType,
+    UnprocessedExecResult,
+} from '../../types/execution/execution.interfaces.js';
+import {addHeaptrackResults} from '../artifact-utils.js';
+import {assert, unwrap} from '../assert.js';
+import {CompilationEnvironment} from '../compilation-env.js';
+import * as exec from '../exec.js';
+import {logger} from '../logger.js';
+import {Packager} from '../packager.js';
+import {propsFor} from '../properties.js';
+import {HeaptrackWrapper} from '../runtime-tools/heaptrack-wrapper.js';
+import * as utils from '../utils.js';
+
+import {ExecutablePackageCacheMiss, IExecutionEnvironment} from './execution-env.interfaces.js';
+
+export class LocalExecutionEnvironment implements IExecutionEnvironment {
+    protected packager: Packager;
+    protected dirPath: string;
+    protected buildResult: BuildResult | undefined;
+    protected environment: CompilationEnvironment;
+    protected timeoutMs: number;
+    protected sandboxType: string;
+    protected useSanitizerEnvHints: boolean;
+    protected maxExecOutputSize: number;
+
+    static get key() {
+        return 'local';
+    }
+
+    constructor(environment: CompilationEnvironment) {
+        this.environment = environment;
+        this.timeoutMs = this.environment.ceProps('binaryExecTimeoutMs', 2000);
+        this.maxExecOutputSize = this.environment.ceProps('max-executable-output-size', 32 * 1024);
+
+        this.useSanitizerEnvHints = true;
+
+        const execProps = propsFor('execution');
+        this.sandboxType = execProps('sandboxType', 'none');
+
+        this.packager = new Packager();
+        this.dirPath = 'not initialized';
+    }
+
+    protected async executableGet(hash: string, destinationFolder: string) {
+        const result = await this.environment.executableCache.get(hash);
+        if (!result.hit) return null;
+        const filepath = destinationFolder + '/' + hash;
+        await fs.writeFile(filepath, unwrap(result.data));
+        return filepath;
+    }
+
+    protected async loadPackageWithExecutable(hash: string, dirPath: string): Promise<BuildResult> {
+        const compilationResultFilename = 'compilation-result.json';
+        const startTime = process.hrtime.bigint();
+        const outputFilename = await this.executableGet(hash, dirPath);
+        if (outputFilename) {
+            logger.debug(`Using cached package ${outputFilename}`);
+            await this.packager.unpack(outputFilename, dirPath);
+            const buildResultsBuf = await fs.readFile(path.join(dirPath, compilationResultFilename));
+            const buildResults = JSON.parse(buildResultsBuf.toString('utf8'));
+            // logger.info(hash + ' => ' + JSON.stringify(buildResults));
+            const endTime = process.hrtime.bigint();
+
+            let inputFilename = '';
+            if (buildResults.inputFilename) {
+                inputFilename = path.join(dirPath, path.basename(buildResults.inputFilename));
+            }
+
+            let executableFilename = '';
+            if (buildResults.executableFilename) {
+                const execPath = utils.maskRootdir(buildResults.executableFilename);
+                executableFilename = path.join(dirPath, execPath);
+            }
+
+            return Object.assign({}, buildResults, {
+                code: 0,
+                inputFilename: inputFilename,
+                dirPath: dirPath,
+                executableFilename: executableFilename,
+                packageDownloadAndUnzipTime: ((endTime - startTime) / BigInt(1000000)).toString(),
+            });
+        } else {
+            throw new ExecutablePackageCacheMiss('Tried to get executable from cache, but got a cache miss');
+        }
+    }
+
+    async downloadExecutablePackage(hash: string): Promise<void> {
+        this.dirPath = await temp.mkdir({prefix: utils.ce_temp_prefix, dir: os.tmpdir()});
+
+        this.buildResult = await this.loadPackageWithExecutable(hash, this.dirPath);
+    }
+
+    protected getDefaultExecOptions(params: ExecutionParams): ExecutionOptions & {env: Record<string, string>} {
+        const env: Record<string, string> = {};
+        env.PATH = '';
+
+        if (params.runtimeTools) {
+            const runtimeEnv = params.runtimeTools.find(tool => tool.name === RuntimeToolType.env);
+            if (runtimeEnv) {
+                for (const opt of runtimeEnv.options) {
+                    env[(opt.name = opt.value)];
+                }
+            }
+        }
+
+        // todo: what to do about the rest of the runtimeTools?
+
+        if (
+            this.buildResult &&
+            this.buildResult.defaultExecOptions &&
+            this.buildResult.defaultExecOptions.env &&
+            this.buildResult.defaultExecOptions.env.PATH
+        ) {
+            if (env.PATH.length > 0)
+                env.PATH = env.PATH + path.delimiter + this.buildResult.defaultExecOptions.env.PATH;
+            else env.PATH = this.buildResult.defaultExecOptions.env.PATH;
+        }
+
+        const execOptions: ExecutionOptions & {env: Record<string, string>} = {
+            env,
+        };
+
+        if (this.buildResult && this.buildResult.preparedLdPaths) {
+            execOptions.ldPath = this.buildResult.preparedLdPaths;
+        }
+
+        return execOptions;
+    }
+
+    async execute(params: ExecutionParams): Promise<BasicExecutionResult> {
+        assert(this.buildResult);
+
+        const execExecutableOptions: ExecutableExecutionOptions = {
+            args: typeof params.args === 'string' ? utils.splitArguments(params.args) : params.args || [],
+            stdin: params.stdin || '',
+            ldPath: this.buildResult.preparedLdPaths || [],
+            env: {},
+            runtimeTools: params.runtimeTools,
+        };
+
+        const homeDir = await temp.mkdir({prefix: utils.ce_temp_prefix, dir: os.tmpdir()});
+
+        return await this.execBinary(this.buildResult.executableFilename, execExecutableOptions, homeDir);
+    }
+
+    protected setEnvironmentVariablesFromRuntime(
+        configuredTools: ConfiguredRuntimeTools,
+        execOptions: ExecutionOptions,
+    ) {
+        for (const runtime of configuredTools) {
+            if (runtime.name === RuntimeToolType.env) {
+                for (const env of runtime.options) {
+                    if (!execOptions.env) execOptions.env = {};
+
+                    execOptions.env[env.name] = env.value;
+                }
+            }
+        }
+    }
+
+    async execBinary(
+        executable: string,
+        executeParameters: ExecutableExecutionOptions,
+        homeDir: string,
+        extraConfiguration?: any,
+    ): Promise<BasicExecutionResult> {
+        try {
+            const execOptions: ExecutionOptions = {
+                maxOutput: this.maxExecOutputSize,
+                timeoutMs: this.timeoutMs,
+                ldPath: [...executeParameters.ldPath],
+                input: executeParameters.stdin,
+                customCwd: homeDir,
+                appHome: homeDir,
+            };
+
+            if (this.useSanitizerEnvHints) {
+                execOptions.env = {
+                    ASAN_OPTIONS: 'color=always',
+                    UBSAN_OPTIONS: 'color=always',
+                    MSAN_OPTIONS: 'color=always',
+                    LSAN_OPTIONS: 'color=always',
+                    ...executeParameters.env,
+                };
+            } else {
+                execOptions.env = {
+                    ...executeParameters.env,
+                };
+            }
+
+            return this.execBinaryMaybeWrapped(
+                executable,
+                executeParameters.args,
+                execOptions,
+                executeParameters,
+                homeDir,
+            );
+        } catch (err: UnprocessedExecResult | any) {
+            if (err.code && err.stderr) {
+                return utils.processExecutionResult(err);
+            } else {
+                return {
+                    ...utils.getEmptyExecutionResult(),
+                    stdout: err.stdout ? utils.parseOutput(err.stdout) : [],
+                    stderr: err.stderr ? utils.parseOutput(err.stderr) : [],
+                    code: err.code === undefined ? -1 : err.code,
+                };
+            }
+        }
+    }
+
+    protected async execBinaryMaybeWrapped(
+        executable: string,
+        args: string[],
+        execOptions: ExecutionOptions,
+        executeParameters: ExecutableExecutionOptions,
+        homeDir: string,
+    ): Promise<BasicExecutionResult> {
+        let runWithHeaptrack: ConfiguredRuntimeTool | undefined = undefined;
+
+        if (!execOptions.env) execOptions.env = {};
+
+        if (executeParameters.runtimeTools) {
+            this.setEnvironmentVariablesFromRuntime(executeParameters.runtimeTools, execOptions);
+
+            for (const runtime of executeParameters.runtimeTools) {
+                if (runtime.name === RuntimeToolType.heaptrack) {
+                    runWithHeaptrack = runtime;
+                }
+            }
+        }
+
+        if (runWithHeaptrack && HeaptrackWrapper.isSupported(this.environment)) {
+            const wrapper = new HeaptrackWrapper(
+                homeDir,
+                exec.sandbox,
+                exec.execute,
+                runWithHeaptrack.options,
+                this.environment.ceProps,
+                this.sandboxType,
+            );
+            const execResult: UnprocessedExecResult = await wrapper.exec(executable, args, execOptions);
+            const processed = this.processUserExecutableExecutionResult(execResult, [utils.LineParseOption.AtFileLine]);
+
+            if (executeParameters.runtimeTools) {
+                for (const runtime of executeParameters.runtimeTools) {
+                    if (runtime.name === RuntimeToolType.heaptrack) {
+                        await addHeaptrackResults(processed, homeDir);
+                    }
+                }
+            }
+
+            return processed;
+        } else {
+            const execResult: UnprocessedExecResult = await exec.sandbox(executable, args, execOptions);
+            return this.processUserExecutableExecutionResult(execResult, []);
+        }
+    }
+
+    processUserExecutableExecutionResult(
+        input: UnprocessedExecResult,
+        stdErrlineParseOptions: utils.LineParseOptions,
+    ): BasicExecutionResult {
+        const start = performance.now();
+        const stdout = utils.parseOutput(input.stdout, undefined, undefined, []);
+        const stderr = utils.parseOutput(input.stderr, undefined, undefined, stdErrlineParseOptions);
+        const end = performance.now();
+        return {
+            ...input,
+            stdout,
+            stderr,
+            processExecutionResultTime: end - start,
+        };
+    }
+}
diff --git a/lib/execution/dotnet-execution-env.ts b/lib/execution/dotnet-execution-env.ts
new file mode 100644
index 000000000..ec5ebe2c8
--- /dev/null
+++ b/lib/execution/dotnet-execution-env.ts
@@ -0,0 +1,94 @@
+// Copyright (c) 2024, 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 path from 'path';
+
+import {BasicExecutionResult, ExecutableExecutionOptions} from '../../types/execution/execution.interfaces.js';
+import * as utils from '../utils.js';
+
+import {LocalExecutionEnvironment} from './base-execution-env.js';
+
+export const AssemblyName = 'CompilerExplorer';
+
+export type DotnetExtraConfiguration = {
+    buildConfig: string;
+    clrBuildDir: string;
+    langVersion: string;
+    targetFramework: string;
+    corerunPath: string;
+};
+
+export class DotnetExecutionEnvironment extends LocalExecutionEnvironment {
+    static override get key() {
+        return 'local-dotnet';
+    }
+
+    override async execBinary(
+        executable: string,
+        executeParameters: ExecutableExecutionOptions,
+        homeDir: string,
+        extraConfiguration: any,
+    ): Promise<BasicExecutionResult> {
+        const programDir = path.dirname(executable);
+        const programOutputPath = path.join(
+            programDir,
+            'bin',
+            extraConfiguration.buildConfig,
+            extraConfiguration.targetFramework,
+        );
+        const programDllPath = path.join(programOutputPath, `${AssemblyName}.dll`);
+        const execOptions = this.getDefaultExecOptions(executeParameters);
+        execOptions.maxOutput = this.maxExecOutputSize;
+        execOptions.timeoutMs = this.timeoutMs;
+        execOptions.ldPath = executeParameters.ldPath;
+        execOptions.customCwd = homeDir;
+        execOptions.appHome = homeDir;
+        execOptions.env = executeParameters.env;
+        execOptions.env.DOTNET_EnableWriteXorExecute = '0';
+        execOptions.env.DOTNET_CLI_HOME = programDir;
+        execOptions.env.CORE_ROOT = extraConfiguration.clrBuildDir;
+        execOptions.input = executeParameters.stdin;
+        const execArgs = ['-p', 'System.Runtime.TieredCompilation=false', programDllPath, ...executeParameters.args];
+        try {
+            return this.execBinaryMaybeWrapped(
+                extraConfiguration.corerunPath,
+                execArgs,
+                execOptions,
+                executeParameters,
+                homeDir,
+            );
+        } catch (err: any) {
+            if (err.code && err.stderr) {
+                return utils.processExecutionResult(err);
+            } else {
+                return {
+                    ...utils.getEmptyExecutionResult(),
+                    stdout: err.stdout ? utils.parseOutput(err.stdout) : [],
+                    stderr: err.stderr ? utils.parseOutput(err.stderr) : [],
+                    code: err.code === undefined ? -1 : err.code,
+                };
+            }
+        }
+    }
+}
diff --git a/lib/execution/execution-env.interfaces.ts b/lib/execution/execution-env.interfaces.ts
new file mode 100644
index 000000000..8e493c03f
--- /dev/null
+++ b/lib/execution/execution-env.interfaces.ts
@@ -0,0 +1,39 @@
+// Copyright (c) 2024, 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 {ExecutionParams} from '../../types/compilation/compilation.interfaces.js';
+import {BasicExecutionResult, ExecutableExecutionOptions} from '../../types/execution/execution.interfaces.js';
+
+export interface IExecutionEnvironment {
+    downloadExecutablePackage(hash: string): Promise<void>;
+    execute(params: ExecutionParams): Promise<BasicExecutionResult>;
+    execBinary(
+        executable: string,
+        executeParameters: ExecutableExecutionOptions,
+        homeDir: string,
+        extraConfiguration?: any,
+    ): Promise<BasicExecutionResult>;
+}
+
+export class ExecutablePackageCacheMiss extends Error {}
diff --git a/lib/execution/index.ts b/lib/execution/index.ts
new file mode 100644
index 000000000..5506f58d5
--- /dev/null
+++ b/lib/execution/index.ts
@@ -0,0 +1,31 @@
+// Copyright (c) 2024, 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 {makeKeyedTypeGetter} from '../keyed-type.js';
+
+import * as all from './_all.js';
+
+export * from './_all.js';
+
+export const getExecutionEnvironmentByKey = makeKeyedTypeGetter('executionEnvironment', all);
diff --git a/lib/handlers/api.ts b/lib/handlers/api.ts
index c64f8c269..1e610b485 100644
--- a/lib/handlers/api.ts
+++ b/lib/handlers/api.ts
@@ -31,6 +31,9 @@ import {CompilerInfo} from '../../types/compiler.interfaces.js';
 import {Language, LanguageKey} from '../../types/languages.interfaces.js';
 import {assert, unwrap} from '../assert.js';
 import {ClientStateNormalizer} from '../clientstate-normalizer.js';
+import {CompilationEnvironment} from '../compilation-env.js';
+import {LocalExecutionEnvironment} from '../execution/base-execution-env.js';
+import {IExecutionEnvironment} from '../execution/execution-env.interfaces.js';
 import {logger} from '../logger.js';
 import {ClientOptionsHandler} from '../options-handler.js';
 import {PropertyGetter} from '../properties.interfaces.js';
@@ -60,14 +63,17 @@ export class ApiHandler {
         gitReleaseName: '',
         releaseBuildNumber: '',
     };
+    private readonly compilationEnvironment: CompilationEnvironment;
 
     constructor(
         compileHandler: CompileHandler,
         ceProps: PropertyGetter,
         private readonly storageHandler: StorageBase,
         urlShortenService: string,
+        compilationEnvironment: CompilationEnvironment,
     ) {
         this.handle = express.Router();
+        this.compilationEnvironment = compilationEnvironment;
         const cacheHeader = `public, max-age=${ceProps('apiMaxAgeSecs', 24 * 60 * 60)}`;
         this.handle.use((req, res, next) => {
             res.header({
@@ -107,6 +113,10 @@ export class ApiHandler {
             .post(compileHandler.handleCmake.bind(compileHandler))
             .all(methodNotAllowed);
 
+        if (this.compilationEnvironment.ceProps('localexecutionEndpoint', false)) {
+            this.handle.route('/localexecution/:hash').post(this.handleLocalExecution.bind(this)).all(methodNotAllowed);
+        }
+
         this.handle
             .route('/popularArguments/:compiler')
             .post(compileHandler.handlePopularArguments.bind(compileHandler))
@@ -277,6 +287,29 @@ export class ApiHandler {
         }
     }
 
+    async handleLocalExecution(req: express.Request, res: express.Response, next: express.NextFunction) {
+        if (!req.params.hash) {
+            next({statusCode: 404, message: 'No hash supplied'});
+            return;
+        }
+
+        if (!req.body.ExecutionParams) {
+            next({statusCode: 404, message: 'No ExecutionParams'});
+            return;
+        }
+
+        try {
+            const env: IExecutionEnvironment = new LocalExecutionEnvironment(this.compilationEnvironment);
+            await env.downloadExecutablePackage(req.params.hash);
+            const execResult = await env.execute(req.body.ExecutionParams);
+            logger.debug('execResult', execResult);
+            res.send(execResult);
+        } catch (e) {
+            logger.error(e);
+            next({statusCode: 500, message: 'Internal error'});
+        }
+    }
+
     handleAllLibraries(req: express.Request, res: express.Response, next: express.NextFunction) {
         if (this.options) {
             res.send(this.options.options.libs);
diff --git a/lib/handlers/route-api.ts b/lib/handlers/route-api.ts
index fb58fe76f..004521946 100644
--- a/lib/handlers/route-api.ts
+++ b/lib/handlers/route-api.ts
@@ -29,6 +29,7 @@ import {isString} from '../../shared/common-utils.js';
 import {assert, unwrap} from '../assert.js';
 import {ClientStateGoldenifier, ClientStateNormalizer} from '../clientstate-normalizer.js';
 import {ClientState} from '../clientstate.js';
+import {CompilationEnvironment} from '../compilation-env.js';
 import {logger} from '../logger.js';
 import {ClientOptionsHandler} from '../options-handler.js';
 import {PropertyGetter} from '../properties.interfaces.js';
@@ -51,6 +52,7 @@ export type HandlerConfig = {
     renderGoldenLayout: any;
     staticHeaders: any;
     contentPolicyHeader: any;
+    compilationEnvironment: CompilationEnvironment;
 };
 
 type ShortLinkMetaData = {
@@ -80,6 +82,7 @@ export class RouteAPI {
                 config.ceProps,
                 config.storageHandler,
                 config.clientOptionsHandler.options.urlShortenService,
+                config.compilationEnvironment,
             );
 
             this.apiHandler.setReleaseInfo(config.defArgs.gitReleaseName, config.defArgs.releaseBuildNumber);
diff --git a/lib/parsers/asm-parser.ts b/lib/parsers/asm-parser.ts
index bcf2f9fb0..e18f7baf1 100644
--- a/lib/parsers/asm-parser.ts
+++ b/lib/parsers/asm-parser.ts
@@ -110,7 +110,7 @@ export class AsmParser extends AsmRegex implements IAsmParser {
         this.definesGlobal = /^\s*\.(?:globa?l|GLB|export)\s*([.A-Z_a-z][\w$.]*)/;
         this.definesWeak = /^\s*\.(?:weakext|weak)\s*([.A-Z_a-z][\w$.]*)/;
         this.indentedLabelDef = /^\s*([$.A-Z_a-z][\w$.]*):/;
-        this.assignmentDef = /^\s*([$.A-Z_a-z][\w$.]*)\s*=/;
+        this.assignmentDef = /^\s*([$.A-Z_a-z][\w$.]*)\s*=\s*(.*)/;
         this.directive = /^\s*\..*$/;
         // These four regexes when phrased as /\s*#APP.*/ etc exhibit costly polynomial backtracking
         // Instead use ^$ and test with regex.test(line.trim()), more robust anyway
@@ -619,7 +619,12 @@ export class AsmParser extends AsmRegex implements IAsmParser {
 
                 // g-as shows local labels as eg: "1:  call  mcount". We characterize such a label as
                 // "the label-matching part doesn't equal the whole line" and treat it as used.
-                if (labelsUsed[match[1]] === undefined && match[0] === line) {
+                // As a special case, consider assignments of the form "symbol = ." to be labels.
+                if (
+                    labelsUsed[match[1]] === undefined &&
+                    match[0] === line &&
+                    (match[2] === undefined || match[2].trim() === '.')
+                ) {
                     // It's an unused label.
                     if (filters.labels) {
                         continue;
diff --git a/lib/utils.ts b/lib/utils.ts
index e8c94d670..39358cf1d 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -34,6 +34,7 @@ import {parse as quoteParse} from 'shell-quote';
 import _ from 'underscore';
 
 import type {CacheableValue} from '../types/cache.interfaces.js';
+import {BasicExecutionResult, UnprocessedExecResult} from '../types/execution/execution.interfaces.js';
 import type {ResultLine} from '../types/resultline/resultline.interfaces.js';
 
 const tabsRe = /\t/g;
@@ -541,3 +542,28 @@ export function asSafeVer(semver: string | number | null | undefined): string {
     }
     return magic_semver.non_trunk;
 }
+
+export function processExecutionResult(input: UnprocessedExecResult, inputFilename?: string): BasicExecutionResult {
+    const start = performance.now();
+    const stdout = parseOutput(input.stdout, inputFilename);
+    const stderr = parseOutput(input.stderr, inputFilename);
+    const end = performance.now();
+    return {
+        ...input,
+        stdout,
+        stderr,
+        processExecutionResultTime: end - start,
+    };
+}
+
+export function getEmptyExecutionResult(): BasicExecutionResult {
+    return {
+        code: -1,
+        okToCache: false,
+        filenameTransform: x => x,
+        stdout: [],
+        stderr: [],
+        execTime: '',
+        timedOut: false,
+    };
+}
diff --git a/static/event-map.ts b/static/event-map.ts
index aea53d43e..a1f8b5a6e 100644
--- a/static/event-map.ts
+++ b/static/event-map.ts
@@ -23,7 +23,6 @@
 // POSSIBILITY OF SUCH DAMAGE.
 
 import {Language} from '../types/languages.interfaces.js';
-import {CompilerOutputOptions} from '../types/features/filters.interfaces.js';
 import {MessageWithLocation} from '../types/resultline/resultline.interfaces.js';
 import {SiteSettings} from './settings.js';
 import {Theme} from './themes.js';
@@ -88,7 +87,6 @@ export type EventMap = {
     ) => void;
     executorClose: (executorId: number) => void;
     executorOpen: (executorId: number, editorId: boolean | number) => void;
-    filtersChange: (compilerId: number, filters: Partial<CompilerOutputOptions>) => void;
     findCompilers: () => void;
     findEditors: () => void;
     findExecutors: () => void;
@@ -144,7 +142,6 @@ export type EventMap = {
     requestTheme: () => void;
     resendCompilation: (compilerId: number) => void;
     requestCompiler: (compilerId: number) => void;
-    requestFilters: (compilerId: number) => void;
     resendExecution: (executorId: number) => void;
     resize: () => void;
     rustHirViewClosed: (compilerId: number) => void;
diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts
index ac8df72f7..b89f88884 100644
--- a/static/panes/compiler.ts
+++ b/static/panes/compiler.ts
@@ -2996,11 +2996,6 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
 
         this.eventHub.on('cfgViewOpened', this.onCfgViewOpened, this);
         this.eventHub.on('cfgViewClosed', this.onCfgViewClosed, this);
-        this.eventHub.on('requestFilters', id => {
-            if (id === this.id) {
-                this.eventHub.emit('filtersChange', this.id, this.getEffectiveFilters());
-            }
-        });
         this.eventHub.on('requestCompiler', id => {
             if (id === this.id) {
                 this.sendCompiler();
@@ -3216,8 +3211,6 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
     }
 
     onFilterChange(): void {
-        const filters = this.getEffectiveFilters();
-        this.eventHub.emit('filtersChange', this.id, filters);
         this.updateState();
         this.compile();
         this.updateButtons();
diff --git a/static/panes/diff.ts b/static/panes/diff.ts
index da0f57711..6ddba55d9 100644
--- a/static/panes/diff.ts
+++ b/static/panes/diff.ts
@@ -74,7 +74,7 @@ class DiffStateObject {
     id?: number | string;
     model: monaco.editor.ITextModel;
     compiler: CompilerEntry | null;
-    result: CompilationResult | null;
+    result?: CompilationResult;
     difftype: DiffType;
     extraoption?: string;
 
@@ -87,7 +87,7 @@ class DiffStateObject {
         this.id = id;
         this.model = model;
         this.compiler = null;
-        this.result = null;
+        this.result = undefined;
         this.difftype = difftype;
         this.extraoption = extraoption;
     }
@@ -115,10 +115,10 @@ class DiffStateObject {
                     output = this.result.stderr;
                     break;
                 case DiffType.ExecStdOut:
-                    if (this.result.execResult) output = this.result.execResult.stdout || [];
+                    if (this.result.execResult) output = this.result.execResult.stdout;
                     break;
                 case DiffType.ExecStdErr:
-                    if (this.result.execResult) output = this.result.execResult.stderr || [];
+                    if (this.result.execResult) output = this.result.execResult.stderr;
                     break;
                 case DiffType.GNAT_ExpandedCode:
                     output = this.result.gnatDebugOutput || [];
diff --git a/static/panes/executor.ts b/static/panes/executor.ts
index a3a536f58..7a8cc76cd 100644
--- a/static/panes/executor.ts
+++ b/static/panes/executor.ts
@@ -559,7 +559,7 @@ export class Executor extends Pane<ExecutorState> {
     }
 
     getExecutionStdoutfromResult(result: CompilationResult): ResultLine[] {
-        if (result.execResult && result.execResult.stdout !== undefined) {
+        if (result.execResult) {
             return result.execResult.stdout;
         }
 
@@ -569,7 +569,7 @@ export class Executor extends Pane<ExecutorState> {
 
     getExecutionStderrfromResult(result: CompilationResult): ResultLine[] {
         if (result.execResult) {
-            return result.execResult.stderr as ResultLine[];
+            return result.execResult.stderr;
         }
 
         // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -702,6 +702,8 @@ export class Executor extends Pane<ExecutorState> {
                     this.offerViewInSpeedscope(artifact);
                 }
             }
+        } else if (result.execResult) {
+            this.offerFilesIfPossible(result.execResult);
         }
     }
 
diff --git a/static/panes/output.ts b/static/panes/output.ts
index 92d4d531e..4967b4e5b 100644
--- a/static/panes/output.ts
+++ b/static/panes/output.ts
@@ -222,8 +222,8 @@ export class Output extends Pane<OutputState> {
 
         if (result.execResult && (result.execResult.didExecute || result.didExecute)) {
             this.add('Program returned: ' + result.execResult.code);
-            if (result.execResult.stderr?.length || result.execResult.stdout?.length) {
-                for (const obj of result.execResult.stderr ?? []) {
+            if (result.execResult.stderr.length || result.execResult.stdout.length) {
+                for (const obj of result.execResult.stderr) {
                     // Conserve empty lines as they are discarded by ansiToHtml
                     if (obj.text === '') {
                         this.programOutput('<br/>');
@@ -232,7 +232,7 @@ export class Output extends Pane<OutputState> {
                     }
                 }
 
-                for (const obj of result.execResult.stdout ?? []) {
+                for (const obj of result.execResult.stdout) {
                     // Conserve empty lines as they are discarded by ansiToHtml
                     if (obj.text === '') {
                         this.programOutput('<br/>');
diff --git a/types/compilation/compilation.interfaces.ts b/types/compilation/compilation.interfaces.ts
index 4defbbaa1..81fbb3b0a 100644
--- a/types/compilation/compilation.interfaces.ts
+++ b/types/compilation/compilation.interfaces.ts
@@ -150,14 +150,8 @@ export type CompilationResult = {
     stderr: ResultLine[];
     truncated?: boolean;
     didExecute?: boolean;
-    execResult?: {
-        stdout?: ResultLine[];
-        stderr?: ResultLine[];
-        code: number;
-        didExecute: boolean;
-        buildResult?: BuildResult;
-        execTime?: number;
-    };
+    executableFilename?: string;
+    execResult?: CompilationResult;
     gnatDebugOutput?: ResultLine[];
     gnatDebugTreeOutput?: ResultLine[];
     tools?: ToolResult[];
@@ -236,6 +230,8 @@ export type BuildResult = CompilationResult & {
     downloads: BuildEnvDownloadInfo[];
     executableFilename: string;
     compilationOptions: string[];
+    preparedLdPaths?: string[];
+    defaultExecOptions?: ExecutionOptions;
     stdout: ResultLine[];
     stderr: ResultLine[];
     code: number;


Test patch:
diff --git a/test/demangler-tests.ts b/test/demangler-tests.ts
index d6b81f33e..f225cfefd 100644
--- a/test/demangler-tests.ts
+++ b/test/demangler-tests.ts
@@ -42,7 +42,9 @@ class DummyCompiler extends BaseCompiler {
     constructor() {
         const env = {
             ceProps: properties.fakeProps({}),
-            compilerProps: () => {},
+            getCompilerPropsForLanguage: () => {
+                return (prop, def) => def;
+            },
         } as unknown as CompilationEnvironment;
 
         // using c++ as the compiler needs at least one language
diff --git a/test/filters-cases/bug-660.asm.directives.comments.json b/test/filters-cases/bug-660.asm.directives.comments.json
index 7f4ca4c29..0c4e371f9 100644
--- a/test/filters-cases/bug-660.asm.directives.comments.json
+++ b/test/filters-cases/bug-660.asm.directives.comments.json
@@ -457,8 +457,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 2,
-    "$LFB12": 4,
-    "$LVL0": 13,
     "main": 5
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/bug-660.asm.directives.json b/test/filters-cases/bug-660.asm.directives.json
index 7f4ca4c29..0c4e371f9 100644
--- a/test/filters-cases/bug-660.asm.directives.json
+++ b/test/filters-cases/bug-660.asm.directives.json
@@ -457,8 +457,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 2,
-    "$LFB12": 4,
-    "$LVL0": 13,
     "main": 5
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/bug-660.asm.directives.labels.comments.json b/test/filters-cases/bug-660.asm.directives.labels.comments.json
index a11f664d7..a82388a4b 100644
--- a/test/filters-cases/bug-660.asm.directives.labels.comments.json
+++ b/test/filters-cases/bug-660.asm.directives.labels.comments.json
@@ -10,11 +10,6 @@
       "source": null,
       "text": "  .ascii \"hello world\\000\""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB12 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -89,11 +84,6 @@
       "source": null,
       "text": ""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LVL0 = ."
-    },
     {
       "labels": [],
       "source": {
@@ -142,8 +132,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 1,
-    "$LFB12": 3,
-    "$LVL0": 12,
-    "main": 4
+    "main": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/bug-660.asm.directives.labels.comments.library.dontMaskFilenames.json b/test/filters-cases/bug-660.asm.directives.labels.comments.library.dontMaskFilenames.json
index 20d0f2c4c..32c34bb22 100644
--- a/test/filters-cases/bug-660.asm.directives.labels.comments.library.dontMaskFilenames.json
+++ b/test/filters-cases/bug-660.asm.directives.labels.comments.library.dontMaskFilenames.json
@@ -10,11 +10,6 @@
       "source": null,
       "text": "  .ascii \"hello world\\000\""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB12 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -95,11 +90,6 @@
       "source": null,
       "text": ""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LVL0 = ."
-    },
     {
       "labels": [],
       "source": {
@@ -153,8 +143,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 1,
-    "$LFB12": 3,
-    "$LVL0": 12,
-    "main": 4
+    "main": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/bug-660.asm.directives.labels.comments.library.json b/test/filters-cases/bug-660.asm.directives.labels.comments.library.json
index a11f664d7..a82388a4b 100644
--- a/test/filters-cases/bug-660.asm.directives.labels.comments.library.json
+++ b/test/filters-cases/bug-660.asm.directives.labels.comments.library.json
@@ -10,11 +10,6 @@
       "source": null,
       "text": "  .ascii \"hello world\\000\""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB12 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -89,11 +84,6 @@
       "source": null,
       "text": ""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LVL0 = ."
-    },
     {
       "labels": [],
       "source": {
@@ -142,8 +132,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 1,
-    "$LFB12": 3,
-    "$LVL0": 12,
-    "main": 4
+    "main": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/bug-660.asm.directives.labels.json b/test/filters-cases/bug-660.asm.directives.labels.json
index a11f664d7..a82388a4b 100644
--- a/test/filters-cases/bug-660.asm.directives.labels.json
+++ b/test/filters-cases/bug-660.asm.directives.labels.json
@@ -10,11 +10,6 @@
       "source": null,
       "text": "  .ascii \"hello world\\000\""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB12 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -89,11 +84,6 @@
       "source": null,
       "text": ""
     },
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LVL0 = ."
-    },
     {
       "labels": [],
       "source": {
@@ -142,8 +132,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 1,
-    "$LFB12": 3,
-    "$LVL0": 12,
-    "main": 4
+    "main": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/bug-660.asm.directives.library.json b/test/filters-cases/bug-660.asm.directives.library.json
index 7f4ca4c29..0c4e371f9 100644
--- a/test/filters-cases/bug-660.asm.directives.library.json
+++ b/test/filters-cases/bug-660.asm.directives.library.json
@@ -457,8 +457,6 @@
   ],
   "labelDefinitions": {
     "$LC0": 2,
-    "$LFB12": 4,
-    "$LVL0": 13,
     "main": 5
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.comments.json b/test/filters-cases/mips5-square.asm.directives.comments.json
index 06d7765f1..cfb2cbee7 100644
--- a/test/filters-cases/mips5-square.asm.directives.comments.json
+++ b/test/filters-cases/mips5-square.asm.directives.comments.json
@@ -163,7 +163,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 2,
     "_Z6squarei": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.json b/test/filters-cases/mips5-square.asm.directives.json
index 06d7765f1..cfb2cbee7 100644
--- a/test/filters-cases/mips5-square.asm.directives.json
+++ b/test/filters-cases/mips5-square.asm.directives.json
@@ -163,7 +163,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 2,
     "_Z6squarei": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.labels.comments.json b/test/filters-cases/mips5-square.asm.directives.labels.comments.json
index b15810dc9..813ce47bf 100644
--- a/test/filters-cases/mips5-square.asm.directives.labels.comments.json
+++ b/test/filters-cases/mips5-square.asm.directives.labels.comments.json
@@ -1,10 +1,5 @@
 {
   "asm": [
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB0 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -113,7 +108,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 1,
-    "_Z6squarei": 2
+    "_Z6squarei": 1
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.labels.comments.library.dontMaskFilenames.json b/test/filters-cases/mips5-square.asm.directives.labels.comments.library.dontMaskFilenames.json
index d8833c9d8..26867d069 100644
--- a/test/filters-cases/mips5-square.asm.directives.labels.comments.library.dontMaskFilenames.json
+++ b/test/filters-cases/mips5-square.asm.directives.labels.comments.library.dontMaskFilenames.json
@@ -1,10 +1,5 @@
 {
   "asm": [
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB0 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -125,7 +120,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 1,
-    "_Z6squarei": 2
+    "_Z6squarei": 1
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.labels.comments.library.json b/test/filters-cases/mips5-square.asm.directives.labels.comments.library.json
index b15810dc9..813ce47bf 100644
--- a/test/filters-cases/mips5-square.asm.directives.labels.comments.library.json
+++ b/test/filters-cases/mips5-square.asm.directives.labels.comments.library.json
@@ -1,10 +1,5 @@
 {
   "asm": [
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB0 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -113,7 +108,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 1,
-    "_Z6squarei": 2
+    "_Z6squarei": 1
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.labels.json b/test/filters-cases/mips5-square.asm.directives.labels.json
index b15810dc9..813ce47bf 100644
--- a/test/filters-cases/mips5-square.asm.directives.labels.json
+++ b/test/filters-cases/mips5-square.asm.directives.labels.json
@@ -1,10 +1,5 @@
 {
   "asm": [
-    {
-      "labels": [],
-      "source": null,
-      "text": "$LFB0 = ."
-    },
     {
       "labels": [],
       "source": null,
@@ -113,7 +108,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 1,
-    "_Z6squarei": 2
+    "_Z6squarei": 1
   }
-}
+}
\ No newline at end of file
diff --git a/test/filters-cases/mips5-square.asm.directives.library.json b/test/filters-cases/mips5-square.asm.directives.library.json
index 06d7765f1..cfb2cbee7 100644
--- a/test/filters-cases/mips5-square.asm.directives.library.json
+++ b/test/filters-cases/mips5-square.asm.directives.library.json
@@ -163,7 +163,6 @@
     }
   ],
   "labelDefinitions": {
-    "$LFB0": 2,
     "_Z6squarei": 3
   }
-}
+}
\ No newline at end of file
diff --git a/test/handlers/api-tests.ts b/test/handlers/api-tests.ts
index 9cb717fc0..e809a25f6 100644
--- a/test/handlers/api-tests.ts
+++ b/test/handlers/api-tests.ts
@@ -27,6 +27,7 @@ import express from 'express';
 import request from 'supertest';
 import {beforeAll, describe, expect, it} from 'vitest';
 
+import {CompilationEnvironment} from '../../lib/compilation-env.js';
 import {ApiHandler} from '../../lib/handlers/api.js';
 import {CompileHandler} from '../../lib/handlers/compile.js';
 import {CompilerProps, fakeProps} from '../../lib/properties.js';
@@ -94,6 +95,7 @@ describe('API handling', () => {
             }),
             new StorageNull('/', new CompilerProps(languages, fakeProps({}))),
             'default',
+            {ceProps: (key, def) => def} as CompilationEnvironment,
         );
         app.use(bodyParser.json());
         app.use('/api', apiHandler.handle);
diff --git a/test/options-handler.ts b/test/options-handler.ts
index 1bdabbc34..07c572784 100644
--- a/test/options-handler.ts
+++ b/test/options-handler.ts
@@ -180,6 +180,9 @@ describe('Options handler', () => {
         env = {
             ceProps: properties.fakeProps({}),
             compilerProps: () => {},
+            getCompilerPropsForLanguage: () => {
+                return (prop, def) => def;
+            },
         } as unknown as CompilationEnvironment;
     });
 
@@ -489,6 +492,9 @@ describe('Options handler', () => {
         const env = {
             ceProps: properties.fakeProps({}),
             compilerProps: () => {},
+            getCompilerPropsForLanguage: () => {
+                return (prop, def) => def;
+            },
         } as unknown as CompilationEnvironment;
 
         const compiler = new BaseCompiler(compilerInfo, env);
diff --git a/test/pp-output-test.ts b/test/pp-output-test.ts
index 091286ede..86851dbe2 100644
--- a/test/pp-output-test.ts
+++ b/test/pp-output-test.ts
@@ -49,6 +49,9 @@ describe('Preprocessor Output Handling', () => {
         const env: Partial<CompilationEnvironment> = {
             ceProps: properties.fakeProps({}),
             compilerProps: (() => {}) as unknown as any,
+            getCompilerPropsForLanguage: () => {
+                return (prop, def) => def;
+            },
         };
         const compiler = new BaseCompiler(compilerInfo as CompilerInfo, env as CompilationEnvironment);
         for (const testCase of cases) {