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" +}