Repository: compiler-explorer/compiler-explorer
Pull request #7143 (2024-11-28 14:38:26)

Remove the restrictions on compiler options
Follow up to #7141

As per https://github.com/compiler-explorer/compiler-explorer/pull/7141#issuecomment-2506116434, remove the restrictions on compiler options.

cc: @junlarsen


Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 7657d4aea..6ea83bdd5 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -149,6 +149,7 @@
       - any-glob-to-any-file:
           - 'lib/compilers/flang.ts'
           - 'lib/compilers/flang-fc1.ts'
+          - 'lib/compilers/lfortran.ts'
           - 'lib/compilers/fortran.ts'
           - 'etc/config/fortran.*.properties'
           - 'static/modes/fortran-mode.ts'
diff --git a/app.ts b/app.ts
index db8537215..12c70e5df 100755
--- a/app.ts
+++ b/app.ts
@@ -58,10 +58,10 @@ import {RemoteExecutionQuery} from './lib/execution/execution-query.js';
 import {initHostSpecialties} from './lib/execution/execution-triple.js';
 import {startExecutionWorkerThread} from './lib/execution/sqs-execution-queue.js';
 import {AssemblyDocumentationController} from './lib/handlers/api/assembly-documentation-controller.js';
+import {HealthcheckController} from './lib/handlers/api/healthcheck-controller.js';
 import {SiteTemplateController} from './lib/handlers/api/site-template-controller.js';
 import {SourceController} from './lib/handlers/api/source-controller.js';
 import {CompileHandler} from './lib/handlers/compile.js';
-import * as healthCheck from './lib/handlers/health-check.js';
 import {NoScriptHandler} from './lib/handlers/noscript.js';
 import {RouteAPI, ShortLinkMetaData} from './lib/handlers/route-api.js';
 import {languages as allLanguages} from './lib/languages.js';
