diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs new file mode 100644 index 00000000000..b885433d304 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs @@ -0,0 +1,58 @@ +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using System; +using System.Linq; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner +{ + public class StripEmbeddedLibrariesStep : IAssemblyModifierPipelineStep + { + public TaskLoggingHelper Log { get; } + + public StripEmbeddedLibrariesStep (TaskLoggingHelper log) + { + Log = log; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + if (context.IsFrameworkAssembly) + return; + + bool assembly_modified = false; + foreach (var mod in assembly.Modules) { + foreach (var r in mod.Resources.ToArray ()) { + if (ShouldStripResource (r)) { + Log.LogDebugMessage ($" Stripped {r.Name} from {assembly.Name.Name}.dll"); + mod.Resources.Remove (r); + assembly_modified = true; + } + } + } + if (assembly_modified) { + context.IsAssemblyModified = true; + } + } + + bool ShouldStripResource (Resource r) + { + if (!(r is EmbeddedResource)) + return false; + // embedded jars + if (r.Name.EndsWith (".jar", StringComparison.InvariantCultureIgnoreCase)) + return true; + // embedded AndroidNativeLibrary archive + if (r.Name == "__AndroidNativeLibraries__.zip") + return true; + // embedded AndroidResourceLibrary archive + if (r.Name == "__AndroidLibraryProjects__.zip") + return true; + // embedded AndroidEnvironment item + if (r.Name.StartsWith ("__AndroidEnvironment__", StringComparison.Ordinal)) + return true; + return false; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 68fd6b77705..5eb1b723ff1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -203,7 +203,6 @@ Type="MonoDroid.Tuner.AddKeepAlivesStep" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" AfterStep="CleanStep" Type="MonoDroid.Tuner.StripEmbeddedLibraries" /> <_TrimmerCustomSteps Condition=" '$(AndroidLinkResources)' == 'true' " Include="$(_AndroidLinkerCustomStepAssembly)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index 08a558de918..164aa7b9c29 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -140,6 +140,10 @@ protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCont findJavaObjectsStep.Initialize (context); pipeline.Steps.Add (findJavaObjectsStep); + // StripEmbeddedLibrariesStep + var stripEmbeddedLibrariesStep = new StripEmbeddedLibrariesStep (Log); + pipeline.Steps.Add (stripEmbeddedLibrariesStep); + // SaveChangedAssemblyStep var writerParameters = new WriterParameters { DeterministicMvid = Deterministic, @@ -192,9 +196,42 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { if (context.IsAssemblyModified) { Log.LogDebugMessage ($"Saving modified assembly: {context.Destination.ItemSpec}"); - Directory.CreateDirectory (Path.GetDirectoryName (context.Destination.ItemSpec)); + + // Write back pure IL even for crossgen-ed (R2R) assemblies, matching ILLink's OutputStep behavior. + // Mono.Cecil cannot write mixed-mode assemblies, so we strip the R2R metadata before writing. + // The native R2R code is discarded since the assembly has been modified and would need to be + // re-crossgen'd anyway. + foreach (var module in assembly.Modules) { + if (IsCrossgened (module)) { + module.Attributes |= ModuleAttributes.ILOnly; + module.Attributes ^= ModuleAttributes.ILLibrary; + module.Architecture = TargetArchitecture.I386; // I386+ILOnly translates to AnyCPU + module.Characteristics |= ModuleCharacteristics.NoSEH; + } + } + + // Write to a temp "new/" subdirectory then immediately copy back, to avoid + // reading and writing from the same path. Follows the same pattern as + // MarshalMethodsAssemblyRewriter. + string destPath = context.Destination.ItemSpec; + string directory = Path.Combine (Path.GetDirectoryName (destPath), "new"); + Directory.CreateDirectory (directory); + string tempPath = Path.Combine (directory, Path.GetFileName (destPath)); + WriterParameters.WriteSymbols = assembly.MainModule.HasSymbols; - assembly.Write (context.Destination.ItemSpec, WriterParameters); + assembly.Write (tempPath, WriterParameters); + + CopyFile (tempPath, destPath); + RemoveFile (tempPath); + + if (assembly.MainModule.HasSymbols) { + string tempPdb = Path.ChangeExtension (tempPath, ".pdb"); + string destPdb = Path.ChangeExtension (destPath, ".pdb"); + if (File.Exists (tempPdb)) { + CopyFile (tempPdb, destPdb); + } + RemoveFile (tempPdb); + } } else { // If we didn't write a modified file, copy the original to the destination CopyIfChanged (context.Source, context.Destination); @@ -204,6 +241,44 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) context.IsAssemblyModified = false; } + void CopyFile (string source, string target) + { + Log.LogDebugMessage ($"Copying rewritten assembly: {source} -> {target}"); + + string targetBackup = $"{target}.bak"; + if (File.Exists (target)) { + // Try to avoid sharing violations by first renaming the target + File.Move (target, targetBackup); + } + + File.Copy (source, target, true); + + if (File.Exists (targetBackup)) { + try { + File.Delete (targetBackup); + } catch (Exception ex) { + // On Windows the deletion may fail, depending on lock state of the original `target` file before the move. + Log.LogDebugMessage ($"While trying to delete '{targetBackup}', exception was thrown: {ex}"); + Log.LogDebugMessage ($"Failed to delete backup file '{targetBackup}', ignoring."); + } + } + } + + void RemoveFile (string? path) + { + if (String.IsNullOrEmpty (path) || !File.Exists (path)) { + return; + } + + try { + Log.LogDebugMessage ($"Deleting: {path}"); + File.Delete (path); + } catch (Exception ex) { + Log.LogWarning ($"Unable to delete source file '{path}'"); + Log.LogDebugMessage ($"{ex}"); + } + } + void CopyIfChanged (ITaskItem source, ITaskItem destination) { if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) { @@ -215,4 +290,14 @@ void CopyIfChanged (ITaskItem source, ITaskItem destination) File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow); } } + + /// + /// Check if a module has been crossgen-ed (ReadyToRun compiled), matching + /// ILLink's ModuleDefinitionExtensions.IsCrossgened() implementation. + /// + static bool IsCrossgened (ModuleDefinition module) + { + return (module.Attributes & ModuleAttributes.ILOnly) == 0 && + (module.Attributes & ModuleAttributes.ILLibrary) != 0; + } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index 79f6e450ae3..b0262d3197b 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -55,6 +55,7 @@ +