Skip to content

Commit

Permalink
Flush.
Browse files Browse the repository at this point in the history
  • Loading branch information
jpobst committed Jan 19, 2024
1 parent 586a769 commit f3e42e5
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 54 deletions.
116 changes: 116 additions & 0 deletions Documentation/guides/JavaDependencyVerification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Java Dependency Verification

Note: This feature is only available in .NET 9+.

## Description

A common problem when creating Java binding libraries for .NET Android is not providing the required Java dependencies. The binding process ignores API that requires missing dependencies, so this can result in large portions of desired API not being bound.

Unlike .NET assemblies, a Java library does not specify its dependencies in the package. The dependency information is stored in external files called POM files. In order to consume this information to ensure correct dependencies an additional layer of files must be added to a binding project.

Note: the preferred way of interacting with this system is to use [`<AndroidMavenLibrary>`](AndroidMavenLibrary.md) which will automatically download and include any needed POM files.

For example:

```xml
<AndroidMavenLibrary Include="com.squareup.okio:okio" Version="1.17.4" />
```

automatically gets expanded to:

```xml
<AndroidLibrary
Include="<MavenCacheDir>/Central/com.squareup.okio/okio/1.17.4/com.squareup.okio_okio.jar"
Manifest="<MavenCacheDir>/Central/com.squareup.okio/okio/1.17.4/com.squareup.okio_okio.pom"
JavaArtifact="com.squareup.okio:okio"
JavaVersion="1.17.4" />

<AndroidAdditionalJavaManifest
Include="<MavenCacheDir>/Central/com.squareup.okio/okio-parent/1.17.4/okio-parent-1.17.4.pom"
JavaArtifact="com.squareup.okio:okio-parent"
JavaVersion="1.17.4" />

etc.
```

However it is also possible to manually opt in to Java dependency verification using the build items documented here.

## Specification

To manually opt in to Java dependency verification, add the `Manifest`, `JavaArtifact`, and `JavaVersion` attributes to an `<AndroidLibrary>` item:

```xml
<!-- JavaArtifact format is {GroupId}:{ArtifactId} -->
<ItemGroup>
<AndroidLibrary
Include="my_binding_library.jar"
Manifest="my_binding_library.pom"
JavaArtifact="com.example:mybinding"
JavaVersion="1.0.0" />
</ItemGroup>
```

Building the binding project now should result in verification errors if `my_binding_library.pom` specifies dependencies that are not met.

For example:

```
error : Java dependency 'androidx.collection:collection' version '1.0.0' is not satisfied.
```

Seeing these error(s) or no errors should indicate that the Java dependency verification is working. Follow the [Resolving Java Dependencies](ResolvingJavaDependencies.md) guide to fix any missing dependency errors.

## Additional POM Files

POM files can sometimes have some optional features in use that make them more complicated than the above example.

That is, a POM file can depend on a "parent" POM file:

```xml
<parent>
<groupId>com.squareup.okio</groupId>
<artifactId>okio-parent</artifactId>
<version>1.17.4</version>
</parent>
```

Additionally, a POM file can "import" dependency information from another POM file:

```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio-bom</artifactId>
<version>3.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```

Dependency information cannot be accurately determined without also having access to these additional POM files, and will results in errors like:

```
error : Unable to resolve POM for artifact 'com.squareup.okio:okio-parent:1.17.4'.
error : Could not verify Java dependencies for artifact 'com.squareup.okio:okio:1.17.4' due to missing POM files. See other error(s) for details.
```

In this case, we need to provide the POM file for `com.squareup.okio:okio-parent:1.17.4`:

```xml
<!-- JavaArtifact format is {GroupId}:{ArtifactId} -->
<ItemGroup>
<AndroidAdditionalJavaManifest
Include="com.square.okio.okio-parent.1.17.4.pom"
JavaArtifact="com.squareup.okio:okio-parent"
JavaVersion="1.17.4" />
</ItemGroup>
```

Note that as "Parent" and "Import" POMs can themselves have parent and imported POMs, this step may need to be repeated until all POM files can be resolved.

