From 1167830bcdbce529de32d6a8551d38687f61a66f Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Thu, 4 Jun 2026 15:00:20 -0700 Subject: [PATCH 01/18] Add Microsoft.Build.MsixPackaging SDK New MSBuild SDK that packages multiple .NET projects into a single sideloadable MSIX using per-project AppxFragment.xml manifest merging. Replaces WAP/DesktopBridge with a transparent, SDK-style workflow. Includes: - 3 compiled MSBuild tasks (netstandard2.0): - MergeAppxFragments: multi-section fragment merge + version/arch stamping - ValidateAppxManifest: XML well-formedness + required elements + duplicate IDs - FindWindowsSdkTool: locates MakeAppx/SignTool/MakePri from Windows SDK - Sdk.props: imports NoTargets, defines 19 configurable properties - Sdk.targets: BuildMsix orchestrator with 7-stage pipeline + 4 opt-in targets - VS Property Page (XAML Rule) with 10 properties across 4 categories - build/ files for traditional NuGet package consumption Ported from SEnglard Ideas/MsixPackagingSdk prototype and adapted to MSBuildSdks repo conventions (Microsoft.Build.* naming, central package management, signing, Nerdbank.GitVersioning). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Build.MsixPackaging.csproj | 40 +++ src/MsixPackaging/README.md | 171 ++++++++++ ...soft.Build.MsixPackaging.PropertyPage.xaml | 170 ++++++++++ src/MsixPackaging/Sdk/Sdk.props | 65 ++++ src/MsixPackaging/Sdk/Sdk.targets | 317 ++++++++++++++++++ src/MsixPackaging/Tasks/FindWindowsSdkTool.cs | 82 +++++ src/MsixPackaging/Tasks/MergeAppxFragments.cs | 276 +++++++++++++++ .../Tasks/ValidateAppxManifest.cs | 190 +++++++++++ .../build/Microsoft.Build.MsixPackaging.props | 13 + .../Microsoft.Build.MsixPackaging.targets | 13 + src/MsixPackaging/version.json | 4 + 11 files changed, 1341 insertions(+) create mode 100644 src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj create mode 100644 src/MsixPackaging/README.md create mode 100644 src/MsixPackaging/Sdk/Microsoft.Build.MsixPackaging.PropertyPage.xaml create mode 100644 src/MsixPackaging/Sdk/Sdk.props create mode 100644 src/MsixPackaging/Sdk/Sdk.targets create mode 100644 src/MsixPackaging/Tasks/FindWindowsSdkTool.cs create mode 100644 src/MsixPackaging/Tasks/MergeAppxFragments.cs create mode 100644 src/MsixPackaging/Tasks/ValidateAppxManifest.cs create mode 100644 src/MsixPackaging/build/Microsoft.Build.MsixPackaging.props create mode 100644 src/MsixPackaging/build/Microsoft.Build.MsixPackaging.targets create mode 100644 src/MsixPackaging/version.json diff --git a/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj b/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj new file mode 100644 index 00000000..8aa928f9 --- /dev/null +++ b/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj @@ -0,0 +1,40 @@ + + + netstandard2.0 + build\ + true + MSBuild SDK for packaging multiple .NET projects into a single sideloadable MSIX using per-project AppxFragment manifest merging. + MSBuild MSBuildSdk MSIX Packaging AppxManifest + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MsixPackaging/README.md b/src/MsixPackaging/README.md new file mode 100644 index 00000000..68695b51 --- /dev/null +++ b/src/MsixPackaging/README.md @@ -0,0 +1,171 @@ +# Microsoft.Build.MsixPackaging + +An MSBuild SDK that packages multiple .NET projects into a single sideloadable MSIX. Replaces the WAP/DesktopBridge packaging pipeline with a transparent, SDK-style workflow using per-project `AppxFragment.xml` manifest entries. + +## Quick Start + +### 1. Create a packaging project + +```xml + + + + net10.0 + MyAppBundle + + + + + + +``` + +### 2. Create `Package.base.appxmanifest` + +Place in the same directory as the `.msbuildproj`: + +```xml + + + + + My App Bundle + My Team + Images\StoreLogo.png + + + + + + + + + + + + + +``` + +### 3. Add `AppxFragment.xml` to each app project + +```xml + + + +``` + +### 4. Build + +```powershell +dotnet build MyPackage.msbuildproj -c Release +``` + +## How It Works + +``` +Package.base.appxmanifest (template with markers) + + App1/AppxFragment.xml + + App2/AppxFragment.xml + ──────────────────────────── + = MsixLayout/AppxManifest.xml (generated at build time) + +dotnet publish → MsixLayout/{LayoutDir}/ (each project published separately) +MsixImages/*.png → MsixLayout/Images/ (auto-discovered from project dirs) +MsixContent → MsixLayout/{PackagePath} (arbitrary content files) +MakePri.exe → resources.pri (auto-detected .resw resources) +MakeAppx.exe pack → MyAppBundle.msix +``` + +The `BuildMsix` orchestrator target drives 7 pipeline targets via `DependsOnTargets`: + +| # | Target | Description | +|---|--------|-------------| +| 1 | `PublishToLayout` | Publishes each `ProjectReference` with `LayoutDir` to `MsixLayout/{LayoutDir}/` | +| 2 | `MergeAppxManifest` | Discovers and merges `AppxFragment.xml` files into the base manifest | +| 3 | `ValidateAppxManifest` | Validates the merged manifest: XML well-formedness, required elements, duplicate Application IDs | +| 4 | `CopyMsixAssets` | Copies images + `MsixContent` items to the layout | +| 5 | `GenerateResourceIndex` | Runs `MakePri.exe` to generate `resources.pri` (auto-detected or explicit) | +| 6 | `PackMsix` | Calls `MakeAppx.exe pack` to produce the `.msix` | +| 7 | `SignMsix` | Optionally signs with `SignTool.exe` when `MsixSigningEnabled=true` | + +Additional opt-in targets: + +| Target | Description | +|--------|-------------| +| `CleanMsixLayout` | Removes layout directory and `.msix` on `dotnet clean` | +| `InstallMsix` | Installs the built `.msix` via `Add-AppxPackage` | +| `RegisterMsixLayout` | Registers the layout directory for dev-loop testing without packing | +| `UninstallMsix` | Removes the installed package by name | + +## Properties + +| Property | Default | Description | +|----------|---------|-------------| +| `MsixLayoutDir` | `obj/{Config}/MsixLayout` | Intermediate layout directory | +| `MsixOutputDir` | `bin/{Config}/` | Output directory for the `.msix` | +| `MsixFileName` | `$(MSBuildProjectName)` | Output file name (without `.msix`) | +| `BaseAppxManifest` | `Package.base.appxmanifest` | Path to the base manifest template | +| `AppxFragmentFileName` | `AppxFragment.xml` | Name of per-project fragment files | +| `MsixPackageImagesDir` | `$(ProjectDir)\Images` | Package-level images directory | +| `MsixSigningEnabled` | `false` | Enable MSIX signing | +| `MsixCertificatePath` | — | Path to `.pfx` certificate | +| `MsixCertificatePassword` | — | Certificate password | +| `MsixResourceIndexEnabled` | `auto` | Resource indexing: `true`, `false`, `auto` | +| `MsixPriConfigPath` | — | Custom MakePri config file | +| `MsixPriDefaultLanguage` | `en-US` | Default language for PRI config | +| `MsixPackageVersion` | — | Patches `Identity/@Version` (four-part numeric) | +| `MsixTargetArchitecture` | — | Patches `Identity/@ProcessorArchitecture` | +| `MsixToolArchitecture` | auto-detect | Host architecture for Windows SDK tools | +| `MsixDeployOnBuild` | `false` | Auto-register layout after build | +| `MsixAutoDeployInVS` | `true` | Auto-enables deploy when building in VS | +| `MsixDeployMode` | `layout` | `layout` (fast) or `msix` (full install) | + +## Items + +| Item | Metadata | Description | +|------|----------|-------------| +| `ProjectReference` | `LayoutDir` | Subdirectory name in the MSIX layout | +| `MsixContent` | `PackagePath` | Arbitrary content files to include in the package | + +## Multi-Section Fragments + +Fragment files can use a structured format to contribute to multiple manifest sections: + +```xml + + + + + + + +``` + +Supported insertion markers: +- `` — in `` (required) +- `` — in `` (optional) +- `` — in `` (optional) +- `` — in `` (optional) + +## VS Property Page + +The SDK includes a XAML Rule file that automatically adds an **MSIX Packaging** page to the VS Project Properties UI. + +**Categories:** +- **Package Identity** — `MsixFileName`, `MsixPackageVersion`, `MsixTargetArchitecture` +- **Deployment** — `MsixDeployOnBuild`, `MsixAutoDeployInVS`, `MsixDeployMode` +- **Signing** — `MsixSigningEnabled`, `MsixCertificatePath` +- **Resources** — `MsixResourceIndexEnabled`, `MsixPriDefaultLanguage` + +## Build Requirements + +- .NET SDK (version matching your `TargetFramework`) +- Windows SDK (for `MakeAppx.exe`) — any version 10.0.17763.0+ diff --git a/src/MsixPackaging/Sdk/Microsoft.Build.MsixPackaging.PropertyPage.xaml b/src/MsixPackaging/Sdk/Microsoft.Build.MsixPackaging.PropertyPage.xaml new file mode 100644 index 00000000..02ee3144 --- /dev/null +++ b/src/MsixPackaging/Sdk/Microsoft.Build.MsixPackaging.PropertyPage.xaml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props new file mode 100644 index 00000000..50d84a58 --- /dev/null +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -0,0 +1,65 @@ + + + + + + + + + + $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(IntermediateOutputPath), 'MsixLayout')) + + + $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(OutputPath))) + + + $(MSBuildProjectName) + + + $(MSBuildProjectDirectory)\Package.base.appxmanifest + + + AppxFragment.xml + + + false + + + $(MSBuildProjectDirectory)\Images + + + auto + + en-US + + + false + + true + + layout + + + + + true + + + + + + Project + + + + + + + true + + + diff --git a/src/MsixPackaging/Sdk/Sdk.targets b/src/MsixPackaging/Sdk/Sdk.targets new file mode 100644 index 00000000..014d4277 --- /dev/null +++ b/src/MsixPackaging/Sdk/Sdk.targets @@ -0,0 +1,317 @@ + + + + + + + + + + + <_MsixSdkTasksAssembly Condition="'$(_MsixSdkTasksAssembly)' == ''">$(MSBuildThisFileDirectory)..\build\netstandard2.0\Microsoft.Build.MsixPackaging.dll + + + + + + + + + + <_BuildMsixCoreDependsOn> + PublishToLayout; + DiscoverAppxFragments; + MergeAppxManifest; + ValidateAppxManifest; + CopyMsixAssets; + GenerateResourceIndex + + + + + $(_BuildMsixCoreDependsOn); + DeployMsixLayout + + + + + $(_BuildMsixCoreDependsOn); + PackMsix; + SignMsix; + DeployMsixInstall + + + + + $(_BuildMsixCoreDependsOn); + PackMsix; + SignMsix + + + + + + + + + + + + + + + + + + + + + <_FragmentCandidate Include="@(ProjectReference->'%(RootDir)%(Directory)$(AppxFragmentFileName)')" /> + <_DiscoveredFragment Include="@(_FragmentCandidate)" Condition="Exists('%(Identity)')" /> + + + + + + + + + + + + + + + + + + + + + + + + + <_PackageImage Include="$(MsixPackageImagesDir)\*.png" Condition="Exists('$(MsixPackageImagesDir)')" /> + + + + + + <_MsixImageCandidate Include="@(ProjectReference->'%(RootDir)%(Directory)MsixImages\*.png')" /> + <_DiscoveredMsixImage Include="@(_MsixImageCandidate)" Condition="Exists('%(Identity)')" /> + + + + + + + + + + + + + + <_ReswFile Include="$(MsixLayoutDir)\**\*.resw" /> + + + + <_RunMakePri Condition="'$(MsixResourceIndexEnabled)' == 'true'">true + <_RunMakePri Condition="'$(MsixResourceIndexEnabled)' == 'auto' AND '@(_ReswFile)' != ''">true + <_RunMakePri Condition="'$(MsixResourceIndexEnabled)' == 'false'">false + <_RunMakePri Condition="'$(_RunMakePri)' == ''">false + + <_PriConfigPath>$(MsixPriConfigPath) + <_PriConfigPath Condition="'$(_PriConfigPath)' == ''">$(IntermediateOutputPath)priconfig.xml + <_GeneratePriConfig Condition="'$(_RunMakePri)' == 'true' AND '$(MsixPriConfigPath)' == ''">true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MsixPackaging/Tasks/FindWindowsSdkTool.cs b/src/MsixPackaging/Tasks/FindWindowsSdkTool.cs new file mode 100644 index 00000000..01400d5d --- /dev/null +++ b/src/MsixPackaging/Tasks/FindWindowsSdkTool.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.MsixPackaging.Tasks +{ + /// + /// MSBuild task that locates a tool (MakeAppx.exe, SignTool.exe, MakePri.exe) + /// from the Windows 10 SDK installation. Searches for the latest installed SDK + /// version that contains the requested tool. + /// + public class FindWindowsSdkTool : Task + { + /// + /// Name of the tool to find (e.g., "makeappx.exe", "signtool.exe"). + /// + [Required] + public string ToolName { get; set; } = string.Empty; + + /// + /// Target architecture subdirectory to search (e.g., "x64", "x86", "arm64"). + /// Defaults to auto-detected host architecture. + /// + public string Architecture { get; set; } = string.Empty; + + /// + /// Full path to the discovered tool. Set as output if the tool is found. + /// + [Output] + public string ToolPath { get; set; } = string.Empty; + + public override bool Execute() + { + if (string.IsNullOrEmpty(Architecture)) + { + Architecture = DetectHostArchitecture(); + Log.LogMessage(MessageImportance.Low, "Auto-detected host architecture: {0}", Architecture); + } + + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + var sdkRoot = Path.Combine(programFilesX86, "Windows Kits", "10", "bin"); + + if (!Directory.Exists(sdkRoot)) + { + Log.LogError("Windows SDK not found at: {0}", sdkRoot); + return false; + } + + // Find the latest SDK version directory that contains the tool + ToolPath = Directory.GetDirectories(sdkRoot, "10.*") + .OrderByDescending(d => d) + .Select(d => Path.Combine(d, Architecture, ToolName)) + .FirstOrDefault(File.Exists) + ?? string.Empty; + + if (string.IsNullOrEmpty(ToolPath)) + { + Log.LogError("{0} not found in any Windows SDK version under: {1} (arch: {2})", + ToolName, sdkRoot, Architecture); + return false; + } + + Log.LogMessage(MessageImportance.High, "Found {0}: {1}", ToolName, ToolPath); + return true; + } + + internal static string DetectHostArchitecture() + { + var arch = RuntimeInformation.ProcessArchitecture; + switch (arch) + { + case System.Runtime.InteropServices.Architecture.X64: return "x64"; + case System.Runtime.InteropServices.Architecture.X86: return "x86"; + case System.Runtime.InteropServices.Architecture.Arm64: return "arm64"; + default: return "x64"; + } + } + } +} diff --git a/src/MsixPackaging/Tasks/MergeAppxFragments.cs b/src/MsixPackaging/Tasks/MergeAppxFragments.cs new file mode 100644 index 00000000..a7166f9f --- /dev/null +++ b/src/MsixPackaging/Tasks/MergeAppxFragments.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.MsixPackaging.Tasks +{ + /// + /// MSBuild task that merges per-project AppxFragment.xml files into a base + /// AppxManifest template. Supports multiple insertion markers for different + /// manifest sections and optional version stamping. + /// + public class MergeAppxFragments : Task + { + internal const string ApplicationsMarker = ""; + internal const string CapabilitiesMarker = ""; + internal const string ExtensionsMarker = ""; + internal const string DependenciesMarker = ""; + + /// + /// Path to the base AppxManifest template containing the fragment marker(s). + /// + [Required] + public string BaseManifestPath { get; set; } = string.Empty; + + /// + /// Paths to AppxFragment.xml files to merge into the manifest. + /// + public ITaskItem[] FragmentPaths { get; set; } + + /// + /// Path where the merged manifest will be written. + /// + [Required] + public string OutputPath { get; set; } = string.Empty; + + /// + /// The primary marker comment to replace in the base manifest (for Application entries). + /// Defaults to <!-- APPX_FRAGMENTS_INSERTED_HERE -->. + /// + public string Marker { get; set; } = ApplicationsMarker; + + /// + /// When set, patches the Identity/@Version attribute in the merged manifest. + /// Must be a valid four-part numeric version (e.g. 1.2.3.0). + /// + public string PackageVersion { get; set; } + + /// + /// When set, patches the Identity/@ProcessorArchitecture attribute in the merged manifest. + /// Valid values: x64, x86, arm64, neutral. + /// + public string TargetArchitecture { get; set; } + + public override bool Execute() + { + if (!File.Exists(BaseManifestPath)) + { + Log.LogError("Base manifest not found: {0}", BaseManifestPath); + return false; + } + + var baseContent = File.ReadAllText(BaseManifestPath); + if (!baseContent.Contains(Marker)) + { + Log.LogError("Base manifest does not contain the primary marker: {0}", Marker); + return false; + } + + // Accumulators for each section + var applicationFragments = new StringBuilder(); + var capabilityFragments = new StringBuilder(); + var extensionFragments = new StringBuilder(); + var dependencyFragments = new StringBuilder(); + int fragmentCount = 0; + + if (FragmentPaths != null && FragmentPaths.Length > 0) + { + var sortedPaths = new List(FragmentPaths.Length); + foreach (var item in FragmentPaths) + { + sortedPaths.Add(item.ItemSpec); + } + sortedPaths.Sort(StringComparer.OrdinalIgnoreCase); + + foreach (var fragmentPath in sortedPaths) + { + if (!File.Exists(fragmentPath)) + { + Log.LogWarning("Fragment file not found, skipping: {0}", fragmentPath); + continue; + } + + var content = File.ReadAllText(fragmentPath).Trim(); + Log.LogMessage(MessageImportance.High, " Merging fragment: {0}", fragmentPath); + + if (IsStructuredFragment(content)) + { + ParseStructuredFragment(content, fragmentPath, + applicationFragments, capabilityFragments, + extensionFragments, dependencyFragments); + } + else + { + // Plain fragment — treat as Application entry (backward compatible) + AppendIndented(applicationFragments, content); + } + fragmentCount++; + } + } + + // Replace markers with accumulated content + var merged = baseContent.Replace(Marker, applicationFragments.ToString()); + + if (baseContent.Contains(CapabilitiesMarker)) + merged = merged.Replace(CapabilitiesMarker, capabilityFragments.ToString()); + + if (baseContent.Contains(ExtensionsMarker)) + merged = merged.Replace(ExtensionsMarker, extensionFragments.ToString()); + + if (baseContent.Contains(DependenciesMarker)) + merged = merged.Replace(DependenciesMarker, dependencyFragments.ToString()); + + // Version stamping + if (!string.IsNullOrEmpty(PackageVersion)) + { + if (!IsValidMsixVersion(PackageVersion)) + { + Log.LogError("MsixPackageVersion '{0}' is not a valid four-part numeric version (e.g. 1.2.3.0)", PackageVersion); + return false; + } + merged = PatchAttribute(merged, "Version", PackageVersion); + Log.LogMessage(MessageImportance.High, " Stamped version: {0}", PackageVersion); + } + + // Architecture stamping + if (!string.IsNullOrEmpty(TargetArchitecture)) + { + merged = PatchAttribute(merged, "ProcessorArchitecture", TargetArchitecture); + Log.LogMessage(MessageImportance.High, " Stamped architecture: {0}", TargetArchitecture); + } + + var outputDir = Path.GetDirectoryName(OutputPath); + if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + + File.WriteAllText(OutputPath, merged); + Log.LogMessage(MessageImportance.High, + "Generated manifest with {0} fragment(s): {1}", fragmentCount, OutputPath); + + return true; + } + + /// + /// Checks if a fragment uses the structured format with an AppxFragment root element. + /// + internal static bool IsStructuredFragment(string content) + { + return content.StartsWith(" + /// Parses a structured fragment and distributes child elements to the appropriate section accumulators. + /// + private void ParseStructuredFragment(string content, string fragmentPath, + StringBuilder applications, StringBuilder capabilities, + StringBuilder extensions, StringBuilder dependencies) + { + XmlDocument doc; + try + { + // Wrap in a context element that declares all common MSIX namespaces + var wrapped = "<_Root xmlns=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10\" " + + "xmlns:uap=\"http://schemas.microsoft.com/appx/manifest/uap/windows10\" " + + "xmlns:uap3=\"http://schemas.microsoft.com/appx/manifest/uap/windows10/3\" " + + "xmlns:uap5=\"http://schemas.microsoft.com/appx/manifest/uap/windows10/5\" " + + "xmlns:desktop=\"http://schemas.microsoft.com/appx/manifest/desktop/windows10\" " + + "xmlns:desktop6=\"http://schemas.microsoft.com/appx/manifest/desktop/windows10/6\" " + + "xmlns:rescap=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities\">" + + content + ""; + doc = new XmlDocument(); + doc.LoadXml(wrapped); + } + catch (XmlException ex) + { + Log.LogWarning("Fragment '{0}' is not valid XML, treating as plain Application entry: {1}", + fragmentPath, ex.Message); + AppendIndented(applications, content); + return; + } + + var root = doc.DocumentElement; + if (root == null) return; + + // The AppxFragment element is the first child of our wrapper + var fragment = root.FirstChild; + if (fragment == null) return; + + foreach (XmlNode child in fragment.ChildNodes) + { + if (child.NodeType != XmlNodeType.Element) continue; + + var outerXml = child.OuterXml; + switch (child.LocalName) + { + case "Application": + AppendIndented(applications, outerXml); + break; + case "Capability": + case "rescap:Capability": + case "DeviceCapability": + AppendIndented(capabilities, outerXml); + break; + case "Extension": + case "uap:Extension": + case "uap3:Extension": + case "uap5:Extension": + case "desktop:Extension": + AppendIndented(extensions, outerXml); + break; + case "TargetDeviceFamily": + case "PackageDependency": + AppendIndented(dependencies, outerXml); + break; + default: + // Unknown section — default to applications + AppendIndented(applications, outerXml); + break; + } + } + } + + internal static void AppendIndented(StringBuilder sb, string content) + { + sb.AppendLine(); + sb.Append(" "); + sb.AppendLine(content.Replace("\n", "\n ")); + } + + internal static string PatchAttribute(string xml, string attributeName, string value) + { + // Find the attribute in the Identity element and replace its value. + // This is intentionally simple string-based patching to avoid + // full XML round-tripping which can alter whitespace/formatting. + var searchPattern = attributeName + "=\""; + var idx = xml.IndexOf(" + /// MSBuild task that validates a merged AppxManifest.xml for well-formedness + /// and required elements. Catches common authoring errors that would cause + /// package installation failures. + /// + public class ValidateAppxManifest : Task + { + internal const string AppxNamespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10"; + + /// + /// Path to the AppxManifest.xml to validate. + /// + [Required] + public string ManifestPath { get; set; } = string.Empty; + + /// + /// When true, treat validation warnings as errors. + /// + public bool TreatWarningsAsErrors { get; set; } + + public override bool Execute() + { + if (!File.Exists(ManifestPath)) + { + Log.LogError("Manifest not found: {0}", ManifestPath); + return false; + } + + XmlDocument doc; + try + { + doc = new XmlDocument(); + doc.Load(ManifestPath); + } + catch (XmlException ex) + { + Log.LogError("Manifest is not well-formed XML: {0} (line {1}, pos {2})", + ex.Message, ex.LineNumber, ex.LinePosition); + return false; + } + + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("appx", AppxNamespace); + + bool valid = true; + + // Validate Identity element + var identity = doc.SelectSingleNode("//appx:Identity", nsmgr); + if (identity == null) + { + LogValidation("Missing required element: Identity"); + valid = false; + } + else + { + valid &= ValidateAttribute(identity, "Name", "Identity"); + valid &= ValidateAttribute(identity, "Publisher", "Identity"); + valid &= ValidateAttribute(identity, "Version", "Identity"); + + var version = identity.Attributes?["Version"]?.Value; + if (version != null && !IsValidMsixVersion(version)) + { + LogValidation("Identity/@Version '{0}' is not a valid four-part numeric version (e.g. 1.0.0.0)", version); + valid = false; + } + } + + // Validate Properties + var displayName = doc.SelectSingleNode("//appx:Properties/appx:DisplayName", nsmgr); + if (displayName == null || string.IsNullOrWhiteSpace(displayName.InnerText)) + { + LogValidation("Missing required element: Properties/DisplayName"); + valid = false; + } + + var logo = doc.SelectSingleNode("//appx:Properties/appx:Logo", nsmgr); + if (logo == null || string.IsNullOrWhiteSpace(logo.InnerText)) + { + LogValidation("Missing required element: Properties/Logo"); + valid = false; + } + + // Validate Dependencies + var targetDeviceFamily = doc.SelectSingleNode("//appx:Dependencies/appx:TargetDeviceFamily", nsmgr); + if (targetDeviceFamily == null) + { + LogValidation("Missing required element: Dependencies/TargetDeviceFamily"); + valid = false; + } + + // Validate Applications + var applications = doc.SelectNodes("//appx:Applications/appx:Application", nsmgr); + if (applications == null || applications.Count == 0) + { + // Also check for applications without namespace prefix (from fragments) + var unqualifiedApps = doc.SelectNodes("//appx:Applications/Application", nsmgr); + if (unqualifiedApps != null && unqualifiedApps.Count > 0) + { + applications = unqualifiedApps; + } + else + { + LogValidation("No Application elements found in the manifest"); + valid = false; + } + } + + if (applications != null && applications.Count > 0) + { + var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (XmlNode app in applications) + { + var id = app.Attributes?["Id"]?.Value; + if (string.IsNullOrEmpty(id)) + { + LogValidation("Application element is missing required 'Id' attribute"); + valid = false; + continue; + } + + if (!seenIds.Add(id)) + { + LogValidation("Duplicate Application Id: '{0}'", id); + valid = false; + } + + if (string.IsNullOrEmpty(app.Attributes?["Executable"]?.Value)) + { + LogValidation("Application '{0}' is missing required 'Executable' attribute", id); + valid = false; + } + + if (string.IsNullOrEmpty(app.Attributes?["EntryPoint"]?.Value)) + { + LogValidation("Application '{0}' is missing required 'EntryPoint' attribute", id); + valid = false; + } + } + } + + if (valid) + { + Log.LogMessage(MessageImportance.High, + " Manifest validation passed: {0} application(s)", applications?.Count ?? 0); + } + + return valid; + } + + private bool ValidateAttribute(XmlNode node, string attributeName, string elementName) + { + if (string.IsNullOrEmpty(node.Attributes?[attributeName]?.Value)) + { + LogValidation("{0} is missing required '{1}' attribute", elementName, attributeName); + return false; + } + return true; + } + + private static bool IsValidMsixVersion(string version) + { + var parts = version.Split('.'); + if (parts.Length != 4) return false; + + foreach (var part in parts) + { + if (!ushort.TryParse(part, out _)) return false; + } + return true; + } + + private void LogValidation(string message, params object[] args) + { + if (TreatWarningsAsErrors) + Log.LogError(message, args); + else + Log.LogWarning(message, args); + } + } +} diff --git a/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.props b/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.props new file mode 100644 index 00000000..f8c44e72 --- /dev/null +++ b/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.props @@ -0,0 +1,13 @@ + + + + + $(MSBuildAllProjects);$(MsBuildThisFileFullPath) + + + + diff --git a/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.targets b/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.targets new file mode 100644 index 00000000..dde0d4c8 --- /dev/null +++ b/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.targets @@ -0,0 +1,13 @@ + + + + + $(MSBuildAllProjects);$(MsBuildThisFileFullPath) + + + + diff --git a/src/MsixPackaging/version.json b/src/MsixPackaging/version.json new file mode 100644 index 00000000..5157e3f0 --- /dev/null +++ b/src/MsixPackaging/version.json @@ -0,0 +1,4 @@ +{ + "inherit": true, + "version": "1.0" +} From 85f285337989ff51a9cc1efd43d5e4716ddb7770 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Thu, 4 Jun 2026 15:00:29 -0700 Subject: [PATCH 02/18] Add unit tests for Microsoft.Build.MsixPackaging 15 xunit tests covering the 3 compiled MSBuild tasks: - MergeAppxFragmentsTests: version validation, structured fragment detection, Identity attribute patching, indentation formatting - ValidateAppxManifestTests: valid manifest, missing Identity, duplicate Application IDs, malformed XML, missing file, invalid version - FindWindowsSdkToolTests: host architecture detection All tests pass across net472, net8.0, net9.0, and net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FindWindowsSdkToolTests.cs | 20 ++ .../MergeAppxFragmentsTests.cs | 78 +++++++ ...osoft.Build.MsixPackaging.UnitTests.csproj | 32 +++ .../MockBuildEngine.cs | 54 +++++ .../ValidateAppxManifestTests.cs | 210 ++++++++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs create mode 100644 src/MsixPackaging.UnitTests/MergeAppxFragmentsTests.cs create mode 100644 src/MsixPackaging.UnitTests/Microsoft.Build.MsixPackaging.UnitTests.csproj create mode 100644 src/MsixPackaging.UnitTests/MockBuildEngine.cs create mode 100644 src/MsixPackaging.UnitTests/ValidateAppxManifestTests.cs diff --git a/src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs b/src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs new file mode 100644 index 00000000..c0867718 --- /dev/null +++ b/src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.MsixPackaging.Tasks; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.MsixPackaging.UnitTests +{ + public class FindWindowsSdkToolTests + { + [Fact] + public void DetectHostArchitecture_ReturnsValidValue() + { + var arch = FindWindowsSdkTool.DetectHostArchitecture(); + arch.ShouldBeOneOf("x64", "x86", "arm64"); + } + } +} diff --git a/src/MsixPackaging.UnitTests/MergeAppxFragmentsTests.cs b/src/MsixPackaging.UnitTests/MergeAppxFragmentsTests.cs new file mode 100644 index 00000000..80290059 --- /dev/null +++ b/src/MsixPackaging.UnitTests/MergeAppxFragmentsTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.MsixPackaging.Tasks; +using Shouldly; +using System.Text; +using Xunit; + +namespace Microsoft.Build.MsixPackaging.UnitTests +{ + public class MergeAppxFragmentsTests + { + [Fact] + public void IsValidMsixVersion_ValidVersion_ReturnsTrue() + { + MergeAppxFragments.IsValidMsixVersion("1.0.0.0").ShouldBeTrue(); + MergeAppxFragments.IsValidMsixVersion("10.20.30.40").ShouldBeTrue(); + MergeAppxFragments.IsValidMsixVersion("65535.65535.65535.65535").ShouldBeTrue(); + } + + [Fact] + public void IsValidMsixVersion_InvalidVersion_ReturnsFalse() + { + MergeAppxFragments.IsValidMsixVersion("1.0.0").ShouldBeFalse(); + MergeAppxFragments.IsValidMsixVersion("1.0.0.0.0").ShouldBeFalse(); + MergeAppxFragments.IsValidMsixVersion("1.0.0.abc").ShouldBeFalse(); + MergeAppxFragments.IsValidMsixVersion(string.Empty).ShouldBeFalse(); + } + + [Fact] + public void IsStructuredFragment_WithAppxFragmentRoot_ReturnsTrue() + { + MergeAppxFragments.IsStructuredFragment("").ShouldBeTrue(); + } + + [Fact] + public void IsStructuredFragment_WithPlainApplication_ReturnsFalse() + { + MergeAppxFragments.IsStructuredFragment("").ShouldBeFalse(); + } + + [Fact] + public void PatchAttribute_PatchesVersionInIdentityElement() + { + var xml = ""; + var result = MergeAppxFragments.PatchAttribute(xml, "Version", "2.0.0.0"); + result.ShouldContain("Version=\"2.0.0.0\""); + result.ShouldNotContain("Version=\"1.0.0.0\""); + } + + [Fact] + public void PatchAttribute_PatchesArchitectureInIdentityElement() + { + var xml = ""; + var result = MergeAppxFragments.PatchAttribute(xml, "ProcessorArchitecture", "arm64"); + result.ShouldContain("ProcessorArchitecture=\"arm64\""); + result.ShouldNotContain("ProcessorArchitecture=\"x64\""); + } + + [Fact] + public void PatchAttribute_NoIdentityElement_ReturnsUnchanged() + { + var xml = "Test"; + var result = MergeAppxFragments.PatchAttribute(xml, "Version", "2.0.0.0"); + result.ShouldBe(xml); + } + + [Fact] + public void AppendIndented_AppendsWithIndentation() + { + var sb = new StringBuilder(); + MergeAppxFragments.AppendIndented(sb, ""); + var result = sb.ToString(); + result.ShouldContain(" "); + } + } +} diff --git a/src/MsixPackaging.UnitTests/Microsoft.Build.MsixPackaging.UnitTests.csproj b/src/MsixPackaging.UnitTests/Microsoft.Build.MsixPackaging.UnitTests.csproj new file mode 100644 index 00000000..282cb0cb --- /dev/null +++ b/src/MsixPackaging.UnitTests/Microsoft.Build.MsixPackaging.UnitTests.csproj @@ -0,0 +1,32 @@ + + + net472;net8.0;net9.0;net10.0 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/src/MsixPackaging.UnitTests/MockBuildEngine.cs b/src/MsixPackaging.UnitTests/MockBuildEngine.cs new file mode 100644 index 00000000..8d2948bc --- /dev/null +++ b/src/MsixPackaging.UnitTests/MockBuildEngine.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Framework; +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.MsixPackaging.UnitTests +{ + /// + /// Minimal IBuildEngine implementation for unit testing MSBuild tasks. + /// + internal class MockBuildEngine : IBuildEngine + { + public List Errors { get; } = new List(); + + public List Warnings { get; } = new List(); + + public List Messages { get; } = new List(); + + public bool ContinueOnError => false; + + public int LineNumberOfTaskNode => 0; + + public int ColumnNumberOfTaskNode => 0; + + public string ProjectFileOfTaskNode => string.Empty; + + public bool BuildProjectFile(string projectFileName, string[] targetNames, System.Collections.IDictionary globalProperties, System.Collections.IDictionary targetOutputs) + { + return true; + } + + public void LogCustomEvent(CustomBuildEventArgs e) + { + } + + public void LogErrorEvent(BuildErrorEventArgs e) + { + Errors.Add(e); + } + + public void LogMessageEvent(BuildMessageEventArgs e) + { + Messages.Add(e); + } + + public void LogWarningEvent(BuildWarningEventArgs e) + { + Warnings.Add(e); + } + } +} diff --git a/src/MsixPackaging.UnitTests/ValidateAppxManifestTests.cs b/src/MsixPackaging.UnitTests/ValidateAppxManifestTests.cs new file mode 100644 index 00000000..b7e2605d --- /dev/null +++ b/src/MsixPackaging.UnitTests/ValidateAppxManifestTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.MsixPackaging.Tasks; +using Shouldly; +using System.IO; +using Xunit; + +namespace Microsoft.Build.MsixPackaging.UnitTests +{ + public class ValidateAppxManifestTests + { + private const string ValidManifest = @" + + + + Test App + Test + Images\StoreLogo.png + + + + + + + + +"; + + [Fact] + public void ValidManifest_Passes() + { + var path = CreateTempManifest(ValidManifest); + try + { + var task = new ValidateAppxManifest + { + ManifestPath = path, + BuildEngine = new MockBuildEngine(), + }; + task.Execute().ShouldBeTrue(); + } + finally + { + Cleanup(path); + } + } + + [Fact] + public void MissingIdentity_Fails() + { + var manifest = @" + + + Test + Images\StoreLogo.png + + + + + + + +"; + + var path = CreateTempManifest(manifest); + try + { + var engine = new MockBuildEngine(); + var task = new ValidateAppxManifest + { + ManifestPath = path, + TreatWarningsAsErrors = true, + BuildEngine = engine, + }; + task.Execute().ShouldBeFalse(); + } + finally + { + Cleanup(path); + } + } + + [Fact] + public void DuplicateApplicationIds_Fails() + { + var manifest = @" + + + + Test + Test + Images\StoreLogo.png + + + + + + + + +"; + + var path = CreateTempManifest(manifest); + try + { + var engine = new MockBuildEngine(); + var task = new ValidateAppxManifest + { + ManifestPath = path, + TreatWarningsAsErrors = true, + BuildEngine = engine, + }; + task.Execute().ShouldBeFalse(); + } + finally + { + Cleanup(path); + } + } + + [Fact] + public void MalformedXml_Fails() + { + var path = CreateTempManifest(" + + + + Test + Test + Images\StoreLogo.png + + + + + + + +"; + + var path = CreateTempManifest(manifest); + try + { + var engine = new MockBuildEngine(); + var task = new ValidateAppxManifest + { + ManifestPath = path, + TreatWarningsAsErrors = true, + BuildEngine = engine, + }; + task.Execute().ShouldBeFalse(); + } + finally + { + Cleanup(path); + } + } + + private static string CreateTempManifest(string content) + { + var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".xml"); + File.WriteAllText(path, content); + return path; + } + + private static void Cleanup(params string[] paths) + { + foreach (var path in paths) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + } +} From 02c209559a37efd1bf3a0d5f673c4ee2cf3b5a61 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Thu, 4 Jun 2026 15:00:38 -0700 Subject: [PATCH 03/18] Add MsixPackaging sample project Minimal consumer project demonstrating the SDK: - SampleConsoleApp: trivial console app with AppxFragment.xml - SamplePackaging.msbuildproj: imports SDK from source tree - Package.base.appxmanifest: manifest template with fragment marker - Directory.Build.props: overrides task assembly path for local dev Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/MsixPackaging/Directory.Build.props | 6 +++ samples/MsixPackaging/Images/StoreLogo.png | Bin 0 -> 66 bytes .../MsixPackaging/Package.base.appxmanifest | 47 ++++++++++++++++++ .../SampleConsoleApp/AppxFragment.xml | 21 ++++++++ .../SampleConsoleApp.Square44x44Logo.png | Bin 0 -> 66 bytes .../MsixPackaging/SampleConsoleApp/Program.cs | 1 + .../SampleConsoleApp/SampleConsoleApp.csproj | 11 ++++ .../SamplePackaging.msbuildproj | 30 +++++++++++ 8 files changed, 116 insertions(+) create mode 100644 samples/MsixPackaging/Directory.Build.props create mode 100644 samples/MsixPackaging/Images/StoreLogo.png create mode 100644 samples/MsixPackaging/Package.base.appxmanifest create mode 100644 samples/MsixPackaging/SampleConsoleApp/AppxFragment.xml create mode 100644 samples/MsixPackaging/SampleConsoleApp/MsixImages/SampleConsoleApp.Square44x44Logo.png create mode 100644 samples/MsixPackaging/SampleConsoleApp/Program.cs create mode 100644 samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj create mode 100644 samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj diff --git a/samples/MsixPackaging/Directory.Build.props b/samples/MsixPackaging/Directory.Build.props new file mode 100644 index 00000000..443d686b --- /dev/null +++ b/samples/MsixPackaging/Directory.Build.props @@ -0,0 +1,6 @@ + + + + <_MsixSdkTasksAssembly>$(MSBuildThisFileDirectory)..\..\src\MsixPackaging\bin\Release\netstandard2.0\Microsoft.Build.MsixPackaging.dll + + diff --git a/samples/MsixPackaging/Images/StoreLogo.png b/samples/MsixPackaging/Images/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/samples/MsixPackaging/Package.base.appxmanifest b/samples/MsixPackaging/Package.base.appxmanifest new file mode 100644 index 00000000..08932f3c --- /dev/null +++ b/samples/MsixPackaging/Package.base.appxmanifest @@ -0,0 +1,47 @@ + + + + + + + + MsixPackaging Sample + Microsoft.Build.MsixPackaging + Images\StoreLogo.png + Sample package built with Microsoft.Build.MsixPackaging. + + + + + + + + + + + + + + + + + + + + diff --git a/samples/MsixPackaging/SampleConsoleApp/AppxFragment.xml b/samples/MsixPackaging/SampleConsoleApp/AppxFragment.xml new file mode 100644 index 00000000..40b4b306 --- /dev/null +++ b/samples/MsixPackaging/SampleConsoleApp/AppxFragment.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/samples/MsixPackaging/SampleConsoleApp/MsixImages/SampleConsoleApp.Square44x44Logo.png b/samples/MsixPackaging/SampleConsoleApp/MsixImages/SampleConsoleApp.Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/samples/MsixPackaging/SampleConsoleApp/Program.cs b/samples/MsixPackaging/SampleConsoleApp/Program.cs new file mode 100644 index 00000000..aeeadf59 --- /dev/null +++ b/samples/MsixPackaging/SampleConsoleApp/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Hello from SampleConsoleApp inside an MSIX package!"); diff --git a/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj b/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj new file mode 100644 index 00000000..b2acff83 --- /dev/null +++ b/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj @@ -0,0 +1,11 @@ + + + + Exe + net10.0 + enable + enable + SampleConsoleApp + + + diff --git a/samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj b/samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj new file mode 100644 index 00000000..fa88f8d4 --- /dev/null +++ b/samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj @@ -0,0 +1,30 @@ + + + + + + + + net10.0 + MsixPackagingSample + + + + + + + + + + + From 75fb334d6592c22f5a1b791bb83cfa96cd22a38b Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Thu, 4 Jun 2026 15:00:46 -0700 Subject: [PATCH 04/18] Add MsixPackaging projects to solution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MSBuildSdks.sln | 180 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/MSBuildSdks.sln b/MSBuildSdks.sln index 11c4f73c..31e6e05c 100644 --- a/MSBuildSdks.sln +++ b/MSBuildSdks.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11205.157 d18.0 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Traversal", "src\Traversal\Microsoft.Build.Traversal.csproj", "{B93918D4-75EA-467E-8F50-393A1324FF91}" EndProject @@ -78,80 +78,254 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Cargo.UnitT EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.UniversalPackages", "src\UniversalPackages\Microsoft.Build.UniversalPackages.csproj", "{D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MsixPackaging", "MsixPackaging", "{0C4D6365-362B-1199-AD45-EBA40BBCFC6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.MsixPackaging", "src\MsixPackaging\Microsoft.Build.MsixPackaging.csproj", "{581C0DEB-60A0-4E44-8BC6-7C84758153DC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MsixPackaging.UnitTests", "MsixPackaging.UnitTests", "{F1C7B73A-D8D3-4640-92EA-EE2C7DD1949B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.MsixPackaging.UnitTests", "src\MsixPackaging.UnitTests\Microsoft.Build.MsixPackaging.UnitTests.csproj", "{22ED619F-6DF9-4504-AB3B-06DAF94B550A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B93918D4-75EA-467E-8F50-393A1324FF91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B93918D4-75EA-467E-8F50-393A1324FF91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Debug|x64.ActiveCfg = Debug|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Debug|x64.Build.0 = Debug|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Debug|x86.ActiveCfg = Debug|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Debug|x86.Build.0 = Debug|Any CPU {B93918D4-75EA-467E-8F50-393A1324FF91}.Release|Any CPU.ActiveCfg = Release|Any CPU {B93918D4-75EA-467E-8F50-393A1324FF91}.Release|Any CPU.Build.0 = Release|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Release|x64.ActiveCfg = Release|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Release|x64.Build.0 = Release|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Release|x86.ActiveCfg = Release|Any CPU + {B93918D4-75EA-467E-8F50-393A1324FF91}.Release|x86.Build.0 = Release|Any CPU {86A02D27-6A67-461B-931C-96051F363CAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {86A02D27-6A67-461B-931C-96051F363CAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Debug|x64.ActiveCfg = Debug|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Debug|x64.Build.0 = Debug|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Debug|x86.ActiveCfg = Debug|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Debug|x86.Build.0 = Debug|Any CPU {86A02D27-6A67-461B-931C-96051F363CAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {86A02D27-6A67-461B-931C-96051F363CAD}.Release|Any CPU.Build.0 = Release|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Release|x64.ActiveCfg = Release|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Release|x64.Build.0 = Release|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Release|x86.ActiveCfg = Release|Any CPU + {86A02D27-6A67-461B-931C-96051F363CAD}.Release|x86.Build.0 = Release|Any CPU {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Debug|x64.ActiveCfg = Debug|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Debug|x64.Build.0 = Debug|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Debug|x86.ActiveCfg = Debug|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Debug|x86.Build.0 = Debug|Any CPU {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Release|Any CPU.ActiveCfg = Release|Any CPU {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Release|Any CPU.Build.0 = Release|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Release|x64.ActiveCfg = Release|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Release|x64.Build.0 = Release|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Release|x86.ActiveCfg = Release|Any CPU + {720D8DE8-C7BA-4CD0-A00A-D8A169D7FE80}.Release|x86.Build.0 = Release|Any CPU {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Debug|x64.ActiveCfg = Debug|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Debug|x64.Build.0 = Debug|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Debug|x86.ActiveCfg = Debug|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Debug|x86.Build.0 = Debug|Any CPU {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Release|Any CPU.ActiveCfg = Release|Any CPU {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Release|Any CPU.Build.0 = Release|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Release|x64.ActiveCfg = Release|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Release|x64.Build.0 = Release|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Release|x86.ActiveCfg = Release|Any CPU + {48AEFD7C-4F21-4855-9EB0-75B1EB58955C}.Release|x86.Build.0 = Release|Any CPU {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Debug|x64.Build.0 = Debug|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Debug|x86.Build.0 = Debug|Any CPU {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Release|Any CPU.Build.0 = Release|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Release|x64.ActiveCfg = Release|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Release|x64.Build.0 = Release|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Release|x86.ActiveCfg = Release|Any CPU + {FE997E79-94D9-4663-9727-ABF40B67E1CF}.Release|x86.Build.0 = Release|Any CPU {9D14030D-B050-48B0-82A4-9ADE28392533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D14030D-B050-48B0-82A4-9ADE28392533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Debug|x64.ActiveCfg = Debug|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Debug|x64.Build.0 = Debug|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Debug|x86.ActiveCfg = Debug|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Debug|x86.Build.0 = Debug|Any CPU {9D14030D-B050-48B0-82A4-9ADE28392533}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D14030D-B050-48B0-82A4-9ADE28392533}.Release|Any CPU.Build.0 = Release|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Release|x64.ActiveCfg = Release|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Release|x64.Build.0 = Release|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Release|x86.ActiveCfg = Release|Any CPU + {9D14030D-B050-48B0-82A4-9ADE28392533}.Release|x86.Build.0 = Release|Any CPU {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Debug|x64.ActiveCfg = Debug|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Debug|x64.Build.0 = Debug|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Debug|x86.ActiveCfg = Debug|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Debug|x86.Build.0 = Debug|Any CPU {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Release|Any CPU.ActiveCfg = Release|Any CPU {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Release|Any CPU.Build.0 = Release|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Release|x64.ActiveCfg = Release|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Release|x64.Build.0 = Release|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Release|x86.ActiveCfg = Release|Any CPU + {48F56A6B-A285-4B40-9E96-044F7AA2C532}.Release|x86.Build.0 = Release|Any CPU {6E84BA77-308F-4780-852F-B27F8BFD2797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E84BA77-308F-4780-852F-B27F8BFD2797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Debug|x64.ActiveCfg = Debug|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Debug|x64.Build.0 = Debug|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Debug|x86.ActiveCfg = Debug|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Debug|x86.Build.0 = Debug|Any CPU {6E84BA77-308F-4780-852F-B27F8BFD2797}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E84BA77-308F-4780-852F-B27F8BFD2797}.Release|Any CPU.Build.0 = Release|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Release|x64.ActiveCfg = Release|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Release|x64.Build.0 = Release|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Release|x86.ActiveCfg = Release|Any CPU + {6E84BA77-308F-4780-852F-B27F8BFD2797}.Release|x86.Build.0 = Release|Any CPU {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Debug|x64.ActiveCfg = Debug|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Debug|x64.Build.0 = Debug|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Debug|x86.ActiveCfg = Debug|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Debug|x86.Build.0 = Debug|Any CPU {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Release|Any CPU.ActiveCfg = Release|Any CPU {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Release|Any CPU.Build.0 = Release|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Release|x64.ActiveCfg = Release|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Release|x64.Build.0 = Release|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Release|x86.ActiveCfg = Release|Any CPU + {359B2C2B-B9B8-496F-B4B1-9E4359729F89}.Release|x86.Build.0 = Release|Any CPU {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Debug|x64.ActiveCfg = Debug|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Debug|x64.Build.0 = Debug|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Debug|x86.ActiveCfg = Debug|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Debug|x86.Build.0 = Debug|Any CPU {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Release|Any CPU.ActiveCfg = Release|Any CPU {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Release|Any CPU.Build.0 = Release|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Release|x64.ActiveCfg = Release|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Release|x64.Build.0 = Release|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Release|x86.ActiveCfg = Release|Any CPU + {18C7CBF8-98D3-4C47-A11B-2905AF23A20B}.Release|x86.Build.0 = Release|Any CPU {2035141B-4345-4E79-83DB-979A43BA5C29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2035141B-4345-4E79-83DB-979A43BA5C29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Debug|x64.ActiveCfg = Debug|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Debug|x64.Build.0 = Debug|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Debug|x86.ActiveCfg = Debug|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Debug|x86.Build.0 = Debug|Any CPU {2035141B-4345-4E79-83DB-979A43BA5C29}.Release|Any CPU.ActiveCfg = Release|Any CPU {2035141B-4345-4E79-83DB-979A43BA5C29}.Release|Any CPU.Build.0 = Release|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Release|x64.ActiveCfg = Release|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Release|x64.Build.0 = Release|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Release|x86.ActiveCfg = Release|Any CPU + {2035141B-4345-4E79-83DB-979A43BA5C29}.Release|x86.Build.0 = Release|Any CPU {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Debug|x64.ActiveCfg = Debug|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Debug|x64.Build.0 = Debug|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Debug|x86.ActiveCfg = Debug|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Debug|x86.Build.0 = Debug|Any CPU {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Release|Any CPU.Build.0 = Release|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Release|x64.ActiveCfg = Release|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Release|x64.Build.0 = Release|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Release|x86.ActiveCfg = Release|Any CPU + {153D1183-2953-4D4D-A5AD-AA2CF99B0DE3}.Release|x86.Build.0 = Release|Any CPU {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Debug|x64.Build.0 = Debug|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Debug|x86.Build.0 = Debug|Any CPU {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Release|Any CPU.Build.0 = Release|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Release|x64.ActiveCfg = Release|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Release|x64.Build.0 = Release|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Release|x86.ActiveCfg = Release|Any CPU + {AF9F2AFE-04D4-40B3-B17F-54ABD3DE7E4E}.Release|x86.Build.0 = Release|Any CPU {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Debug|x64.ActiveCfg = Debug|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Debug|x64.Build.0 = Debug|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Debug|x86.ActiveCfg = Debug|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Debug|x86.Build.0 = Debug|Any CPU {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Release|Any CPU.Build.0 = Release|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Release|x64.ActiveCfg = Release|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Release|x64.Build.0 = Release|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Release|x86.ActiveCfg = Release|Any CPU + {B4CA4749-4CDE-499F-8372-C71966C6DB16}.Release|x86.Build.0 = Release|Any CPU {D80866C1-FF2A-441B-984F-D256164BB56E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D80866C1-FF2A-441B-984F-D256164BB56E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Debug|x64.Build.0 = Debug|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Debug|x86.Build.0 = Debug|Any CPU {D80866C1-FF2A-441B-984F-D256164BB56E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D80866C1-FF2A-441B-984F-D256164BB56E}.Release|Any CPU.Build.0 = Release|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Release|x64.ActiveCfg = Release|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Release|x64.Build.0 = Release|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Release|x86.ActiveCfg = Release|Any CPU + {D80866C1-FF2A-441B-984F-D256164BB56E}.Release|x86.Build.0 = Release|Any CPU {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Debug|x64.Build.0 = Debug|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Debug|x86.Build.0 = Debug|Any CPU {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Release|Any CPU.Build.0 = Release|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Release|x64.ActiveCfg = Release|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Release|x64.Build.0 = Release|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Release|x86.ActiveCfg = Release|Any CPU + {D6EF1644-D06C-4877-A8F7-3543E5D3175B}.Release|x86.Build.0 = Release|Any CPU {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Debug|x64.Build.0 = Debug|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Debug|x86.Build.0 = Debug|Any CPU {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Release|Any CPU.Build.0 = Release|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Release|x64.ActiveCfg = Release|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Release|x64.Build.0 = Release|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Release|x86.ActiveCfg = Release|Any CPU + {D60201AA-45FE-4F15-BEDE-356BBDCA4E2F}.Release|x86.Build.0 = Release|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Debug|x64.Build.0 = Debug|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Debug|x86.Build.0 = Debug|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Release|Any CPU.Build.0 = Release|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Release|x64.ActiveCfg = Release|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Release|x64.Build.0 = Release|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Release|x86.ActiveCfg = Release|Any CPU + {581C0DEB-60A0-4E44-8BC6-7C84758153DC}.Release|x86.Build.0 = Release|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Debug|x64.ActiveCfg = Debug|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Debug|x64.Build.0 = Debug|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Debug|x86.ActiveCfg = Debug|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Debug|x86.Build.0 = Debug|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Release|Any CPU.Build.0 = Release|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Release|x64.ActiveCfg = Release|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Release|x64.Build.0 = Release|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Release|x86.ActiveCfg = Release|Any CPU + {22ED619F-6DF9-4504-AB3B-06DAF94B550A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -165,6 +339,10 @@ Global {18C7CBF8-98D3-4C47-A11B-2905AF23A20B} = {615210F2-B751-431E-B2F1-C5D3C205F899} {2035141B-4345-4E79-83DB-979A43BA5C29} = {A9CC411B-67F8-4644-873C-1ACBFC12AAA5} {469437EE-241A-4B8A-B7E0-E0F913F5529D} = {516F0D1D-C4FE-4832-9E49-903A2C57D3F3} + {0C4D6365-362B-1199-AD45-EBA40BBCFC6B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {581C0DEB-60A0-4E44-8BC6-7C84758153DC} = {0C4D6365-362B-1199-AD45-EBA40BBCFC6B} + {F1C7B73A-D8D3-4640-92EA-EE2C7DD1949B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {22ED619F-6DF9-4504-AB3B-06DAF94B550A} = {F1C7B73A-D8D3-4640-92EA-EE2C7DD1949B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6AB3C4FB-938A-42B8-8E9E-A53178C94301} From 869c4a27b5afd1e5b5118dd159e4ae692e4f1ebe Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Fri, 5 Jun 2026 12:28:11 -0700 Subject: [PATCH 05/18] Fix latent StyleCop violations in MSIX task sources MergeAppxFragments.cs and ValidateAppxManifest.cs had StyleCop violations (missing file headers, using order, omitted braces, member ordering, blank lines) that were masked by incremental analyzer caching and only surfaced on a clean build. Rewrap to satisfy the analyzers with no behavioral change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/Tasks/MergeAppxFragments.cs | 179 ++++++++++++------ .../Tasks/ValidateAppxManifest.cs | 66 +++++-- 2 files changed, 166 insertions(+), 79 deletions(-) diff --git a/src/MsixPackaging/Tasks/MergeAppxFragments.cs b/src/MsixPackaging/Tasks/MergeAppxFragments.cs index a7166f9f..712ca193 100644 --- a/src/MsixPackaging/Tasks/MergeAppxFragments.cs +++ b/src/MsixPackaging/Tasks/MergeAppxFragments.cs @@ -1,10 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.Build.MsixPackaging.Tasks { @@ -21,40 +25,38 @@ public class MergeAppxFragments : Task internal const string DependenciesMarker = ""; /// - /// Path to the base AppxManifest template containing the fragment marker(s). + /// Gets or sets the path to the base AppxManifest template containing the fragment marker(s). /// [Required] public string BaseManifestPath { get; set; } = string.Empty; /// - /// Paths to AppxFragment.xml files to merge into the manifest. + /// Gets or sets the paths to AppxFragment.xml files to merge into the manifest. /// public ITaskItem[] FragmentPaths { get; set; } /// - /// Path where the merged manifest will be written. + /// Gets or sets the path where the merged manifest will be written. /// [Required] public string OutputPath { get; set; } = string.Empty; /// - /// The primary marker comment to replace in the base manifest (for Application entries). - /// Defaults to <!-- APPX_FRAGMENTS_INSERTED_HERE -->. + /// Gets or sets the primary marker comment to replace in the base manifest (for Application entries). /// public string Marker { get; set; } = ApplicationsMarker; /// - /// When set, patches the Identity/@Version attribute in the merged manifest. - /// Must be a valid four-part numeric version (e.g. 1.2.3.0). + /// Gets or sets the version stamped into the Identity/@Version attribute. Must be four-part numeric. /// public string PackageVersion { get; set; } /// - /// When set, patches the Identity/@ProcessorArchitecture attribute in the merged manifest. - /// Valid values: x64, x86, arm64, neutral. + /// Gets or sets the architecture stamped into the Identity/@ProcessorArchitecture attribute. /// public string TargetArchitecture { get; set; } + /// public override bool Execute() { if (!File.Exists(BaseManifestPath)) @@ -84,6 +86,7 @@ public override bool Execute() { sortedPaths.Add(item.ItemSpec); } + sortedPaths.Sort(StringComparer.OrdinalIgnoreCase); foreach (var fragmentPath in sortedPaths) @@ -99,15 +102,14 @@ public override bool Execute() if (IsStructuredFragment(content)) { - ParseStructuredFragment(content, fragmentPath, - applicationFragments, capabilityFragments, - extensionFragments, dependencyFragments); + ParseStructuredFragment(content, fragmentPath, applicationFragments, capabilityFragments, extensionFragments, dependencyFragments); } else { // Plain fragment — treat as Application entry (backward compatible) AppendIndented(applicationFragments, content); } + fragmentCount++; } } @@ -116,13 +118,19 @@ public override bool Execute() var merged = baseContent.Replace(Marker, applicationFragments.ToString()); if (baseContent.Contains(CapabilitiesMarker)) + { merged = merged.Replace(CapabilitiesMarker, capabilityFragments.ToString()); + } if (baseContent.Contains(ExtensionsMarker)) + { merged = merged.Replace(ExtensionsMarker, extensionFragments.ToString()); + } if (baseContent.Contains(DependenciesMarker)) + { merged = merged.Replace(DependenciesMarker, dependencyFragments.ToString()); + } // Version stamping if (!string.IsNullOrEmpty(PackageVersion)) @@ -132,6 +140,7 @@ public override bool Execute() Log.LogError("MsixPackageVersion '{0}' is not a valid four-part numeric version (e.g. 1.2.3.0)", PackageVersion); return false; } + merged = PatchAttribute(merged, "Version", PackageVersion); Log.LogMessage(MessageImportance.High, " Stamped version: {0}", PackageVersion); } @@ -150,8 +159,7 @@ public override bool Execute() } File.WriteAllText(OutputPath, merged); - Log.LogMessage(MessageImportance.High, - "Generated manifest with {0} fragment(s): {1}", fragmentCount, OutputPath); + Log.LogMessage(MessageImportance.High, "Generated manifest with {0} fragment(s): {1}", fragmentCount, OutputPath); return true; } @@ -159,17 +167,94 @@ public override bool Execute() /// /// Checks if a fragment uses the structured format with an AppxFragment root element. /// + /// The fragment content. + /// if the fragment is structured. internal static bool IsStructuredFragment(string content) { return content.StartsWith(" + /// Appends content to the accumulator with manifest indentation. + /// + /// The accumulator. + /// The content to append. + internal static void AppendIndented(StringBuilder sb, string content) + { + sb.AppendLine(); + sb.Append(" "); + sb.AppendLine(content.Replace("\n", "\n ")); + } + + /// + /// Replaces the value of an attribute on the Identity element using simple string patching. + /// + /// The manifest XML. + /// The attribute to patch. + /// The new value. + /// The patched XML. + internal static string PatchAttribute(string xml, string attributeName, string value) + { + // Find the attribute in the Identity element and replace its value. + // This is intentionally simple string-based patching to avoid + // full XML round-tripping which can alter whitespace/formatting. + var searchPattern = attributeName + "=\""; + var idx = xml.IndexOf(" + /// Determines whether a version string is a valid four-part numeric version. + /// + /// The version string. + /// if the version is valid. + internal static bool IsValidMsixVersion(string version) + { + var parts = version.Split('.'); + if (parts.Length != 4) + { + return false; + } + + foreach (var part in parts) + { + if (!ushort.TryParse(part, out _)) + { + return false; + } + } + + return true; + } + /// /// Parses a structured fragment and distributes child elements to the appropriate section accumulators. /// - private void ParseStructuredFragment(string content, string fragmentPath, - StringBuilder applications, StringBuilder capabilities, - StringBuilder extensions, StringBuilder dependencies) + /// The fragment content. + /// The fragment file path (for diagnostics). + /// The applications accumulator. + /// The capabilities accumulator. + /// The extensions accumulator. + /// The dependencies accumulator. + private void ParseStructuredFragment(string content, string fragmentPath, StringBuilder applications, StringBuilder capabilities, StringBuilder extensions, StringBuilder dependencies) { XmlDocument doc; try @@ -188,22 +273,30 @@ private void ParseStructuredFragment(string content, string fragmentPath, } catch (XmlException ex) { - Log.LogWarning("Fragment '{0}' is not valid XML, treating as plain Application entry: {1}", - fragmentPath, ex.Message); + Log.LogWarning("Fragment '{0}' is not valid XML, treating as plain Application entry: {1}", fragmentPath, ex.Message); AppendIndented(applications, content); return; } var root = doc.DocumentElement; - if (root == null) return; + if (root == null) + { + return; + } // The AppxFragment element is the first child of our wrapper var fragment = root.FirstChild; - if (fragment == null) return; + if (fragment == null) + { + return; + } foreach (XmlNode child in fragment.ChildNodes) { - if (child.NodeType != XmlNodeType.Element) continue; + if (child.NodeType != XmlNodeType.Element) + { + continue; + } var outerXml = child.OuterXml; switch (child.LocalName) @@ -234,43 +327,5 @@ private void ParseStructuredFragment(string content, string fragmentPath, } } } - - internal static void AppendIndented(StringBuilder sb, string content) - { - sb.AppendLine(); - sb.Append(" "); - sb.AppendLine(content.Replace("\n", "\n ")); - } - - internal static string PatchAttribute(string xml, string attributeName, string value) - { - // Find the attribute in the Identity element and replace its value. - // This is intentionally simple string-based patching to avoid - // full XML round-tripping which can alter whitespace/formatting. - var searchPattern = attributeName + "=\""; - var idx = xml.IndexOf(" - /// Path to the AppxManifest.xml to validate. + /// Gets or sets the path to the AppxManifest.xml to validate. /// [Required] public string ManifestPath { get; set; } = string.Empty; /// - /// When true, treat validation warnings as errors. + /// Gets or sets a value indicating whether validation warnings are treated as errors. /// public bool TreatWarningsAsErrors { get; set; } + /// public override bool Execute() { if (!File.Exists(ManifestPath)) @@ -43,8 +48,7 @@ public override bool Execute() } catch (XmlException ex) { - Log.LogError("Manifest is not well-formed XML: {0} (line {1}, pos {2})", - ex.Message, ex.LineNumber, ex.LinePosition); + Log.LogError("Manifest is not well-formed XML: {0} (line {1}, pos {2})", ex.Message, ex.LineNumber, ex.LinePosition); return false; } @@ -150,41 +154,69 @@ public override bool Execute() if (valid) { - Log.LogMessage(MessageImportance.High, - " Manifest validation passed: {0} application(s)", applications?.Count ?? 0); + Log.LogMessage(MessageImportance.High, " Manifest validation passed: {0} application(s)", applications?.Count ?? 0); } return valid; } - private bool ValidateAttribute(XmlNode node, string attributeName, string elementName) + /// + /// Determines whether a version string is a valid four-part numeric version. + /// + /// The version string. + /// if the version is valid. + private static bool IsValidMsixVersion(string version) { - if (string.IsNullOrEmpty(node.Attributes?[attributeName]?.Value)) + var parts = version.Split('.'); + if (parts.Length != 4) { - LogValidation("{0} is missing required '{1}' attribute", elementName, attributeName); return false; } + + foreach (var part in parts) + { + if (!ushort.TryParse(part, out _)) + { + return false; + } + } + return true; } - private static bool IsValidMsixVersion(string version) + /// + /// Validates that a required attribute is present and non-empty. + /// + /// The element node. + /// The required attribute name. + /// The element name (for diagnostics). + /// if the attribute is present. + private bool ValidateAttribute(XmlNode node, string attributeName, string elementName) { - var parts = version.Split('.'); - if (parts.Length != 4) return false; - - foreach (var part in parts) + if (string.IsNullOrEmpty(node.Attributes?[attributeName]?.Value)) { - if (!ushort.TryParse(part, out _)) return false; + LogValidation("{0} is missing required '{1}' attribute", elementName, attributeName); + return false; } + return true; } + /// + /// Logs a validation problem as an error or warning depending on configuration. + /// + /// The message format string. + /// The message arguments. private void LogValidation(string message, params object[] args) { if (TreatWarningsAsErrors) + { Log.LogError(message, args); + } else + { Log.LogWarning(message, args); + } } } } From e286eb551b4c6cb1e90361b327596d866670bc86 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Fri, 5 Jun 2026 12:28:29 -0700 Subject: [PATCH 06/18] Use Microsoft.Windows.SDK.BuildTools.MSIX tasks for tooling, packing, signing Replace the SDK's hand-rolled Windows SDK tool discovery and the raw Exec calls to MakeAppx/SignTool with the compiled MSBuild tasks from the Microsoft.Windows.SDK.BuildTools.MSIX package (namespace Microsoft.Build.Msix): - Tool discovery: FindWindowsSdkTool (custom C# task) -> WinAppSdkGetSdkFileFullPath, used to locate MakeAppx.exe, SignTool.exe, and MakePri.exe. - Packing: PackMsix Exec(makeappx /d) -> WinAppSdkMakeAppxPack, packing from a generated [Files] mapping file built from the layout directory. - Signing: SignMsix Exec(signtool) -> WinAppSdkSignAppxPackage (adds timestamp and Azure Code Signing / Key Vault support). Integration (no bundling): the package is injected as a package reference from Sdk.props with GeneratePathProperty and ExcludeAssets=build;buildTransitive, so only the task assembly is used and the package's heavyweight WinAppSDK pipeline is not imported. VersionOverride is used under Central Package Management so no central PackageVersion entry is required. The task assembly is resolved per runtime (net6.0 for Core, net472 for desktop MSBuild). A small RoslynCodeTaskFactory inline task resolves the latest installed Windows SDK version for TargetPlatformVersion (honoring MsixWindowsSdkVersion when set). Removes FindWindowsSdkTool and its unit test. New properties: MsixSdkBuildToolsVersion, MsixWindowsSdkVersion, MsixHashAlgorithmId. MsixToolArchitecture is now a no-op (architecture resolved by the package). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FindWindowsSdkToolTests.cs | 20 --- src/MsixPackaging/README.md | 25 +++- src/MsixPackaging/Sdk/Sdk.props | 39 ++++++ src/MsixPackaging/Sdk/Sdk.targets | 129 +++++++++++++++--- src/MsixPackaging/Tasks/FindWindowsSdkTool.cs | 82 ----------- 5 files changed, 169 insertions(+), 126 deletions(-) delete mode 100644 src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs delete mode 100644 src/MsixPackaging/Tasks/FindWindowsSdkTool.cs diff --git a/src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs b/src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs deleted file mode 100644 index c0867718..00000000 --- a/src/MsixPackaging.UnitTests/FindWindowsSdkToolTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the MIT license. - -using Microsoft.Build.MsixPackaging.Tasks; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.MsixPackaging.UnitTests -{ - public class FindWindowsSdkToolTests - { - [Fact] - public void DetectHostArchitecture_ReturnsValidValue() - { - var arch = FindWindowsSdkTool.DetectHostArchitecture(); - arch.ShouldBeOneOf("x64", "x86", "arm64"); - } - } -} diff --git a/src/MsixPackaging/README.md b/src/MsixPackaging/README.md index 68695b51..4444d52f 100644 --- a/src/MsixPackaging/README.md +++ b/src/MsixPackaging/README.md @@ -123,7 +123,10 @@ Additional opt-in targets: | `MsixPriDefaultLanguage` | `en-US` | Default language for PRI config | | `MsixPackageVersion` | — | Patches `Identity/@Version` (four-part numeric) | | `MsixTargetArchitecture` | — | Patches `Identity/@ProcessorArchitecture` | -| `MsixToolArchitecture` | auto-detect | Host architecture for Windows SDK tools | +| `MsixHashAlgorithmId` | `SHA256` | Hash algorithm used when packing and signing | +| `MsixWindowsSdkVersion` | auto-detect | Windows SDK version (e.g. `10.0.26100.0`) used to locate MakeAppx/SignTool/MakePri. Empty = latest installed | +| `MsixSdkBuildToolsVersion` | pinned | Version of `Microsoft.Windows.SDK.BuildTools.MSIX` restored for the build tasks | +| `MsixToolArchitecture` | — | **Deprecated / no-op.** Tool architecture is resolved automatically by the build tools package | | `MsixDeployOnBuild` | `false` | Auto-register layout after build | | `MsixAutoDeployInVS` | `true` | Auto-enables deploy when building in VS | | `MsixDeployMode` | `layout` | `layout` (fast) or `msix` (full install) | @@ -168,4 +171,22 @@ The SDK includes a XAML Rule file that automatically adds an **MSIX Packaging** ## Build Requirements - .NET SDK (version matching your `TargetFramework`) -- Windows SDK (for `MakeAppx.exe`) — any version 10.0.17763.0+ +- Windows SDK (for `MakeAppx.exe`/`SignTool.exe`/`MakePri.exe`) — any version 10.0.17763.0+ + +## Build tooling + +The SDK delegates SDK-tool discovery, MSIX packing, and signing to the compiled +MSBuild tasks in the [`Microsoft.Windows.SDK.BuildTools.MSIX`](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.MSIX) +package. That package is **restored automatically** when you build (it is injected +as a package reference by the SDK) — you do not need to add it yourself, and it is +not bundled into this SDK. Only the package's task assembly is used; its full +WinAppSDK packaging pipeline is not imported. + +Pin a specific version with `MsixSdkBuildToolsVersion`, and pin the Windows SDK +version used to locate the tools with `MsixWindowsSdkVersion` (otherwise the latest +installed Windows SDK is used). + +Signing is performed by the package's SignTool task, which also supports +timestamping and Azure Code Signing / Azure Key Vault when the corresponding +properties are supplied. + diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props index 50d84a58..0d93ab08 100644 --- a/src/MsixPackaging/Sdk/Sdk.props +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -36,6 +36,21 @@ en-US + + + 1.7.260518100 + + + + + + SHA256 + false @@ -44,6 +59,30 @@ layout + + + + + + + + true diff --git a/src/MsixPackaging/Sdk/Sdk.targets b/src/MsixPackaging/Sdk/Sdk.targets index 014d4277..02e39cdd 100644 --- a/src/MsixPackaging/Sdk/Sdk.targets +++ b/src/MsixPackaging/Sdk/Sdk.targets @@ -10,7 +10,7 @@ + + <_MsixBuildToolsTfm Condition="'$(MSBuildRuntimeType)' == 'Core'">net6.0 + <_MsixBuildToolsTfm Condition="'$(_MsixBuildToolsTfm)' == ''">net472 + <_MsixBuildToolsAssembly Condition="'$(_MsixBuildToolsAssembly)' == ''">$(PkgMicrosoft_Windows_SDK_BuildTools_MSIX)\tools\$(_MsixBuildToolsTfm)\Microsoft.Windows.SDK.BuildTools.MSIX.dll + + + + + + + + + + + + + best) best = v; + } + } + } + } + LatestVersion = best == null ? string.Empty : best.ToString(); + ]]> + + + + + + <_MsixGetLatestWindowsSdkVersion Condition="'$(MsixWindowsSdkVersion)' == ''"> + + + + <_MsixResolvedSdkVersion Condition="'$(MsixWindowsSdkVersion)' != ''">$(MsixWindowsSdkVersion) + + + + + - + <_ReswFile Include="$(MsixLayoutDir)\**\*.resw" /> @@ -186,10 +251,10 @@ - - - - + + + + - - - - + + + + + + + <_MsixLayoutFile Include="$(MsixLayoutDir)\**\*" /> + <_MsixMapLine Include=""%(_MsixLayoutFile.FullPath)" "%(_MsixLayoutFile.RecursiveDir)%(_MsixLayoutFile.Filename)%(_MsixLayoutFile.Extension)"" /> + + + <_MsixMapFile>$(IntermediateOutputPath)$(MsixFileName).map.txt + + + + - + - - - + + + - - - + diff --git a/src/MsixPackaging/Tasks/FindWindowsSdkTool.cs b/src/MsixPackaging/Tasks/FindWindowsSdkTool.cs deleted file mode 100644 index 01400d5d..00000000 --- a/src/MsixPackaging/Tasks/FindWindowsSdkTool.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Microsoft.Build.MsixPackaging.Tasks -{ - /// - /// MSBuild task that locates a tool (MakeAppx.exe, SignTool.exe, MakePri.exe) - /// from the Windows 10 SDK installation. Searches for the latest installed SDK - /// version that contains the requested tool. - /// - public class FindWindowsSdkTool : Task - { - /// - /// Name of the tool to find (e.g., "makeappx.exe", "signtool.exe"). - /// - [Required] - public string ToolName { get; set; } = string.Empty; - - /// - /// Target architecture subdirectory to search (e.g., "x64", "x86", "arm64"). - /// Defaults to auto-detected host architecture. - /// - public string Architecture { get; set; } = string.Empty; - - /// - /// Full path to the discovered tool. Set as output if the tool is found. - /// - [Output] - public string ToolPath { get; set; } = string.Empty; - - public override bool Execute() - { - if (string.IsNullOrEmpty(Architecture)) - { - Architecture = DetectHostArchitecture(); - Log.LogMessage(MessageImportance.Low, "Auto-detected host architecture: {0}", Architecture); - } - - var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); - var sdkRoot = Path.Combine(programFilesX86, "Windows Kits", "10", "bin"); - - if (!Directory.Exists(sdkRoot)) - { - Log.LogError("Windows SDK not found at: {0}", sdkRoot); - return false; - } - - // Find the latest SDK version directory that contains the tool - ToolPath = Directory.GetDirectories(sdkRoot, "10.*") - .OrderByDescending(d => d) - .Select(d => Path.Combine(d, Architecture, ToolName)) - .FirstOrDefault(File.Exists) - ?? string.Empty; - - if (string.IsNullOrEmpty(ToolPath)) - { - Log.LogError("{0} not found in any Windows SDK version under: {1} (arch: {2})", - ToolName, sdkRoot, Architecture); - return false; - } - - Log.LogMessage(MessageImportance.High, "Found {0}: {1}", ToolName, ToolPath); - return true; - } - - internal static string DetectHostArchitecture() - { - var arch = RuntimeInformation.ProcessArchitecture; - switch (arch) - { - case System.Runtime.InteropServices.Architecture.X64: return "x64"; - case System.Runtime.InteropServices.Architecture.X86: return "x86"; - case System.Runtime.InteropServices.Architecture.Arm64: return "arm64"; - default: return "x64"; - } - } - } -} From 68dd193998ef14b1a926c5935feaac645d38d084 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Fri, 5 Jun 2026 12:28:40 -0700 Subject: [PATCH 07/18] Fix MsixPackaging sample layout and dev-time task path The sample packaging project was nested a level too deep, so its SDK imports, ProjectReference, and default Package.base.appxmanifest/Images paths did not resolve (the project was never built in the original change). Flatten it to samples/MsixPackaging/ to match the sibling manifest, images, and app project, and point the dev-time _MsixSdkTasksAssembly at the artifacts output. Verified end-to-end: the sample now publishes, merges fragments, validates, and packs a valid .msix via the new build-tools pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/MsixPackaging/Directory.Build.props | 2 +- .../{SamplePackaging => }/SamplePackaging.msbuildproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename samples/MsixPackaging/{SamplePackaging => }/SamplePackaging.msbuildproj (92%) diff --git a/samples/MsixPackaging/Directory.Build.props b/samples/MsixPackaging/Directory.Build.props index 443d686b..6a23f340 100644 --- a/samples/MsixPackaging/Directory.Build.props +++ b/samples/MsixPackaging/Directory.Build.props @@ -1,6 +1,6 @@ - <_MsixSdkTasksAssembly>$(MSBuildThisFileDirectory)..\..\src\MsixPackaging\bin\Release\netstandard2.0\Microsoft.Build.MsixPackaging.dll + <_MsixSdkTasksAssembly>$(MSBuildThisFileDirectory)..\..\artifacts\bin\Microsoft.Build.MsixPackaging\debug_netstandard2.0\Microsoft.Build.MsixPackaging.dll diff --git a/samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj b/samples/MsixPackaging/SamplePackaging.msbuildproj similarity index 92% rename from samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj rename to samples/MsixPackaging/SamplePackaging.msbuildproj index fa88f8d4..37bdec20 100644 --- a/samples/MsixPackaging/SamplePackaging/SamplePackaging.msbuildproj +++ b/samples/MsixPackaging/SamplePackaging.msbuildproj @@ -20,7 +20,7 @@ - From f2d5af157a181b8e449d0a5923c0ecdc45ff7636 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Tue, 9 Jun 2026 12:56:30 -0700 Subject: [PATCH 08/18] Add timestamp and Azure/Key Vault signing options Surface the timestamping, Azure Code Signing (Trusted Signing), and Azure Key Vault parameters that the WinAppSdkSignAppxPackage task already accepts: - MsixTimestampUrl / MsixTimestampDigestAlgorithm - MsixAzureCodeSigningEnabled + Dlib/Endpoint/AccountName/CertificateProfileName - MsixAzureKeyVaultEnabled + Dlib/Url/CertificateId SignMsix now runs when signing is enabled and either a certificate file is supplied or an Azure signing mode is enabled (Azure modes need no .pfx). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/Sdk/Sdk.props | 14 ++++++++++++++ src/MsixPackaging/Sdk/Sdk.targets | 23 ++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props index 0d93ab08..d20736e3 100644 --- a/src/MsixPackaging/Sdk/Sdk.props +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -28,6 +28,20 @@ false + + + SHA256 + + + false + + + + false + + $(MSBuildProjectDirectory)\Images diff --git a/src/MsixPackaging/Sdk/Sdk.targets b/src/MsixPackaging/Sdk/Sdk.targets index 02e39cdd..9d86090a 100644 --- a/src/MsixPackaging/Sdk/Sdk.targets +++ b/src/MsixPackaging/Sdk/Sdk.targets @@ -309,15 +309,21 @@ + + <_MsixSignReady Condition="'$(MsixSigningEnabled)' == 'true' AND ('$(MsixCertificatePath)' != '' OR '$(MsixAzureCodeSigningEnabled)' == 'true' OR '$(MsixAzureKeyVaultEnabled)' == 'true')">true + + Condition="'$(_MsixSignReady)' == 'true'"> - + + HashAlgorithmId="$(MsixHashAlgorithmId)" + SigningTimestampServerUrl="$(MsixTimestampUrl)" + SigningTimestampDigestAlgorithm="$(MsixTimestampDigestAlgorithm)" + AzureCodeSigningEnabled="$(MsixAzureCodeSigningEnabled)" + AzureCodeSigningDlibFilePath="$(MsixAzureCodeSigningDlibPath)" + AzureCodeSigningEndpoint="$(MsixAzureCodeSigningEndpoint)" + AzureCodeSigningAccountName="$(MsixAzureCodeSigningAccountName)" + AzureCodeSigningCertificateProfileName="$(MsixAzureCodeSigningCertificateProfileName)" + AzureKeyVaultEnabled="$(MsixAzureKeyVaultEnabled)" + AzureKeyVaultDlibFilePath="$(MsixAzureKeyVaultDlibPath)" + AzureKeyVaultUrl="$(MsixAzureKeyVaultUrl)" + AzureKeyVaultCertificateId="$(MsixAzureKeyVaultCertificateId)" /> From 5728a399155b3c329d1205768213b785ea6fc95b Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Tue, 9 Jun 2026 12:58:05 -0700 Subject: [PATCH 09/18] Validate publisher and certificate before signing Before signing with a certificate file, run WinAppSdkValidateSigningCertificate and WinAppSdkValidatePublisherName so a mismatch between the manifest Publisher and the signing certificate fails the build early with a clear message. Gated by MsixValidateSigningCertificate (default true) and skipped for the Azure signing modes, which do not use a local certificate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/Sdk/Sdk.props | 3 +++ src/MsixPackaging/Sdk/Sdk.targets | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props index d20736e3..22099698 100644 --- a/src/MsixPackaging/Sdk/Sdk.props +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -28,6 +28,9 @@ false + + true + SHA256 diff --git a/src/MsixPackaging/Sdk/Sdk.targets b/src/MsixPackaging/Sdk/Sdk.targets index 9d86090a..0c57a75d 100644 --- a/src/MsixPackaging/Sdk/Sdk.targets +++ b/src/MsixPackaging/Sdk/Sdk.targets @@ -38,6 +38,8 @@ + + + + + <_MsixMergedManifest Include="$(MsixLayoutDir)\AppxManifest.xml" /> + + + + Date: Tue, 9 Jun 2026 13:06:06 -0700 Subject: [PATCH 10/18] Add auto-generated test certificate for local signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When MsixSigningEnabled=true, MsixGenerateTestCertificate=true, and no certificate is supplied, generate a throwaway self-signed code-signing certificate whose subject matches the manifest Publisher, sign with it, and delete the temporary .pfx afterward. The certificate is created in memory via .NET CertificateRequest in a shipped PowerShell script (New-MsixTestCertificate.ps1) — nothing is written to the certificate store, so no store cleanup is required and it works under both Windows PowerShell and PowerShell 7. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Sdk/New-MsixTestCertificate.ps1 | 50 +++++++++++++ src/MsixPackaging/Sdk/Sdk.props | 4 ++ src/MsixPackaging/Sdk/Sdk.targets | 70 ++++++++++++++++--- 3 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/MsixPackaging/Sdk/New-MsixTestCertificate.ps1 diff --git a/src/MsixPackaging/Sdk/New-MsixTestCertificate.ps1 b/src/MsixPackaging/Sdk/New-MsixTestCertificate.ps1 new file mode 100644 index 00000000..c6244fb6 --- /dev/null +++ b/src/MsixPackaging/Sdk/New-MsixTestCertificate.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Creates a throwaway self-signed code-signing certificate for local MSIX signing. +.DESCRIPTION + Generates a self-signed certificate (in memory, via .NET CertificateRequest) whose + subject matches the package Publisher and exports it to a password-protected .pfx. + Nothing is written to the certificate store. Used by Microsoft.Build.MsixPackaging + when MsixGenerateTestCertificate=true. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Subject, + [Parameter(Mandatory = $true)][string]$OutputPath, + [Parameter(Mandatory = $true)][string]$Password +) + +$ErrorActionPreference = 'Stop' + +$rsa = [System.Security.Cryptography.RSA]::Create(2048) +try { + $request = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( + $Subject, + $rsa, + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) + + # Code signing EKU (1.3.6.1.5.5.7.3.3) + $eku = [System.Security.Cryptography.OidCollection]::new() + [void]$eku.Add([System.Security.Cryptography.Oid]::new('1.3.6.1.5.5.7.3.3')) + $request.CertificateExtensions.Add( + [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($eku, $true)) + $request.CertificateExtensions.Add( + [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new( + [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature, $false)) + $request.CertificateExtensions.Add( + [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($false, $false, 0, $false)) + + $now = [System.DateTimeOffset]::UtcNow + $cert = $request.CreateSelfSigned($now.AddDays(-1), $now.AddYears(1)) + try { + $pfxBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $Password) + [System.IO.File]::WriteAllBytes($OutputPath, $pfxBytes) + } + finally { + $cert.Dispose() + } +} +finally { + $rsa.Dispose() +} diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props index 22099698..a3c8cfbf 100644 --- a/src/MsixPackaging/Sdk/Sdk.props +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -31,6 +31,10 @@ true + + false + SHA256 diff --git a/src/MsixPackaging/Sdk/Sdk.targets b/src/MsixPackaging/Sdk/Sdk.targets index 0c57a75d..bbd3b109 100644 --- a/src/MsixPackaging/Sdk/Sdk.targets +++ b/src/MsixPackaging/Sdk/Sdk.targets @@ -316,38 +316,87 @@ file is not required. ============================================================ --> - <_MsixSignReady Condition="'$(MsixSigningEnabled)' == 'true' AND ('$(MsixCertificatePath)' != '' OR '$(MsixAzureCodeSigningEnabled)' == 'true' OR '$(MsixAzureKeyVaultEnabled)' == 'true')">true + <_MsixUseTestCert Condition="'$(MsixSigningEnabled)' == 'true' AND '$(MsixGenerateTestCertificate)' == 'true' AND '$(MsixCertificatePath)' == '' AND '$(MsixAzureCodeSigningEnabled)' != 'true' AND '$(MsixAzureKeyVaultEnabled)' != 'true'">true + <_MsixSignReady Condition="'$(MsixSigningEnabled)' == 'true' AND ('$(MsixCertificatePath)' != '' OR '$(_MsixUseTestCert)' == 'true' OR '$(MsixAzureCodeSigningEnabled)' == 'true' OR '$(MsixAzureKeyVaultEnabled)' == 'true')">true + + + + + + + + + + + + + <_MsixTestCertPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)$(MsixFileName).testcert.pfx')) + <_MsixTestCertPassword>$([System.Guid]::NewGuid().ToString()) + + + + + + + + + + + + + + <_MsixSignCertFile>$(MsixCertificatePath) + <_MsixSignCertPassword>$(MsixCertificatePassword) + <_MsixSignCertFile Condition="'$(_MsixUseTestCert)' == 'true'">$(_MsixTestCertPath) + <_MsixSignCertPassword Condition="'$(_MsixUseTestCert)' == 'true'">$(_MsixTestCertPassword) + + - + <_MsixMergedManifest Include="$(MsixLayoutDir)\AppxManifest.xml" /> + CertificateFile="$(_MsixSignCertFile)" + CertificatePassword="$(_MsixSignCertPassword)" /> + + + From 436d178a06a843a168f1b0a4a78a3f00907584d5 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Tue, 9 Jun 2026 13:08:03 -0700 Subject: [PATCH 11/18] Add symbol package (.msixsym) generation Opt-in GenerateMsixSymbolPackage target (MsixSymbolPackageEnabled) collects the PDBs from the layout and produces a .msixsym via WinAppSdkGenerateAppxSymbolPackage for Partner Center crash analysis. Output defaults to the .msix path with a .msixsym extension and can be overridden with MsixSymbolPackageOutput. Runs after PackMsix in the build pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/Sdk/Sdk.props | 4 ++++ src/MsixPackaging/Sdk/Sdk.targets | 32 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props index a3c8cfbf..e1572aa3 100644 --- a/src/MsixPackaging/Sdk/Sdk.props +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -57,6 +57,10 @@ en-US + + false + + $(_BuildMsixCoreDependsOn); PackMsix; + GenerateMsixSymbolPackage; SignMsix; DeployMsixInstall @@ -123,6 +125,7 @@ $(_BuildMsixCoreDependsOn); PackMsix; + GenerateMsixSymbolPackage; SignMsix @@ -308,6 +311,35 @@ + + + + $(MsixOutputDir)\$(MsixFileName).msixsym + + + + <_MsixPdbPayload Include="$(MsixLayoutDir)\**\*.pdb" /> + + + + + + + + + + + + + + false + + + + 0 + + + + + + + + <_AppInstallerNs><Namespace Prefix='appx' Uri='http://schemas.microsoft.com/appx/manifest/foundation/windows10' /> + $(MsixOutputDir)\$(MsixFileName).appinstaller + <_AppInstallerIsBundle Condition="'$(MsixBundleEnabled)' == 'true'">true + <_AppInstallerPkgFile Condition="'$(_AppInstallerIsBundle)' == 'true'">$(MsixFileName).msixbundle + <_AppInstallerPkgFile Condition="'$(_AppInstallerPkgFile)' == ''">$(MsixFileName).msix + + + + + + + + + + + + + + + + + + + <_AppInstallerPkgUri>$(MsixAppInstallerPackageUri) + <_AppInstallerPkgUri Condition="'$(_AppInstallerPkgUri)' == ''">$(MsixAppInstallerUri.Substring(0, $(MsixAppInstallerUri.LastIndexOf('/'))))/$(_AppInstallerPkgFile) + <_AppInstallerMainElement Condition="'$(_AppInstallerIsBundle)' == 'true'"><MainBundle Name="$(_AppInstallerName)" Publisher="$(_AppInstallerPublisher)" Version="$(_AppInstallerVersion)" Uri="$(_AppInstallerPkgUri)" /> + <_AppInstallerMainElement Condition="'$(_AppInstallerMainElement)' == ''"><MainPackage Name="$(_AppInstallerName)" Publisher="$(_AppInstallerPublisher)" Version="$(_AppInstallerVersion)" ProcessorArchitecture="$(_AppInstallerArch)" Uri="$(_AppInstallerPkgUri)" /> + + + + + + + <_AppInstallerLines Include="<AppInstaller xmlns="http://schemas.microsoft.com/appx/appinstaller/2018" Version="$(_AppInstallerVersion)" Uri="$(MsixAppInstallerUri)">" /> + <_AppInstallerLines Include=" $(_AppInstallerMainElement)" /> + <_AppInstallerLines Include=" <UpdateSettings>" /> + <_AppInstallerLines Include=" <OnLaunch HoursBetweenUpdateChecks="$(MsixAppInstallerUpdateCheckHours)" />" /> + <_AppInstallerLines Include=" </UpdateSettings>" /> + <_AppInstallerLines Include="</AppInstaller>" /> + + + + + + + + + false + + x64 + + + $(MsixOutputDir)\$(MsixFileName).msixbundle + <_MsixPrimaryPackage Condition="'$(MsixBundleEnabled)' == 'true'">$(MsixBundleOutput) + <_MsixPrimaryPackage Condition="'$(_MsixPrimaryPackage)' == ''">$(MsixOutputDir)\$(MsixFileName).msix + + + + BundleMsix; + GenerateMsixSymbolPackage; + SignMsix; + GenerateMsixAppInstaller + + - + $(_BuildMsixCoreDependsOn); DeployMsixLayout - + $(_BuildMsixCoreDependsOn); PackMsix; GenerateMsixSymbolPackage; @@ -123,7 +136,7 @@ - + $(_BuildMsixCoreDependsOn); PackMsix; GenerateMsixSymbolPackage; @@ -146,6 +159,8 @@ @@ -313,6 +329,84 @@ + + + + <_MsixBundleRoot>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(IntermediateOutputPath), 'bundle')) + <_MsixBundlePackagesDir>$(_MsixBundleRoot)\packages + + <_MsixFirstArch>$(MsixBundlePlatforms) + <_MsixFirstArch Condition="$(MsixBundlePlatforms.Contains('|'))">$(MsixBundlePlatforms.Substring(0, $(MsixBundlePlatforms.IndexOf('|')))) + + + + <_MsixBundleArch Include="$([MSBuild]::Unescape($(MsixBundlePlatforms.Replace('|', ';'))))" /> + + + + + + + + + + + + + + + + + + + + + + + + <_MsixRepresentativeLayout Include="$(_MsixBundleRoot)\$(_MsixFirstArch)\MsixLayout\**\*" /> + + + + + + + + + + + @@ -486,7 +580,7 @@ AppxManifest="@(_MsixMergedManifest)" /> Date: Tue, 9 Jun 2026 13:27:51 -0700 Subject: [PATCH 14/18] Add Store upload package (.msixupload) creation Opt-in MsixStoreUploadEnabled wraps the .msixbundle and, when present, the .msixsym symbol package into a .msixupload container for Partner Center via WinAppSdkCreateAppStoreContainer. Requires MsixBundleEnabled (a .msixupload wraps a bundle); errors clearly otherwise. Runs at the end of the bundle pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/Sdk/Sdk.props | 4 ++++ src/MsixPackaging/Sdk/Sdk.targets | 38 +++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/MsixPackaging/Sdk/Sdk.props b/src/MsixPackaging/Sdk/Sdk.props index 4d462076..51eeb04c 100644 --- a/src/MsixPackaging/Sdk/Sdk.props +++ b/src/MsixPackaging/Sdk/Sdk.props @@ -75,6 +75,10 @@ x64 + + false + + $(MsixBundleOutput) <_MsixPrimaryPackage Condition="'$(_MsixPrimaryPackage)' == ''">$(MsixOutputDir)\$(MsixFileName).msix - + BundleMsix; GenerateMsixSymbolPackage; SignMsix; - GenerateMsixAppInstaller + GenerateMsixAppInstaller; + CreateMsixUpload @@ -399,6 +401,38 @@ + + + + + + + $(MsixOutputDir)\$(MsixFileName).msixupload + + + + <_MsixUploadItem Include="$(MsixBundleOutput)" /> + <_MsixUploadItem Include="$(MsixSymbolPackageOutput)" + Condition="'$(MsixSymbolPackageEnabled)' == 'true' AND '$(MsixSymbolPackageOutput)' != '' AND Exists('$(MsixSymbolPackageOutput)')" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 45eacc132dd1b554d816477d2c636c9fb151aa60 Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Tue, 9 Jun 2026 13:30:43 -0700 Subject: [PATCH 16/18] Demonstrate new MSIX features in the sample The sample build now exercises test-certificate signing, certificate validation, the symbol package, and the App Installer end-to-end. SampleConsoleApp declares so the multi-architecture bundle and Store-upload paths can be enabled via documented opt-in properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SampleConsoleApp/SampleConsoleApp.csproj | 2 ++ .../MsixPackaging/SamplePackaging.msbuildproj | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj b/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj index b2acff83..0458b458 100644 --- a/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj +++ b/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj @@ -6,6 +6,8 @@ enable enable SampleConsoleApp + + win-x64;win-x86;win-arm64 diff --git a/samples/MsixPackaging/SamplePackaging.msbuildproj b/samples/MsixPackaging/SamplePackaging.msbuildproj index 37bdec20..79693723 100644 --- a/samples/MsixPackaging/SamplePackaging.msbuildproj +++ b/samples/MsixPackaging/SamplePackaging.msbuildproj @@ -16,6 +16,27 @@ net10.0 MsixPackagingSample + + + true + true + + + true + true + https://example.com/apps/MsixPackagingSample.appinstaller + + From a4e43ff67d3f461fe709bc85c6d5a6d7ce57303a Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Tue, 9 Jun 2026 13:32:11 -0700 Subject: [PATCH 17/18] Document signing, bundle, and distribution features in the README Add property tables for signing (test certificate, validation, timestamp, Azure) and distribution/bundling (symbol package, App Installer, bundle, Store upload), plus a Signing section and a Multi-architecture bundles & distribution section, and update the VS property page category listing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/README.md | 81 +++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/MsixPackaging/README.md b/src/MsixPackaging/README.md index 4444d52f..033ca10a 100644 --- a/src/MsixPackaging/README.md +++ b/src/MsixPackaging/README.md @@ -100,6 +100,10 @@ Additional opt-in targets: | Target | Description | |--------|-------------| +| `BundleMsix` | Builds each architecture and combines them into a `.msixbundle` (bundle mode) | +| `GenerateMsixSymbolPackage` | Produces a `.msixsym` symbol package from the layout PDBs | +| `GenerateMsixAppInstaller` | Writes an `.appinstaller` file for sideload auto-update | +| `CreateMsixUpload` | Wraps the bundle (and symbol) into a `.msixupload` for Partner Center | | `CleanMsixLayout` | Removes layout directory and `.msix` on `dotnet clean` | | `InstallMsix` | Installs the built `.msix` via `Add-AppxPackage` | | `RegisterMsixLayout` | Registers the layout directory for dev-loop testing without packing | @@ -115,9 +119,6 @@ Additional opt-in targets: | `BaseAppxManifest` | `Package.base.appxmanifest` | Path to the base manifest template | | `AppxFragmentFileName` | `AppxFragment.xml` | Name of per-project fragment files | | `MsixPackageImagesDir` | `$(ProjectDir)\Images` | Package-level images directory | -| `MsixSigningEnabled` | `false` | Enable MSIX signing | -| `MsixCertificatePath` | — | Path to `.pfx` certificate | -| `MsixCertificatePassword` | — | Certificate password | | `MsixResourceIndexEnabled` | `auto` | Resource indexing: `true`, `false`, `auto` | | `MsixPriConfigPath` | — | Custom MakePri config file | | `MsixPriDefaultLanguage` | `en-US` | Default language for PRI config | @@ -131,6 +132,36 @@ Additional opt-in targets: | `MsixAutoDeployInVS` | `true` | Auto-enables deploy when building in VS | | `MsixDeployMode` | `layout` | `layout` (fast) or `msix` (full install) | +### Signing + +| Property | Default | Description | +|----------|---------|-------------| +| `MsixSigningEnabled` | `false` | Enable MSIX signing | +| `MsixCertificatePath` | — | Path to `.pfx` certificate | +| `MsixCertificatePassword` | — | Certificate password | +| `MsixGenerateTestCertificate` | `false` | When signing with no certificate, generate a throwaway self-signed test certificate matching the manifest Publisher | +| `MsixValidateSigningCertificate` | `true` | Validate the manifest Publisher matches the signing certificate before signing | +| `MsixTimestampUrl` | — | RFC 3161 timestamp server URL | +| `MsixTimestampDigestAlgorithm` | `SHA256` | Timestamp digest algorithm | +| `MsixAzureCodeSigningEnabled` | `false` | Sign via Azure Code Signing (Trusted Signing). Also set `MsixAzureCodeSigningDlibPath`, `…Endpoint`, `…AccountName`, `…CertificateProfileName` | +| `MsixAzureKeyVaultEnabled` | `false` | Sign via Azure Key Vault. Also set `MsixAzureKeyVaultDlibPath`, `…Url`, `…CertificateId` | + +### Distribution & bundling + +| Property | Default | Description | +|----------|---------|-------------| +| `MsixSymbolPackageEnabled` | `false` | Produce a `.msixsym` symbol package from the layout PDBs | +| `MsixSymbolPackageOutput` | `.msixsym` | Symbol package output path | +| `MsixAppInstallerEnabled` | `false` | Generate an `.appinstaller` file | +| `MsixAppInstallerUri` | — | URL where the `.appinstaller` is hosted (required when enabled) | +| `MsixAppInstallerPackageUri` | derived | URL of the hosted `.msix`/`.msixbundle`; derived from `MsixAppInstallerUri` when empty | +| `MsixAppInstallerUpdateCheckHours` | `0` | Hours between update checks on launch (`0` = every launch) | +| `MsixBundleEnabled` | `false` | Build each architecture and combine into a `.msixbundle` | +| `MsixBundlePlatforms` | `x64` | Pipe-separated architectures, e.g. `x64\|x86\|arm64` | +| `MsixBundleOutput` | `.msixbundle` | Bundle output path | +| `MsixStoreUploadEnabled` | `false` | Wrap the bundle (and symbol) into a `.msixupload` (requires a bundle) | +| `MsixStoreUploadOutput` | `.msixupload` | Store upload package output path | + ## Items | Item | Metadata | Description | @@ -158,6 +189,46 @@ Supported insertion markers: - `` — in `` (optional) - `` — in `` (optional) +## Signing + +Signing is opt-in (`MsixSigningEnabled=true`) and uses the Windows SDK SignTool task. Provide a certificate file, generate a test certificate for local development, or sign in the cloud: + +```xml + +true +my.pfx + +http://timestamp.digicert.com + + +true +true +``` + +Azure Code Signing (Trusted Signing) and Azure Key Vault are supported via the `MsixAzureCodeSigning*` / `MsixAzureKeyVault*` properties. The manifest `Publisher` is validated against the certificate before signing (`MsixValidateSigningCertificate`). + +## Multi-architecture bundles & distribution + +Build one package per architecture and combine them into a `.msixbundle`: + +```powershell +dotnet build MyPackage.msbuildproj ` + /p:MsixBundleEnabled=true ` + "/p:MsixBundlePlatforms=x64|x86|arm64" +``` + +Each referenced app must declare the target architectures so restore covers them: + +```xml +win-x64;win-x86;win-arm64 +``` + +Optional distribution outputs (each opt-in): + +- **Symbol package** — `MsixSymbolPackageEnabled=true` produces a `.msixsym` (layout PDBs) for Partner Center crash analysis. +- **App Installer** — `MsixAppInstallerEnabled=true` with `MsixAppInstallerUri` writes an `.appinstaller` for sideload auto-update (references the bundle when bundling, else the `.msix`). +- **Store upload** — `MsixStoreUploadEnabled=true` wraps the bundle (and symbol) into a `.msixupload` for Partner Center (requires a bundle). + ## VS Property Page The SDK includes a XAML Rule file that automatically adds an **MSIX Packaging** page to the VS Project Properties UI. @@ -165,7 +236,9 @@ The SDK includes a XAML Rule file that automatically adds an **MSIX Packaging** **Categories:** - **Package Identity** — `MsixFileName`, `MsixPackageVersion`, `MsixTargetArchitecture` - **Deployment** — `MsixDeployOnBuild`, `MsixAutoDeployInVS`, `MsixDeployMode` -- **Signing** — `MsixSigningEnabled`, `MsixCertificatePath` +- **Signing** — `MsixSigningEnabled`, `MsixCertificatePath`, `MsixGenerateTestCertificate`, `MsixValidateSigningCertificate`, `MsixTimestampUrl` +- **Bundle** — `MsixBundleEnabled`, `MsixBundlePlatforms`, `MsixStoreUploadEnabled` +- **Distribution** — `MsixSymbolPackageEnabled`, `MsixAppInstallerEnabled`, `MsixAppInstallerUri` - **Resources** — `MsixResourceIndexEnabled`, `MsixPriDefaultLanguage` ## Build Requirements From 83622a69181fe270e952408c6a8d6715c59e188d Mon Sep 17 00:00:00 2001 From: Shmueli Englard Date: Tue, 9 Jun 2026 13:33:40 -0700 Subject: [PATCH 18/18] Ship the test-certificate script in the SDK package The Sdk\New-MsixTestCertificate.ps1 helper triggers NuGet's legacy PowerShell-install-script validation (NU5110/NU5111), which the repo promotes to errors. The script is invoked by the SDK targets, not by NuGet install, so suppress those warnings for this package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj b/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj index 8aa928f9..8d616546 100644 --- a/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj +++ b/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj @@ -6,6 +6,9 @@ MSBuild SDK for packaging multiple .NET projects into a single sideloadable MSIX using per-project AppxFragment manifest merging. MSBuild MSBuildSdk MSIX Packaging AppxManifest true + + $(NoWarn);NU5110;NU5111