async Task DownloadAll(string cacheDirectory, List <PartialZipDownload> parts) { // Get the parts all grouped by their URL so we can batch requests with multiple ranges // instead of making a request to the same url for each part // also only grab the parts that don't already locally exist var uniqueUrls = parts .Where(p => !File.Exists(Path.Combine(cacheDirectory, p.Id, p.ToFile))) .GroupBy(p => p.Url); // For each unique url... foreach (var partsByUrl in uniqueUrls) { var downloadUrl = partsByUrl.Key; LogMessage("Downloading Partial Zip parts from: " + downloadUrl); try { // Create a lock file based on the hash of the URL we are downloading from // Since we could download a multipart request, we are locking on the url from any other process downloading from it var lockFile = Path.Combine(cacheDirectory, DownloadUtils.Crc64(downloadUrl) + ".locked"); using (var lockStream = DownloadUtils.ObtainExclusiveFileLock(lockFile, base.Token, TimeSpan.FromSeconds(30))) { if (lockStream == null) { LogCodedError(ErrorCodes.ExclusiveLockTimeout, "Timed out waiting for exclusive file lock on: {0}", lockFile); LogMessage("Timed out waiting for an exclusive file lock on: " + lockFile, MessageImportance.High); // Log Custom error if one was specified in the partial download info var firstPart = partsByUrl.FirstOrDefault(); if (!string.IsNullOrEmpty(firstPart?.CustomErrorCode) && !string.IsNullOrEmpty(firstPart?.CustomErrorMessage)) { LogCodedError(firstPart.CustomErrorCode, firstPart.CustomErrorMessage); } return; } try { await Download(cacheDirectory, partsByUrl.Key, partsByUrl.ToList()).ConfigureAwait(false); } catch (Exception ex) { LogCodedError(ErrorCodes.PartialDownloadFailed, "Partial Download Failed for one or more parts"); LogErrorFromException(ex); // Log Custom error if one was specified in the partial download info var firstPart = partsByUrl.FirstOrDefault(); if (!string.IsNullOrEmpty(firstPart?.CustomErrorCode) && !string.IsNullOrEmpty(firstPart?.CustomErrorMessage)) { LogCodedError(firstPart.CustomErrorCode, firstPart.CustomErrorMessage); } } } try { if (File.Exists(lockFile)) { File.Delete(lockFile); } } catch { } } catch (Exception ex) { LogCodedError(ErrorCodes.PartialDownloadFailed, "Partial Download Failed for one or more parts"); LogErrorFromException(ex); // Log Custom error if one was specified in the partial download info var firstPart = partsByUrl.FirstOrDefault(); if (!string.IsNullOrEmpty(firstPart?.CustomErrorCode) && !string.IsNullOrEmpty(firstPart?.CustomErrorMessage)) { LogCodedError(firstPart.CustomErrorCode, firstPart.CustomErrorMessage); } } } }
internal static void FixupAar(string filename, bool androidFixManifests, TaskLoggingHelper loggingHelper) { using (var fileStream = new FileStream(filename, FileMode.Open)) using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Update, true)) { var entryNames = zipArchive.Entries.Select(zae => zae.FullName).ToList(); loggingHelper.LogMessage("Found {0} entries in {1}", entryNames.Count, filename); foreach (var entryName in entryNames) { var newName = entryName; // Open the old entry var oldEntry = zipArchive.GetEntry(entryName); // Some .aars contain an AndroidManifest.xml in the aapt folder which is essentially a duplicate of the main one // but a sanitized version with placeholders like ${applicationId} being escaped to _dollar blah // we don't care about these for xamarin.android, which picks up both manifests and merges both // This will ensure the 'sanitized' version doesn't get packaged if (entryName.TrimStart('/').Equals("aapt/AndroidManifest.xml", StringComparison.InvariantCultureIgnoreCase)) { loggingHelper.LogMessage("Found aapt/AndroidManifest.xml, skipping..."); // Delete the entry entirely and continue oldEntry.Delete(); continue; } // We are only re-adding non empty folders, otherwise we end up with a corrupt zip in mono if (!string.IsNullOrEmpty(oldEntry.Name)) { // SPOILER ALERT: UGLY WORKAROUND // In the Android Support libraries, there exist multiple .aar files which have a `libs/internal_impl-25.0.0` file. // In Xamarin.Android, there is a Task "CheckDuplicateJavaLibraries" which inspects jar files being pulled in from .aar files // in assemblies to see if there exist any files with the same name but different content, and will throw an error if it finds any. // However, for us, it is perfectly valid to have this scenario and we should not see an error. // We are working around this by detecting files named like this, and renaming them to some unique value // in this case, a part of the hash of the assembly name. var newFile = Path.GetFileName(newName); var newDir = Path.GetDirectoryName(newName); if (newFile.StartsWith("internal_impl", StringComparison.InvariantCulture)) { newName = Path.Combine(newDir, "internal_impl-" + DownloadUtils.Crc64(filename).Substring(0, 6) + ".jar"); } loggingHelper.LogMessage("Renaming: {0} to {1}", entryName, newName); // Create a new entry based on our new name var newEntry = zipArchive.CreateEntry(newName); // Since Xamarin.Android's AndoridManifest.xml merging code is not as sophisticated as gradle's yet, we may need // to fix some things up in the manifest file to get it to merge properly into our applications // Here we will check to see if Fixing manifests was enabled, and if the entry we are on is the AndroidManifest.xml file if (androidFixManifests && oldEntry.Length > 0 && newName.EndsWith("AndroidManifest.xml", StringComparison.OrdinalIgnoreCase)) { // android: namespace XNamespace xns = "http://schemas.android.com/apk/res/android"; using (var oldStream = oldEntry.Open()) using (var xmlReader = System.Xml.XmlReader.Create(oldStream)) { var xdoc = XDocument.Load(xmlReader); // BEGIN FIXUP #1 // Some `android:name` attributes will start with a . indicating, that the `package` value of the `manifest` element // should be prefixed dynamically/at merge to this attribute value. Xamarin.Android doesn't handle this case yet // so we are going to manually take care of it. // Get the package name from the manifest node var packageName = xdoc.Document.Descendants("manifest")?.FirstOrDefault()?.Attribute("package")?.Value; if (!string.IsNullOrEmpty(packageName)) { // Find all elements in the xml document that have a `android:name` attribute which starts with a . // Select all of them, and then change the `android:name` attribute value to be the // package name we found in the `manifest` element previously + the original attribute value xdoc.Document.Descendants() .Where(elem => elem.Attribute(xns + "name")?.Value?.StartsWith(".", StringComparison.Ordinal) ?? false) .ToList() .ForEach(elem => elem.SetAttributeValue(xns + "name", packageName + elem.Attribute(xns + "name").Value)); } // END FIXUP #1 using (var newStream = newEntry.Open()) using (var xmlWriter = System.Xml.XmlWriter.Create(newStream)) { xdoc.WriteTo(xmlWriter); } } } else { // Copy file contents over if they exist if (oldEntry.Length > 0) { using (var oldStream = oldEntry.Open()) using (var newStream = newEntry.Open()) { oldStream.CopyTo(newStream); } } } } // Delete the old entry regardless of if it's a folder or not oldEntry.Delete(); } } }