Note also that if using `<AndroidMavenLibrary>` this should all be handled automatically.

At this point, if there are dependency errors, follow the [Resolving Java Dependencies](ResolvingJavaDependencies.md) guide to fix any missing dependency errors.
99 changes: 99 additions & 0 deletions Documentation/guides/ResolvingJavaDependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Resolving Java Dependencies

Note: This feature is only available in .NET 9+.

## Description

Once Java dependency verification has been enabled for a bindings project, either automatically via `<AndroidMavenLibrary>` or manually via `<AndroidLibrary>`, there may be errors to resolve, such as:

```
error : Java dependency 'androidx.collection:collection' version '1.0.0' is not satisfied.
```

These dependencies can be fulfilled in many different ways.

## `<PackageReference>`

In the best case scenario, there is already an existing binding of the Java dependency available on NuGet.org. This package may be provided by Microsoft or the .NET community. Packages maintained by Microsoft may be surfaced in the error message like this:

```
error : Java dependency 'androidx.collection:collection' version '1.0.0' is not satisfied. Microsoft maintains the NuGet package 'Xamarin.AndroidX.Collection' that could fulfill this dependency.
```

Adding the `Xamarin.AndroidX.Collection` package to the project should automatically resolve this error, as the package provides metadata to advertise that it provides the `androidx.collection:collection` dependency. This is done by looking for a specially crafted NuGet tag. For example, for the AndroidX Collection library, the tag looks like this:

```xml
<!-- artifact={GroupId}:{ArtifactId}:{Java Library Version} -->
<PackageTags>artifact=androidx.collection:collection:1.0.0</PackageTags>
```

However there may be NuGet packages which fulfill a dependency but have not had this metadata added to it. In this case, you will need to explicitly specify which dependency the package contains with `JavaArtifact` and `JavaVersion`:

```xml
<PackageReference
Include="Xamarin.Kotlin.StdLib"
Version="1.7.10"
JavaArtifact="org.jetbrains.kotlin:kotlin-stdlib"
JavaVersion="1.7.10" />
```

With this, the binding process knows the Java dependency is satisfied by the NuGet package.

> Note: NuGet packages specify their own dependencies, so you will not need to worry about transitive dependencies.
## `<ProjectReference>`

If the needed Java dependency is provided by another project in your solution, you can annotate the `<ProjectReference>` to specify the dependency it fulfills:

```xml
<ProjectReference
Include="..\My.Other.Binding\My.Other.Binding.csproj"
JavaArtifact="my.other.binding:helperlib"
JavaVersion="1.0.0" />
```

With this, the binding process knows the Java dependency is satisfied by the referenced project.

> Note: Each project specifies their own dependencies, so you will not need to worry about transitive dependencies.
## `<AndroidLibrary>`

If you are creating a public NuGet package, you will want to follow NuGet's "one library per package" policy so that the NuGet dependency graph works. However, if creating a binding for private use, you may want to include your Java dependencies directly inside the parent binding.

This can be done by adding additional `<AndroidLibrary>` items to the project:

```xml
<ItemGroup>
<AndroidLibrary JavaArtifact="my.library:dependency-library" JavaVersion="1.0.0" />
</ItemGroup>
```

To include the Java library but not produce C# bindings for it, mark it with `Bind="false"`:

```xml
<ItemGroup>
<AndroidLibrary JavaArtifact="my.library:dependency-library" JavaVersion="1.0.0" Bind="false" />
</ItemGroup>
```

Alternatively, `<AndroidMavenLibrary>` can be used to retrieve a Java library from a Maven repository:

```
<ItemGroup>
<AndroidMavenLibrary Include="my.library:dependency-library" Version="1.0.0" />
</ItemGroup>
```

> Note: If the dependency library has its own dependencies, you will be required to ensure they are fulfilled.
## `<AndroidIgnoredJavaDependency>`

As a last resort, a needed Java dependency can be ignored. An example of when this is useful is if the dependency library is a collection of Java annotations that are only used at compile type and not runtime.

