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('dotnetSdkDirectory', ''); if (sdkDir) { // The setting points at dotnet/sdk/; 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 { 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 { 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; 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 }; }