diff --git a/external/Xamarin.MacDev b/external/Xamarin.MacDev index 188089e8e69d..f0e6305bf153 160000 --- a/external/Xamarin.MacDev +++ b/external/Xamarin.MacDev @@ -1 +1 @@ -Subproject commit 188089e8e69df1a271d4a1260a30868c58643936 +Subproject commit f0e6305bf153798374817e8634a3cdcb395631b3 diff --git a/msbuild/Xamarin.MacDev.Tasks/Decompress.cs b/msbuild/Xamarin.MacDev.Tasks/Decompress.cs index b28428f41faa..b4e9a293ef7a 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Decompress.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Decompress.cs @@ -89,7 +89,30 @@ public static bool IsCompressed (string path) /// True if successfully decompressed, false otherwise. public static bool TryDecompress (TaskLoggingHelper log, string zip, string resource, string decompressionDir, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? decompressedResource) { - decompressedResource = Path.Combine (decompressionDir, resource); + return TryDecompress (log, zip, resource, decompressionDir, null, null, createdFiles, cancellationToken, out decompressedResource); + } + + /// + /// Extracts the specified resource (may be either a file or a directory) from the given zip file. + /// A stamp file will be created to avoid re-extracting unnecessarily. + /// + /// Fails if: + /// * The resource is or contains a symlink and we're executing on Windows. + /// * The resource isn't found inside the zip file. + /// + /// + /// The zip to search in + /// The relative path inside the zip to extract (may be a file or a directory). + /// The location on disk to store the extracted results + /// The name of the extracted resource (will be combined with ). The default is . + /// The cancellation token (if any= + /// The location on disk to the extracted resource + /// True if successfully decompressed, false otherwise. + public static bool TryDecompress (TaskLoggingHelper log, string zip, string resource, string decompressionDir, string? decompressionName, UnzipFilter? filter, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? decompressedResource) + { + if (string.IsNullOrEmpty (decompressionName)) + decompressionName = resource; + decompressedResource = Path.Combine (decompressionDir, decompressionName); var stampFile = decompressedResource.TrimEnd ('\\', '/') + ".stamp"; @@ -105,11 +128,11 @@ public static bool TryDecompress (TaskLoggingHelper log, string zip, string reso bool rv; if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, cancellationToken); + rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, filter, cancellationToken); } else if (!string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION"))) { - rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, cancellationToken); + rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, filter, cancellationToken); } else { - rv = TryDecompressUsingUnzip (log, zip, resource, decompressionDir, cancellationToken); + rv = TryDecompressUsingUnzip (log, zip, resource, decompressionDir, filter, cancellationToken); } if (rv) { @@ -132,41 +155,21 @@ public static bool TryDecompress (TaskLoggingHelper log, string zip, string reso // The dir separator character in zip files is always "/", even on Windows const char zipDirectorySeparator = '/'; - static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string resource, string decompressionDir, CancellationToken? cancellationToken) - { - Directory.CreateDirectory (decompressionDir); - var args = new List { - "-u", "-o", - "-d", decompressionDir, - zip, - }; - - if (!string.IsNullOrEmpty (resource)) { - using var archive = ZipFile.OpenRead (zip); - resource = resource.Replace ('\\', zipDirectorySeparator); - var entry = archive.GetEntry (resource); - if (entry is null) { - entry = archive.GetEntry (resource + zipDirectorySeparator); - if (entry is null) { - log.LogError (MSBStrings.E7112 /* Could not find the file or directory '{0}' in the zip file '{1}'. */, resource, zip); - return false; - } - } - - var zipPattern = entry.FullName; - if (zipPattern.Length > 0 && zipPattern [zipPattern.Length - 1] == zipDirectorySeparator) { - zipPattern += "*"; - } - - args.Add (zipPattern); - } + /// + /// A filter to determine whether an entry in a zip file should be extracted or not. + /// Returns the relative target path for the entry (relative to the target directory). + /// + /// The name of the entry inside the zip file. The path separator will always be '/'. + /// Whether the entry is a directory. + /// + public delegate string? UnzipFilter (string entryPath, bool isDirectory); - var rv = XamarinTask.ExecuteAsync (log, "unzip", args, cancellationToken: cancellationToken).Result; - return rv.ExitCode == 0; - } + delegate bool DecompressImplementation (TaskLoggingHelper log, string zip, ZipArchiveEntry entry, string targetPath, CancellationToken? cancellationToken); - static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string zip, string resource, string decompressionDir, CancellationToken? cancellationToken) + static bool TryDecompressFiltered (TaskLoggingHelper log, string zip, string resource, string decompressionDir, UnzipFilter? filter, DecompressImplementation decompress, CancellationToken? cancellationToken) { + log.LogMessage (MessageImportance.Low, $"TryDecompressFiltered (zip={zip}, resource={resource}, decompressionDir={decompressionDir})\n{Environment.StackTrace}"); + var rv = true; // canonicalize input @@ -182,38 +185,35 @@ static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string if (entryPath.Length == 0) continue; - if (string.IsNullOrEmpty (resource)) { - // an empty resource means extract everything, so we want this - } else if (entryPath.StartsWith (resourceAsDir, StringComparison.Ordinal)) { + var isDir = entryPath [entryPath.Length - 1] == zipDirectorySeparator; + var canonicalizedEntryPath = entryPath.Replace (zipDirectorySeparator, Path.DirectorySeparatorChar); + + if (string.IsNullOrEmpty (resource) || canonicalizedEntryPath == resource || canonicalizedEntryPath.StartsWith (resourceAsDir, StringComparison.Ordinal)) { // yep, we want this entry - } else if (entryPath == resource) { - // we want this one too } else { - log.LogMessage (MessageImportance.Low, "Did not extract {0} because it didn't match the resource {1}", entryPath, resource); + log.LogMessage (MessageImportance.Low, "Did not extract {0} because it didn't match the resource {1}", canonicalizedEntryPath, resource); // but otherwise nope continue; } - // Check if the file or directory is a symlink, and show an error if so. Symlinks are only supported - // on non-Windows platforms. - var entryAttributes = ((uint) GetExternalAttributes (entry)) >> 16; - const uint S_IFLNK = 0xa000; // #define S_IFLNK 0120000 /* symbolic link */ - var isSymlink = (entryAttributes & S_IFLNK) == S_IFLNK; - if (isSymlink) { - log.LogError (MSBStrings.E7113 /* Can't process the zip file '{0}' on this platform: the file '{1}' is a symlink. */, zip, entryPath); - rv = false; + var relativeTargetPath = filter is null ? canonicalizedEntryPath : filter (canonicalizedEntryPath, isDir); + if (string.IsNullOrEmpty (relativeTargetPath)) { + log.LogMessage (MessageImportance.Low, "Did not extract {0} because the filter filtered it out.", entryPath); + // but otherwise nope continue; } - var isDir = entryPath [entryPath.Length - 1] == zipDirectorySeparator; - var targetPath = Path.Combine (decompressionDir, entryPath.Replace (zipDirectorySeparator, Path.DirectorySeparatorChar)); + // canonicalize the target path + var targetPath = Path.GetFullPath (Path.Combine (decompressionDir, relativeTargetPath)); + + log.LogMessage (MessageImportance.Low, "Extracting '{0}' to '{1}' => '{2}'.", entryPath, relativeTargetPath, targetPath); - // canonicalize the path - targetPath = Path.GetFullPath (targetPath); // validate that the unzipped file is inside the target directory - var decompressionDirectoryPath = decompressionDir.Trim (Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + var decompressionDirectoryPath = decompressionDir.TrimEnd (Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; if (!targetPath.StartsWith (decompressionDirectoryPath)) { + log.LogMessage (MessageImportance.Low, $"targetPath: {targetPath}"); + log.LogMessage (MessageImportance.Low, $"decompressionDirectoryPath: {decompressionDirectoryPath}"); log.LogWarning (7144, null, MSBStrings.W7144 /* Did not extract {0} because it would write outside the target directory. */, entryPath); continue; } @@ -222,18 +222,110 @@ static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string Directory.CreateDirectory (targetPath); } else { Directory.CreateDirectory (Path.GetDirectoryName (targetPath)); - using var streamWrite = File.OpenWrite (targetPath); - using var streamRead = entry.Open (); + File.Delete (targetPath); + if (!decompress (log, zip, entry, targetPath, cancellationToken)) { + rv = false; + continue; + } + log.LogMessage (MessageImportance.Low, "Extracted {0} into {1}", entryPath, targetPath); + } + } + + return rv; + } + + static bool DecompressFileEntryWithStream (TaskLoggingHelper log, string zip, ZipArchiveEntry entry, string targetPath, CancellationToken? cancellationToken) + { + // Check if the file or directory is a symlink, and show an error if so. Symlinks are only supported + // on non-Windows platforms. + var entryPath = entry.FullName; + var entryAttributes = ((uint) GetExternalAttributes (entry)) >> 16; + const uint S_IFLNK = 0xa000; // #define S_IFLNK 0120000 /* symbolic link */ + var isSymlink = (entryAttributes & S_IFLNK) == S_IFLNK; + if (isSymlink) { + log.LogError (MSBStrings.E7113 /* Can't process the zip file '{0}' on this platform: the file '{1}' is a symlink. */, zip, entryPath); + return false; + } + + using var streamWrite = File.OpenWrite (targetPath); + using var streamRead = entry.Open (); #if NET - streamRead.CopyToAsync (streamWrite, cancellationToken ?? CancellationToken.None).Wait (); + streamRead.CopyToAsync (streamWrite, cancellationToken ?? CancellationToken.None).Wait (); #else - streamRead.CopyToAsync (streamWrite, 81920 /* default buffer size according to docs */, cancellationToken ?? CancellationToken.None).Wait (); + streamRead.CopyToAsync (streamWrite, 81920 /* default buffer size according to docs */, cancellationToken ?? CancellationToken.None).Wait (); #endif - log.LogMessage (MessageImportance.Low, "Extracted {0} into {1}", entryPath, targetPath); + return true; + } + + static bool DecompressFileEntryWithUnzip (TaskLoggingHelper log, string zip, ZipArchiveEntry entry, string targetPath, CancellationToken? cancellationToken) + { + // Check if the file or directory is a symlink, and show an error if so. Symlinks are only supported + // on non-Windows platforms. + var entryPath = entry.FullName; + var targetDirectory = Path.GetDirectoryName (targetPath); + + var args = new List { + "-u", "-o", "-j", + "-d", targetDirectory, + zip, + entryPath, + }; + + var rv = XamarinTask.ExecuteAsync (log, "unzip", args, cancellationToken: cancellationToken).Result; + if (rv.ExitCode != 0) + return false; + + if (entry.Name != Path.GetFileName (targetPath)) + File.Move (Path.Combine (targetDirectory, entry.Name), targetPath); + + return true; + } + + static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string resource, string decompressionDir, UnzipFilter? filter, CancellationToken? cancellationToken) + { + if (filter is null) + return TryDecompressUsingUnzip (log, zip, resource, decompressionDir, cancellationToken); + + return TryDecompressFiltered (log, zip, resource, decompressionDir, filter, DecompressFileEntryWithUnzip, cancellationToken); + } + + // Does not support filtering nor extracting partial contents into a custom directory hierarchy. + static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string resource, string decompressionDir, CancellationToken? cancellationToken) + { + Directory.CreateDirectory (decompressionDir); + var args = new List { + "-u", "-o", + "-d", decompressionDir, + zip, + }; + + if (!string.IsNullOrEmpty (resource)) { + using var archive = ZipFile.OpenRead (zip); + resource = resource.Replace ('\\', zipDirectorySeparator); + var entry = archive.GetEntry (resource); + if (entry is null) { + entry = archive.GetEntry (resource + zipDirectorySeparator); + if (entry is null) { + log.LogError (MSBStrings.E7112 /* Could not find the file or directory '{0}' in the zip file '{1}'. */, resource, zip); + return false; + } } + + var zipPattern = entry.FullName; + if (zipPattern.Length > 0 && zipPattern [zipPattern.Length - 1] == zipDirectorySeparator) { + zipPattern += "*"; + } + + args.Add (zipPattern); } - return rv; + var rv = XamarinTask.ExecuteAsync (log, "unzip", args, cancellationToken: cancellationToken).Result; + return rv.ExitCode == 0; + } + + static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string zip, string resource, string decompressionDir, UnzipFilter? filter, CancellationToken? cancellationToken) + { + return TryDecompressFiltered (log, zip, resource, decompressionDir, filter, DecompressFileEntryWithStream, cancellationToken); } /// diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ResolveNativeReferences.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ResolveNativeReferences.cs index f42cfc369862..c0eea8d62476 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ResolveNativeReferences.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ResolveNativeReferences.cs @@ -16,6 +16,7 @@ using Xamarin.Localization.MSBuild; using Xamarin.Messaging.Build.Client; using Xamarin.Utils; +using System.Text.RegularExpressions; #nullable enable @@ -48,6 +49,11 @@ public class ResolveNativeReferences : XamarinTask, ITaskCallback { [Required] public string? Architectures { get; set; } + public bool UseExperimentalIntermediateDirectory { get; set; } + + // list of regexp of files to exclude when extracting + public ITaskItem [] ExtractionFilters { get; set; } = Array.Empty (); + [Required] public string FrameworksDirectory { get; set; } = string.Empty; @@ -80,6 +86,9 @@ string GetIntermediateDecompressionDir (ITaskItem item) string GetIntermediateDecompressionDir (string item) { + if (UseExperimentalIntermediateDirectory) + return IntermediateOutputPath; + return Path.Combine (IntermediateOutputPath, Path.GetFileName (item)); } @@ -195,7 +204,7 @@ void ProcessNativeReference (ITaskItem item, string name, List native // (compressed) xcframework if (name.EndsWith (".xcframework", StringComparison.OrdinalIgnoreCase) || name.EndsWith (".xcframework.zip", StringComparison.OrdinalIgnoreCase)) { - if (!TryResolveXCFramework (Log, TargetFrameworkMoniker, SdkIsSimulator, Architectures, name, GetIntermediateDecompressionDir (item), createdFiles, cancellationToken, out var nativeLibraryPath)) + if (!TryResolveXCFramework (Log, TargetFrameworkMoniker, SdkIsSimulator, Architectures, name, GetIntermediateDecompressionDir (item), ExtractionFilters, createdFiles, cancellationToken, out var nativeLibraryPath)) return; var nr = new TaskItem (item); SetMetadataNativeLibrary (nr, nativeLibraryPath); @@ -205,7 +214,7 @@ void ProcessNativeReference (ITaskItem item, string name, List native // compressed framework if (name.EndsWith (".framework.zip", StringComparison.OrdinalIgnoreCase)) { - if (!CompressionHelper.TryDecompress (Log, name, Path.GetFileNameWithoutExtension (name), GetIntermediateDecompressionDir (item), createdFiles, cancellationToken, out var frameworkPath)) + if (!TryDecompress (name, Path.GetFileNameWithoutExtension (name), GetIntermediateDecompressionDir (item), createdFiles, cancellationToken, out var frameworkPath)) return; var nr = new TaskItem (item); nr.ItemSpec = GetActualLibrary (frameworkPath); @@ -302,20 +311,20 @@ void ProcessSidecar (ITaskItem r, string resources, List native_frame return; var isCompressed = CompressionHelper.IsCompressed (resources); - XmlDocument document = new XmlDocument (); + var document = new XmlDocument (); document.LoadXmlWithoutNetworkAccess (manifestContents); foreach (XmlNode referenceNode in document.GetElementsByTagName ("NativeReference")) { ITaskItem t = new TaskItem (r); var name = referenceNode.Attributes ["Name"].Value.Trim ('\\', '/'); if (name.EndsWith (".xcframework", StringComparison.Ordinal) || name.EndsWith (".xcframework.zip", StringComparison.Ordinal)) { - if (!TryResolveXCFramework (Log, TargetFrameworkMoniker, SdkIsSimulator, Architectures, resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out var nativeLibraryPath)) + if (!TryResolveXCFramework (Log, TargetFrameworkMoniker, SdkIsSimulator, Architectures, resources, name, GetIntermediateDecompressionDir (resources), ExtractionFilters, createdFiles, cancellationToken, out var nativeLibraryPath)) continue; SetMetadataNativeLibrary (t, nativeLibraryPath); } else if (name.EndsWith (".framework", StringComparison.Ordinal)) { string? frameworkPath; if (!isCompressed) { frameworkPath = Path.Combine (resources, name); - } else if (!CompressionHelper.TryDecompress (Log, resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out frameworkPath)) { + } else if (!TryDecompress (resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out frameworkPath)) { continue; } t.ItemSpec = GetActualLibrary (frameworkPath); @@ -327,7 +336,7 @@ void ProcessSidecar (ITaskItem r, string resources, List native_frame string? dylibPath; if (!isCompressed) { dylibPath = Path.Combine (resources, name); - } else if (!CompressionHelper.TryDecompress (Log, resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out dylibPath)) { + } else if (!TryDecompress (resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out dylibPath)) { continue; } t.ItemSpec = dylibPath; @@ -338,7 +347,7 @@ void ProcessSidecar (ITaskItem r, string resources, List native_frame string? aPath; if (!isCompressed) { aPath = Path.Combine (resources, name); - } else if (!CompressionHelper.TryDecompress (Log, resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out aPath)) { + } else if (!TryDecompress (resources, name, GetIntermediateDecompressionDir (resources), createdFiles, cancellationToken, out aPath)) { continue; } t.ItemSpec = aPath; @@ -375,7 +384,7 @@ void ProcessSidecar (ITaskItem r, string resources, List native_frame /// A full path to the resolved native library within the xcframework. If 'resourcePath' is compressed, this will point to where the native library is decompressed on disk. /// /// True if a native library was successfully found. Otherwise false, and an error will have been printed to the log. - public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFrameworkMoniker, bool isSimulator, string? architectures, string path, string intermediateDecompressionDir, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? nativeLibraryPath) + public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFrameworkMoniker, bool isSimulator, string? architectures, string path, string intermediateDecompressionDir, ITaskItem [] filters, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? nativeLibraryPath) { string resourcePath; string xcframework; @@ -387,7 +396,7 @@ public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFr resourcePath = Path.GetDirectoryName (path); xcframework = Path.GetFileName (path); } - return TryResolveXCFramework (log, targetFrameworkMoniker, isSimulator, architectures, resourcePath, xcframework, intermediateDecompressionDir, createdFiles, cancellationToken, out nativeLibraryPath); + return TryResolveXCFramework (log, targetFrameworkMoniker, isSimulator, architectures, resourcePath, xcframework, intermediateDecompressionDir, filters, createdFiles, cancellationToken, out nativeLibraryPath); } /// @@ -402,7 +411,7 @@ public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFr /// A full path to the resolved native library within the xcframework. If 'resourcePath' is compressed, this will point to where the native library is decompressed on disk. /// /// True if a native library was successfully found. Otherwise false, and an error will have been printed to the log. - public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFrameworkMoniker, bool isSimulator, string? architectures, string resourcePath, string xcframework, string intermediateDecompressionDir, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? nativeLibraryPath) + public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFrameworkMoniker, bool isSimulator, string? architectures, string resourcePath, string xcframework, string intermediateDecompressionDir, ITaskItem [] filters, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? nativeLibraryPath) { nativeLibraryPath = null; @@ -412,29 +421,30 @@ public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFr var isCompressed = CompressionHelper.IsCompressed (resourcePath); var xcframeworkPath = isCompressed ? resourcePath : Path.Combine (resourcePath, xcframework); - if (!TryResolveXCFramework (log, plist, xcframeworkPath, targetFrameworkMoniker, isSimulator, architectures!, cancellationToken, out var nativeLibraryRelativePath)) + if (!TryResolveXCFramework (log, plist, xcframeworkPath, targetFrameworkMoniker, isSimulator, architectures!, cancellationToken, out var nativeRelativePath)) return false; if (!isCompressed && CompressionHelper.IsCompressed (xcframework)) { var zipPath = Path.Combine (resourcePath, xcframework); var xcframeworkName = Path.GetFileNameWithoutExtension (xcframework); - if (!CompressionHelper.TryDecompress (log, zipPath, xcframeworkName, intermediateDecompressionDir, createdFiles, cancellationToken, out var decompressedXcframeworkPath)) + var resource = Path.Combine (xcframeworkName, nativeRelativePath); + if (!TryDecompress (log, zipPath, resource, filters, intermediateDecompressionDir, createdFiles, cancellationToken, out var decompressedFrameworkPath)) return false; - nativeLibraryPath = Path.Combine (intermediateDecompressionDir, xcframeworkName, nativeLibraryRelativePath); + nativeLibraryPath = decompressedFrameworkPath; return true; } if (!isCompressed) { - nativeLibraryPath = Path.Combine (resourcePath, xcframework, nativeLibraryRelativePath); + nativeLibraryPath = Path.Combine (resourcePath, xcframework, nativeRelativePath); return true; } - var zipResource = Path.Combine (xcframework, Path.GetDirectoryName (nativeLibraryRelativePath)); - if (!CompressionHelper.TryDecompress (log, resourcePath, zipResource, intermediateDecompressionDir, createdFiles, cancellationToken, out var decompressedPath)) + var zipResource = Path.Combine (xcframework, nativeRelativePath); + if (!TryDecompress (log, resourcePath, zipResource, filters, intermediateDecompressionDir, createdFiles, cancellationToken, out var decompressedPath)) return false; - nativeLibraryPath = Path.Combine (intermediateDecompressionDir, xcframework, nativeLibraryRelativePath); + nativeLibraryPath = Path.Combine (intermediateDecompressionDir, Path.GetFileName (zipResource)); return true; } catch (Exception e) { @@ -451,14 +461,16 @@ public static bool TryResolveXCFramework (TaskLoggingHelper log, string targetFr /// The log to log any errors and/or warnings. /// The plist inside the xcframework. /// The path to the xcframework. This is only used for error messages, so it can also point to a compressed xcframework. - /// If we're targeting the simulator /// The target framework moniker. - /// The target architectures - /// A relative path to the resolved native library within the xcframework. + /// If we're targeting the simulator. + /// The target architectures. + /// A cancellation token. + /// The relative path to the library (.framework/.dylib/.a) inside the xcframework. /// True if a native library was successfully found. Otherwise false, and an error will have been printed to the log. - public static bool TryResolveXCFramework (TaskLoggingHelper log, PDictionary plist, string xcframeworkPath, string targetFrameworkMoniker, bool isSimulator, string architectures, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? nativeLibraryPath) + public static bool TryResolveXCFramework (TaskLoggingHelper log, PDictionary plist, string xcframeworkPath, string targetFrameworkMoniker, bool isSimulator, string architectures, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? nativeRelativePath) { - nativeLibraryPath = null; + nativeRelativePath = null; + var platform = PlatformFrameworkHelper.GetFramework (targetFrameworkMoniker); string platformName; switch (platform) { @@ -522,9 +534,9 @@ public static bool TryResolveXCFramework (TaskLoggingHelper log, PDictionary pli return false; } } - var library_path = (PString?) item ["LibraryPath"]; - var library_identifier = (PString?) item ["LibraryIdentifier"]; - nativeLibraryPath = GetActualLibrary (Path.Combine (library_identifier!, library_path!)); + var library_path = (string) (PString) item ["LibraryPath"]!; + var library_identifier = (string?) (PString?) item ["LibraryIdentifier"]; + nativeRelativePath = Path.Combine (library_identifier!, library_path); return true; } @@ -532,6 +544,43 @@ public static bool TryResolveXCFramework (TaskLoggingHelper log, PDictionary pli return false; } + bool TryDecompress (string zip, string resource, string decompressionDir, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? decompressedResource) + { + return TryDecompress (Log, zip, resource, ExtractionFilters, decompressionDir, createdFiles, cancellationToken, out decompressedResource); + } + + static bool TryDecompress (TaskLoggingHelper log, string zip, string resource, IEnumerable? extractionFilters, string decompressionDir, List createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? decompressedResource) + { + var regexps = extractionFilters?. + Select (v => v.ItemSpec)?. + Select (v => new Regex (v, RegexOptions.Compiled))?. + ToArray (); + + var decompressionName = Path.GetFileName (resource); + var directoryComponentToRemove = Path.GetDirectoryName (resource); + + if (!string.IsNullOrEmpty (directoryComponentToRemove)) + directoryComponentToRemove = directoryComponentToRemove.TrimEnd ('\\', '/') + "/"; + + var filter = new CompressionHelper.UnzipFilter ((entryPath, isDirectory) => { + if (regexps is not null) { + foreach (var exp in regexps) { + if (exp.IsMatch (entryPath)) { + log.LogMessage (MessageImportance.Low, "Did not extract {0} because the filter '{1}' filtered it out.", entryPath, exp); + return null; + } + } + } + + if (string.IsNullOrEmpty (resource) || string.IsNullOrEmpty (decompressionName)) + return entryPath; + + var targetPath = entryPath.Substring (directoryComponentToRemove.Length); + return targetPath; + }); + + return CompressionHelper.TryDecompress (log, zip, resource, decompressionDir, decompressionName, filter, createdFiles, cancellationToken, out decompressedResource); + } public void Cancel () { if (ShouldExecuteRemotely ()) { diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 984ba1c916a5..1dff2f12cd80 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -164,6 +164,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. References="@(ReferencePath)" SdkIsSimulator="$(_SdkIsSimulator)" TargetFrameworkMoniker="$(_ComputedTargetFrameworkMoniker)" + UseExperimentalIntermediateDirectory="$(_UseExperimentalIntermediateDirectory)" > diff --git a/tests/dotnet/UnitTests/ProjectTest.cs b/tests/dotnet/UnitTests/ProjectTest.cs index 8354884182b2..d08feafab7e7 100644 --- a/tests/dotnet/UnitTests/ProjectTest.cs +++ b/tests/dotnet/UnitTests/ProjectTest.cs @@ -2437,6 +2437,43 @@ public void CompressedXCFrameworkInBindingProjectApp (ApplePlatform platform) var appExecutable = GetNativeExecutable (platform, appPath); Assert.That (appExecutable, Does.Exist, "There is an executable"); + var objDir = GetObjDir (project_path, platform, runtimeIdentifiers); + var sidecarDir = Path.Combine (objDir, "BindingWithCompressedXCFramework.resources"); + var frameworks = new string [] { + "XStaticArTest", + "XStaticObjectTest", + "XTest", + }; + foreach (var fw in frameworks) { + var fwDir = Path.Combine (sidecarDir, $"{fw}.framework"); + var stampFile = Path.Combine (sidecarDir, $"{fw}.framework.stamp"); + Assert.That (stampFile, Does.Exist.IgnoreDirectories, "Stamp file"); + Assert.That (fwDir, Does.Exist.IgnoreFiles, "Framework directory"); + // assert the maximum length of any file extracted from the compressed sidecar (we don't want the max length to be too high, because we run into MAX_PATH issues on Windows) + var allFilesInFrameworkDirectory = Directory.GetFileSystemEntries (fwDir, "*", SearchOption.AllDirectories); + var maxLength = allFilesInFrameworkDirectory.Select (v => v.Length).Max (); + foreach (var fw2 in allFilesInFrameworkDirectory.OrderBy (v => v)) + Console.WriteLine ($"{fw2.Length}: {fw2}"); + maxLength -= fwDir.Length; + maxLength -= fw.Length; + maxLength -= 1; // directory separator + Console.WriteLine ($"fwDir: {fwDir}"); + Console.WriteLine ($"fw: {fw} {fw.Length}"); + Assert.That (maxLength, Is.GreaterThanOrEqualTo (0), "Length A"); + switch (platform) { + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + Assert.That (maxLength, Is.LessThanOrEqualTo ("/Versions/A/Resources/Info.plist".Length), "Length B"); + break; + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + Assert.That (maxLength, Is.LessThanOrEqualTo ("/Info.plist".Length), "Length B"); + break; + default: + throw new NotImplementedException (); + } + } + if (CanExecute (platform, properties)) { ExecuteWithMagicWordAndAssert (appExecutable); } diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ResolveNativeReferencesTaskTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ResolveNativeReferencesTaskTest.cs index ec39d36da20e..cc52a15a821b 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ResolveNativeReferencesTaskTest.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ResolveNativeReferencesTaskTest.cs @@ -1,10 +1,16 @@ using System; +using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using System.IO.Enumeration; +using System.Linq; using System.Threading; - +using Microsoft.Build.Experimental.ProjectCache; +using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using NUnit.Framework; +using Xamarin.Tests; using Xamarin.Utils; #nullable enable @@ -12,52 +18,774 @@ namespace Xamarin.MacDev.Tasks.Tests { [TestFixture] - public class ResolveNativeReferencesTaskTest { + public class ResolveNativeReferencesTaskTest : TestBase { TaskLoggingHelper log = new TaskLoggingHelper (new TestEngine (), "ResolveNativeReferences"); // single arch request (subset are fine) - [TestCase (TargetFramework.DotNet_iOS_String, false, "arm64", "ios-arm64/Universal.framework/Universal")] - [TestCase (TargetFramework.DotNet_iOS_String, true, "x86_64", "ios-arm64_x86_64-simulator/Universal.framework/Universal")] // subset - [TestCase (TargetFramework.DotNet_MacCatalyst_String, false, "x86_64", "ios-arm64_x86_64-maccatalyst/Universal.framework/Universal")] // subset - [TestCase (TargetFramework.DotNet_tvOS_String, false, "arm64", "tvos-arm64/Universal.framework/Universal")] - [TestCase (TargetFramework.DotNet_tvOS_String, true, "x86_64", "tvos-arm64_x86_64-simulator/Universal.framework/Universal")] // subset - [TestCase (TargetFramework.DotNet_macOS_String, false, "x86_64", "macos-arm64_x86_64/Universal.framework/Universal")] // subset + [TestCase (TargetFramework.DotNet_iOS_String, false, "arm64", "ios-arm64/Universal.framework/Universal", "ios-arm64/Universal.framework")] + [TestCase (TargetFramework.DotNet_iOS_String, true, "x86_64", "ios-arm64_x86_64-simulator/Universal.framework/Universal", "ios-arm64_x86_64-simulator/Universal.framework")] // subset + [TestCase (TargetFramework.DotNet_MacCatalyst_String, false, "x86_64", "ios-arm64_x86_64-maccatalyst/Universal.framework/Universal", "ios-arm64_x86_64-maccatalyst/Universal.framework")] // subset + [TestCase (TargetFramework.DotNet_tvOS_String, false, "arm64", "tvos-arm64/Universal.framework/Universal", "tvos-arm64/Universal.framework")] + [TestCase (TargetFramework.DotNet_tvOS_String, true, "x86_64", "tvos-arm64_x86_64-simulator/Universal.framework/Universal", "tvos-arm64_x86_64-simulator/Universal.framework")] // subset + [TestCase (TargetFramework.DotNet_macOS_String, false, "x86_64", "macos-arm64_x86_64/Universal.framework/Universal", "macos-arm64_x86_64/Universal.framework")] // subset // multiple arch request (all must be present) - [TestCase (TargetFramework.DotNet_macOS_String, false, "x86_64, arm64", "macos-arm64_x86_64/Universal.framework/Universal")] + [TestCase (TargetFramework.DotNet_macOS_String, false, "x86_64, arm64", "macos-arm64_x86_64/Universal.framework/Universal", "macos-arm64_x86_64/Universal.framework")] // failure to resolve requested architecture - [TestCase (TargetFramework.DotNet_iOS_String, true, "i386, x86_64", null)] // i386 not available + [TestCase (TargetFramework.DotNet_iOS_String, true, "i386, x86_64", null, null)] // i386 not available // failure to resolve mismatched variant - [TestCase (TargetFramework.DotNet_macOS_String, true, "x86_64", null)] // simulator not available on macOS - public void Xcode12_x (string targetFrameworkMoniker, bool isSimulator, string architecture, string expected) + [TestCase (TargetFramework.DotNet_macOS_String, true, "x86_64", null, null)] // simulator not available on macOS + public void Xcode12_x (string targetFrameworkMoniker, bool isSimulator, string architecture, string expected, string expectedNativeRelativePath) { // on Xcode 12.2+ you get arm64 for all (iOS, tvOS) simulators var path = Path.Combine (Path.GetDirectoryName (GetType ().Assembly.Location)!, "Resources", "xcf-xcode12.2.plist"); var plist = PDictionary.FromFile (path)!; - var result = ResolveNativeReferences.TryResolveXCFramework (log, plist, "N/A", targetFrameworkMoniker, isSimulator, architecture, null, out var frameworkPath); + var result = ResolveNativeReferences.TryResolveXCFramework (log, plist, "N/A", targetFrameworkMoniker, isSimulator, architecture, null, out var nativeRelativePath); Assert.AreEqual (result, !string.IsNullOrEmpty (expected), "result"); - Assert.That (frameworkPath, Is.EqualTo (expected), "frameworkPath"); + Assert.That (nativeRelativePath, Is.EqualTo (expectedNativeRelativePath), "frameworkPath"); } - [TestCase (TargetFramework.DotNet_iOS_String, false, "ARMv7", "ios-arm64_armv7_armv7s/XTest.framework/XTest")] - public void PreXcode12 (string targetFrameworkMoniker, bool isSimulator, string architecture, string expected) + [TestCase (TargetFramework.DotNet_iOS_String, false, "ARMv7", "ios-arm64_armv7_armv7s/XTest.framework/XTest", "ios-arm64_armv7_armv7s/XTest.framework")] + public void PreXcode12 (string targetFrameworkMoniker, bool isSimulator, string architecture, string expected, string expectedNativeRelativePath) { var path = Path.Combine (Path.GetDirectoryName (GetType ().Assembly.Location)!, "Resources", "xcf-prexcode12.plist"); var plist = PDictionary.FromFile (path)!; - var result = ResolveNativeReferences.TryResolveXCFramework (log, plist, "N/A", targetFrameworkMoniker, isSimulator, architecture, null, out var frameworkPath); + var result = ResolveNativeReferences.TryResolveXCFramework (log, plist, "N/A", targetFrameworkMoniker, isSimulator, architecture, null, out var nativeRelativePath); Assert.AreEqual (result, !string.IsNullOrEmpty (expected), "result"); - Assert.That (frameworkPath, Is.EqualTo (expected), "frameworkPath"); + Assert.That (nativeRelativePath, Is.EqualTo (expectedNativeRelativePath), "frameworkPath"); } [Test] public void BadInfoPlist () { var plist = new PDictionary (); - var result = ResolveNativeReferences.TryResolveXCFramework (log, plist, "N/A", TargetFramework.DotNet_iOS_String, false, "x86_64", null, out var frameworkPath); + var result = ResolveNativeReferences.TryResolveXCFramework (log, plist, "N/A", TargetFramework.DotNet_iOS_String, false, "x86_64", null, out var nativeRelativePath); Assert.IsFalse (result, "Invalid Info.plist"); } + + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.MacCatalyst, false)] + public void CompressedNativeReference (ApplePlatform platform, bool useSystemIOCompression) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + var tmpdir = Cache.CreateTemporaryDirectory (); + + var item = new TaskItem (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XTest.xcframework.zip")); + item.SetMetadata ("Kind", "Framework"); + + var task = CreateTask (); + task.Architectures = "ARM64"; + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + task.FrameworksDirectory = ""; + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + task.FrameworksDirectory = "Contents/Frameworks/"; + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + task.IntermediateOutputPath = tmpdir; + task.NativeReferences = new TaskItem [] { + item, + }; + task.SdkIsSimulator = false; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); + + var originalSystemIOCompression = Environment.GetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION"); + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", "1"); + + try { + Assert.IsTrue (task.Execute (), "Execute"); + + var expectedFiles = new List () { + Path.Combine ("XTest.xcframework.zip"), + Path.Combine ("XTest.xcframework.zip", "XTest.framework"), + Path.Combine ("XTest.xcframework.zip", "XTest.framework", "XTest"), + Path.Combine ("XTest.xcframework.zip", "XTest.framework.stamp"), + }; + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Info.plist")); + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Resources")); + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Versions")); + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Versions", "A", "Resources", "Info.plist")); + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Versions", "A", "XTest")); + expectedFiles.Add (Path.Combine ("XTest.xcframework.zip", "XTest.framework", "Versions", "Current")); + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + + var files = new FileSystemEnumerable ( + directory: tmpdir, + transform: (ref FileSystemEntry entry) => entry.ToFullPath (), + options: new EnumerationOptions { + RecurseSubdirectories = true, + }) { + ShouldRecursePredicate = (ref FileSystemEntry entry) => { + return entry.ToFileSystemInfo ().LinkTarget is null; + } + } + .Select (v => v [(tmpdir.Length + 1)..]) + .OrderBy (v => v) + .ToArray (); + + var expectedFilesSorted = expectedFiles.OrderBy (v => v).ToArray (); + + Assert.That (files, Is.EqualTo (expectedFilesSorted), "Unzipped files"); + + var nativeFrameworks = task.NativeFrameworks?.OrderBy (v => v.ItemSpec).ToArray () ?? Array.Empty (); + var nativeFrameworkNames = nativeFrameworks.Select (v => v.ItemSpec).ToArray (); + var expectedNativeFrameworkNames = new string [] { + Path.Combine (tmpdir, "XTest.xcframework.zip", "XTest.framework/XTest"), + }; + Assert.That (nativeFrameworkNames, Is.EqualTo (expectedNativeFrameworkNames), "Native frameworks"); + } finally { + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", originalSystemIOCompression); + } + } + + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.MacCatalyst, false)] + public void SidecarFromReferenceWithCompressedNativeReferences (ApplePlatform platform, bool useSystemIOCompression) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + var tmpdir = Cache.CreateTemporaryDirectory (); + var inputdir = Path.Combine (tmpdir, "input"); + var outputdir = Path.Combine (tmpdir, "output"); + + var dll = Path.Combine (inputdir, "BindingWithCompressedXCFramework.dll"); + var sidecar = Path.Combine (inputdir, "BindingWithCompressedXCFramework.resources"); + Directory.CreateDirectory (sidecar); + var manifest = + $""" + + + + + ../../../test-libraries/.libs/XTest.xcframework.zip + + Framework + + + + + + + + + + ../../../test-libraries/.libs/XStaticArTest.xcframework.zip + + Static + + + + + + + + + + ../../../test-libraries/.libs/XStaticObjectTest.xcframework.zip + + Static + + + + + + + + """; + File.WriteAllText (Path.Combine (sidecar, "manifest"), manifest); + File.Copy (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XTest.xcframework.zip"), Path.Combine (sidecar, "XTest.xcframework.zip")); + File.Copy (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XStaticArTest.xcframework.zip"), Path.Combine (sidecar, "XStaticArTest.xcframework.zip")); + File.Copy (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XStaticObjectTest.xcframework.zip"), Path.Combine (sidecar, "XStaticObjectTest.xcframework.zip")); + + var item = new TaskItem (dll); + + var task = CreateTask (); + task.Architectures = "ARM64"; + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + task.FrameworksDirectory = ""; + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + task.FrameworksDirectory = "Contents/Frameworks/"; + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + task.IntermediateOutputPath = outputdir; + task.References = new TaskItem [] { + item, + }; + task.SdkIsSimulator = false; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); + + var originalSystemIOCompression = Environment.GetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION"); + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", "1"); + + try { + Assert.IsTrue (task.Execute (), "Execute"); + + var expectedFiles = new List (); + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "XStaticArTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "XStaticObjectTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Info.plist")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "XTest")); + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "XStaticArTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "XStaticObjectTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "Resources", "Info.plist")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "XTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "Current")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "XTest")); + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + + var files = new FileSystemEnumerable ( + directory: task.IntermediateOutputPath, + transform: (ref FileSystemEntry entry) => entry.ToFullPath (), + options: new EnumerationOptions { + RecurseSubdirectories = true, + }) { + ShouldRecursePredicate = (ref FileSystemEntry entry) => { + return entry.ToFileSystemInfo ().LinkTarget is null; + } + } + .Select (v => v [(task.IntermediateOutputPath.Length + 1)..]) + .OrderBy (v => v) + .ToArray (); + + var expectedFilesSorted = expectedFiles.OrderBy (v => v).ToArray (); + + Assert.That (files, Is.EqualTo (expectedFilesSorted), "Unzipped files"); + + var nativeFrameworks = task.NativeFrameworks?.OrderBy (v => v.ItemSpec).ToArray () ?? Array.Empty (); + var nativeFrameworkNames = nativeFrameworks.Select (v => v.ItemSpec).ToArray (); + var expectedNativeFrameworkNames = new string [] { + Path.Combine (outputdir, "BindingWithCompressedXCFramework.resources", "XStaticArTest.framework/XStaticArTest"), + Path.Combine (outputdir, "BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework/XStaticObjectTest"), + Path.Combine (outputdir, "BindingWithCompressedXCFramework.resources", "XTest.framework/XTest"), + }; + Assert.That (nativeFrameworkNames, Is.EqualTo (expectedNativeFrameworkNames), "Native frameworks"); + } finally { + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", originalSystemIOCompression); + } + } + + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.MacCatalyst, false)] + public void CompressedSidecarFromReference (ApplePlatform platform, bool useSystemIOCompression) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + var tmpdir = Cache.CreateTemporaryDirectory (); + var inputdir = Path.Combine (tmpdir, "input"); + var outputdir = Path.Combine (tmpdir, "output"); + + var dll = Path.Combine (inputdir, "bindings-test.dll"); + var sidecar = Path.Combine (inputdir, Path.GetFileNameWithoutExtension (dll) + ".resources.zip"); + var manifest = + $""" + + + + CoreLocation Foundation ModelIO + /Users/rolf/work/dotnet/macios/msbuild/macios/tests/test-libraries/.libs/libtest.xcframework + + Static + + + + + + + + + + /Users/rolf/work/dotnet/macios/msbuild/macios/tests/test-libraries/.libs/SwiftTest.xcframework + + Framework + + true + + + + + + + + /Users/rolf/work/dotnet/macios/msbuild/macios/tests/test-libraries/.libs/SwiftTest2.xcframework + + Framework + + true + + + + + + """; + + Directory.CreateDirectory (Path.GetDirectoryName (sidecar)!); + using var stream = File.OpenWrite (sidecar); + using (var archive = new ZipArchive (stream, ZipArchiveMode.Create)) { + archive.CreateEntryFromString (manifest, "manifest"); + } // dispose here to make sure everything is written to disk + var rootDirectory = Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs"); + ZipHelpers.AddDirectoryToZipFile (sidecar, Path.Combine (rootDirectory, "libtest.xcframework"), rootDirectory); + ZipHelpers.AddDirectoryToZipFile (sidecar, Path.Combine (rootDirectory, "SwiftTest.xcframework"), rootDirectory); + ZipHelpers.AddDirectoryToZipFile (sidecar, Path.Combine (rootDirectory, "SwiftTest2.xcframework"), rootDirectory); + + var item = new TaskItem (dll); + + var task = CreateTask (); + task.Architectures = "ARM64"; + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + task.FrameworksDirectory = ""; + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + task.FrameworksDirectory = "Contents/Frameworks/"; + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + task.IntermediateOutputPath = outputdir; + task.References = new TaskItem [] { + item, + }; + task.SdkIsSimulator = false; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); + + var originalSystemIOCompression = Environment.GetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION"); + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", "1"); + + try { + Assert.IsTrue (task.Execute (), "Execute"); + + var expectedFiles = new List (); + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "libtest.a")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "libtest.a.stamp")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Info.plist")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "SwiftTest")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework.stamp")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Info.plist")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "SwiftTest2")); + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "libtest.a")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "libtest.a.stamp")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Resources")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "SwiftTest")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Versions")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Versions", "A", "Resources", "Info.plist")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Versions", "A", "SwiftTest")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest.framework", "Versions", "Current")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework.stamp")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Resources")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "SwiftTest2")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Versions")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Versions", "A", "Resources", "Info.plist")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Versions", "A", "SwiftTest2")); + expectedFiles.Add (Path.Combine ("bindings-test.resources.zip", "SwiftTest2.framework", "Versions", "Current")); + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + + var files = new FileSystemEnumerable ( + directory: task.IntermediateOutputPath, + transform: (ref FileSystemEntry entry) => entry.ToFullPath (), + options: new EnumerationOptions { + RecurseSubdirectories = true, + }) { + ShouldRecursePredicate = (ref FileSystemEntry entry) => { + return entry.ToFileSystemInfo ().LinkTarget is null; + } + } + .Select (v => v [(task.IntermediateOutputPath.Length + 1)..]) + .OrderBy (v => v) + .ToArray (); + + var expectedFilesSorted = expectedFiles.OrderBy (v => v).ToArray (); + + Assert.That (files, Is.EqualTo (expectedFilesSorted), "Unzipped files"); + + var nativeFrameworks = task.NativeFrameworks?.OrderBy (v => v.ItemSpec).ToArray () ?? Array.Empty (); + var nativeFrameworkNames = nativeFrameworks.Select (v => v.ItemSpec).ToArray (); + var expectedNativeFrameworkNames = new string [] { + Path.Combine (outputdir, "bindings-test.resources.zip", "libtest.a"), + Path.Combine (outputdir, "bindings-test.resources.zip", "SwiftTest.framework/SwiftTest"), + Path.Combine (outputdir, "bindings-test.resources.zip", "SwiftTest2.framework/SwiftTest2"), + }; + Assert.That (nativeFrameworkNames, Is.EqualTo (expectedNativeFrameworkNames), "Native frameworks"); + } finally { + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", originalSystemIOCompression); + } + } + + static void AddFileToZip (ZipArchive archive, string pathInZip, string contents) + { + var entry = archive.CreateEntry (pathInZip); + using var entryStream = entry.Open (); + using var writer = new StreamWriter (entryStream); + writer.Write (contents); + } + + static void StuffZipWithFiles (string zipFile) + { + using var stream = File.Open (zipFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + using var archive = new ZipArchive (stream, ZipArchiveMode.Update); + var manifests = archive.Entries.Where (v => { + if (v.Name != "Info.plist") + return false; + var dir = Path.GetDirectoryName (v.FullName); + if (dir?.EndsWith (".xcframework", StringComparison.Ordinal) != true) + return false; + + if (Path.GetFileName (dir) != dir) + return false; // inside an unexpected subdirectory somewhere + + return true; + }).ToArray (); + + var frameworks = new List<(string Path, string Platform)> (); + foreach (var manifest in manifests) { + using var manifestStream = manifest.Open (); + var dict = (PDictionary) PDictionary.FromStream (manifestStream)!; + var availableLibraries = dict.Get ("AvailableLibraries")!; + foreach (PDictionary lib in availableLibraries) { + var libraryIdentifier = (string) lib.GetString ("LibraryIdentifier")!; + var libraryPath = (string) lib.GetString ("LibraryPath")!; + var platform = (string) lib.GetString ("SupportedPlatform")!; + var platformVariant = (string) lib.GetString ("SupportedPlatformVariant")!; + + if (platformVariant == "maccatalyst") + platform = platformVariant; + + frameworks.Add ((Path.Combine (Path.GetDirectoryName (manifest.FullName)!, libraryIdentifier, libraryPath), platform)); + } + } + + foreach (var (path, platform) in frameworks) { + var isDesktop = platform == "macos" || platform == "maccatalyst"; + var fwName = Path.GetFileNameWithoutExtension (path); + var architectures = AfterFirst (Path.GetFileName (Path.GetDirectoryName (path)!)!, '-').Split ('_'); + string infix = ""; + var resourceInfix = ""; + if (isDesktop) { + infix = Path.Combine ("Versions", "A"); + resourceInfix = "Resources"; + } + AddFileToZip (archive, Path.Combine (path, infix, "Headers", "MyHeader.h"), "// myheader"); + AddFileToZip (archive, Path.Combine (path, infix, "PrivateHeaders", "MyPrivateHeader.h"), "// myprivateheader"); + AddFileToZip (archive, Path.Combine (path, infix, resourceInfix, "PrivacyInfo.xcprivacy"), ""); + AddFileToZip (archive, Path.Combine (path, infix, "Modules", "module.modulemap"), "// modulemap"); + foreach (var arch in architectures) + AddFileToZip (archive, Path.Combine (path, infix, "Modules", fwName + ".swiftmodule", $"{arch}-{platform}.swiftinterface"), "// swiftinterface"); + AddFileToZip (archive, Path.Combine (path, infix, "dSYMs", fwName + ".dSYM", "Contents", "Resources", "DWARF", fwName), "// dsym"); + } + } + + static string AfterFirst (string value, char needle) + { + var idx = value.IndexOf (needle); + if (idx == -1) + return value; + return value [(idx + 1)..]; + } + + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.MacCatalyst, false)] + public void SidecarFromReferenceWithCompressedNativeReferencesAndThenFiltered (ApplePlatform platform, bool useSystemIOCompression) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + var tmpdir = Cache.CreateTemporaryDirectory (); + var inputdir = Path.Combine (tmpdir, "input"); + var outputdir = Path.Combine (tmpdir, "output"); + + var dll = Path.Combine (inputdir, "BindingWithCompressedXCFramework.dll"); + var sidecar = Path.Combine (inputdir, "BindingWithCompressedXCFramework.resources"); + Directory.CreateDirectory (sidecar); + var manifest = + $""" + + + + + ../../../test-libraries/.libs/XTest.xcframework.zip + + Framework + + + + + + + + + + ../../../test-libraries/.libs/XStaticArTest.xcframework.zip + + Static + + + + + + + + + + ../../../test-libraries/.libs/XStaticObjectTest.xcframework.zip + + Static + + + + + + + + """; + File.WriteAllText (Path.Combine (sidecar, "manifest"), manifest); + + var XTestFrameworkZipPath = Path.Combine (sidecar, "XTest.xcframework.zip"); + File.Copy (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XTest.xcframework.zip"), XTestFrameworkZipPath); + StuffZipWithFiles (XTestFrameworkZipPath); + + var XStaticArTestFrameworkZipPath = Path.Combine (sidecar, "XStaticArTest.xcframework.zip"); + File.Copy (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XStaticArTest.xcframework.zip"), XStaticArTestFrameworkZipPath); + StuffZipWithFiles (XStaticArTestFrameworkZipPath); + + var XStaticObjectTestFrameworkZipPath = Path.Combine (sidecar, "XStaticObjectTest.xcframework.zip"); + File.Copy (Path.Combine (Configuration.RootPath, "tests", "test-libraries", ".libs", "XStaticObjectTest.xcframework.zip"), XStaticObjectTestFrameworkZipPath); + StuffZipWithFiles (XStaticObjectTestFrameworkZipPath); + + var item = new TaskItem (dll); + + var task = CreateTask (); + task.Architectures = "ARM64"; + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + task.FrameworksDirectory = ""; + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + task.FrameworksDirectory = "Contents/Frameworks/"; + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + task.IntermediateOutputPath = outputdir; + task.References = new TaskItem [] { + item, + }; + task.SdkIsSimulator = false; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); + task.ExtractionFilters = new [] { + new TaskItem (".*/Headers/.*"), + new TaskItem ("Modules/.*"), + new TaskItem ("dSYMs/.*"), + new TaskItem ("PrivateHeaders/.*"), + new TaskItem ("PrivateHeaders/.*"), + }; + + var originalSystemIOCompression = Environment.GetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION"); + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", "1"); + + try { + Assert.IsTrue (task.Execute (), "Execute"); + + var expectedFiles = new List (); + switch (platform) { + case ApplePlatform.iOS: + case ApplePlatform.TVOS: + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "XStaticArTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "PrivacyInfo.xcprivacy")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "XStaticObjectTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "PrivacyInfo.xcprivacy")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Info.plist")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "PrivacyInfo.xcprivacy")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "XTest")); + break; + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "XStaticArTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "Versions")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticArTest.framework", "Versions", "A", "Resources", "PrivacyInfo.xcprivacy")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "XStaticObjectTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "Versions")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework", "Versions", "A", "Resources", "PrivacyInfo.xcprivacy")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework.stamp")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "Resources")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "Resources", "Info.plist")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "Resources", "PrivacyInfo.xcprivacy")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "A", "XTest")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "Versions", "Current")); + expectedFiles.Add (Path.Combine ("BindingWithCompressedXCFramework.resources", "XTest.framework", "XTest")); + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + + // Get all extracted files, but don't recurse into directories that are symlinks + var files = new FileSystemEnumerable ( + directory: task.IntermediateOutputPath, + transform: (ref FileSystemEntry entry) => entry.ToFullPath (), + options: new EnumerationOptions { + RecurseSubdirectories = true, + }) { + ShouldRecursePredicate = (ref FileSystemEntry entry) => { + return entry.ToFileSystemInfo ().LinkTarget is null; + } + } + .Select (v => v [(task.IntermediateOutputPath.Length + 1)..]) + .OrderBy (v => v) + .ToArray (); + + var expectedFilesSorted = expectedFiles.OrderBy (v => v).ToArray (); + + Assert.That (files, Is.EqualTo (expectedFilesSorted), "Unzipped files"); + + var nativeFrameworks = task.NativeFrameworks?.OrderBy (v => v.ItemSpec).ToArray () ?? Array.Empty (); + var nativeFrameworkNames = nativeFrameworks.Select (v => v.ItemSpec).ToArray (); + var expectedNativeFrameworkNames = new string [] { + Path.Combine (outputdir, "BindingWithCompressedXCFramework.resources", "XStaticArTest.framework/XStaticArTest"), + Path.Combine (outputdir, "BindingWithCompressedXCFramework.resources", "XStaticObjectTest.framework/XStaticObjectTest"), + Path.Combine (outputdir, "BindingWithCompressedXCFramework.resources", "XTest.framework/XTest"), + }; + Assert.That (nativeFrameworkNames, Is.EqualTo (expectedNativeFrameworkNames), "Native frameworks"); + } finally { + if (useSystemIOCompression) + Environment.SetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION", originalSystemIOCompression); + } + } + + } +} + +static class ZipHelpers { + public static void AddDirectoryToZipFile (string zipFile, string sourceDirectory, string rootDirectory) + { + var task = Execution.RunWithStringBuildersAsync ("zip", [zipFile, "--symlink", "-r", sourceDirectory.Substring (rootDirectory.Length + 1)], workingDirectory: rootDirectory); + task.Wait (); + Assert.That (task.Result.ExitCode, Is.EqualTo (0), "Zip command failed"); + } + + public static void CreateEntryFromString (this ZipArchive archive, string content, string entryName) + { + var entry = archive.CreateEntry (entryName); + using var stream = entry.Open (); + using var writer = new StreamWriter (stream); + writer.Write (content); } }