Note that while the error message will go away, it does not mean the package will magically work. If the dependency is actually needed at runtime and not provided the Android application will crash with a `Java.Lang.NoClassDefFoundError` error.

```xml
<ItemGroup>
<AndroidIgnoredJavaDependency Include="com.google.errorprone:error_prone_annotations" Version="2.15.0" />
</ItemGroup>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--
***********************************************************************************************
Xamarin.Android.Bindings.JavaDependencyVerification.targets
This file contains MSBuild targets used to enable features using POM files to ensure
that Java binding dependencies are satisfied.
***********************************************************************************************
-->

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<UsingTask TaskName="Xamarin.Android.Tasks.GetMicrosoftNuGetPackagesMap" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
<UsingTask TaskName="Xamarin.Android.Tasks.JavaDependencyVerification" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />

<Target Name="VerifyJavaDependencies"
Condition=" '@(AndroidLibrary->Count())' != '0' "
BeforeTargets="_CategorizeAndroidLibraries"
DependsOnTargets="MavenRestore">

<!-- Find the microsoft-package.json file for NuGet package hints. -->
<GetMicrosoftNuGetPackagesMap MavenCacheDirectory="$(MavenCacheDirectory)">
<Output TaskParameter="ResolvedPackageMap" PropertyName="_ResolvedPackageMap" />
</GetMicrosoftNuGetPackagesMap>

<!-- Use downloaded POM files to ensure all Java dependencies are met. -->
<JavaDependencyVerification
AndroidLibraries="@(AndroidLibrary)"
AdditionalManifests="@(AndroidAdditionalJavaManifest)"
PackageReferences="@(PackageReference)"
ProjectReferences="@(ProjectReference)"
IgnoredDependencies="@(AndroidIgnoredDependency)"
MicrosoftPackagesFile="$(_ResolvedPackageMap)"
ProjectAssetsLockFile="$(ProjectAssetsFile)" />

</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This file is imported *after* the Microsoft.NET.Sdk/Sdk.targets.
<Import Project="..\tools\Xamarin.Android.Bindings.Core.targets" />
<Import Project="..\tools\Xamarin.Android.Bindings.ClassParse.targets" />
<Import Project="..\tools\Xamarin.Android.Bindings.Maven.targets" />
<Import Project="..\tools\Xamarin.Android.Bindings.JavaDependencyVerification.targets" />
<Import Project="Microsoft.Android.Sdk.AndroidLibraries.targets" />
<Import Project="Microsoft.Android.Sdk.Aot.targets" Condition=" '$(AndroidApplication)' == 'true' " />
<Import Project="Microsoft.Android.Sdk.Application.targets" Condition=" '$(AndroidApplication)' == 'true' " />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#nullable enable

using System;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Android.Build.Tasks;
using System.Net.Http;

namespace Xamarin.Android.Tasks;

public class GetMicrosoftNuGetPackagesMap : AndroidAsyncTask
{
public override string TaskPrefix => "GNP";

/// <summary>
/// The cache directory to use for Maven artifacts.
/// </summary>
[Required]
public string MavenCacheDirectory { get; set; } = null!; // NRT enforced by [Required]

[Output]
public string? ResolvedPackageMap { get; set; }

public async override System.Threading.Tasks.Task RunTaskAsync ()
{
// TODO: We should check the age of the existing file and only download if it's too old
var outfile = Path.Combine (MavenCacheDirectory, "microsoft-packages.json");

if (File.Exists (outfile)) {
ResolvedPackageMap = outfile;
return;
}

// File missing, download new one
try {
var http = new HttpClient ();
var json = await http.GetStringAsync ("https://aka.ms/ms-nuget-packages");

File.WriteAllText (outfile, json);
ResolvedPackageMap = outfile;
} catch (Exception ex) {
Log.LogMessage ("Could not download microsoft-packages.json: {0}", ex);
}
}
}

Loading

0 comments on commit f3e42e5

Please sign in to comment.