diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs index 49402865722..4b3952d8141 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs @@ -22,6 +22,7 @@ sealed class Config public string? AssemblerPath; public string? AssemblerOptions; public string? InputSource; + public string? OutputFile; } [Required] @@ -43,6 +44,14 @@ public override System.Threading.Tasks.Task RunTaskAsync () void RunAssembler (Config config) { + if (config.OutputFile is not null && config.InputSource is not null && File.Exists (config.OutputFile)) { + string sourceFile = Path.Combine (WorkingDirectory, Path.GetFileName (config.InputSource)); + if (File.Exists (sourceFile) && File.GetLastWriteTimeUtc (config.OutputFile) >= File.GetLastWriteTimeUtc (sourceFile)) { + LogDebugMessage ($"[LLVM llc] Skipping '{Path.GetFileName (config.InputSource)}' because '{Path.GetFileName (config.OutputFile)}' is up to date"); + return; + } + } + var stdout_completed = new ManualResetEvent (false); var stderr_completed = new ManualResetEvent (false); var psi = new ProcessStartInfo () { @@ -118,10 +127,13 @@ IEnumerable GetAssemblerConfigs () string executableDir = Path.GetDirectoryName (llcPath); string executableName = MonoAndroidHelper.GetExecutablePath (executableDir, Path.GetFileName (llcPath)); + string outputFilePath = Path.Combine (WorkingDirectory, sourceFile.Replace (".ll", ".o")); + yield return new Config { InputSource = item.ItemSpec, AssemblerPath = Path.Combine (executableDir, executableName), AssemblerOptions = $"{assemblerOptions} -o={outputFile} {QuoteFileName (sourceFile)}", + OutputFile = outputFilePath, }; } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs index 8e3600f20dd..7b30a0e4a79 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs @@ -1289,6 +1289,37 @@ public void GenerateJavaStubsAndAssembly ([Values] bool isRelease, [Values] Andr } } + [Test] + public void CompileNativeAssemblySourcesSkipsUnchangedFiles ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) + { + if (IgnoreUnsupportedConfiguration (runtime, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (runtime); + + string abi = "arm64-v8a"; + proj.SetRuntimeIdentifier (abi); + + using (var b = CreateApkBuilder ()) { + b.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue (b.Build (proj), "first build should have succeeded."); + + // Modify MainActivity to trigger recompilation of typemap sources + proj.MainActivity = proj.DefaultMainActivity + Environment.NewLine + "// test comment"; + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (b.Build (proj), "second build should have succeeded."); + + Assert.IsFalse (b.Output.IsTargetSkipped ("_CompileNativeAssemblySources"), "`_CompileNativeAssemblySources` should *not* be skipped!"); + + // At least one .ll file should have been skipped as up to date (e.g., environment.arm64-v8a.ll) + StringAssertEx.ContainsRegex (@"\[LLVM llc\] Skipping.*up to date", b.LastBuildOutput, + message: "Expected at least one .ll file to be skipped as up to date" + ); + } + } + readonly string [] ExpectedAssemblyFiles = new [] { Path.Combine ("android", "environment.@ABI@.o"), Path.Combine ("android", "environment.@ABI@.ll"), diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs index 0bd20902776..2b5e61c968f 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs @@ -73,10 +73,6 @@ public override string GetComment (object data, string fieldName) { var entry = EnsureType (data); - if (MonoAndroidHelper.StringEquals ("mvid_hash", fieldName)) { - return $" MVID: {entry.MVID}"; - } - if (MonoAndroidHelper.StringEquals ("name_offset", fieldName)) { return $" {entry.Name}"; } @@ -171,11 +167,6 @@ sealed class TypeMapAssembly [NativeAssembler (Ignore = true)] public string Name = String.Empty; - [NativeAssembler (Ignore = true)] - public Guid MVID; - - [NativeAssembler (UsesDataProvider = true, NumberFormat = LlvmIrVariableNumberFormat.Hexadecimal)] - public ulong mvid_hash; public ulong name_length; [NativeAssembler (UsesDataProvider = true)] @@ -274,21 +265,26 @@ protected override void Construct (LlvmIrModule module) Log.LogMessage ("Managed-to-java typemaps will use string-based matching."); } + // Sort assemblies by name before building the blob so that both the blob offsets + // and the uniqueAssemblies array are in a deterministic order that is stable across + // incremental builds (assembly names don't change, unlike MVIDs). + data.UniqueAssemblies.Sort ((a, b) => StringComparer.Ordinal.Compare (a.Name, b.Name)); + var assemblyNamesBlob = new LlvmIrStringBlob (); foreach (TypeMapGenerator.TypeMapDebugAssembly asm in data.UniqueAssemblies) { (int assemblyNameOffset, int assemblyNameLength) = assemblyNamesBlob.Add (asm.Name); var entry = new TypeMapAssembly { Name = asm.Name, - MVID = asm.MVID, - mvid_hash = MonoAndroidHelper.GetXxHash (asm.MVIDBytes, is64Bit: true), name_length = (ulong)assemblyNameLength, // without the trailing NUL name_offset = (ulong)assemblyNameOffset, }; uniqueAssemblies.Add (new StructureInstance (typeMapAssemblyStructureInfo, entry)); } + // Sort by assembly name for deterministic output. This ensures the .ll content + // is stable across incremental builds when only MVIDs change. uniqueAssemblies.Sort ((StructureInstance a, StructureInstance b) => { if (a.Instance == null) { return b.Instance == null ? 0 : -1; @@ -298,7 +294,7 @@ protected override void Construct (LlvmIrModule module) return 1; } - return a.Instance.mvid_hash.CompareTo (b.Instance.mvid_hash); + return StringComparer.Ordinal.Compare (a.Instance.Name, b.Instance.Name); }); var managedTypeInfos = new List> (); diff --git a/src/native/clr/host/typemap.cc b/src/native/clr/host/typemap.cc index 85e5a37a3a5..c402373d6c2 100644 --- a/src/native/clr/host/typemap.cc +++ b/src/native/clr/host/typemap.cc @@ -141,30 +141,33 @@ auto TypeMapper::index_to_name (ssize_t idx, const char* typeName, const TypeMap } [[gnu::always_inline, gnu::flatten]] -auto TypeMapper::managed_to_java_debug (const char *typeName, const uint8_t *mvid) noexcept -> const char* +auto TypeMapper::managed_to_java_debug (const char *typeName, [[maybe_unused]] const uint8_t *mvid) noexcept -> const char* { - dynamic_local_path_string full_type_name; - full_type_name.append (typeName); - - hash_t mvid_hash = xxhash::hash (mvid, 16z); // we must hope managed land called us with valid data - - auto equal = [](TypeMapAssembly const& entry, hash_t key) -> bool { return entry.mvid_hash == key; }; - auto less_than = [](TypeMapAssembly const& entry, hash_t key) -> bool { return entry.mvid_hash < key; }; - ssize_t idx = Search::binary_search (mvid_hash, type_map_unique_assemblies, type_map.unique_assemblies_count); - - if (idx >= 0) [[likely]] { - TypeMapAssembly const& assm = type_map_unique_assemblies[idx]; + // type_map_unique_assemblies is sorted by assembly name for stable build output (no + // build-specific data like MVIDs). We iterate through assemblies to find which one + // contains this type by trying each "TypeName, AssemblyName" candidate against the + // managed-to-java map. The array is small (~80-100 entries), so this is negligible. + for (size_t i = 0; i < type_map.unique_assemblies_count; i++) { + TypeMapAssembly const& assm = type_map_unique_assemblies[i]; + + dynamic_local_path_string full_type_name; + full_type_name.append (typeName); full_type_name.append (", "sv); - - // We explicitly trust the build process here, with regards to validity of offsets full_type_name.append (&type_map_assembly_names[assm.name_offset], assm.name_length); - } else { - log_warn (LOG_ASSEMBLY, "typemap: unable to look up assembly name for type '{}', trying without it."sv, typeName); + + ssize_t idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); + if (idx >= 0) { + return index_to_name (idx, full_type_name.get (), type_map.managed_to_java, type_map_java_type_names, MANAGED, JAVA); + } } - // If hashes are used for matching, the type names array is not used. If, however, string-based matching is in - // effect, the managed type name is looked up and then... - idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); + // Fallback: try without assembly name + dynamic_local_path_string full_type_name; + full_type_name.append (typeName); + + log_warn (LOG_ASSEMBLY, "typemap: unable to look up assembly name for type '{}', trying without it."sv, typeName); + + ssize_t idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); // ...either method gives us index into the Java type names array return index_to_name (idx, full_type_name.get (), type_map.managed_to_java, type_map_java_type_names, MANAGED, JAVA); diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index c2140e2c882..b7cc9485cc7 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -69,7 +69,6 @@ struct TypeMap // MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs struct TypeMapAssembly { - xamarin::android::hash_t mvid_hash; uint64_t name_length; uint64_t name_offset; // into the assembly names blob };