Initial version of .NET Solution Launcher

Startup project and solution configuration (Debug|x64, Test|x64, ...) in
the status bar, with build and debug tasks that pass both configuration
and platform. Made for DotRush, whose build never passes -p:Platform and
whose status item shows the configuration but not the project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
This commit is contained in:
max
2026-09-08 13:10:55 +02:00
co-authored by Claude Fable 5.1
commit e2ba32a647
21 changed files with 3279 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { execFile } from 'child_process';
/** Evaluated output properties of a project under one configuration. */
export interface TargetInfo {
/** The built assembly, usually a .dll. */
targetPath: string;
targetDir: string;
assemblyName: string;
/** The apphost executable next to the dll, when the project produces one. */
executablePath: string | undefined;
/** The framework the evaluation used, when the project multi-targets. */
targetFramework: string | undefined;
}
export interface EvaluationRequest {
projectPath: string;
configuration: string;
/** MSBuild spelling: `AnyCPU`, not `Any CPU`. */
platform: string;
targetFramework?: string;
}
const PROPERTIES = ['TargetPath', 'TargetDir', 'AssemblyName', 'UseAppHost', 'OutputType', 'TargetExt'];
export function dotnetPath(): string {
// DotRush has its own SDK directory setting; honour it so both agree on the SDK.
const sdkDir = vscode.workspace.getConfiguration('dotrush.roslyn').get<string>('dotnetSdkDirectory', '');
if (sdkDir) {
// The setting points at dotnet/sdk/<version>; the host is two levels up.
const host = path.join(sdkDir, '..', '..', process.platform === 'win32' ? 'dotnet.exe' : 'dotnet');
return path.normalize(host);
}
return 'dotnet';
}
function run(args: string[], cwd: string, log: vscode.OutputChannel): Promise<string> {
const command = dotnetPath();
log.appendLine(`> ${command} ${args.join(' ')}`);
return new Promise((resolve, reject) => {
execFile(command, args, { cwd, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
if (error) {
log.appendLine(stdout);
log.appendLine(stderr);
reject(new Error(`${command} ${args[0]} failed: ${error.message}`));
return;
}
resolve(stdout);
});
});
}
/**
* Evaluates the project without building it, the way `-getProperty` does, and returns
* where the output lands under the requested configuration.
*
* Reading the csproj would not do: OutDir here comes from a Directory.Build.props, and
* AppendTargetFrameworkToOutputPath is off, so only MSBuild knows the answer.
*/
export async function evaluateTarget(request: EvaluationRequest, log: vscode.OutputChannel): Promise<TargetInfo> {
const args = [
'msbuild',
request.projectPath,
'-nologo',
`-p:Configuration=${request.configuration}`,
`-p:Platform=${request.platform}`,
...(request.targetFramework ? [`-p:TargetFramework=${request.targetFramework}`] : []),
...PROPERTIES.map(name => `-getProperty:${name}`),
];
const stdout = await run(args, path.dirname(request.projectPath), log);
let properties: Record<string, string>;
try {
// With several -getProperty switches the output is JSON: { "Properties": { ... } }.
const start = stdout.indexOf('{');
properties = JSON.parse(stdout.slice(start)).Properties ?? {};
} catch {
throw new Error(`Could not read MSBuild properties for ${request.projectPath}:\n${stdout}`);
}
const targetPath = properties.TargetPath ?? '';
if (!targetPath) {
throw new Error(
`MSBuild returned no TargetPath for ${path.basename(request.projectPath)} ` +
`(${request.configuration}|${request.platform}). Multi-targeting projects need a TargetFramework.`);
}
const targetDir = properties.TargetDir || path.dirname(targetPath) + path.sep;
const assemblyName = properties.AssemblyName || path.basename(targetPath, path.extname(targetPath));
const executable = /^(win)?exe$/i.test((properties.OutputType ?? '').trim());
const useAppHost = (properties.UseAppHost ?? 'true').trim().toLowerCase() !== 'false';
const executablePath = executable && useAppHost
? path.join(targetDir, assemblyName + (process.platform === 'win32' ? '.exe' : ''))
: undefined;
return { targetPath, targetDir, assemblyName, executablePath, targetFramework: request.targetFramework };
}