Skip to content

Commit 2321e01

Browse files
authored
[net10.0] [msbuild] Only decompress binding resource packages remotely. Fixes #23346. (#23859)
This hopefully fixes a problem where the build would run into MAX_PATH issues on Windows, because now we won't be extracting any xcframework files on Windows at all. Also: * Change the default to always create compressed binding resource packages. This will ensure NuGets will successfully extract on Windows, because they'll contain the compressed binding resource package, instead of the individual files. * Speed up the ResolveNativeReferences task: * Only run it if there's anything to do. * Only pass paths that contain references to resolve - this will speed up the build on Windows, because it massively reduces the amount of files that have to be transferred to the remote mac. Fixes #23346.
1 parent 624eac3 commit 2321e01

File tree

22 files changed

+394
-54
lines changed

22 files changed

+394
-54
lines changed

docs/building-apps/build-properties.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ The `CompressBindingResourcePackage` property specifies whether to create a zip
279279

280280
The possible values are:
281281

282-
* `auto`: create a zip file if a native reference contains symlinks (which is typical on macOS and Mac Catalyst, but rare on iOS and tvOS).
283-
* `true`: create a zipe file
282+
* `auto`: automatically decide the best option (currently a zip file is always created, but once Visual Studio supports long paths on Windows, this may change to only zip binding resource packages with symlinks).
283+
* `true`: create a zip file
284284
* `false`: create a directory
285285

286286
The default is `auto`.

dotnet/targets/Xamarin.Shared.Sdk.targets

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -918,10 +918,44 @@
918918
</ItemGroup>
919919
</Target>
920920

921+
<!-- When copying files into the app bundle, the final paths can be *long*. This is not a problem on macOS, but Windows still lives in a world where MAX_PATH is unreasonable,
922+
so we have to come up with some hopefully not too unreasonable workarounds.
923+
924+
We want the _CopyDirectoriesToBundle task to have correct Inputs and Outputs (to support incremental builds properly), but to avoid ending up
925+
with Outputs paths that cause problems on Windows, we use a stamp file as the Output, and the stamp file is a hashed version of the actual Output location
926+
927+
This target will compute that stamp file location.
928+
-->
929+
<Target Name="_CreateStampLocationForCopyDirectoriesToBundle">
930+
<ComputeHashForItems
931+
Input="@(_DirectoriesToPublish)"
932+
InputMetadata="Identity;TargetDirectory"
933+
OutputMetadata="StampHash"
934+
>
935+
<Output TaskParameter="Output" ItemName="_DirectoriesToPublishWithStampHash" />
936+
</ComputeHashForItems>
937+
<ItemGroup>
938+
<_DirectoriesToPublish />
939+
<_DirectoriesToPublish Include="@(_DirectoriesToPublishWithStampHash)">
940+
<StampLocation>$(DeviceSpecificIntermediateOutputPath)copieddirectories\%(_DirectoriesToPublishWithStampHash.StampHash)</StampLocation>
941+
</_DirectoriesToPublish>
942+
943+
<_StampFilesToDelete Include="@(_DirectoriesToPublish -> '%(StampLocation)')" Condition="!Exists('%(_DirectoriesToPublish.TargetDirectory)')">
944+
<SourceDirectory>%(_DirectoriesToPublish.SourceDirectory)</SourceDirectory>
945+
<TargetDirectory>%(_DirectoriesToPublish.TargetDirectory)</TargetDirectory>
946+
</_StampFilesToDelete>
947+
</ItemGroup>
948+
949+
<Delete
950+
SessionId="$(BuildSessionId)"
951+
Files="@(_StampFilesToDelete)"
952+
/>
953+
</Target>
954+
921955
<Target Name="_CopyDirectoriesToBundle"
922-
DependsOnTargets="_CollectDecompressedPlugins;_ComputeFrameworkFilesToPublish;_CollectDecompressedXpcServices"
956+
DependsOnTargets="_CollectDecompressedPlugins;_ComputeFrameworkFilesToPublish;_CollectDecompressedXpcServices;_CreateStampLocationForCopyDirectoriesToBundle"
923957
Inputs="@(_DirectoriesToPublish)"
924-
Outputs="@(_DirectoriesToPublish -> '%(TargetDirectory)/%(Filename)')"
958+
Outputs="@(_DirectoriesToPublish -> '%(StampLocation)')"
925959
>
926960

927961
<!-- Ask ditto to not copy architectures we don't care about. This can be overridden by passing 'SkipLibraryThinning=true' -->
@@ -942,6 +976,8 @@
942976
Source="%(_DirectoriesToPublish.SourceDirectory)"
943977
Destination="%(_DirectoriesToPublish.TargetDirectory)"
944978
TouchDestinationFiles="true"
979+
StampFile="%(_DirectoriesToPublish.StampLocation)"
980+
CreateOutputFiles="false"
945981
/>
946982
</Target>
947983

