Skip to content

Commit

Permalink
[Java.Base] Begin binding JDK-11 java.base module (#909)
Browse files Browse the repository at this point in the history
Context: #858

What do *I* want?  To be able to use our wonderful Java binding
infrastructure against *Desktop Java*, not just Android.

At the same time, I don't want "Android-isms" "leaking" into such a
binding.  *Just* Java.Interop, no xamarin-android.

"Take over" the `generator --codegen-target=JavaInterop1` format
so that it *isn't* useful for Xamarin.Android, and is instead usable
for non-Android usage.

This is a work-in-progress, and *far* from complete.  For prototype
purposes, this *only* binds:

  * `java.lang.Class`
  * `java.lang.Math`
  * `java.lang.Number`
  * `java.lang.Object`
  * `java.lang.Throwable`

The `Java.Base` binding is only for .NET 6 and above.  I'm not
interested in .NET Standard support at this point in time.

~~ Build Changes ~~

Additionally, this `Java.Base` binding is only for the
`java.base.jmod` file provided by JDK-11.  `make prepare` is updated
to look for a JDK-11 installation path.

~~ Test Changes ~~

Update `samples/Hello` so that it (1) works, and (2) instantiates the
`Java.Lang.Object` binding:

	dotnet run --project samples/Hello

Update `tests/generator-Tests` so that
`tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1`
is now used *exclusively* for `generator --codegen-target=JavaInterop1`
output; `generator --codegen-target=XAJavaInterop1` now has its own
separate files in
`tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1`.
(This contributes to much of the commit diff.)

Update `tests/generator-Tests/SupportFiles` so that types in
`Android.*` are excluded when `JAVA_INTEROP1` is `#define`d, and
update the unit test infrastructure so that building for `JavaInterop1`
causes `JAVA_INTEROP1` to be `#define`d.  This allows many of the unit
tests to ensure that *some* `JavaInterop1` constructs *compile*.

However, not all unit tests compile under `JavaInterop1`.  The new
`BaseGeneratorTest.TryJavaInterop1` property can be set to false to
prevent compiling a test suite using `JavaInterop1`.  This will allow
us to slowly implement `JavaInterop1` support over time.

The build logs will also now emit a command-line that can be used to
manually compile unit test code.  See e.g.
`bin/TestDebug/TestOutput-generator-Tests.txt`.

~~ API Changes ~~

Update `Java.Interop.JavaObject` so that
`JavaObject.DisposeUnlessReferenced()` is now `virtual`.
Override `DisposeUnlessReferenced()` from the `Java*Array` types
so that if the instance was created via the new
`JniEnvironment.Arrays.CreateMarshal*Array()` methods, the array
instance will be disposed.  This is intended for marshaling array
parameters:

	public void WithArray(int[] array)
	{
	    const string __id = "withArray.[I";
	    var native_array = JniEnvironment.Arrays.CreateMarshalInt32Array (array);
	    try {
	        JniArgumentValue* __args = stackalloc JniArgumentValue [1];
	        __args [0] = new JniArgumentValue (native_array);
	        _members.StaticMethods.InvokeVoidMethod (__id, __args);
	    } finally {
	        if (array != null) native_array.DisposeUnlessReferenced ();
	    }
	}

Add `Java.Interop.JavaTypeParametersAttribute(string[] typeParameters)`
from Xamarin.Android.

~~ Bindings vs. Xamarin.Android ~~

Pull in `src/Java.Base/Transforms/map.csv` [from xamarin-android][0],
removing the `android*` types.

Instead of `[Android.Runtime.RegisterAttribute]` on types, use
`[Java.Interop.JniTypeSignatureAttribute]`.

Java arrays are bound as appropriate `IList<T>`, using the
`Java.Interop.Java*Array` types as an intermediary.  This should help
reduce marshaling logic & overhead, as if the "source" array is a
`Java*Array`, it doesn't need to be "deep copied".  The exception is
C# `params` arrays, which continue to be bound as arrays, and are
marshaled via an appropriate `Java*Array` type.

`java.io.InputStream` isn't bound as `System.IO.Stream`, etc.

"Java.Interop-style" constructors are used (25de1f3), e.g.

	// This
	DeclaringType (ref JniObjectReference reference, JniObjectReferenceOptions options);

	// Not Xamarin.Android-style
	DeclaringType (IntPtr handle, JniHandleOwnership transfer);

"Java.Interop-style" wrapper construction is used, e.g.

	// This
	var wrapper = JniEnvironment.Runtime.ValueManager.GetValue<DeclaringType>(ref h, JniObjectReferenceOptions.CopyAndDispose);

	// Not this
	var wrapper = Java.Lang.Object.GetObject<DeclaringType>(handle);

~~ TODO: Marshal Methods ~~

Marshal methods are currently skipped.  Java-to-managed invocations
are not currently supported.

Xamarin.Android uses Java Callable Wrappers + `Runtime.register()`
to specify which methods to register, via lots of reflection, etc.

Proposal: For Desktop, JCW's shouldn't need to specify all the
methods to register.  Instead, use the `jnimarshalmethod-gen`-
originated strategy of `[JniAddNativeMethodRegistrationAttribute]`
within the binding, and then have it use `MarshalMethodBuilder` to
generate the marshal methods.  Need to update `MarshalMethodBuilder`
to look for overrides in addition to methods with `[JavaCallable]`,
which in turn may require an equivalent to
`[Android.Runtime.RegisterAttribute(…)]`.

Perhaps `[JniMethodSignatureAttribute(string name, string sig)]`?

In the meantime, `Java.Base` will skip all marshal-method logic
plus runtime method generation.  Leave that for later.

~~ TODO: Other Binding Changes? ~~

We should eventually "unify" `java.lang.Object` and `System.Object`.
Consider `java.lang.Class`:

	/* partial */ class Class<T> {
	    public boolean isInstance(java.lang.Object);
	    public java.lang.Object[] getSigners();
	}

If we unify `java.lang.Object` and `System.Object`, we'd have a
binding of:

	partial class Class {
	    public bool IsInstance (object value);
	    public IList<object> GetSigners();
	}

~~ Open Questions ~~

What's up with `java.lang.Class.getAnnotationsByType()`?

During an iteration of this PR, I got:

	public unsafe Java.Interop.JavaObjectArray<Java.Lang.Object>? GetAnnotationsByType (Java.Lang.Class? annotationClass)
	{
	    const string __id = "getAnnotationsByType.(Ljava/lang/Class;)[Ljava/lang/annotation/Annotation;";

From `__id` we see that the Java return type is `Annotation[]`, yet
we bind it as an `Object` array?  This is because of
[#669][1].

That said, it's currently "differently *worse*"; I don't know why,
but `__id` is now:

	const string __id = "getAnnotationsByType.(Ljava/lang/Class;)[Ljava/lang/Object;";

i.e. the return type is an `Object` array instead of an `Annotation`
array, which is wrong, as per `javap`:

	% javap -s java.lang.Class
	…
	  public <A extends java.lang.annotation.Annotation> A getAnnotation(java.lang.Class<A>);
	    descriptor: (Ljava/lang/Class;)Ljava/lang/annotation/Annotation;

This needs additional investigation.

[0]: https://github.com/xamarin/xamarin-android/blob/99523feab02e8622a3357e9e6a025f5afc44c970/src/Mono.Android/map.csv
[1]: #669
  • Loading branch information
jonpryor authored Dec 7, 2021
1 parent af91b9c commit bc5bcf4
Show file tree
Hide file tree
Showing 462 changed files with 13,758 additions and 5,050 deletions.
4 changes: 4 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
Project="$(_OutputPath)JdkInfo.props"
Condition="Exists('$(_OutputPath)JdkInfo.props')"
/>
<Import
Project="$(_OutputPath)JdkInfo-11.props"
Condition="Exists('$(_OutputPath)JdkInfo-11.props')"
/>
<Import
Project="$(_OutputPath)MonoInfo.props"
Condition="Exists('$(_OutputPath)MonoInfo.props')"
Expand Down
7 changes: 7 additions & 0 deletions Java.Interop.sln
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Java.Interop.Tools.JavaType
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.JavaTypeSystem-Tests", "tests\Java.Interop.Tools.JavaTypeSystem-Tests\Java.Interop.Tools.JavaTypeSystem-Tests.csproj", "{11942DE9-AEC2-4B95-87AB-CA707C37643D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Base", "src\Java.Base\Java.Base.csproj", "{30DCECA5-16FD-4FD0-883C-E5E83B11565D}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5
Expand Down Expand Up @@ -296,6 +298,10 @@ Global
{11942DE9-AEC2-4B95-87AB-CA707C37643D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11942DE9-AEC2-4B95-87AB-CA707C37643D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11942DE9-AEC2-4B95-87AB-CA707C37643D}.Release|Any CPU.Build.0 = Release|Any CPU
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -346,6 +352,7 @@ Global
{BF5A4019-F2FF-45AC-949D-EF7E8C94196B} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
{B173F53B-986C-4E0D-881C-063BBB116E1D} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
{11942DE9-AEC2-4B95-87AB-CA707C37643D} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
{30DCECA5-16FD-4FD0-883C-E5E83B11565D} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class JdkInfo : Task
{
public string JdksRoot { get; set; }

public string PropertyNameModifier { get; set; } = "";
public string MinimumJdkVersion { get; set; }
public string MaximumJdkVersion { get; set; }

public string DotnetToolPath { get; set; }
Expand All @@ -28,19 +30,22 @@ public class JdkInfo : Task
[Required]
public ITaskItem PropertyFile { get; set; }

[Required]
public ITaskItem MakeFragmentFile { get; set; }

[Output]
public string JavaHomePath { get; set; }

public override bool Execute ()
{
var minVersion = GetVersion (MinimumJdkVersion);
var maxVersion = GetVersion (MaximumJdkVersion);

XATInfo jdk = XATInfo.GetKnownSystemJdkInfos (CreateLogger ())
var explicitJdks = GetJdkRoots ();
var defaultJdks = XATInfo.GetKnownSystemJdkInfos (CreateLogger ())
.Where (j => minVersion != null ? j.Version >= minVersion : true)
.Where (j => maxVersion != null ? j.Version <= maxVersion : true)
.Where (j => j.IncludePath.Any ())
.Where (j => j.IncludePath.Any ());
var jdk = explicitJdks.Concat (defaultJdks)
.FirstOrDefault ();

if (jdk == null) {
Expand All @@ -56,14 +61,31 @@ public override bool Execute ()
JavaHomePath = jdk.HomePath;

Directory.CreateDirectory (Path.GetDirectoryName (PropertyFile.ItemSpec));
Directory.CreateDirectory (Path.GetDirectoryName (MakeFragmentFile.ItemSpec));

WritePropertyFile (jdk.JavaPath, jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath);
WriteMakeFragmentFile (jdk.JavaPath, jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath);

if (MakeFragmentFile != null) {
Directory.CreateDirectory (Path.GetDirectoryName (MakeFragmentFile.ItemSpec));
WriteMakeFragmentFile (jdk.JavaPath, jdk.JarPath, jdk.JavacPath, jdk.JdkJvmPath, rtJarPath, jdk.IncludePath);
}

return !Log.HasLoggedErrors;
}

XATInfo[] GetJdkRoots ()
{
XATInfo jdk = null;
try {
if (!string.IsNullOrEmpty (JdksRoot))
jdk = new XATInfo (JdksRoot);
} catch (Exception e) {
Log.LogWarning ($"Could not get information about JdksRoot path `{JdksRoot}`: {e.Message}");
Log.LogMessage (MessageImportance.Low, e.ToString ());
}
return jdk == null
? Array.Empty<XATInfo>()
: new[] { jdk };
}

Version GetVersion (string value)
{
if (string.IsNullOrEmpty (value))
Expand Down Expand Up @@ -97,39 +119,35 @@ Action<TraceLevel, string> CreateLogger ()

void WritePropertyFile (string javaPath, string jarPath, string javacPath, string jdkJvmPath, string rtJarPath, IEnumerable<string> includes)
{
var dotnet = string.IsNullOrEmpty (DotnetToolPath) ? "dotnet" : DotnetToolPath;
var msbuild = XNamespace.Get ("http://schemas.microsoft.com/developer/msbuild/2003");
var jdkJvmP = $"JdkJvm{PropertyNameModifier}Path";
var project = new XElement (msbuild + "Project",
new XElement (msbuild + "Choose",
new XElement (msbuild + "When", new XAttribute ("Condition", " '$(JdkJvmPath)' == '' "),
new XElement (msbuild + "When", new XAttribute ("Condition", $" '$({jdkJvmP})' == '' "),
new XElement (msbuild + "PropertyGroup",
new XElement (msbuild + "JdkJvmPath", jdkJvmPath)),
new XElement (msbuild + jdkJvmP, jdkJvmPath)),
new XElement (msbuild + "ItemGroup",
includes.Select (i => new XElement (msbuild + "JdkIncludePath", new XAttribute ("Include", i)))))),
includes.Select (i => new XElement (msbuild + $"Jdk{PropertyNameModifier}IncludePath", new XAttribute ("Include", i)))))),
new XElement (msbuild + "PropertyGroup",
new XElement (msbuild + "JavaSdkDirectory", new XAttribute ("Condition", " '$(JavaSdkDirectory)' == '' "),
JavaHomePath),
new XElement (msbuild + "JavaPath", new XAttribute ("Condition", " '$(JavaPath)' == '' "),
javaPath),
new XElement (msbuild + "JavaCPath", new XAttribute ("Condition", " '$(JavaCPath)' == '' "),
javacPath),
new XElement (msbuild + "JarPath", new XAttribute ("Condition", " '$(JarPath)' == '' "),
jarPath),
new XElement (msbuild + "DotnetToolPath", new XAttribute ("Condition", " '$(DotnetToolPath)' == '' "),
dotnet),
CreateJreRtJarPath (msbuild, rtJarPath)));
CreateProperty (msbuild, $"Java{PropertyNameModifier}SdkDirectory", JavaHomePath),
CreateProperty (msbuild, $"Java{PropertyNameModifier}Path", javaPath),
CreateProperty (msbuild, $"JavaC{PropertyNameModifier}Path", javacPath),
CreateProperty (msbuild, $"Jar{PropertyNameModifier}Path", jarPath),
CreateProperty (msbuild, $"Dotnet{PropertyNameModifier}ToolPath", DotnetToolPath),
CreateProperty (msbuild, $"Jre{PropertyNameModifier}RtJarPath", rtJarPath)));
project.Save (PropertyFile.ItemSpec);
}

static XElement CreateJreRtJarPath (XNamespace msbuild, string rtJarPath)
XElement CreateProperty (XNamespace msbuild, string propertyName, string propertyValue)
{
if (rtJarPath == null)
if (string.IsNullOrEmpty (propertyValue)) {
return null;
return new XElement (msbuild + "JreRtJarPath",
new XAttribute ("Condition", " '$(JreRtJarPath)' == '' "),
rtJarPath);
}
}

return new XElement (msbuild + propertyName,
new XAttribute ("Condition", $" '$({propertyName})' == '' "),
propertyValue);
}
void WriteMakeFragmentFile (string javaPath, string jarPath, string javacPath, string jdkJvmPath, string rtJarPath, IEnumerable<string> includes)
{
using (var o = new StreamWriter (MakeFragmentFile.ItemSpec)) {
Expand Down
14 changes: 13 additions & 1 deletion build-tools/scripts/Prepare.targets
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,26 @@
<PropertyGroup>
<_MaxJdk>$(MaxJdkVersion)</_MaxJdk>
<_MaxJdk Condition=" '$(_MaxJdk)' == '' ">$(JI_MAX_JDK)</_MaxJdk>
<Jdks8Root Condition=" '$(Jdks8Root)' == '' And '$(JAVA_HOME_8_X64)' != '' And Exists($(JAVA_HOME_8_X64)) ">$(JAVA_HOME_8_X64)</Jdks8Root>
</PropertyGroup>
<JdkInfo
JdksRoot="$(ProgramFiles)\Java"
JdksRoot="$(Jdks8Root)"
MakeFragmentFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\JdkInfo.mk"
MaximumJdkVersion="$(_MaxJdk)"
DotnetToolPath="$(DotnetToolPath)"
PropertyFile="$(_TopDir)\bin\Build$(Configuration)\JdkInfo.props">
<Output TaskParameter="JavaHomePath" PropertyName="_JavaSdkDirectory" />
</JdkInfo>
<PropertyGroup>
<Jdks11Root Condition=" '$(Jdks11Root)' == '' And '$(JAVA_HOME_11_X64)' != '' And Exists($(JAVA_HOME_11_X64)) ">$(JAVA_HOME_11_X64)</Jdks11Root>
</PropertyGroup>
<JdkInfo
JdksRoot="$(Jdks11Root)"
PropertyNameModifier="11"
MinimumJdkVersion="11.0"
MaximumJdkVersion="11.99.0"
PropertyFile="$(_TopDir)\bin\Build$(Configuration)\JdkInfo-11.props">
<Output TaskParameter="JavaHomePath" PropertyName="Java11SdkDirectory"/>
</JdkInfo>
</Target>
</Project>
8 changes: 8 additions & 0 deletions build-tools/scripts/jdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,13 @@
PropertyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\JdkInfo.props">
<Output TaskParameter="JavaHomePath" PropertyName="_JavaHome"/>
</JdkInfo>
<JdkInfo
JdksRoot="$(JdksRoot)"
PropertyNameModifier="11"
MinimumJdkVersion="11.0"
MaximumJdkVersion="11.99.0"
PropertyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\JdkInfo-11.props">
<Output TaskParameter="JavaHomePath" PropertyName="Java11SdkDirectory"/>
</JdkInfo>
</Target>
</Project>
10 changes: 4 additions & 6 deletions samples/Hello/Hello.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType>
<OutputPath>..\..\bin\Test$(Configuration)</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup>
<OutputPath>$(TestOutputFullPath)</OutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand All @@ -18,6 +14,8 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Java.Interop\Java.Interop.csproj" />
<ProjectReference Include="..\..\src\Java.Runtime.Environment\Java.Runtime.Environment.csproj" />
<ProjectReference Include="..\..\src\Java.Base\Java.Base.csproj" />
<ProjectReference Include="..\..\tests\TestJVM\TestJVM.csproj" />
</ItemGroup>

</Project>
56 changes: 49 additions & 7 deletions samples/Hello/Program.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
using System;
using System.Threading;

using Mono.Options;

using Java.Interop;

namespace Hello
{
class MainClass
class App
{
public static unsafe void Main (string[] args)
public static void Main (string[] args)
{
Console.WriteLine ("Hello World!");
try {
var ignore = JniRuntime.CurrentRuntime;
} catch (InvalidOperationException e) {
Console.WriteLine (e);
string? jvmPath = global::Java.InteropTests.TestJVM.GetJvmLibraryPath ();
bool createMultipleVMs = false;
bool showHelp = false;
var options = new OptionSet () {
"Using the JVM from C#!",
"",
"Options:",
{ "jvm=",
$"{{PATH}} to JVM to use. Default is:\n {jvmPath}",
v => jvmPath = v },
{ "m",
"Create multiple Java VMs. This will likely creash.",
v => createMultipleVMs = v != null },
{ "h|help",
"Show this message and exit.",
v => showHelp = v != null },
};
options.Parse (args);
if (showHelp) {
options.WriteOptionDescriptions (Console.Out);
return;
}
Console.WriteLine ("Hello World!");
var builder = new JreRuntimeOptions () {
JniAddNativeMethodRegistrationAttributePresent = true,
JvmLibraryPath = jvmPath,
};
builder.AddOption ("-Xcheck:jni");
var jvm = builder.CreateJreVM ();
Console.WriteLine ($"JniRuntime.CurrentRuntime == jvm? {ReferenceEquals (JniRuntime.CurrentRuntime, jvm)}");
foreach (var h in JniRuntime.GetAvailableInvocationPointers ()) {
Console.WriteLine ("PRE: GetCreatedJavaVMHandles: {0}", h);
}

CreateJLO ();

if (createMultipleVMs) {
CreateAnotherJVM ();
}
}

static void CreateJLO ()
{
var jlo = new Java.Lang.Object ();
Console.WriteLine ($"binding? {jlo.ToString ()}");
}

static unsafe void CreateAnotherJVM ()
{
Console.WriteLine ("Part 2!");
using (var vm = new JreRuntimeOptions ().CreateJreVM ()) {
Console.WriteLine ("# JniEnvironment.EnvironmentPointer={0}", JniEnvironment.EnvironmentPointer);
Expand Down
18 changes: 18 additions & 0 deletions src/Java.Base/Java.Base.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<NoWarn>8764</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Java.Interop\Java.Interop.csproj" />
<ProjectReference Include="..\..\tools\class-parse\class-parse.csproj" ReferenceOutputAssembly="False" />
<ProjectReference Include="..\..\tools\generator\generator.csproj" ReferenceOutputAssembly="False" />
</ItemGroup>

<Import Project="Java.Base.targets" />

</Project>
Loading

0 comments on commit bc5bcf4

Please sign in to comment.