diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index 37ff74999ec..7a6b814a2d8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -70,17 +70,23 @@ public class GenerateJavaStubs : Task public override bool Execute () { + var temp = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ()); try { + Directory.CreateDirectory (temp); + // We're going to do 3 steps here instead of separate tasks so // we can share the list of JLO TypeDefinitions between them using (var res = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: true)) { - Run (res); + Run (res, temp); } - } - catch (XamarinAndroidException e) { + } catch (XamarinAndroidException e) { Log.LogCodedError (string.Format ("XA{0:0000}", e.Code), e.MessageWithoutCode); if (MonoAndroidHelper.LogInternalExceptions) Log.LogMessage (e.ToString ()); + } finally { + // Delete our temp directory + if (Directory.Exists (temp)) + Directory.Delete (temp, true); } if (Log.HasLoggedErrors) { @@ -95,12 +101,10 @@ public override bool Execute () return !Log.HasLoggedErrors; } - void Run (DirectoryAssemblyResolver res) + void Run (DirectoryAssemblyResolver res, string temp) { PackageNamingPolicy pnp; JavaNativeTypeManager.PackageNamingPolicy = Enum.TryParse (PackageNamingPolicy, out pnp) ? pnp : PackageNamingPolicyEnum.LowercaseHash; - var temp = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ()); - Directory.CreateDirectory (temp); foreach (var dir in FrameworkDirectories) { if (Directory.Exists (dir.ItemSpec)) @@ -111,9 +115,10 @@ void Run (DirectoryAssemblyResolver res) // Put every assembly we'll need in the resolver foreach (var assembly in ResolvedAssemblies) { - res.Load (Path.GetFullPath (assembly.ItemSpec)); + var assemblyFullPath = Path.GetFullPath (assembly.ItemSpec); + res.Load (assemblyFullPath); if (MonoAndroidHelper.FrameworkAttributeLookupTargets.Any (a => Path.GetFileName (assembly.ItemSpec) == a)) - selectedWhitelistAssemblies.Add (Path.GetFullPath (assembly.ItemSpec)); + selectedWhitelistAssemblies.Add (assemblyFullPath); } // However we only want to look for JLO types in user code @@ -133,7 +138,7 @@ void Run (DirectoryAssemblyResolver res) var java_types = all_java_types.Where (t => !JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration (t)); // Step 2 - Generate Java stub code - var keep_going = Generator.CreateJavaSources ( + var success = Generator.CreateJavaSources ( Log, java_types, temp, @@ -141,64 +146,54 @@ void Run (DirectoryAssemblyResolver res) UseSharedRuntime, int.Parse (AndroidSdkPlatform) <= 10, ResolvedAssemblies.Any (assembly => Path.GetFileName (assembly.ItemSpec) == "Mono.Android.Export.dll")); - - var temp_map_file = Path.Combine (temp, "acw-map.temp"); + if (!success) + return; // We need to save a map of .NET type -> ACW type for resource file fixups var managed = new Dictionary (); var java = new Dictionary (); - var acw_map = new StreamWriter (temp_map_file); - - foreach (var type in java_types) { - string managedKey = type.FullName.Replace ('/', '.'); - string javaKey = JavaNativeTypeManager.ToJniName (type).Replace ('/', '.'); - - acw_map.WriteLine ("{0};{1}", type.GetPartialAssemblyQualifiedName (), javaKey); - - TypeDefinition conflict; - if (managed.TryGetValue (managedKey, out conflict)) { - Log.LogWarning ( - "Duplicate managed type found! Mappings between managed types and Java types must be unique. " + - "First Type: '{0}'; Second Type: '{1}'.", - conflict.GetAssemblyQualifiedName (), - type.GetAssemblyQualifiedName ()); - Log.LogWarning ( - "References to the type '{0}' will refer to '{1}'.", - managedKey, conflict.GetAssemblyQualifiedName ()); - continue; - } - if (java.TryGetValue (javaKey, out conflict)) { - Log.LogError ( - "Duplicate Java type found! Mappings between managed types and Java types must be unique. " + - "First Type: '{0}'; Second Type: '{1}'", - conflict.GetAssemblyQualifiedName (), - type.GetAssemblyQualifiedName ()); - keep_going = false; - continue; + using (var stream = new MemoryStream ()) + using (var acw_map = new StreamWriter (stream)) { + foreach (var type in java_types) { + string managedKey = type.FullName.Replace ('/', '.'); + string javaKey = JavaNativeTypeManager.ToJniName (type).Replace ('/', '.'); + + acw_map.WriteLine ("{0};{1}", type.GetPartialAssemblyQualifiedName (), javaKey); + + TypeDefinition conflict; + if (managed.TryGetValue (managedKey, out conflict)) { + Log.LogWarning ( + "Duplicate managed type found! Mappings between managed types and Java types must be unique. " + + "First Type: '{0}'; Second Type: '{1}'.", + conflict.GetAssemblyQualifiedName (), + type.GetAssemblyQualifiedName ()); + Log.LogWarning ( + "References to the type '{0}' will refer to '{1}'.", + managedKey, conflict.GetAssemblyQualifiedName ()); + continue; + } + if (java.TryGetValue (javaKey, out conflict)) { + Log.LogError ( + "Duplicate Java type found! Mappings between managed types and Java types must be unique. " + + "First Type: '{0}'; Second Type: '{1}'", + conflict.GetAssemblyQualifiedName (), + type.GetAssemblyQualifiedName ()); + success = false; + continue; + } + managed.Add (managedKey, type); + java.Add (javaKey, type); + acw_map.WriteLine ("{0};{1}", managedKey, javaKey); + acw_map.WriteLine ("{0};{1}", JavaNativeTypeManager.ToCompatJniName (type).Replace ('/', '.'), javaKey); } - managed.Add (managedKey, type); - java.Add (javaKey, type); - acw_map.WriteLine ("{0};{1}", managedKey, javaKey); - acw_map.WriteLine ("{0};{1}", JavaNativeTypeManager.ToCompatJniName (type).Replace ('/', '.'), javaKey); - } - - acw_map.Close (); - //The previous steps found an error, so we must abort and not generate any further output - //We must do so subsequent unchanged builds fail too. - if (!keep_going) { - File.Delete (temp_map_file); - return; + acw_map.Flush (); + MonoAndroidHelper.CopyIfStreamChanged (stream, AcwMapFile); } - MonoAndroidHelper.CopyIfChanged (temp_map_file, AcwMapFile); - - try { File.Delete (temp_map_file); } catch (Exception) { } - // Only overwrite files if the contents actually changed foreach (var file in Directory.GetFiles (temp, "*", SearchOption.AllDirectories)) { - var dest = Path.GetFullPath (Path.Combine (OutputDirectory, "src", file.Substring (temp.Length + 1))); - + var dest = Path.Combine (OutputDirectory, "src", file.Substring (temp.Length + 1)); MonoAndroidHelper.CopyIfChanged (file, dest); } @@ -219,38 +214,24 @@ void Run (DirectoryAssemblyResolver res) var additionalProviders = manifest.Merge (all_java_types, selectedWhitelistAssemblies, ApplicationJavaClass, EmbedAssemblies, BundledWearApplicationName, MergedManifestDocuments); - var temp_manifest = Path.Combine (temp, "AndroidManifest.xml"); - var real_manifest = Path.GetFullPath (MergedAndroidManifestOutput); - - manifest.Save (temp_manifest); + using (var stream = new MemoryStream ()) { + manifest.Save (stream); - // Only write the new manifest if it actually changed - MonoAndroidHelper.CopyIfChanged (temp_manifest, real_manifest); + // Only write the new manifest if it actually changed + MonoAndroidHelper.CopyIfStreamChanged (stream, MergedAndroidManifestOutput); + } // Create additional runtime provider java sources. string providerTemplateFile = UseSharedRuntime ? "MonoRuntimeProvider.Shared.java" : "MonoRuntimeProvider.Bundled.java"; - string providerTemplate = new StreamReader (typeof (JavaCallableWrapperGenerator).Assembly.GetManifestResourceStream (providerTemplateFile)).ReadToEnd (); + string providerTemplate = GetResource (providerTemplateFile); foreach (var provider in additionalProviders) { - var temp_provider = Path.Combine (temp, provider + ".java"); - File.WriteAllText (temp_provider, providerTemplate.Replace ("MonoRuntimeProvider", provider)); - var real_provider_dir = Path.GetFullPath (Path.Combine (OutputDirectory, "src", "mono")); - Directory.CreateDirectory (real_provider_dir); - var real_provider = Path.Combine (real_provider_dir, provider + ".java"); - MonoAndroidHelper.CopyIfChanged (temp_provider, real_provider); + var contents = providerTemplate.Replace ("MonoRuntimeProvider", provider); + var real_provider = Path.Combine (OutputDirectory, "src", "mono", provider + ".java"); + MonoAndroidHelper.CopyIfStringChanged (contents, real_provider); } // Create additional application java sources. - - Action> save = (resource, filename, destDir, applyTemplate) => { - string temp_file = Path.Combine (temp, filename); - string template = applyTemplate (new StreamReader (typeof (GenerateJavaStubs).Assembly.GetManifestResourceStream (resource)).ReadToEnd ()); - File.WriteAllText (temp_file, template); - Directory.CreateDirectory (destDir); - var real_file = Path.Combine (destDir, filename); - MonoAndroidHelper.CopyIfChanged (temp_file, real_file); - }; - StringWriter regCallsWriter = new StringWriter (); regCallsWriter.WriteLine ("\t\t// Application and Instrumentation ACWs must be registered first."); foreach (var type in java_types) { @@ -262,17 +243,28 @@ void Run (DirectoryAssemblyResolver res) } regCallsWriter.Close (); - var real_app_dir = Path.GetFullPath (Path.Combine (OutputDirectory, "src", "mono", "android", "app")); + var real_app_dir = Path.Combine (OutputDirectory, "src", "mono", "android", "app"); string applicationTemplateFile = "ApplicationRegistration.java"; - save (applicationTemplateFile, applicationTemplateFile, real_app_dir, + SaveResource (applicationTemplateFile, applicationTemplateFile, real_app_dir, template => template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ())); // Create NotifyTimeZoneChanges java sources. string notifyTimeZoneChangesFile = "NotifyTimeZoneChanges.java"; - save (notifyTimeZoneChangesFile, notifyTimeZoneChangesFile, real_app_dir, template => template); - - // Delete our temp directory - try { Directory.Delete (temp, true); } catch (Exception) { } + SaveResource (notifyTimeZoneChangesFile, notifyTimeZoneChangesFile, real_app_dir, template => template); + } + + string GetResource (string resource) + { + using (var stream = typeof (T).Assembly.GetManifestResourceStream (resource)) + using (var reader = new StreamReader (stream)) + return reader.ReadToEnd (); + } + + void SaveResource (string resource, string filename, string destDir, Func applyTemplate) + { + string template = GetResource (resource); + template = applyTemplate (template); + MonoAndroidHelper.CopyIfStringChanged (template, Path.Combine (destDir, filename)); } void WriteTypeMappings (List types) @@ -287,11 +279,10 @@ void WriteTypeMappings (List types) void UpdateWhenChanged (string path, Action generator) { - var np = path + ".new"; - using (var o = File.OpenWrite (np)) - generator (o); - MonoAndroidHelper.CopyIfChanged (np, path); - File.Delete (np); + using (var stream = new MemoryStream ()) { + generator (stream); + MonoAndroidHelper.CopyIfStreamChanged (stream, path); + } } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs index 5201c8792c0..889380d290c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs @@ -239,12 +239,8 @@ private void WriteFile (string file, CodeTypeDeclaration resources, string langu } } } - - var temp_o = Path.Combine (Path.GetDirectoryName (file), "__" + Path.GetFileName (file) + ".new"); - using (TextWriter o = File.CreateText (temp_o)) - o.Write (code); - MonoAndroidHelper.CopyIfChanged (temp_o, file); - try { File.Delete (temp_o); } catch (Exception) { } + + MonoAndroidHelper.CopyIfStringChanged (code, file); } private void AddRename (string android, string user) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/MonoAndroidHelperTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/MonoAndroidHelperTests.cs new file mode 100644 index 00000000000..ea92de8f776 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/MonoAndroidHelperTests.cs @@ -0,0 +1,136 @@ +using System.IO; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class MonoAndroidHelperTests + { + string temp; + + [SetUp] + public void SetUp () + { + temp = Path.Combine (Path.GetTempPath (), TestContext.CurrentContext.Test.Name); + } + + [TearDown] + public void TearDown () + { + File.Delete (temp); + } + + [Test] + public void CopyIfStringChanged () + { + var foo = "bar"; + Assert.IsTrue (MonoAndroidHelper.CopyIfStringChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + Assert.IsFalse (MonoAndroidHelper.CopyIfStringChanged (foo, temp), "Should *not* write unless changed."); + foo += "\n"; + Assert.IsTrue (MonoAndroidHelper.CopyIfStringChanged (foo, temp), "Should write when changed."); + } + + [Test] + public void CopyIfBytesChanged () + { + var foo = new byte [32]; + Assert.IsTrue (MonoAndroidHelper.CopyIfBytesChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + Assert.IsFalse (MonoAndroidHelper.CopyIfBytesChanged (foo, temp), "Should *not* write unless changed."); + foo [0] = 0xFF; + Assert.IsTrue (MonoAndroidHelper.CopyIfBytesChanged (foo, temp), "Should write when changed."); + } + + [Test] + public void CopyIfStreamChanged () + { + using (var foo = new MemoryStream ()) + using (var writer = new StreamWriter (foo)) { + writer.WriteLine ("bar"); + writer.Flush (); + + Assert.IsTrue (MonoAndroidHelper.CopyIfStreamChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + Assert.IsFalse (MonoAndroidHelper.CopyIfStreamChanged (foo, temp), "Should *not* write unless changed."); + writer.WriteLine (); + writer.Flush (); + Assert.IsTrue (MonoAndroidHelper.CopyIfStreamChanged (foo, temp), "Should write when changed."); + } + } + + [Test] + public void CopyIfStringChanged_NewDirectory () + { + temp = Path.Combine (temp, "foo.txt"); + + var foo = "bar"; + Assert.IsTrue (MonoAndroidHelper.CopyIfStringChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + } + + [Test] + public void CopyIfBytesChanged_NewDirectory () + { + temp = Path.Combine (temp, "foo.bin"); + + var foo = new byte [32]; + Assert.IsTrue (MonoAndroidHelper.CopyIfBytesChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + } + + [Test] + public void CopyIfStreamChanged_NewDirectory () + { + temp = Path.Combine (temp, "foo.txt"); + + using (var foo = new MemoryStream ()) + using (var writer = new StreamWriter (foo)) { + writer.WriteLine ("bar"); + writer.Flush (); + + Assert.IsTrue (MonoAndroidHelper.CopyIfStreamChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + } + } + + [Test] + public void CopyIfStringChanged_Readonly () + { + File.WriteAllText (temp, ""); + File.SetAttributes (temp, FileAttributes.ReadOnly); + + var foo = "bar"; + Assert.IsTrue (MonoAndroidHelper.CopyIfStringChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + } + + [Test] + public void CopyIfBytesChanged_Readonly () + { + File.WriteAllText (temp, ""); + File.SetAttributes (temp, FileAttributes.ReadOnly); + + var foo = new byte [32]; + Assert.IsTrue (MonoAndroidHelper.CopyIfBytesChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + } + + [Test] + public void CopyIfStreamChanged_Readonly () + { + File.WriteAllText (temp, ""); + File.SetAttributes (temp, FileAttributes.ReadOnly); + + using (var foo = new MemoryStream ()) + using (var writer = new StreamWriter (foo)) { + writer.WriteLine ("bar"); + writer.Flush (); + + Assert.IsTrue (MonoAndroidHelper.CopyIfStreamChanged (foo, temp), "Should write on new file."); + FileAssert.Exists (temp); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.Shared.projitems b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.Shared.projitems index 4caedf0eed4..ac351869cfb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.Shared.projitems +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.Shared.projitems @@ -20,6 +20,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/Files.cs b/src/Xamarin.Android.Build.Tasks/Utilities/Files.cs index 4a8da6df2ea..8a0a9b59dcc 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/Files.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/Files.cs @@ -4,6 +4,7 @@ using Xamarin.Tools.Zip; using System.Collections.Generic; +using System.Text; #if MSBUILD using Microsoft.Build.Utilities; using Xamarin.Android.Tasks; @@ -72,6 +73,43 @@ public static bool CopyIfChanged (string source, string destination) return false; } + public static bool CopyIfStringChanged (string contents, string destination) + { + var bytes = Encoding.UTF8.GetBytes (contents); + return CopyIfBytesChanged (bytes, destination); + } + + public static bool CopyIfBytesChanged (byte[] bytes, string destination) + { + if (HasBytesChanged (bytes, destination)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + MonoAndroidHelper.SetWriteable (destination); + File.WriteAllBytes (destination, bytes); + return true; + } + return false; + } + + public static bool CopyIfStreamChanged (Stream stream, string destination) + { + if (HasStreamChanged (stream, destination)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + MonoAndroidHelper.SetWriteable (destination); + using (var fileStream = File.Create (destination)) { + stream.Position = 0; //HasStreamChanged read to the end + stream.CopyTo (fileStream); + } + return true; + } + return false; + } + public static bool CopyIfZipChanged (Stream source, string destination) { string hash; @@ -162,7 +200,39 @@ public static bool HasFileChanged (string source, string destination) var src_hash = HashFile (source); var dst_hash = HashFile (destination); - // If the hashed don't match, then the file has changed + // If the hashes don't match, then the file has changed + if (src_hash != dst_hash) + return true; + + return false; + } + + public static bool HasStreamChanged (Stream source, string destination) + { + //If destination is missing, that's definitely a change + if (!File.Exists (destination)) + return true; + + var src_hash = HashStream (source); + var dst_hash = HashFile (destination); + + // If the hashes don't match, then the file has changed + if (src_hash != dst_hash) + return true; + + return false; + } + + public static bool HasBytesChanged (byte [] bytes, string destination) + { + //If destination is missing, that's definitely a change + if (!File.Exists (destination)) + return true; + + var src_hash = HashBytes (bytes); + var dst_hash = HashFile (destination); + + // If the hashes don't match, then the file has changed if (src_hash != dst_hash) return true; @@ -271,9 +341,16 @@ public static bool ExtractAll(ZipArchive zip, string destination, Action subclasses, i public void Save (string filename) { - using (var file = new StreamWriter (filename, false, new UTF8Encoding (false))) + using (var file = new StreamWriter (filename, append: false, encoding: new UTF8Encoding (false))) + Save (file); + } + + public void Save (Stream stream) + { + using (var file = new StreamWriter (stream, new UTF8Encoding (false), bufferSize: 1024, leaveOpen: true)) Save (file); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs index 4a126097ef7..143a868e6f9 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs @@ -386,6 +386,21 @@ public static bool CopyIfChanged (string source, string destination) return Files.CopyIfChanged (source, destination); } + public static bool CopyIfStringChanged (string contents, string destination) + { + return Files.CopyIfStringChanged (contents, destination); + } + + public static bool CopyIfBytesChanged (byte [] bytes, string destination) + { + return Files.CopyIfBytesChanged (bytes, destination); + } + + public static bool CopyIfStreamChanged (Stream source, string destination) + { + return Files.CopyIfStreamChanged (source, destination); + } + public static bool CopyIfZipChanged (Stream source, string destination) { return Files.CopyIfZipChanged (source, destination);