@@ -2168,14 +2204,16 @@
21682204
</ItemGroup>
21692205

21702206
<!-- resolve any .xcframeworks and binding resource packages -->
2171-
<!-- Skip doing this when not building on macOS if we're not a remoteable platform (i.e. iOS), because we want such builds to succeed (even if they produce non-working output), and ResolveNativeReferences will fail if running into symlinks (which happens for macOS and Mac Catalyst builds) -->
2207+
<ItemGroup>
2208+
<_NativeReferencesToResolve Include="@(_UnresolvedXCFrameworks);@(_AppleBindingResourcePackage);@(_CompressedAppleFrameworks);@(_CompressedAppleBindingResourcePackage)" />
2209+
</ItemGroup>
21722210
<ResolveNativeReferences
21732211
SessionId="$(BuildSessionId)"
2174-
Condition="'$(IsMacEnabled)' == 'true' And '$(_IsNonMacOSBuildForNonRemotablePlatform)' != 'true'"
2212+
Condition="'$(IsMacEnabled)' == 'true' And @(_NativeReferencesToResolve->Count()) &gt; 0"
21752213
Architectures="$(TargetArchitectures)"
21762214
FrameworksDirectory="$(_AppFrameworksRelativePath)"
21772215
IntermediateOutputPath="$(DeviceSpecificIntermediateOutputPath)"
2178-
NativeReferences="@(_UnresolvedXCFrameworks);@(_AppleBindingResourcePackage);@(_CompressedAppleFrameworks);@(_CompressedAppleBindingResourcePackage)"
2216+
NativeReferences="@(_NativeReferencesToResolve)"
21792217
SdkIsSimulator="$(_SdkIsSimulator)"
21802218
TargetFrameworkMoniker="$(_ComputedTargetFrameworkMoniker)"
21812219
>

msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,4 +1613,12 @@
16131613
{1}: the name of the property that is empty (SourceFiles, Items, FrameworkToPublish, etc.).
16141614
</comment>
16151615
</data>
1616+
1617+
<data name="W7160" xml:space="preserve">
1618+
<value>Creating a compressed binding resource package to avoid MAX_PATH problems on Windows.</value>
1619+
</data>
1620+
1621+
<data name="W7161" xml:space="preserve">
1622+
<value>Not creating a compressed binding resource package, because all the native references are already compressed.</value>
1623+
</data>
16161624
</root>

msbuild/Xamarin.MacDev.Tasks/Decompress.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ public static bool IsCompressed (string path)
2929
return path.EndsWith (".zip", StringComparison.OrdinalIgnoreCase);
3030
}
3131

