Skip to content

Commit afd54cd

Browse files
authored
Add iOS/tvOS signing capability to Helix SDK (XHarness) (#6823)
Adds the possibility to target real Apple devices and sign the applications before deploying them using XHarness. The signing process requires certificates to be placed on each Helix machine in a KeyChain and a provisioning profile from netcorenativeassets that is embedded into the application. Integration testing will be coming soon - The `OSX.1015.Amd64.Iphone.Open` queue is already populated with signing certificates and is ready to start signing - The `OSX.1015.Amd64.AppleTV.Open` will be ready soon (#11675) Resolves dotnet/core-eng#11678
1 parent c81c7e1 commit afd54cd

12 files changed

+237
-49
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ format text eol=lf
6161
compat text eol=lf
6262
*.bats text eol=lf
6363
*.1 text eol=lf
64+
*.plist text eol=lf

azure-pipelines.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,24 @@ stages:
181181
-projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.proj
182182
/bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.iOS.binlog
183183
/p:RestoreUsingNuGetTargets=false
184-
displayName: XHarness iOS Helix Testing
184+
displayName: XHarness iOS Simulator Helix Testing
185185
env:
186186
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
187187
HelixAccessToken: ''
188+
# TODO (prvysoky): This will be enabled once we get a better app to test with (core-eng/11893)
189+
# - script: eng/common/build.sh
190+
# -configuration $(_BuildConfig)
191+
# -prepareMachine
192+
# -ci
193+
# -restore
194+
# -test
195+
# -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.Device.proj
196+
# /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.iOS.Device.binlog
197+
# /p:RestoreUsingNuGetTargets=false
198+
# displayName: XHarness iOS Device Helix Testing
199+
# env:
200+
# SYSTEM_ACCESSTOKEN: $(System.AccessToken)
201+
# HelixAccessToken: ''
188202
- script: eng/common/build.sh
189203
-configuration $(_BuildConfig)
190204
-prepareMachine

src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private async Task<ITaskItem> PrepareWorkItem(ITaskItem appPackage)
6666
Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appPackage.ItemSpec}, Command: {command}");
6767

6868
string workItemZip = await CreateZipArchiveOfPackageAsync(appPackage.ItemSpec);
69-
69+
7070
return new Build.Utilities.TaskItem(workItemName, new Dictionary<string, string>()
7171
{
7272
{ "Identity", workItemName },
@@ -82,28 +82,17 @@ private async Task<string> CreateZipArchiveOfPackageAsync(string fileToZip)
8282
string fileName = $"xharness-apk-payload-{Path.GetFileNameWithoutExtension(fileToZip).ToLowerInvariant()}.zip";
8383
string outputZipAbsolutePath = Path.Combine(directoryOfPackage, fileName);
8484
using (FileStream fs = File.OpenWrite(outputZipAbsolutePath))
85+
using (var zip = new ZipArchive(fs, ZipArchiveMode.Create, false))
8586
{
86-
using (var zip = new ZipArchive(fs, ZipArchiveMode.Create, false))
87-
{
88-
zip.CreateEntryFromFile(fileToZip, Path.GetFileName(fileToZip));
89-
// WorkItem payloads of APKs can be reused if sent to multiple queues at once,
90-
// so we'll always include both scripts (very small)
91-
await AddEntryPointScriptsToWorkItemPayloadAsync(zip, PosixAndroidWrapperScript);
92-
await AddEntryPointScriptsToWorkItemPayloadAsync(zip, NonPosixAndroidWrapperScript);
93-
}
87+
zip.CreateEntryFromFile(fileToZip, Path.GetFileName(fileToZip));
9488
}
95-
return outputZipAbsolutePath;
96-
}
9789

98-
private async Task AddEntryPointScriptsToWorkItemPayloadAsync(ZipArchive zip, string scriptName)
99-
{
100-
var runnerScriptEntry = zip.CreateEntry(scriptName);
101-
using (var helixPayloadStreamWriter = new StreamWriter(runnerScriptEntry.Open()))
102-
{
103-
var thisAssembly = GetType().Assembly;
104-
using Stream embeddedScriptResourceStream = thisAssembly.GetManifestResourceStream($"{thisAssembly.GetName().Name}.tools.xharness_runner.{scriptName}");
105-
await embeddedScriptResourceStream.CopyToAsync(helixPayloadStreamWriter.BaseStream);
106-
}
90+
// WorkItem payloads of APKs can be reused if sent to multiple queues at once,
91+
// so we'll always include both scripts (very small)
92+
await AddResourceFileToPayload(outputZipAbsolutePath, PosixAndroidWrapperScript);
93+
await AddResourceFileToPayload(outputZipAbsolutePath, NonPosixAndroidWrapperScript);
94+
95+
return outputZipAbsolutePath;
10796
}
10897

10998
private string ValidateMetadataAndGetXHarnessAndroidCommand(ITaskItem appPackage, TimeSpan xHarnessTimeout, int expectedExitCode)

src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public class CreateXHarnessAppleWorkItems : XHarnessTaskBase
3131
/// </summary>
3232
public string XcodeVersion { get; set; }
3333

34+
/// <summary>
35+
/// Path to the provisioning profile that will be used to sign the app (in case of real device targets).
36+
/// </summary>
37+
public string ProvisioningProfilePath { get; set; }
38+
3439
/// <summary>
3540
/// The main method of this MSBuild task which calls the asynchronous execution method and
3641
/// collates logged errors in order to determine the success of HelixWorkItems
@@ -85,6 +90,14 @@ private async Task<ITaskItem> PrepareWorkItem(ITaskItem appBundleItem)
8590
return null;
8691
}
8792

93+
bool isDeviceTarget = targets.Contains("device");
94+
string provisioningProfileDest = Path.Combine(appFolderPath, "embedded.mobileprovision");
95+
if (isDeviceTarget && string.IsNullOrEmpty(ProvisioningProfilePath) && !File.Exists(provisioningProfileDest))
96+
{
97+
Log.LogError("ProvisioningProfilePath parameter not set but required for real device targets!");
98+
return null;
99+
}
100+
88101
// Optional timeout for the how long it takes for the app to be installed, booted and tests start executing
89102
TimeSpan launchTimeout = TimeSpan.FromMinutes(DefaultLaunchTimeoutInMinutes);
90103
if (appBundleItem.TryGetMetadata(LaunchTimeoutPropName, out string launchTimeoutProp))
@@ -110,15 +123,29 @@ private async Task<ITaskItem> PrepareWorkItem(ITaskItem appBundleItem)
110123
Log.LogWarning("The ExpectedExitCode property is ignored in the `ios test` scenario");
111124
}
112125

