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
-
-
-
-
-