32+
static string CanonicalizeZipEntryPath (string path)
33+
{
34+
// The directory separator character is supposed to be '/' on all platforms in zip files,
35+
// but that's not always the case, so canonicalize the path.
36+
return path.Replace ('\\', '/');
37+
}
38+
39+
static ZipArchiveEntry? GetCanonicalizedEntry (ZipArchive archive, string path)
40+
{
41+
var canonicalizedPath = CanonicalizeZipEntryPath (path);
42+
return archive.Entries.SingleOrDefault (e => string.Equals (CanonicalizeZipEntryPath (e.FullName), canonicalizedPath, StringComparison.Ordinal));
43+
}
44+
3245
/// <summary>
3346
/// Finds a file from either a directory or a zip file.
3447
/// </summary>
@@ -45,7 +58,7 @@ public static bool IsCompressed (string path)
4558
return null;
4659
}
4760
using var zip = ZipFile.OpenRead (resources);
48-
var contentEntry = zip.GetEntry (relativeFilePath.Replace ('\\', '/')); // directory separator character is '/' on all platforms in zip files.
61+
var contentEntry = GetCanonicalizedEntry (zip, relativeFilePath);
4962
if (contentEntry is null) {
5063
log.LogWarning (MSBStrings.W7106 /* Expected a file named '{1}' in the zip file {0}. */, resources, relativeFilePath);
5164
return null;
@@ -144,18 +157,20 @@ static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string r
144157
if (!string.IsNullOrEmpty (resource)) {
145158
using var archive = ZipFile.OpenRead (zip);
146159
resource = resource.Replace ('\\', zipDirectorySeparator);
147-
var entry = archive.GetEntry (resource);
160+
var entry = GetCanonicalizedEntry (archive, resource);
148161
if (entry is null) {
149-
entry = archive.GetEntry (resource + zipDirectorySeparator);
162+
entry = GetCanonicalizedEntry (archive, resource + zipDirectorySeparator);
150163
if (entry is null) {
151164
log.LogError (MSBStrings.E7112 /* Could not find the file or directory '{0}' in the zip file '{1}'. */, resource, zip);
152165
return false;
153166
}
154167
}
155168

156169
var zipPattern = entry.FullName;
157-
if (zipPattern.Length > 0 && zipPattern [zipPattern.Length - 1] == zipDirectorySeparator) {
158-
zipPattern += "*";
170+
if (zipPattern.Length > 0) {
171+
var last = zipPattern [zipPattern.Length - 1];
172+
if (last == '\\' || last == '/')
173+
zipPattern += "*";
159174
}
160175

161176
args.Add (zipPattern);
@@ -178,7 +193,7 @@ static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string
178193
using var archive = ZipFile.OpenRead (zip);
179194
foreach (var entry in archive.Entries) {
180195
cancellationToken?.ThrowIfCancellationRequested ();
181-
var entryPath = entry.FullName;
196+
var entryPath = CanonicalizeZipEntryPath (entry.FullName);
182197
if (entryPath.Length == 0)
183198
continue;
184199

@@ -331,16 +346,17 @@ static bool TryCompressUsingSystemIOCompression (TaskLoggingHelper log, string z
331346

332347
var rootDirLength = workingDirectory.Length + 1;
333348
foreach (var resource in resourcePaths) {
334-
log.LogMessage (MessageImportance.Low, $"Procesing {resource}");
349+
log.LogMessage (MessageImportance.Low, $"Processing {resource}");
335350
if (Directory.Exists (resource)) {
351+
if (rootDirLength < resource.Length) {
352+
var resourceZipName = resource.Substring (rootDirLength);
353+
archive.CreateEntry (CanonicalizeZipEntryPath (resourceZipName) + zipDirectorySeparator);
354+
}
336355
var entries = Directory.GetFileSystemEntries (resource, "*", SearchOption.AllDirectories);
337356
var entriesWithZipName = entries.Select (v => new { Path = v, ZipName = v.Substring (rootDirLength) });
338357
foreach (var entry in entriesWithZipName) {
339358
if (Directory.Exists (entry.Path)) {
340-
if (entries.Where (v => v.StartsWith (entry.Path, StringComparison.Ordinal)).Count () == 1) {
341-
// this is a directory with no files inside, we need to create an entry with a trailing directory separator.
342-
archive.CreateEntry (entry.ZipName + zipDirectorySeparator);
343-
}
359+
archive.CreateEntry (CanonicalizeZipEntryPath (entry.ZipName) + zipDirectorySeparator);
344360
} else {
345361
WriteFileToZip (log, archive, entry.Path, entry.ZipName, maxCompression);
346362
}
@@ -359,7 +375,7 @@ static bool TryCompressUsingSystemIOCompression (TaskLoggingHelper log, string z
359375

360376
static void WriteFileToZip (TaskLoggingHelper log, ZipArchive archive, string path, string zipName, bool maxCompression)
361377
{
362-
var zipEntry = archive.CreateEntry (zipName, maxCompression ? SmallestCompressionLevel : CompressionLevel.Optimal);
378+
var zipEntry = archive.CreateEntry (CanonicalizeZipEntryPath (zipName), maxCompression ? SmallestCompressionLevel : CompressionLevel.Optimal);
363379
using var fs = File.OpenRead (path);
364380
using var zipStream = zipEntry.Open ();
365381
fs.CopyTo (zipStream);

msbuild/Xamarin.MacDev.Tasks/Tasks/Codesign.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,42 @@ bool NeedsCodesign (ITaskItem? [] sortedItems, int index, string stampFileConten
194194
return true;
195195
}
196196

197+
var signatureFile = GetSignatureFileLocation (item);
198+
if (!string.IsNullOrEmpty (signatureFile) && !File.Exists (signatureFile)) {
199+
Log.LogMessage (MessageImportance.Low, "The signature file '{0}' for the item '{0}' does not exist, so the item must be codesigned.", signatureFile, item.ItemSpec);
200+
return true;
201+
}
202+
197203
Log.LogMessage (MessageImportance.Low, "The stamp file '{0}' for the item '{1}' is up-to-date, so the item does not need to be codesigned.", stampFile, item.ItemSpec);
198204
return false;
199205
}
200206

207+
string? GetSignatureFileLocation (ITaskItem? item)
208+
{
209+
var path = item?.ItemSpec;
210+
#if NET
211+
if (string.IsNullOrEmpty (path))
212+
#else
213+
if (path is null || string.IsNullOrEmpty (path))
214+
#endif
215+
return null;
216+
217+
if (path.EndsWith (".framework", StringComparison.OrdinalIgnoreCase) || path.EndsWith (".xpc", StringComparison.OrdinalIgnoreCase)) {
218+
switch (Platform) {
219+
case ApplePlatform.iOS:
220+
case ApplePlatform.TVOS:
221+
return Path.Combine (path, "_CodeSignature", "CodeResources");
222+
case ApplePlatform.MacOSX:
223+
case ApplePlatform.MacCatalyst:
224+
return Path.Combine (path, "Versions", "A", "_CodeSignature", "CodeResources");
225+
default:
226+
throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform));
227+
}
228+
}
229+
230+
return null;
231+
}
232+
201233
bool ParseBoolean (ITaskItem item, string metadataName, bool fallbackValue)
202234
{
203235
var metadataValue = item.GetMetadata (metadataName);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Security.Cryptography;
8+
using System.Text;
9+
using System.Threading;
10+
11+
using Microsoft.Build.Framework;
12+
using Microsoft.Build.Utilities;
13+
14+
using Xamarin.Localization.MSBuild;
15+
using Xamarin.Messaging.Build.Client;
16+
using Xamarin.Utils;
17+
18+
namespace Xamarin.MacDev.Tasks {
19+
// This task will iterate over each input item, compute a hash value for all the specified metadata in the input items, and then set the specified output metadata to the hashed value
20+
public class ComputeHashForItems : XamarinTask {
21+
[Required]
22+
public ITaskItem [] Input { get; set; } = Array.Empty<ITaskItem> ();
23+
24+
// The metadata in each input item to use as input for the hash algorithm.
25+
[Required]
26+
public ITaskItem [] InputMetadata { get; set; } = Array.Empty<ITaskItem> ();
27+
28+
// The name of the metadata where to store the computed hashed value
29+
[Required]
30+
public string OutputMetadata { get; set; } = string.Empty;
31+
32+
// The output items. This will be Input, where each item will also have 'OutputMetadata' set to the computed hash value.
33+
[Output]
34+
public ITaskItem [] Output { get; set; } = Array.Empty<ITaskItem> ();
35+
36+
public override bool Execute ()
37+
{
38+
if (Input.Length == 0)
39+
return true;
40+
41+
using var sha = CreateHashAlgorithm ();
42+
43+
var buffer = new List<byte> ();
44+
for (var i = 0; i < Input.Length; i++) {
45+
var input = Input [i];
46+
buffer.Clear ();
47+
foreach (var im in InputMetadata) {
48+
buffer.AddRange (Encoding.UTF8.GetBytes (input.GetMetadata (im.ItemSpec)));
49+
}
50+
var hashBytes = sha.ComputeHash (buffer.ToArray ());
51+
var hash = string.Join ("", hashBytes.Select (b => $"{b:x2}"));
52+
input.SetMetadata (OutputMetadata, hash);
53+
}
54+
55+
Output = Input;
56+
57+
return !Log.HasLoggedErrors;
58+
}
59+
60+
HashAlgorithm CreateHashAlgorithm ()
61+
{
62+
return SHA256.Create ();
63+
}
64+
}
65+
}

msbuild/Xamarin.MacDev.Tasks/Tasks/CreateBindingResourcePackage.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,16 @@ public override bool Execute ()
6565
} else if (string.Equals (Compress, "false", StringComparison.OrdinalIgnoreCase)) {
6666
compress = false;
6767
} else if (string.Equals (Compress, "auto", StringComparison.OrdinalIgnoreCase)) {
68-
compress = ContainsSymlinks (NativeReferences);
69-
if (compress)
68+
if (ContainsSymlinks (NativeReferences)) {
7069
Log.LogMessage (MessageImportance.Low, MSBStrings.W7085 /* "Creating a compressed binding resource package because there are symlinks in the input." */);
70+
compress = true;
71+
} else if (NativeReferences.All (v => v.ItemSpec.EndsWith (".zip", StringComparison.OrdinalIgnoreCase))) {
72+
compress = false;
73+
Log.LogMessage (MessageImportance.Low, MSBStrings.W7161 /* "Not creating a compressed binding resource package, because all the native references are already compressed." */);
74+
} else {
75+
Log.LogMessage (MessageImportance.Low, MSBStrings.W7160 /* "Creating a compressed binding resource package to avoid MAX_PATH problems on Windows." */);
76+
compress = true;
77+
}
7178
} else {
7279
Log.LogError (MSBStrings.E7086 /* "The value '{0}' is invalid for the Compress property. Valid values: 'true', 'false' or 'auto'." */, Compress);
7380
}

0 commit comments

Comments
 (0)