public override bool Execute() { LogMessage("Starting XamarinDownloadPartialZips"); LogMessage("DestinationBase: {0}", DestinationBase); LogMessage("CacheDirectory: {0}", CacheDirectory); Task.Run(async() => { List <PartialZipDownload> parts = null; try { http = new HttpClient(); var cacheDir = DownloadUtils.GetCacheDir(CacheDirectory); var downloadUtils = new DownloadUtils(this, cacheDir); parts = downloadUtils.ParsePartialZipDownloadItems(Parts, AllowUnsecureUrls); await DownloadAll(cacheDir, parts).ConfigureAwait(false); } catch (Exception ex) { LogErrorFromException(ex); // Log Custom error if one was specified in the partial download info var firstPart = parts?.FirstOrDefault(); if (!string.IsNullOrEmpty(firstPart?.CustomErrorCode) && !string.IsNullOrEmpty(firstPart?.CustomErrorMessage)) { LogCodedError(firstPart.CustomErrorCode, firstPart.CustomErrorMessage); } } finally { Complete(); } }); var result = base.Execute(); return(result && !Log.HasLoggedErrors); }
async Task <bool> ExtractPartAndValidate(PartialZipDownload part, Stream partInputStream, string cacheDirectory) { string fileHash = null; var outputPath = GetOutputPath(cacheDirectory, part); using (var iis = new System.IO.Compression.DeflateStream(partInputStream, System.IO.Compression.CompressionMode.Decompress)) //using (var iis = new ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream (partInputStream, new ICSharpCode.SharpZipLib.Zip.Compression.Inflater (true))) using (var fs = File.Open(outputPath, FileMode.Create)) { await iis.CopyToAsync(fs).ConfigureAwait(false); await fs.FlushAsync().ConfigureAwait(false); fs.Seek(0, SeekOrigin.Begin); fileHash = DownloadUtils.HashMd5(fs); LogDebugMessage("Hash of Downloaded File: {0}", fileHash); fs.Close(); } if (!string.IsNullOrEmpty(part.Md5) && !part.Md5.Equals(fileHash, StringComparison.InvariantCultureIgnoreCase)) { // TODO: HANDLE LogMessage("File MD5 Hash was invalid, deleting file: {0}", part.ToFile); File.Delete(outputPath); return(false); } return(true); }
public override bool Execute() { var results = new List <ITaskItem> (); downloadUtils = new DownloadUtils(this, CacheDirectory); var items = downloadUtils.ParseDownloadItems(Archives); if (items != null) { foreach (var item in items) { if (downloadUtils.IsAlreadyDownloaded(item)) { continue; } var taskItem = new TaskItem(item.Id); taskItem.SetMetadata("Type", "Download"); taskItem.SetMetadata("Url", item.Url); taskItem.SetMetadata("CacheFile", item.CacheFile); if (!string.IsNullOrEmpty(item.Sha1)) { taskItem.SetMetadata("Sha1", item.Sha1); } results.Add(taskItem); } } var partials = downloadUtils.ParsePartialZipDownloadItems(PartialZipDownloads); if (partials != null) { foreach (var partialZipDownload in partials) { if (downloadUtils.IsAlreadyDownloaded(CacheDirectory, partialZipDownload)) { continue; } var taskItem = new TaskItem(partialZipDownload.Id); taskItem.SetMetadata("Type", "PartialZipDownload"); taskItem.SetMetadata("Url", partialZipDownload.Url); taskItem.SetMetadata("RangeStart", partialZipDownload.RangeStart.ToString()); taskItem.SetMetadata("RangeEnd", partialZipDownload.RangeEnd.ToString()); if (!string.IsNullOrEmpty(partialZipDownload.Md5)) { taskItem.SetMetadata("Md5", partialZipDownload.Md5); } results.Add(taskItem); } } ArchivesToDownload = results.ToArray(); return(true); }
public override bool Execute() { downloadUtils = new DownloadUtils(this, CacheDirectory); Task.Run(async() => { try { var items = downloadUtils.ParseDownloadItems(Archives); foreach (var item in items) { await MakeSureLibraryIsInPlace(item, Token); } } catch (Exception ex) { LogErrorFromException(ex); } finally { Complete(); } }); var result = base.Execute(); return(result && !Log.HasLoggedErrors); }
// We intentionally won't call the base implementation in this override // since other tasks should handle the restores // This task is just responsible for extracting proguard config files // from the .aar input files so we are reusing the base task to help // track down the .aar files themselves. public override bool Execute() { // Get the dir to store proguard config files in proguardIntermediateOutputPath = Path.Combine(MergeOutputDir, "proguard"); if (!Directory.Exists(proguardIntermediateOutputPath)) { Directory.CreateDirectory(proguardIntermediateOutputPath); } var additionalFileWrites = new List <ITaskItem> (); // Make sure our XbdMerge directory exists var outputDir = MergeOutputDir; Directory.CreateDirectory(outputDir); // Get our assembly restore map var restoreMap = BuildRestoreMap(RestoreAssemblyResources); if (restoreMap == null) { return(false); } // Look through all the assemblies we would restore for foreach (var asm in restoreMap) { // We only want to find proguard files in .aar files referenced // for assemblies we actually have referenced and have them mapped to var asmName = new AssemblyName(asm.Key); ITaskItem item = FindMatchingAssembly(InputReferencePaths, asmName); if (item == null) { if (ThrowOnMissingAssembly) { return(false); } else { continue; } } // Use a hash for the assembly name to keep paths shorter var saveNameHash = DownloadUtils.HashMd5(asmName.Name)?.Substring(0, 8); // We keep a stamp file around to avoid reprocessing, so skip if it exists var stampPath = Path.Combine(outputDir, saveNameHash + ".proguard.stamp"); if (File.Exists(stampPath)) { continue; } // Get all the mapped .aar files var resourceItems = asm.Value; // We want to increment on the hash name in case there are multiple .aar files and/or proguard config // files for a given assembly, so we use them all and not overwrite the same name var entryCount = 0; // In theory we could have multiple .aar files? Probably never happen... foreach (var resourceItem in resourceItems) { // Full path to .aar file var resourceFullPath = resourceItem.GetMetadata("FullPath"); using (var fileStream = File.OpenRead(resourceFullPath)) using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read)) { // Look for proguard config files in the archive foreach (var entry in zipArchive.Entries) { // Skip entries which are not proguard configs if (!entry.Name.Equals("proguard.txt", StringComparison.OrdinalIgnoreCase) && !entry.Name.Equals("proguard.cfg", StringComparison.OrdinalIgnoreCase)) { continue; } // Figure out our destination filename var proguardSaveFilename = Path.Combine(proguardIntermediateOutputPath, saveNameHash + entryCount + ".txt"); // Add this to our file writes additionalFileWrites.Add(new TaskItem(proguardSaveFilename)); // Save out the proguard file using (var entryStream = entry.Open()) using (var fs = File.Create(proguardSaveFilename)) { entryStream.CopyTo(fs); fs.Flush(); fs.Close(); } entryCount++; } } } // *.proguard.stamp files are additional file writes File.WriteAllText(stampPath, string.Empty); additionalFileWrites.Add(new TaskItem(stampPath)); } AdditionalFileWrites = additionalFileWrites.ToArray(); return(true); }
async Task <bool> MakeSureLibraryIsInPlace(XamarinBuildDownload xbd, CancellationToken token) { // Skip extraction if the file is already in place var flagFile = xbd.DestinationDir + ".unpacked"; if (File.Exists(flagFile)) { return(true); } try { Directory.CreateDirectory(xbd.DestinationDir); } catch (Exception ex) { LogCodedError(ThisOrThat(xbd.CustomErrorCode, ErrorCodes.DirectoryCreateFailed), ThisOrThat(xbd.CustomErrorMessage, () => string.Format("Failed to create directory '{0}'.", xbd.DestinationDir))); LogMessage("Directory creation failure reason: " + ex.ToString(), MessageImportance.High); return(false); } var lockFile = xbd.DestinationDir + ".locked"; using (var lockStream = DownloadUtils.ObtainExclusiveFileLock(lockFile, Token, TimeSpan.FromSeconds(xbd.ExclusiveLockTimeout), this)) { 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); return(false); } if (!File.Exists(xbd.CacheFile) || !IsValidDownload(xbd.DestinationDir + ".sha1", xbd.CacheFile, xbd.Sha1)) { try { int progress = -1; DownloadProgressChangedEventHandler downloadHandler = (o, e) => { if (e.ProgressPercentage % 10 != 0 || progress == e.ProgressPercentage) { return; } progress = e.ProgressPercentage; LogMessage( "\t({0}/{1}b), total {2:F1}%", e.BytesReceived, e.TotalBytesToReceive, e.ProgressPercentage ); }; using (var client = new WebClient()) { client.DownloadProgressChanged += downloadHandler; LogMessage(" Downloading {0} to {1}", xbd.Url, xbd.CacheFile); client.DownloadFileTaskAsync(xbd.Url, xbd.CacheFile).Wait(token); LogMessage(" Downloading Complete"); client.DownloadProgressChanged -= downloadHandler; } } catch (Exception e) { LogCodedError(ThisOrThat(xbd.CustomErrorCode, ErrorCodes.DownloadFailed), ThisOrThat(xbd.CustomErrorMessage, () => string.Format("Download failed. Please download {0} to a file called {1}.", xbd.Url, xbd.CacheFile))); LogMessage("Download failure reason: " + e.GetBaseException().Message, MessageImportance.High); File.Delete(xbd.CacheFile); return(false); } } if (!File.Exists(xbd.CacheFile)) { LogCodedError(ThisOrThat(xbd.CustomErrorCode, ErrorCodes.DownloadedFileMissing), ThisOrThat(xbd.CustomErrorMessage, () => string.Format("Downloaded file '{0}' is missing.", xbd.CacheFile))); return(false); } if (xbd.Kind == ArchiveKind.Uncompressed) { var uncompressedCacheFile = xbd.CacheFile; if (!string.IsNullOrEmpty(xbd.ToFile)) { uncompressedCacheFile = xbd.ToFile; } File.Move(xbd.CacheFile, Path.Combine(xbd.DestinationDir, Path.GetFileName(uncompressedCacheFile))); File.WriteAllText(flagFile, "This marks that the extraction completed successfully"); return(true); } else { if (await ExtractArchive(xbd, flagFile, token)) { File.WriteAllText(flagFile, "This marks that the extraction completed successfully"); return(true); } } } // We will attempt to delete the lock file when we're done try { if (File.Exists(lockFile)) { File.Delete(lockFile); } } catch { } return(false); }
public List <PartialZipDownload> ParsePartialZipDownloadItems(ITaskItem [] items, bool allowUnsecureUrls) { if (items == null || items.Length <= 0) { return(new List <PartialZipDownload> ()); } var result = new List <PartialZipDownload> (); foreach (var part in items) { var id = part.ItemSpec; if (!DownloadUtils.ValidateId(id)) { Log.LogCodedError(ErrorCodes.XbdInvalidItemId, "Invalid item ID {0}", id); continue; } var toFile = part.GetMetadata("ToFile"); if (string.IsNullOrEmpty(toFile)) { Log.LogCodedError(ErrorCodes.XbdInvalidToFile, "Invalid or missing required ToFile metadata on item {0}", id); continue; } var url = part.GetMetadata("Url"); if (string.IsNullOrEmpty(url)) { Log.LogCodedError(ErrorCodes.XbdInvalidUrl, "Missing required Url metadata on item {0}", id); continue; } if (!EnsureSecureUrl(part, url, allowUnsecureUrls)) { continue; } var sha256 = part.GetMetadata("Sha256"); long rangeStart = -1L; long.TryParse(part.GetMetadata("RangeStart"), out rangeStart); if (rangeStart < 0) { Log.LogCodedError(ErrorCodes.XbdInvalidRangeStart, "Invalid or Missing required RangeStart metadata on item {0}", id); continue; } long rangeEnd = -1L; long.TryParse(part.GetMetadata("RangeEnd"), out rangeEnd); if (rangeEnd < 0) { Log.LogCodedError(ErrorCodes.XbdInvalidRangeEnd, "Invalid or Missing required RangeEnd metadata on item {0}", id); continue; } if (rangeEnd <= rangeStart) { Log.LogCodedError(ErrorCodes.XbdInvalidRange, "Invalid RangeStart and RangeEnd values, RangeEnd cannot be less than or equal to RangeStart, on item {0}", id); continue; } var customErrorMsg = part.GetMetadata("CustomErrorMessage"); var customErrorCode = part.GetMetadata("CustomErrorCode"); result.Add(new PartialZipDownload { Id = id, ToFile = toFile, Url = url, Sha256 = sha256, RangeStart = rangeStart, RangeEnd = rangeEnd, CustomErrorMessage = customErrorMsg, CustomErrorCode = customErrorCode, }); } // Deduplicate multiple id's of the same value var uniqueParts = result.GroupBy(p => p.Id).Select(kvp => kvp.FirstOrDefault()); return(uniqueParts.ToList()); }
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); } } } }
protected override Stream LoadResource(string resourceFullPath, string assemblyName) { const string AAR_DIR_PREFIX = "library_project_imports"; var memoryStream = new MemoryStream(); using (var fileStream = base.LoadResource(resourceFullPath, assemblyName)) fileStream.CopyTo(memoryStream); using (var zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Update, true)) { var entryNames = zipArchive.Entries.Select(zae => zae.FullName).ToList(); Log.LogMessage("Found {0} entries in {1}", entryNames.Count, resourceFullPath); foreach (var entryName in entryNames) { // Calculate the new name with the aar directory prefix var newName = entryName; if (!entryName.StartsWith(AAR_DIR_PREFIX, StringComparison.InvariantCulture)) { newName = AAR_DIR_PREFIX + Path.DirectorySeparatorChar + newName; } // Open the old entry var oldEntry = zipArchive.GetEntry(entryName); // 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.HashSha1(assemblyName).Substring(0, 6) + ".jar"); } Log.LogMessage("Renaming: {0} to {1}", entryName, newName); // Create a new entry based on our new name var newEntry = zipArchive.CreateEntry(newName); // 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(); } } memoryStream.Position = 0; return(memoryStream); }
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(); } } }
protected override Stream LoadResource(string resourceFullPath, string assemblyName) { const string AAR_DIR_PREFIX = "library_project_imports"; var memoryStream = new MemoryStream(); using (var fileStream = base.LoadResource(resourceFullPath, assemblyName)) fileStream.CopyTo(memoryStream); using (var zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Update, true)) { var entryNames = zipArchive.Entries.Select(zae => zae.FullName).ToList(); Log.LogMessage("Found {0} entries in {1}", entryNames.Count, resourceFullPath); foreach (var entryName in entryNames) { // Calculate the new name with the aar directory prefix var newName = entryName; if (!entryName.StartsWith(AAR_DIR_PREFIX, StringComparison.InvariantCulture)) { newName = AAR_DIR_PREFIX + "/" + newName; } // Open the old entry var oldEntry = zipArchive.GetEntry(entryName); // 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.HashSha1(assemblyName).Substring(0, 6) + ".jar"); } Log.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 (FixAndroidManifests && 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(); } } memoryStream.Position = 0; return(memoryStream); }