diff --git a/docs/building-apps/build-items.md b/docs/building-apps/build-items.md index 2fd81202cf18..84329b453a04 100644 --- a/docs/building-apps/build-items.md +++ b/docs/building-apps/build-items.md @@ -249,6 +249,35 @@ An item group that contains environment variables that will be set when the app > [!NOTE] > This only applies when launching the app from the command line (`dotnet run` or `dotnet build -t:Run`), not when launching from the IDE. +## ApplicationArtifact + +An item group that contains final application artifacts produced by Apple platform builds and publishes. The item identity is the absolute path to the artifact. This can include: + +* `.app` app bundles for iOS, tvOS, macOS, and Mac Catalyst apps. +* `.ipa` packages when [BuildIpa](build-properties.md#buildipa) is enabled. +* `.pkg` installer packages when [CreatePackage](build-properties.md#createpackage) is enabled. +* `.xcarchive` directories when [ArchiveOnBuild](build-properties.md#archiveonbuild) is enabled. + +The following metadata is set: + +* `PackageFormat`: The artifact format. Possible values are `app`, `ipa`, `pkg`, and `xcarchive`. +* `IsDirectory`: `true` for `.app` and `.xcarchive` outputs; `false` for `.ipa` and `.pkg` outputs. +* `PlatformName`: The Apple platform name, such as `iOS`, `tvOS`, `macOS`, or `MacCatalyst`. +* `BundleIdentifier`: The resolved app bundle identifier. + +Example: + +```xml + + + +``` + +See also the [GetApplicationArtifacts](build-targets.md#getapplicationartifacts) target. + ## NativeReference An item group that contains any native references that should be linked into diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 634396282b90..3f61ec1902fa 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -109,6 +109,8 @@ Only applicable to iOS projects (since only iOS projects can be built remotely f If an Xcode archive should be created at the end of the build. +Created archives are exposed in the [ApplicationArtifact](build-items.md#applicationartifact) item group. + ## BGenEmitDebugInformation Whether the `bgen` tool (the binding generator) should emit debug information or not. @@ -139,6 +141,8 @@ Only applicable to iOS and tvOS projects. See [CreatePackage](#createpackage) for macOS and Mac Catalyst projects. +Created IPA packages are exposed in the [ApplicationArtifact](build-items.md#applicationartifact) item group. + ## BundleCreateDump CoreCLR has a command-line utility called [`createdump`][createdump] to create @@ -350,6 +354,8 @@ Only applicable to macOS and Mac Catalyst projects. See [BuildIpa](#buildipa) for iOS and tvOS projects. +Created PKG packages are exposed in the [ApplicationArtifact](build-items.md#applicationartifact) item group. + ## Device Specifies which mobile device or simulator to target when using `dotnet run --device ` or MSBuild targets that interact with devices (such as `Run`, `Install`, or `Uninstall`). @@ -535,6 +541,36 @@ Default: true Where the generated source from the generator are saved. +## GetApplicationArtifactsDependsOn + +A semi-colon delimited property that can be used to extend the +[GetApplicationArtifacts](build-targets.md#getapplicationartifacts) and +`Publish` targets. `Build` is a mandatory dependency of +`GetApplicationArtifacts`; MSBuild targets added to this property execute after +the platform build has collected `@(ApplicationArtifact)` items and before +`GetApplicationArtifacts` or `Publish` returns them. + +This can be used by SDKs such as .NET MAUI to add shared application metadata +to platform-produced artifacts. Extension targets should update existing +`@(ApplicationArtifact)` items to add metadata; they should only add new items +when introducing additional artifacts. + +Example: + +```xml + + $(GetApplicationArtifactsDependsOn);AddApplicationArtifactMetadata + + + + + + $(ApplicationTitle) + + + +``` + ## IBToolPath The full path to the `ibtool` tool. @@ -669,6 +705,8 @@ Specifies the path to the resulting .ipa file when creating an IPA package (see Only applicable to iOS and tvOS projects. +The resulting IPA is exposed in the [ApplicationArtifact](build-items.md#applicationartifact) item group. + ## IsAppExtension If a project is an app extension. @@ -1139,6 +1177,8 @@ Specifies the path to the resulting .pkg file when creating a package (see [Crea Only applicable to macOS and Mac Catalyst apps. +The resulting PKG is exposed in the [ApplicationArtifact](build-items.md#applicationartifact) item group. + ## PlutilPath The full path to the `plutil` command-line tool. diff --git a/docs/building-apps/build-targets.md b/docs/building-apps/build-targets.md index fbf0abff811e..82ebe9677f5d 100644 --- a/docs/building-apps/build-targets.md +++ b/docs/building-apps/build-targets.md @@ -44,6 +44,36 @@ $ dotnet run --device UDID Added in .NET 11. +## GetApplicationArtifacts + +Builds the project and returns the `@(ApplicationArtifact)` item group. This +target always runs `Build` first so platform `.app`, `.ipa`, `.pkg`, and +`.xcarchive` artifacts are produced and collected before any custom metadata +extension targets run. The `Publish` target returns the same item group for +artifacts it creates. + +The returned item group reflects the artifacts the current invocation is +configured to build; it does not scan for `.ipa`, `.pkg`, or `.xcarchive` files +left on disk by a previous build. The `.app` bundle is always collected for an +application project (excluding app extension and watch app projects), while the +`.ipa`, `.pkg`, and `.xcarchive` outputs are only collected when +[BuildIpa](build-properties.md#buildipa), +[CreatePackage](build-properties.md#createpackage), or +[ArchiveOnBuild](build-properties.md#archiveonbuild) (respectively) is enabled in +the same invocation. The `Publish` target enables `BuildIpa` (mobile) or +`CreatePackage` (desktop) by default. + +```shell +$ dotnet build MyApp.csproj -t:GetApplicationArtifacts -getItem:ApplicationArtifact +$ dotnet build MyApp.csproj -t:Publish -getItem:ApplicationArtifact +``` + +See [ApplicationArtifact](build-items.md#applicationartifact) for supported metadata. + +Targets that need to add or update `@(ApplicationArtifact)` metadata before +`GetApplicationArtifacts` or `Publish` returns can append to +[GetApplicationArtifactsDependsOn](build-properties.md#getapplicationartifactsdependson). + ## Run Builds the source code within a project and all dependencies, and then deploys and runs it diff --git a/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets b/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets index c053793baf15..8f6dca62905e 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets @@ -20,7 +20,7 @@ Condition="$(RuntimeIdentifiers.Contains('iossimulator-')) Or $(RuntimeIdentifiers.Contains('tvossimulator-'))" /> - + diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 38577d9196a7..5accac63eb06 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2370,6 +2370,24 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + + + + app + true + $(_PlatformName) + $(_BundleIdentifier) + + + + - + + - + @@ -3257,6 +3280,28 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + + + <_ApplicationArtifactArchivePath>$(ArchiveDir) + <_ApplicationArtifactArchivePath Condition="'$(ArchivePath)' != '' And !Exists('$(_ApplicationArtifactArchivePath)')">$(ArchivePath) + + + + xcarchive + true + $(_PlatformName) + $(_BundleIdentifier) + + + + <_AppContainerDir Condition="'$(IsAppDistribution)' != 'true'">$(DeviceSpecificOutputPath) @@ -3360,6 +3405,19 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + + + pkg + false + $(_PlatformName) + $(_BundleIdentifier) + + + + + $(GetApplicationArtifactsDependsOn);AddMauiApplicationArtifactMetadata + + + + + + + + + + + <_MauiObservedAppArtifact Include="@(ApplicationArtifact)" Condition="'%(ApplicationArtifact.PackageFormat)' == 'app'" /> + <_MauiObservedPackageArtifact Include="@(ApplicationArtifact)" Condition="'%(ApplicationArtifact.PackageFormat)' == '$(ExpectedAugmentedPackageFormat)'" /> + + My MAUI App + %(ApplicationArtifact.PackageFormat) + + + + + + diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.mk b/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.mk new file mode 100644 index 000000000000..0e8169558074 --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.mk @@ -0,0 +1,3 @@ +TOP=../../../.. +TESTNAME=MySimpleAppWithArtifactMetadata +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/Makefile b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/MySimpleAppWithArtifactMetadata.csproj b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/MySimpleAppWithArtifactMetadata.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/MySimpleAppWithArtifactMetadata.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 3f8d8b76e8a6..cf2a01541974 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; using Mono.Cecil; @@ -45,6 +47,7 @@ public void ArchiveTest (ApplePlatform platform, string runtimeIdentifiers) Assert.That (archiveDirs.Count, Is.GreaterThan (0), "ArchiveDir"); var archiveDir = archiveDirs [0]!.Trim (); Assert.That (archiveDir, Does.Exist, "Archive directory existence"); + AssertApplicationArtifact (result.BinLogPath, archiveDir, platform, "xcarchive", isDirectory: true); AssertDSymDirectory (appPath); } @@ -64,15 +67,141 @@ public void BuildIpaTest (ApplePlatform platform, string runtimeIdentifiers) properties ["BuildIpa"] = "true"; properties ["Configuration"] = configuration; - DotNet.AssertBuild (project_path, properties); + var result = DotNet.AssertBuild (project_path, properties); var pkgPath = Path.Combine (appPath, "..", $"{project}.ipa"); Assert.That (pkgPath, Does.Exist, "pkg creation"); + AssertApplicationArtifact (result.BinLogPath, appPath, platform, "app", isDirectory: true); + AssertApplicationArtifact (result.BinLogPath, pkgPath, platform, "ipa", isDirectory: false); AssertBundleAssembliesStripStatus (appPath, true); AssertDSymDirectory (appPath); } + [Test] + [Category ("RemoteWindows")] + [TestCase (ApplePlatform.iOS, "ios-arm64")] + public void BuildIpaOnRemoteWindowsTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + Configuration.IgnoreIfNotOnWindows (); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["BuildIpa"] = "true"; + properties ["Configuration"] = configuration; + + var result = DotNet.AssertBuild (project_path, properties, timeout: TimeSpan.FromMinutes (15)); + + // The .ipa is built on the paired Mac and copied back to this Windows machine, so it must be + // surfaced in @(ApplicationArtifact) with the local (Windows) path. The .app bundle stays on the + // Mac, so it's intentionally not surfaced as an artifact on Windows. + var ipaPath = Path.Combine (appPath, "..", $"{project}.ipa"); + Assert.That (ipaPath, Does.Exist, "ipa creation"); + AssertApplicationArtifact (result.BinLogPath, ipaPath, platform, "ipa", isDirectory: false); + } + + [Test] + [Category ("RemoteWindows")] + [TestCase (ApplePlatform.iOS, "ios-arm64")] + public void ArchiveOnRemoteWindowsTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + Configuration.IgnoreIfNotOnWindows (); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["ArchiveOnBuild"] = "true"; + properties ["Configuration"] = configuration; + + var result = DotNet.AssertBuild (project_path, properties, timeout: TimeSpan.FromMinutes (20)); + + // The .xcarchive is produced on the paired Mac and copied back to this Windows machine by + // CopyArchiveFromMac, which sets $(ArchivePath) to the local (Windows) path while $(ArchiveDir) + // keeps pointing at the Mac path. The Windows archive path is date-based, so match on the + // package format rather than an exact path. + AssertApplicationArtifact (result.BinLogPath, platform, "xcarchive", isDirectory: true); + } + + [Test] + [TestCase (ApplePlatform.iOS, "ios-arm64")] + [TestCase (ApplePlatform.TVOS, "tvos-arm64")] + public void GetApplicationArtifactsIpaTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["BuildIpa"] = "true"; + properties ["Configuration"] = configuration; + + var outputs = GetApplicationArtifacts (project_path, properties); + var pkgPath = Path.Combine (appPath, "..", $"{project}.ipa"); + + Assert.That (pkgPath, Does.Exist, "pkg creation"); + AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + AssertApplicationArtifact (outputs, pkgPath, platform, "ipa", isDirectory: false); + } + + [Test] + [TestCase (ApplePlatform.iOS, "ios-arm64", "ipa")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", "pkg")] + public void GetApplicationArtifactsDependsOnTest (ApplePlatform platform, string runtimeIdentifiers, string packageFormat) + { + var project = "MySimpleAppWithArtifactMetadata"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["BuildIpa"] = packageFormat == "ipa" ? "true" : "false"; + properties ["CreatePackage"] = packageFormat == "pkg" ? "true" : "false"; + properties ["Configuration"] = configuration; + properties ["ExpectedAugmentedPackageFormat"] = packageFormat; + + var outputs = GetApplicationArtifacts (project_path, properties); + var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + var packageOutput = AssertApplicationArtifact (outputs, platform, packageFormat, isDirectory: false); + Assert.That (GetMetadata (appOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (appOutput, "MauiObservedPackageFormat"), Is.EqualTo ("app"), "MauiObservedPackageFormat"); + Assert.That (GetMetadata (packageOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (packageOutput, "MauiObservedPackageFormat"), Is.EqualTo (packageFormat), "MauiObservedPackageFormat"); + } + + [Test] + [TestCase (ApplePlatform.iOS, "ios-arm64")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")] + public void GetApplicationArtifactsArchiveTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers, platform, out _, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["ArchiveOnBuild"] = "true"; + properties ["Configuration"] = configuration; + + var outputs = GetApplicationArtifacts (project_path, properties); + AssertApplicationArtifact (outputs, platform, "xcarchive", isDirectory: true); + } + [Test] [TestCase ("MySimpleApp", ApplePlatform.iOS, "ios-arm64", true)] [TestCase ("MySimpleApp", ApplePlatform.iOS, "ios-arm64", false)] @@ -137,10 +266,35 @@ public void BuildPackageTest (string project, ApplePlatform platform, string run var properties = GetDefaultProperties (runtimeIdentifiers); properties ["CreatePackage"] = "true"; - DotNet.AssertBuild (project_path, properties); + var result = DotNet.AssertBuild (project_path, properties); + + var pkgPath = Path.Combine (appPath, "..", $"{project}-{projectVersion}.pkg"); + Assert.That (pkgPath, Does.Exist, "pkg creation"); + AssertApplicationArtifact (result.BinLogPath, appPath, platform, "app", isDirectory: true); + AssertApplicationArtifact (result.BinLogPath, pkgPath, platform, "pkg", isDirectory: false); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")] + [TestCase (ApplePlatform.MacOSX, "osx-x64")] + public void GetApplicationArtifactsPkgTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var projectVersion = "3.14"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["CreatePackage"] = "true"; + + var outputs = GetApplicationArtifacts (project_path, properties); var pkgPath = Path.Combine (appPath, "..", $"{project}-{projectVersion}.pkg"); + Assert.That (pkgPath, Does.Exist, "pkg creation"); + AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + AssertApplicationArtifact (outputs, pkgPath, platform, "pkg", isDirectory: false); } [TestCase (ApplePlatform.iOS, "ios-arm64")] @@ -180,9 +334,39 @@ public void PublishTest (ApplePlatform platform, string runtimeIdentifiers) var properties = GetDefaultProperties (runtimeIdentifiers); properties [pathVariable] = pkgPath; - DotNet.AssertPublish (project_path, properties); + var result = DotNet.AssertPublish (project_path, properties); Assert.That (pkgPath, Does.Exist, "ipa/pkg creation"); + AssertApplicationArtifact (result.BinLogPath, pkgPath, platform, packageExtension, isDirectory: false); + } + + [TestCase (ApplePlatform.iOS, "ios-arm64", "ipa", "IpaPackagePath")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", "pkg", "PkgPackagePath")] + public void PublishApplicationArtifactsDependsOnTest (ApplePlatform platform, string runtimeIdentifiers, string packageFormat, string pathVariable) + { + var project = "MySimpleAppWithArtifactMetadata"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + + var tmpdir = Cache.CreateTemporaryDirectory (); + var pkgPath = Path.Combine (tmpdir, $"MyPackage.{packageFormat}"); + + var properties = GetDefaultProperties (runtimeIdentifiers); + properties [pathVariable] = pkgPath; + properties ["Configuration"] = configuration; + properties ["ExpectedAugmentedPackageFormat"] = packageFormat; + + var outputs = GetApplicationArtifacts (project_path, properties, target: "Publish"); + var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + var packageOutput = AssertApplicationArtifact (outputs, pkgPath, platform, packageFormat, isDirectory: false); + Assert.That (GetMetadata (appOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (appOutput, "MauiObservedPackageFormat"), Is.EqualTo ("app"), "MauiObservedPackageFormat"); + Assert.That (GetMetadata (packageOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (packageOutput, "MauiObservedPackageFormat"), Is.EqualTo (packageFormat), "MauiObservedPackageFormat"); } @@ -374,7 +558,81 @@ public void StaticFrameworksNotInPostProcessing (ApplePlatform platform, string Assert.That (staticFrameworkItems, Is.Empty, $"Static framework XStaticArTest should not be in post-processing items. All items:\n\t{string.Join ("\n\t", postProcessingItems.Select (i => i.ItemSpec))}"); } + static ITaskItem AssertApplicationArtifact (string binLogPath, string path, ApplePlatform platform, string packageFormat, bool isDirectory) + { + var outputs = GetItems (binLogPath, "ApplicationArtifact"); + var fullPath = Path.GetFullPath (path); + var output = outputs.SingleOrDefault (v => Path.GetFullPath (v.ItemSpec) == fullPath); + Assert.That (output, Is.Not.Null, $"Could not find {packageFormat} output for {fullPath}. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => v.ItemSpec))}"); + Assert.That (output!.GetMetadata ("PackageFormat"), Is.EqualTo (packageFormat), "PackageFormat"); + Assert.That (output.GetMetadata ("IsDirectory"), Is.EqualTo (isDirectory ? "true" : "false"), "IsDirectory"); + Assert.That (output.GetMetadata ("PlatformName"), Is.EqualTo (platform.AsString ()), "PlatformName"); + Assert.That (output.GetMetadata ("BundleIdentifier"), Is.Not.Empty, "BundleIdentifier"); + Assert.That (output.GetMetadata ("ArtifactKind"), Is.Null.Or.Empty, "ArtifactKind"); + Assert.That (output.GetMetadata ("AppBundlePath"), Is.Null.Or.Empty, "AppBundlePath"); + Assert.That (output.GetMetadata ("CodeSigned"), Is.Null.Or.Empty, "CodeSigned"); + Assert.That (output.GetMetadata ("Signed"), Is.Null.Or.Empty, "Signed"); + Assert.That (output.GetMetadata ("PackageSigned"), Is.Null.Or.Empty, "PackageSigned"); + return output; + } + + static ITaskItem AssertApplicationArtifact (string binLogPath, ApplePlatform platform, string packageFormat, bool isDirectory) + { + var outputs = GetItems (binLogPath, "ApplicationArtifact"); + var matchingOutputs = outputs.Where (v => v.GetMetadata ("PackageFormat") == packageFormat).ToList (); + Assert.That (matchingOutputs, Has.Count.EqualTo (1), $"Expected one {packageFormat} output. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => v.ItemSpec))}"); + + var itemSpec = matchingOutputs [0].ItemSpec; + Assert.That (itemSpec, Does.Exist, packageFormat); + return AssertApplicationArtifact (binLogPath, itemSpec, platform, packageFormat, isDirectory); + } + + static JsonElement [] GetApplicationArtifacts (string projectPath, Dictionary properties, string target = "GetApplicationArtifacts") + { + using var document = JsonDocument.Parse (DotNet.GetItems (projectPath, "ApplicationArtifact", target: target, properties: properties)); + var outputs = document.RootElement.GetProperty ("Items").GetProperty ("ApplicationArtifact").EnumerateArray ().Select (v => v.Clone ()).ToArray (); + Assert.That (outputs, Is.Not.Empty, "ApplicationArtifact items"); + return outputs; + } + + static JsonElement AssertApplicationArtifact (JsonElement [] outputs, string path, ApplePlatform platform, string packageFormat, bool isDirectory) + { + var fullPath = Path.GetFullPath (path); + var output = outputs.SingleOrDefault (v => Path.GetFullPath (GetMetadata (v, "FullPath")) == fullPath); + Assert.That (output.ValueKind, Is.Not.EqualTo (JsonValueKind.Undefined), $"Could not find {packageFormat} output for {fullPath}. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => GetMetadata (v, "FullPath")))}"); + Assert.That (GetMetadata (output, "PackageFormat"), Is.EqualTo (packageFormat), "PackageFormat"); + Assert.That (GetMetadata (output, "IsDirectory"), Is.EqualTo (isDirectory ? "true" : "false"), "IsDirectory"); + Assert.That (GetMetadata (output, "PlatformName"), Is.EqualTo (platform.AsString ()), "PlatformName"); + Assert.That (GetMetadata (output, "BundleIdentifier"), Is.Not.Empty, "BundleIdentifier"); + Assert.That (GetMetadata (output, "ArtifactKind"), Is.Empty, "ArtifactKind"); + Assert.That (GetMetadata (output, "AppBundlePath"), Is.Empty, "AppBundlePath"); + Assert.That (GetMetadata (output, "CodeSigned"), Is.Empty, "CodeSigned"); + Assert.That (GetMetadata (output, "Signed"), Is.Empty, "Signed"); + Assert.That (GetMetadata (output, "PackageSigned"), Is.Empty, "PackageSigned"); + return output; + } + + static JsonElement AssertApplicationArtifact (JsonElement [] outputs, ApplePlatform platform, string packageFormat, bool isDirectory) + { + var matchingOutputs = outputs.Where (v => GetMetadata (v, "PackageFormat") == packageFormat).ToArray (); + Assert.That (matchingOutputs, Has.Length.EqualTo (1), $"Expected one {packageFormat} output. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => GetMetadata (v, "FullPath")))}"); + + var fullPath = GetMetadata (matchingOutputs [0], "FullPath"); + Assert.That (fullPath, Does.Exist, packageFormat); + return AssertApplicationArtifact (outputs, fullPath, platform, packageFormat, isDirectory); + } + + static string GetMetadata (JsonElement item, string name) + { + return item.TryGetProperty (name, out var value) ? value.GetString () ?? "" : ""; + } + static List GetPostProcessingItems (string binLogPath) + { + return GetItems (binLogPath, "_PostProcessingItem"); + } + + static List GetItems (string binLogPath, string itemType) { var items = new Dictionary (); foreach (var args in BinLog.ReadBuildEvents (binLogPath)) { @@ -382,7 +640,7 @@ static List GetPostProcessingItems (string binLogPath) continue; if (tpea.Kind != TaskParameterMessageKind.AddItem) continue; - if (tpea.ItemType != "_PostProcessingItem") + if (tpea.ItemType != itemType) continue; foreach (var item in tpea.Items) { if (item is ITaskItem taskItem)