diff --git a/Arcade.sln b/Arcade.sln index b6fae28925c..2a8f0465077 100644 --- a/Arcade.sln +++ b/Arcade.sln @@ -153,6 +153,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.GenAPI.Too EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.GenAPI.Tasks", "src\Microsoft.DotNet.GenAPI\Tasks\Microsoft.DotNet.GenAPI.Tasks.csproj", "{79AD710B-57F4-41CA-AC2F-42625B8978FD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.GenAPI.Tests", "src\Microsoft.DotNet.GenAPI\Tests\Microsoft.DotNet.GenAPI.Tests.csproj", "{EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -963,6 +965,18 @@ Global {79AD710B-57F4-41CA-AC2F-42625B8978FD}.Release|x64.Build.0 = Release|Any CPU {79AD710B-57F4-41CA-AC2F-42625B8978FD}.Release|x86.ActiveCfg = Release|Any CPU {79AD710B-57F4-41CA-AC2F-42625B8978FD}.Release|x86.Build.0 = Release|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Debug|x64.Build.0 = Debug|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Debug|x86.Build.0 = Debug|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Release|Any CPU.Build.0 = Release|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Release|x64.ActiveCfg = Release|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Release|x64.Build.0 = Release|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Release|x86.ActiveCfg = Release|Any CPU + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1004,6 +1018,7 @@ Global {4710F39B-B555-4B9B-AD4C-FBC777C87A1C} = {898AA2C6-9A76-4C0F-9DC3-518CDAA99087} {5CF1A5F1-31FE-46F0-9F45-971BD6B508D0} = {898AA2C6-9A76-4C0F-9DC3-518CDAA99087} {79AD710B-57F4-41CA-AC2F-42625B8978FD} = {898AA2C6-9A76-4C0F-9DC3-518CDAA99087} + {EBC3531D-97AF-494D-B2F8-2EAB47A9D58F} = {898AA2C6-9A76-4C0F-9DC3-518CDAA99087} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {32B9C883-432E-4FC8-A1BF-090EB033DD5B} diff --git a/Documentation/Operations/Helix-Machine-Management/Helix-Machine-Lifecycle-Processes.md b/Documentation/Operations/Helix-Machine-Management/Helix-Machine-Lifecycle-Processes.md index 18b091bf3b2..e6569af190a 100644 --- a/Documentation/Operations/Helix-Machine-Management/Helix-Machine-Lifecycle-Processes.md +++ b/Documentation/Operations/Helix-Machine-Management/Helix-Machine-Lifecycle-Processes.md @@ -117,6 +117,28 @@ If you feel this removal is in error, or believe a specific expiration should be - If removal is deemed inappropriate, make pull requests to the dotnet-helix-machines repo extending the time to a new, agreed-upon, date. - If pull requests are created, monitor subsequent builds in the pipeline until it has succeeded; - Open issues in the dotnet/arcade for all actions, with the "First Responder" tag. Add to list of queues for end-of-week update. + - Create pull requests removing these test or build images after the current week's rollout in the week they will be removed, and follow these until merged. + +- If any warnings about "Update-required" images arise: (e.g. "`##[warning] has update-required date: YYYY-MM-DD, in the next three weeks. Please either update the image to newer, file an issue requesting this, or extend the date with a comment explaining why if no action is taken.`") + - Check whether updated images exist: + - Refer to the yaml for these images, found under the "[definitions](https://dnceng.visualstudio.com/internal/_git/dotnet-helix-machines?path=/definitions)" folder of [the dotnet-helix-machines repo](https://dnceng.visualstudio.com/internal/_git/dotnet-helix-machines). Some windows images may be found in [definition-base\windows.base.yaml](https://dnceng.visualstudio.com/internal/_git/dotnet-helix-machines?path=/definition-base/windows.base.yaml) + - Find the image referenced in the yaml, or directly inside definitions\shared in the dotnet-helix-machines repository. + - Run the osob CLI. The following commands assume you have .NET Core runtime on the path and are inside the `tools\OsobCli` folder of this repository. + - `dotnet run list-image-versions -l westus -d ..\..\definitions` + - `dotnet run list-imagefactory-image-versions -d ..\..\definitions` + - Images with newer versions available will look like: +``` +For : + Current version: + Latest available version: + ** Upgrade! ** +``` + - If there are updated versions of the image: + - Wherever "Current Version" and "Latest Version" do not match, modify the image version to match the version printed out by the OSOB CLI tool above + - Set the new `UpdateRequiredDate` to 90 days after the previous date, or the `EstimatedRemovalDate`, (whichever comes first). + - Create a pull request with these changes and follow until merged. + - If there are no updated versions of the image, simply set the update-required date to 90 days past the previous one + - After merging any pull requests where multiple images are updated, as usual monitor "main" branch builds until they are successful. - If the main build has failed (red): - Ping [DncEng First Responders Teams Channel](https://teams.microsoft.com/l/channel/19%3aafba3d1545dd45d7b79f34c1821f6055%40thread.skype/First%2520Responders?groupId=4d73664c-9f2f-450d-82a5-c2f02756606d&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47) and ask for next steps. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 192a4ea6288..2ebe78dadee 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -5,13 +5,16 @@ trigger: - main - release/3.x - release/5.0 - + - release/6.0 + - release/7.0 pr: branches: include: - main - release/3.x - release/5.0 + - release/6.0 + - release/7.0 - templates variables: @@ -20,7 +23,7 @@ variables: resources: containers: - container: LinuxContainer - image: mcr.microsoft.com/dotnet-buildtools/prereqs:centos-7-20210714125435-9b5bbc2 + image: mcr.microsoft.com/dotnet-buildtools/prereqs:centos-7-latest stages: - stage: build diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7f4e30dc4c0..30e511a7a8a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -15,25 +15,25 @@ - + https://github.com/dotnet/arcade - bf47db2617320c82f94713d7b538f7bc0fa9d662 + 53c19829c61fa652687e90075ec391f7610d506f - + https://github.com/dotnet/arcade - bf47db2617320c82f94713d7b538f7bc0fa9d662 + 53c19829c61fa652687e90075ec391f7610d506f - + https://github.com/dotnet/arcade - bf47db2617320c82f94713d7b538f7bc0fa9d662 + 53c19829c61fa652687e90075ec391f7610d506f - + https://github.com/dotnet/arcade - bf47db2617320c82f94713d7b538f7bc0fa9d662 + 53c19829c61fa652687e90075ec391f7610d506f - + https://github.com/dotnet/arcade - bf47db2617320c82f94713d7b538f7bc0fa9d662 + 53c19829c61fa652687e90075ec391f7610d506f https://github.com/dotnet/arcade-services @@ -43,26 +43,26 @@ https://github.com/dotnet/arcade-services a5f3ed9d5f560555ff6d26b286acdcfbb7ce3b14 - + https://github.com/dotnet/xharness - a5369fa3807cec875ceaaaaf2502a7c51c3dbbf3 + a8e8fc0ffeb9b8c56ab799597597ce35d5be6111 - + https://github.com/dotnet/roslyn - 50008a99118bdb6091b692c393c1a1a28947f934 + 80a8ce8d5fdb9ceda4101e2acb8e8eb7be4ebcea https://github.com/dotnet/linker 3efd231da430baa0fd670e278f6b5c3e62834bde - + https://github.com/dotnet/sourcelink - e2d938cd694feb0d09bae5928ffe4ea1a4dfa9f2 + 9d3d07c9b25717607023b42a5a639117eadba860 - + https://github.com/dotnet/sourcelink - e2d938cd694feb0d09bae5928ffe4ea1a4dfa9f2 + 9d3d07c9b25717607023b42a5a639117eadba860 https://github.com/dotnet/symreader-converter diff --git a/eng/Versions.props b/eng/Versions.props index a33bfed3c3c..b8844fb22e6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -35,12 +35,12 @@ 3.17.2 2.3.13 2.1.0 - 2.1.1 - 2.1.1 + 6.0.0 + 6.0.0 2.0.0 - 2.1.1 + 6.0.0 2.1.0 - 4.3.0-3.22457.6 + 4.3.0-3.22470.13 17.4.0-preview-20220707-01 6.0.100-1.22103.2 1.0.0-v3.14.0.5722 @@ -48,44 +48,44 @@ 5.3.0.1 2.3.0 13.0.1 - 4.7.0 + 6.0.0 4.4.0 6.2.1 0.32.0 2.2.143 3.0.0 - 4.5.0 + 4.5.1 1.5.0 4.0.0 4.3.0 4.5.0 4.3.0 - 4.5.3 + 4.5.4 4.5.0 1.6.0 - 4.7.0 + 6.0.0 4.3.0 4.3.0 - 4.7.2 - 4.5.2 + 6.0.0 + 4.5.4 4.5.0 8.5.0 2.4.2 2.0.3 - 8.0.0-beta.22466.3 - 8.0.0-beta.22466.3 + 8.0.0-beta.22479.2 + 8.0.0-beta.22479.2 2.0.0 1.6.0 1.0.0 2.0.4 1.1.0-beta2-19575-01 1.1.0-beta.21553.1 - 1.2.0-beta-22468-01 - 1.2.0-beta-22468-01 - 8.0.0-beta.22466.3 + 1.2.0-beta-22478-02 + 1.2.0-beta-22478-02 + 8.0.0-beta.22479.2 1.0.0-beta.22462.1 1.1.0-beta.22076.4 - 1.0.0-prerelease.22467.1 + 1.0.0-prerelease.22473.1 2.0.0-preview.1.21526.15 2.0.0-preview.1.21526.15 7.0.100-preview.5.22273.2 diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index 8943da242f6..33a6f2d0e24 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -26,6 +26,7 @@ Param( [string] $runtimeSourceFeed = '', [string] $runtimeSourceFeedKey = '', [switch] $excludePrereleaseVS, + [switch] $nativeToolsOnMachine, [switch] $help, [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) @@ -67,6 +68,7 @@ function Print-Usage() { Write-Host " -warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio" + Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." @@ -146,6 +148,9 @@ try { $nodeReuse = $false } + if ($nativeToolsOnMachine) { + $env:NativeToolsOnMachine = $true + } if ($restore) { InitializeNativeTools } diff --git a/eng/common/init-tools-native.ps1 b/eng/common/init-tools-native.ps1 index ac42f04a9d8..fbc67effc36 100644 --- a/eng/common/init-tools-native.ps1 +++ b/eng/common/init-tools-native.ps1 @@ -113,6 +113,7 @@ try { $ToolPath = Convert-Path -Path $BinPath Write-Host "Adding $ToolName to the path ($ToolPath)..." Write-Host "##vso[task.prependpath]$ToolPath" + $env:PATH = "$ToolPath;$env:PATH" $InstalledTools += @{ $ToolName = $ToolDirectory.FullName } } } diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml index 781a41c9404..65f87b40c66 100644 --- a/eng/common/templates/job/execute-sdl.yml +++ b/eng/common/templates/job/execute-sdl.yml @@ -34,7 +34,7 @@ jobs: - job: Run_SDL dependsOn: ${{ parameters.dependsOn }} displayName: Run SDL tool - condition: eq( ${{ parameters.enable }}, 'true') + condition: and(succeededOrFailed(), eq( ${{ parameters.enable }}, 'true')) variables: - group: DotNet-VSTS-Bot - name: AzDOProjectName diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index 459f3c4fcbb..9f55d3f4666 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -25,6 +25,7 @@ parameters: enablePublishTestResults: false enablePublishUsingPipelines: false disableComponentGovernance: false + componentGovernanceIgnoreDirectories: '' mergeTestResults: false testRunTitle: '' testResultsFormat: '' @@ -146,6 +147,8 @@ jobs: - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), ne(parameters.disableComponentGovernance, 'true')) }}: - task: ComponentGovernanceComponentDetection@0 continueOnError: true + inputs: + ignoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: @@ -223,4 +226,5 @@ jobs: parameters: PackageVersion: ${{ parameters.packageVersion}} BuildDropPath: ${{ parameters.buildDropPath }} + IgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} diff --git a/eng/common/templates/jobs/source-build.yml b/eng/common/templates/jobs/source-build.yml index 00aa98eb3bf..bcd8279944b 100644 --- a/eng/common/templates/jobs/source-build.yml +++ b/eng/common/templates/jobs/source-build.yml @@ -14,7 +14,7 @@ parameters: # This is the default platform provided by Arcade, intended for use by a managed-only repo. defaultManagedPlatform: name: 'Managed' - container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-7-3e800f1-20190501005343' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8-latest' # Defines the platforms on which to run build jobs. One job is created for each platform, and the # object in this array is sent to the job template as 'platform'. If no platforms are specified, diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml index 87fcae940cf..258ed2d1108 100644 --- a/eng/common/templates/post-build/post-build.yml +++ b/eng/common/templates/post-build/post-build.yml @@ -98,7 +98,7 @@ stages: jobs: - job: displayName: NuGet Validation - condition: eq( ${{ parameters.enableNugetValidation }}, 'true') + condition: and(succeededOrFailed(), eq( ${{ parameters.enableNugetValidation }}, 'true')) pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: @@ -282,4 +282,4 @@ stages: -MaestroToken '$(MaestroApiAccessToken)' -WaitPublishingFinish true -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' - -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' \ No newline at end of file + -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' diff --git a/eng/common/templates/steps/generate-sbom.yml b/eng/common/templates/steps/generate-sbom.yml index 4cea8c33187..a06373f38fa 100644 --- a/eng/common/templates/steps/generate-sbom.yml +++ b/eng/common/templates/steps/generate-sbom.yml @@ -2,12 +2,14 @@ # PackageName - The name of the package this SBOM represents. # PackageVersion - The version of the package this SBOM represents. # ManifestDirPath - The path of the directory where the generated manifest files will be placed +# IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: PackageVersion: 7.0.0 BuildDropPath: '$(Build.SourcesDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom + IgnoreDirectories: '' sbomContinueOnError: true steps: @@ -34,6 +36,8 @@ steps: BuildDropPath: ${{ parameters.buildDropPath }} PackageVersion: ${{ parameters.packageVersion }} ManifestDirPath: ${{ parameters.manifestDirPath }} + ${{ if ne(parameters.IgnoreDirectories, '') }}: + AdditionalComponentDetectorArgs: '--IgnoreDirectories ${{ parameters.IgnoreDirectories }}' - task: PublishPipelineArtifact@1 displayName: Publish SBOM manifest diff --git a/eng/common/templates/steps/source-build.yml b/eng/common/templates/steps/source-build.yml index 12a8ff94d8e..4ec5577d28a 100644 --- a/eng/common/templates/steps/source-build.yml +++ b/eng/common/templates/steps/source-build.yml @@ -68,6 +68,11 @@ steps: publishArgs='--publish' fi + assetManifestFileName=SourceBuild_RidSpecific.xml + if [ '${{ parameters.platform.name }}' != '' ]; then + assetManifestFileName=SourceBuild_${{ parameters.platform.name }}.xml + fi + ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ --configuration $buildConfig \ --restore --build --pack $publishArgs -bl \ @@ -76,7 +81,8 @@ steps: $internalRestoreArgs \ $targetRidArgs \ /p:SourceBuildNonPortable=${{ parameters.platform.nonPortable }} \ - /p:ArcadeBuildFromSource=true + /p:ArcadeBuildFromSource=true \ + /p:AssetManifestFileName=$assetManifestFileName displayName: Build # Upload build logs for diagnosis. diff --git a/global.json b/global.json index 4ed4b8d5d25..f728fc98aa4 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "tools": { - "dotnet": "7.0.100-preview.7.22377.5" + "dotnet": "7.0.100-rc.1.22431.12" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.22466.3", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.22466.3" + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.22479.2", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.22479.2" } } diff --git a/src/Common/Microsoft.Arcade.Common/CompactConsoleLoggerFormatter.cs b/src/Common/Microsoft.Arcade.Common/CompactConsoleLoggerFormatter.cs new file mode 100644 index 00000000000..735776a5361 --- /dev/null +++ b/src/Common/Microsoft.Arcade.Common/CompactConsoleLoggerFormatter.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; + +#nullable enable +namespace Microsoft.Arcade.Common; + +/// +/// Copied over from SimpleConsoleFormatter. Leaves out the logger name and new line, turning +/// info: test[0] +/// Log message +/// Second line of the message +/// +/// into +/// +/// info: Log message +/// Second line of the message +/// +/// Only using SimpleConsoleFormatterOptions.SingleLine didn't help because multi-line messages +/// were put together on a single line so things like stack traces of exceptions were unreadable. +/// +/// See https://github.com/dotnet/runtime/blob/0817e748b7698bef1e812fd74c8a3558b7f86421/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs +/// +public class CompactConsoleLoggerFormatter : ConsoleFormatter +{ + private const string LoglevelPadding = ": "; + private const string DefaultForegroundColor = "\x1B[39m\x1B[22m"; // reset to default foreground color + private const string DefaultBackgroundColor = "\x1B[49m"; // reset to the background color + + public const string FormatterName = "compact"; + + private readonly SimpleConsoleFormatterOptions _options; + private readonly string _messagePadding; + private readonly string _newLineWithMessagePadding; + + public CompactConsoleLoggerFormatter(IOptionsMonitor options) + : base(FormatterName) + { + _options = options.CurrentValue; + _messagePadding = new string(' ', GetLogLevelString(LogLevel.Information).Length + LoglevelPadding.Length + (_options.TimestampFormat?.Length ?? 0)); + _newLineWithMessagePadding = Environment.NewLine + _messagePadding; + } + + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) + { + if (logEntry.Formatter == null) + { + return; + } + + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + if (logEntry.Exception == null && message == null) + { + return; + } + + LogLevel logLevel = logEntry.LogLevel; + var logLevelColors = GetLogLevelConsoleColors(logLevel); + var logLevelString = GetLogLevelString(logLevel); + + if (_options.TimestampFormat != null) + { + var timestamp = DateTimeOffset.Now.ToString(_options.TimestampFormat); + textWriter.Write(timestamp); + } + + WriteColoredMessage(textWriter, logLevelString, logLevelColors.Background, logLevelColors.Foreground); + + textWriter.Write(LoglevelPadding); + + WriteMessage(textWriter, message, false); + + // Example: + // System.InvalidOperationException + // at Namespace.Class.Function() in File:line X + if (logEntry.Exception != null) + { + // exception message + WriteMessage(textWriter, logEntry.Exception.ToString()); + } + } + + private void WriteMessage(TextWriter textWriter, string message, bool includePadding = true) + { + if (message == null) + { + return; + } + + if (includePadding) + { + textWriter.Write(_messagePadding); + } + + textWriter.WriteLine(message.Replace(Environment.NewLine, _newLineWithMessagePadding)); + } + + private static string GetLogLevelString(LogLevel logLevel) => logLevel switch + { + LogLevel.Trace => "trce", + LogLevel.Debug => "dbug", + LogLevel.Information => "info", + LogLevel.Warning => "warn", + LogLevel.Error => "fail", + LogLevel.Critical => "crit", + _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) + }; + + private (ConsoleColor? Foreground, ConsoleColor? Background) GetLogLevelConsoleColors(LogLevel logLevel) + { + if (_options.ColorBehavior == LoggerColorBehavior.Disabled) + { + return (null, null); + } + + // We must explicitly set the background color if we are setting the foreground color, + // since just setting one can look bad on the users console. + return logLevel switch + { + LogLevel.Trace => (ConsoleColor.Gray, ConsoleColor.Black), + LogLevel.Debug => (ConsoleColor.Gray, ConsoleColor.Black), + LogLevel.Information => (ConsoleColor.DarkGreen, ConsoleColor.Black), + LogLevel.Warning => (ConsoleColor.Yellow, ConsoleColor.Black), + LogLevel.Error => (ConsoleColor.Black, ConsoleColor.DarkRed), + LogLevel.Critical => (ConsoleColor.White, ConsoleColor.DarkRed), + _ => (null, null) + }; + } + + private static void WriteColoredMessage(TextWriter textWriter, string message, ConsoleColor? background, ConsoleColor? foreground) + { + // Order: backgroundcolor, foregroundcolor, Message, reset foregroundcolor, reset backgroundcolor + if (background.HasValue) + { + textWriter.Write(GetBackgroundColorEscapeCode(background.Value)); + } + + if (foreground.HasValue) + { + textWriter.Write(GetForegroundColorEscapeCode(foreground.Value)); + } + + textWriter.Write(message); + + if (foreground.HasValue) + { + textWriter.Write(DefaultForegroundColor); // reset to default foreground color + } + + if (background.HasValue) + { + textWriter.Write(DefaultBackgroundColor); // reset to the background color + } + } + + private static string GetForegroundColorEscapeCode(ConsoleColor color) => color switch + { + ConsoleColor.Black => "\x1B[30m", + ConsoleColor.DarkRed => "\x1B[31m", + ConsoleColor.DarkGreen => "\x1B[32m", + ConsoleColor.DarkYellow => "\x1B[33m", + ConsoleColor.DarkBlue => "\x1B[34m", + ConsoleColor.DarkMagenta => "\x1B[35m", + ConsoleColor.DarkCyan => "\x1B[36m", + ConsoleColor.Gray => "\x1B[37m", + ConsoleColor.Red => "\x1B[1m\x1B[31m", + ConsoleColor.Green => "\x1B[1m\x1B[32m", + ConsoleColor.Yellow => "\x1B[1m\x1B[33m", + ConsoleColor.Blue => "\x1B[1m\x1B[34m", + ConsoleColor.Magenta => "\x1B[1m\x1B[35m", + ConsoleColor.Cyan => "\x1B[1m\x1B[36m", + ConsoleColor.White => "\x1B[1m\x1B[37m", + _ => DefaultForegroundColor // default foreground color + }; + + private static string GetBackgroundColorEscapeCode(ConsoleColor color) => color switch + { + ConsoleColor.Black => "\x1B[40m", + ConsoleColor.DarkRed => "\x1B[41m", + ConsoleColor.DarkGreen => "\x1B[42m", + ConsoleColor.DarkYellow => "\x1B[43m", + ConsoleColor.DarkBlue => "\x1B[44m", + ConsoleColor.DarkMagenta => "\x1B[45m", + ConsoleColor.DarkCyan => "\x1B[46m", + ConsoleColor.Gray => "\x1B[47m", + _ => DefaultBackgroundColor // Use default background color + }; +} diff --git a/src/Common/Microsoft.Arcade.Common/Microsoft.Arcade.Common.csproj b/src/Common/Microsoft.Arcade.Common/Microsoft.Arcade.Common.csproj index 41977e73fed..8b90ff620bf 100644 --- a/src/Common/Microsoft.Arcade.Common/Microsoft.Arcade.Common.csproj +++ b/src/Common/Microsoft.Arcade.Common/Microsoft.Arcade.Common.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.cs b/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.cs index 53fbe66caf4..e33b2094a27 100644 --- a/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.cs +++ b/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.cs @@ -291,7 +291,7 @@ private ref struct TypeNameRecursiveState } private int WriteTypeNameRecursive(ITypeReference type, NameFormattingOptions namingOptions, - string[] valueTupleNames, ref int valueTupleNameIndex, ref int nullableIndex, ref TypeNameRecursiveState state, + string[] valueTupleNames, scoped ref int valueTupleNameIndex, scoped ref int nullableIndex, ref TypeNameRecursiveState state, int typeDepth = 0, int genericParameterIndex = 0, bool isValueTupleParameter = false) { object dynamicAttributeArgument = state.DynamicAttributeArgument; diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/VisualStudio.BuildIbcTrainingSettings.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/VisualStudio.BuildIbcTrainingSettings.proj index abf2b732160..b637f86bd7c 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/VisualStudio.BuildIbcTrainingSettings.proj +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/VisualStudio.BuildIbcTrainingSettings.proj @@ -17,7 +17,6 @@ --> - <_VisualStudioBuildTasksAssembly>$(NuGetPackageRoot)microsoft.dotnet.build.tasks.visualstudio\$(MicrosoftDotNetBuildTasksVisualStudioVersion)\tools\net472\Microsoft.DotNet.Build.Tasks.VisualStudio.dll @@ -43,4 +42,4 @@ Query="/RunSettings/SessionConfiguration" Value="$(_SessionConfiguration)" /> - \ No newline at end of file + diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs index 40e9cabdcda..4c60ced7450 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/CreateVisualStudioWorkloadTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Arcade.Test.Common; @@ -38,6 +40,7 @@ public static void ItCanCreateWorkloads() new TaskItem("microsoft-net-sdk-emscripten") .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") + .WithMetadata(Metadata.Version, "5.6.7.8") }; ITaskItem[] shortNames = new[] @@ -85,12 +88,13 @@ public static void ItCanCreateWorkloads() Assert.Equal("x64;1033", si.Template); // Verify the SWIX authoring for the component representing the workload in VS. - string componentSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "microsoft.net.sdk.emscripten.6.0.4", "component.swr")); + string componentSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "microsoft.net.sdk.emscripten.5.6.7.8", "component.swr")); Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr); // Emscripten is an abstract workload so it should be a component group. Assert.Contains("vs.package.type=component", componentSwr); Assert.Contains("isUiGroup=yes", componentSwr); + Assert.Contains("version=5.6.7.8", componentSwr); // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the @@ -100,15 +104,120 @@ public static void ItCanCreateWorkloads() Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr); // Verify the SWIX authoring for the VS package wrapping the manifest MSI - string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "Emscripten.Manifest-6.0.200", "x64", "msi.swr")); + string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "x64", "msi.swr")); Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr); Assert.Contains("vs.package.type=msi", manifestMsiSwr); + Assert.Contains("vs.package.chip=x64", manifestMsiSwr); + Assert.DoesNotContain("vs.package.machineArch", manifestMsiSwr); + + // Verify that no arm64 MSI authoring for VS. EMSDK doesn't define RIDs for arm64, but manifests always generate + // arm64 MSIs for the CLI based installs so we should not see that. + string swixRootDirectory = Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200"); + IEnumerable arm64Directories = Directory.EnumerateDirectories(swixRootDirectory, "arm64", SearchOption.AllDirectories); + Assert.DoesNotContain(arm64Directories, s => s.Contains("arm64")); // Verify the SWIX authoring for one of the workload pack MSIs. Packs get assigned random sub-folders so we // need to filter out the SWIX project output items the task produced. ITaskItem pythonPackSwixItem = createWorkloadTask.SwixProjects.Where(s => s.ItemSpec.Contains(@"Microsoft.Emscripten.Python.6.0.4\x64")).FirstOrDefault(); string packMsiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(pythonPackSwixItem.ItemSpec), "msi.swr")); Assert.Contains("package name=Microsoft.Emscripten.Python.6.0.4", packMsiSwr); + Assert.Contains("vs.package.chip=x64", packMsiSwr); + Assert.DoesNotContain("vs.package.machineArch", packMsiSwr); + } + + [WindowsOnlyFact] + public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio() + { + // Create intermediate outputs under %temp% to avoid path issues and make sure it's clean so we don't pick up + // conflicting sources from previous runs. + string baseIntermediateOutputPath = Path.Combine(Path.GetTempPath(), "WLa64"); + + if (Directory.Exists(baseIntermediateOutputPath)) + { + Directory.Delete(baseIntermediateOutputPath, recursive: true); + } + + ITaskItem[] manifestsPackages = new[] + { + new TaskItem(Path.Combine(TestBase.TestAssetsPath, "microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4.nupkg")) + .WithMetadata(Metadata.MsiVersion, "6.33.28") + .WithMetadata(Metadata.SupportsMachineArch, "true") + }; + + ITaskItem[] componentResources = new[] + { + new TaskItem("microsoft-net-sdk-emscripten") + .WithMetadata(Metadata.Title, ".NET WebAssembly Build Tools (Emscripten)") + .WithMetadata(Metadata.Description, "Build tools for WebAssembly ahead-of-time (AoT) compilation and native linking.") + .WithMetadata(Metadata.Version, "5.6.7.8") + }; + + ITaskItem[] shortNames = new[] + { + new TaskItem("Microsoft.NET.Workload.Emscripten").WithMetadata("Replacement", "Emscripten"), + new TaskItem("microsoft.netcore.app.runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("Microsoft.NETCore.App.Runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("microsoft.net.runtime").WithMetadata("Replacement", "Microsoft"), + new TaskItem("Microsoft.NET.Runtime").WithMetadata("Replacement", "Microsoft") + }; + + IBuildEngine buildEngine = new MockBuildEngine(); + + CreateVisualStudioWorkload createWorkloadTask = new CreateVisualStudioWorkload() + { + AllowMissingPacks = true, + BaseOutputPath = TestBase.BaseOutputPath, + BaseIntermediateOutputPath = baseIntermediateOutputPath, + BuildEngine = buildEngine, + ComponentResources = componentResources, + ManifestMsiVersion = null, + PackageSource = TestBase.TestAssetsPath, + ShortNames = shortNames, + WixToolsetPath = TestBase.WixToolsetPath, + WorkloadManifestPackageFiles = manifestsPackages, + }; + + bool result = createWorkloadTask.Execute(); + + Assert.True(result); + ITaskItem manifestMsiItem = createWorkloadTask.Msis.Where(m => m.ItemSpec.ToLowerInvariant().Contains("microsoft.net.workload.emscripten.manifest-6.0.200.6.0.4-arm64.msi")).FirstOrDefault(); + Assert.NotNull(manifestMsiItem); + + // Spot check one of the manifest MSIs. We have additional tests that cover MSI generation. + // The UpgradeCode is predictable/stable for manifest MSIs since they are upgradable withing an SDK feature band, + Assert.Equal("{CBA7CF4A-F3C9-3B75-8F1F-0D08AF7CD7BE}", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.UpgradeCode)); + // The version should match the value passed to the build task. For actual builds like dotnet/runtiem, this value would + // be generated. + Assert.Equal("6.33.28", MsiUtils.GetProperty(manifestMsiItem.ItemSpec, MsiProperty.ProductVersion)); + Assert.Equal("Microsoft.NET.Workload.Emscripten,6.0.200,arm64", MsiUtils.GetProviderKeyName(manifestMsiItem.ItemSpec)); + + // Process the template in the summary information stream. This is the only way to verify the intended platform + // of the MSI itself. + using SummaryInfo si = new(manifestMsiItem.ItemSpec, enableWrite: false); + Assert.Equal("Arm64;1033", si.Template); + + // Verify the SWIX authoring for the component representing the workload in VS. + string componentSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "microsoft.net.sdk.emscripten.5.6.7.8", "component.swr")); + Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr); + + // Emscripten is an abstract workload so it should be a component group. + Assert.Contains("vs.package.type=component", componentSwr); + Assert.Contains("isUiGroup=yes", componentSwr); + Assert.Contains("version=5.6.7.8", componentSwr); + + // Verify pack dependencies. These should map to MSI packages. The VS package IDs should be the non-aliased + // pack IDs and version from the workload manifest. The actual VS packages will point to the MSIs generated from the + // aliased workload pack packages. + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Node.6.0.4", componentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Python.6.0.4", componentSwr); + Assert.Contains("vs.dependency id=Microsoft.Emscripten.Sdk.6.0.4", componentSwr); + + // Verify the SWIX authoring for the VS package wrapping the manifest MSI + string manifestMsiSwr = File.ReadAllText(Path.Combine(baseIntermediateOutputPath, "src", "swix", "6.0.200", "Emscripten.Manifest-6.0.200", "arm64", "msi.swr")); + Assert.Contains("package name=Emscripten.Manifest-6.0.200", manifestMsiSwr); + Assert.Contains("vs.package.type=msi", manifestMsiSwr); + Assert.DoesNotContain("vs.package.chip", manifestMsiSwr); + Assert.Contains("vs.package.machineArch=arm64", manifestMsiSwr); } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs index d1ddd8f070c..21ce62b5cb2 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixComponentTests.cs @@ -30,6 +30,7 @@ public void ItAssignsDefaultValues() string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); Assert.Contains("package name=microsoft.net.sdk.blazorwebassembly.aot", componentSwr); + Assert.Contains("version=1.0.0", componentSwr); string componentResSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.res.swr")); Assert.Contains(@"title=""Blazor WebAssembly AOT workload""", componentResSwr); @@ -37,6 +38,34 @@ public void ItAssignsDefaultValues() Assert.Contains(@"category="".NET""", componentResSwr); } + [WindowsOnlyFact] + public void ItPrefersComponentResourcesOverDefaults() + { + ITaskItem[] componentResources = new[] + { + new TaskItem("microsoft-net-sdk-blazorwebassembly-aot").WithMetadata(Metadata.Version, "4.5.6") + .WithMetadata(Metadata.Description, "A long wordy description about Blazor.") + .WithMetadata(Metadata.Category, "WebAssembly") + }; + + WorkloadManifest manifest = Create("WorkloadManifest.json"); + WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value; + SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, + componentResources); + + ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath); + string swixProj = project.Create(); + + string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr")); + Assert.Contains("package name=microsoft.net.sdk.blazorwebassembly.aot", componentSwr); + Assert.Contains("version=4.5.6", componentSwr); + + string componentResSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.res.swr")); + Assert.Contains(@"title=""Blazor WebAssembly AOT workload""", componentResSwr); + Assert.Contains(@"description=""A long wordy description about Blazor.""", componentResSwr); + Assert.Contains(@"category=""WebAssembly""", componentResSwr); + } + [WindowsOnlyFact] public void ItShortensComponentIds() { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs index 9c48496b694..69a724318a2 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads.Tests/SwixPackageTests.cs @@ -6,6 +6,7 @@ using Microsoft.Arcade.Test.Common; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Microsoft.Deployment.DotNet.Releases; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; using Microsoft.DotNet.Build.Tasks.Workloads.Swix; using Microsoft.NET.Sdk.WorkloadManifestReader; @@ -25,10 +26,12 @@ public void ItThrowsIfPackageRelativePathExceedsLimit() Exception e = Assert.Throws(() => { - MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath); + MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath, + new ReleaseVersion("6.0.100"), + chip: "x64", machineArch: "x64", productArch: "neutral"); }); - Assert.Equal(@"Relative package path exceeds the maximum length (182): Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100,version=6.0.0.0,chip=x64,productarch=neutral\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi.", e.Message); + Assert.Equal(@"Relative package path exceeds the maximum length (182): Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100,version=6.0.0.0,chip=x64,productarch=neutral,machinearch=x64\Microsoft.NET.Workload.Mono.ToolChain.Manifest-6.0.100.6.0.0-preview.7.21377.12-x64.msi.", e.Message); } [WindowsOnlyFact] @@ -48,5 +51,36 @@ public void SwixPackageIdsIncludeThePackageVersion() Assert.Equal("Microsoft.iOS.Templates.15.2.302-preview.14.122", item.GetMetadata(Metadata.SwixPackageId)); } + + [WindowsOnlyFact] + public void ItOnlyIncludesDefinedPropertiesForMsiPackages() + { + // Build to a different path to avoid any file read locks on the MSI from other tests + // that can open it. + string PackageRootDirectory = Path.Combine(BaseIntermediateOutputPath, Path.GetRandomFileName()); + string packagePath = Path.Combine(TestAssetsPath, "microsoft.ios.templates.15.2.302-preview.14.122.nupkg"); + + WorkloadPack p = new(new WorkloadPackId("Microsoft.iOS.Templates"), "15.2.302-preview.14.122", WorkloadPackKind.Template, null); + TemplatePackPackage pkg = new(p, packagePath, new[] { "x64" }, PackageRootDirectory); + pkg.Extract(); + WorkloadPackMsi msi = new(pkg, "x64", new MockBuildEngine(), WixToolsetPath, BaseIntermediateOutputPath); + + ITaskItem msiItem = msi.Build(MsiOutputPath); + msiItem.SetMetadata(Metadata.Platform, "x64"); + + Assert.Equal("Microsoft.iOS.Templates.15.2.302-preview.14.122", msiItem.GetMetadata(Metadata.SwixPackageId)); + + MsiSwixProject swixProject = new(msiItem, BaseIntermediateOutputPath, BaseOutputPath, + new ReleaseVersion("6.0.100"), + chip: msiItem.GetMetadata(Metadata.Platform), + machineArch: DefaultValues.x64); + string swixProj = swixProject.Create(); + string msiSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "msi.swr")); + + Assert.DoesNotContain("vs.package.productArch", msiSwr); + Assert.Contains("vs.package.chip=x64", msiSwr); + Assert.Contains("vs.package.machineArch=x64", msiSwr); + } + } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs index 38ae8da0597..bbbb4608956 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/BuildData.wix.cs @@ -23,7 +23,7 @@ public WorkloadPackPackage Package /// /// For each platform, the set of feature bands that include contain a reference to this pack. /// - public Dictionary> FeatureBands = new(); + public Dictionary> FeatureBands = new(); public BuildData(WorkloadPackPackage package) { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs index 252833af699..dab6cadf67e 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs @@ -21,6 +21,11 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads /// public class CreateVisualStudioWorkload : Task { + /// + /// Used to track which feature bands support the machineArch property. + /// + private Dictionary _supportsMachineArch = new(); + /// /// A set of all supported MSI platforms. /// @@ -200,10 +205,23 @@ public override bool Execute() foreach (ITaskItem workloadManifestPackageFile in WorkloadManifestPackageFiles) { // 1. Process the manifest package and create a set of installers. - WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, + WorkloadManifestPackage manifestPackage = new(workloadManifestPackageFile, PackageRootDirectory, string.IsNullOrWhiteSpace(ManifestMsiVersion) ? null : new Version(ManifestMsiVersion), ShortNames, Log); manifestPackages.Add(manifestPackage); + if (!_supportsMachineArch.ContainsKey(manifestPackage.SdkFeatureBand)) + { + // Log the original setting and manifest that created the machineArch setting for the featureband. + Log.LogMessage(MessageImportance.Low, $"Setting {nameof(_supportsMachineArch)} to {manifestPackage.SupportsMachineArch} for {Path.GetFileName(manifestPackage.PackageFileName)}"); + _supportsMachineArch[manifestPackage.SdkFeatureBand] = manifestPackage.SupportsMachineArch; + } + else if (_supportsMachineArch[manifestPackage.SdkFeatureBand] != manifestPackage.SupportsMachineArch) + { + // If multiple manifest packages for the same feature band have conflicting machineArch values + // then we'll treat it as an warning. It will likely fail the build. + Log.LogWarning($"{_supportsMachineArch} was previously set to {_supportsMachineArch[manifestPackage.SdkFeatureBand]}"); + } + Dictionary manifestMsisByPlatform = new(); foreach (string platform in SupportedPlatforms) { @@ -244,7 +262,7 @@ public override bool Execute() }; packGroupJsonList.Add(packGroupJson); } - + foreach (WorkloadPackId packId in wd.Packs) { @@ -306,7 +324,7 @@ public override bool Execute() buildData.Remove(sourcePackage); } } - } + } if (CreateWorkloadPackGroups) { @@ -369,23 +387,29 @@ public override bool Execute() msiItems.Add(msiOutputItem); } - MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); - string swixProj = swixProject.Create(); - foreach (ReleaseVersion sdkFeatureBand in data.FeatureBands[platform]) { - ITaskItem swixProjectItem = new TaskItem(swixProj); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}"); - - lock (swixProjectItems) + // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch + if (_supportsMachineArch[sdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) { - swixProjectItems.Add(swixProjectItem); + MsiSwixProject swixProject = _supportsMachineArch[sdkFeatureBand] ? + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)) : + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, sdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform)); + string swixProj = swixProject.Create(); + + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{sdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } } } }); }); - // Parallel processing of pack groups was causing file access errors for heat in an earlier version of this code // So we support a flag to disable the parallelization if that starts happening again PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroupPackages.Values, packGroup => @@ -411,39 +435,51 @@ public override bool Execute() if (UseWorkloadPackGroupsForVS) { - MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); - string swixProj = swixProject.Create(); - PossiblyParallelForEach(!DisableParallelPackageGroupProcessing, packGroup.ManifestsPerPlatform[platform], manifestPackage => { - ITaskItem swixProjectItem = new TaskItem(swixProj); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{manifestPackage.SdkFeatureBand}"); - - lock (swixProjectItems) + // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch + if (_supportsMachineArch[manifestPackage.SdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) { - swixProjectItems.Add(swixProjectItem); + MsiSwixProject swixProject = _supportsMachineArch[manifestPackage.SdkFeatureBand] ? + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, manifestPackage.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)) : + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, manifestPackage.SdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform)); + string swixProj = swixProject.Create(); + + ITaskItem swixProjectItem = new TaskItem(swixProj); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{manifestPackage.SdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiPack); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } } }); } } }); - // Generate MSIs for the workload manifests along with - // a .csproj to package the MSI and a SWIX project for + // Generate MSIs for the workload manifests along with a .csproj to package the MSI and a SWIX project for // Visual Studio. _ = Parallel.ForEach(manifestMsisToBuild, msi => { ITaskItem msiOutputItem = msi.Build(MsiOutputPath, IceSuppressions); - // Generate SWIX authoring for the MSI package. - MsiSwixProject swixProject = new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath); - ITaskItem swixProjectItem = new TaskItem(swixProject.Create()); - swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{((WorkloadManifestPackage)msi.Package).SdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, swixProject.PackageType); - - lock (swixProjectItems) + // Don't generate a SWIX package if the MSI targets arm64 and VS doesn't support machineArch + if (_supportsMachineArch[msi.Package.SdkFeatureBand] || !string.Equals(msiOutputItem.GetMetadata(Metadata.Platform), DefaultValues.arm64)) { - swixProjectItems.Add(swixProjectItem); + // Generate SWIX authoring for the MSI package. + MsiSwixProject swixProject = _supportsMachineArch[msi.Package.SdkFeatureBand] ? + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.Package.SdkFeatureBand, chip: null, machineArch: msiOutputItem.GetMetadata(Metadata.Platform)) : + new(msiOutputItem, BaseIntermediateOutputPath, BaseOutputPath, msi.Package.SdkFeatureBand, chip: msiOutputItem.GetMetadata(Metadata.Platform)); + ITaskItem swixProjectItem = new TaskItem(swixProject.Create()); + swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{((WorkloadManifestPackage)msi.Package).SdkFeatureBand}"); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeMsiManifest); + + lock (swixProjectItems) + { + swixProjectItems.Add(swixProjectItem); + } } // Generate a .csproj to package the MSI and its manifest for CLI installs. @@ -464,7 +500,7 @@ public override bool Execute() ComponentSwixProject swixComponentProject = new(swixComponent, BaseIntermediateOutputPath, BaseOutputPath); ITaskItem swixProjectItem = new TaskItem(swixComponentProject.Create()); swixProjectItem.SetMetadata(Metadata.SdkFeatureBand, $"{swixComponent.SdkFeatureBand}"); - swixProjectItem.SetMetadata(Metadata.PackageType, swixComponentProject.PackageType); + swixProjectItem.SetMetadata(Metadata.PackageType, DefaultValues.PackageTypeComponent); lock (swixProjectItems) { diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs index 4eba3f9c4e5..b4fbc9851e5 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs @@ -18,5 +18,25 @@ internal static class DefaultValues /// The default value to assign to the Manufacturer property of an MSI. /// public static readonly string Manufacturer = "Microsoft Corporation"; + + public static readonly string x86 = "x64"; + public static readonly string x64 = "x64"; + public static readonly string arm64 = "arm64"; + public static readonly string Neutral = "neutral"; + + /// + /// A value indicating that the SWIX project creates an MSI package for a workload manifest. + /// + public static readonly string PackageTypeMsiManifest = "msi-manifest"; + + /// + /// A value indicating that the SWIX project creates an MSI package for a workload pack. + /// + public static readonly string PackageTypeMsiPack = "msi-pack"; + + /// + /// A value indicating that the SWIX project creates a component package for a workload. + /// + public static readonly string PackageTypeComponent = "component"; } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Extensions.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Extensions.cs new file mode 100644 index 00000000000..6bb0028407b --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Extensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Build.Tasks.Workloads +{ + public static class Extensions + { + /// + /// Determines if the item contains a the specified metadata. + /// + /// The item to evaluate. + /// The name of the metadata to check. + /// if the metadata exists; otherwise. + public static bool HasMetadata(this ITaskItem item, string metadataName) + { + foreach (string name in item.MetadataNames) + { + if (string.Equals(metadataName, name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs index 48b64b3f157..40a637ff763 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Metadata.cs @@ -10,21 +10,39 @@ internal static class Metadata { public static readonly string AliasTo = nameof(AliasTo); public static readonly string Category = nameof(Category); + public static readonly string Chip = nameof(Chip); public static readonly string Description = nameof(Description); public static readonly string Filename = nameof(Filename); public static readonly string FullPath = nameof(FullPath); public static readonly string JsonProperties = nameof(JsonProperties); + + /// + /// Metadata describing the native machine architecture of an installation + /// package for VS. + /// + public static readonly string MachineArch = nameof(MachineArch); public static readonly string MsiVersion = nameof(MsiVersion); + + /// + /// Metadata describing the platform of the MSI itself. + /// public static readonly string Platform = nameof(Platform); public static readonly string RelativeDir = nameof(RelativeDir); public static readonly string Replacement = nameof(Replacement); public static readonly string PackageProject = nameof(PackageProject); public static readonly string PackageType = nameof(PackageType); + public static readonly string ProductArch = nameof(ProductArch); public static readonly string SdkFeatureBand = nameof(SdkFeatureBand); public static readonly string ShortName = nameof(ShortName); public static readonly string SourcePackage = nameof(SourcePackage); public static readonly string SwixPackageId = nameof(SwixPackageId); public static readonly string SwixProject = nameof(SwixProject); + + /// + /// Metadata describing whether the VS authoring supports the machineArch SWIX property. + /// + public static readonly string SupportsMachineArch = nameof(SupportsMachineArch); + public static readonly string Title = nameof(Title); public static readonly string Version = nameof(Version); diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs index 9c07f47fddd..9701fe994bf 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.Designer.cs @@ -87,6 +87,24 @@ internal static string CannotExtractSdkVersionFromPackageId { } } + /// + /// Looks up a localized string similar to At least one of the following properties must contain a valid value: Chip, MachineArch.. + /// + internal static string ChipOrMachineArchRequired { + get { + return ResourceManager.GetString("ChipOrMachineArchRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Components must have define at lease on category.. + /// + internal static string ComponentCategoryCannotBeNull { + get { + return ResourceManager.GetString("ComponentCategoryCannotBeNull", resourceCulture); + } + } + /// /// Looks up a localized string similar to Components cannot have a null description. Either provide a custom resource or add a description to the workload definition.. /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx index e68d9f88641..d9287552055 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Strings.resx @@ -126,6 +126,12 @@ Unable to extract the SDK version from the package ID: {0}. + + At least one of the following properties must contain a valid value: Chip, MachineArch. + + + Components must define at least one category. + Components cannot have a null description. Either provide a custom resource or add a description to the workload definition. diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs index 1b122f5f486..a82d3f2ac9b 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/ComponentSwixProject.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.IO; -using System.Linq; namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix { @@ -15,9 +13,6 @@ internal class ComponentSwixProject : SwixProjectBase { private SwixComponent _component; - /// - public override string PackageType => "component"; - protected override string ProjectFile { get; @@ -33,7 +28,7 @@ public ComponentSwixProject(SwixComponent component, string baseIntermediateOutp base(component.Name, component.Version, baseIntermediateOutputPath, baseOutputPath) { _component = component; - ValidateRelativePackagePath($@"{component.Name},version={component.Version}\_package.json"); + ValidateRelativePackagePath(GetRelativePackagePath()); // Components must have 1 or more dependencies. if (!component.HasDependencies) @@ -41,7 +36,7 @@ public ComponentSwixProject(SwixComponent component, string baseIntermediateOutp throw new ArgumentException(string.Format(Strings.ComponentMustHaveAtLeastOneDependency, component.Name)); } - ProjectSourceDirectory = Path.Combine(SwixDirectory, $"{component.SdkFeatureBand}", + ProjectSourceDirectory = Path.Combine(SwixDirectory, $"{component.SdkFeatureBand}", $"{component.Name}.{component.Version}"); ReplacementTokens[SwixTokens.__VS_COMPONENT_TITLE__] = component.Title; @@ -76,5 +71,9 @@ public override string Create() return swixProj; } + + /// + protected override string GetRelativePackagePath() => + Path.Combine(base.GetRelativePackagePath(), "_package.json"); } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs index 975240b63ca..02fae6cfcc0 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/MsiSwixProject.wix.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Text; using System.IO; using System.Security.Cryptography; -using System.Text; using Microsoft.Build.Framework; using Microsoft.DotNet.Build.Tasks.Workloads.Msi; +using Microsoft.Deployment.DotNet.Releases; namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix { @@ -15,6 +16,8 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Swix /// public class MsiSwixProject : SwixProjectBase { + ITaskItem _msi; + /// /// The target platform of the package. /// @@ -23,8 +26,29 @@ protected string Chip get; } - /// - public override string PackageType => "Msi"; + /// + /// The machine architecture of the package. + /// + protected string MachineArch + { + get; + } + + /// + /// The platform associated with the MSI. + /// + protected string Platform + { + get; + } + + /// + /// The product architecture of Visual Studio. + /// + protected string ProductArch + { + get; + } /// protected override string ProjectFile @@ -38,36 +62,97 @@ protected override string ProjectSourceDirectory get; } - public MsiSwixProject(ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, - string visualStudioProductArchitecture = "neutral") : base(msi.GetMetadata(Metadata.SwixPackageId), new Version(msi.GetMetadata(Metadata.Version)), baseIntermediateOutputPath, baseOutputPath) + public MsiSwixProject(ITaskItem msi, string baseIntermediateOutputPath, string baseOutputPath, + ReleaseVersion sdkFeatureBand, + string chip = null, string machineArch = null, string productArch = null) : base(msi.GetMetadata(Metadata.SwixPackageId), new Version(msi.GetMetadata(Metadata.Version)), baseIntermediateOutputPath, baseOutputPath) { - Chip = msi.GetMetadata(Metadata.Platform); - ProjectSourceDirectory = Path.Combine(SwixDirectory, Id, Chip); + _msi = msi; + Chip = chip; + MachineArch = machineArch; + ProductArch = productArch; + Platform = msi.GetMetadata(Metadata.Platform); - ValidateRelativePackagePath($@"{Id},version={Version},chip={Chip},productarch={visualStudioProductArchitecture}\{Path.GetFileName(msi.ItemSpec)}"); + // At least one of Chip or MachineArch should have a value, otherwise we cannot generate valid SWIX. + if (string.IsNullOrWhiteSpace(Chip) && string.IsNullOrWhiteSpace(MachineArch)) + { + throw new ArgumentOutOfRangeException(Strings.ChipOrMachineArchRequired); + } + + // We need to always use the platform as an output folder because the chip value will be x64 for both arm64/x64 MSIs + // and machineArch is not guaranteed to be applicable. + ProjectSourceDirectory = Path.Combine(SwixDirectory, $"{sdkFeatureBand}", Id, Platform); + ValidateRelativePackagePath(GetRelativePackagePath()); // The name of the .swixproj file is used to create the JSON manifest that will be merged into the .vsman file later. // For drop publishing all the JSON manifests and payloads must reside in the same folder so we shorten the project names // and use a hashed filename to avoid path too long errors. - string projectName = $"{Id}.{Version.ToString(3)}.{Chip}"; - ProjectFile = $"{Utils.GetHash(projectName, HashAlgorithmName.MD5)}.swixproj"; - - FileInfo fileInfo = new(msi.ItemSpec); + string hashInputs = $"{Id},{Version.ToString(3)},{sdkFeatureBand},{Platform},{Chip},{machineArch}"; + ProjectFile = $"{Utils.GetHash(hashInputs, HashAlgorithmName.MD5)}.swixproj"; - ReplacementTokens[SwixTokens.__VS_PACKAGE_CHIP__] = Chip; - ReplacementTokens[SwixTokens.__VS_PACKAGE_INSTALL_SIZE_SYSTEM_DRIVE__] = $"{MsiUtils.GetInstallSize(msi.ItemSpec)}"; - ReplacementTokens[SwixTokens.__VS_PACKAGE_PRODUCT_ARCH__] = visualStudioProductArchitecture; - ReplacementTokens[SwixTokens.__VS_PAYLOAD_SIZE__] = $"{fileInfo.Length}"; ReplacementTokens[SwixTokens.__VS_PAYLOAD_SOURCE__] = msi.GetMetadata(Metadata.FullPath); } + /// + protected override string GetRelativePackagePath() + { + string relativePath = base.GetRelativePackagePath(); + + relativePath += !string.IsNullOrEmpty(Chip) ? $",chip={Chip}" : string.Empty; + relativePath += !string.IsNullOrEmpty(ProductArch) ? $",productarch={ProductArch}" : string.Empty; + relativePath += !string.IsNullOrEmpty(Chip) ? $",machinearch={MachineArch}" : string.Empty; + + return Path.Combine(relativePath, Path.GetFileName(_msi.ItemSpec)); + } + /// public override string Create() { string swixProj = EmbeddedTemplates.Extract("msi.swixproj", ProjectSourceDirectory, ProjectFile); - Utils.StringReplace(swixProj, ReplacementTokens, Encoding.UTF8); - Utils.StringReplace(EmbeddedTemplates.Extract("msi.swr", ProjectSourceDirectory), ReplacementTokens, Encoding.UTF8); + FileInfo fileInfo = new(_msi.ItemSpec); + + // Since we can't use preprocessor directives in the source, we'll do the conditional authoring inline instead. + using StreamWriter msiWriter = File.CreateText(Path.Combine(ProjectSourceDirectory, "msi.swr")); + + msiWriter.WriteLine($"use vs"); + msiWriter.WriteLine(); + msiWriter.WriteLine($"package name={Id}"); + msiWriter.WriteLine($" version={Version}"); + + // VS does support setting chip, productArch, and machineArch on a single package. + if (!string.IsNullOrWhiteSpace(Chip)) + { + msiWriter.WriteLine($" vs.package.chip={Chip}"); + } + + if (!string.IsNullOrWhiteSpace(ProductArch)) + { + msiWriter.WriteLine($" vs.package.productArch={ProductArch}"); + } + + if (!string.IsNullOrEmpty(MachineArch)) + { + msiWriter.WriteLine($" vs.package.machineArch={MachineArch}"); + } + + msiWriter.WriteLine($" vs.package.type=msi"); + msiWriter.WriteLine(); + msiWriter.WriteLine($"vs.installSize"); + msiWriter.WriteLine($" SystemDrive={MsiUtils.GetInstallSize(_msi.ItemSpec)}"); + msiWriter.WriteLine($" TargetDrive=0"); + msiWriter.WriteLine($" SharedDrive=0"); + msiWriter.WriteLine(); + msiWriter.WriteLine($"vs.logFiles"); + msiWriter.WriteLine($" vs.logFile pattern=\"dd_setup*{Id}*.log\""); + msiWriter.WriteLine(); + msiWriter.WriteLine($"vs.msiProperties"); + msiWriter.WriteLine($" vs.msiProperty name=\"MSIFASTINSTALL\" value=\"7\""); + msiWriter.WriteLine($" vs.msiProperty name=\"VSEXTUI\" value=\"1\""); + msiWriter.WriteLine(); + msiWriter.WriteLine($"vs.payloads"); + msiWriter.WriteLine($" vs.payload source=$(PayloadSource)"); + msiWriter.WriteLine($" size={fileInfo.Length}"); + msiWriter.WriteLine(); return swixProj; } diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs index 28ed9debe7f..e0d49c393b4 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixComponent.cs @@ -176,12 +176,12 @@ public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefini new Version((new ReleaseVersion(manifest.Version)).ToString(3)); // Since workloads only define a description, if no custom resources were provided, both the title and description of - // the SWIX component will default to the workload description. - SwixComponent component = new(sdkFeatureBand, Utils.ToSafeId(workload.Id), - resourceItem?.GetMetadata(Metadata.Title) ?? workload.Description ?? throw new Exception(Strings.ComponentTitleCannotBeNull), - resourceItem?.GetMetadata(Metadata.Description) ?? workload.Description ?? throw new Exception(Strings.ComponentDescriptionCannotBeNull), + // the SWIX component will default to the workload description. + SwixComponent component = new(sdkFeatureBand, Utils.ToSafeId(workload.Id), + resourceItem != null && !string.IsNullOrEmpty(resourceItem.GetMetadata(Metadata.Title)) ? resourceItem.GetMetadata(Metadata.Title) : workload.Description ?? throw new Exception(Strings.ComponentTitleCannotBeNull), + resourceItem != null && !string.IsNullOrEmpty(resourceItem.GetMetadata(Metadata.Description)) ? resourceItem.GetMetadata(Metadata.Description) : workload.Description ?? throw new Exception(Strings.ComponentDescriptionCannotBeNull), componentVersion, workload.IsAbstract, - resourceItem?.GetMetadata(Metadata.Category) ?? DefaultValues.ComponentCategory, + resourceItem != null && !string.IsNullOrEmpty(resourceItem.GetMetadata(Metadata.Category)) ? resourceItem.GetMetadata(Metadata.Category) : DefaultValues.ComponentCategory ?? throw new Exception(Strings.ComponentCategoryCannotBeNull), shortNames); // If the workload extends other workloads, we add those as component dependencies before diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs index d75d42aff86..5db14a35b3f 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Swix/SwixProjectBase.cs @@ -28,14 +28,6 @@ public string Id set; } - /// - /// The package type associated with the SWIX project. - /// - public abstract string PackageType - { - get; - } - /// /// The version of the SWIX package. /// @@ -74,6 +66,12 @@ protected void ReplaceTokens(string path) Utils.StringReplace(path, ReplacementTokens, Encoding.UTF8); } + /// + /// Compute the relative path of the package within the Visual Studio package cache. + /// + /// The relative path of the package. + protected virtual string GetRelativePackagePath() => $"{Id},version={Version}"; + /// /// Validates that the length of the relative package path does not execeed the maximum limit allowed by Visual Studio. The length /// accounts for the location of the Visual Studio installer package cache. @@ -81,8 +79,8 @@ protected void ReplaceTokens(string path) /// internal static void ValidateRelativePackagePath(string relativePackagePath) { - _ = relativePackagePath ?? throw new ArgumentNullException(nameof(relativePackagePath)); - + _ = relativePackagePath ?? throw new ArgumentNullException(nameof(relativePackagePath)); + // Visual Studio will verify this as part of its manifest validation logic during PR builds, but // any error would require rebuilding workloads and effectively reset .NET builds. if (relativePackagePath.Length > MaxRelativePackagePath) diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs index ef9a89b67b9..978b9a9ddb7 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs @@ -81,7 +81,7 @@ internal static void StringReplace(string fileName, Dictionary t /// /// The name of the parameter to check. /// The value of the parameter. - internal static void CheckNullOrEmpty(string name, string value) + internal static string CheckNullOrEmpty(string name, string value) { if (value is null) { @@ -92,6 +92,8 @@ internal static void CheckNullOrEmpty(string name, string value) { throw new ArgumentException($"Parameter cannot be empty: ${name}"); } + + return value; } /// diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs index 3679c4852a6..058beb9ecee 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/WorkloadManifestPackage.wix.cs @@ -53,6 +53,15 @@ public ReleaseVersion SdkFeatureBand get; } + /// + /// Returns if the Visual Studio version targeted by the feature band supports the machineArch property; + /// otherwise. + /// + public bool SupportsMachineArch + { + get; + } + /// /// Creates a new instance of a . /// @@ -87,7 +96,8 @@ public WorkloadManifestPackage(ITaskItem package, string destinationBaseDirector SdkFeatureBand = GetSdkFeatureBandVersion(GetSdkVersion(Id)); ManifestId = GetManifestId(Id); SwixPackageId = $"{Id.Replace(shortNames)}"; - } + SupportsMachineArch = bool.TryParse(package.GetMetadata(Metadata.SupportsMachineArch), out bool supportsMachineArch) ? supportsMachineArch : false; + } /// /// Gets the path of the workload manifest file. diff --git a/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolDisplayFormat.cs b/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolDisplayFormat.cs new file mode 100644 index 00000000000..7c8be79babd --- /dev/null +++ b/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolDisplayFormat.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DotNet.GenAPI.Shared; + +public class AssemblySymbolDisplayFormats +{ + public static readonly SymbolDisplayFormat NamespaceDisplayFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + public static readonly SymbolDisplayFormat NamedTypeDisplayFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.RemoveAttributeSuffix | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + genericsOptions: + SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeVariance); + + public static readonly SymbolDisplayFormat BaseTypeDisplayFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeVariance); + + public static readonly SymbolDisplayFormat MemberDisplayFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature, + extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod, + propertyStyle: SymbolDisplayPropertyStyle.NameOnly, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.RemoveAttributeSuffix | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + kindOptions: SymbolDisplayKindOptions.IncludeMemberKeyword, + parameterOptions: SymbolDisplayParameterOptions.IncludeDefaultValue | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeType, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeVariance, + memberOptions: SymbolDisplayMemberOptions.IncludeExplicitInterface | + SymbolDisplayMemberOptions.IncludeConstantValue | + SymbolDisplayMemberOptions.IncludeModifiers | + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeType | + SymbolDisplayMemberOptions.IncludeAccessibility); +} diff --git a/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolLoader.cs b/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolLoader.cs index 42f2c6e826c..3c4f03b9b73 100644 --- a/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolLoader.cs +++ b/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolLoader.cs @@ -8,7 +8,7 @@ namespace Microsoft.DotNet.GenAPI.Shared; -internal class AssemblySymbolLoader : IAssemblySymbolLoader +public class AssemblySymbolLoader : IAssemblySymbolLoader { public IAssemblySymbol? LoadAssembly(string path) { @@ -32,6 +32,7 @@ internal class AssemblySymbolLoader : IAssemblySymbolLoader var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable); var compilation = CSharpCompilation.Create($"AssemblyLoader_{DateTime.Now:MM_dd_yy_HH_mm_ss_FFF}", options: compilationOptions); + compilation = compilation.AddReferences(reference); return compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; } } diff --git a/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolTraverser.cs b/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolTraverser.cs index 52fb9f10eb4..789b63faf14 100644 --- a/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolTraverser.cs +++ b/src/Microsoft.DotNet.GenAPI/Shared/AssemblySymbolTraverser.cs @@ -3,11 +3,7 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; namespace Microsoft.DotNet.GenAPI.Shared; @@ -15,48 +11,51 @@ namespace Microsoft.DotNet.GenAPI.Shared; public abstract class AssemblySymbolTraverser { private readonly IAssemblySymbolOrderProvider _orderProvider; - private readonly IAssemblySymbolFilter _filter; + + protected IAssemblySymbolFilter Filter { get; } public AssemblySymbolTraverser(IAssemblySymbolOrderProvider orderProvider, IAssemblySymbolFilter filter) { _orderProvider = orderProvider; - _filter = filter; + Filter = filter; } public void Visit(IAssemblySymbol assembly) { - var namespaces = EnumerateNamespaces(assembly).Where(_filter.Include); + var namespaces = EnumerateNamespaces(assembly).Where(Filter.Include); foreach (var namespaceSymbol in _orderProvider.Order(namespaces)) { - Process(namespaceSymbol); + using IDisposable rs = ProcessBlock(namespaceSymbol); Visit(namespaceSymbol); } } public void Visit(INamespaceSymbol namespaceSymbol) { - var typeMembers = namespaceSymbol.GetTypeMembers().Where(_filter.Include); + var typeMembers = namespaceSymbol.GetTypeMembers().Where(Filter.Include); foreach (var typeMember in _orderProvider.Order(typeMembers)) { - foreach (var attribute in typeMember.GetAttributes().Where(_filter.Include)) + foreach (var attribute in typeMember.GetAttributes().Where(Filter.Include)) { Process(attribute); } - Process(typeMember); + using IDisposable rs = ProcessBlock(typeMember); Visit(typeMember); } } - public void Visit(INamedTypeSymbol typeMember) + public void Visit(INamedTypeSymbol namedType) { - var members = typeMember.GetMembers().Where(_filter.Include); + VisitInnerNamedTypes(namedType); + + var members = namedType.GetMembers().Where(Filter.Include); foreach (var member in _orderProvider.Order(members)) { - foreach (var attribute in member.GetAttributes().Where(_filter.Include)) + foreach (var attribute in member.GetAttributes().Where(Filter.Include)) { Process(attribute); } @@ -65,10 +64,21 @@ public void Visit(INamedTypeSymbol typeMember) } } - protected abstract void Process(INamespaceSymbol namespaceSymbol); - protected abstract void Process(INamedTypeSymbol typeMember); + protected abstract IDisposable ProcessBlock(INamespaceSymbol namespaceSymbol); + protected abstract IDisposable ProcessBlock(INamedTypeSymbol namedType); protected abstract void Process(ISymbol member); - protected abstract void Process(AttributeData attribute); + protected abstract void Process(AttributeData data); + + private void VisitInnerNamedTypes(INamedTypeSymbol namedType) + { + var innerNamedTypes = namedType.GetTypeMembers().Where(Filter.Include); + + foreach (var innerNamedType in _orderProvider.Order(innerNamedTypes)) + { + using var rs = ProcessBlock(innerNamedType); + Visit(innerNamedType); + } + } private IEnumerable EnumerateNamespaces(IAssemblySymbol assembly) { diff --git a/src/Microsoft.DotNet.GenAPI/Shared/CSharpBuilder.cs b/src/Microsoft.DotNet.GenAPI/Shared/CSharpBuilder.cs new file mode 100644 index 00000000000..46522fe173b --- /dev/null +++ b/src/Microsoft.DotNet.GenAPI/Shared/CSharpBuilder.cs @@ -0,0 +1,228 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.DotNet.GenAPI.Shared; + +/// +/// Processes assemly symbols to build correspoding structures in C# language. +/// Depend on ISyntaxWriter impelemtenation, the result could be C# source file, xml etc. +/// +public class CSharpBuilder : AssemblySymbolTraverser, IAssemblySymbolWriter, IDisposable +{ + private readonly ISyntaxWriter _syntaxWriter; + + public CSharpBuilder(IAssemblySymbolOrderProvider orderProvider, + IAssemblySymbolFilter filter, ISyntaxWriter syntaxWriter) + : base(orderProvider, filter) => _syntaxWriter = syntaxWriter; + + public void WriteAssembly(IAssemblySymbol assembly) => Visit(assembly); + + protected override IDisposable ProcessBlock(INamespaceSymbol namespaceSymbol) + { + var namespacePath = new List(); + + foreach (var part in namespaceSymbol.ToDisplayParts( + AssemblySymbolDisplayFormats.NamespaceDisplayFormat)) + { + if (part.Kind == SymbolDisplayPartKind.NamespaceName) + { + namespacePath.Add(part.ToString()); + } + } + return _syntaxWriter.WriteNamespace(namespacePath); + } + + protected override IDisposable ProcessBlock(INamedTypeSymbol namedType) + { + var typeName = namedType.ToDisplayString(AssemblySymbolDisplayFormats.NamedTypeDisplayFormat); + + var accessibility = BuildAccessibility(namedType as ISymbol); + var keywords = GetKeywords((ITypeSymbol)namedType); + + var baseTypeNames = BuildBaseTypes(namedType); + var constraints = BuildConstraints(namedType); + + return _syntaxWriter.WriteTypeDefinition(accessibility, keywords, typeName, baseTypeNames, constraints); + } + + protected override void Process(ISymbol member) + { + switch (member.Kind) + { + case SymbolKind.Property: + Process((IPropertySymbol)member); + break; + + case SymbolKind.Event: + Process((IEventSymbol)member); + break; + + case SymbolKind.Method: + Process((IMethodSymbol)member); + break; + + default: + break; + } + } + + protected override void Process(AttributeData data) + { + var attribute = data.ToString(); + if (attribute != null) + { + _syntaxWriter.WriteAttribute(attribute); + } + } + + public void Dispose() => _syntaxWriter.Dispose(); + + #region Private methods + + private void Process(IPropertySymbol ps) + { + _syntaxWriter.WriteProperty(ps.ToDisplayString(AssemblySymbolDisplayFormats.MemberDisplayFormat), + hasImplementation: !ps.IsAbstract, ps.GetMethod != null, ps.SetMethod != null); + } + + private void Process(IEventSymbol es) + { + _syntaxWriter.WriteEvent(es.ToDisplayString(AssemblySymbolDisplayFormats.MemberDisplayFormat), + es.AddMethod != null, es.RemoveMethod != null); + } + + private void Process(IMethodSymbol ms) + { + _syntaxWriter.WriteMethod(ms.ToDisplayString(AssemblySymbolDisplayFormats.MemberDisplayFormat), + hasImplementation: !ms.IsAbstract); + } + + private IEnumerable BuildAccessibility(ISymbol symbol) + { + return symbol.DeclaredAccessibility switch + { + Accessibility.Private => new SyntaxKind[] { SyntaxKind.PrivateKeyword }, + Accessibility.Internal => new SyntaxKind[] { SyntaxKind.InternalKeyword }, + Accessibility.ProtectedAndInternal => new SyntaxKind[] { SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword }, + Accessibility.Protected => new SyntaxKind[] { SyntaxKind.ProtectedKeyword }, + Accessibility.ProtectedOrInternal => new SyntaxKind[] { SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword }, + Accessibility.Public => new SyntaxKind[] { SyntaxKind.PublicKeyword }, + _ => throw new Exception(string.Format("Unexpected accessibility modifier found {0}", + SyntaxFacts.GetText(symbol.DeclaredAccessibility))) + }; + } + + private IEnumerable BuildBaseTypes(INamedTypeSymbol namedType) + { + var baseTypeNames = new List(); + + if (namedType.BaseType != null && namedType.BaseType.SpecialType == SpecialType.None && + Filter.Include(namedType.BaseType)) + { + baseTypeNames.Add(namedType.BaseType.ToDisplayString()); + } + + foreach (var interfaceSymbol in namedType.Interfaces) + { + if (!Filter.Include(interfaceSymbol)) continue; + + baseTypeNames.Add(interfaceSymbol.ToDisplayString()); + } + + return baseTypeNames; + } + + private IEnumerable> BuildConstraints(INamedTypeSymbol namedType) + { + bool whereKeywordFound = false; + var currConstraint = new List(); + var constraints = new List>(); + + foreach (var part in namedType.ToDisplayParts(AssemblySymbolDisplayFormats.BaseTypeDisplayFormat)) + { + if (part.Kind == SymbolDisplayPartKind.Keyword && + part.ToString() == SyntaxFacts.GetText(SyntaxKind.WhereKeyword)) + { + if (whereKeywordFound) + { + constraints.Add(currConstraint); + currConstraint.Clear(); + } + + currConstraint.Add(part); + whereKeywordFound = true; + } + else if (whereKeywordFound) + { + currConstraint.Add(part); + } + } + + if (currConstraint.Any()) + { + constraints.Add(currConstraint); + } + + return constraints; + } + + private IEnumerable GetKeywords(ITypeSymbol namedType) + { + var keywords = new List(); + + switch (namedType.TypeKind) + { + case TypeKind.Class: + if (namedType.IsAbstract) + { + keywords.Add(SyntaxKind.AbstractKeyword); + } + if (namedType.IsStatic) + { + keywords.Add(SyntaxKind.StaticKeyword); + } + if (namedType.IsSealed) + { + keywords.Add(SyntaxKind.SealedKeyword); + } + + keywords.Add(SyntaxKind.PartialKeyword); + keywords.Add(SyntaxKind.ClassKeyword); + break; + case TypeKind.Delegate: + keywords.Add(SyntaxKind.DelegateKeyword); + break; + case TypeKind.Enum: + keywords.Add(SyntaxKind.EnumKeyword); + break; + case TypeKind.Interface: + keywords.Add(SyntaxKind.InterfaceKeyword); + break; + case TypeKind.Struct: + { + if (namedType.IsReadOnly) + keywords.Add(SyntaxKind.ReadOnlyKeyword); + } + if (namedType.IsRefLikeType) + { + keywords.Add(SyntaxKind.RefKeyword); + } + keywords.Add(SyntaxKind.PartialKeyword); + keywords.Add(SyntaxKind.StructKeyword); + break; + } + + return keywords; + } + + #endregion +} + diff --git a/src/Microsoft.DotNet.GenAPI/Shared/CSharpSyntaxWriter.cs b/src/Microsoft.DotNet.GenAPI/Shared/CSharpSyntaxWriter.cs new file mode 100644 index 00000000000..3951b80ddf4 --- /dev/null +++ b/src/Microsoft.DotNet.GenAPI/Shared/CSharpSyntaxWriter.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.DotNet.GenAPI.Shared; + +/// +/// Writes C# source code into IO.File or Console. +/// +public class CSharpSyntaxWriter: ISyntaxWriter +{ + private readonly TextWriter _textWriter; + + public CSharpSyntaxWriter(TextWriter textWriter) => _textWriter = textWriter; + + public IDisposable WriteNamespace(IEnumerable namespacePath) + { + WriteKeyword(SyntaxKind.NamespaceKeyword); + + bool root = true; + foreach (var ns in namespacePath) + { + if (!root) + { + WriteKeyword(SyntaxKind.DotToken, writeSpace: false); + } + else + { + root = false; + } + + _textWriter.Write(ns); + } + + OpenBrace(); + + return new Block(() => + { + CloseBrace(); + }); + } + + public IDisposable WriteTypeDefinition( + IEnumerable accessibility, + IEnumerable keywords, + string typeName, + IEnumerable baseTypeNames, + IEnumerable> constraints) + { + foreach (var keyword in accessibility) + { + WriteKeyword(keyword); + } + + foreach (var keyword in keywords) + { + WriteKeyword(keyword); + } + + _textWriter.Write(typeName); + WriteSpace(); + + bool first = true; + + foreach (var baseSymbol in baseTypeNames) + { + if (first) + { + WriteKeyword(SyntaxKind.ColonToken); + } + else + { + WriteKeyword(SyntaxKind.CommaToken); + } + + first = false; + + _textWriter.Write(baseSymbol); + } + + foreach (var currConstrain in constraints) + { + WriteSpace(); + foreach (var part in currConstrain) + { + _textWriter.Write(part.ToString()); + } + } + + OpenBrace(); + + return new Block(() => + { + CloseBrace(); + }); + } + + public void WriteAttribute(string attribute) + { + WriteKeyword(SyntaxKind.OpenBracketToken, writeSpace: false); + _textWriter.Write(attribute); + WriteKeyword(SyntaxKind.CloseBracketToken, writeSpace: false); + _textWriter.WriteLine(); + } + + public void WriteProperty(string definition, bool hasImplementation, bool hasGetMethod, bool hasSetMethod) + { + _textWriter.Write(definition); + + if (hasGetMethod || hasSetMethod) + { + var _writeAccessorMethod = (SyntaxKind method) => + { + WriteKeyword(method, writeSpace: false); + if (hasImplementation) + { + WriteImplementation(); + WriteSpace(); + } + else + WriteKeyword(SyntaxKind.SemicolonToken); + }; + + WriteSpace(); + WriteKeyword(SyntaxKind.OpenBraceToken); + + if (hasGetMethod) + { + _writeAccessorMethod(SyntaxKind.GetKeyword); + } + + if (hasSetMethod) + { + _writeAccessorMethod(SyntaxKind.SetKeyword); + } + + WriteKeyword(SyntaxKind.CloseBraceToken, writeSpace: false); + } + else + { + WriteKeyword(SyntaxKind.SemicolonToken, writeSpace: false); + } + + _textWriter.WriteLine(); + } + + public void WriteEvent(string definition, bool hasAddMethod, bool hasRemoveMethod) + { + _textWriter.Write(definition); + + if (hasAddMethod || hasRemoveMethod) + { + var _writeAccessorMethod = (SyntaxKind method) => + { + WriteKeyword(method); + WriteImplementation(); + WriteSpace(); + }; + + WriteSpace(); + WriteKeyword(SyntaxKind.OpenBraceToken); + + if (hasAddMethod) + { + _writeAccessorMethod(SyntaxKind.AddKeyword); + } + + if (hasRemoveMethod) + { + _writeAccessorMethod(SyntaxKind.RemoveKeyword); + } + + WriteKeyword(SyntaxKind.CloseBraceToken, writeSpace: false); + } + else + { + WriteKeyword(SyntaxKind.SemicolonToken, writeSpace: false); + } + + _textWriter.WriteLine(); + } + + public void WriteMethod(string definition, bool hasImplementation) + { + _textWriter.Write(definition); + if (hasImplementation) + { + WriteSpace(); + WriteImplementation(); + } + else + { + WriteKeyword(SyntaxKind.SemicolonToken); + } + + _textWriter.WriteLine(); + } + + public void Dispose() => _textWriter.Dispose(); + + #region Private methods + + private void WriteSpace() + { + _textWriter.Write(' '); + } + + private void OpenBrace() + { + WriteSpace(); + WriteKeyword(SyntaxKind.OpenBraceToken, writeSpace: false); + _textWriter.WriteLine(); + } + + private void CloseBrace() + { + WriteKeyword(SyntaxKind.CloseBraceToken, writeSpace: false); + _textWriter.WriteLine(); + } + + private void WriteKeyword(SyntaxKind keyword, bool writeSpace = true) + { + _textWriter.Write(SyntaxFacts.GetText(keyword)); + if (writeSpace) + { + WriteSpace(); + } + } + + private void WriteImplementation() + { + WriteKeyword(SyntaxKind.OpenBraceToken); + WriteKeyword(SyntaxKind.ThrowKeyword); + WriteKeyword(SyntaxKind.NullKeyword, writeSpace: false); + WriteKeyword(SyntaxKind.SemicolonToken); + WriteKeyword(SyntaxKind.CloseBraceToken, writeSpace: false); + } + + private class Block : IDisposable + { + private readonly Action _endBlock; + + public Block(Action endBlock) => _endBlock = endBlock; + + public void Dispose() => _endBlock(); + } + + #endregion +} diff --git a/src/Microsoft.DotNet.GenAPI/Shared/CSharpWriter.cs b/src/Microsoft.DotNet.GenAPI/Shared/CSharpWriter.cs deleted file mode 100644 index f47abef074d..00000000000 --- a/src/Microsoft.DotNet.GenAPI/Shared/CSharpWriter.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.CodeAnalysis; - -namespace Microsoft.DotNet.GenAPI.Shared; - -public abstract class CSharpWriter : AssemblySymbolTraverser, IWriter, IDisposable -{ - private readonly ISyntaxWriter _syntaxWriter; - - public CSharpWriter(IAssemblySymbolOrderProvider orderProvider, - IAssemblySymbolFilter filter, ISyntaxWriter syntaxWriter) - : base(orderProvider, filter) - { - _syntaxWriter = syntaxWriter; - } - - public void WriteAssemblies(IAssemblySymbol assembly) - { - Visit(assembly); - } - - public void Dispose() - { - _syntaxWriter.Dispose(); - } -} diff --git a/src/Microsoft.DotNet.GenAPI/Shared/IWriter.cs b/src/Microsoft.DotNet.GenAPI/Shared/IAssemblySymbolWriter.cs similarity index 77% rename from src/Microsoft.DotNet.GenAPI/Shared/IWriter.cs rename to src/Microsoft.DotNet.GenAPI/Shared/IAssemblySymbolWriter.cs index 33252dd5ac8..bf89fbad600 100644 --- a/src/Microsoft.DotNet.GenAPI/Shared/IWriter.cs +++ b/src/Microsoft.DotNet.GenAPI/Shared/IAssemblySymbolWriter.cs @@ -8,7 +8,7 @@ namespace Microsoft.DotNet.GenAPI.Shared; /// /// Interface incapsulates logic for processing symbol assemblies. /// -public interface IWriter +public interface IAssemblySymbolWriter { - void WriteAssemblies(IAssemblySymbol assembly); + void WriteAssembly(IAssemblySymbol assembly); } diff --git a/src/Microsoft.DotNet.GenAPI/Shared/ISyntaxWriter.cs b/src/Microsoft.DotNet.GenAPI/Shared/ISyntaxWriter.cs index 1cbf1680dc0..4b84d078017 100644 --- a/src/Microsoft.DotNet.GenAPI/Shared/ISyntaxWriter.cs +++ b/src/Microsoft.DotNet.GenAPI/Shared/ISyntaxWriter.cs @@ -2,16 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; namespace Microsoft.DotNet.GenAPI.Shared; /// -/// Interface responsible for writing C# code in various formats: code file, xml, etc. +/// Interface responsible for writing various outputs: C# code, XML etc. /// public interface ISyntaxWriter : IDisposable { - void WriteSymbol(string str); - void WriteKeyword(SyntaxKind sk); - void WriteLine(); + IDisposable WriteNamespace(IEnumerable namespacePath); + + IDisposable WriteTypeDefinition(IEnumerable accessibility, IEnumerable keywords, + string typeName, IEnumerable baseTypeNames, IEnumerable> constraints); + + void WriteAttribute(string attribute); + + void WriteProperty(string definition, bool hasImplementation, bool hasGetMethod, bool hasSetMethod); + + void WriteEvent(string definition, bool hasAddMethod, bool hasRemoveMethod); + + void WriteMethod(string definition, bool hasImplementation); } diff --git a/src/Microsoft.DotNet.GenAPI/Tests/CSharpBuilderTests.cs b/src/Microsoft.DotNet.GenAPI/Tests/CSharpBuilderTests.cs new file mode 100644 index 00000000000..2e23525e1cf --- /dev/null +++ b/src/Microsoft.DotNet.GenAPI/Tests/CSharpBuilderTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.DotNet.GenAPI.Shared; +using Moq; +using Xunit; + +namespace Microsoft.DotNet.GenAPI.Tests; + +public class CSharpBuilderTests +{ + [Fact] + public void BuildSimpleNamespacesTest() + { + var syntaxTree = """ + namespace A + { + namespace B {} + + namespace C.D {} + } + """; + + var assembly = CompilationHelper.GetAssemblyFromSyntax(syntaxTree, enableNullable: false); + + var orderProvider = new Mock(MockBehavior.Strict); + var assemblySymbolFilter = new Mock(MockBehavior.Strict); + var syntaxWriter = new Mock(MockBehavior.Strict); + + orderProvider.Setup(o => o.Order(It.IsAny>())).Returns( + (IEnumerable v) => { return v; }); + orderProvider.Setup(o => o.Order(It.IsAny>())).Returns( + (IEnumerable v) => { return v; }); + + assemblySymbolFilter.Setup(o => o.Include(It.IsAny())).Returns(true); + + var block = new Mock(); + + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { /* global namespace */ })).Returns(block.Object); + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { "A" })).Returns(block.Object); + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { "A", "B" })).Returns(block.Object); + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { "A", "C" })).Returns(block.Object); + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { "A", "C", "D" })).Returns(block.Object); + + var builder = new CSharpBuilder(orderProvider.Object, + assemblySymbolFilter.Object, syntaxWriter.Object); + builder.WriteAssembly(assembly); + } + + [Fact] + public void BuildSimpleStructureTest() + { + var syntaxTree = """ + namespace A + { + public struct PublicStruct { } + struct InternalStruct { } + readonly struct ReadonlyStruct { } + public readonly struct PublicReadonlyStruct { } + record struct RecordStruct { } + readonly record struct ReadonlyRecordStruct { } + public ref struct PublicRefStruct { } + public readonly ref struct PublicReadonlyRefStruct { } + } + """; + + var assembly = CompilationHelper.GetAssemblyFromSyntax(syntaxTree, enableNullable: false); + + var orderProvider = new Mock(MockBehavior.Strict); + var assemblySymbolFilter = new Mock(MockBehavior.Strict); + var syntaxWriter = new Mock(MockBehavior.Strict); + + orderProvider.Setup(o => o.Order(It.IsAny>())).Returns( + (IEnumerable v) => { return v; }); + orderProvider.Setup(o => o.Order(It.IsAny>())).Returns( + (IEnumerable v) => { return v; }); + orderProvider.Setup(o => o.Order(It.IsAny>())).Returns( + new ISymbol[] { /* return empty list */ }); + + assemblySymbolFilter.Setup(o => o.Include(It.IsAny())).Returns(true); + assemblySymbolFilter.Setup(o => o.Include(It.IsAny())).Returns(true); + assemblySymbolFilter.Setup(o => o.Include(It.IsAny())).Returns(false); + + var block = new Mock(); + + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { /* global namespace */ })).Returns(block.Object); + syntaxWriter.Setup(o => o.WriteNamespace(new string[] { "A" })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.PublicKeyword }, + new SyntaxKind[] { SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "PublicStruct", + new string[] { }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.InternalKeyword }, + new SyntaxKind[] { SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "InternalStruct", + new string[] { }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.InternalKeyword }, + new SyntaxKind[] { SyntaxKind.ReadOnlyKeyword, SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "ReadonlyStruct", + new string[] { }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.PublicKeyword }, + new SyntaxKind[] { SyntaxKind.ReadOnlyKeyword, SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "PublicReadonlyStruct", + new string[] { }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.InternalKeyword }, + new SyntaxKind[] { SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "RecordStruct", + new string[] { "System.IEquatable" }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.InternalKeyword }, + new SyntaxKind[] { SyntaxKind.ReadOnlyKeyword, SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "ReadonlyRecordStruct", + new string[] { "System.IEquatable" }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.PublicKeyword }, + new SyntaxKind[] { SyntaxKind.RefKeyword, SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "PublicRefStruct", + new string[] { }, + new IEnumerable[] { })).Returns(block.Object); + + syntaxWriter.Setup(o => o.WriteTypeDefinition( + new SyntaxKind[] { SyntaxKind.PublicKeyword }, + new SyntaxKind[] { SyntaxKind.ReadOnlyKeyword, SyntaxKind.RefKeyword, SyntaxKind.PartialKeyword, SyntaxKind.StructKeyword }, + "PublicReadonlyRefStruct", + new string[] { }, + new IEnumerable[] { })).Returns(block.Object); + + var builder = new CSharpBuilder(orderProvider.Object, + assemblySymbolFilter.Object, syntaxWriter.Object); + builder.WriteAssembly(assembly); + } +} diff --git a/src/Microsoft.DotNet.GenAPI/Tests/CompilationHelper.cs b/src/Microsoft.DotNet.GenAPI/Tests/CompilationHelper.cs new file mode 100644 index 00000000000..3820e6b30ce --- /dev/null +++ b/src/Microsoft.DotNet.GenAPI/Tests/CompilationHelper.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.DotNet.GenAPI.Tests; + +internal class CompilationHelper +{ + internal static IAssemblySymbol GetAssemblyFromSyntax(string syntax, bool enableNullable = false, byte[] publicKey = null, [CallerMemberName] string assemblyName = "") + { + CSharpCompilation compilation = CreateCSharpCompilationFromSyntax(syntax, assemblyName, enableNullable, publicKey); + + Assert.Empty(compilation.GetDiagnostics()); + + return compilation.Assembly; + } + + private static CSharpCompilation CreateCSharpCompilationFromSyntax(string syntax, string name, bool enableNullable, byte[] publicKey) + { + CSharpCompilation compilation = CreateCSharpCompilation(name, enableNullable, publicKey); + return compilation.AddSyntaxTrees(GetSyntaxTree(syntax)); + } + + private static SyntaxTree GetSyntaxTree(string syntax) + { + return CSharpSyntaxTree.ParseText(syntax, ParseOptions); + } + + private static CSharpCompilation CreateCSharpCompilation(string name, bool enableNullable, byte[] publicKey) + { + bool publicSign = publicKey != null ? true : false; + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, + publicSign: publicSign, + cryptoPublicKey: publicSign ? publicKey.ToImmutableArray() : default, + nullableContextOptions: enableNullable ? NullableContextOptions.Enable : NullableContextOptions.Disable, + specificDiagnosticOptions: DiagnosticOptions); + + return CSharpCompilation.Create(name, options: compilationOptions, references: DefaultReferences); + } + + private static CSharpParseOptions ParseOptions { get; } = new(preprocessorSymbols: +#if NETFRAMEWORK + new string[] { "NETFRAMEWORK" } +#else + Array.Empty() +#endif + ); + + private static IEnumerable> DiagnosticOptions { get; } = new[] + { + // Suppress warning for unused events. + new KeyValuePair("CS0067", ReportDiagnostic.Suppress) + }; + + private static IEnumerable DefaultReferences { get; } = new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + }; + +} diff --git a/src/Microsoft.DotNet.GenAPI/Tests/Microsoft.DotNet.GenAPI.Tests.csproj b/src/Microsoft.DotNet.GenAPI/Tests/Microsoft.DotNet.GenAPI.Tests.csproj new file mode 100644 index 00000000000..2b0a0d4fb49 --- /dev/null +++ b/src/Microsoft.DotNet.GenAPI/Tests/Microsoft.DotNet.GenAPI.Tests.csproj @@ -0,0 +1,19 @@ + + + + $(TargetFrameworkForNETSDK) + true + false + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests.csproj b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests.csproj index a84f46cb6c6..429bd6aab83 100644 --- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests.csproj +++ b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests.csproj @@ -6,14 +6,14 @@ - - + + - - + + - + diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/Microsoft.DotNet.Helix.Sdk.MonoQueue.targets b/src/Microsoft.DotNet.Helix/Sdk/tools/Microsoft.DotNet.Helix.Sdk.MonoQueue.targets index 20c875c5637..ce0de6a0264 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/Microsoft.DotNet.Helix.Sdk.MonoQueue.targets +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/Microsoft.DotNet.Helix.Sdk.MonoQueue.targets @@ -74,9 +74,10 @@ <_AccessTokenSuffix /> <_AccessTokenSuffix Condition=" '$(HelixAccessToken)' != '' ">&access_token={Get this from helix.dot.net} + [System.String]::Copy('$(HelixBaseUri)').TrimEnd('/') - - + + false - 7.0.0-preview.7.22376.6 + 7.0.0-rc.1.22423.7 runtime $(BundledNETCoreAppPackageVersion) diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets index d601eb43954..5af41dc74f1 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets @@ -2,7 +2,7 @@ true net7.0 - 7.0.100-preview.7.22377.5 + 7.0.100-rc.1.22431.12 6.0.202 sdk diff --git a/src/Microsoft.DotNet.SignTool/src/ZipData.cs b/src/Microsoft.DotNet.SignTool/src/ZipData.cs index 991192cc824..a798d97076e 100644 --- a/src/Microsoft.DotNet.SignTool/src/ZipData.cs +++ b/src/Microsoft.DotNet.SignTool/src/ZipData.cs @@ -138,8 +138,17 @@ private void RepackRawZip(TaskLoggingHelper log) } private void RepackWixPack(TaskLoggingHelper log, string tempDir, string wixToolsPath) { - string workingDir = Path.Combine(tempDir, "extract", Guid.NewGuid().ToString()); - string outputDir = Path.Combine(tempDir, "output", Guid.NewGuid().ToString()); + // The wixpacks can have rather long paths when fully extracted. + // To avoid issues, use the first element of the GUID (up to first -). + // This does leave the very remote possibility of the dir already existing. In this case, the + // create.cmd file will always end up being extracted twice, and ExtractToDirectory + // will fail. Because of the very very remote possibility of this happening, no + // attempt to workaround this possibility is made. + var workingDirGuidSegment = Guid.NewGuid().ToString().Split('-')[0]; + var outputDirGuidSegment = Guid.NewGuid().ToString().Split('-')[0]; + + string workingDir = Path.Combine(tempDir, "extract", workingDirGuidSegment); + string outputDir = Path.Combine(tempDir, "output", outputDirGuidSegment); string createFileName = Path.Combine(workingDir, "create.cmd"); string outputFileName = Path.Combine(outputDir, FileSignInfo.FileName); diff --git a/tests/UnitTests.proj b/tests/UnitTests.proj index bf5438e8ca5..ab19e278ea7 100644 --- a/tests/UnitTests.proj +++ b/tests/UnitTests.proj @@ -14,6 +14,7 @@ sdk $(AGENT_JOBNAME) + 300 @@ -60,7 +61,7 @@ - + @@ -72,7 +73,7 @@ - +