126+
if (isDeviceTarget)
127+
{
128+
if (!File.Exists(provisioningProfileDest))
129+
{
130+
Log.LogMessage("Adding provisioning profile into the app bundle");
131+
File.Copy(ProvisioningProfilePath, provisioningProfileDest);
132+
}
133+
else
134+
{
135+
Log.LogMessage("Bundle already contains a provisioning profile");
136+
}
137+
}
138+
113139
string appName = Path.GetFileName(appBundleItem.ItemSpec);
114140
string command = GetHelixCommand(appName, targets, testTimeout, launchTimeout, includesTestRunner, expectedExitCode);
141+
string payloadArchivePath = await CreateZipArchiveOfFolder(appFolderPath);
115142

116143
Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appFolderPath}, Command: {command}");
117144

118-
return new Microsoft.Build.Utilities.TaskItem(workItemName, new Dictionary<string, string>()
145+
return new Build.Utilities.TaskItem(workItemName, new Dictionary<string, string>()
119146
{
120147
{ "Identity", workItemName },
121-
{ "PayloadArchive", await CreateZipArchiveOfFolder(appFolderPath) },
148+
{ "PayloadArchive", payloadArchivePath },
122149
{ "Command", command },
123150
{ "Timeout", workItemTimeout.ToString() },
124151
});
@@ -158,21 +185,10 @@ private async Task<string> CreateZipArchiveOfFolder(string folderToZip)
158185
ZipFile.CreateFromDirectory(folderToZip, outputZipPath, CompressionLevel.Fastest, includeBaseDirectory: true);
159186

160187
Log.LogMessage($"Adding the Helix job payload scripts into the ziparchive");
161-
await AddFileToPayload(outputZipPath, EntryPointScriptName);
162-
await AddFileToPayload(outputZipPath, RunnerScriptName);
188+
await AddResourceFileToPayload(outputZipPath, EntryPointScriptName);
189+
await AddResourceFileToPayload(outputZipPath, RunnerScriptName);
163190

164191
return outputZipPath;
165192
}
166-
167-
private async Task AddFileToPayload(string payloadArchivePath, string fileName)
168-
{
169-
var thisAssembly = typeof(CreateXHarnessAppleWorkItems).Assembly;
170-
using Stream fileStream = thisAssembly.GetManifestResourceStream($"{thisAssembly.GetName().Name}.tools.xharness_runner.{fileName}");
171-
using FileStream archiveStream = new FileStream(payloadArchivePath, FileMode.Open);
172-
using ZipArchive archive = new ZipArchive(archiveStream, ZipArchiveMode.Update);
173-
ZipArchiveEntry entry = archive.CreateEntry(fileName);
174-
using StreamWriter zipEntryWriter = new StreamWriter(entry.Open());
175-
await fileStream.CopyToAsync(zipEntryWriter.BaseStream);
176-
}
177193
}
178194
}

