Skip to content

Commit f7d706c

Browse files
committed
[api-compat] Provide API diffs around API breakage (#4362)
Context: #4356 Context: 54beb90 Context: a20be39 The use of `Microsoft.DotNet.ApiCompat.exe` added in 07e7477 has one major deficiency: The error messages reported by `Microsoft.DotNet.ApiCompat.exe` are *awful* and borderline useless or misleading. For example, consider commit PR #4356, which attempts to bring sanity and consistency around `$(AndroidPlatformId)` and `Mono.Android.dll` builds. It contains an API break, which we'll hand wave away and accept for preview release purposes, in which the property type for `Android.Telephony.CellInfoGsm.CellIdentity` changes from `CellIdentityGsm` to `CellIdentity`: // API-29 namespace Android.Telephony { public sealed partial class CellInfoGsm: Android.Telephony.CellInfo, Android.OS.IParcelable { public unsafe Android.Telephony.CellIdentityGsm CellIdentity { } } // API-R namespace Android.Telephony { public sealed partial class CellInfoGsm : Android.Telephony.CellInfo, Android.OS.IParcelable { public unsafe Android.Telephony.CellIdentity CellIdentity { } } This is clearly a break. How does `Microsoft.DotNet.ApiCompat.exe` report the breakage? error : MembersMustExist : Member 'Android.Telephony.CellInfoGsm.CellIdentity.get()' does not exist in the implementation but it does exist in the contract. Which is infuriatingly terrible. The message *implies* that `Android.Telephony.CellInfoGsm.get_CellIdentity()` doesn't exist, but it *does* exist. The problem is that the return type changed! Or consider 54beb90, in which we now emit a slew of default interface members within the `Mono.Android.dll` binding, which *should* be API compatible. `Microsoft.DotNet.ApiCompat.exe` complains as well: InterfacesShouldHaveSameMembers : Interface member 'Java.Util.Functions.IUnaryOperator.Identity()' is present in the implementation but not in the contract. What these messages have in common is that they provide no context, lack important types, and in no way suggest how to *fix* the error other than to just ignore it. Overhaul this infrastructure so that crucial context is provided. The context is provided by using "reference assembly source": the [`Microsoft.DotNet.GenAPI.exe` utility][0] can be run on an assembly to generate C# source code that shows the same API but no implementation: namespace Android.Accounts { [Android.Runtime.RegisterAttribute("android/accounts/AbstractAccountAuthenticator", DoNotGenerateAcw=true, ApiSince=5)] public abstract partial class AbstractAccountAuthenticator : Java.Lang.Object { [Android.Runtime.RegisterAttribute("KEY_CUSTOM_TOKEN_EXPIRY", ApiSince=23)] public const string KeyCustomTokenExpiry = "android.accounts.expiry"; [Android.Runtime.RegisterAttribute(".ctor", "(Landroid/content/Context;)V", "")] public AbstractAccountAuthenticator(Android.Content.Context context) { } Update the `src/Mono.Android` build so that after every build, when `Microsoft.DotNet.ApiCompat.exe` fails we *also* run `Microsoft.DotNet.GenAPI.exe` on the generated assembly, then run `git diff -u` against the recently created assembly and the reference assembly source for the contract assembly. This allows us to get *useful diffs* in the API: Task "Exec" (TaskId:570) Task Parameter:Command=git diff --no-index "../../tests/api-compatibility/reference/Mono.Android.dll.cs" "/Volumes/Xamarin-Work/xamarin-android/bin/Debug/lib/xamarin.android/xbuild-frameworks/MonoAndroid/v10.0/Mono.Android.dll.cs" (TaskId:570) diff -u "../../tests/api-compatibility/reference/Mono.Android.dll.cs" "/Volumes/Xamarin-Work/xamarin-android/bin/Debug/lib/xamarin.android/xbuild-frameworks/MonoAndroid/v10.0/Mono.Android.dll.cs" (TaskId:570) --- ../../tests/api-compatibility/reference/Mono.Android.dll.cs 2020-03-05 13:20:59.000000000 -0500 (TaskId:570) +++ /Volumes/Xamarin-Work/xamarin-android/bin/Debug/lib/xamarin.android/xbuild-frameworks/MonoAndroid/v10.0/Mono.Android.dll.cs 2020-03-05 13:40:12.000000000 -0500 (TaskId:570) @@ -27,7 +27,7 @@ (TaskId:570) { (TaskId:570) [Android.Runtime.RegisterAttribute("ACCEPT_HANDOVER", ApiSince=28)] (TaskId:570) public const string AcceptHandover = "android.permission.ACCEPT_HANDOVER"; (TaskId:570) - [Android.Runtime.RegisterAttribute("ACCESS_BACKGROUND_LOCATION")] (TaskId:570) + [Android.Runtime.RegisterAttribute("ACCESS_BACKGROUND_LOCATION", ApiSince=29)] (TaskId:570) (The above changes are courtesy commmit 4cd2060, which added `RegisterAttribute.ApiSince` on a large number of members.) Finally, how do we update the "contract" `Mono.Android.dll` assembly? Add a new `tests/api-compatibility/api-compatibility.targets` file which contains a `UpdateMonoAndroidContract` target which will update `tests/api-compatibility/reference/Mono.Android.zip` with the contents of a `cil-strip`'d `Mono.Android.dll` and updated "reference assembly source". `Mono.Android.zip` contains a contract from Xamarin.Android 10.2.0.100 for `$(TargetFrameworkVersion)` v10.0. [0]: https://github.com/dotnet/arcade/tree/bc4fa8e7149769db4efd466f160417a32b11f0bf/src/Microsoft.DotNet.GenAPI
1 parent 3991f08 commit f7d706c

File tree

9 files changed

+91
-6
lines changed

9 files changed

+91
-6
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props
2424
external/monodroid/
2525
external/mono/
2626
tests/api-compatibility/reference/*.dll
27+
tests/api-compatibility/reference/*.cs
2728
Novell

Before.Xamarin.Android.sln.targets

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
<Import Project="$(MSBuildThisFileDirectory)build-tools\scripts\ImportExportDocs.targets" />
44
<Import Project="$(MSBuildThisFileDirectory)build-tools\scripts\PrepareWindows.targets" Condition=" '$(OS)' == 'Windows_NT' " />
55
<Import Project="$(MSBuildThisFileDirectory)build-tools\scripts\RunTests.targets" />
6+
<Import Project="$(MSBuildThisFileDirectory)tests\api-compatibility\api-compatibility.targets" />
67
</Project>

build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/Zip.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ public override bool Execute ()
5757
if (prefix != null && entryDir.StartsWith (prefix, StringComparison.OrdinalIgnoreCase)) {
5858
zipDir = entryDir.Substring (prefix.Length);
5959
}
60-
zip.AddFileToDirectory (entryPath, zipDir, useFileDirectory: false);
60+
if (string.IsNullOrEmpty (zipDir)) {
61+
// JonP can't figure out how to actually clear the archive directory name
62+
// using AddFileToDirectory(). This works as desired.
63+
zip.AddFile (entryPath, Path.GetFileName (entryPath));
64+
} else {
65+
zip.AddFileToDirectory (entryPath, zipDir, useFileDirectory: false);
66+
}
6167
}
6268
}
6369
return !Log.HasLoggedErrors;

build-tools/xa-prep-tasks/Xamarin.Android.BuildTools.PrepTasks/Git.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ protected virtual bool LogTaskMessages {
2626
get { return true; }
2727
}
2828

29+
protected virtual bool PreserveOutput {
30+
get { return true; }
31+
}
32+
2933
protected override string ToolBaseName {
3034
get { return "git"; }
3135
}
@@ -68,7 +72,9 @@ protected override string GetWorkingDirectory ()
6872
protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
6973
{
7074
base.LogEventsFromTextOutput (singleLine, messageImportance);
71-
Lines.Add (singleLine);
75+
if (PreserveOutput) {
76+
Lines.Add (singleLine);
77+
}
7278
}
7379
}
7480
}

src/Mono.Android/Mono.Android.targets

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<UsingTask AssemblyFile="$(PrepTasksAssembly)" TaskName="Xamarin.Android.BuildTools.PrepTasks.GitDiff" />
34
<UsingTask AssemblyFile="$(PrepTasksAssembly)" TaskName="Xamarin.Android.BuildTools.PrepTasks.ReplaceFileContents" />
45
<UsingTask AssemblyFile="$(BootstrapTasksAssembly)" TaskName="Xamarin.Android.Tools.BootstrapTasks.CheckApiCompatibility" />
56
<Import Project="..\..\build-tools\scripts\XAVersionInfo.targets" />
@@ -183,23 +184,41 @@
183184
<ApiCompatibilityFiles Include="$(ApiCompatibilityDir)/reference/*.*" />
184185
</ItemGroup>
185186
<Target
186-
Name="AfterBuild"
187+
Name="_CheckApiCompatibility"
187188
Condition=" '$(DisableApiCompatibilityCheck)' != 'True' "
189+
AfterTargets="Build"
188190
Inputs="$(TargetPath);@(ApiCompatibilityFiles)"
189191
Outputs="$(IntermediateOutputPath)CheckApiCompatibility.stamp">
190192
<CheckApiCompatibility
193+
ContinueOnError="ErrorAndContinue"
191194
ApiCompatPath="$(XAPackagesDir)\microsoft.dotnet.apicompat\5.0.0-beta.20078.1\tools\net472\"
192195
ApiLevel="$(AndroidFrameworkVersion)"
193196
LastStableApiLevel="$(AndroidLatestStableFrameworkVersion)"
194197
TargetImplementationPath="$(OutputPath)"
195198
ApiCompatibilityPath="$(ApiCompatibilityDir)"
196199
/>
197-
<Touch
198-
Files="$(IntermediateOutputPath)CheckApiCompatibility.stamp"
199-
AlwaysCreate="True"
200+
<PropertyGroup>
201+
<_RunApiDiff Condition=" '$(MSBuildLastTaskResult)' == 'False' ">True</_RunApiDiff>
202+
<_GenAPI>"$(XAPackagesDir)\microsoft.dotnet.genapi\5.0.0-beta.20078.1\tools\net472\Microsoft.DotNet.GenAPI.exe"</_GenAPI>
203+
<_ContractRefSrc>"$(ApiCompatibilityDir)\reference\Mono.Android.dll.cs"</_ContractRefSrc>
204+
<_TargetRefSrc>"$(TargetPath).cs"</_TargetRefSrc>
205+
</PropertyGroup>
206+
<Exec
207+
Condition=" '$(_RunApiDiff)' == 'True' "
208+
Command="$(ManagedRuntime) $(ManagedRuntimeArgs) $(_GenAPI) &quot;$(TargetPath)&quot; > $(_TargetRefSrc)"
209+
/>
210+
<GitDiff
211+
Condition=" '$(_RunApiDiff)' == 'True' "
212+
ContinueOnError="ErrorAndContinue"
213+
Arguments="--no-index $(_ContractRefSrc) $(_TargetRefSrc)"
214+
WorkingDirectory="$(MSBuildThisFileDirectory)"
200215
/>
201216
<ItemGroup>
202217
<FileWrites Include="$(IntermediateOutputPath)CheckApiCompatibility.stamp" />
203218
</ItemGroup>
219+
<Touch
220+
Files="$(IntermediateOutputPath)CheckApiCompatibility.stamp"
221+
AlwaysCreate="True"
222+
/>
204223
</Target>
205224
</Project>

tests/api-compatibility/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ current version of an assembly. We could also called it the V2 assembly.
1616
[mdac]: https://github.com/dotnet/arcade/tree/master/src/Microsoft.DotNet.ApiCompat
1717

1818

19+
## Update Contract Assembly
20+
21+
To update the contract assembly, run the `UpdateMonoAndroidContract` target
22+
and provide the `$(ContractAssembly)` MSBuild property. `$(ContractAssembly)`
23+
should be the path to the new contract assembly to use:
24+
25+
msbuild Xamarin.Android.sln /t:UpdateMonoAndroidContract '/p:ContractAssembly=/Users/example/Downloads/\$ReferenceAssemblies/Microsoft/Framework/MonoAndroid/v10.0/Mono.Android.dll'
26+
27+
1928
## Build Task
2029

2130
We have developed a build task that will wrap *Microsoft.DotNet.ApiCompat.exe*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<Import Project="..\..\Configuration.props" />
4+
<UsingTask AssemblyFile="$(BootstrapTasksAssembly)" TaskName="Xamarin.Android.Tools.BootstrapTasks.Zip" />
5+
<Target Name="UpdateMonoAndroidContract">
6+
<Error
7+
Condition=" '$(ContractAssembly)' == '' "
8+
Text="Please set the %24(ContractAssembly) property!"
9+
/>
10+
<PropertyGroup>
11+
<_GenAPI>"$(XAPackagesDir)\microsoft.dotnet.genapi\5.0.0-beta.20078.1\tools\net472\Microsoft.DotNet.GenAPI.exe"</_GenAPI>
12+
<_CilStrip>"$(XAInstallPrefix)\xbuild\Xamarin\Android\cil-strip.exe"</_CilStrip>
13+
<_ContractRefDll>$(MSBuildThisFileDirectory)\reference\Mono.Android.dll</_ContractRefDll>
14+
<_ContractRefSrc>$(MSBuildThisFileDirectory)\reference\Mono.Android.dll.cs</_ContractRefSrc>
15+
</PropertyGroup>
16+
<Copy
17+
SourceFiles="$(ContractAssembly)"
18+
DestinationFiles="$(_ContractRefDll)"
19+
/>
20+
<Exec
21+
Command="$(ManagedRuntime) $(ManagedRuntimeArgs) $(_GenAPI) &quot;$(_ContractRefDll)&quot; > &quot;$(_ContractRefSrc)&quot;"
22+
/>
23+
<Exec
24+
Command="$(ManagedRuntime) $(ManagedRuntimeArgs) $(_CilStrip) &quot;$(_ContractRefDll)&quot;"
25+
/>
26+
<ItemGroup>
27+
<_ZipEntry Include="$(_ContractRefDll)" />
28+
<_ZipEntry Include="$(_ContractRefSrc)" />
29+
</ItemGroup>
30+
<Zip
31+
File="$(MSBuildThisFileDirectory)\reference\Mono.Android.zip"
32+
Entries="@(_ZipEntry)"
33+
Prefix="$(MSBuildThisFileDirectory)\reference\"
34+
Overwrite="True"
35+
/>
36+
<!--
37+
<Exec
38+
Command="zip Mono.Android.zip Mono.Android.dll Mono.Android.dll.cs"
39+
WorkingDirectory="$(MSBuildThisFileDirectory)\reference"
40+
/>
41+
-->
42+
</Target>
43+
</Project>
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)