diff --git a/Helix.XHarness.Android.Device.Tests.binlog b/Helix.XHarness.Android.Device.Tests.binlog new file mode 100644 index 00000000000..aeb627f3a55 Binary files /dev/null and b/Helix.XHarness.Android.Device.Tests.binlog differ diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8f649aa6c82..aa267a85920 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -154,8 +154,8 @@ stages: -ci -restore -test - -projects $(Build.SourcesDirectory)\tests\UnitTests.XHarness.Android.Device.proj - /bl:$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)\Helix.XHarness.Android.Device.binlog + -projects $(Build.SourcesDirectory)\tests\XHarness.Android.DeviceTests.proj + /bl:$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)\Helix.XHarness.Android.Device.Tests.binlog /p:RestoreUsingNuGetTargets=false displayName: XHarness Android Helix Testing (Windows) env: @@ -195,10 +195,10 @@ stages: -ci -restore -test - -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.Simulator.proj - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/UnitTests.XHarness.iOS.Simulator.binlog + -projects $(Build.SourcesDirectory)/tests/XHarness.Apple.SimulatorTests.proj + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/XHarness.Apple.Simulator.Tests.binlog /p:RestoreUsingNuGetTargets=false - displayName: XHarness iOS Simulator Helix Testing + displayName: XHarness Apple Simulator Helix Testing env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) HelixAccessToken: '' @@ -208,10 +208,10 @@ stages: -ci -restore -test - -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.Device.proj - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.iOS.Device.binlog + -projects $(Build.SourcesDirectory)/tests/XHarness.Apple.DeviceTests.proj + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.Apple.Device.Tests.binlog /p:RestoreUsingNuGetTargets=false - displayName: XHarness iOS Device Helix Testing + displayName: XHarness Apple Device Helix Testing env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) HelixAccessToken: '' @@ -221,8 +221,8 @@ stages: -ci -restore -test - -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.Android.Simulator.proj - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.Android.Simulator.binlog + -projects $(Build.SourcesDirectory)/tests/XHarness.Android.SimulatorTests.proj + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.Android.Simulator.Tests.binlog /p:RestoreUsingNuGetTargets=false displayName: XHarness Android Helix Testing (Linux) env: diff --git a/src/Common/Microsoft.Arcade.Common/FileSystem.cs b/src/Common/Microsoft.Arcade.Common/FileSystem.cs index 83fd1df0dcf..1c9ab5939d4 100644 --- a/src/Common/Microsoft.Arcade.Common/FileSystem.cs +++ b/src/Common/Microsoft.Arcade.Common/FileSystem.cs @@ -33,7 +33,7 @@ public void WriteToFile(string path, string content) File.WriteAllText(path, content); } - public void FileCopy(string sourceFileName, string destFileName) => File.Copy(sourceFileName, destFileName); + public void CopyFile(string sourceFileName, string destFileName, bool overwrite = false) => File.Copy(sourceFileName, destFileName, overwrite); public Stream GetFileStream(string path, FileMode mode, FileAccess access) => new FileStream(path, mode, access); diff --git a/src/Common/Microsoft.Arcade.Common/IFileSystem.cs b/src/Common/Microsoft.Arcade.Common/IFileSystem.cs index 2aed8429879..9ca0dfd2786 100644 --- a/src/Common/Microsoft.Arcade.Common/IFileSystem.cs +++ b/src/Common/Microsoft.Arcade.Common/IFileSystem.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable enable using System.IO; +#nullable enable namespace Microsoft.Arcade.Common { public interface IFileSystem @@ -28,7 +28,7 @@ public interface IFileSystem void DeleteFile(string path); - void FileCopy(string sourceFileName, string destFileName); + void CopyFile(string sourceFileName, string destFileName, bool overwrite = false); Stream GetFileStream(string path, FileMode mode, FileAccess access); diff --git a/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs b/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs index 87a59d883d5..d1fecb1d6a0 100644 --- a/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs +++ b/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs @@ -2,12 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.IO.Compression; using System.Threading.Tasks; namespace Microsoft.Arcade.Common { public interface IZipArchiveManager { + /// + /// Opens a given archive. + /// + /// Path to the zip archive + /// Access mode + ZipArchive OpenArchive(string archivePath, ZipArchiveMode mode); + /// /// Loads an embedded resource and adds it to a target archive. /// diff --git a/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs b/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs index b022fe7238f..dc1c04ea252 100644 --- a/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs +++ b/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs @@ -12,6 +12,9 @@ namespace Microsoft.Arcade.Common { public class ZipArchiveManager : IZipArchiveManager { + public ZipArchive OpenArchive(string archivePath, ZipArchiveMode mode) + => ZipFile.Open(archivePath, mode); + public async Task AddResourceFileToArchive(string archivePath, string resourceName, string targetFileName = null) { using Stream fileStream = GetResourceFileContent(resourceName); diff --git a/src/Common/Microsoft.Arcade.Test.Common/MockFileSystem.cs b/src/Common/Microsoft.Arcade.Test.Common/MockFileSystem.cs index ed126d9d21a..b122c2630ab 100644 --- a/src/Common/Microsoft.Arcade.Test.Common/MockFileSystem.cs +++ b/src/Common/Microsoft.Arcade.Test.Common/MockFileSystem.cs @@ -54,7 +54,7 @@ public void DeleteFile(string path) public void WriteToFile(string path, string content) => Files[path] = content; - public void FileCopy(string sourceFileName, string destFileName) => Files[destFileName] = Files[sourceFileName]; + public void CopyFile(string sourceFileName, string destFileName, bool overwrite = false) => Files[destFileName] = Files[sourceFileName]; public Stream GetFileStream(string path, FileMode mode, FileAccess access) => FileExists(path) ? new MemoryStream() : new MockFileStream(this, path); diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs index 4652965fbaf..5ce4b415574 100644 --- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs +++ b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs @@ -92,7 +92,7 @@ public void AppleXHarnessWorkItemIsCreated() command.Should().Contain("--launch-timeout \"00:02:33\""); _profileProvider - .Verify(x => x.AddProfilesToBundles(It.Is(bundles => bundles.Any(b => b.ItemSpec == "/apps/System.Foo.app"))), Times.Once); + .Verify(x => x.AddProfileToPayload(payloadArchive, "ios-device_13.5"), Times.Once); _zipArchiveManager .Verify(x => x.ArchiveDirectory("/apps/System.Foo.app", payloadArchive, true), Times.Once); _zipArchiveManager @@ -238,7 +238,7 @@ public void ZippedAppIsProvided() command.Should().Contain("--launch-timeout \"00:02:33\""); _profileProvider - .Verify(x => x.AddProfilesToBundles(It.Is(bundles => bundles.Any(b => b.ItemSpec == "/apps/System.Foo.zip"))), Times.Once); + .Verify(x => x.AddProfileToPayload(payloadArchive, "ios-device_13.5"), Times.Once); _zipArchiveManager .Verify(x => x.ArchiveDirectory("/apps/System.Foo.app", payloadArchive, true), Times.Never); _zipArchiveManager diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs deleted file mode 100644 index f84e7a6f117..00000000000 --- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net; -using System.Net.Http; -using FluentAssertions; -using Microsoft.Arcade.Common; -using Microsoft.Arcade.Test.Common; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.DotNet.Arcade.Test.Common; -using Moq; -using Xunit; - -#nullable enable -namespace Microsoft.DotNet.Helix.Sdk.Tests -{ - public class ProvisioningProfileProviderTests - { - private readonly MockFileSystem _fileSystem; - private readonly Mock _helpersMock; - private readonly ProvisioningProfileProvider _profileProvider; - private int _downloadCount = 0; - - public ProvisioningProfileProviderTests() - { - _helpersMock = new Mock(); - _helpersMock - .Setup(x => x.DirectoryMutexExec(It.IsAny>(), It.IsAny())) - .Callback, string>((function, path) => { - ++_downloadCount; - function().GetAwaiter().GetResult(); - }); - - _fileSystem = new MockFileSystem(); - - var response1 = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("iOS content"), - }; - - var response2 = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("tvOS content"), - }; - - var httpClient = FakeHttpClient.WithResponses(response1, response2); - - _profileProvider = new ProvisioningProfileProvider( - new TaskLoggingHelper(new MockBuildEngine(), nameof(ProvisioningProfileProviderTests)), - _helpersMock.Object, - _fileSystem, - httpClient, - "https://netcorenativeassets.azure.com/profiles/{PLATFORM}.mobileprovision", - "/tmp"); - } - - [Fact] - public void NonDeviceTargetsAreIgnored() - { - _profileProvider.AddProfilesToBundles(new[] - { - CreateAppBundle("/apps/System.Foo.app", "ios-simulator-64_13.5"), - CreateAppBundle("/apps/System.Bar.app", "tvos-simulator-64"), - }); - - _downloadCount.Should().Be(0); - _fileSystem.Files.Should().BeEmpty(); - } - - [Fact] - public void MultipleiOSDeviceTargetsGetTheSameProfile() - { - _profileProvider.AddProfilesToBundles(new[] - { - CreateAppBundle("/apps/System.Device1.app", "ios-device"), - CreateAppBundle("/apps/System.Simulator.app", "tvos-simulator-64"), - CreateAppBundle("/apps/System.Device2.app", "ios-device"), - CreateAppBundle("/apps/System.Foo.app", "ios-simulator-64_13.5"), - }); - - _downloadCount.Should().Be(1); - - _fileSystem.Files.Keys.Should().BeEquivalentTo( - "/tmp/iOS.mobileprovision", - "/apps/System.Device1.app/embedded.mobileprovision", - "/apps/System.Device2.app/embedded.mobileprovision"); - - _fileSystem.Files["/apps/System.Device1.app/embedded.mobileprovision"].Should().Be("iOS content"); - _fileSystem.Files["/apps/System.Device2.app/embedded.mobileprovision"].Should().Be("iOS content"); - } - - [Fact] - public void MultiplePlatformsGetTheirProfile() - { - _profileProvider.AddProfilesToBundles(new[] - { - CreateAppBundle("/apps/System.Device1.iOS.app", "ios-device"), - CreateAppBundle("/apps/System.Simulator.app", "tvos-simulator-64"), - CreateAppBundle("/apps/System.Device2.iOS.app", "ios-device"), - CreateAppBundle("/apps/System.Device3.tvOS.app", "tvos-device"), - }); - - _downloadCount.Should().Be(2); - - _fileSystem.Files.Keys.Should().BeEquivalentTo( - "/tmp/iOS.mobileprovision", - "/tmp/tvOS.mobileprovision", - "/apps/System.Device1.iOS.app/embedded.mobileprovision", - "/apps/System.Device2.iOS.app/embedded.mobileprovision", - "/apps/System.Device3.tvOS.app/embedded.mobileprovision"); - - _fileSystem.Files["/apps/System.Device1.iOS.app/embedded.mobileprovision"].Should().Be("iOS content"); - _fileSystem.Files["/apps/System.Device2.iOS.app/embedded.mobileprovision"].Should().Be("iOS content"); - _fileSystem.Files["/apps/System.Device3.tvOS.app/embedded.mobileprovision"].Should().Be("tvOS content"); - } - - [Fact] - public void BundlesContainingProfileAreIgnored() - { - _fileSystem.WriteToFile("/apps/System.Device1.app/embedded.mobileprovision", "iOS content"); - _profileProvider.AddProfilesToBundles(new[] - { - CreateAppBundle("/apps/System.Device1.app", "ios-device"), - CreateAppBundle("/apps/System.Simulator.app", "tvos-simulator-64"), - }); - - _downloadCount.Should().Be(0); - } - - private static ITaskItem CreateAppBundle(string path, string targets) - { - var mockBundle = new Mock(); - mockBundle.SetupGet(x => x.ItemSpec).Returns(path); - mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.Target)).Returns(targets); - return mockBundle.Object; - } - } -} diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs index b9667c4bdc2..3bed8a524c2 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs @@ -73,8 +73,7 @@ public bool ExecuteTask( IZipArchiveManager zipArchiveManager, IFileSystem fileSystem) { - provisioningProfileProvider.AddProfilesToBundles(AppBundles); - var tasks = AppBundles.Select(bundle => PrepareWorkItem(zipArchiveManager, fileSystem, bundle)); + var tasks = AppBundles.Select(bundle => PrepareWorkItem(zipArchiveManager, fileSystem, provisioningProfileProvider, bundle)); WorkItems = Task.WhenAll(tasks).GetAwaiter().GetResult().Where(wi => wi != null).ToArray(); @@ -89,6 +88,7 @@ public bool ExecuteTask( private async Task PrepareWorkItem( IZipArchiveManager zipArchiveManager, IFileSystem fileSystem, + IProvisioningProfileProvider provisioningProfileProvider, ITaskItem appBundleItem) { var (workItemName, appFolderPath) = GetNameAndPath(appBundleItem, MetadataNames.AppBundlePath, fileSystem); @@ -107,6 +107,17 @@ private async Task PrepareWorkItem( Log.LogError($"App bundle not found in {appFolderPath}"); return null; } + + // If we are re-using one .zip for multiple work items, we need to copy it to a new location + // because we will be changing the contents (we assume we don't mind otherwise) + if (isAlreadyArchived && appBundleItem.TryGetMetadata(MetadataNames.AppBundlePath, out string metadata) && !string.IsNullOrEmpty(metadata)) + { + string appFolderDirectory = fileSystem.GetDirectoryName(appFolderPath); + string fileName = $"xharness-payload-{workItemName.ToLowerInvariant()}.zip"; + string archiveCopyPath = fileSystem.PathCombine(appFolderDirectory, fileName); + fileSystem.CopyFile(appFolderPath, archiveCopyPath, overwrite: true); + appFolderPath = archiveCopyPath; + } var (testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appBundleItem); @@ -142,7 +153,7 @@ private async Task PrepareWorkItem( if (includesTestRunner && expectedExitCode != 0 && customCommands != null) { - Log.LogWarning("The ExpectedExitCode property is ignored in the `apple test` scenario"); + Log.LogWarning($"The {MetadataName.ExpectedExitCode} property is ignored in the `apple test` scenario"); } bool resetSimulator = false; @@ -172,6 +183,8 @@ private async Task PrepareWorkItem( customCommands, new[] { EntryPointScript, RunnerScript }); + provisioningProfileProvider.AddProfileToPayload(payloadArchivePath, target); + return CreateTaskItem(workItemName, payloadArchivePath, helixCommand, workItemTimeout); } diff --git a/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs b/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs index 0dcfdcdfbb7..d1313f0311c 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using System.Linq; using System.Net.Http; +using System.Text.RegularExpressions; using Microsoft.Arcade.Common; -using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -22,11 +24,23 @@ public enum ApplePlatform public interface IProvisioningProfileProvider { - void AddProfilesToBundles(ITaskItem[] appBundles); + void AddProfileToPayload(string archivePath, string testTarget); } + /// + /// This class embeds Apple provisioning profiles into app bundles. + /// App bundles are directories with files that represent an iOS or tvOS application. + /// Provisioning profile is a file used for signing and differs per platform (iOS/tvOS). + /// This class makes sure each app bundle has one before it is sent to Helix. + /// It injects the profiles into all top-level app bundles in a given .zip archive. + /// public class ProvisioningProfileProvider : IProvisioningProfileProvider { + // The name of the profile that Apple expects + private const string ProfileFileName = "embedded.mobileprovision"; + + // Matches all paths to .app bundle directories in archive's root + private static readonly Regex s_topLevelAppPattern = new("^[^" + Regex.Escape(new string(Path.GetInvalidFileNameChars())) + "]+\\.app/.+"); private static readonly IReadOnlyDictionary s_targetNames = new Dictionary() { { ApplePlatform.iOS, "ios-device" }, @@ -36,14 +50,17 @@ public class ProvisioningProfileProvider : IProvisioningProfileProvider private readonly TaskLoggingHelper _log; private readonly IHelpers _helpers; private readonly IFileSystem _fileSystem; + private readonly IZipArchiveManager _zipArchiveManager; private readonly HttpClient _httpClient; private readonly string? _profileUrlTemplate; private readonly string? _tmpDir; + private readonly Dictionary _downloadedProfiles = new(); public ProvisioningProfileProvider( TaskLoggingHelper log, IHelpers helpers, IFileSystem fileSystem, + IZipArchiveManager zipArchiveManager, HttpClient httpClient, string? profileUrlTemplate, string? tmpDir) @@ -51,85 +68,110 @@ public ProvisioningProfileProvider( _log = log ?? throw new ArgumentNullException(nameof(log)); _helpers = helpers ?? throw new ArgumentNullException(nameof(helpers)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _zipArchiveManager = zipArchiveManager ?? throw new ArgumentNullException(nameof(zipArchiveManager)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _profileUrlTemplate = profileUrlTemplate; _tmpDir = tmpDir; } - public void AddProfilesToBundles(ITaskItem[] appBundles) + public void AddProfileToPayload(string archivePath, string testTarget) { - var profileLocations = new Dictionary(); - - foreach (var appBundle in appBundles) + foreach (var pair in s_targetNames) { - string appBundlePath; - if (appBundle.TryGetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.AppBundlePath, out string pathMetadata) - && !string.IsNullOrEmpty(pathMetadata)) + ApplePlatform platform = pair.Key; + string targetName = pair.Value; + + // Only app bundles that target iOS/tvOS devices need a profile (simulators don't) + if (!testTarget.Contains(targetName)) { - appBundlePath = pathMetadata; + continue; } - else + + // This makes sure we download the profile the first time we see an app that needs it + if (!_downloadedProfiles.TryGetValue(platform, out string? profilePath)) { - appBundlePath = appBundle.ItemSpec; + if (string.IsNullOrEmpty(_tmpDir)) + { + _log.LogError($"{nameof(CreateXHarnessAppleWorkItems.TmpDir)} parameter not set but required for real device targets!"); + return; + } + + if (string.IsNullOrEmpty(_profileUrlTemplate)) + { + _log.LogError($"{nameof(CreateXHarnessAppleWorkItems.ProvisioningProfileUrl)} parameter not set but required for real device targets!"); + return; + } + + profilePath = DownloadProvisioningProfile(platform); + _downloadedProfiles.Add(platform, profilePath); } - if (!appBundle.TryGetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.Target, out string bundleTargets)) + AddProfileToArchive(archivePath, profilePath); + } + } + + /// + /// Adds a provisioning profile to a given zip archive. + /// Either adds it to all .app folders inside or to the root of the archive if no app bundles found. + /// + private void AddProfileToArchive(string archivePath, string profilePath) + { + // App comes with a profile already + using ZipArchive zipArchive = _zipArchiveManager.OpenArchive(archivePath, ZipArchiveMode.Update); + + HashSet rootLevelAppBundles = new(); + HashSet appBundlesWithProfile = new(); + + foreach (ZipArchiveEntry entry in zipArchive.Entries) + { + if (!s_topLevelAppPattern.IsMatch(entry.FullName)) { - _log.LogError("'Targets' metadata must be specified - " + - "expecting list of target device/simulator platforms to execute tests on (e.g. ios-simulator-64)"); continue; } - if (appBundlePath.EndsWith(".zip")) + string appBundleName = entry.FullName.Split(new[] { '/' }, 2).First(); + + if (entry.FullName == appBundleName + "/" + ProfileFileName) { - // TODO: We need to be able to add provisioning profiles into a zipped payload too - continue; + appBundlesWithProfile.Add(appBundleName); + _log.LogMessage($"{appBundleName} already contains provisioning profile"); } - - foreach (var pair in s_targetNames) + else { - var platform = pair.Key; - var targetName = pair.Value; - - if (!bundleTargets.Contains(targetName)) - { - continue; - } + rootLevelAppBundles.Add(appBundleName); + } + } - // App comes with a profile already - var provisioningProfileDestPath = _fileSystem.PathCombine(appBundlePath, "embedded.mobileprovision"); - if (_fileSystem.FileExists(provisioningProfileDestPath)) - { - _log.LogMessage($"Bundle already contains a provisioning profile at `{provisioningProfileDestPath}`"); - continue; - } + rootLevelAppBundles = rootLevelAppBundles.Except(appBundlesWithProfile).ToHashSet(); - // This makes sure we download the profile the first time we see an app that needs it - if (!profileLocations.TryGetValue(platform, out string? profilePath)) - { - if (string.IsNullOrEmpty(_tmpDir)) - { - _log.LogError("TmpDir parameter not set but required for real device targets!"); - return; - } - - if (string.IsNullOrEmpty(_profileUrlTemplate)) - { - _log.LogError("ProvisioningProfileUrl parameter not set but required for real device targets!"); - return; - } - - profilePath = DownloadProvisioningProfile(platform); - profileLocations.Add(platform, profilePath); - } + // If no .app bundles, add it to the root + if (!rootLevelAppBundles.Any()) + { + _log.LogMessage($"No app bundles found in the archive. Adding provisioning profile to root"); - // Copy the profile into the folder - _log.LogMessage($"Adding provisioning profile `{profilePath}` into the app bundle at `{provisioningProfileDestPath}`"); - _fileSystem.FileCopy(profilePath, provisioningProfileDestPath); + // Check if archive comes with a profile already + if (!zipArchive.Entries.Any(e => e.FullName == ProfileFileName)) + { + zipArchive.CreateEntryFromFile(profilePath, ProfileFileName); } + + return; + } + + // Else inject profile to every app bundle in the root of the archive + foreach (string appBundle in rootLevelAppBundles) + { + var profileDestPath = appBundle + "/" + ProfileFileName; + _log.LogMessage($"Adding provisioning profile to {appBundle}"); + zipArchive.CreateEntryFromFile(profilePath, profileDestPath); } } + /// + /// Process-safe download of the profile (several clashing msbuild processes should download once). + /// + /// Which platform to download the profile for + /// Path where the profile was downloaded to private string DownloadProvisioningProfile(ApplePlatform platform) { var targetFile = _fileSystem.PathCombine(_tmpDir!, GetProvisioningProfileFileName(platform)); @@ -138,7 +180,7 @@ private string DownloadProvisioningProfile(ApplePlatform platform) { if (_fileSystem.FileExists(targetFile)) { - _log.LogMessage($"Provisioning profile is already downloaded"); + _log.LogMessage($"Using provisioning profile in {targetFile}"); return; } @@ -169,6 +211,7 @@ public static void TryAddProvisioningProfileProvider(this IServiceCollection col { collection.TryAddTransient(); collection.TryAddTransient(); + collection.TryAddTransient(); collection.TryAddSingleton(_ => new HttpClient(new HttpClientHandler { CheckCertificateRevocationList = true })); collection.TryAddSingleton(serviceProvider => { @@ -176,6 +219,7 @@ public static void TryAddProvisioningProfileProvider(this IServiceCollection col serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), provisioningProfileUrlTemplate, tmpDir); diff --git a/src/Microsoft.DotNet.Helix/Sdk/Readme.md b/src/Microsoft.DotNet.Helix/Sdk/Readme.md index dcc734ceb9c..57a690c29e3 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/Readme.md +++ b/src/Microsoft.DotNet.Helix/Sdk/Readme.md @@ -66,7 +66,7 @@ In order to run them, one has to publish the SDK locally so that the unit tests export SYSTEM_TEAMPROJECT=dnceng export SYSTEM_ACCESSTOKEN='' - eng/common/build.sh -test -projects tests/XHarness.Apple.Device.Tests /v:n /bl:Arcade.binlog + eng/common/build.sh -test -projects tests/XHarness.Apple.DeviceTests.proj /v:n /bl:Arcade.binlog ``` PowerShell @@ -77,7 +77,7 @@ In order to run them, one has to publish the SDK locally so that the unit tests $Env:SYSTEM_TEAMPROJECT = "dnceng" $Env:SYSTEM_ACCESSTOKEN = "" - .\eng\common\build.ps1 -configuration Debug -restore -test -projects tests\XHarness.Apple.Device.Tests /p:RestoreUsingNugetTargets=false /bl:Arcade.binlog + .\eng\common\build.ps1 -configuration Debug -restore -test -projects tests\XHarness.Apple.DeviceTests.proj /p:RestoreUsingNugetTargets=false /bl:Arcade.binlog ``` 5. An MSBuild log file called `Arcade.binlog` will be produced which you can inspect using the [MSBuild Structured Log Viewer](https://msbuildlog.com/). There you can see which props were set with which values, in what order the targets were executed under which conditions and so on. diff --git a/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs b/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs index a1de1cc2428..079ccf16866 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs @@ -111,22 +111,6 @@ protected Build.Utilities.TaskItem CreateTaskItem(string workItemName, string pa }); } - /// - /// This method parses the name for the Helix work item and path of the app from the item's metadata. - /// The user can re-use the same .apk for 2 work items so the name of the work item will come from ItemSpec and path from metadata. - /// - protected (string WorkItemName, string AppPath) GetNameAndPath(ITaskItem item, string pathMetadataName, IFileSystem fileSystem) - { - if (item.TryGetMetadata(pathMetadataName, out string appPathMetadata) && !string.IsNullOrEmpty(appPathMetadata)) - { - return (item.ItemSpec, appPathMetadata); - } - else - { - return (fileSystem.GetFileNameWithoutExtension(item.ItemSpec), item.ItemSpec); - } - } - protected async Task CreatePayloadArchive( IZipArchiveManager zipArchiveManager, IFileSystem fileSystem, @@ -137,18 +121,19 @@ protected async Task CreatePayloadArchive( string injectedCommands, string[] payloadScripts) { - string appFolderDirectory = fileSystem.GetDirectoryName(pathToZip); - string fileName = $"xharness-payload-{workItemName.ToLowerInvariant()}.zip"; - string outputZipPath = fileSystem.PathCombine(appFolderDirectory, fileName); - - if (fileSystem.FileExists(outputZipPath)) - { - Log.LogMessage($"Zip archive '{outputZipPath}' already exists, overwriting.."); - fileSystem.DeleteFile(outputZipPath); - } - + string outputZipPath; if (!isAlreadyArchived) { + string appFolderDirectory = fileSystem.GetDirectoryName(pathToZip); + string fileName = $"xharness-payload-{workItemName.ToLowerInvariant()}.zip"; + outputZipPath = fileSystem.PathCombine(appFolderDirectory, fileName); + + if (fileSystem.FileExists(outputZipPath)) + { + Log.LogMessage($"Zip archive '{outputZipPath}' already exists, overwriting.."); + fileSystem.DeleteFile(outputZipPath); + } + if (fileSystem.GetAttributes(pathToZip).HasFlag(FileAttributes.Directory)) { zipArchiveManager.ArchiveDirectory(pathToZip, outputZipPath, true); @@ -160,8 +145,8 @@ protected async Task CreatePayloadArchive( } else { - Log.LogMessage($"App payload '{workItemName}` has already been zipped. Copying to '{outputZipPath}` instead"); - fileSystem.FileCopy(pathToZip, outputZipPath); + Log.LogMessage($"App payload '{workItemName}` has already been zipped"); + outputZipPath = pathToZip; } Log.LogMessage($"Adding the XHarness job scripts into the payload archive"); @@ -186,5 +171,21 @@ await zipArchiveManager.AddContentToArchive( return outputZipPath; } + + /// + /// This method parses the name for the Helix work item and path of the app from the item's metadata. + /// The user can re-use the same .apk for 2 work items so the name of the work item will come from ItemSpec and path from metadata. + /// + public static (string WorkItemName, string AppPath) GetNameAndPath(ITaskItem item, string pathMetadataName, IFileSystem fileSystem) + { + if (item.TryGetMetadata(pathMetadataName, out string appPathMetadata) && !string.IsNullOrEmpty(appPathMetadata)) + { + return (item.ItemSpec, appPathMetadata); + } + else + { + return (fileSystem.GetFileNameWithoutExtension(item.ItemSpec), item.ItemSpec); + } + } } } diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh index 47fb9126ee1..4e4528e3cc2 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh @@ -63,31 +63,11 @@ function die () exit 1 } -if [ -z "$app" ]; then - die "App bundle path wasn't provided"; -fi - -if [ -z "$target" ]; then - die "No target was provided"; -fi - -if [ -z "$xcode_version" ]; then - xcode_path="$(dirname "$(dirname "$(xcode-select -p)")")" -else - xcode_path="/Applications/Xcode${xcode_version/./}.app" -fi - -# First we need to revive env variables since they were erased by launchctl -# This file already has the expressions in the `export name=value` format -. ./envvars - -output_directory=$HELIX_WORKITEM_UPLOAD_ROOT - -# Signing -if [ "$target" == 'ios-device' ] || [ "$target" == 'tvos-device' ]; then - echo "Real device target detected, application will be signed" +function sign () +{ + echo "Signing $1" - provisioning_profile="$app/embedded.mobileprovision" + provisioning_profile="$1/embedded.mobileprovision" if [ ! -f "$provisioning_profile" ]; then echo "No embedded provisioning profile found at $provisioning_profile! Failed to sign the app!" exit 21 @@ -118,7 +98,36 @@ if [ "$target" == 'ios-device' ] || [ "$target" == 'tvos-device' ]; then /usr/libexec/PlistBuddy -x -c 'Print :Entitlements' provision.plist > entitlements.plist # Sign the app - /usr/bin/codesign -v --force --sign "Apple Development" --keychain "$keychain_name" --entitlements entitlements.plist "$app" + /usr/bin/codesign -v --force --sign "Apple Development" --keychain "$keychain_name" --entitlements entitlements.plist "$1" +} + +if [ -z "$app" ]; then + die "App bundle path wasn't provided"; +fi + +if [ -z "$target" ]; then + die "No target was provided"; +fi + +if [ -z "$xcode_version" ]; then + xcode_path="$(dirname "$(dirname "$(xcode-select -p)")")" +else + xcode_path="/Applications/Xcode${xcode_version/./}.app" +fi + +# First we need to revive env variables since they were erased by launchctl +# This file already has the expressions in the `export name=value` format +. ./envvars + +output_directory=$HELIX_WORKITEM_UPLOAD_ROOT + +# Signing +if [ "$target" == 'ios-device' ] || [ "$target" == 'tvos-device' ]; then + if [ -d "$app" ]; then + sign "$app" + else + echo 'Device target detected but app not found, skipping signing..' + fi elif [[ "$target" =~ "simulator" ]]; then # Start the simulator if it is not running already simulator_app="$xcode_path/Contents/Developer/Applications/Simulator.app" @@ -127,7 +136,7 @@ fi # The xharness alias function xharness() { - dotnet exec $XHARNESS_CLI_PATH "$@" + dotnet exec "$XHARNESS_CLI_PATH" "$@" } # Act out the actual commands diff --git a/tests/UnitTests.XHarness.Android.Device.proj b/tests/XHarness.Android.DeviceTests.proj similarity index 90% rename from tests/UnitTests.XHarness.Android.Device.proj rename to tests/XHarness.Android.DeviceTests.proj index d0e6020a18b..839df67cc7d 100644 --- a/tests/UnitTests.XHarness.Android.Device.proj +++ b/tests/XHarness.Android.DeviceTests.proj @@ -1,5 +1,5 @@ - + diff --git a/tests/UnitTests.XHarness.Android.Simulator.proj b/tests/XHarness.Android.SimulatorTests.proj similarity index 93% rename from tests/UnitTests.XHarness.Android.Simulator.proj rename to tests/XHarness.Android.SimulatorTests.proj index 71451da61c9..9f4e26099e2 100644 --- a/tests/UnitTests.XHarness.Android.Simulator.proj +++ b/tests/XHarness.Android.SimulatorTests.proj @@ -1,5 +1,5 @@ - + - - + + diff --git a/tests/UnitTests.XHarness.iOS.Simulator.proj b/tests/XHarness.Apple.SimulatorTests.proj similarity index 81% rename from tests/UnitTests.XHarness.iOS.Simulator.proj rename to tests/XHarness.Apple.SimulatorTests.proj index e483cb6384a..5e9a98ac97d 100644 --- a/tests/UnitTests.XHarness.iOS.Simulator.proj +++ b/tests/XHarness.Apple.SimulatorTests.proj @@ -1,5 +1,5 @@ - + - - - + + + diff --git a/tests/UnitTests.XHarness.Common.props b/tests/XHarness.Tests.Common.props similarity index 100% rename from tests/UnitTests.XHarness.Common.props rename to tests/XHarness.Tests.Common.props diff --git a/tests/XHarness/.gitattributes b/tests/XHarness/.gitattributes new file mode 100644 index 00000000000..a584c86a720 --- /dev/null +++ b/tests/XHarness/.gitattributes @@ -0,0 +1,2 @@ +# We need to force LF on Windows because the XML contains bash commands which will be executed on a MacOS machine +XHarness.Apple.*.proj text eol=lf \ No newline at end of file diff --git a/tests/XHarness/XHarness.Apple.Device.Archived.proj b/tests/XHarness/XHarness.Apple.Device.Archived.proj new file mode 100644 index 00000000000..7d592369a40 --- /dev/null +++ b/tests/XHarness/XHarness.Apple.Device.Archived.proj @@ -0,0 +1,31 @@ + + + + + + System.Buffers.Tests.app + https://netcorenativeassets.blob.core.windows.net/resource-packages/external/ios/test-app/tvos-device/zipped-apps.zip + $(ArtifactsTmpDir)XHarness.Apple.Device.Archived + + + + + + + + + + + + + tvos-device + 00:18:00 + + sign $(XHarnessTestAppName) + xharness apple test -a $(XHarnessTestAppName) -o $output_directory -t $target -v --timeout 00:08:50 + + + + + + diff --git a/tests/XHarness/XHarness.Simulator.CustomCommands.proj b/tests/XHarness/XHarness.Apple.Simulator.CustomCommands.proj similarity index 100% rename from tests/XHarness/XHarness.Simulator.CustomCommands.proj rename to tests/XHarness/XHarness.Apple.Simulator.CustomCommands.proj diff --git a/tests/XHarness/XHarness.Simulator.AppleRun.proj b/tests/XHarness/XHarness.Apple.Simulator.Run.proj similarity index 100% rename from tests/XHarness/XHarness.Simulator.AppleRun.proj rename to tests/XHarness/XHarness.Apple.Simulator.Run.proj diff --git a/tests/XHarness/XHarness.Simulator.AppleTest.proj b/tests/XHarness/XHarness.Apple.Simulator.Test.proj similarity index 100% rename from tests/XHarness/XHarness.Simulator.AppleTest.proj rename to tests/XHarness/XHarness.Apple.Simulator.Test.proj diff --git a/tests/XHarness/XHarness.Device.AppleTest.proj b/tests/XHarness/XHarness.Device.AppleTest.proj deleted file mode 100644 index dc3c9a83c61..00000000000 --- a/tests/XHarness/XHarness.Device.AppleTest.proj +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - System.Buffers.Tests.app - https://netcorenativeassets.blob.core.windows.net/resource-packages/external/ios/test-app/ios-device/$(XHarnessRunAppBundleName).zip - - - - - - - - - - - - - - - - ios-device - 00:12:00 - 00:10:00 - 00:07:00 - - - - -