src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Reflection;
5+
using System.Threading.Tasks;
26
using Microsoft.Build.Framework;
37
using Newtonsoft.Json;
48

@@ -90,5 +94,26 @@ public abstract class XHarnessTaskBase : BaseTask
9094
WorkItemTimeout: workItemTimeout,
9195
ExpectedExitCode: expectedExitCode);
9296
}
97+
98+
protected static async Task AddResourceFileToPayload(string payloadArchivePath, string resourceFileName, string targetFileName = null)
99+
{
100+
using Stream fileStream = GetResourceFileContent(resourceFileName);
101+
await AddToPayloadArchive(payloadArchivePath, targetFileName ?? resourceFileName, fileStream);
102+
}
103+
104+
protected static async Task AddToPayloadArchive(string payloadArchivePath, string targetFilename, Stream content)
105+
{
106+
using FileStream archiveStream = new FileStream(payloadArchivePath, FileMode.Open);
107+
using ZipArchive archive = new ZipArchive(archiveStream, ZipArchiveMode.Update);
108+
ZipArchiveEntry entry = archive.CreateEntry(targetFilename);
109+
using Stream targetStream = entry.Open();
110+
await content.CopyToAsync(targetStream);
111+
}
112+
113+
protected static Stream GetResourceFileContent(string resourceFileName)
114+
{
115+
Assembly thisAssembly = typeof(XHarnessTaskBase).Assembly;
116+
return thisAssembly.GetManifestResourceStream($"{thisAssembly.GetName().Name}.tools.xharness_runner.{resourceFileName}");
117+
}
93118
}
94119
}