@@ -558,9 +558,18 @@ async function main() {
     const storageHandler = new storageType(httpRoot, compilerProps, awsProps);
     const compilerFinder = new CompilerFinder(compileHandler, compilerProps, awsProps, defArgs, clientOptionsHandler);
 
+    const isExecutionWorker = ceProps<boolean>('execqueue.is_worker', false);
+    const healthCheckFilePath = ceProps('healthCheckFilePath', null) as string | null;
+
     const siteTemplateController = new SiteTemplateController();
     const sourceController = new SourceController(sources);
     const assemblyDocumentationController = new AssemblyDocumentationController();
+    const healthCheckController = new HealthcheckController(
+        compilationQueue,
+        healthCheckFilePath,
+        compileHandler,
+        isExecutionWorker,
+    );
 
     logger.info('=======================================');
     if (gitReleaseName) logger.info(`  git release ${gitReleaseName}`);
@@ -569,8 +578,6 @@ async function main() {
     let initialCompilers: CompilerInfo[];
     let prevCompilers: CompilerInfo[];
 
-    const isExecutionWorker = ceProps<boolean>('execqueue.is_worker', false);
-
     if (opts.prediscovered) {
         const prediscoveredCompilersJson = await fs.readFile(opts.prediscovered, 'utf8');
         initialCompilers = JSON.parse(prediscoveredCompilersJson);
@@ -611,8 +618,6 @@ async function main() {
         process.exit(0);
     }
 
-    const healthCheckFilePath = ceProps('healthCheckFilePath', false);
-
     // Exported to allow compilers to refer to other existing compilers.
     global.handler_config = {
         compileHandler,
@@ -683,12 +688,6 @@ async function main() {
                 }
             }),
         )
-        // Handle healthchecks at the root, as they're not expected from the outside world
-        .use(
-            '/healthcheck',
-            new healthCheck.HealthCheckHandler(compilationQueue, healthCheckFilePath, compileHandler, isExecutionWorker)
-                .handle,
-        )
         .use(httpRoot, router)
         .use((req, res, next) => {
             next({status: 404, message: `page "${req.path}" could not be found`});
@@ -872,13 +871,10 @@ async function main() {
             );
         })
         .use(bodyParser.json({limit: ceProps('bodyParserLimit', maxUploadSize)}))
-        .get('/api/siteTemplates', siteTemplateController.getSiteTemplates.bind(siteTemplateController))
-        .get('/source/:source/list', sourceController.listEntries.bind(sourceController))
-        .get('/source/:source/load/:language/:filename', sourceController.loadEntry.bind(sourceController))
-        .get(
-            '/api/asm/:arch/:opcode',
-            assemblyDocumentationController.getOpcodeDocumentation.bind(assemblyDocumentationController),
-        )
+        .use(siteTemplateController.createRouter())
+        .use(sourceController.createRouter())
+        .use(assemblyDocumentationController.createRouter())
+        .use(healthCheckController.createRouter())
         .get('/g/:id', oldGoogleUrlHandler)
         // Deprecated old route for this -- TODO remove in late 2021
         .post('/shortener', routeApi.apiHandler.shortener.handle.bind(routeApi.apiHandler.shortener));
diff --git a/etc/config/fortran.amazon.properties b/etc/config/fortran.amazon.properties
index 66f70fed9..fc23070e9 100644
--- a/etc/config/fortran.amazon.properties
+++ b/etc/config/fortran.amazon.properties
@@ -1,4 +1,4 @@
-compilers=&gfortran_86:&ifort:&ifx:&nvfortran_x86:&cross:&clang_llvmflang
+compilers=&gfortran_86:&ifort:&ifx:&nvfortran_x86:&cross:&clang_llvmflang:&lfortran
 defaultCompiler=gfortran142
 demangler=/opt/compiler-explorer/gcc-14.2.0/bin/c++filt
 objdumper=/opt/compiler-explorer/gcc-14.2.0/bin/objdump
@@ -735,6 +735,20 @@ compiler.flangtrunk-fc.supportsBinaryObject=false
 compiler.flangtrunk-fc.supportsBinaryExecute=false
 compiler.flangtrunk-fc.alias=flangtrunknew-fc
 
+#################################
+# LFortran
+group.lfortran.compilers=lfortran0420
+group.lfortran.groupName=LFortran
+group.lfortran.baseName=LFortran
+group.lfortran.supportsBinary=true
+group.lfortran.supportsBinaryObject=true
+group.lfortran.supportsExecute=true
+group.lfortran.compilerType=lfortran
+group.lfortran.isSemVer=true
+compiler.lfortran0420.exe=/opt/compiler-explorer/lfortran/v0.42.0/bin/lfortran
+compiler.lfortran0420.semver=0.42.0
+compiler.lfortran0420.clang=/opt/compiler-explorer/clang-19.1.0/bin/clang
+
 ###############################
 # GCC for ARM 64bit
 group.gccaarch64.compilers=farm64g494:farm64g550:farm64g640:farm64g730:farm64g820:farm64g1050:farm64g1210:farm64g1220:farm64g1230:farm64g1310:farm64g1140:farm64g1320:farm64g1410:farm64g1330:farm64g1240:farm64g1420
diff --git a/etc/config/java.amazon.properties b/etc/config/java.amazon.properties
index e3cafe607..0bb2fb1ec 100644
--- a/etc/config/java.amazon.properties
+++ b/etc/config/java.amazon.properties
@@ -1,7 +1,7 @@
 compilers=&java8:&java9:&java10:&java11:&java12:&java13:&java14:&java15:&java16:&java17:&java18:&java19:&java20:&java21:&java22:&java23
 compilerType=java
 versionFlag=-version
-defaultCompiler=java2301
+defaultCompiler=java2102
 demangler=
 postProcess=
 options=
diff --git a/etc/config/kotlin.amazon.properties b/etc/config/kotlin.amazon.properties
index 96134cafd..9c245b057 100644
--- a/etc/config/kotlin.amazon.properties
+++ b/etc/config/kotlin.amazon.properties
@@ -1,9 +1,9 @@
 compilers=&kotlin
 compilerType=kotlin
 versionFlag=-version
-objdumper=/opt/compiler-explorer/jdk-21.0.0/bin/javap
+objdumper=/opt/compiler-explorer/jdk-23.0.1/bin/javap
 instructionSet=java
-defaultCompiler=kotlinc2000
+defaultCompiler=kotlinc2021
 demangler=
 postProcess=
 options=
@@ -12,7 +12,7 @@ needsMulti=false
 supportsExecute=true
 interpreted=true
 
-group.kotlin.compilers=kotlinc1400:kotlinc1410:kotlinc1420:kotlinc1421:kotlinc1430:kotlinc1431:kotlinc1432:kotlinc1500:kotlinc1510:kotlinc1520:kotlinc1521:kotlinc1530:kotlinc1531:kotlinc1600:kotlinc1610:kotlinc1620:kotlinc1700:kotlinc1800:kotlinc1810:kotlinc1820:kotlinc1900:kotlinc1910:kotlinc1920:kotlinc2000
+group.kotlin.compilers=kotlinc1400:kotlinc1410:kotlinc1420:kotlinc1421:kotlinc1430:kotlinc1431:kotlinc1432:kotlinc1500:kotlinc1510:kotlinc1520:kotlinc1521:kotlinc1530:kotlinc1531:kotlinc1600:kotlinc1610:kotlinc1620:kotlinc1700:kotlinc1800:kotlinc1810:kotlinc1820:kotlinc1900:kotlinc1910:kotlinc1920:kotlinc2000:kotlinc2010:kotlinc2020:kotlinc2021
 group.kotlin.groupName=Kotlin
 group.kotlin.baseName=kotlinc
 group.kotlin.isSemVer=true
@@ -115,3 +115,15 @@ compiler.kotlinc2000.exe=/opt/compiler-explorer/kotlin-jvm-2.0.0/bin/kotlinc-jvm
 compiler.kotlinc2000.semver=2.0.0
 compiler.kotlinc2000.java_home=/opt/compiler-explorer/jdk-21.0.0
 compiler.kotlinc2000.runtime=/opt/compiler-explorer/jdk-21.0.0/bin/java
+compiler.kotlinc2010.exe=/opt/compiler-explorer/kotlin-jvm-2.0.10/bin/kotlinc-jvm
+compiler.kotlinc2010.semver=2.0.10
+compiler.kotlinc2010.java_home=/opt/compiler-explorer/jdk-23.0.1
+compiler.kotlinc2010.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
+compiler.kotlinc2020.exe=/opt/compiler-explorer/kotlin-jvm-2.0.20/bin/kotlinc-jvm
+compiler.kotlinc2020.semver=2.0.20
+compiler.kotlinc2020.java_home=/opt/compiler-explorer/jdk-23.0.1
+compiler.kotlinc2020.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
+compiler.kotlinc2021.exe=/opt/compiler-explorer/kotlin-jvm-2.0.21/bin/kotlinc-jvm
+compiler.kotlinc2021.semver=2.0.21
+compiler.kotlinc2021.java_home=/opt/compiler-explorer/jdk-23.0.1
+compiler.kotlinc2021.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
diff --git a/etc/config/rust.amazon.properties b/etc/config/rust.amazon.properties
index 499522442..8f88a2161 100644
--- a/etc/config/rust.amazon.properties
+++ b/etc/config/rust.amazon.properties
@@ -2,14 +2,14 @@ compilers=&rust:&rustgcc:&mrustc:&rustccggcc
 objdumper=/opt/compiler-explorer/gcc-14.1.0/bin/objdump
 linker=/opt/compiler-explorer/gcc-14.1.0/bin/gcc
 aarch64linker=/opt/compiler-explorer/arm64/gcc-14.1.0/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-gcc
-defaultCompiler=r1820
+defaultCompiler=r1830
 demangler=/opt/compiler-explorer/gcc-14.1.0/bin/c++filt
 demanglerArgs=--format=rust
 
 buildenvsetup=ceconan-rust
 buildenvsetup.host=https://conan.compiler-explorer.com
 
-group.rust.compilers=r1820:r1810:r1800:r1790:r1780:r1770:r1760:r1750:r1740:r1730:r1720:r1710:r1700:r1690:r1680:r1670:r1660:r1650:r1640:r1630:r1620:r1610:r1600:r1590:r1580:r1570:r1560:r1550:r1540:r1530:r1520:r1510:r1500:r1490:r1480:r1470:r1460:r1452:r1450:r1440:r1430:r1420:r1410:r1400:r1390:r1380:r1370:r1360:r1350:r1340:r1330:r1320:r1310:r1300:r1290:r1280:r1271:r1270:r1260:r1250:r1240:r1230:r1220:r1210:r1200:r1190:r1180:r1170:r1160:r1151:r1140:r1130:r1120:r1110:r1100:r190:r180:r170:r160:r150:r140:r130:r120:r110:r100:nightly:beta
+group.rust.compilers=r1830:r1820:r1810:r1800:r1790:r1780:r1770:r1760:r1750:r1740:r1730:r1720:r1710:r1700:r1690:r1680:r1670:r1660:r1650:r1640:r1630:r1620:r1610:r1600:r1590:r1580:r1570:r1560:r1550:r1540:r1530:r1520:r1510:r1500:r1490:r1480:r1470:r1460:r1452:r1450:r1440:r1430:r1420:r1410:r1400:r1390:r1380:r1370:r1360:r1350:r1340:r1330:r1320:r1310:r1300:r1290:r1280:r1271:r1270:r1260:r1250:r1240:r1230:r1220:r1210:r1200:r1190:r1180:r1170:r1160:r1151:r1140:r1130:r1120:r1110:r1100:r190:r180:r170:r160:r150:r140:r130:r120:r110:r100:nightly:beta
 group.rust.compilerType=rust
 group.rust.isSemVer=true
 group.rust.unwiseOptions=-Ctarget-cpu=native|target-cpu=native
@@ -19,6 +19,8 @@ group.rust.licenseName=Dual-licensed under Apache 2.0 and MIT terms
 group.rust.licensePreamble=Rust is licensed under the Apache License, Version 2.0 or the MIT license
 group.rust.supportsBinary=true
 group.rust.supportsBinaryObject=true
+compiler.r1830.exe=/opt/compiler-explorer/rust-1.83.0/bin/rustc
+compiler.r1830.semver=1.83.0
 compiler.r1820.exe=/opt/compiler-explorer/rust-1.82.0/bin/rustc
 compiler.r1820.semver=1.82.0
 compiler.r1810.exe=/opt/compiler-explorer/rust-1.81.0/bin/rustc
diff --git a/lib/compilers/_all.ts b/lib/compilers/_all.ts
index 2a9a63528..ba167e568 100644
--- a/lib/compilers/_all.ts
+++ b/lib/compilers/_all.ts
@@ -72,6 +72,7 @@ export {EWAVRCompiler} from './ewavr.js';
 export {FakeCompiler} from './fake-for-test.js';
 export {FlangCompiler} from './flang.js';
 export {FlangFC1Compiler} from './flang-fc1.js';
+export {LFortranCompiler} from './lfortran.js';
 export {FortranCompiler} from './fortran.js';
 export {FPCCompiler} from './pascal.js';
 export {GCCCompiler} from './gcc.js';
diff --git a/lib/compilers/dotnet.ts b/lib/compilers/dotnet.ts
index 226bf565b..926911b40 100644
--- a/lib/compilers/dotnet.ts
+++ b/lib/compilers/dotnet.ts
@@ -119,77 +119,6 @@ class DotNetCompiler extends BaseCompiler {
         }
     }
 
