From 69f11ee0275692528ed034a3885fa9f0c1504704 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 14 Feb 2023 23:11:01 -0800 Subject: Full support for multitargeting project references in a .wixproj Closes 7241 --- src/wix/WixToolset.BuildTasks/Common.cs | 46 -- ...eProjectReferenceDefineConstantsAndBindPaths.cs | 14 +- src/wix/WixToolset.BuildTasks/MetadataValue.cs | 68 +++ src/wix/WixToolset.BuildTasks/MetadataValueList.cs | 100 +++++ src/wix/WixToolset.BuildTasks/ToolsCommon.cs | 45 ++ .../UpdateProjectReferenceMetadata.cs | 466 +++++++++++++++++---- src/wix/test/WixToolsetTest.Sdk/MsbuildFixture.cs | 132 +++++- .../MultiTargetingWixlib/Directory.Build.props | 3 + .../MultiTargetingWixlib/Directory.Build.targets | 3 + .../PackageReleaseAndDebug/Package.wxs | 36 ++ .../PackageReleaseAndDebug.wixproj | 10 + .../PackageUsingExplicitTfmAndRids/Package.wxs | 26 ++ .../PackageUsingExplicitTfmAndRids.wixproj | 10 + .../PackageUsingRids/Package.wxs | 24 ++ .../PackageUsingRids/PackageUsingRids.wixproj | 10 + .../MultiTargetingWixlib/TestExe/Program.cs | 14 + .../MultiTargetingWixlib/TestExe/TestExe.csproj | 12 + 17 files changed, 873 insertions(+), 146 deletions(-) delete mode 100644 src/wix/WixToolset.BuildTasks/Common.cs create mode 100644 src/wix/WixToolset.BuildTasks/MetadataValue.cs create mode 100644 src/wix/WixToolset.BuildTasks/MetadataValueList.cs create mode 100644 src/wix/WixToolset.BuildTasks/ToolsCommon.cs create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.props create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.targets create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/Package.wxs create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/PackageReleaseAndDebug.wixproj create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/Package.wxs create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/PackageUsingExplicitTfmAndRids.wixproj create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/Package.wxs create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/PackageUsingRids.wixproj create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/Program.cs create mode 100644 src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/TestExe.csproj diff --git a/src/wix/WixToolset.BuildTasks/Common.cs b/src/wix/WixToolset.BuildTasks/Common.cs deleted file mode 100644 index 837dfb17..00000000 --- a/src/wix/WixToolset.BuildTasks/Common.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolset.BuildTasks -{ - using System; - using System.Text.RegularExpressions; - using Microsoft.Build.Framework; - - /// - /// Common WixTasks utility methods and types. - /// - public static class ToolsCommon - { - /// Metadata key name to turn off harvesting of project references. - public const string DoNotHarvest = "DoNotHarvest"; - - private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]"); - private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}"); // non 'words' and assorted valid characters - - /// - /// Return an identifier based on passed file/directory name - /// - /// File/directory name to generate identifer from - /// A version of the name that is a legal identifier. - /// This is duplicated from WiX's Common class. - public static string GetIdentifierFromName(string name) - { - var result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_". - - // MSI identifiers must begin with an alphabetic character or an - // underscore. Prefix all other values with an underscore. - if (AddPrefix.IsMatch(name)) - { - result = String.Concat("_", result); - } - - return result; - } - - public static string GetMetadataOrDefault(ITaskItem item, string metadataName, string defaultValue) - { - var value = item.GetMetadata(metadataName); - return String.IsNullOrWhiteSpace(value) ? defaultValue : value; - } - } -} diff --git a/src/wix/WixToolset.BuildTasks/CreateProjectReferenceDefineConstantsAndBindPaths.cs b/src/wix/WixToolset.BuildTasks/CreateProjectReferenceDefineConstantsAndBindPaths.cs index 7ac00241..ae9d8abe 100644 --- a/src/wix/WixToolset.BuildTasks/CreateProjectReferenceDefineConstantsAndBindPaths.cs +++ b/src/wix/WixToolset.BuildTasks/CreateProjectReferenceDefineConstantsAndBindPaths.cs @@ -31,7 +31,7 @@ namespace WixToolset.BuildTasks public override bool Execute() { var bindPaths = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var defineConstants = new Dictionary(); + var defineConstants = new SortedDictionary(); foreach (var resolvedReference in this.ResolvedProjectReferences) { @@ -46,7 +46,7 @@ namespace WixToolset.BuildTasks return true; } - private void AddBindPathsForResolvedReference(Dictionary> bindPathByPaths, ITaskItem resolvedReference) + private void AddBindPathsForResolvedReference(IDictionary> bindPathByPaths, ITaskItem resolvedReference) { // If the BindName was not explicitly provided, try to use the source project's filename // as the bind name. @@ -84,7 +84,7 @@ namespace WixToolset.BuildTasks } } - private void AddDefineConstantsForResolvedReference(Dictionary defineConstants, ITaskItem resolvedReference) + private void AddDefineConstantsForResolvedReference(IDictionary defineConstants, ITaskItem resolvedReference) { var configuration = resolvedReference.GetMetadata("Configuration"); var fullConfiguration = resolvedReference.GetMetadata("FullConfiguration"); @@ -96,7 +96,7 @@ namespace WixToolset.BuildTasks var projectFileName = Path.GetFileName(projectPath); var projectName = Path.GetFileNameWithoutExtension(projectPath); - var referenceName = ToolsCommon.GetIdentifierFromName(ToolsCommon.GetMetadataOrDefault(resolvedReference, "Name", projectName)); + var referenceName = ToolsCommon.CreateIdentifierFromValue(ToolsCommon.GetMetadataOrDefault(resolvedReference, "Name", projectName)); var targetPath = resolvedReference.GetMetadata("FullPath"); var targetDir = Path.GetDirectoryName(targetPath) + Path.DirectorySeparatorChar; @@ -156,12 +156,12 @@ namespace WixToolset.BuildTasks } // If there was only one targetpath we need to create its culture specific define - if (!oldTargetPath.Contains(";")) + if (!oldTargetPath.Contains("%3B")) { var oldSubFolder = FindSubfolder(oldTargetPath, targetDir, targetFileName); if (!String.IsNullOrEmpty(oldSubFolder)) { - defineConstants[referenceName + "." + oldSubFolder.Replace('\\', '_') + ".TargetPath"] = oldTargetPath; + defineConstants[referenceName + "." + ToolsCommon.CreateIdentifierFromValue(oldSubFolder) + ".TargetPath"] = oldTargetPath; } } @@ -169,7 +169,7 @@ namespace WixToolset.BuildTasks var subFolder = FindSubfolder(targetPath, targetDir, targetFileName); if (!String.IsNullOrEmpty(subFolder)) { - defineConstants[referenceName + "." + subFolder.Replace('\\', '_') + ".TargetPath"] = targetPath; + defineConstants[referenceName + "." + ToolsCommon.CreateIdentifierFromValue(subFolder) + ".TargetPath"] = targetPath; } } else diff --git a/src/wix/WixToolset.BuildTasks/MetadataValue.cs b/src/wix/WixToolset.BuildTasks/MetadataValue.cs new file mode 100644 index 00000000..4512ffa5 --- /dev/null +++ b/src/wix/WixToolset.BuildTasks/MetadataValue.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.BuildTasks +{ + using System; + using Microsoft.Build.Framework; + + internal class MetadataValue + { + public MetadataValue(ITaskItem item, string name, string valuePrefix = null, string defaultValue = "") + { + this.Item = item; + this.Name = name; + + var value = item.GetMetadata(name); + + this.HadValue = !String.IsNullOrWhiteSpace(value); + this.OriginalValue = value; + + if (!this.HadValue) + { + this.Value = defaultValue; + this.ValidValue = true; + } + else if (String.IsNullOrWhiteSpace(valuePrefix)) + { + this.Value = value; + this.ValidValue = true; + } + else if (value.StartsWith(valuePrefix) && value.Length > valuePrefix.Length) + { + this.Value = value.Substring(valuePrefix.Length); + this.ValidValue = true; + } + } + + public ITaskItem Item { get; } + + public string Name { get; } + + public bool HadValue { get; } + + public bool Modified => !String.Equals(this.OriginalValue, this.Value, StringComparison.Ordinal); + + public string OriginalValue { get; } + + public string Value { get; private set; } + + public bool ValidValue { get; } + + public void SetValue(string value) + { + this.Value = value; + } + + public void Apply() + { + if (String.IsNullOrWhiteSpace(this.Value)) + { + this.Item.RemoveMetadata(this.Name); + } + else + { + this.Item.SetMetadata(this.Name, this.Value); + } + } + } +} diff --git a/src/wix/WixToolset.BuildTasks/MetadataValueList.cs b/src/wix/WixToolset.BuildTasks/MetadataValueList.cs new file mode 100644 index 00000000..cf93277b --- /dev/null +++ b/src/wix/WixToolset.BuildTasks/MetadataValueList.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.BuildTasks +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.Build.Framework; + + internal class MetadataValueList + { + private static readonly char[] MetadataListSplitter = new char[] { ',', ';' }; + + public MetadataValueList(ITaskItem item, string name) + { + this.Item = item; + this.Name = name; + + var value = item.GetMetadata(name); + + this.HadValue = !String.IsNullOrWhiteSpace(value); + this.OriginalValue = value; + + this.Values = value.Split(MetadataListSplitter).Where(s => !String.IsNullOrWhiteSpace(s)).ToList(); + } + + public ITaskItem Item { get; } + + public string Name { get; } + + public bool HadValue { get; } + + public string OriginalValue { get; } + + public bool Modified { get; private set; } + + public List Values { get; } + + public void Clear() + { + if (this.Values.Count > 0) + { + this.Modified = true; + this.Values.Clear(); + } + } + + public void SetValue(string prefix, string value) + { + if (!String.IsNullOrEmpty(prefix)) + { + value = String.IsNullOrWhiteSpace(value) ? null : prefix + value; + + for (var i = 0; i < this.Values.Count; ++i) + { + if (this.Values[i].StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + if (value == null) + { + this.Values.RemoveAt(i); + } + else + { + this.Values[i] = value; + } + + this.Modified = true; + return; + } + } + } + + if (!String.IsNullOrWhiteSpace(value) && !this.Values.Contains(value)) + { + this.Modified = true; + this.Values.Add(value); + } + } + + public void AddRange(IEnumerable values) + { + foreach (var value in values) + { + this.SetValue(null, value); + } + } + + public void Apply() + { + if (this.Values.Count == 0) + { + this.Item.RemoveMetadata(this.Name); + } + else + { + this.Item.SetMetadata(this.Name, String.Join(";", this.Values)); + } + } + } +} diff --git a/src/wix/WixToolset.BuildTasks/ToolsCommon.cs b/src/wix/WixToolset.BuildTasks/ToolsCommon.cs new file mode 100644 index 00000000..4857de01 --- /dev/null +++ b/src/wix/WixToolset.BuildTasks/ToolsCommon.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.BuildTasks +{ + using System; + using System.Text.RegularExpressions; + using Microsoft.Build.Framework; + + /// + /// Common WixTasks utility methods and types. + /// + public static class ToolsCommon + { + /// Metadata key name to turn off harvesting of project references. + public const string DoNotHarvest = "DoNotHarvest"; + + private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]"); + private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}"); // non 'words' and assorted valid characters + + /// + /// Return an identifier based on passed value. + /// + /// Value to create identifer from. + /// A version of the value that is a legal identifier. + public static string CreateIdentifierFromValue(string value) + { + var result = IllegalIdentifierCharacters.Replace(value, "_"); // replace illegal characters with "_". + + // MSI identifiers must begin with an alphabetic character or an + // underscore. Prefix all other values with an underscore. + if (AddPrefix.IsMatch(value)) + { + result = String.Concat("_", result); + } + + return result; + } + + public static string GetMetadataOrDefault(ITaskItem item, string metadataName, string defaultValue) + { + var value = item.GetMetadata(metadataName); + return String.IsNullOrWhiteSpace(value) ? defaultValue : value; + } + } +} diff --git a/src/wix/WixToolset.BuildTasks/UpdateProjectReferenceMetadata.cs b/src/wix/WixToolset.BuildTasks/UpdateProjectReferenceMetadata.cs index c1ef45bc..d391bdb9 100644 --- a/src/wix/WixToolset.BuildTasks/UpdateProjectReferenceMetadata.cs +++ b/src/wix/WixToolset.BuildTasks/UpdateProjectReferenceMetadata.cs @@ -6,15 +6,16 @@ namespace WixToolset.BuildTasks using System.Collections.Generic; using System.IO; using System.Linq; + using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; /// - /// This task adds publish metadata to the appropriate project references. + /// This task adds mutli-targeting and publish metadata to the appropriate project references. /// public class UpdateProjectReferenceMetadata : Task { - private static readonly char[] TargetFrameworksSplitter = new char[] { ',', ';' }; + private static readonly char[] MetadataPairSplitter = new char[] { '|' }; /// /// The list of project references that exist. @@ -29,168 +30,449 @@ namespace WixToolset.BuildTasks public ITaskItem[] UpdatedProjectReferences { get; private set; } /// - /// Finds all project references requesting publishing and updates them to publish instead of build and + /// Finds all project references requesting multi-targeting and publishing and updates them to publish instead of build and /// sets target framework if requested. /// /// True upon completion of the task execution. public override bool Execute() { - var updatedProjectReferences = new List(); var intermediateFolder = Path.GetFullPath(this.IntermediateFolder); - foreach (var projectReference in this.ProjectReferences) - { - var additionalProjectReferences = new List(); + // Create the project reference facades. + var projectReferenceFacades = this.ProjectReferences.Select(p => ProjectReferenceFacade.CreateFacade(p, this.Log, intermediateFolder)); + + // Expand the facade count by applying Configurations/Platforms. + projectReferenceFacades = this.ExpandProjectReferencesForConfigurationsAndPlatforms(projectReferenceFacades); + + // Expand the facade count by applying TargetFrameworks/RuntimeIdentifiers. + projectReferenceFacades = this.ExpandProjectReferencesForTargetFrameworksAndRuntimeIdentifiers(projectReferenceFacades); - var updatedProjectReference = this.TrySetTargetFrameworksOnProjectReference(projectReference, additionalProjectReferences); + // Assign any metadata added during expansion above to the project references. + this.UpdatedProjectReferences = this.AssignMetadataToProjectReferences(projectReferenceFacades).ToArray(); + + return true; + } - if (this.TryAddPublishPropertiesToProjectReference(projectReference, intermediateFolder)) + private IEnumerable ExpandProjectReferencesForConfigurationsAndPlatforms(IEnumerable projectReferenceFacades) + { + foreach (var projectReferenceFacade in projectReferenceFacades) + { + var configurationsWithPlatforms = ExpandTerms(projectReferenceFacade.AvailableConfigurations, projectReferenceFacade.AvailablePlatforms).ToList(); + + if (configurationsWithPlatforms.Count == 0) + { + yield return projectReferenceFacade; + } + else { - foreach (var additionalProjectReference in additionalProjectReferences) + var expand = new List(configurationsWithPlatforms.Count) { - this.TryAddPublishPropertiesToProjectReference(additionalProjectReference, intermediateFolder); + projectReferenceFacade + }; + + // First, clone the project reference so there are enough facades for all of the + // requested configurations/platforms. + for (var i = 1; i < configurationsWithPlatforms.Count; ++i) + { + expand.Add(projectReferenceFacade.Clone()); } - updatedProjectReference = true; + // Then set the configuration/platform on each project reference. + for (var i = 0; i < configurationsWithPlatforms.Count; ++i) + { + expand[i].Configuration = configurationsWithPlatforms[i].FirstTerm; + expand[i].Platform = configurationsWithPlatforms[i].SecondTerm; + + yield return expand[i]; + } } + } + } + + private IEnumerable ExpandProjectReferencesForTargetFrameworksAndRuntimeIdentifiers(IEnumerable projectReferenceFacades) + { + foreach (var projectReferenceFacade in projectReferenceFacades) + { + var tfmsWithRids = ExpandTerms(projectReferenceFacade.AvailableTargetFrameworks, projectReferenceFacade.AvailableRuntimeIdentifiers).ToList(); - if (updatedProjectReference) + if (tfmsWithRids.Count == 0) { - updatedProjectReferences.Add(projectReference); + yield return projectReferenceFacade; } + else + { + var expand = new List(tfmsWithRids.Count) + { + projectReferenceFacade + }; - updatedProjectReferences.AddRange(additionalProjectReferences); - } + // First, clone the project reference so there are enough facades for all of the + // requested target frameworks/runtime identifiers. + for (var i = 1; i < tfmsWithRids.Count; ++i) + { + expand.Add(projectReferenceFacade.Clone()); + } - this.UpdatedProjectReferences = updatedProjectReferences.ToArray(); + // Then set the target framework/runtime identifier on each project reference. + for (var i = 0; i < tfmsWithRids.Count; ++i) + { + expand[i].TargetFramework = tfmsWithRids[i].FirstTerm; + expand[i].RuntimeIdentifier = tfmsWithRids[i].SecondTerm; - return true; + yield return expand[i]; + } + } + } } - private bool TryAddPublishPropertiesToProjectReference(ITaskItem projectReference, string intermediateFolder) + private IEnumerable AssignMetadataToProjectReferences(IEnumerable facades) { - var publish = projectReference.GetMetadata("Publish"); - var publishDir = projectReference.GetMetadata("PublishDir"); - - if (publish.Equals("true", StringComparison.OrdinalIgnoreCase) || - (String.IsNullOrWhiteSpace(publish) && !String.IsNullOrWhiteSpace(publishDir))) + foreach (var facade in facades) { - if (String.IsNullOrWhiteSpace(publishDir)) + var projectReference = facade.ProjectReference; + var targetsValue = new MetadataValueList(projectReference, "Targets"); + + if (facade.Modified) { - publishDir = CalculatePublishDirFromProjectReference(projectReference, intermediateFolder); + var configurationValue = new MetadataValue(projectReference, "Configuration"); + var platformValue = new MetadataValue(projectReference, "Platform"); + var fullConfigurationValue = new MetadataValue(projectReference, "FullConfiguration"); + var additionalProperties = new MetadataValueList(projectReference, "AdditionalProperties"); + var bindName = new MetadataValue(projectReference, "BindName"); + var bindPath = new MetadataValue(projectReference, "BindPath"); + + var publishDir = facade.CalculatePublishDir(); + + additionalProperties.SetValue("PublishDir=", publishDir); + additionalProperties.SetValue("RuntimeIdentifier=", facade.RuntimeIdentifier); + + additionalProperties.Apply(); + + if (!String.IsNullOrWhiteSpace(facade.Configuration)) + { + projectReference.SetMetadata("SetConfiguration", $"Configuration={facade.Configuration}"); + + if (configurationValue.HadValue) + { + configurationValue.SetValue(facade.Configuration); + configurationValue.Apply(); + } + } + + if (!String.IsNullOrWhiteSpace(facade.Platform)) + { + projectReference.SetMetadata("SetPlatform", $"Platform={facade.Platform}"); + + if (platformValue.HadValue) + { + platformValue.SetValue(facade.Platform); + platformValue.Apply(); + } + } + + if (fullConfigurationValue.HadValue && (configurationValue.Modified || platformValue.Modified)) + { + fullConfigurationValue.SetValue($"{configurationValue.Value}|{platformValue.Value}"); + fullConfigurationValue.Apply(); + } + + if (!String.IsNullOrWhiteSpace(facade.TargetFramework)) + { + projectReference.SetMetadata("SetTargetFramework", $"TargetFramework={facade.TargetFramework}"); + } + + var bindNameSuffix = facade.CalculateBindNameSuffix(); + if (!String.IsNullOrWhiteSpace(bindNameSuffix)) + { + var bindNamePrefix = bindName.HadValue ? bindName.Value : Path.GetFileNameWithoutExtension(projectReference.ItemSpec); + + bindName.SetValue(ToolsCommon.CreateIdentifierFromValue(bindNamePrefix + bindNameSuffix)); + bindName.Apply(); + } + + if (!bindPath.HadValue) + { + bindPath.SetValue(publishDir); + bindPath.Apply(); + } } - publishDir = AppendTargetFrameworkFromProjectReference(projectReference, publishDir); + if (facade.Publish) + { + var publishTargets = new MetadataValueList(projectReference, "PublishTargets"); + + if (publishTargets.HadValue) + { + targetsValue.AddRange(publishTargets.Values); + } + else + { + targetsValue.SetValue(null, "Publish"); + } + + // GetTargetPath target always needs to be last so we can set bind paths to the output location of the project reference. + if (targetsValue.Values.Count == 0 || !"GetTargetPath".Equals(targetsValue.Values[targetsValue.Values.Count - 1], StringComparison.OrdinalIgnoreCase)) + { + targetsValue.Values.Remove("GetTargetPath"); + targetsValue.SetValue(null, "GetTargetPath"); + } + } - publishDir = Path.GetFullPath(publishDir); + if (targetsValue.Modified) + { + targetsValue.Apply(); + } - this.AddPublishPropertiesToProjectReference(projectReference, publishDir); + this.Log.LogMessage(MessageImportance.Low, "Adding metadata to project reference {0} Targets {1}, BindPath {2}={3}, AdditionalProperties: {4}", + projectReference.ItemSpec, projectReference.GetMetadata("Targets"), projectReference.GetMetadata("BindName"), projectReference.GetMetadata("BindPath"), projectReference.GetMetadata("AdditionalProperties")); - return true; + yield return projectReference; } - - return false; } - private bool TrySetTargetFrameworksOnProjectReference(ITaskItem projectReference, List additionalProjectReferences) + private static IEnumerable ExpandTerms(IReadOnlyCollection firstTerms, IReadOnlyCollection secondTerms) { - var setTargetFramework = projectReference.GetMetadata("SetTargetFramework"); - var targetFrameworks = projectReference.GetMetadata("TargetFrameworks"); - var targetFrameworksToSet = targetFrameworks.Split(TargetFrameworksSplitter).Where(s => !String.IsNullOrWhiteSpace(s)).ToList(); + if (firstTerms.Count == 0) + { + firstTerms = new[] { String.Empty }; + } - if (String.IsNullOrWhiteSpace(setTargetFramework) && targetFrameworksToSet.Count > 0) + foreach (var firstTerm in firstTerms) { - // First, clone the project reference so there are enough duplicates for all of the - // requested target frameworks. - for (var i = 1; i < targetFrameworksToSet.Count; ++i) + var pairSplit = firstTerm.Split(MetadataPairSplitter, 2); + + // No pair indicator so expand the first term by the second term. + if (pairSplit.Length == 1) { - additionalProjectReferences.Add(new TaskItem(projectReference)); + if (secondTerms.Count == 0) + { + yield return new ExpansionTerms(firstTerm, null); + } + else + { + foreach (var secondTerm in secondTerms) + { + yield return new ExpansionTerms(firstTerm, secondTerm); + } + } } - - // Then set the target framework on each project reference. - for (var i = 0; i < targetFrameworksToSet.Count; ++i) + else // there was a pair like "first|second" or "first|" or "|second" in the first term, so return that value as the pair. { - var reference = (i == 0) ? projectReference : additionalProjectReferences[i - 1]; - - this.SetTargetFrameworkOnProjectReference(reference, targetFrameworksToSet[i]); + yield return new ExpansionTerms(pairSplit[0], pairSplit[1]); } + } + } - return true; + private class ProjectReferenceFacade + { + private string configuration; + private string platform; + private string targetFramework; + private string runtimeIdentifier; + + public ProjectReferenceFacade(ITaskItem projectReference, IReadOnlyCollection availableConfigurations, string configuration, IReadOnlyCollection availablePlatforms, string platform, IReadOnlyCollection availableTargetFrameworks, string targetFramework, IReadOnlyCollection availableRuntimeIdentifiers, string runtimeIdentifier, string publishBaseDir) + { + this.ProjectReference = projectReference; + this.AvailableConfigurations = availableConfigurations; + this.configuration = configuration; + this.AvailablePlatforms = availablePlatforms; + this.platform = platform; + this.AvailableTargetFrameworks = availableTargetFrameworks; + this.targetFramework = targetFramework; + this.AvailableRuntimeIdentifiers = availableRuntimeIdentifiers; + this.runtimeIdentifier = runtimeIdentifier; + this.PublishBaseDir = publishBaseDir; + this.Modified = !String.IsNullOrWhiteSpace(configuration) || !String.IsNullOrWhiteSpace(platform) || + !String.IsNullOrWhiteSpace(targetFramework) || !String.IsNullOrWhiteSpace(runtimeIdentifier) || + !String.IsNullOrWhiteSpace(publishBaseDir); } - if (!String.IsNullOrWhiteSpace(setTargetFramework) && !String.IsNullOrWhiteSpace(targetFrameworks)) + public ITaskItem ProjectReference { get; } + + public bool Modified { get; private set; } + + public IReadOnlyCollection AvailableConfigurations { get; } + + public IReadOnlyCollection AvailablePlatforms { get; } + + public IReadOnlyCollection AvailableRuntimeIdentifiers { get; } + + public IReadOnlyCollection AvailableTargetFrameworks { get; } + + public bool Publish => !String.IsNullOrEmpty(this.PublishBaseDir); + + public string PublishBaseDir { get; } + + public string Configuration { - this.Log.LogWarning("ProjectReference {0} contains metadata for both SetTargetFramework and TargetFrameworks. SetTargetFramework takes precedent so the TargetFrameworks value '{1}' is ignored", projectReference.ItemSpec, targetFrameworks); + get => this.configuration; + set => this.configuration = this.SetWithModified(value, this.configuration); } - return false; - } + public string Platform + { + get => this.platform; + set => this.platform = this.SetWithModified(value, this.platform); + } - private void AddPublishPropertiesToProjectReference(ITaskItem projectReference, string publishDir) - { - var additionalProperties = projectReference.GetMetadata("AdditionalProperties"); - if (!String.IsNullOrWhiteSpace(additionalProperties)) + public string TargetFramework { - additionalProperties += ";"; + get => this.targetFramework; + set => this.targetFramework = this.SetWithModified(value, this.targetFramework); } - additionalProperties += "PublishDir=" + publishDir; + public string RuntimeIdentifier + { + get => this.runtimeIdentifier; + set => this.runtimeIdentifier = this.SetWithModified(value, this.runtimeIdentifier); + } - var bindPath = ToolsCommon.GetMetadataOrDefault(projectReference, "BindPath", publishDir); + public ProjectReferenceFacade Clone() + { + return new ProjectReferenceFacade(new TaskItem(this.ProjectReference), this.AvailableConfigurations, this.configuration, this.AvailablePlatforms, this.platform, this.AvailableTargetFrameworks, this.targetFramework, this.AvailableRuntimeIdentifiers, this.runtimeIdentifier, this.PublishBaseDir); + } - var publishTargets = projectReference.GetMetadata("PublishTargets"); - if (String.IsNullOrWhiteSpace(publishTargets)) + public static ProjectReferenceFacade CreateFacade(ITaskItem projectReference, TaskLoggingHelper logger, string intermediateFolder) { - publishTargets = "Publish;GetTargetPath"; + var configurationsValue = new MetadataValueList(projectReference, "Configurations"); + var setConfigurationValue = new MetadataValue(projectReference, "SetConfiguration", "Configuration="); + var platformsValue = new MetadataValueList(projectReference, "Platforms"); + var setPlatformValue = new MetadataValue(projectReference, "SetPlatform", "Platform="); + var targetFrameworksValue = new MetadataValueList(projectReference, "TargetFrameworks"); + var setTargetFrameworkValue = new MetadataValue(projectReference, "SetTargetFramework", "TargetFramework="); + var runtimeIdentifiersValue = new MetadataValueList(projectReference, "RuntimeIdentifiers"); + var publishValue = new MetadataValue(projectReference, "Publish", null); + var publishDirValue = new MetadataValue(projectReference, "PublishDir", null); + + var configurations = GetFromListAndValidateSetValue(configurationsValue, setConfigurationValue, logger, projectReference, "Configuration=Release"); + + var platforms = GetFromListAndValidateSetValue(platformsValue, setPlatformValue, logger, projectReference, "Platform=x64"); + + var targetFrameworks = GetFromListAndValidateSetValue(targetFrameworksValue, setTargetFrameworkValue, logger, projectReference, "TargetFramework=tfm"); + + string publishBaseDir = null; + + if (publishValue.Value.Equals("true", StringComparison.OrdinalIgnoreCase) || (!publishValue.HadValue && publishDirValue.HadValue)) + { + if (publishDirValue.HadValue) + { + publishBaseDir = publishDirValue.Value; + } + else + { + publishBaseDir = Path.Combine(intermediateFolder, "publish", Path.GetFileNameWithoutExtension(projectReference.ItemSpec)); + } + } + + return new ProjectReferenceFacade(projectReference, configurations, null, platforms, null, targetFrameworks, null, runtimeIdentifiersValue.Values, null, publishBaseDir); } - else if (!publishTargets.EndsWith(";GetTargetsPath", StringComparison.OrdinalIgnoreCase)) + + public string CalculatePublishDir() { - publishTargets += ";GetTargetsPath"; + if (!this.Publish) + { + return null; + } + + var publishDir = this.PublishBaseDir; + + if (!String.IsNullOrWhiteSpace(this.Configuration)) + { + publishDir = Path.Combine(publishDir, this.Configuration); + } + + if (!String.IsNullOrWhiteSpace(this.Platform)) + { + publishDir = Path.Combine(publishDir, this.Platform); + } + + if (!String.IsNullOrWhiteSpace(this.TargetFramework)) + { + publishDir = Path.Combine(publishDir, this.TargetFramework); + } + + if (!String.IsNullOrWhiteSpace(this.RuntimeIdentifier)) + { + publishDir = Path.Combine(publishDir, this.RuntimeIdentifier); + } + + + return Path.GetFullPath(publishDir); } - projectReference.SetMetadata("AdditionalProperties", additionalProperties); - projectReference.SetMetadata("BindPath", bindPath); - projectReference.SetMetadata("Targets", publishTargets); + public string CalculateBindNameSuffix() + { + var sb = new StringBuilder(); - this.Log.LogMessage(MessageImportance.Low, "Adding publish metadata to project reference {0} Targets {1}, BindPath {2}, AdditionalProperties: {3}", - projectReference.ItemSpec, projectReference.GetMetadata("Targets"), projectReference.GetMetadata("BindPath"), projectReference.GetMetadata("AdditionalProperties")); - } + if (!String.IsNullOrWhiteSpace(this.Configuration)) + { + sb.AppendFormat(".{0}", this.Configuration); + } - private void SetTargetFrameworkOnProjectReference(ITaskItem projectReference, string targetFramework) - { - projectReference.SetMetadata("SetTargetFramework", $"TargetFramework={targetFramework}"); + if (!String.IsNullOrWhiteSpace(this.Platform)) + { + sb.AppendFormat(".{0}", this.Platform); + } + + if (!String.IsNullOrWhiteSpace(this.TargetFramework)) + { + sb.AppendFormat(".{0}", this.TargetFramework); + } + + if (!String.IsNullOrWhiteSpace(this.RuntimeIdentifier)) + { + sb.AppendFormat(".{0}", this.RuntimeIdentifier); + } + + return sb.ToString(); + } - var bindName = projectReference.GetMetadata("BindName"); - if (String.IsNullOrWhiteSpace(bindName)) + private string SetWithModified(string newValue, string oldValue) { - bindName = Path.GetFileNameWithoutExtension(projectReference.ItemSpec); + if (String.IsNullOrWhiteSpace(newValue) && String.IsNullOrWhiteSpace(oldValue)) + { + return String.Empty; + } + else if (oldValue != newValue) + { + this.Modified = true; + return newValue; + } - projectReference.SetMetadata("BindName", $"{bindName}.{targetFramework}"); + return oldValue; } - this.Log.LogMessage(MessageImportance.Low, "Adding target framework metadata to project reference {0} SetTargetFramework: {1}, BindName: {2}", - projectReference.ItemSpec, projectReference.GetMetadata("SetTargetFramework"), projectReference.GetMetadata("BindName")); - } + private static List GetFromListAndValidateSetValue(MetadataValueList listValue, MetadataValue setValue, TaskLoggingHelper logger, ITaskItem projectReference, string setExample) + { + var targetFrameworks = listValue.Values; - private static string CalculatePublishDirFromProjectReference(ITaskItem projectReference, string intermediateFolder) - { - var publishDir = Path.Combine("publish", Path.GetFileNameWithoutExtension(projectReference.ItemSpec)); + if (setValue.HadValue) + { + if (listValue.HadValue) + { + logger.LogMessage("ProjectReference {0} contains metadata for both {1} and {2}. {2} takes precedent so the {1} value '{3}' will be ignored", projectReference.ItemSpec, setValue.Name, listValue.Name, setValue.OriginalValue); + } + else if (!setValue.ValidValue) + { + logger.LogError("ProjectReference {0} contains invalid {1} value '{2}'. The {1} value should look something like '{3}'.", projectReference.ItemSpec, setValue.Name, setValue.OriginalValue, setExample); + } + } - return Path.Combine(intermediateFolder, publishDir); + return targetFrameworks; + } } - private static string AppendTargetFrameworkFromProjectReference(ITaskItem projectReference, string publishDir) + private class ExpansionTerms { - var setTargetFramework = projectReference.GetMetadata("SetTargetFramework"); - - if (setTargetFramework.StartsWith("TargetFramework=") && setTargetFramework.Length > "TargetFramework=".Length) + public ExpansionTerms(string firstTerm, string secondTerm) { - var targetFramework = setTargetFramework.Substring("TargetFramework=".Length); - - publishDir = Path.Combine(publishDir, targetFramework); + this.FirstTerm = firstTerm; + this.SecondTerm = secondTerm; } - return publishDir; + public string FirstTerm { get; } + + public string SecondTerm { get; } } } } diff --git a/src/wix/test/WixToolsetTest.Sdk/MsbuildFixture.cs b/src/wix/test/WixToolsetTest.Sdk/MsbuildFixture.cs index 98fffbbc..6cbd445c 100644 --- a/src/wix/test/WixToolsetTest.Sdk/MsbuildFixture.cs +++ b/src/wix/test/WixToolsetTest.Sdk/MsbuildFixture.cs @@ -670,6 +670,114 @@ namespace WixToolsetTest.Sdk } } + [Theory] + [InlineData(BuildSystem.DotNetCoreSdk)] + [InlineData(BuildSystem.MSBuild)] + [InlineData(BuildSystem.MSBuild64)] + public void CanBuildMultiTargetingWixlibUsingRids(BuildSystem buildSystem) + { + var sourceFolder = TestData.Get(@"TestData", "MultiTargetingWixlib"); + + using (var fs = new TestDataFolderFileSystem()) + { + fs.Initialize(sourceFolder); + var baseFolder = Path.Combine(fs.BaseFolder, "PackageUsingRids"); + var binFolder = Path.Combine(baseFolder, @"bin\"); + var filesFolder = Path.Combine(binFolder, "Release", @"PFiles\"); + var projectPath = Path.Combine(baseFolder, "PackageUsingRids.wixproj"); + + var result = MsbuildUtilities.BuildProject(buildSystem, projectPath, new[] { + "-Restore", + MsbuildUtilities.GetQuotedPropertySwitch(buildSystem, "WixMSBuildProps", MsbuildFixture.WixPropsPath) + }); + result.AssertSuccess(); + + var warnings = result.Output.Where(line => line.Contains(": warning")).ToArray(); + WixAssert.StringCollectionEmpty(warnings); + + var releaseFiles = Directory.EnumerateFiles(filesFolder, "*", SearchOption.AllDirectories); + var releaseFileSizes = releaseFiles.Select(p => PathAndSize(p, filesFolder)).OrderBy(s => s).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"net472_x64\e_sqlite3.dll - 1601536", + @"net472_x86\e_sqlite3.dll - 1207296", + @"net6_x64\e_sqlite3.dll - 1601536", + @"net6_x86\e_sqlite3.dll - 1207296", + }, releaseFileSizes); + } + } + + [Theory] + [InlineData(BuildSystem.DotNetCoreSdk)] + [InlineData(BuildSystem.MSBuild)] + [InlineData(BuildSystem.MSBuild64)] + public void CanBuildMultiTargetingWixlibUsingRidsWithReleaseAndDebug(BuildSystem buildSystem) + { + var sourceFolder = TestData.Get(@"TestData", "MultiTargetingWixlib"); + + using (var fs = new TestDataFolderFileSystem()) + { + fs.Initialize(sourceFolder); + var baseFolder = Path.Combine(fs.BaseFolder, "PackageReleaseAndDebug"); + var binFolder = Path.Combine(baseFolder, @"bin\"); + var filesFolder = Path.Combine(binFolder, "Release", @"PFiles\"); + var projectPath = Path.Combine(baseFolder, "PackageReleaseAndDebug.wixproj"); + + var result = MsbuildUtilities.BuildProject(buildSystem, projectPath, new[] { + "-Restore", + MsbuildUtilities.GetQuotedPropertySwitch(buildSystem, "WixMSBuildProps", MsbuildFixture.WixPropsPath) + }); + result.AssertSuccess(); + + var warnings = result.Output.Where(line => line.Contains(": warning")).ToArray(); + WixAssert.StringCollectionEmpty(warnings); + + var releaseFiles = Directory.EnumerateFiles(filesFolder, "*", SearchOption.AllDirectories); + var releaseFileSizes = releaseFiles.Select(p => PathAndSize(p, filesFolder)).OrderBy(s => s).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"debug_net472_x64\e_sqlite3.dll - 1601536", + @"debug_net472_x86\e_sqlite3.dll - 1207296", + @"debug_net6_x64\e_sqlite3.dll - 1601536", + @"debug_net6_x86\e_sqlite3.dll - 1207296", + @"release_net472_x64\e_sqlite3.dll - 1601536", + @"release_net472_x86\e_sqlite3.dll - 1207296", + @"release_net6_x64\e_sqlite3.dll - 1601536", + @"release_net6_x86\e_sqlite3.dll - 1207296", + }, releaseFileSizes); + } + } + + [Theory] + [InlineData(BuildSystem.DotNetCoreSdk)] + [InlineData(BuildSystem.MSBuild)] + [InlineData(BuildSystem.MSBuild64)] + public void CannotBuildMultiTargetingWixlibUsingExplicitSubsetOfTfmAndRid(BuildSystem buildSystem) + { + var sourceFolder = TestData.Get(@"TestData", "MultiTargetingWixlib"); + + using (var fs = new TestDataFolderFileSystem()) + { + fs.Initialize(sourceFolder); + var baseFolder = Path.Combine(fs.BaseFolder, "PackageUsingExplicitTfmAndRids"); + var binFolder = Path.Combine(baseFolder, @"bin\"); + var filesFolder = Path.Combine(binFolder, "Release", @"PFiles\"); + var projectPath = Path.Combine(baseFolder, "PackageUsingExplicitTfmAndRids.wixproj"); + + var result = MsbuildUtilities.BuildProject(buildSystem, projectPath, new[] { + "-Restore", + MsbuildUtilities.GetQuotedPropertySwitch(buildSystem, "WixMSBuildProps", MsbuildFixture.WixPropsPath) + }); + + var errors = GetDistinctErrorMessages(result.Output, baseFolder); + WixAssert.CompareLineByLine(new[] + { + @"\Package.wxs(22): error WIX0103: Cannot find the File file '!(bindpath.TestExe.net472.win_x86)\e_sqlite3.dll'. The following paths were checked: !(bindpath.TestExe.net472.win_x86)\e_sqlite3.dll [\PackageUsingExplicitTfmAndRids.wixproj]", + }, errors); + } + } [Theory] [InlineData(BuildSystem.DotNetCoreSdk)] @@ -773,9 +881,31 @@ namespace WixToolsetTest.Sdk return ReplacePathsInMessage(message.Substring(start, end - start), baseFolder); } + private static string[] GetDistinctErrorMessages(string[] output, string baseFolder) + { + return output.Where(l => l.Contains(": error ")).Select(line => + { + var trimmed = ReplacePathsInMessage(line, baseFolder); + + // If the message starts with a multi-proc build marker (like: "1>" or "2>") trim it. + if (trimmed[1] == '>') + { + trimmed = trimmed.Substring(2); + } + + return trimmed; + }).Distinct().ToArray(); + } + private static string ReplacePathsInMessage(string message, string baseFolder) { - return message.Replace(baseFolder, "").Trim(); + return message.Trim().Replace(baseFolder, ""); + } + + private static string PathAndSize(string path, string replace) + { + var fi = new FileInfo(path); + return $"{fi.FullName.Replace(replace, String.Empty)} - {fi.Length}"; } } } diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.props b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.props new file mode 100644 index 00000000..16b4c2cd --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.props @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.targets b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.targets new file mode 100644 index 00000000..5901760f --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/Directory.Build.targets @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/Package.wxs b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/Package.wxs new file mode 100644 index 00000000..b6da91ba --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/Package.wxs @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/PackageReleaseAndDebug.wixproj b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/PackageReleaseAndDebug.wixproj new file mode 100644 index 00000000..17e17d98 --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageReleaseAndDebug/PackageReleaseAndDebug.wixproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/Package.wxs b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/Package.wxs new file mode 100644 index 00000000..838461da --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/Package.wxs @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/PackageUsingExplicitTfmAndRids.wixproj b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/PackageUsingExplicitTfmAndRids.wixproj new file mode 100644 index 00000000..42002428 --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingExplicitTfmAndRids/PackageUsingExplicitTfmAndRids.wixproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/Package.wxs b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/Package.wxs new file mode 100644 index 00000000..2fe7a289 --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/Package.wxs @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/PackageUsingRids.wixproj b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/PackageUsingRids.wixproj new file mode 100644 index 00000000..e7d834d6 --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/PackageUsingRids/PackageUsingRids.wixproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/Program.cs b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/Program.cs new file mode 100644 index 00000000..a9c3c485 --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/Program.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.Sdk.TestData.MultiTargetingWixlib.MultiTargetingClassLib +{ + using System; + + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("This is Test.exe"); + } + } +} diff --git a/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/TestExe.csproj b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/TestExe.csproj new file mode 100644 index 00000000..6c72d9c2 --- /dev/null +++ b/src/wix/test/WixToolsetTest.Sdk/TestData/MultiTargetingWixlib/TestExe/TestExe.csproj @@ -0,0 +1,12 @@ + + + + Exe + net6.0;net472 + win-x86;win-x64 + + + + + + -- cgit v1.2.3-55-g6feb