diff --git a/Documentation/guides/AndroidMavenLibrary.md b/Documentation/guides/AndroidMavenLibrary.md index e1e22bc5659..df3e09d6707 100644 --- a/Documentation/guides/AndroidMavenLibrary.md +++ b/Documentation/guides/AndroidMavenLibrary.md @@ -17,9 +17,10 @@ Note: This feature is only available in .NET 9+. ``` -This will do two things at build time: +This will do several things at build time: - Download the Java [artifact](https://central.sonatype.com/artifact/com.squareup.okhttp3/okhttp/4.9.3) with group id `com.squareup.okhttp3`, artifact id `okhttp`, and version `4.9.3` from [Maven Central](https://central.sonatype.com/) to a local cache (if not already cached). - Add the cached package to the .NET Android bindings build as an [``](https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/building-apps/build-items.md#androidlibrary). +- Download the Java artifact's POM file (and any needed parent/imported POM files) to enable [Java Dependency Verification](JavaDependencyVerification.md). To opt out of this feature, add `VerifyDependencies="false"` to the `` item. Note that only the requested Java artifact is added to the .NET Android bindings build. Any artifact dependencies are not added. If the requested artifact has dependencies, they must be fulfilled individually. diff --git a/Documentation/guides/JavaDependencyVerification.md b/Documentation/guides/JavaDependencyVerification.md new file mode 100644 index 00000000000..8972ae4117e --- /dev/null +++ b/Documentation/guides/JavaDependencyVerification.md @@ -0,0 +1,115 @@ +# 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.md) which will automatically download any needed POM files. + +For example: + +```xml + +``` + +automatically gets expanded to: + +```xml + + + + +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 `` item: + +```xml + + + + +``` + +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 + + com.squareup.okio + okio-parent + 1.17.4 + +``` + +Additionally, a POM file can "import" dependency information from another POM file: + +```xml + + + + com.squareup.okio + okio-bom + 3.0.0 + pom + import + + + +``` + +Dependency information cannot be accurately determined without also having access to these additional POM files, and will results in an error like: + +``` +error : Unable to resolve POM for artifact 'com.squareup.okio:okio-parent:1.17.4'. +``` + +In this case, we need to provide the POM file for `com.squareup.okio:okio-parent:1.17.4`: + +```xml + + + + +``` + +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 `` 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. \ No newline at end of file diff --git a/Documentation/guides/ResolvingJavaDependencies.md b/Documentation/guides/ResolvingJavaDependencies.md new file mode 100644 index 00000000000..3bc48d0aa02 --- /dev/null +++ b/Documentation/guides/ResolvingJavaDependencies.md @@ -0,0 +1,101 @@ +# 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 `` or manually via ``, 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. + +## `` + +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=androidx.collection:collection:1.0.0 +``` + +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 + +``` + +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. + +## `` + +If the needed Java dependency is provided by another project in your solution, you can annotate the `` to specify the dependency it fulfills: + +```xml + +``` + +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. + +## `` + +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 `` items to the project: + +```xml + + + +``` + +To include the Java library but not produce C# bindings for it, mark it with `Bind="false"`: + +```xml + + + +``` + +Alternatively, `` can be used to retrieve a Java library from a Maven repository: + +```xml + + + + + +``` + +> Note: If the dependency library has its own dependencies, you will be required to ensure they are fulfilled. + +## `` + +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 + + + +``` diff --git a/Documentation/guides/building-apps/build-items.md b/Documentation/guides/building-apps/build-items.md index 176e875aa3a..e6d6d954d75 100644 --- a/Documentation/guides/building-apps/build-items.md +++ b/Documentation/guides/building-apps/build-items.md @@ -14,6 +14,30 @@ ms.date: 07/26/2022 Build items control how a Xamarin.Android application or library project is built. +## AndroidAdditionalJavaManifest + +`` is used in conjunction with [Java Dependency Resolution](../JavaDependencyVerification.md). + +It is used to specify additional POM files that will be needed to verify dependencies. +These are often parent or imported POM files referenced by a Java library's POM file. + +```xml + + + +``` + +The following MSBuild metadata are required: + +- `%(JavaArtifact)`: The group and artifact id of the Java library matching the specifed POM + file in the form `{GroupId}:{ArtifactId}`. +- `%(JavaVersion)`: The version of the Java library matching the specified POM file. + +See the [Java Dependency Resolution documentation](../JavaDependencyVerification.md) +for more details. + +This build action was introduced in .NET 9. + ## AndroidAsset Supports [Android Assets](https://developer.android.com/guide/topics/resources/providing-resources#OriginalFiles), @@ -115,6 +139,30 @@ Files with a Build action of `AndroidJavaLibrary` are Java Archives ( `.jar` files) that will be included in the final Android package. +## AndroidIgnoredJavaDependency + +`` is used in conjunction with [Java Dependency Resolution](../JavaDependencyVerification.md). + +It is used to specify a Java dependency that should be ignored. This can be +used if a dependency will be fulfilled in a way that Java dependency resolution +cannot detect. + +```xml + + + + +``` + +The following MSBuild metadata are required: + +- `%(Version)`: The version of the Java library matching the specified `%(Include)`. + +See the [Java Dependency Resolution documentation](../JavaDependencyVerification.md) +for more details. + +This build action was introduced in .NET 9. + ## AndroidJavaSource Files with a Build action of `AndroidJavaSource` are Java source code that @@ -218,6 +266,17 @@ hosted in Maven. ``` + +The following MSBuild metadata are supported: + +- `%(Version)`: Required version of the Java library referenced by `%(Include)`. +- `%(Repository)`: Optional Maven repository to use. Supported values are `Central` (default), + `Google`, or an `https` URL to a Maven repository. + +The `` item is translated to an +[``](https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/building-apps/build-items.md#androidlibrary) +item, so any metadata supported by `` like `Bind` or `Pack` are also supported. + See the [AndroidMavenLibrary documentation](../AndroidMavenLibrary.md) for more details. diff --git a/Documentation/guides/messages/README.md b/Documentation/guides/messages/README.md index df2c6915f35..e44f87d9b9b 100644 --- a/Documentation/guides/messages/README.md +++ b/Documentation/guides/messages/README.md @@ -186,6 +186,19 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + XA4230: Error parsing XML: {exception} + [XA4231](xa4231.md): The Android class parser value 'jar2xml' is deprecated and will be removed in a future version of Xamarin.Android. Update the project properties to use 'class-parse'. + [XA4232](xa4232.md): The Android code generation target 'XamarinAndroid' is deprecated and will be removed in a future version of Xamarin.Android. Update the project properties to use 'XAJavaInterop1'. ++ [XA4234](xa4234.md): '<{item}>' item '{itemspec}' is missing required attribute '{name}'. ++ [XA4235](xa4235.md): Maven artifact specification '{artifact}' is invalid. The correct format is 'group_id:artifact_id'. ++ [XA4236](xa4236.md): Cannot download Maven artifact '{group}:{artifact}'. - {jar}: {exception} - {aar}: {exception} ++ [XA4237](xa4237.md): Cannot download POM file for Maven artifact '{artifact}'. - {exception} ++ [XA4239](xa4239.md): Unknown Maven repository: '{repository}'. ++ [XA4241](xa4241.md): Java dependency '{artifact}' is not satisfied. ++ [XA4242](xa4242.md): Java dependency '{artifact}' is not satisfied. Microsoft maintains the NuGet package '{nugetId}' that could fulfill this dependency. ++ [XA4243](xa4243.md): Attribute '{name}' is required when using '{name}' for '{element}' item '{itemspec}'. ++ [XA4244](xa4244.md): Attribute '{name}' cannot be empty for '{element}' item '{itemspec}'. ++ [XA4245](xa4245.md): Specified POM file '{file}' does not exist. ++ [XA4246](xa4246.md): Could not parse POM file '{file}'. - {exception} ++ [XA4247](xa4247.md): Could not resolve POM file for artifact '{artifact}'. ++ [XA4248](xa4248.md): Could not find NuGet package '{nugetId}' version '{version}' in lock file. Ensure NuGet Restore has run since this was added. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/guides/messages/xa4234.md b/Documentation/guides/messages/xa4234.md new file mode 100644 index 00000000000..acb67c440fa --- /dev/null +++ b/Documentation/guides/messages/xa4234.md @@ -0,0 +1,34 @@ +--- +title: .NET Android error XA4234 +description: XA4234 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4234 + +## Example message + +``` +error XA4234: '' item 'com.example:mylib' is missing required attribute 'Version'. +``` + +## Issue + +The specified MSBuild XML item requires the specified XML attribute. + +For example the following item is missing the required 'Version' attribute: + +```xml + + + +``` + +## Solution + +To resolve this error, ensure that the specified XML contains the specified attribute: + +```xml + + + +``` diff --git a/Documentation/guides/messages/xa4235.md b/Documentation/guides/messages/xa4235.md new file mode 100644 index 00000000000..256115b42ce --- /dev/null +++ b/Documentation/guides/messages/xa4235.md @@ -0,0 +1,34 @@ +--- +title: .NET Android error XA4235 +description: XA4235 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4235 + +## Example message + +``` +error XA4235: Maven artifact specification 'com.example' is invalid. The correct format is 'group_id:artifact_id'. +``` + +## Issue + +The specified Maven artifact specification is invalid. + +For example the following item uses a comma separator instead of a colon: + +```xml + + + +``` + +## Solution + +To resolve this error, ensure that the artifact specification is of the form 'group_id:artifact_id': + +```xml + + + +``` diff --git a/Documentation/guides/messages/xa4236.md b/Documentation/guides/messages/xa4236.md new file mode 100644 index 00000000000..ba352dae46a --- /dev/null +++ b/Documentation/guides/messages/xa4236.md @@ -0,0 +1,37 @@ +--- +title: .NET Android error XA4236 +description: XA4236 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4236 + +## Example message + +``` +error XA4236: Cannot download Maven artifact 'com.example:mylib'. +error XA4236: - mylib-1.0.0.jar: Response status code does not indicate success: 404 (Not Found). +error XA4236: - mylib-1.0.0.aar: Response status code does not indicate success: 404 (Not Found). +``` + +## Issue + +Errors were encountered while trying to download the requested Java library from Maven. + +For example the following item doesn't actually exist on Maven Central, resulting in "Not Found": + +```xml + + + +``` + +## Solution + +Resolving this error depends on the error message specified. + +It could be things like: +- Check your internet connection. +- Ensure you have specified the correct group id and artifact id. +- Ensure you have specified the correct Maven repository. + +Additional documentation about configuring `` is available [here](../AndroidMavenLibrary.md). diff --git a/Documentation/guides/messages/xa4237.md b/Documentation/guides/messages/xa4237.md new file mode 100644 index 00000000000..d02c16d8481 --- /dev/null +++ b/Documentation/guides/messages/xa4237.md @@ -0,0 +1,24 @@ +--- +title: .NET Android error XA4237 +description: XA4237 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4237 + +## Example message + +``` +error XA4237: Cannot download POM file for Maven artifact 'com.example:mylib':1.0.0. +error XA4237: - mylib-1.0.0.pom: Response status code does not indicate success: 404 (Not Found). +``` + +## Issue + +An error was encountered while trying to download the requested POM file from Maven. + +## Solution + +Resolving this error depends on the error message specified. + +It could be things like: +- Check your internet connection. diff --git a/Documentation/guides/messages/xa4239.md b/Documentation/guides/messages/xa4239.md new file mode 100644 index 00000000000..dfc49124f7e --- /dev/null +++ b/Documentation/guides/messages/xa4239.md @@ -0,0 +1,34 @@ +--- +title: .NET Android error XA4239 +description: XA4239 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4239 + +## Example message + +``` +error XA4239: Unknown Maven repository: 'example.com'. +``` + +## Issue + +The specified Maven repository is invalid. + +For example the following Maven repository must be specified with `https://`: + +```xml + + + +``` + +## Solution + +To resolve this error, ensure that the Maven repository follows the [documented values](../AndroidMavenLibrary.md): + +```xml + + + +``` diff --git a/Documentation/guides/messages/xa4241.md b/Documentation/guides/messages/xa4241.md new file mode 100644 index 00000000000..8b2efb91dfb --- /dev/null +++ b/Documentation/guides/messages/xa4241.md @@ -0,0 +1,22 @@ +--- +title: .NET Android error XA4241 +description: XA4241 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4241 + +## Example message + +``` +error XA4241: Java dependency 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' is not satisfied. +``` + +## Issue + +The specified Java dependency could not be verified using the [Java Dependency Verification](../JavaDependencyVerification.md) +feature. + +## Solution + +To resolve this error, follow the available options in the [Resolving Java Dependencies](../ResolvingJavaDependencies.md) +documentation. diff --git a/Documentation/guides/messages/xa4242.md b/Documentation/guides/messages/xa4242.md new file mode 100644 index 00000000000..f3c6b0a2e7e --- /dev/null +++ b/Documentation/guides/messages/xa4242.md @@ -0,0 +1,27 @@ +--- +title: .NET Android error XA4242 +description: XA4242 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4242 + +## Example message + +Java dependency '{0}' is not satisfied. Microsoft maintains the NuGet package '{1}' that could fulfill this dependency. + +``` +error XA4242: Java dependency 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' is not satisfied. +Microsoft maintains the NuGet package 'Xamarin.Kotlin.StdLib' that could fulfill this dependency. +``` + +## Issue + +The specified Java dependency could not be verified using the [Java Dependency Verification](../JavaDependencyVerification.md) +feature. + +## Solution + +Add a reference to the specified NuGet package to the project. + +Alternatively, choose one of the other available options in the [Resolving Java Dependencies](../ResolvingJavaDependencies.md) +documentation. diff --git a/Documentation/guides/messages/xa4243.md b/Documentation/guides/messages/xa4243.md new file mode 100644 index 00000000000..02e26b882a5 --- /dev/null +++ b/Documentation/guides/messages/xa4243.md @@ -0,0 +1,36 @@ +--- +title: .NET Android error XA4243 +description: XA4243 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4243 + +## Example message + +``` +error XA4243: Attribute 'JavaVersion' is required when using 'JavaArtifact' for 'ProjectReference' item '../ReferenceProject.csproj'. +``` + +## Issue + +The referenced MSBuild item XML specifies an attribute that makes an additional attribute required. + +For example, using the `JavaArtifact` attribute on a `` requires `JavaVersion` to also be specified. + +Invalid: + +```xml + + + +``` + +## Solution + +To resolve this error, specify the required XML attribute: + +```xml + + + +``` diff --git a/Documentation/guides/messages/xa4244.md b/Documentation/guides/messages/xa4244.md new file mode 100644 index 00000000000..016a1d72d80 --- /dev/null +++ b/Documentation/guides/messages/xa4244.md @@ -0,0 +1,36 @@ +--- +title: .NET Android error XA4244 +description: XA4244 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4244 + +## Example message + +``` +error XA4244: Attribute 'JavaVersion' cannot be empty for 'ProjectReference' item '../ReferenceProject.csproj'. +``` + +## Issue + +The referenced MSBuild item XML specifies a required attribute but omits a required value. + +For example, the `JavaArtifact` attribute on a `` cannot have an empty value. + +Invalid: + +```xml + + + +``` + +## Solution + +To resolve this error, specify a value for the required XML attribute: + +```xml + + + +``` diff --git a/Documentation/guides/messages/xa4245.md b/Documentation/guides/messages/xa4245.md new file mode 100644 index 00000000000..7d72a3c6017 --- /dev/null +++ b/Documentation/guides/messages/xa4245.md @@ -0,0 +1,32 @@ +--- +title: .NET Android error XA4245 +description: XA4245 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4245 + +## Example message + +``` +error XA4245: Specified POM file 'mylib.pom' does not exist. +``` + +## Issue + +The referenced MSBuild item XML specifies a POM file that cannot be found. + +```xml + + + +``` + +```xml + + + +``` + +## Solution + +To resolve this error, ensure the requested POM file exists in the specified location. diff --git a/Documentation/guides/messages/xa4246.md b/Documentation/guides/messages/xa4246.md new file mode 100644 index 00000000000..ce118c42ea5 --- /dev/null +++ b/Documentation/guides/messages/xa4246.md @@ -0,0 +1,21 @@ +--- +title: .NET Android error XA4246 +description: XA4246 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4246 + +## Example message + +``` +error XA4246: Could not parse POM file 'mylib.pom'. +error XA4246: - There is an error in XML document (1, 1). +``` + +## Issue + +The referenced POM file cannot be parsed. + +## Solution + +To resolve this error, ensure the requested POM file is valid XML. diff --git a/Documentation/guides/messages/xa4247.md b/Documentation/guides/messages/xa4247.md new file mode 100644 index 00000000000..0e86ad7326c --- /dev/null +++ b/Documentation/guides/messages/xa4247.md @@ -0,0 +1,22 @@ +--- +title: .NET Android error XA4247 +description: XA4247 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4247 + +## Example message + +``` +error XA4247: Could not resolve POM file for artifact 'com.example:mylib-parent:1.0.0'. +``` + +## Issue + +The specified POM file is needed to complete Java dependency verification, but it could +not be found. This may be due to a missing parent or imported POM file. + +## Solution + +For solutions for fixing this error, refer to the "Additional POM Files" section of the +[Java Dependency Verification](../JavaDependencyVerification.md) documentation. diff --git a/Documentation/guides/messages/xa4248.md b/Documentation/guides/messages/xa4248.md new file mode 100644 index 00000000000..a6508b777cf --- /dev/null +++ b/Documentation/guides/messages/xa4248.md @@ -0,0 +1,21 @@ +--- +title: .NET Android error XA4248 +description: XA4248 error code +ms.date: 02/26/2024 +--- +# .NET Android error XA4248 + +## Example message + +``` +error XA4248: Could not find NuGet package 'Xamarin.Kotlin.StdLib' version '1.9.0' in lock file. Ensure NuGet Restore has run since this was added. +``` + +## Issue + +The NuGet lock file has not been generated and thus Java dependencies from NuGet packages +cannot be resolved. + +## Solution + +Run NuGet Restore for the project to ensure the lock file has been generated. diff --git a/build-tools/installers/create-installers.targets b/build-tools/installers/create-installers.targets index 85c8b990475..daa577014a5 100644 --- a/build-tools/installers/create-installers.targets +++ b/build-tools/installers/create-installers.targets @@ -104,14 +104,14 @@ <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)HtmlAgilityPack.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)HtmlAgilityPack.pdb" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Irony.dll" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Java.Interop.Tools.Maven.dll" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Java.Interop.Tools.Maven.pdb" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java-interop.jar" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java-source-utils.jar" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)LayoutBinding.cs" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libZipSharp.dll" /> <_MSBuildFiles Include="@(_LocalizationLanguages->'$(MicrosoftAndroidSdkOutDir)%(Identity)\libZipSharp.resources.dll')" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libZipSharp.pdb" /> - <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)MavenNet.dll" /> - <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)MavenNet.pdb" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Mono.Unix.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Mono.Unix.pdb" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Microsoft.Android.Build.BaseTasks.dll" /> @@ -139,6 +139,7 @@ <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Assets.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.ClassParse.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.Core.targets" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.JavaDependencyVerification.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.Maven.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Build.Tasks.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Build.Tasks.pdb" /> diff --git a/build-tools/xaprepare/xaprepare/ThirdPartyNotices/MavenNet.cs b/build-tools/xaprepare/xaprepare/ThirdPartyNotices/MavenNet.cs deleted file mode 100644 index a09a63de12f..00000000000 --- a/build-tools/xaprepare/xaprepare/ThirdPartyNotices/MavenNet.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace Xamarin.Android.Prepare -{ - [TPN] - class MavenNet_TPN : ThirdPartyNotice - { - static readonly Uri url = new Uri ("https://github.com/Redth/MavenNet/"); - - public override string LicenseFile => string.Empty; - public override string Name => "Redth/MavenNet"; - public override Uri SourceUrl => url; - public override string LicenseText => @" -MIT License - -Copyright (c) 2017 Jonathan Dick - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the ""Software""), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE."; - - public override bool Include (bool includeExternalDeps, bool includeBuildDeps) => includeExternalDeps; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.JavaDependencyVerification.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.JavaDependencyVerification.targets new file mode 100644 index 00000000000..7fa94d517d1 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.JavaDependencyVerification.targets @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Maven.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Maven.targets index e47e4b6bd93..07e287a991a 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Maven.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Maven.targets @@ -19,7 +19,7 @@ This file contains MSBuild targets used to enable @(AndroidMavenLibrary) support $([MSBuild]::EnsureTrailingSlash('$(MavenCacheDirectory)')) - @@ -27,11 +27,13 @@ This file contains MSBuild targets used to enable @(AndroidMavenLibrary) support + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.After.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.After.targets index 95063269a7b..53614fc28f1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.After.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.After.targets @@ -18,6 +18,7 @@ This file is imported *after* the Microsoft.NET.Sdk/Sdk.targets. + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index c5df25bdcc2..0c326142438 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1191,6 +1191,79 @@ public static string XA4239 { } } + /// + /// Looks up a localized string similar to Java dependency '{0}' is not satisfied.. + /// + public static string XA4241 { + get { + return ResourceManager.GetString("XA4241", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Java dependency '{0}' is not satisfied. Microsoft maintains the NuGet package '{1}' that could fulfill this dependency.. + /// + public static string XA4242 { + get { + return ResourceManager.GetString("XA4242", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attribute '{0}' is required when using '{1}' for '{2}' item '{3}'.. + /// + public static string XA4243 { + get { + return ResourceManager.GetString("XA4243", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attribute '{0}' cannot be empty for '{1}' item '{2}'.. + /// + public static string XA4244 { + get { + return ResourceManager.GetString("XA4244", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Specified POM file '{0}' does not exist.. + /// + public static string XA4245 { + get { + return ResourceManager.GetString("XA4245", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not parse POM file '{0}'. + ///- {1}. + /// + public static string XA4246 { + get { + return ResourceManager.GetString("XA4246", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not resolve POM file for artifact '{0}'.. + /// + public static string XA4247 { + get { + return ResourceManager.GetString("XA4247", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find NuGet package '{0}' version '{1}' in lock file. Ensure NuGet Restore has run since this <PackageReference> was added.. + /// + public static string XA4248 { + get { + return ResourceManager.GetString("XA4248", resourceCulture); + } + } + /// /// Looks up a localized string similar to Native library '{0}' will not be bundled because it has an unsupported ABI. Move this file to a directory with a valid Android ABI name such as 'libs/armeabi-v7a/'.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index f473cd0f265..390cd9272da 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -968,7 +968,7 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS - '<{0}>' item '{1}' is missing required metadata '{2}' + '<{0}>' item '{1}' is missing required attribute '{2}'. {0} - The MSBuild ItemGroup Item name {1} - The MSBuild Item ItemSpec {2} - The omitted MSBuild Item metadata attribute @@ -991,23 +991,12 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS {5} - The HttpClient provided download exception message - Cannot download POM file for Maven artifact '{0}:{1}'. -- {2}: {3} + Cannot download POM file for Maven artifact '{0}'. +- {1} The following are literal names and should not be translated: POM, Maven -{0} - Maven artifact group id -{1} - Maven artifact id -{2} - The .pom filename we tried to download -{3} - The HttpClient reported download exception message +{0} - Maven artifact id +{1} - The HttpClient reported download exception message - - - Cannot download parent POM file for Maven artifact '{0}:{1}'. -- {2}: {3} - The following are literal names and should not be translated: POM, Maven -{0} - Maven artifact group id -{1} - Maven artifact id -{2} - The .pom filename we tried to download -{3} - The HttpClient reported download exception message Unknown Maven repository: '{0}'. @@ -1024,4 +1013,50 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS The Android Support libraries are not supported in .NET 9 and later, please migrate to AndroidX. See https://aka.ms/xamarin/androidx for more details. The following are literal names and should not be translated: Android Support, AndroidX, .NET. + + Java dependency '{0}' is not satisfied. + The following are literal names and should not be translated: Java. +{0} - Maven dependency id + + + Java dependency '{0}' is not satisfied. Microsoft maintains the NuGet package '{1}' that could fulfill this dependency. + The following are literal names and should not be translated: Java, Microsoft, NuGet. +{0} - Maven dependency id +{1} - NuGet package id + + + Attribute '{0}' is required when using '{1}' for '{2}' item '{3}'. + {0}, {1} - MSBuild XML attribute +{2} - MSBuild XML element name +{3} - MSBuild ItemSpec value + + + Attribute '{0}' cannot be empty for '{1}' item '{2}'. + {0} - MSBuild XML attribute +{1} - MSBuild XML element name +{2} - MSBuild ItemSpec value + + + Specified POM file '{0}' does not exist. + The following are literal names and should not be translated: POM. +{0} - File path to POM file + + + Could not parse POM file '{0}'. +- {1} + The following are literal names and should not be translated: POM. +{0} - File path to POM file +{1} - Exception message + + + Could not resolve POM file for artifact '{0}'. + The following are literal names and should not be translated: POM. +{0} - Java artifact id + + + Could not find NuGet package '{0}' version '{1}' in lock file. Ensure NuGet Restore has run since this <PackageReference> was added. + The following are literal names and should not be translated: NuGet, PackageReference. +{0} - NuGet package id +{1} - NuGet package version + \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetMicrosoftNuGetPackagesMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GetMicrosoftNuGetPackagesMap.cs new file mode 100644 index 00000000000..d6a1557944d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GetMicrosoftNuGetPackagesMap.cs @@ -0,0 +1,112 @@ +#nullable enable + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Android.Build.Tasks; +using System.Net.Http; +using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; + +namespace Xamarin.Android.Tasks; + +public class GetMicrosoftNuGetPackagesMap : AndroidAsyncTask +{ + static readonly HttpClient http_client = new HttpClient (); + + public override string TaskPrefix => "GNP"; + + /// + /// The cache directory to use for Maven artifacts. + /// + [Required] + public string MavenCacheDirectory { get; set; } = null!; // NRT enforced by [Required] + + [Output] + public string? ResolvedPackageMap { get; set; } + + public override async System.Threading.Tasks.Task RunTaskAsync () + { + Directory.CreateDirectory (MavenCacheDirectory); + + // We're going to store the resolved package map in the cache directory as + // "microsoft-packages-{YYYYMMDD}.json". If the file is older than today, + // we'll try to download a new one. + var all_files = PackagesFile.FindAll (MavenCacheDirectory); + + if (!all_files.Any (x => x.IsToday)) { + // No file for today, download a new one + try { + var json = await http_client.GetStringAsync ("https://aka.ms/ms-nuget-packages"); + var outfile = Path.Combine (MavenCacheDirectory, $"microsoft-packages-{DateTime.Today:yyyyMMdd}.json"); + + File.WriteAllText (outfile, json); + + if (PackagesFile.TryParse (outfile, out var packagesFile)) + all_files.Insert (0, packagesFile); // Sorted so this one is first + + } catch (Exception ex) { + Log.LogMessage ("Could not download microsoft-packages.json: {0}", ex.Message); + } + } + + // Delete all files but the latest + foreach (var file in all_files.Skip (1)) { + try { + File.Delete (Path.Combine (MavenCacheDirectory, file.FileName)); + } catch { + // Ignore exceptions + } + } + + ResolvedPackageMap = all_files.FirstOrDefault ()?.FileName; + } +} + +class PackagesFile +{ + public string FileName { get; } + public DateTime DownloadDate { get; } + public bool IsToday => DownloadDate == DateTime.Today; + + PackagesFile (string filename, DateTime downloadDate) + { + FileName = filename; + DownloadDate = downloadDate; + } + + public static List FindAll (string directory) + { + var files = new List (); + + foreach (var file in Directory.GetFiles (directory, "microsoft-packages-*.json")) { + if (TryParse (file, out var packagesFile)) + files.Add (packagesFile); + } + + files.OrderByDescending (x => x.DownloadDate); + + return files; + } + + public static bool TryParse (string filepath, [NotNullWhen (true)]out PackagesFile? file) + { + file = default; + + var filename = Path.GetFileNameWithoutExtension (filepath); + + if (!filename.StartsWith ("microsoft-packages-", StringComparison.OrdinalIgnoreCase)) + return false; + + var date = filename.Substring ("microsoft-packages-".Length); + + if (!DateTime.TryParseExact (date, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var downloadDate)) + return false; + + file = new PackagesFile (filepath, downloadDate); + + return true; + } +} + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs b/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs new file mode 100644 index 00000000000..441e1b3ccd5 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs @@ -0,0 +1,428 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Java.Interop.Tools.Maven; +using Java.Interop.Tools.Maven.Models; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Newtonsoft.Json; +using NuGet.ProjectModel; + +namespace Xamarin.Android.Tasks; + +public class JavaDependencyVerification : AndroidTask +{ + public override string TaskPrefix => "JDV"; + + /// + /// Java libraries whose dependencies we are being asked to verify. + /// + public ITaskItem []? AndroidLibraries { get; set; } + + /// + /// Additional POM files (like parent POMs) that we should use to resolve dependencies. + /// + public ITaskItem []? AdditionalManifests { get; set; } + + /// + /// NuGet packages this project consumes, which may fulfill Java dependencies. + /// + public ITaskItem []? PackageReferences { get; set; } + + /// + /// Projects this project references, which may fulfill Java dependencies. + /// + public ITaskItem []? ProjectReferences { get; set; } + + /// + /// Dependencies that we should ignore if they are missing. + /// + public ITaskItem []? IgnoredDependencies { get; set; } + + /// + /// The file location of 'microsoft-packages.json'. + /// + public string? MicrosoftPackagesFile { get; set; } + + public string? ProjectAssetsLockFile { get; set; } + + public override bool RunTask () + { + // Bail if no specifies a "Manifest" we need to verify + if (!(AndroidLibraries?.Select (al => al.GetMetadata ("Manifest")).Any (al => al.HasValue ()) ?? false)) + return true; + + // Populate the POM resolver with the POMs we know about + var pom_resolver = new MSBuildLoggingPomResolver (Log); + var poms_to_verify = new List (); + + foreach (var pom in AndroidLibraries ?? []) + if (pom_resolver.RegisterFromAndroidLibrary (pom) is Artifact art) + poms_to_verify.Add (art); + + foreach (var pom in AdditionalManifests ?? []) + pom_resolver.RegisterFromAndroidAdditionalJavaManifest (pom); + + // If there were errors loading the requested POMs, bail + if (Log.HasLoggedErrors) + return false; + + // Populate the dependency resolver with every dependency we know about + var resolver = new DependencyResolver (ProjectAssetsLockFile, Log); + + resolver.AddAndroidLibraries (AndroidLibraries); + resolver.AddPackageReferences (PackageReferences); + resolver.AddProjectReferences (ProjectReferences); + resolver.AddIgnoredDependencies (IgnoredDependencies); + + // Parse microsoft-packages.json so we can provide package recommendations + var ms_packages = new MicrosoftNuGetPackageFinder (MicrosoftPackagesFile, Log); + + // Verify dependencies + foreach (var pom in poms_to_verify) { + if (TryResolveProject (pom, pom_resolver, out var resolved_pom)) { + foreach (var dependency in resolved_pom.Dependencies.Where (d => (d.IsRuntimeDependency () || d.IsCompileDependency ()) && !d.IsOptional ())) + resolver.EnsureDependencySatisfied (dependency, ms_packages); + } + } + + return !Log.HasLoggedErrors; + } + + static bool TryResolveProject (Artifact artifact, IProjectResolver resolver, [NotNullWhen (true)]out ResolvedProject? project) + { + // ResolvedProject.FromArtifact will throw if a POM cannot be resolved, but our MSBuildLoggingPomResolver + // has already logged the failure as an MSBuild error. We don't want to log it again as an unhandled exception. + try { + project = ResolvedProject.FromArtifact (artifact, resolver); + return true; + } catch { + project = null; + return false; + } + } +} + +class DependencyResolver +{ + readonly Dictionary artifacts = new (); + + readonly NuGetPackageVersionFinder? finder; + readonly TaskLoggingHelper log; + + public DependencyResolver (string? lockFile, TaskLoggingHelper log) + { + this.log = log; + + if (File.Exists (lockFile)) + finder = NuGetPackageVersionFinder.Create (lockFile!, log); + } + + public bool EnsureDependencySatisfied (ResolvedDependency dependency, MicrosoftNuGetPackageFinder packages) + { + if (!dependency.Version.HasValue ()) + log.LogMessage ("Could not determine required version of Java dependency '{0}:{1}'. Validation of this dependency will not take version into account.", dependency.GroupId, dependency.ArtifactId); + + var satisfied = TrySatisfyDependency (dependency); + + if (satisfied) + return true; + + var suggestion = packages.GetNuGetPackage ($"{dependency.GroupId}:{dependency.ArtifactId}"); + var artifact_spec = dependency.Version.HasValue () ? dependency.VersionedArtifactString : dependency.ArtifactString; + + if (suggestion is string nuget) + log.LogCodedError ("XA4242", Properties.Resources.XA4242, artifact_spec, nuget); + else + log.LogCodedError ("XA4241", Properties.Resources.XA4241, artifact_spec); + + return false; + } + + public void AddAndroidLibraries (ITaskItem []? tasks) + { + foreach (var task in tasks.OrEmpty ()) { + var id = task.GetMetadataOrDefault ("JavaArtifact", string.Empty); + var version = task.GetMetadataOrDefault ("JavaVersion", string.Empty); + + // TODO: Should raise an error if JavaArtifact is specified but JavaVersion is not + if (!id.HasValue () || !version.HasValue ()) + continue; + + if (version != null && MavenExtensions.TryParseArtifactWithVersion (id, version, log, out var art)) { + log.LogMessage ("Found Java dependency '{0}:{1}' version '{2}' from AndroidLibrary '{3}'", art.GroupId, art.Id, art.Version, task.ItemSpec); + artifacts.Add (art.ArtifactString, art); + } + } + } + + public void AddPackageReferences (ITaskItem []? tasks) + { + foreach (var task in tasks.OrEmpty ()) { + + // See if JavaArtifact/JavaVersion overrides were used + if (task.TryParseJavaArtifactAndJavaVersion ("PackageReference", log, out var explicit_artifact, out var attributes_specified)) { + artifacts.Add (explicit_artifact.ArtifactString, explicit_artifact); + continue; + } + + // If user tried to specify JavaArtifact or JavaVersion, but did it incorrectly, we do not perform any fallback + if (attributes_specified) + continue; + + // Try parsing the NuGet metadata for Java version information instead + var artifact = finder?.GetJavaInformation (task.ItemSpec, task.GetMetadataOrDefault ("Version", string.Empty), log); + + if (artifact != null) { + log.LogMessage ("Found Java dependency '{0}:{1}' version '{2}' from PackageReference '{3}'", artifact.GroupId, artifact.Id, artifact.Version, task.ItemSpec); + artifacts.Add (artifact.ArtifactString, artifact); + + continue; + } + + log.LogMessage ("No Java artifact information found for PackageReference '{0}'", task.ItemSpec); + } + } + + public void AddProjectReferences (ITaskItem []? tasks) + { + foreach (var task in tasks.OrEmpty ()) { + // See if JavaArtifact/JavaVersion overrides were used + if (task.TryParseJavaArtifactAndJavaVersion ("ProjectReference", log, out var explicit_artifact, out var attributes_specified)) { + artifacts.Add (explicit_artifact.ArtifactString, explicit_artifact); + continue; + } + + // If user tried to specify JavaArtifact or JavaVersion, but did it incorrectly, we do not perform any fallback + if (attributes_specified) + continue; + + // There currently is no alternate way to figure this out. Perhaps in + // the future we could somehow parse the project to find it automatically? + } + } + + public void AddIgnoredDependencies (ITaskItem []? tasks) + { + foreach (var task in tasks.OrEmpty ()) { + var id = task.ItemSpec; + var version = task.GetRequiredMetadata ("AndroidIgnoredJavaDependency", "Version", log); + + if (version is null) + continue; + + if (version != null && MavenExtensions.TryParseArtifactWithVersion (id, version, log, out var art)) { + log.LogMessage ("Ignoring Java dependency '{0}:{1}' version '{2}'", art.GroupId, art.Id, art.Version); + artifacts.Add (art.ArtifactString, art); + } + } + } + + bool TrySatisfyDependency (ResolvedDependency dependency) + { + if (!dependency.Version.HasValue ()) + return artifacts.ContainsKey (dependency.ArtifactString); + + var dep_versions = MavenVersionRange.Parse (dependency.Version); + + if (artifacts.TryGetValue (dependency.ArtifactString, out var artifact)) + return dep_versions.Any (r => r.ContainsVersion (MavenVersion.Parse (artifact.Version))); + + return false; + } +} + +class MSBuildLoggingPomResolver : IProjectResolver +{ + readonly TaskLoggingHelper logger; + readonly Dictionary poms = new (); + + public MSBuildLoggingPomResolver (TaskLoggingHelper logger) + { + this.logger = logger; + } + + public Artifact? RegisterFromAndroidLibrary (ITaskItem item) + { + var pom_file = item.GetMetadata ("Manifest"); + + if (!pom_file.HasValue ()) + return null; + + return RegisterFromTaskItem (item, "AndroidLibrary", pom_file); + } + + public Artifact? RegisterFromAndroidAdditionalJavaManifest (ITaskItem item) + => RegisterFromTaskItem (item, "AndroidAdditionalJavaManifest", item.ItemSpec); + + Artifact? RegisterFromTaskItem (ITaskItem item, string itemName, string filename) + { + item.TryParseJavaArtifactAndJavaVersion (itemName, logger, out var artifact, out var _); + + if (!File.Exists (filename)) { + logger.LogCodedError ("XA4245", Properties.Resources.XA4245, filename); + return null; + } + + try { + using (var file = File.OpenRead (filename)) { + var project = Project.Load (file); + var registered_artifact = Artifact.Parse (project.VersionedArtifactString); + + // Return the registered artifact, preferring any overrides specified in the task item + var final_artifact = new Artifact ( + artifact?.GroupId ?? registered_artifact.GroupId, + artifact?.Id ?? registered_artifact.Id, + artifact?.Version ?? registered_artifact.Version + ); + + // Use index instead of Add to handle duplicates + poms [final_artifact.VersionedArtifactString] = project; + + logger.LogDebugMessage ("Registered POM for artifact '{0}' from '{1}'", final_artifact, filename); + + return final_artifact; + } + } catch (Exception ex) { + logger.LogCodedError ("XA4246", Properties.Resources.XA4246, filename, ex.Message); + return null; + } + } + + public Project Resolve (Artifact artifact) + { + if (poms.TryGetValue (artifact.VersionedArtifactString, out var project)) + return project; + + logger.LogCodedError ("XA4247", Properties.Resources.XA4247, artifact); + + throw new InvalidOperationException ($"No POM registered for {artifact}"); + } +} + +class MicrosoftNuGetPackageFinder +{ + readonly PackageListFile? package_list; + + public MicrosoftNuGetPackageFinder (string? file, TaskLoggingHelper log) + { + if (file is null || !File.Exists (file)) { + log.LogMessage ("'microsoft-packages.json' file not found, Android NuGet suggestions will not be provided"); + return; + } + + try { + var json = File.ReadAllText (file); + package_list = JsonConvert.DeserializeObject (json); + } catch (Exception ex) { + log.LogMessage ("There was an error reading 'microsoft-packages.json', Android NuGet suggestions will not be provided: {0}", ex); + } + } + + public string? GetNuGetPackage (string javaId) + { + return package_list?.Packages?.FirstOrDefault (p => p.JavaId?.Equals (javaId, StringComparison.OrdinalIgnoreCase) == true)?.NuGetId; + } + + public class PackageListFile + { + [JsonProperty ("packages")] + public List? Packages { get; set; } + } + + public class Package + { + [JsonProperty ("javaId")] + public string? JavaId { get; set; } + + [JsonProperty ("nugetId")] + public string? NuGetId { get; set; } + } +} + +public class NuGetPackageVersionFinder +{ + readonly LockFile lock_file; + readonly Dictionary cache = new Dictionary (); + readonly Regex tag = new Regex ("artifact_versioned=(?.+)?:(?.+?):(?.+)\\s?", RegexOptions.Compiled); + readonly Regex tag2 = new Regex ("artifact=(?.+)?:(?.+?):(?.+)\\s?", RegexOptions.Compiled); + + NuGetPackageVersionFinder (LockFile lockFile) + { + lock_file = lockFile; + } + + public static NuGetPackageVersionFinder? Create (string filename, TaskLoggingHelper log) + { + try { + var lock_file_format = new LockFileFormat (); + var lock_file = lock_file_format.Read (filename); + return new NuGetPackageVersionFinder (lock_file); + } catch (Exception e) { + log.LogMessage ("Could not parse NuGet lock file. Java dependencies fulfilled by NuGet packages may not be available: '{0}'.", e.Message); + return null; + } + } + + public Artifact? GetJavaInformation (string library, string version, TaskLoggingHelper log) + { + // Check if we already have this one in the cache + var dictionary_key = $"{library.ToLowerInvariant ()}:{version}"; + + if (cache.TryGetValue (dictionary_key, out var artifact)) + return artifact; + + // Find the LockFileLibrary + var nuget = lock_file.GetLibrary (library, new NuGet.Versioning.NuGetVersion (version)); + + if (nuget is null) { + log.LogCodedError ("XA4248", Properties.Resources.XA4248, library, version); + return null; + } + + foreach (var path in lock_file.PackageFolders) + if (CheckFilePath (path.Path, nuget) is Artifact art) { + cache.Add (dictionary_key, art); + return art; + } + + return null; + } + + Artifact? CheckFilePath (string nugetPackagePath, LockFileLibrary package) + { + // Check NuGet tags + var nuspec = package.Files.FirstOrDefault (f => f.EndsWith (".nuspec", StringComparison.OrdinalIgnoreCase)); + + if (nuspec is null) + return null; + + nuspec = Path.Combine (nugetPackagePath, package.Path, nuspec); + + if (!File.Exists (nuspec)) + return null; + + var reader = new NuGet.Packaging.NuspecReader (nuspec); + var tags = reader.GetTags (); + + // Try the first tag format + var match = tag.Match (tags); + + // Try the second tag format + if (!match.Success) + match = tag2.Match (tags); + + if (!match.Success) + return null; + + // TODO: Define a well-known file that can be included in the package like "java-package.txt" + + return new Artifact (match.Groups ["GroupId"].Value, match.Groups ["ArtifactId"].Value, match.Groups ["Version"].Value); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs b/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs index 1a0928d1552..383cc1a2d2f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs @@ -1,8 +1,14 @@ +#nullable enable + using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using MavenNet; -using MavenNet.Models; +using System.Security.Cryptography; +using System.Text; +using Java.Interop.Tools.Maven; +using Java.Interop.Tools.Maven.Models; +using Java.Interop.Tools.Maven.Repositories; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -30,11 +36,18 @@ public class MavenDownload : AndroidAsyncTask [Output] public ITaskItem []? ResolvedAndroidMavenLibraries { get; set; } + /// + /// The set of additional parent and imported POM files needed to verify these Maven libraries. + /// + [Output] + public ITaskItem []? AndroidAdditionalJavaManifest { get; set; } + public async override System.Threading.Tasks.Task RunTaskAsync () { var resolved = new List (); + var additional_poms = new List (); - // Note each called function is responsible for raising any errors it encounters to the user + // Note each called function is responsible for reporting any errors it encounters to the user foreach (var library in AndroidMavenLibraries.OrEmpty ()) { // Validate artifact @@ -43,14 +56,12 @@ public async override System.Threading.Tasks.Task RunTaskAsync () if (version is null) continue; - - var artifact = MavenExtensions.ParseArtifact (id, version, Log); - - if (artifact is null) + + if (!MavenExtensions.TryParseArtifactWithVersion (id, version, Log, out var artifact)) continue; // Check for repository files - if (await GetRepositoryArtifactOrDefault (artifact, library, Log) is TaskItem result) { + if (await GetRepositoryArtifactOrDefault (artifact, library, additional_poms) is TaskItem result) { library.CopyMetadataTo (result); resolved.Add (result); continue; @@ -58,9 +69,10 @@ public async override System.Threading.Tasks.Task RunTaskAsync () } ResolvedAndroidMavenLibraries = resolved.ToArray (); + AndroidAdditionalJavaManifest = additional_poms.ToArray (); } - async System.Threading.Tasks.Task GetRepositoryArtifactOrDefault (Artifact artifact, ITaskItem item, TaskLoggingHelper log) + async System.Threading.Tasks.Task GetRepositoryArtifactOrDefault (Artifact artifact, ITaskItem item, List additionalPoms) { // Handles a Repository="Central|Google|" entry, like: // TryGetParentPom (ITaskItem item, TaskLoggingHelper log) - { - var child_pom_file = item.GetRequiredMetadata ("AndroidMavenLibrary", "ArtifactPom", Log); + // Resolve and download POM, and any parent or imported POMs + try { + var resolver = new LoggingPomResolver (repository); + var project = ResolvedProject.FromArtifact (artifact, resolver); - // Shouldn't be possible because we just created this items - if (child_pom_file is null) - return null; + // Set the POM file path for _this_ artifact + var primary_pom = resolver.ResolvedPoms [artifact.VersionedArtifactString]; + result.SetMetadata ("Manifest", primary_pom); - // No parent POM needed - if (!(MavenExtensions.CheckForNeededParentPom (child_pom_file) is Artifact artifact)) - return null; + Log.LogMessage ("Found POM file '{0}' for Java artifact '{1}'.", primary_pom, artifact); - // Initialize repo (parent will be in same repository as child) - var repository = GetRepository (item); + // Create TaskItems for any other POMs we resolved + foreach (var kv in resolver.ResolvedPoms.Where (k => k.Key != artifact.VersionedArtifactString)) { - if (repository is null) - return null; + var pom_item = new TaskItem (kv.Value); + var pom_artifact = Artifact.Parse (kv.Key); - artifact.Repository = repository; + pom_item.SetMetadata ("JavaArtifact", $"{pom_artifact.GroupId}:{pom_artifact.Id}"); + pom_item.SetMetadata ("JavaVersion", pom_artifact.Version); - // Download POM - var pom_file = await MavenExtensions.DownloadPom (artifact, MavenCacheDirectory, Log, CancellationToken); + additionalPoms.Add (pom_item); - if (pom_file is null) + Log.LogMessage ("Found POM file '{0}' for Java artifact '{1}'.", kv.Value, pom_artifact); + } + } catch (Exception ex) { + Log.LogCodedError ("XA4237", Properties.Resources.XA4237, artifact, ex.Unwrap ().Message); return null; - - var result = new TaskItem ($"{artifact.GroupId}:{artifact.Id}"); - - result.SetMetadata ("Version", artifact.Versions.FirstOrDefault ()); - result.SetMetadata ("ArtifactPom", pom_file); - - // Copy repository data - item.CopyMetadataTo (result); + } return result; } - MavenRepository? GetRepository (ITaskItem item) + CachedMavenRepository? GetRepository (ITaskItem item) { var type = item.GetMetadataOrDefault ("Repository", "Central"); var repo = type.ToLowerInvariant () switch { - "central" => MavenRepository.FromMavenCentral (), - "google" => MavenRepository.FromGoogle (), - _ => (MavenRepository?) null + "central" => MavenRepository.Central, + "google" => MavenRepository.Google, + _ => null }; - if (repo is null && type.StartsWith ("http", StringComparison.OrdinalIgnoreCase)) - repo = MavenRepository.FromUrl (type); + if (repo is null && type.StartsWith ("http", StringComparison.OrdinalIgnoreCase)) { + using var hasher = SHA256.Create (); + var hash = hasher.ComputeHash (Encoding.UTF8.GetBytes (type)); + var cache_name = Convert.ToBase64String (hash); + + repo = new MavenRepository (type, cache_name); + } if (repo is null) Log.LogCodedError ("XA4239", Properties.Resources.XA4239, type); - return repo; + return repo is not null ? new CachedMavenRepository (MavenCacheDirectory, repo) : null; + } +} + +// This wrapper around CachedMavenRepository is used to log the POMs that are resolved. +// We need these on-disk file locations so we can pass them as items. +class LoggingPomResolver : IProjectResolver +{ + readonly CachedMavenRepository repository; + + public Dictionary ResolvedPoms { get; } = new Dictionary (); + + public LoggingPomResolver (CachedMavenRepository repository) + { + this.repository = repository; + } + + public Project Resolve (Artifact artifact) + { + if (repository.TryGetFilePath (artifact, $"{artifact.Id}-{artifact.Version}.pom", out var path)) { + using (var stream = File.OpenRead (path)) { + var pom = Project.Load (stream) ?? throw new InvalidOperationException ($"Could not deserialize POM for {artifact}"); + + // Use index instead of Add to handle duplicates + ResolvedPoms [artifact.VersionedArtifactString] = path; + + return pom; + } + } + + throw new InvalidOperationException ($"No POM found for {artifact}"); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs index d4e24312561..58d8012352a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs @@ -791,5 +791,103 @@ public void AndroidMavenLibrary () FileAssert.Exists (cs_file); } } + + [Test] + public void AndroidMavenLibrary_FailsDueToUnverifiedDependency () + { + // Test that triggers Java dependency verification + // + var item = new BuildItem ("AndroidMavenLibrary", "androidx.core:core"); + item.Metadata.Add ("Version", "1.9.0"); + item.Metadata.Add ("Repository", "Google"); + + var proj = new XamarinAndroidBindingProject { + Jars = { item } + }; + + using (var b = CreateDllBuilder ()) { + b.ThrowOnBuildFailure = false; + Assert.IsFalse (b.Build (proj), "Build should have failed."); + + // Ensure an error was raised + StringAssertEx.Contains ("error XA4242: Java dependency 'androidx.annotation:annotation:1.2.0' is not satisfied.", b.LastBuildOutput); + } + } + + [Test] + public void AndroidMavenLibrary_IgnoreDependencyVerification () + { + // Test that ignores Java dependency verification + // + var item = new BuildItem ("AndroidMavenLibrary", "androidx.core:core"); + item.Metadata.Add ("Version", "1.9.0"); + item.Metadata.Add ("Repository", "Google"); + item.Metadata.Add ("VerifyDependencies", "false"); + item.Metadata.Add ("Bind", "false"); + + var proj = new XamarinAndroidBindingProject { + Jars = { item } + }; + + using (var b = CreateDllBuilder ()) { + Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + } + } + + [Test] + public void AndroidMavenLibrary_AllDependenciesAreVerified () + { + // Test that triggers Java dependency verification and that + // all dependencies are verified via various supported mechanisms + + // + var item = new BuildItem ("AndroidMavenLibrary", "androidx.core:core"); + item.Metadata.Add ("Version", "1.9.0"); + item.Metadata.Add ("Repository", "Google"); + item.Metadata.Add ("Bind", "false"); + + // Dependency fulfilled by + var annotations_nuget = new Package { + Id = "Xamarin.AndroidX.Annotation", + Version = "1.7.0.3" + }; + + // Dependency fulfilled by + var annotations_experimental_androidlib = new BuildItem ("AndroidMavenLibrary", "androidx.annotation:annotation-experimental"); + annotations_experimental_androidlib.Metadata.Add ("Version", "1.3.0"); + annotations_experimental_androidlib.Metadata.Add ("Repository", "Google"); + annotations_experimental_androidlib.Metadata.Add ("Bind", "false"); + annotations_experimental_androidlib.Metadata.Add ("VerifyDependencies", "false"); + + // Dependency fulfilled by + var collection = new XamarinAndroidBindingProject (); + + // Dependencies ignored by + var concurrent = new BuildItem ("AndroidIgnoredJavaDependency", "androidx.concurrent:concurrent-futures"); + concurrent.Metadata.Add ("Version", "1.1.0"); + + var lifecycle = new BuildItem ("AndroidIgnoredJavaDependency", "androidx.lifecycle:lifecycle-runtime"); + lifecycle.Metadata.Add ("Version", "2.6.2"); + + var parcelable = new BuildItem ("AndroidIgnoredJavaDependency", "androidx.versionedparcelable:versionedparcelable"); + parcelable.Metadata.Add ("Version", "1.2.0"); + + var proj = new XamarinAndroidBindingProject { + Jars = { item, annotations_experimental_androidlib }, + PackageReferences = { annotations_nuget }, + OtherBuildItems = { concurrent, lifecycle, parcelable }, + }; + + proj.AddReference (collection); + var collection_proj = proj.References.First (); + collection_proj.Metadata.Add ("JavaArtifact", "androidx.collection:collection"); + collection_proj.Metadata.Add ("JavaVersion", "1.3.0"); + + using var a = CreateDllBuilder (); + using var b = CreateDllBuilder (); + + Assert.IsTrue (a.Build (proj), "ProjectReference build should have succeeded."); + Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetMicrosoftNuGetPackagesMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetMicrosoftNuGetPackagesMapTests.cs new file mode 100644 index 00000000000..c07a33bdc1c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetMicrosoftNuGetPackagesMapTests.cs @@ -0,0 +1,158 @@ +#nullable enable + +using System; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + public class GetMicrosoftNuGetPackagesMapTests + { + [Test] + public async Task NoCachedFile () + { + var engine = new MockBuildEngine (TestContext.Out, []); + var temp_cache_dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + var today_file = Path.Combine (temp_cache_dir, $"microsoft-packages-{DateTime.Today:yyyyMMdd}.json"); + + try { + var task = new GetMicrosoftNuGetPackagesMap { + BuildEngine = engine, + MavenCacheDirectory = temp_cache_dir, + }; + + await task.RunTaskAsync (); + + Assert.AreEqual (0, engine.Errors.Count); + Assert.AreEqual (today_file, task.ResolvedPackageMap); + Assert.IsTrue (File.Exists (today_file)); + + } finally { + MavenDownloadTests.DeleteTempDirectory (temp_cache_dir); + } + } + + [Test] + public async Task CachedTodayFile () + { + // If a file already exists for today, it should be used and nothing new should be downloaded + var engine = new MockBuildEngine (TestContext.Out, []); + var temp_cache_dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + var today_file = Path.Combine (temp_cache_dir, $"microsoft-packages-{DateTime.Today:yyyyMMdd}.json"); + + try { + Directory.CreateDirectory (temp_cache_dir); + File.WriteAllText (today_file, "dummy file"); + + var task = new GetMicrosoftNuGetPackagesMap { + BuildEngine = engine, + MavenCacheDirectory = temp_cache_dir, + }; + + await task.RunTaskAsync (); + + Assert.AreEqual (0, engine.Errors.Count); + Assert.AreEqual (today_file, task.ResolvedPackageMap); + Assert.IsTrue (File.Exists (today_file)); + + // Ensure file didn't change + var text = File.ReadAllText (today_file); + Assert.AreEqual ("dummy file", text); + + } finally { + MavenDownloadTests.DeleteTempDirectory (temp_cache_dir); + } + } + + [Test] + public async Task CachedYesterdayFile () + { + // If a file only exists for yesterday, a new one should be downloaded and the old one should be deleted + var engine = new MockBuildEngine (TestContext.Out, []); + var temp_cache_dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + var yesterday_file = Path.Combine (temp_cache_dir, $"microsoft-packages-{DateTime.Today.AddDays (-1):yyyyMMdd}.json"); + var today_file = Path.Combine (temp_cache_dir, $"microsoft-packages-{DateTime.Today:yyyyMMdd}.json"); + + try { + Directory.CreateDirectory (temp_cache_dir); + File.WriteAllText (yesterday_file, "dummy file"); + + var task = new GetMicrosoftNuGetPackagesMap { + BuildEngine = engine, + MavenCacheDirectory = temp_cache_dir, + }; + + await task.RunTaskAsync (); + + Assert.AreEqual (0, engine.Errors.Count); + Assert.AreEqual (today_file, task.ResolvedPackageMap); + Assert.IsFalse (File.Exists (yesterday_file)); + + } finally { + MavenDownloadTests.DeleteTempDirectory (temp_cache_dir); + } + } + + [Test] + public async Task MalformedFileName () + { + // Make sure a malformed file name doesn't cause an exception, a new file should be downloaded and returned + var engine = new MockBuildEngine (TestContext.Out, []); + var temp_cache_dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + var malformed_file = Path.Combine (temp_cache_dir, $"microsoft-packages-dummy.json"); + var today_file = Path.Combine (temp_cache_dir, $"microsoft-packages-{DateTime.Today:yyyyMMdd}.json"); + + try { + Directory.CreateDirectory (temp_cache_dir); + File.WriteAllText (malformed_file, "dummy file"); + + var task = new GetMicrosoftNuGetPackagesMap { + BuildEngine = engine, + MavenCacheDirectory = temp_cache_dir, + }; + + await task.RunTaskAsync (); + + Assert.AreEqual (0, engine.Errors.Count); + Assert.AreEqual (today_file, task.ResolvedPackageMap); + Assert.IsTrue (File.Exists (today_file)); + + } finally { + MavenDownloadTests.DeleteTempDirectory (temp_cache_dir); + } + } + + // This test can only be run manually, since it requires changing the URL to a non-existent one. + // But I wanted to ensure I had tested this scenario. + //[Test] + public async Task CachedYesterdayFile_NewFileFails () + { + // If a file only exists for yesterday, but we fail to download a new file today, return + // the old file and don't delete it + var engine = new MockBuildEngine (TestContext.Out, []); + var temp_cache_dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + var yesterday_file = Path.Combine (temp_cache_dir, $"microsoft-packages-{DateTime.Today.AddDays (-1):yyyyMMdd}.json"); + + try { + Directory.CreateDirectory (temp_cache_dir); + File.WriteAllText (yesterday_file, "dummy file"); + + var task = new GetMicrosoftNuGetPackagesMap { + BuildEngine = engine, + MavenCacheDirectory = temp_cache_dir, + }; + + await task.RunTaskAsync (); + + Assert.AreEqual (0, engine.Errors.Count); + Assert.AreEqual (yesterday_file, task.ResolvedPackageMap); + Assert.IsTrue (File.Exists (yesterday_file)); + + } finally { + MavenDownloadTests.DeleteTempDirectory (temp_cache_dir); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/JavaDependencyVerificationTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/JavaDependencyVerificationTests.cs new file mode 100644 index 00000000000..f854c422b7e --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/JavaDependencyVerificationTests.cs @@ -0,0 +1,497 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Xml; +using Java.Interop.Tools.Maven.Models; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests; + +public class JavaDependencyVerificationTests +{ + [Test] + public void NoManifestsSpecified () + { + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + }; + + Assert.True (task.RunTask ()); + } + + [Test] + public void MissingPom () + { + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [CreateAndroidLibraryTaskItem ("com.google.android.material.jar", "missing.pom")], + }; + + var result = task.RunTask (); + + Assert.False (result); + Assert.AreEqual (1, engine.Errors.Count); + Assert.AreEqual ("Specified POM file 'missing.pom' does not exist.", engine.Errors [0].Message); + } + + [Test] + public void MalformedPom () + { + using var pom = new TemporaryFile ("this is not valid XML"); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath)], + }; + + var result = task.RunTask (); + + Assert.False (result); + Assert.AreEqual (1, engine.Errors.Count); + Assert.True (engine.Errors [0].Message?.StartsWith ("Could not parse POM file")); + } + + [Test] + public void NoSpecifiedDependencies () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0").BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath)], + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + } + + [Test] + public void MissingSpecifiedDependency () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "missing", "1.0") + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath)], + }; + + var result = task.RunTask (); + + Assert.False (result); + Assert.AreEqual (1, engine.Errors.Count); + Assert.AreEqual ("Java dependency 'com.google.android:missing:1.0' is not satisfied.", engine.Errors [0].Message); + } + + [Test] + public void MissingParentSpecifiedDependency () + { + using var parent_pom = new PomBuilder ("com.google.android", "material-parent", "1.0") + .WithDependencyManagement ("com.google.android", "missing", "2.0") + .BuildTemporary (); + + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithParent ("com.google.android", "material-parent", "1.0") + .WithDependency ("com.google.android", "missing", "") + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath)], + AdditionalManifests = [CreateAndroidAdditionManifestTaskItem (parent_pom.FilePath)], + }; + + var result = task.RunTask (); + + Assert.False (result); + Assert.AreEqual (1, engine.Errors.Count); + Assert.AreEqual ("Java dependency 'com.google.android:missing:2.0' is not satisfied.", engine.Errors [0].Message); + } + + [Test] + public void MissingSpecifiedDependencyWithNugetSuggestion () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", "1.0") + .BuildTemporary (); + + using var package_finder = CreateMicrosoftNuGetPackageFinder ("com.google.android:material-core", "Xamarin.Google.Material.Core"); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath)], + MicrosoftPackagesFile = package_finder.FilePath, + }; + + var result = task.RunTask (); + + Assert.False (result); + Assert.AreEqual (1, engine.Errors.Count); + Assert.AreEqual ("Java dependency 'com.google.android:material-core:1.0' is not satisfied. Microsoft maintains the NuGet package 'Xamarin.Google.Material.Core' that could fulfill this dependency.", engine.Errors [0].Message); + } + + [Test] + public void MalformedMicrosoftPackagesJson () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", "1.0") + .BuildTemporary (); + + using var package_finder = new TemporaryFile ("This is not valid json!", "microsoft-packages.json"); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + CreateAndroidLibraryTaskItem ("com.google.android.material-core.jar", null, "com.google.android:material-core", "1.0"), + ], + MicrosoftPackagesFile = package_finder.FilePath, + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + } + + [Test] + public void DependencyFulfilledByAndroidLibrary () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", "1.0") + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + CreateAndroidLibraryTaskItem ("com.google.android.material-core.jar", null, "com.google.android:material-core", "1.0"), + ], + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + } + + [Test] + public void DependencyFulfilledByProjectReferenceExplicitMetadata () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", "1.0") + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + ], + ProjectReferences = [ + CreateAndroidLibraryTaskItem ("Google.Material.Core.csproj", null, "com.google.android:material-core", "1.0"), + ], + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + } + + [Test] + public void DependencyFulfilledByPackageReferenceExplicitMetadata () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", "1.0") + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + ], + PackageReferences = [ + CreateAndroidLibraryTaskItem ("Xamarin.Google.Material.Core", null, "com.google.android:material-core", "1.0"), + ], + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + } + + [Test] + public void DependencyIgnored () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", "1.0") + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + ], + IgnoredDependencies = [ + CreateAndroidLibraryTaskItem ("com.google.android:material-core", rawVersion: "1.0"), + ], + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + } + + [Test] + public void DependencyWithoutVersionFulfilled () + { + // The dependency is fulfilled but the version isn't checked + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", null) + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, [], []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + CreateAndroidLibraryTaskItem ("com.google.android.material-core.jar", null, "com.google.android:material-core", "1.0"), + ], + }; + + var result = task.RunTask (); + + Assert.True (result); + Assert.AreEqual (0, engine.Errors.Count); + Assert.AreEqual (0, engine.Warnings.Count); + } + + [Test] + public void DependencyWithoutVersionNotFulfilled () + { + using var pom = new PomBuilder ("com.google.android", "material", "1.0") + .WithDependency ("com.google.android", "material-core", null) + .BuildTemporary (); + + var engine = new MockBuildEngine (TestContext.Out, [], []); + var task = new JavaDependencyVerification { + BuildEngine = engine, + AndroidLibraries = [ + CreateAndroidLibraryTaskItem ("com.google.android.material.jar", pom.FilePath), + ], + }; + + var result = task.RunTask (); + + Assert.False (result); + Assert.AreEqual (1, engine.Errors.Count); + Assert.AreEqual ("Java dependency 'com.google.android:material-core' is not satisfied.", engine.Errors [0].Message); + } + + TaskItem CreateAndroidLibraryTaskItem (string name, string? manifest = null, string? javaArtifact = null, string? javaVersion = null, string? rawVersion = null) + { + var item = new TaskItem (name); + + if (manifest is not null) + item.SetMetadata ("Manifest", manifest); + if (javaArtifact is not null) + item.SetMetadata ("JavaArtifact", javaArtifact); + if (javaVersion is not null) + item.SetMetadata ("JavaVersion", javaVersion); + if (rawVersion is not null) + item.SetMetadata ("Version", rawVersion); + + return item; + } + + TaskItem CreateAndroidAdditionManifestTaskItem (string name) + { + var item = new TaskItem (name); + + return item; + } + + TemporaryFile CreateMicrosoftNuGetPackageFinder (string javaId, string nugetId) + { + var package = new MicrosoftNuGetPackageFinder.PackageListFile { + Packages = [new MicrosoftNuGetPackageFinder.Package { JavaId = javaId, NuGetId = nugetId }] + }; + + return new TemporaryFile (JsonSerializer.Serialize (package), "microsoft-packages.json"); + } +} + +class TemporaryFile : IDisposable +{ + public string Content { get; } + public string FilePath { get; } + + public TemporaryFile (string content, string? filename = null) + { + Content = content; + FilePath = Path.Combine (Path.GetTempPath (), filename ?? Path.GetTempFileName ()); + + File.WriteAllText (FilePath, content); + } + + public void Dispose () + { + try { + File.Delete (FilePath); + } catch { + } + } +} + +class PomBuilder +{ + public string GroupId { get; } + public string ArtifactId { get; } + public string? Version { get; } + public List Dependencies { get; } = new (); + public List DependencyManagement { get; } = new (); + public string? ParentGroupId { get; set; } + public string? ParentArtifactId { get; set; } + public string? ParentVersion { get; set; } + + public PomBuilder (string groupId, string artifactId, string? version) + { + GroupId = groupId; + ArtifactId = artifactId; + Version = version; + } + + public string Build () + { + using var sw = new Utf8StringWriter (); + using var xw = XmlWriter.Create (sw); + + xw.WriteStartDocument (); + xw.WriteStartElement ("project", "http://maven.apache.org/POM/4.0.0"); + + xw.WriteElementString ("modelVersion", "4.0.0"); + xw.WriteElementString ("groupId", GroupId); + xw.WriteElementString ("artifactId", ArtifactId); + + if (Version.HasValue ()) + xw.WriteElementString ("version", Version); + + if (ParentGroupId.HasValue () && ParentArtifactId.HasValue ()) { + xw.WriteStartElement ("parent"); + + xw.WriteElementString ("groupId", ParentGroupId); + xw.WriteElementString ("artifactId", ParentArtifactId); + + if (ParentVersion.HasValue ()) + xw.WriteElementString ("version", ParentVersion); + + xw.WriteEndElement (); // parent + } + + if (DependencyManagement.Any ()) { + xw.WriteStartElement ("dependencyManagement"); + xw.WriteStartElement ("dependencies"); + + foreach (var dependency in DependencyManagement) { + xw.WriteStartElement ("dependency"); + + xw.WriteElementString ("groupId", dependency.GroupId); + xw.WriteElementString ("artifactId", dependency.ArtifactId); + + if (dependency.Version.HasValue ()) + xw.WriteElementString ("version", dependency.Version); + + xw.WriteEndElement (); // dependency + } + + xw.WriteEndElement (); // dependencies + xw.WriteEndElement (); // dependencyManagement + } + + + if (Dependencies.Any ()) { + xw.WriteStartElement ("dependencies"); + + foreach (var dependency in Dependencies) { + xw.WriteStartElement ("dependency"); + + xw.WriteElementString ("groupId", dependency.GroupId); + xw.WriteElementString ("artifactId", dependency.ArtifactId); + + if (dependency.Version.HasValue ()) + xw.WriteElementString ("version", dependency.Version); + + xw.WriteEndElement (); // dependency + } + + xw.WriteEndElement (); // dependencies + } + xw.WriteEndElement (); // project + xw.Close (); + + return sw.ToString (); + } + + public PomBuilder WithDependency (string groupId, string artifactId, string? version) + { + Dependencies.Add (new Dependency { + GroupId = groupId, + ArtifactId = artifactId, + Version = version, + }); + + return this; + } + + public PomBuilder WithDependencyManagement (string groupId, string artifactId, string? version) + { + DependencyManagement.Add (new Dependency { + GroupId = groupId, + ArtifactId = artifactId, + Version = version, + }); + + return this; + } + + public PomBuilder WithParent (string groupId, string artifactId, string? version) + { + ParentGroupId = groupId; + ParentArtifactId = artifactId; + ParentVersion = version; + + return this; + } + + public TemporaryFile BuildTemporary () => new TemporaryFile (Build ()); + + // Trying to write XML to a StringWriter defaults to UTF-16, but we want UTF-8 + class Utf8StringWriter : StringWriter + { + public override Encoding Encoding => Encoding.UTF8; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs index 5d9c6bda151..c2ad0117fe4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.IO; @@ -22,7 +24,7 @@ public async Task MissingVersionMetadata () await task.RunTaskAsync (); Assert.AreEqual (1, engine.Errors.Count); - Assert.AreEqual ("'' item 'com.google.android.material:material' is missing required metadata 'Version'", engine.Errors [0].Message); + Assert.AreEqual ("'' item 'com.google.android.material:material' is missing required attribute 'Version'.", engine.Errors [0].Message); } [Test] @@ -83,7 +85,7 @@ public async Task UnknownArtifact () await task.RunTaskAsync (); Assert.AreEqual (1, engine.Errors.Count); - Assert.AreEqual ($"Cannot download Maven artifact 'com.example:dummy'.{Environment.NewLine}- com.example_dummy.jar: Response status code does not indicate success: 404 (Not Found).{Environment.NewLine}- com.example_dummy.aar: Response status code does not indicate success: 404 (Not Found).", engine.Errors [0].Message.ReplaceLineEndings ()); + Assert.AreEqual ($"Cannot download Maven artifact 'com.example:dummy'.{Environment.NewLine}- dummy-1.0.0.jar: Response status code does not indicate success: 404 (Not Found).{Environment.NewLine}- dummy-1.0.0.aar: Response status code does not indicate success: 404 (Not Found).", engine.Errors [0].Message?.ReplaceLineEndings ()); } [Test] @@ -100,15 +102,15 @@ public async Task UnknownPom () }; // Create the dummy jar so we bypass that step and try to download the dummy pom - var dummy_jar = Path.Combine (temp_cache_dir, "central", "com.example", "dummy", "1.0.0", "com.example_dummy.jar"); - Directory.CreateDirectory (Path.GetDirectoryName (dummy_jar)); + var dummy_jar = Path.Combine (temp_cache_dir, "central", "com.example", "dummy", "1.0.0", "dummy-1.0.0.jar"); + Directory.CreateDirectory (Path.GetDirectoryName (dummy_jar)!); using (File.Create (dummy_jar)) { } await task.RunTaskAsync (); Assert.AreEqual (1, engine.Errors.Count); - Assert.AreEqual ($"Cannot download POM file for Maven artifact 'com.example:dummy'.{Environment.NewLine}- com.example_dummy.pom: Response status code does not indicate success: 404 (Not Found).", engine.Errors [0].Message.ReplaceLineEndings ()); + Assert.AreEqual ($"Cannot download POM file for Maven artifact 'com.example:dummy:1.0.0'.{Environment.NewLine}- Response status code does not indicate success: 404 (Not Found).", engine.Errors [0].Message?.ReplaceLineEndings ()); } finally { DeleteTempDirectory (temp_cache_dir); } @@ -130,16 +132,13 @@ public async Task MavenCentralSuccess () await task.RunTaskAsync (); Assert.AreEqual (0, engine.Errors.Count); - Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries.Length); - - var output_item = task.ResolvedAndroidMavenLibraries [0]; + Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries?.Length); - Assert.AreEqual ("com.google.auto.value:auto-value-annotations", output_item.GetMetadata ("ArtifactSpec")); - Assert.AreEqual (Path.Combine (temp_cache_dir, "central", "com.google.auto.value", "auto-value-annotations", "1.10.4", "com.google.auto.value_auto-value-annotations.jar"), output_item.GetMetadata ("ArtifactFile")); - Assert.AreEqual (Path.Combine (temp_cache_dir, "central", "com.google.auto.value", "auto-value-annotations", "1.10.4", "com.google.auto.value_auto-value-annotations.pom"), output_item.GetMetadata ("ArtifactPom")); + var output_item = task.ResolvedAndroidMavenLibraries! [0]; - Assert.True (File.Exists (output_item.GetMetadata ("ArtifactFile"))); - Assert.True (File.Exists (output_item.GetMetadata ("ArtifactPom"))); + Assert.AreEqual ("com.google.auto.value:auto-value-annotations", output_item.GetMetadata ("JavaArtifact")); + Assert.AreEqual ("1.10.4", output_item.GetMetadata ("JavaVersion")); + Assert.AreEqual (Path.Combine (temp_cache_dir, "central", "com.google.auto.value", "auto-value-annotations", "1.10.4", "auto-value-annotations-1.10.4.pom"), output_item.GetMetadata ("Manifest")); } finally { DeleteTempDirectory (temp_cache_dir); } @@ -161,22 +160,19 @@ public async Task MavenGoogleSuccess () await task.RunTaskAsync (); Assert.AreEqual (0, engine.Errors.Count); - Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries.Length); - - var output_item = task.ResolvedAndroidMavenLibraries [0]; + Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries?.Length); - Assert.AreEqual ("androidx.core:core", output_item.GetMetadata ("ArtifactSpec")); - Assert.AreEqual (Path.Combine (temp_cache_dir, "google", "androidx.core", "core", "1.12.0", "androidx.core_core.aar"), output_item.GetMetadata ("ArtifactFile")); - Assert.AreEqual (Path.Combine (temp_cache_dir, "google", "androidx.core", "core", "1.12.0", "androidx.core_core.pom"), output_item.GetMetadata ("ArtifactPom")); + var output_item = task.ResolvedAndroidMavenLibraries! [0]; - Assert.True (File.Exists (output_item.GetMetadata ("ArtifactFile"))); - Assert.True (File.Exists (output_item.GetMetadata ("ArtifactPom"))); + Assert.AreEqual ("androidx.core:core", output_item.GetMetadata ("JavaArtifact")); + Assert.AreEqual ("1.12.0", output_item.GetMetadata ("JavaVersion")); + Assert.AreEqual (Path.Combine (temp_cache_dir, "google", "androidx.core", "core", "1.12.0", "core-1.12.0.pom"), output_item.GetMetadata ("Manifest")); } finally { DeleteTempDirectory (temp_cache_dir); } } - ITaskItem CreateMavenTaskItem (string name, string version, string repository = null) + ITaskItem CreateMavenTaskItem (string name, string? version, string? repository = null) { var item = new TaskItem (name); @@ -188,7 +184,7 @@ ITaskItem CreateMavenTaskItem (string name, string version, string repository = return item; } - void DeleteTempDirectory (string dir) + public static void DeleteTempDirectory (string dir) { try { Directory.Delete (dir, true); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MavenExtensions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MavenExtensions.cs index 4fc7f031833..a9b881adec6 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MavenExtensions.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MavenExtensions.cs @@ -1,16 +1,15 @@ +#nullable enable + using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Serialization; -using MavenNet; -using MavenNet.Models; +using Java.Interop.Tools.Maven.Models; +using Java.Interop.Tools.Maven.Repositories; using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Xamarin.Android.Tasks; @@ -18,7 +17,6 @@ namespace Xamarin.Android.Tasks; static class MavenExtensions { static readonly char [] separator = [':']; - static XmlSerializer pom_serializer = new XmlSerializer (typeof (Project)); /// /// Shortcut for !string.IsNullOrWhiteSpace (s) @@ -31,53 +29,87 @@ public static T [] OrEmpty (this T []? value) return value ?? Array.Empty (); } - public static Artifact? ParseArtifact (string id, string version, TaskLoggingHelper log) + // Removes AggregateException wrapping around an exception + public static Exception Unwrap (this Exception ex) { + while (ex is AggregateException && ex.InnerException is not null) + ex = ex.InnerException; + + return ex; + } + + public static bool TryParseArtifactWithVersion (string id, string version, TaskLoggingHelper log, [NotNullWhen (true)] out Artifact? artifact) + { + artifact = null; + var parts = id.Split (separator, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2 || parts.Any (string.IsNullOrWhiteSpace)) { log.LogCodedError ("XA4235", Properties.Resources.XA4235, id); - return null; + return false; } - var artifact = new Artifact (parts [1], parts [0], version); + artifact = new Artifact (parts [0], parts [1], version); - return artifact; + return true; } - public static Project ParsePom (string pomFile) + public static bool TryParseJavaArtifactAndJavaVersion (this ITaskItem task, string type, TaskLoggingHelper log, [NotNullWhen (true)] out Artifact? artifact, out bool attributesSpecified) { - Project result = null; + artifact = null; + var item_name = task.ItemSpec; - using (var sr = File.OpenRead (pomFile)) - result = (Project) pom_serializer.Deserialize (new XmlTextReader (sr) { - Namespaces = false, - }); + // Convert "../../src/blah/Blah.csproj" to "Blah.csproj" + if (type == "ProjectReference") + item_name = Path.GetFileName (item_name); - return result; - } + var has_artifact = task.HasMetadata ("JavaArtifact"); + var has_version = task.HasMetadata ("JavaVersion"); - public static Artifact? CheckForNeededParentPom (string pomFile) - => ParsePom (pomFile).GetParentPom (); + // Lets callers know if user attempted to specify JavaArtifact or JavaVersion, even if they did it incorrectly + attributesSpecified = has_artifact || has_version; - public static Artifact? GetParentPom (this Project? pom) - { - if (pom?.Parent != null) - return new Artifact (pom.Parent.ArtifactId, pom.Parent.GroupId, pom.Parent.Version); + if (has_artifact && !has_version) { + log.LogCodedError ("XA4243", Properties.Resources.XA4243, "JavaVersion", "JavaArtifact", type, item_name); + return false; + } - return null; + if (!has_artifact && has_version) { + log.LogCodedError ("XA4243", Properties.Resources.XA4243, "JavaArtifact", "JavaVersion", type, item_name); + return false; + } + + if (has_artifact && has_version) { + var id = task.GetMetadata ("JavaArtifact"); + var version = task.GetMetadata ("JavaVersion"); + + if (string.IsNullOrWhiteSpace (id)) { + log.LogCodedError ("XA4244", Properties.Resources.XA4244, "JavaArtifact", type, item_name); + return false; + } + + if (string.IsNullOrWhiteSpace (version)) { + log.LogCodedError ("XA4244", Properties.Resources.XA4244, "JavaVersion", type, item_name); + return false; + } + + if (TryParseArtifactWithVersion (id, version, log, out artifact)) { + log.LogMessage ("Found Java dependency '{0}:{1}' version '{2}' from {3} '{4}' (JavaArtifact)", artifact.GroupId, artifact.Id, artifact.Version, type, item_name); + return true; + } + } + + return false; } // Returns artifact output path - public static async Task DownloadPayload (Artifact artifact, string cacheDir, TaskLoggingHelper log, CancellationToken cancellationToken) + public static async Task DownloadPayload (CachedMavenRepository repository, Artifact artifact, string cacheDir, TaskLoggingHelper log, CancellationToken cancellationToken) { - var version = artifact.Versions.First (); - - var output_directory = Path.Combine (cacheDir, artifact.GetRepositoryCacheName (), artifact.GroupId, artifact.Id, version); + var output_directory = Path.Combine (cacheDir, repository.Name, artifact.GroupId, artifact.Id, artifact.Version); Directory.CreateDirectory (output_directory); - var filename = $"{artifact.GroupId}_{artifact.Id}"; + var filename = $"{artifact.Id}-{artifact.Version}"; var jar_filename = Path.Combine (output_directory, Path.Combine ($"{filename}.jar")); var aar_filename = Path.Combine (output_directory, Path.Combine ($"{filename}.aar")); @@ -88,10 +120,10 @@ public static Project ParsePom (string pomFile) if (File.Exists (aar_filename)) return aar_filename; - if (await TryDownloadPayload (artifact, jar_filename, cancellationToken) is not string jar_error) + if (await TryDownloadPayload (repository, artifact, jar_filename, cancellationToken) is not string jar_error) return jar_filename; - if (await TryDownloadPayload (artifact, aar_filename, cancellationToken) is not string aar_error) + if (await TryDownloadPayload (repository, artifact, aar_filename, cancellationToken) is not string aar_error) return aar_filename; log.LogCodedError ("XA4236", Properties.Resources.XA4236, artifact.GroupId, artifact.Id, Path.GetFileName (jar_filename), jar_error, Path.GetFileName (aar_filename), aar_error); @@ -99,130 +131,27 @@ public static Project ParsePom (string pomFile) return null; } - // Returns artifact output path - public static async Task DownloadPom (Artifact artifact, string cacheDir, TaskLoggingHelper log, CancellationToken cancellationToken, bool isParent = false) + // Return value is download error message, null represents success (async methods cannot have out parameters) + static async Task TryDownloadPayload (CachedMavenRepository repository, Artifact artifact, string filename, CancellationToken cancellationToken) { - var version = artifact.Versions.First (); - var output_directory = Path.Combine (cacheDir, artifact.GetRepositoryCacheName (), artifact.GroupId, artifact.Id, version); - - Directory.CreateDirectory (output_directory); - - var filename = $"{artifact.GroupId}_{artifact.Id}"; - var pom_filename = Path.Combine (output_directory, Path.Combine ($"{filename}.pom")); - - // We don't need to redownload if we already have a cached copy - if (File.Exists (pom_filename)) - return pom_filename; - - if (await TryDownloadPayload (artifact, pom_filename, cancellationToken) is not string pom_error) - return pom_filename; - - if (!isParent) - log.LogCodedError ("XA4237", Properties.Resources.XA4237, artifact.GroupId, artifact.Id, Path.GetFileName (pom_filename), pom_error); - else - log.LogCodedError ("XA4238", Properties.Resources.XA4238, artifact.GroupId, artifact.Id, Path.GetFileName (pom_filename), pom_error); - - return null; - } + var maven_filename = $"{artifact.Id}-{artifact.Version}{Path.GetExtension (filename)}"; - // Return value indicates download success - static async Task TryDownloadPayload (Artifact artifact, string filename, CancellationToken cancellationToken) - { try { - using var src = await artifact.OpenLibraryFile (artifact.Versions.First (), Path.GetExtension (filename)); - using var sw = File.Create (filename); + if ((await repository.GetFilePathAsync (artifact, maven_filename, cancellationToken)) is string path) { + return null; + } else { + // This probably(?) cannot be hit, everything should come back as an exception + return $"Could not download {maven_filename}"; + } - await src.CopyToAsync (sw, 81920, cancellationToken); - - return null; } catch (Exception ex) { - return ex.Message; - } - } - - public static string GetRepositoryCacheName (this Artifact artifact) - { - var type = artifact.Repository; - - if (type is MavenCentralRepository) - return "central"; - - if (type is GoogleMavenRepository) - return "google"; - - if (type is UrlMavenRepository url) { - using var hasher = SHA256.Create (); - var hash = hasher.ComputeHash (Encoding.UTF8.GetBytes (url.BaseUri.ToString ())); - return Convert.ToBase64String (hash); - } - - // Should never be hit - throw new ArgumentException ($"Unexpected repository type: {type.GetType ()}"); - } - - public static void FixDependency (Project project, Project? parent, Dependency dependency) - { - // Handle Parent POM - if ((string.IsNullOrEmpty (dependency.Version) || string.IsNullOrEmpty (dependency.Scope)) && parent != null) { - var parent_dependency = parent.FindParentDependency (dependency); - - // Try to fish a version out of the parent POM - if (string.IsNullOrEmpty (dependency.Version)) - dependency.Version = ReplaceVersionProperties (parent, parent_dependency?.Version); - - // Try to fish a scope out of the parent POM - if (string.IsNullOrEmpty (dependency.Scope)) - dependency.Scope = parent_dependency?.Scope; + return ex.Unwrap ().Message; } - - var version = dependency.Version; - - if (string.IsNullOrWhiteSpace (version)) - return; - - version = ReplaceVersionProperties (project, version); - - // VersionRange.Parse cannot handle single number versions that we sometimes see in Maven, like "1". - // Fix them to be "1.0". - // https://github.com/NuGet/Home/issues/10342 - if (version != null && !version.Contains (".")) - version += ".0"; - - dependency.Version = version; - } - - static string? ReplaceVersionProperties (Project project, string? version) - { - // Handle versions with Properties, like: - // - // 1.8 - // 2.8.6 - // - // - // - // com.google.code.gson - // gson - // ${gson.version} - // - // - if (string.IsNullOrWhiteSpace (version) || project?.Properties == null) - return version; - - foreach (var prop in project.Properties.Any) - version = version?.Replace ($"${{{prop.Name.LocalName}}}", prop.Value); - - return version; } - public static bool IsCompileDependency (this Dependency dependency) => string.IsNullOrWhiteSpace (dependency.Scope) || dependency.Scope.IndexOf ("compile", StringComparison.OrdinalIgnoreCase) != -1; + public static bool IsCompileDependency (this ResolvedDependency dependency) => string.IsNullOrWhiteSpace (dependency.Scope) || dependency.Scope.IndexOf ("compile", StringComparison.OrdinalIgnoreCase) != -1; - public static bool IsRuntimeDependency (this Dependency dependency) => dependency?.Scope != null && dependency.Scope.IndexOf ("runtime", StringComparison.OrdinalIgnoreCase) != -1; - - public static Dependency? FindParentDependency (this Project project, Dependency dependency) - { - return project.DependencyManagement?.Dependencies?.FirstOrDefault ( - d => d.GroupAndArtifactId () == dependency.GroupAndArtifactId () && d.Classifier != "sources"); - } + public static bool IsRuntimeDependency (this ResolvedDependency dependency) => dependency?.Scope != null && dependency.Scope.IndexOf ("runtime", StringComparison.OrdinalIgnoreCase) != -1; - public static string GroupAndArtifactId (this Dependency dependency) => $"{dependency.GroupId}.{dependency.ArtifactId}"; + public static bool IsOptional (this ResolvedDependency dependency) => dependency?.Optional != null && dependency.Optional.IndexOf ("true", StringComparison.OrdinalIgnoreCase) != -1; } 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 3bef384882d..2588f29e729 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -14,7 +14,7 @@ true $(MicrosoftAndroidSdkOutDir) false - $(DefineConstants);TRACE;HAVE_CECIL;MSBUILD;ANDROID_24;ANDROID_26;ANDROID_31 + $(DefineConstants);TRACE;HAVE_CECIL;MSBUILD;ANDROID_24;ANDROID_26;ANDROID_31;INTERNAL_NULLABLE_ATTRIBUTES ..\..\src\Mono.Android\obj\$(Configuration)\$(DotNetTargetFramework)\android-$(AndroidLatestStablePlatformId)\mcw 8632 false @@ -28,7 +28,9 @@ - + + + @@ -98,6 +100,9 @@ Utilities\StringRocks.cs + + Utilities\NullableAttributes.cs + Mono.Android\UsesLibraryAttribute.cs @@ -225,6 +230,7 @@ +