src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/Readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ You can configure the execution further via MSBuild properties:
115115
</PropertyGroup>
116116
```
117117

118+
#### Targeting real iOS/tvOS devices
119+
120+
To deploy an app bundle to a real device, the app bundle needs to be signed before the deployment.
121+
The Helix machines, that have devices attached to them, already contain the signing certificates and a provisioning profile will be downloaded as part of the job.
122+
123+
When using the Helix SDK and targeting real devices:
124+
- You have to ideally supply a non-signed app bundle - the app will be signed for you on the Helix machine where your job gets executed
125+
- Only the basic set of app permissions are supported at the moment and we cannot re-sign an app that was already signed with a different set of permissions
126+
- Bundle id has to start with `net.dot.` since we only support those application IDs at the moment
127+
118128
### Android .apk payloads
119129

120130
To execute .apks, declare one or more `XHarnessApkToTest` items:

src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
<_HelixMonoQueueTargets>$(_HelixMonoQueueTargets);$(MSBuildThisFileDirectory)XHarnessRunner.targets</_HelixMonoQueueTargets>
77

88
<XHarnessPackageSource Condition=" '$(XHarnessPackageSource)' == '' ">https://dnceng.pkgs.visualstudio.com/public/_packaging/dotnet-eng/nuget/v3/index.json</XHarnessPackageSource>
9+
10+
<!-- Needed for app signing and tied to certificates installed in Helix machines -->
11+
<XHarnessAppleProvisioningProfileUrl>https://netcorenativeassets.blob.core.windows.net/resource-packages/external/ios/NET_Apple_Development_iOS.mobileprovision</XHarnessAppleProvisioningProfileUrl>
912
</PropertyGroup>
1013
</Project>

src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,18 @@
102102
<Target Name="CreateAppleWorkItems"
103103
Condition=" '@(XHarnessAppBundleToTest)' != '' "
104104
BeforeTargets="CoreTest">
105+
<DownloadFile SourceUrl="$(XHarnessAppleProvisioningProfileUrl)"
106+
Condition=" '$(XHarnessAppleProvisioningProfileUrl)' != '' "
107+
DestinationFolder="$(ArtifactsTmpDir)"
108+
SkipUnchangedFiles="True"
109+
Retries="5">
110+
<Output TaskParameter="DownloadedFile" ItemName="_XHarnessProvisioningProfile" />
111+
</DownloadFile>
112+
105113
<CreateXHarnessAppleWorkItems AppBundles="@(XHarnessAppBundleToTest)"
106-
IsPosixShell="$(IsPosixShell)"
107-
XcodeVersion="$(XHarnessXcodeVersion)">
114+
IsPosixShell="$(IsPosixShell)"
115+
XcodeVersion="$(XHarnessXcodeVersion)"
116+
ProvisioningProfilePath="@(_XHarnessProvisioningProfile)">
108117
<Output TaskParameter="WorkItems" ItemName="HelixWorkItem"/>
109118
</CreateXHarnessAppleWorkItems>
110119
</Target>

src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,47 @@ else
9999
xcode_path="/Applications/Xcode${xcode_version/./}.app"
100100
fi
101101

102-
# Start the simulator if it is not running already
103-
simulator_app="$xcode_path/Contents/Developer/Applications/Simulator.app"
104-
open -a "$simulator_app"
102+
# Signing
103+
if [ "$targets" == 'ios-device' ] || [ "$targets" == 'tvos-device' ]; then
104+
echo "Real device target detected, application will be signed"
105+
106+
provisioning_profile="$app/embedded.mobileprovision"
107+
if [ ! -f "$provisioning_profile" ]; then
108+
echo "No embedded provisioning profile found at $provisioning_profile! Failed to sign the app!"
109+
exit 21
110+
fi
111+
112+
# Unlock the keychain with certs
113+
keychain_name='signing-certs.keychain-db'
114+
keychain_password=$(cat ~/.config/keychain)
115+
116+
security list-keychains | grep "$keychain_name"
117+
result=$?
118+
if [ $result != 0 ]; then
119+
echo "Keychain '$keychain_name' was not found"
120+
exit 22
121+
fi
122+
123+
security find-identity -vp codesigning "$keychain_name" | grep " 0 valid identities found"
124+
result=$?
125+
if [ $result == 0 ]; then
126+
echo "No valid signing identities found in the keychain"
127+
exit 23
128+
fi
129+
130+
security unlock-keychain -p "$keychain_password" "$keychain_name"
131+
132+
# Generate entitlements file
133+
security cms -D -i "$provisioning_profile" > provision.plist
134+
/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' provision.plist > entitlements.plist
135+
136+
# Sign the app
137+
/usr/bin/codesign -v --force --sign "Apple Development" --keychain "$keychain_name" --entitlements entitlements.plist "$app"
138+
else
139+
# Start the simulator if it is not running already
140+
simulator_app="$xcode_path/Contents/Developer/Applications/Simulator.app"
141+
open -a "$simulator_app"
142+
fi
105143

106144
export XHARNESS_DISABLE_COLORED_OUTPUT=true
107145
export XHARNESS_LOG_WITH_TIMESTAMPS=true
@@ -110,12 +148,12 @@ export XHARNESS_LOG_WITH_TIMESTAMPS=true
110148
# which come from outside and are appeneded behind "--" and forwarded to the iOS application from XHarness.
111149
# shellcheck disable=SC2086,SC2090
112150
dotnet exec "$xharness_cli_path" apple $command \
113-
--app="$app" \
114-
--output-directory="$output_directory" \
115-
--targets="$targets" \
116-
--timeout="$timeout" \
117-
--xcode="$xcode_path" \
118-
-v \
151+
--app="$app" \
152+
--output-directory="$output_directory" \
153+
--targets="$targets" \
154+
--timeout="$timeout" \
155+
--xcode="$xcode_path" \
156+
-v \
119157
$app_arguments
120158

121159
exit_code=$?
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<Project DefaultTargets="Test">
2+
<!-- See UnitTests.proj in above folder for why this is included directly-from-repo -->
3+
<Import Project="$(MSBuildThisFileDirectory)\..\src\Microsoft.DotNet.Helix\Sdk\sdk\Sdk.props"/>
4+
5+
<!--
6+
This is a project used in integration tests of Arcade.
7+
It tests sending iOS (XHarness) workloads using the Helix SDK.
8+
It builds two mock projects that do not build iOS apps but only downloads them from a storage account.
9+
-->
10+
11+
<PropertyGroup>
12+
<MicrosoftDotNetHelixSdkTasksAssembly Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)../artifacts/bin/Microsoft.DotNet.Helix.Sdk/$(Configuration)/netcoreapp2.1/publish/Microsoft.DotNet.Helix.Sdk.dll</MicrosoftDotNetHelixSdkTasksAssembly>
13+
<MicrosoftDotNetHelixSdkTasksAssembly Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)../artifacts/bin/Microsoft.DotNet.Helix.Sdk/$(Configuration)/net472/publish/Microsoft.DotNet.Helix.Sdk.dll</MicrosoftDotNetHelixSdkTasksAssembly>
14+
</PropertyGroup>
15+
16+
<PropertyGroup>
17+
<HelixType>test/product/</HelixType>
18+
<TestRunNamePrefix>$(AGENT_JOBNAME)</TestRunNamePrefix>
19+
<IncludeXHarnessCli>true</IncludeXHarnessCli>
20+
<EnableXUnitReporter>true</EnableXUnitReporter>
21+
<EnableAzurePipelinesReporter>true</EnableAzurePipelinesReporter>
22+
<HelixBaseUri>https://helix.dot.net</HelixBaseUri>
23+
</PropertyGroup>
24+
25+
<!-- Test project which builds app bundle to run via XHarness -->
26+
<ItemGroup>
27+
<XHarnessAppleProject Include="$(MSBuildThisFileDirectory)XHarness\XHarness.RunAppBundle.Device.proj" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<HelixTargetQueue Include="osx.1015.amd64.iphone.open" />
32+
</ItemGroup>
33+
34+
<PropertyGroup Condition=" '$(HelixAccessToken)' == '' ">
35+
<IsExternal>true</IsExternal>
36+
<Creator>$(BUILD_SOURCEVERSIONAUTHOR)</Creator>
37+
<Creator Condition=" '$(Creator)' == ''">anon</Creator>
38+
</PropertyGroup>
39+
40+
<!-- Useless stuff to make Arcade SDK happy -->
41+
<PropertyGroup>
42+
<Language>msbuild</Language>
43+
</PropertyGroup>
44+
45+
<Target Name="Pack"/>
46+
47+
<!-- See UnitTests.proj in above folder for why this is included directly-from-repo -->
48+
<Import Project="$(MSBuildThisFileDirectory)\..\src\Microsoft.DotNet.Helix\Sdk\sdk\Sdk.targets"/>
49+
</Project>

0 commit comments

Comments
 (0)