-    get configurableOptions() {
-        return [
-            '--targetos',
-            '--targetarch',
-            '--instruction-set',
-            '--singlemethodtypename',
-            '--singlemethodname',
-            '--singlemethodindex',
-            '--singlemethodgenericarg',
-            '--codegenopt',
-            '--codegen-options',
-            '--maxgenericcycle',
-            '--maxgenericcyclebreadth',
-            '--max-vectort-bitwidth',
-            '--runtimeopt',
-            '--runtimeknob',
-            '--feature',
-            '--directpinvoke',
-            '--root',
-            '--conditionalroot',
-            '--trim',
-            '--jitpath',
-            '--generateunmanagedentrypoints',
-            '--guard',
-            '--initassembly',
-            '--reflectiondata',
-        ];
-    }
-
-    get configurableSwitches() {
-        return [
-            '-o',
-            '--optimize',
-            '--od',
-            '--optimize-disabled',
-            '--os',
-            '--optimize-space',
-            '--ot',
-            '--optimize-time',
-            '--enable-generic-cycle-detection',
-            '--inputbubble',
-            '--compilebubblegenerics',
-            '--aot',
-            '--crossgen2',
-            '--dehydrate',
-            '--methodbodyfolding',
-            '--stacktracedata',
-            '--defaultrooting',
-            '--preinitstatics',
-            '--nopreinitstatics',
-            '--scan',
-            '--noscan',
-            '--noinlinetls',
-            '--completetypemetadata',
-            '--help',
-            '-bytes',
-            '-raweh',
-            '-tokens',
-            '-quoteallnames',
-            '-noca',
-            '-caverbal',
-            '-noil',
-            '-forward',
-            '-typelist',
-            '-headers',
-            '-stats',
-            '-classlist',
-            '-all',
-        ];
-    }
-
     setCompilerExecOptions(execOptions: ExecutionOptionsWithEnv, programDir: string) {
         if (!execOptions) {
             execOptions = this.getDefaultExecOptions();
@@ -603,7 +532,6 @@ do()
         execOptions: ExecutionOptionsWithEnv,
         filters: ParseFiltersAndOutputOptions,
     ): Promise<CompilationResult> {
-        const corerunArgs: string[] = [];
         const compilerInfo = await this.getCompilerInfo(this.lang.id);
         const programDir = path.dirname(inputFilename);
         const programOutputPath = path.join(programDir, 'bin', this.buildConfig, compilerInfo.targetFramework);
@@ -611,8 +539,8 @@ do()
         const envVarFileContents = ['DOTNET_EnableWriteXorExecute=0'];
         const isIlDasm = this.compiler.group === 'dotnetildasm';
         const isIlSpy = this.compiler.group === 'dotnetilspy';
-        const toolOptions: string[] = isIlDasm || isIlSpy ? [] : ['--parallelism', '1'];
-        const toolSwitches: string[] = [];
+        const isCoreRun = this.compiler.group === 'dotnetcoreclr';
+        const toolOptions: string[] = isIlDasm || isIlSpy || isCoreRun ? [] : ['--parallelism', '1'];
 
         let overrideDiffable = false;
         let overrideDisasm = false;
@@ -624,13 +552,14 @@ do()
             this.compiler.group === 'dotnetcrossgen2' ||
             (this.compiler.group === 'dotnetlegacy' && compilerInfo.sdkMajorVersion === 6);
 
-        while (options.length > 0) {
+        // we need to skip the last option, because the last option is filename
+        while (options.length > 1) {
             const currentOption = options.shift();
             if (!currentOption) {
                 continue;
             }
 
-            if (currentOption === '-e' || currentOption === '--env') {
+            if (isCoreRun && (currentOption === '-e' || currentOption === '--env')) {
                 const envVar = options.shift();
                 if (envVar) {
                     const [name] = envVar.split('=');
@@ -649,12 +578,7 @@ do()
                     }
                     envVarFileContents.push(envVar);
                 }
-            } else if (currentOption === '-p' || currentOption === '--property') {
-                const property = options.shift();
-                if (property) {
-                    corerunArgs.push('-p', property);
-                }
-            } else if (this.configurableSwitches.includes(currentOption.toLowerCase())) {
+            } else {
                 if (this.compiler.group === 'dotnetlegacy') {
                     if (currentOption === '--aot') {
                         isAot = true;
@@ -662,41 +586,57 @@ do()
                         isCrossgen2 = true;
                     } else if (currentOption === '--mono') {
                         isMono = true;
+                    } else {
+                        toolOptions.push(currentOption);
                     }
                 } else {
-                    toolSwitches.push(currentOption);
-                }
-            } else if (this.configurableOptions.includes(currentOption.toLowerCase())) {
-                const value = options.shift();
-                if (value) {
-                    toolOptions.push(currentOption, value);
-                    const normalizedValue = value.trim().toUpperCase();
-                    if (
-                        (currentOption === '--codegenopt' || currentOption === '--codegen-options') &&
-                        (normalizedValue.startsWith('JITDIFFABLEDASM=') ||
-                            normalizedValue.startsWith('JITDISASMDIFFABLE='))
-                    ) {
-                        overrideDiffable = true;
+                    toolOptions.push(currentOption);
+                    if (currentOption === '--codegenopt' || currentOption === '--codegen-options') {
+                        const value = options.shift();
+                        if (value) {
+                            toolOptions.push(value);
+                            const normalizedValue = value.trim().toUpperCase();
+                            if (normalizedValue.startsWith('TIEREDCOMPILATION=')) {
+                                overrideTiered = true;
+                            }
+                            if (normalizedValue.startsWith('JITDISASM=')) {
+                                overrideDisasm = true;
+                            }
+                            if (normalizedValue.startsWith('JITDISASMASSEMBILES=')) {
+                                overrideAssembly = true;
+                            }
+                            if (
+                                normalizedValue.startsWith('JITDIFFABLEDASM=') ||
+                                normalizedValue.startsWith('JITDISASMDIFFABLE=')
+                            ) {
+                                overrideDiffable = true;
+                            }
+                        }
                     }
                 }
             }
         }
 
         if (!isIlDasm && !isIlSpy) {
-            if (!overrideDiffable) {
-                if (compilerInfo.sdkMajorVersion < 8) {
+            if (!overrideDiffable && compilerInfo.sdkMajorVersion < 8) {
+                if (!isCoreRun) {
                     toolOptions.push('--codegenopt', 'JitDiffableDasm=1');
-                    envVarFileContents.push('DOTNET_JitDiffableDasm=1');
                 }
+                envVarFileContents.push('DOTNET_JitDiffableDasm=1');
             }
 
             if (!overrideDisasm) {
-                toolOptions.push('--codegenopt', compilerInfo.sdkMajorVersion === 6 ? 'NgenDisasm=*' : 'JitDisasm=*');
+                if (!isCoreRun) {
+                    toolOptions.push(
+                        '--codegenopt',
+                        compilerInfo.sdkMajorVersion === 6 ? 'NgenDisasm=*' : 'JitDisasm=*',
+                    );
+                }
                 envVarFileContents.push('DOTNET_JitDisasm=*');
             }
 
             if (!overrideAssembly) {
-                if (compilerInfo.sdkMajorVersion >= 9) {
+                if (!isCoreRun && compilerInfo.sdkMajorVersion >= 9) {
                     toolOptions.push('--codegenopt', 'JitDisasmAssemblies=CompilerExplorer');
                 }
                 envVarFileContents.push('DOTNET_JitDisasmAssemblies=CompilerExplorer');
@@ -725,7 +665,6 @@ do()
                 execOptions,
                 programDllPath,
                 toolOptions,
-                toolSwitches,
                 this.getOutputFilename(programDir, this.outputFilebase),
             );
 
@@ -737,7 +676,6 @@ do()
                 execOptions,
                 programDllPath,
                 toolOptions,
-                toolSwitches,
                 this.getOutputFilename(programDir, this.outputFilebase),
                 compilerInfo.sdkMajorVersion <= 6,
             );
@@ -753,7 +691,6 @@ do()
                 this.clrBuildDir,
                 programDllPath,
                 toolOptions,
-                toolSwitches,
                 this.getOutputFilename(programDir, this.outputFilebase),
             );
 
@@ -766,7 +703,6 @@ do()
                 execOptions,
                 programDllPath,
                 toolOptions,
-                toolSwitches,
                 this.getOutputFilename(programDir, this.outputFilebase),
                 filters.binary ?? false,
             );
@@ -780,7 +716,7 @@ do()
                 this.clrBuildDir,
                 envVarFileContents,
                 programDllPath,
-                corerunArgs,
+                toolOptions,
                 this.getOutputFilename(programDir, this.outputFilebase),
                 isMono,
                 compilerInfo.sdkMajorVersion >= 7,
@@ -861,8 +797,7 @@ do()
     async runIlSpy(
         execOptions: ExecutionOptions,
         dllPath: string,
-        toolOptions: string[],
-        toolSwitches: string[],
+        options: string[],
         outputPath: string,
         useDotNetHost: boolean,
     ) {
@@ -875,7 +810,7 @@ do()
         const ilspyPath = path.join(ilspyToolsDir, targetFramework, 'any', 'ilspycmd.dll');
 
         // prettier-ignore
-        const ilspyOptions = [ilspyPath, dllPath, '--disable-updatecheck'].concat(toolOptions).concat(toolSwitches);
+        const ilspyOptions = [ilspyPath, dllPath, '--disable-updatecheck'].concat(options);
         const compilerPath = useDotNetHost ? this.compiler.exe : this.corerunPath;
         const compilerExecResult = await this.exec(compilerPath, ilspyOptions, execOptions);
         const result = this.transformToCompilationResult(compilerExecResult, dllPath);
@@ -890,15 +825,9 @@ do()
         return result;
     }
 
-    async runIlDasm(
-        execOptions: ExecutionOptions,
-        dllPath: string,
-        toolOptions: string[],
-        toolSwitches: string[],
-        outputPath: string,
-    ) {
+    async runIlDasm(execOptions: ExecutionOptions, dllPath: string, options: string[], outputPath: string) {
         // prettier-ignore
-        const ildasmOptions = [dllPath, '-utf8'].concat(toolOptions).concat(toolSwitches);
+        const ildasmOptions = [dllPath, '-utf8'].concat(options);
 
         const compilerExecResult = await this.exec(this.ildasmPath, ildasmOptions, execOptions);
         const result = this.transformToCompilationResult(compilerExecResult, dllPath);
@@ -919,8 +848,7 @@ do()
         execOptions: ExecutionOptions,
         bclPath: string,
         dllPath: string,
-        toolOptions: string[],
-        toolSwitches: string[],
+        options: string[],
         outputPath: string,
     ) {
         // prettier-ignore
@@ -929,7 +857,7 @@ do()
             '-r', this.disassemblyLoaderPath,
             dllPath,
             '-o', `${AssemblyName}.r2r.dll`,
-        ].concat(toolOptions).concat(toolSwitches);
+        ].concat(options);
 
         if (sdkMajorVersion >= 9) {
             crossgen2Options.push('--inputbubble', '--compilebubblegenerics');
@@ -958,8 +886,7 @@ do()
         compiler: string,
         execOptions: ExecutionOptions,
         dllPath: string,
-        toolOptions: string[],
-        toolSwitches: string[],
+        options: string[],
         outputPath: string,
         buildToBinary: boolean,
     ) {
@@ -986,7 +913,7 @@ do()
             '--generateunmanagedentrypoints:System.Private.CoreLib',
             '--notrimwarn',
             '--noaotwarn',
-        ].concat(toolOptions).concat(toolSwitches);
+        ].concat(options);
 
         if (!buildToBinary) {
             ilcOptions.push('--nativelib', '--root:CompilerExplorer');
diff --git a/lib/compilers/lfortran.ts b/lib/compilers/lfortran.ts
new file mode 100644
index 000000000..7f7b30c9d
--- /dev/null
+++ b/lib/compilers/lfortran.ts
@@ -0,0 +1,69 @@
+// 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 type {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
+import {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
+import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
+import {CompilationEnvironment} from '../compilation-env.js';
+
+import {FortranCompiler} from './fortran.js';
+
+export class LFortranCompiler extends FortranCompiler {
+    private clang: string;
+
+    static override get key() {
+        return 'lfortran';
+    }
+
+    constructor(compilerInfo: PreliminaryCompilerInfo & {disabledFilters?: string[]}, env: CompilationEnvironment) {
+        super(compilerInfo, env);
+        // TODO(#7150) consider a more general purpose version of this.
+        this.clang = this.compilerProps<string>(`compiler.${this.compiler.id}.clang`);
+    }
+
+    override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: string) {
+        // TODO(@Thirumalai-Shaktivel) add `-g` to the options once that's supported in the compilers.
+        let options = ['-o', this.filename(outputFilename), '--generate-object-code'];
+
+        if (!filters.binary && !filters.binaryObject) {
+            options = options.concat('-S');
+        } else if (filters.binaryObject) {
+            options = options.concat('-c');
+        }
+        return options;
+    }
+
+    override async runCompiler(
+        compiler: string,
+        options: string[],
+        inputFilename: string,
+        execOptions: ExecutionOptionsWithEnv,
+    ): Promise<CompilationResult> {
+        if (!execOptions) {
+            execOptions = this.getDefaultExecOptions();
+        }
+        execOptions.env.LFORTRAN_CC = this.clang;
+        return super.runCompiler(compiler, options, inputFilename, execOptions);
+    }
+}
diff --git a/lib/handlers/api/assembly-documentation-controller.ts b/lib/handlers/api/assembly-documentation-controller.ts
index f4560591f..9893105f4 100644
--- a/lib/handlers/api/assembly-documentation-controller.ts
+++ b/lib/handlers/api/assembly-documentation-controller.ts
@@ -27,7 +27,15 @@ import express from 'express';
 import {addStaticHeaders} from '../../../app.js';
 import {getDocumentationProviderTypeByKey} from '../../asm-docs/index.js';
 
-export class AssemblyDocumentationController {
+import {HttpController} from './controller.interfaces.js';
+
+export class AssemblyDocumentationController implements HttpController {
+    createRouter(): express.Router {
+        const router = express.Router();
+        router.get('/api/asm/:arch/:opcode', this.getOpcodeDocumentation.bind(this));
+        return router;
+    }
+
     /**
      * Handle request to `/asm/:arch/:opcode` endpoint
      */
diff --git a/lib/handlers/api/controller.interfaces.ts b/lib/handlers/api/controller.interfaces.ts
new file mode 100644
index 000000000..52ab0e28f
--- /dev/null
+++ b/lib/handlers/api/controller.interfaces.ts
@@ -0,0 +1,35 @@
+// 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 express from 'express';
+
+export interface HttpController {
+    /**
+     * Create a separate express router for this controller.
+     *
+     * The purpose of this is to allow for a consistent API surface across both test and production, as well as being
+     * able to create multiple mergable routes for the final server.
+     */
+    createRouter(): express.Router;
+}
diff --git a/lib/handlers/api/healthcheck-controller.ts b/lib/handlers/api/healthcheck-controller.ts
new file mode 100644
index 000000000..b45e642cb
--- /dev/null
+++ b/lib/handlers/api/healthcheck-controller.ts
@@ -0,0 +1,87 @@
+// 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 fs from 'node:fs/promises';
+
+import express from 'express';
+
+import {CompilationQueue} from '../../compilation-queue.js';
+import {logger} from '../../logger.js';
+import {SentryCapture} from '../../sentry.js';
+import {ICompileHandler} from '../compile.interfaces.js';
+
+import {HttpController} from './controller.interfaces.js';
+
+export class HealthcheckController implements HttpController {
+    public constructor(
+        private readonly compilationQueue: CompilationQueue,
+        private readonly healthCheckFilePath: string | null,
+        private readonly compileHandler: ICompileHandler,
+        private readonly isExecutionWorker: boolean,
+    ) {}
+
+    createRouter(): express.Router {
+        const router = express.Router();
+        // TODO: Consider if this could be `GET` only. It doesn't quite make sense to do anything but GET the
+        //  healthcheck
+        router.use('/healthcheck', this.healthcheck.bind(this));
+        return router;
+    }
+
+    /**
+     * Handle request to `/healthcheck` endpoint
+     * @param req
+     * @param res
+     */
+    public async healthcheck(req: express.Request, res: express.Response) {
+        // Enqueue an empty job to ensure the queue is running. This check simply tests that the queue is being
+        // processed by _something_.
+        await this.compilationQueue.enqueue(async () => {}, {highPriority: true});
+
+        // If this is a worker, we don't require that the server has languages configured.
+        if (!this.isExecutionWorker && !this.compileHandler.hasLanguages()) {
+            logger.error(`*** HEALTH CHECK FAILURE: no languages/compilers detected`);
+            return res.status(500).send();
+        }
+
+        // If we have a healthcheck file, we require that it exists and it is non-empty. The /efs/.health file contents
+        // are not important, but the file acts as a health check for EFS mounts.
+        if (this.healthCheckFilePath !== null) {
+            try {
+                const content = await fs.readFile(this.healthCheckFilePath);
+                if (content.length === 0) {
+                    throw new Error('File is empty');
+                }
+                res.set('Content-Type', 'text/html');
+                return res.send(content);
+            } catch (e) {
+                logger.error(`*** HEALTH CHECK FAILURE: while reading file '${this.healthCheckFilePath}' got ${e}`);
+                SentryCapture(e, 'Health check');
+                return res.status(500).send();
+            }
+        }
+
+        return res.send('Everything is awesome');
+    }
+}
diff --git a/lib/handlers/api/site-template-controller.ts b/lib/handlers/api/site-template-controller.ts
index 6f49a9732..cc9ccca7b 100644
--- a/lib/handlers/api/site-template-controller.ts
+++ b/lib/handlers/api/site-template-controller.ts
@@ -24,9 +24,17 @@
 
 import express from 'express';
 
-import {getSiteTemplates} from '../site-templates.js';
+import {getSiteTemplates} from '../../site-templates.js';
+
+import {HttpController} from './controller.interfaces.js';
+
+export class SiteTemplateController implements HttpController {
+    createRouter(): express.Router {
+        const router = express.Router();
+        router.get('/api/siteTemplates', this.getSiteTemplates.bind(this));
+        return router;
+    }
 
-export class SiteTemplateController {
     public async getSiteTemplates(req: express.Request, res: express.Response) {
         const templates = await getSiteTemplates();
         res.send(templates);
diff --git a/lib/handlers/api/source-controller.ts b/lib/handlers/api/source-controller.ts
index 065689401..b7a48f609 100644
--- a/lib/handlers/api/source-controller.ts
+++ b/lib/handlers/api/source-controller.ts
@@ -28,9 +28,18 @@ import _ from 'underscore';
 import {addStaticHeaders} from '../../../app.js';
 import {Source} from '../../../types/source.interfaces.js';
 
-export class SourceController {
+import {HttpController} from './controller.interfaces.js';
+
+export class SourceController implements HttpController {
     public constructor(private readonly sources: Source[]) {}
 
+    createRouter(): express.Router {
+        const router = express.Router();
+        router.get('/source/:source/list', this.listEntries.bind(this));
+        router.get('/source/:source/load/:language/:filename', this.loadEntry.bind(this));
+        return router;
+    }
+
     /**
      * Handle request to `/source/<source>/list` endpoint
      */
diff --git a/lib/handlers/health-check.ts b/lib/handlers/health-check.ts
deleted file mode 100644
index f64fcdf6f..000000000
--- a/lib/handlers/health-check.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (c) 2017, 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 express from 'express';
-import fs from 'fs-extra';
-
-import {CompilationQueue} from '../compilation-queue.js';
-import {logger} from '../logger.js';
-import {SentryCapture} from '../sentry.js';
-
-import {ICompileHandler} from './compile.interfaces.js';
-
-export class HealthCheckHandler {
-    public readonly handle: (req: any, res: any) => Promise<void>;
-
-    constructor(
-        private readonly compilationQueue: CompilationQueue,
-        private readonly filePath: any,
-        private readonly compileHandler: ICompileHandler,
-        private readonly isExecutionWorker: boolean,
-    ) {
-        this.handle = this._handle.bind(this);
-    }
-
-    async _handle(req: express.Request, res: express.Response) {
-        /* wait on an empty job to pass through the compilation queue
-         * to ensure the health check will timeout if it is deadlocked
-         * we set the priority to super high here and rely on the fact
-         * that timed out jobs will auto-cancel. this health check then
-         * is just testing that _something_ is still processing jobs in
-         * the async queue.
-         *
-         * we perform the remainder of the health check outside of the
-         * job to minimize the duration that we hold an execution slot
-         */
-        await this.compilationQueue.enqueue(async () => {}, {highPriority: true});
-
-        if (!this.isExecutionWorker && !this.compileHandler.hasLanguages()) {
-            logger.error(`*** HEALTH CHECK FAILURE: no languages/compilers detected`);
-            res.status(500).end();
-            return;
-        }
-
-        if (!this.filePath) {
-            res.send('Everything is awesome');
-            return;
-        }
-
-        try {
-            const content = await fs.readFile(this.filePath);
-            if (content.length === 0) throw new Error('File is empty');
-            res.set('Content-Type', 'text/html');
-            res.send(content);
-        } catch (e) {
-            logger.error(`*** HEALTH CHECK FAILURE: while reading file '${this.filePath}' got ${e}`);
-            SentryCapture(e, 'Health check');
-            res.status(500).end();
-        }
-    }
-}
diff --git a/lib/handlers/site-templates.ts b/lib/site-templates.ts
similarity index 96%
rename from lib/handlers/site-templates.ts
rename to lib/site-templates.ts
index f65c54298..56068a1af 100644
--- a/lib/handlers/site-templates.ts
+++ b/lib/site-templates.ts
@@ -27,7 +27,7 @@ import path from 'node:path';
 
 import _ from 'underscore';
 
-import {SiteTemplatesType} from '../../types/features/site-templates.interfaces.js';
+import {SiteTemplatesType} from '../types/features/site-templates.interfaces.js';
 
 let siteTemplates: SiteTemplatesType;
 


Test patch:
diff --git a/test/handlers/asm-docs-tests.ts b/test/handlers/asm-docs-tests.ts
index 9593eb078..168557b7c 100644
--- a/test/handlers/asm-docs-tests.ts
+++ b/test/handlers/asm-docs-tests.ts
@@ -111,10 +111,8 @@ describe('Assembly Documentation API', () => {
 
     beforeAll(() => {
         app = express();
-        const router = express.Router();
         const controller = new AssemblyDocumentationController();
-        router.get('/asm/:arch/:opcode', controller.getOpcodeDocumentation.bind(controller));
-        app.use('/api', router);
+        app.use('/', controller.createRouter());
     });
 
     it('should return 404 for unknown architecture', async () => {
diff --git a/test/handlers/health-check-tests.ts b/test/handlers/health-check-tests.ts
index 426de43bd..7aa7185ae 100644
--- a/test/handlers/health-check-tests.ts
+++ b/test/handlers/health-check-tests.ts
@@ -28,11 +28,11 @@ import request from 'supertest';
 import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
 
 import {CompilationQueue} from '../../lib/compilation-queue.js';
-import {HealthCheckHandler} from '../../lib/handlers/health-check.js';
+import {HealthcheckController} from '../../lib/handlers/api/healthcheck-controller.js';
 
 describe('Health checks', () => {
-    let app;
-    let compilationQueue;
+    let app: express.Express;
+    let compilationQueue: CompilationQueue;
 
     beforeEach(() => {
         const compileHandlerMock = {
@@ -40,26 +40,28 @@ describe('Health checks', () => {
         };
         compilationQueue = new CompilationQueue(1, 0, 0);
         app = express();
-        app.use('/hc', new HealthCheckHandler(compilationQueue, '', compileHandlerMock, false).handle);
+        const controller = new HealthcheckController(compilationQueue, null, compileHandlerMock, false);
+        app.use(controller.createRouter());
     });
 
     it('should respond with OK', async () => {
-        await request(app).get('/hc').expect(200, 'Everything is awesome');
+        await request(app).get('/healthcheck').expect(200, 'Everything is awesome');
     });
 
     it('should use compilation queue', async () => {
         let count = 0;
+        // @ts-expect-error: bypass the private _queue property
         compilationQueue._queue.on('active', () => {
             count++;
         });
-        await request(app).get('/hc');
+        await request(app).get('/healthcheck');
         expect(count).toEqual(1);
     });
 });
 
 describe('Health checks without lang/comp', () => {
-    let app;
-    let compilationQueue;
+    let app: express.Express;
+    let compilationQueue: CompilationQueue;
 
     beforeEach(() => {
         const compileHandlerMock = {
@@ -67,17 +69,18 @@ describe('Health checks without lang/comp', () => {
         };
         compilationQueue = new CompilationQueue(1, 0, 0);
         app = express();
-        app.use('/hc', new HealthCheckHandler(compilationQueue, '', compileHandlerMock, false).handle);
+        const controller = new HealthcheckController(compilationQueue, null, compileHandlerMock, false);
+        app.use(controller.createRouter());
     });
 
     it('should respond with error', async () => {
-        await request(app).get('/hc').expect(500);
+        await request(app).get('/healthcheck').expect(500);
     });
 });
 
 describe('Health checks without lang/comp but in execution worker mode', () => {
-    let app;
-    let compilationQueue;
+    let app: express.Express;
+    let compilationQueue: CompilationQueue;
 
     beforeEach(() => {
         const compileHandlerMock = {
@@ -85,16 +88,18 @@ describe('Health checks without lang/comp but in execution worker mode', () => {
         };
         compilationQueue = new CompilationQueue(1, 0, 0);
         app = express();
-        app.use('/hc', new HealthCheckHandler(compilationQueue, '', compileHandlerMock, true).handle);
+        const controller = new HealthcheckController(compilationQueue, null, compileHandlerMock, true);
+        app.use(controller.createRouter());
     });
 
     it('should respond with ok', async () => {
-        await request(app).get('/hc').expect(200);
+        await request(app).get('/healthcheck').expect(200);
     });
 });
 
 describe('Health checks on disk', () => {
-    let app;
+    let healthyApp: express.Express;
+    let unhealthyApp: express.Express;
 
     beforeAll(() => {
         const compileHandlerMock = {
@@ -102,9 +107,13 @@ describe('Health checks on disk', () => {
         };
         const compilationQueue = new CompilationQueue(1, 0, 0);
 
-        app = express();
-        app.use('/hc', new HealthCheckHandler(compilationQueue, '/fake/.nonexist', compileHandlerMock, false).handle);
-        app.use('/hc2', new HealthCheckHandler(compilationQueue, '/fake/.health', compileHandlerMock, false).handle);
+        healthyApp = express();
+        const hc1 = new HealthcheckController(compilationQueue, '/fake/.health', compileHandlerMock, false);
+        healthyApp.use(hc1.createRouter());
+
+        unhealthyApp = express();
+        const hc2 = new HealthcheckController(compilationQueue, '/fake/.nonexist', compileHandlerMock, false);
+        unhealthyApp.use(hc2.createRouter());
 
         mockfs({
             '/fake': {
@@ -118,10 +127,13 @@ describe('Health checks on disk', () => {
     });
 
     it('should respond with 500 when file not found', async () => {
-        await request(app).get('/hc').expect(500);
+        await request(unhealthyApp).get('/healthcheck').expect(500);
     });
 
     it('should respond with OK and file contents when found', async () => {
-        await request(app).get('/hc2').expect(200, 'Everything is fine');
+        await request(healthyApp)
+            .get('/healthcheck')
+            .expect(200, 'Everything is fine')
+            .expect('content-type', /text\/html/);
     });
 });
diff --git a/test/handlers/site-templates-tests.ts b/test/handlers/site-templates-tests.ts
index aca4dc2ef..c1875add4 100644
--- a/test/handlers/site-templates-tests.ts
+++ b/test/handlers/site-templates-tests.ts
@@ -3,14 +3,14 @@ import request from 'supertest';
 import {beforeAll, describe, expect, it} from 'vitest';
 
 import {SiteTemplateController} from '../../lib/handlers/api/site-template-controller.js';
-import {getSiteTemplates} from '../../lib/handlers/site-templates.js';
+import {getSiteTemplates} from '../../lib/site-templates.js';
 
 describe('Site Templates Backend', () => {
     let app: express.Express;
     beforeAll(() => {
         app = express();
         const controller = new SiteTemplateController();
-        app.use('/api/siteTemplates', controller.getSiteTemplates.bind(controller));
+        app.use('/', controller.createRouter());
     });
 
     it('should load site templates properly', async () => {
diff --git a/test/handlers/source-tests.ts b/test/handlers/source-tests.ts
index a8708a1a7..61e95e005 100644
--- a/test/handlers/source-tests.ts
+++ b/test/handlers/source-tests.ts
@@ -38,8 +38,7 @@ describe('Sources', () => {
             load: (lang, name) => Promise.resolve({file: `File called ${name} in ${lang}`}),
         },
     ]);
-    app.use('/source/:source/list', handler.listEntries.bind(handler));
-    app.use('/source/:source/load/:language/:filename', handler.loadEntry.bind(handler));
+    app.use('/', handler.createRouter());
 
     it('should list', async () => {
         await request(app)