diff --git a/MSBuildSdks.sln b/MSBuildSdks.sln
index 11c4f73..31e6e05 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}
diff --git a/samples/MsixPackaging/Directory.Build.props b/samples/MsixPackaging/Directory.Build.props
new file mode 100644
index 0000000..6a23f34
--- /dev/null
+++ b/samples/MsixPackaging/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+
+ <_MsixSdkTasksAssembly>$(MSBuildThisFileDirectory)..\..\artifacts\bin\Microsoft.Build.MsixPackaging\debug_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 0000000..554e8a3
Binary files /dev/null and b/samples/MsixPackaging/Images/StoreLogo.png differ
diff --git a/samples/MsixPackaging/Package.base.appxmanifest b/samples/MsixPackaging/Package.base.appxmanifest
new file mode 100644
index 0000000..08932f3
--- /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 0000000..40b4b30
--- /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 0000000..554e8a3
Binary files /dev/null and b/samples/MsixPackaging/SampleConsoleApp/MsixImages/SampleConsoleApp.Square44x44Logo.png differ
diff --git a/samples/MsixPackaging/SampleConsoleApp/Program.cs b/samples/MsixPackaging/SampleConsoleApp/Program.cs
new file mode 100644
index 0000000..aeeadf5
--- /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 0000000..0458b45
--- /dev/null
+++ b/samples/MsixPackaging/SampleConsoleApp/SampleConsoleApp.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ SampleConsoleApp
+
+ win-x64;win-x86;win-arm64
+
+
+
diff --git a/samples/MsixPackaging/SamplePackaging.msbuildproj b/samples/MsixPackaging/SamplePackaging.msbuildproj
new file mode 100644
index 0000000..7969372
--- /dev/null
+++ b/samples/MsixPackaging/SamplePackaging.msbuildproj
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ net10.0
+ MsixPackagingSample
+
+
+ true
+ true
+
+
+ true
+ true
+ https://example.com/apps/MsixPackagingSample.appinstaller
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MsixPackaging.UnitTests/MergeAppxFragmentsTests.cs b/src/MsixPackaging.UnitTests/MergeAppxFragmentsTests.cs
new file mode 100644
index 0000000..8029005
--- /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 0000000..282cb0c
--- /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 0000000..8d2948b
--- /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 0000000..b7e2605
--- /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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj b/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj
new file mode 100644
index 0000000..8d61654
--- /dev/null
+++ b/src/MsixPackaging/Microsoft.Build.MsixPackaging.csproj
@@ -0,0 +1,43 @@
+
+
+ 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
+
+ $(NoWarn);NU5110;NU5111
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MsixPackaging/README.md b/src/MsixPackaging/README.md
new file mode 100644
index 0000000..033ca10
--- /dev/null
+++ b/src/MsixPackaging/README.md
@@ -0,0 +1,265 @@
+# 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 |
+|--------|-------------|
+| `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 |
+| `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 |
+| `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` |
+| `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) |
+
+### 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 |
+|------|----------|-------------|
+| `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)
+
+## 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.
+
+**Categories:**
+- **Package Identity** — `MsixFileName`, `MsixPackageVersion`, `MsixTargetArchitecture`
+- **Deployment** — `MsixDeployOnBuild`, `MsixAutoDeployInVS`, `MsixDeployMode`
+- **Signing** — `MsixSigningEnabled`, `MsixCertificatePath`, `MsixGenerateTestCertificate`, `MsixValidateSigningCertificate`, `MsixTimestampUrl`
+- **Bundle** — `MsixBundleEnabled`, `MsixBundlePlatforms`, `MsixStoreUploadEnabled`
+- **Distribution** — `MsixSymbolPackageEnabled`, `MsixAppInstallerEnabled`, `MsixAppInstallerUri`
+- **Resources** — `MsixResourceIndexEnabled`, `MsixPriDefaultLanguage`
+
+## Build Requirements
+
+- .NET SDK (version matching your `TargetFramework`)
+- 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/Microsoft.Build.MsixPackaging.PropertyPage.xaml b/src/MsixPackaging/Sdk/Microsoft.Build.MsixPackaging.PropertyPage.xaml
new file mode 100644
index 0000000..d0e78e1
--- /dev/null
+++ b/src/MsixPackaging/Sdk/Microsoft.Build.MsixPackaging.PropertyPage.xaml
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MsixPackaging/Sdk/New-MsixTestCertificate.ps1 b/src/MsixPackaging/Sdk/New-MsixTestCertificate.ps1
new file mode 100644
index 0000000..c6244fb
--- /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
new file mode 100644
index 0000000..51eeb04
--- /dev/null
+++ b/src/MsixPackaging/Sdk/Sdk.props
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+ $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(IntermediateOutputPath), 'MsixLayout'))
+
+
+ $([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(OutputPath)))
+
+
+ $(MSBuildProjectName)
+
+
+ $(MSBuildProjectDirectory)\Package.base.appxmanifest
+
+
+ AppxFragment.xml
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+ SHA256
+
+
+ false
+
+
+
+ false
+
+
+
+ $(MSBuildProjectDirectory)\Images
+
+
+ auto
+
+ en-US
+
+
+ false
+
+
+
+ false
+
+
+
+ 0
+
+
+
+ false
+
+ x64
+
+
+
+ false
+
+
+
+
+ 1.7.260518100
+
+
+
+
+
+ SHA256
+
+
+ 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 0000000..dd5cac0
--- /dev/null
+++ b/src/MsixPackaging/Sdk/Sdk.targets
@@ -0,0 +1,710 @@
+
+
+
+
+
+
+
+
+
+
+ <_MsixSdkTasksAssembly Condition="'$(_MsixSdkTasksAssembly)' == ''">$(MSBuildThisFileDirectory)..\build\netstandard2.0\Microsoft.Build.MsixPackaging.dll
+
+
+
+
+
+
+
+ <_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)
+
+
+
+
+
+
+
+
+ <_BuildMsixCoreDependsOn>
+ PublishToLayout;
+ DiscoverAppxFragments;
+ MergeAppxManifest;
+ ValidateAppxManifest;
+ CopyMsixAssets;
+ GenerateResourceIndex
+
+
+
+ $(MsixOutputDir)\$(MsixFileName).msixbundle
+ <_MsixPrimaryPackage Condition="'$(MsixBundleEnabled)' == 'true'">$(MsixBundleOutput)
+ <_MsixPrimaryPackage Condition="'$(_MsixPrimaryPackage)' == ''">$(MsixOutputDir)\$(MsixFileName).msix
+
+
+
+ BundleMsix;
+ GenerateMsixSymbolPackage;
+ SignMsix;
+ GenerateMsixAppInstaller;
+ CreateMsixUpload
+
+
+
+
+ $(_BuildMsixCoreDependsOn);
+ DeployMsixLayout
+
+
+
+
+ $(_BuildMsixCoreDependsOn);
+ PackMsix;
+ GenerateMsixSymbolPackage;
+ SignMsix;
+ GenerateMsixAppInstaller;
+ DeployMsixInstall
+
+
+
+
+ $(_BuildMsixCoreDependsOn);
+ PackMsix;
+ GenerateMsixSymbolPackage;
+ SignMsix;
+ GenerateMsixAppInstaller
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_MsixLayoutFile Include="$(MsixLayoutDir)\**\*" />
+ <_MsixMapLine Include=""%(_MsixLayoutFile.FullPath)" "%(_MsixLayoutFile.RecursiveDir)%(_MsixLayoutFile.Filename)%(_MsixLayoutFile.Extension)"" />
+
+
+ <_MsixMapFile>$(IntermediateOutputPath)$(MsixFileName).map.txt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_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\**\*" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MsixOutputDir)\$(MsixFileName).msixupload
+
+
+
+ <_MsixUploadItem Include="$(MsixBundleOutput)" />
+ <_MsixUploadItem Include="$(MsixSymbolPackageOutput)"
+ Condition="'$(MsixSymbolPackageEnabled)' == 'true' AND '$(MsixSymbolPackageOutput)' != '' AND Exists('$(MsixSymbolPackageOutput)')" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MsixOutputDir)\$(MsixFileName).msixsym
+
+
+
+ <_MsixPdbPayload Include="$(MsixLayoutDir)\**\*.pdb" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_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>" />
+
+
+
+
+
+
+
+
+
+ <_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" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MsixPackaging/Tasks/MergeAppxFragments.cs b/src/MsixPackaging/Tasks/MergeAppxFragments.cs
new file mode 100644
index 0000000..712ca19
--- /dev/null
+++ b/src/MsixPackaging/Tasks/MergeAppxFragments.cs
@@ -0,0 +1,331 @@
+// 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;
+
+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 = "";
+
+ ///
+ /// Gets or sets the path to the base AppxManifest template containing the fragment marker(s).
+ ///
+ [Required]
+ public string BaseManifestPath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the paths to AppxFragment.xml files to merge into the manifest.
+ ///
+ public ITaskItem[] FragmentPaths { get; set; }
+
+ ///
+ /// Gets or sets the path where the merged manifest will be written.
+ ///
+ [Required]
+ public string OutputPath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the primary marker comment to replace in the base manifest (for Application entries).
+ ///
+ public string Marker { get; set; } = ApplicationsMarker;
+
+ ///
+ /// Gets or sets the version stamped into the Identity/@Version attribute. Must be four-part numeric.
+ ///
+ public string PackageVersion { get; set; }
+
+ ///
+ /// Gets or sets the architecture stamped into the Identity/@ProcessorArchitecture attribute.
+ ///
+ 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.
+ ///
+ /// 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.
+ ///
+ /// 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
+ {
+ // 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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/MsixPackaging/Tasks/ValidateAppxManifest.cs b/src/MsixPackaging/Tasks/ValidateAppxManifest.cs
new file mode 100644
index 0000000..9e6a369
--- /dev/null
+++ b/src/MsixPackaging/Tasks/ValidateAppxManifest.cs
@@ -0,0 +1,222 @@
+// 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.Xml;
+
+namespace Microsoft.Build.MsixPackaging.Tasks
+{
+ ///
+ /// 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";
+
+ ///
+ /// Gets or sets the path to the AppxManifest.xml to validate.
+ ///
+ [Required]
+ public string ManifestPath { get; set; } = string.Empty;
+
+ ///
+ /// 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))
+ {
+ 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;
+ }
+
+ ///
+ /// 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)
+ {
+ var parts = version.Split('.');
+ if (parts.Length != 4)
+ {
+ return false;
+ }
+
+ foreach (var part in parts)
+ {
+ if (!ushort.TryParse(part, out _))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// 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)
+ {
+ if (string.IsNullOrEmpty(node.Attributes?[attributeName]?.Value))
+ {
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.props b/src/MsixPackaging/build/Microsoft.Build.MsixPackaging.props
new file mode 100644
index 0000000..f8c44e7
--- /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 0000000..dde0d4c
--- /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 0000000..5157e3f
--- /dev/null
+++ b/src/MsixPackaging/version.json
@@ -0,0 +1,4 @@
+{
+ "inherit": true,
+ "version": "1.0"
+}