diff --git a/resources/netCore/GetBlazorManifestLocations.targets b/resources/netCore/GetBlazorManifestLocations.targets deleted file mode 100644 index 1c49e80c..00000000 --- a/resources/netCore/GetBlazorManifestLocations.targets +++ /dev/null @@ -1,19 +0,0 @@ - - - - $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(StaticWebAssetDevelopmentManifestPath))) - $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(OutputPath), $(TargetName).staticwebassets.runtime.json)) - - - - $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(_GeneratedStaticWebAssetsDevelopmentManifest))) - $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(OutputPath), $(TargetName).StaticWebAssets.xml)) - - - - - diff --git a/resources/netCore/GetProjectProperties.targets b/resources/netCore/GetProjectProperties.targets deleted file mode 100644 index 72fe4422..00000000 --- a/resources/netCore/GetProjectProperties.targets +++ /dev/null @@ -1,33 +0,0 @@ - - - - $(SDKContainerSupportEnabled) - $(GetProjectPropertiesDependsOn);ComputeContainerConfig; - - - $(GetProjectPropertiesDependsOn);_ComputeContainerExecutionArgs - - - $(GetProjectPropertiesDependsOn);_ContainerEstablishRIDNess;_ComputeContainerExecutionArgs - - - - - - $(ContainerRepository) - $(ContainerImageName) - - - - diff --git a/src/debugging/netSdk/NetSdkDebugHelper.ts b/src/debugging/netSdk/NetSdkDebugHelper.ts index 7e5c7ee9..68e2e072 100644 --- a/src/debugging/netSdk/NetSdkDebugHelper.ts +++ b/src/debugging/netSdk/NetSdkDebugHelper.ts @@ -111,31 +111,29 @@ export class NetSdkDebugHelper extends NetCoreDebugHelper { const ridOS = await normalizeOsToRidOs(); const ridArchitecture = await normalizeArchitectureToRidArchitecture(); const additionalProperties = composeArgs( - withNamedArg('/p:ContainerRuntimeIdentifier', `"${ridOS}-${ridArchitecture}"`, { assignValue: true }), // We have to pre-quote the file paths because we cannot simultaneously use `assignValue` and `shouldQuote` + withNamedArg('-p:ContainerRuntimeIdentifier', `"${ridOS}-${ridArchitecture}"`, { assignValue: true }), // We have to pre-quote the RID because we cannot simultaneously use `assignValue` and `shouldQuote` )(); const resolvedAppProject = resolveVariables(debugConfiguration.netCore?.appProject, folder); - const projectInfo = await getNetCoreProjectInfo('GetProjectProperties', resolvedAppProject, additionalProperties); + const projectInfo = await getNetCoreProjectInfo(resolvedAppProject, additionalProperties); - if (projectInfo.length < 6 || !projectInfo[5]) { + if (!projectInfo.enableSdkContainerSupport) { throw new Error(l10n.t("Your current project configuration or .NET SDK version doesn't support SDK Container build. Please choose a compatible project or update .NET SDK.")); } - const projectProperties: NetSdkProjectProperties = { - assemblyName: projectInfo[0], - targetFramework: projectInfo[1], - appOutput: projectInfo[2], - containerWorkingDirectory: projectInfo[3], - isSdkContainerSupportEnabled: projectInfo[4] === 'true', - imageName: projectInfo[5], + return { + assemblyName: projectInfo.assemblyName, + targetFramework: projectInfo.targetFrameworks[0], + appOutput: projectInfo.assemblyRelativeOutputPath, + containerWorkingDirectory: projectInfo.assemblyContainerPath, + isSdkContainerSupportEnabled: projectInfo.enableSdkContainerSupport, + imageName: projectInfo.imageName, }; - - return projectProperties; } private async normalizeAppOutput(unnormalizedContainerWorkingDirectory: string, isSdkContainerSupportEnabled: boolean): Promise { if (isSdkContainerSupportEnabled) { - return await getDockerOSType() === 'windows' // fourth is output path + return await getDockerOSType() === 'windows' ? path.win32.normalize(unnormalizedContainerWorkingDirectory) : path.posix.normalize(unnormalizedContainerWorkingDirectory); } else { diff --git a/src/debugging/netcore/NetCoreDebugHelper.ts b/src/debugging/netcore/NetCoreDebugHelper.ts index c4ff0358..e3da177f 100644 --- a/src/debugging/netcore/NetCoreDebugHelper.ts +++ b/src/debugging/netcore/NetCoreDebugHelper.ts @@ -204,20 +204,13 @@ export class NetCoreDebugHelper implements DebugHelper { } protected async getProjectProperties(debugConfiguration: DockerDebugConfiguration): Promise { - const projectInfo = await getNetCoreProjectInfo('GetProjectProperties', debugConfiguration.netCore?.appProject); + const projectInfo = await getNetCoreProjectInfo(debugConfiguration.netCore?.appProject); - if (projectInfo.length < 3) { - throw new Error(l10n.t('Unable to determine assembly output path.')); - } - - // First line is assembly name, second is target framework, third+ are output path(s) - const projectProperties: NetCoreProjectProperties = { - assemblyName: projectInfo[0], - targetFramework: projectInfo[1], - appOutput: projectInfo[2] + return { + assemblyName: projectInfo.assemblyName, + targetFramework: projectInfo.targetFrameworks[0], + appOutput: projectInfo.assemblyRelativeOutputPath, }; - - return projectProperties; } private async acquireDebuggers(platformOS: PlatformOS): Promise { diff --git a/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts b/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts index d245efff..c274eb07 100644 --- a/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts +++ b/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts @@ -28,18 +28,14 @@ export class NetCoreGatherInformationStep extends GatherInformationStep { await this.ensureNetCoreBuildTasks(wizardContext); - const projectInfo = await getNetCoreProjectInfo('GetProjectProperties', wizardContext.artifact); - - if (projectInfo.length < 2) { - throw new Error(vscode.l10n.t('Unable to determine project info for \'{0}\'', wizardContext.artifact)); - } + const projectInfo = await getNetCoreProjectInfo(wizardContext.artifact); if (!wizardContext.netCoreAssemblyName) { - wizardContext.netCoreAssemblyName = projectInfo[0]; // Line 1 is the assembly name including ".dll" + wizardContext.netCoreAssemblyName = projectInfo.assemblyName; } if (!wizardContext.netCoreRuntimeBaseImage || !wizardContext.netCoreSdkBaseImage) { - this.targetFramework = projectInfo[1]; // Line 2 is the value, or first item from + this.targetFramework = projectInfo.targetFrameworks[0]; const regexMatch = /net(coreapp)?([\d.]+)/i.exec(this.targetFramework); diff --git a/src/tasks/netcore/updateBlazorManifest.ts b/src/tasks/netcore/updateBlazorManifest.ts index 80064c8e..fb4fd471 100644 --- a/src/tasks/netcore/updateBlazorManifest.ts +++ b/src/tasks/netcore/updateBlazorManifest.ts @@ -4,45 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as fse from 'fs-extra'; -import * as path from 'path'; -import { l10n } from 'vscode'; -import * as xml2js from 'xml2js'; -import { getNetCoreProjectInfo } from '../../utils/netCoreUtils'; +import * as vscode from 'vscode'; +import { getBlazorManifestInfo } from '../../utils/netCoreUtils'; import { pathNormalize } from '../../utils/pathNormalize'; import { PlatformOS } from '../../utils/platform'; import { DockerContainerVolume } from '../DockerRunTaskDefinitionBase'; import { DockerRunTaskDefinition } from "../DockerRunTaskProvider"; import { DockerRunTaskContext } from "../TaskHelper"; -interface ContentRootAttributes { - BasePath: string; - Path: string; -} - -interface ContentRoot { - $: ContentRootAttributes; -} - -interface StaticWebAssets { - ContentRoot: ContentRoot[]; -} - -interface XmlManifest { - StaticWebAssets: StaticWebAssets; -} - interface JsonManifest { ContentRoots: string[]; } export async function updateBlazorManifest(context: DockerRunTaskContext, runDefinition: DockerRunTaskDefinition): Promise { - const contents = await getNetCoreProjectInfo('GetBlazorManifestLocations', runDefinition.netCore.appProject); - - if (contents.length < 2) { - throw new Error(l10n.t('Unable to determine Blazor manifest locations from output file.')); - } - - await transformBlazorManifest(context, contents[0].trim(), contents[1].trim(), runDefinition.dockerRun.volumes, runDefinition.dockerRun.os); + const blazorInfo = await getBlazorManifestInfo(runDefinition.netCore.appProject); + await transformBlazorManifest(context, blazorInfo.inputManifestPath, blazorInfo.outputManifestPath, runDefinition.dockerRun.volumes, runDefinition.dockerRun.os); } async function transformBlazorManifest(context: DockerRunTaskContext, inputManifest: string, outputManifest: string, volumes: DockerContainerVolume[], os: PlatformOS): Promise { @@ -58,20 +34,16 @@ async function transformBlazorManifest(context: DockerRunTaskContext, inputManif os = os || 'Linux'; - context.terminal.writeOutputLine(l10n.t('Attempting to containerize Blazor static web assets manifest...')); + context.terminal.writeOutputLine(vscode.l10n.t('Attempting to containerize Blazor static web assets manifest...')); - if (path.extname(inputManifest) === '.json') { - await transformJsonBlazorManifest(inputManifest, outputManifest, volumes, os); - } else { - await transformXmlBlazorManifest(inputManifest, outputManifest, volumes, os); - } + await transformJsonBlazorManifest(inputManifest, outputManifest, volumes, os); } async function transformJsonBlazorManifest(inputManifest: string, outputManifest: string, volumes: DockerContainerVolume[], os: PlatformOS): Promise { const manifest = await fse.readJson(inputManifest); if (!manifest?.ContentRoots) { - throw new Error(l10n.t('Failed to parse Blazor static web assets manifest.')); + throw new Error(vscode.l10n.t('Failed to parse Blazor static web assets manifest.')); } if (!Array.isArray(manifest.ContentRoots)) { @@ -87,33 +59,6 @@ async function transformJsonBlazorManifest(inputManifest: string, outputManifest await fse.utimes(outputManifest, 0, 0); } -async function transformXmlBlazorManifest(inputManifest: string, outputManifest: string, volumes: DockerContainerVolume[], os: PlatformOS): Promise { - const contents = (await fse.readFile(inputManifest)).toString(); - const manifest = await xml2js.parseStringPromise(contents); - - if (!manifest?.StaticWebAssets) { - throw new Error(l10n.t('Failed to parse Blazor static web assets manifest.')); - } - - if (!Array.isArray(manifest.StaticWebAssets.ContentRoot)) { - return; - } - - for (const contentRoot of manifest.StaticWebAssets.ContentRoot) { - if (contentRoot && contentRoot.$) { - contentRoot.$.Path = tryContainerizePath(contentRoot.$.Path, volumes, os); - } - } - - const outputContents = (new xml2js.Builder()).buildObject(manifest); - - // Write out a new manifest - await fse.writeFile(outputManifest, outputContents); - - // Set the mtime to 1970 so that next time .NET builds, it will overwrite the output file - await fse.utimes(outputManifest, 0, 0); -} - function tryContainerizePath(oldPath: string, volumes: DockerContainerVolume[], os: PlatformOS): string { const matchingVolume: DockerContainerVolume = volumes.find(v => oldPath.toLowerCase().startsWith(v.localPath.toLowerCase())); diff --git a/src/utils/netCoreUtils.ts b/src/utils/netCoreUtils.ts index b4329517..07985a07 100644 --- a/src/utils/netCoreUtils.ts +++ b/src/utils/netCoreUtils.ts @@ -4,49 +4,123 @@ *--------------------------------------------------------------------------------------------*/ import { parseError } from '@microsoft/vscode-azext-utils'; -import { CommandLineArgs, composeArgs, withArg, withNamedArg, withQuotedArg } from '@microsoft/vscode-processutils'; -import * as fse from 'fs-extra'; +import { CommandLineArgs, composeArgs, withArg, withQuotedArg } from '@microsoft/vscode-processutils'; import * as path from 'path'; -import { l10n } from 'vscode'; -import { ext } from '../extensionVariables'; +import * as vscode from 'vscode'; +import { z } from 'zod'; import { execAsync } from './execAsync'; -import { getTempFileName } from './osUtils'; -export async function getNetCoreProjectInfo(target: 'GetBlazorManifestLocations' | 'GetProjectProperties', project: string, additionalProperties?: CommandLineArgs): Promise { - const targetsFile = path.join(ext.context.asAbsolutePath('resources'), 'netCore', `${target}.targets`); - const outputFile = getTempFileName(); +interface NetCoreCommonProjectInfo { + assemblyName: string; + targetFrameworks: string[]; + assemblyRelativeOutputPath: string; +} + +interface NetCoreContainerProjectInfo { + enableSdkContainerSupport: true; + assemblyContainerPath: string; + imageName: string; +} + +interface NetCoreNonContainerProjectInfo { + enableSdkContainerSupport: false; + assemblyContainerPath: never; + imageName: never; +} + +export type NetCoreProjectInfo = NetCoreCommonProjectInfo & (NetCoreContainerProjectInfo | NetCoreNonContainerProjectInfo); +const RawNetCoreProjectInfoSchema = z.object({ + Properties: z + .object({ + AssemblyName: z.string().min(1, vscode.l10n.t('AssemblyName must have a value')), + OutputPath: z.string().min(1, vscode.l10n.t('OutputPath must have a value')), + TargetFramework: z.string().optional(), + TargetFrameworks: z.string().optional(), + EnableSdkContainerSupport: z.stringbool().optional(), + ContainerWorkingDirectory: z.string().optional(), + ContainerRepository: z.string().optional(), + }) + .refine(info => info.TargetFramework || info.TargetFrameworks, vscode.l10n.t('Either TargetFramework or TargetFrameworks must have a value')) + .refine(info => !info.EnableSdkContainerSupport || (info.ContainerWorkingDirectory && info.ContainerRepository), vscode.l10n.t('ContainerWorkingDirectory and ContainerRepository must have values when EnableSdkContainerSupport is true')) +}); + +export async function getNetCoreProjectInfo(project: string, additionalProperties?: CommandLineArgs): Promise { const args = composeArgs( - withArg('build'), - withArg('/r:false'), - withArg(`/t:${target}`), // Target name doesn't need quoting - withNamedArg('/p:CustomAfterMicrosoftCommonTargets', `"${targetsFile}"`, { assignValue: true }), // We have to pre-quote the file paths because we cannot simultaneously use `assignValue` and `shouldQuote` - withNamedArg('/p:CustomAfterMicrosoftCommonCrossTargetingTargets', `"${targetsFile}"`, { assignValue: true }), - withNamedArg('/p:InfoOutputPath', `"${outputFile}"`, { assignValue: true }), + withArg('build', '--no-restore'), + withArg('-target:ComputeContainerConfig'), + withArg('-getProperty:AssemblyName,TargetFramework,TargetFrameworks,OutputPath,EnableSdkContainerSupport,ContainerWorkingDirectory,ContainerRepository'), withArg(...(additionalProperties ?? [])), withQuotedArg(project), )(); try { - try { - await execAsync('dotnet', args, { timeout: 20000 }); - } catch (err) { - const error = parseError(err); - throw new Error(l10n.t('Unable to determine project information for target \'{0}\' on project \'{1}\' {2}', target, project, error.message)); - } + const { stdout } = await execAsync('dotnet', args, { timeout: 20000 }); + const rawInfo = RawNetCoreProjectInfoSchema.parse(JSON.parse(stdout)); - if (await fse.pathExists(outputFile)) { - const contents = await fse.readFile(outputFile, 'utf-8'); + const assemblyName = `${rawInfo.Properties.AssemblyName}.dll`; + const targetFrameworks = rawInfo.Properties.TargetFrameworks ? + rawInfo.Properties.TargetFrameworks.split(';') : [rawInfo.Properties.TargetFramework!]; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- we know it must be one of the two due to the schema refinement - if (contents) { - return contents.split(/\r?\n/ig); - } - } + const commonInfo = { + assemblyName: assemblyName, + targetFrameworks: targetFrameworks, + assemblyRelativeOutputPath: path.join(rawInfo.Properties.OutputPath, assemblyName), + }; - throw new Error(l10n.t('Unable to determine project information for target \'{0}\' on project \'{1}\'', target, project)); - } finally { - if (await fse.pathExists(outputFile)) { - await fse.unlink(outputFile); + if (rawInfo.Properties.EnableSdkContainerSupport) { + return { + ...commonInfo, + enableSdkContainerSupport: true, + assemblyContainerPath: path.posix.join(rawInfo.Properties.ContainerWorkingDirectory!, assemblyName), // eslint-disable-line @typescript-eslint/no-non-null-assertion -- we know this is set if EnableSdkContainerSupport is true due to the schema refinement + imageName: rawInfo.Properties.ContainerRepository!, // eslint-disable-line @typescript-eslint/no-non-null-assertion -- we know this is set if EnableSdkContainerSupport is true due to the schema refinement + }; + } else { + return { + ...commonInfo, + enableSdkContainerSupport: false, + assemblyContainerPath: undefined as never, + imageName: undefined as never, + }; } + } catch (err) { + const error = parseError(err); + throw new Error(vscode.l10n.t('Unable to determine project information for project \'{0}\': {1}', project, error.message)); + } +} + +export interface BlazorManifestInfo { + inputManifestPath: string; + outputManifestPath: string; +} + +const RawBlazorManifestInfoSchema = z.object({ + Properties: z.object({ + MSBuildProjectDirectory: z.string().min(1, vscode.l10n.t('MSBuildProjectDirectory must have a value')), + StaticWebAssetDevelopmentManifestPath: z.string().min(1, vscode.l10n.t('StaticWebAssetDevelopmentManifestPath must have a value')), + OutputPath: z.string().min(1, vscode.l10n.t('OutputPath must have a value')), + TargetName: z.string().min(1, vscode.l10n.t('TargetName must have a value')), + }) +}); + +export async function getBlazorManifestInfo(project: string): Promise { + const args = composeArgs( + withArg('build', '--no-restore'), + withArg('-target:ResolveStaticWebAssetsConfiguration'), + withArg('-getProperty:MSBuildProjectDirectory,StaticWebAssetDevelopmentManifestPath,OutputPath,TargetName'), + withQuotedArg(project), + )(); + + try { + const { stdout } = await execAsync('dotnet', args, { timeout: 20000 }); + const rawInfo = RawBlazorManifestInfoSchema.parse(JSON.parse(stdout)); + + return { + inputManifestPath: path.join(rawInfo.Properties.MSBuildProjectDirectory, rawInfo.Properties.StaticWebAssetDevelopmentManifestPath), + outputManifestPath: path.join(rawInfo.Properties.MSBuildProjectDirectory, rawInfo.Properties.OutputPath, `${rawInfo.Properties.TargetName}.staticwebassets.runtime.json`), + }; + } catch (err) { + const error = parseError(err); + throw new Error(vscode.l10n.t('Unable to determine Blazor project information for project \'{0}\': {1}', project, error.message)); } }