Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] Use binutils for Aot and LLVM
Browse files Browse the repository at this point in the history
It appears that an Android NDK installation is no longer needed when
using Aot with LLVM.  Projects which enable Aot and LLVM will no longer
attempt to use the NDK unless it is explicitly requested by setting
`$(AndroidNdkDirectory)` to a valid NDK path in the project file.
  • Loading branch information
pjcollins committed Aug 25, 2022
1 parent a689fb9 commit 538bf6b
Show file tree
Hide file tree
Showing 15 changed files with 62 additions and 91 deletions.
3 changes: 0 additions & 3 deletions build-tools/automation/azure-pipelines.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -691,10 +691,7 @@ stages:

- template: yaml-templates/apk-instrumentation.yaml
parameters:
# TODO: disable LLVM test, see:
# https://github.com/dotnet/runtime/issues/68914
# https://github.com/dotnet/runtime/issues/73304
condition: false
configuration: $(XA.Build.Configuration)
testName: Mono.Android.NET_Tests-AotLlvm
project: tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ They run in a context of an inner build with a single $(RuntimeIdentifier).
</ItemGroup>
<GetAotAssemblies
AndroidAotMode="$(AndroidAotMode)"
AndroidNdkDirectory="$(_AndroidNdkDirectory)"
AndroidNdkDirectory="$(AndroidNdkDirectory)"
AndroidBinUtilsDirectory="$(AndroidBinUtilsDirectory)"
AndroidApiLevel="$(_AndroidApiLevel)"
MinimumSupportedApiLevel="$(AndroidMinimumSupportedApiLevel)"
Expand Down
4 changes: 2 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Tasks/Aot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ static string QuoteFileName(string fileName)

public async override System.Threading.Tasks.Task RunTaskAsync ()
{
NdkTools ndk = NdkTools.Create (AndroidNdkDirectory, logErrors: EnableLLVM, log: Log);
NdkTools ndk = NdkTools.Create (AndroidNdkDirectory, logErrors: UseAndroidNdk, log: Log);
if (Log.HasLoggedErrors) {
return; // NdkTools.Create will log appropriate error
}
Expand Down Expand Up @@ -114,7 +114,7 @@ IEnumerable<Config> GetAotConfigs (NdkTools ndk)
foreach (var abi in SupportedAbis) {
(string aotCompiler, string outdir, string mtriple, AndroidTargetArch arch) = GetAbiSettings (abi);

if (EnableLLVM && !ndk.ValidateNdkPlatform (LogMessage, LogCodedError, arch, enableLLVM:EnableLLVM)) {
if (UseAndroidNdk && !ndk.ValidateNdkPlatform (LogMessage, LogCodedError, arch, enableLLVM:EnableLLVM)) {
yield return Config.Invalid;
yield break;
}
Expand Down
13 changes: 8 additions & 5 deletions src/Xamarin.Android.Build.Tasks/Tasks/GetAotArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public abstract class GetAotArguments : AndroidAsyncTask
protected AotMode AotMode;
protected SequencePointsMode SequencePointsMode;
protected string SdkBinDirectory = "";
protected bool UseAndroidNdk => !string.IsNullOrWhiteSpace (AndroidNdkDirectory);

public static bool GetAndroidAotMode(string androidAotMode, out AotMode aotMode)
{
Expand Down Expand Up @@ -125,7 +126,7 @@ public static bool TryGetSequencePointsMode (string value, out SequencePointsMod
protected string GetToolPrefix (NdkTools ndk, AndroidTargetArch arch, out int level)
{
level = 0;
return EnableLLVM
return UseAndroidNdk
? ndk.GetNdkToolPrefixForAOT (arch, level = GetNdkApiLevel (ndk, arch))
: Path.Combine (AndroidBinUtilsDirectory, $"{ndk.GetArchDirName (arch)}-");
}
Expand Down Expand Up @@ -230,7 +231,7 @@ protected void GetAotOptions (NdkTools ndk, AndroidTargetArch arch, int level, s
MsymPath = outdir;

string ldName;
if (EnableLLVM) {
if (UseAndroidNdk) {
ldName = ndk.GetToolPath (NdkToolKind.Linker, arch, level);
if (!string.IsNullOrEmpty (ldName)) {
ldName = Path.GetFileName (ldName);
Expand All @@ -250,11 +251,11 @@ protected void GetAotOptions (NdkTools ndk, AndroidTargetArch arch, int level, s
}
}

string GetLdFlags(NdkTools ndk, AndroidTargetArch arch, int level, string toolPrefix)
string GetLdFlags (NdkTools ndk, AndroidTargetArch arch, int level, string toolPrefix)
{
var toolchainPath = toolPrefix.Substring (0, toolPrefix.LastIndexOf (Path.DirectorySeparatorChar));
var ldFlags = new StringBuilder ();
if (EnableLLVM) {
if (UseAndroidNdk && EnableLLVM) {
string androidLibPath = string.Empty;
try {
androidLibPath = ndk.GetDirectoryPath (NdkToolchainDir.PlatformLib, arch, level);
Expand Down Expand Up @@ -306,7 +307,9 @@ string GetLdFlags(NdkTools ndk, AndroidTargetArch arch, int level, string toolPr
// Without the flag, `lld` will modify AOT-generated code in a way that the Mono runtime doesn't support. Until
// the runtime issue is fixed, we need to pass this flag then.
//
ldFlags.Append ("--no-relax");
if (!UseAndroidNdk) {
ldFlags.Append ("--no-relax");
}
}

if (StripLibraries) {
Expand Down
2 changes: 1 addition & 1 deletion src/Xamarin.Android.Build.Tasks/Tasks/GetAotAssemblies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class GetAotAssemblies : GetAotArguments

public override Task RunTaskAsync ()
{
NdkTools ndk = NdkTools.Create (AndroidNdkDirectory, logErrors: EnableLLVM, log: Log);
NdkTools ndk = NdkTools.Create (AndroidNdkDirectory, logErrors: UseAndroidNdk, log: Log);
if (Log.HasLoggedErrors) {
return Task.CompletedTask; // NdkTools.Create will log appropriate error
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,74 +111,43 @@ public void BuildBasicApplicationReleaseProfiledAotWithoutDefaultProfile ()
}

static object [] AotChecks () => new object [] {
new object[] {
/* supportedAbis */ "armeabi-v7a",
/* enableLLVM */ false,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
},
new object[] {
/* supportedAbis */ "armeabi-v7a",
/* enableLLVM */ false,
/* expectedResult */ true,
/* usesAssemblyBlobs */ true,
},
new object[] {
/* supportedAbis */ "armeabi-v7a",
/* enableLLVM */ true,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
},
new object[] {
/* supportedAbis */ "arm64-v8a",
/* enableLLVM */ false,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
},
new object[] {
/* supportedAbis */ "arm64-v8a",
/* enableLLVM */ true,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
},
new object[] {
/* supportedAbis */ "x86",
/* enableLLVM */ false,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
},
new object[] {
/* supportedAbis */ "x86",
/* supportedAbis */ "armeabi-v7a;x86",
/* enableLLVM */ true,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
/* usesAssemblyBlobs */ true,
},
new object[] {
/* supportedAbis */ "x86_64",
/* supportedAbis */ "armeabi-v7a;arm64-v8a;x86;x86_64",
/* enableLLVM */ false,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
/* usesAssemblyBlobs */ true,
},
new object[] {
/* supportedAbis */ "x86_64",
/* supportedAbis */ "armeabi-v7a;arm64-v8a;x86;x86_64",
/* enableLLVM */ true,
/* expectedResult */ true,
/* usesAssemblyBlobs */ false,
},
};

[Test]
[TestCaseSource (nameof (AotChecks))]
public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableLLVM, bool expectedResult, bool usesAssemblyBlobs)
public void BuildAotApplicationWithNdkAndBundleAndÜmläüts (string supportedAbis, bool enableLLVM, bool usesAssemblyBlobs)
{
var path = Path.Combine ("temp", string.Format ("BuildAotApplication AndÜmläüts_{0}_{1}_{2}_{3}", supportedAbis, enableLLVM, expectedResult, usesAssemblyBlobs));
var abisSanitized = supportedAbis.Replace (";", "").Replace ("-", "").Replace ("_", "");
var path = Path.Combine ("temp", string.Format ("BuildAotNdk AndÜmläüts_{0}_{1}_{2}", abisSanitized, enableLLVM, usesAssemblyBlobs));
var proj = new XamarinAndroidApplicationProject () {
IsRelease = true,
BundleAssemblies = false,
AotAssemblies = true,
PackageName = "com.xamarin.buildaotappwithspecialchars",
};
proj.SetProperty (KnownProperties.TargetFrameworkVersion, "v5.1");
if (!Builder.UseDotNet) {
proj.BundleAssemblies = true;
}
proj.SetProperty ("AndroidNdkDirectory", AndroidNdkPath);
proj.SetAndroidSupportedAbis (supportedAbis);
proj.SetProperty ("EnableLLVM", enableLLVM.ToString ());
proj.SetProperty ("AndroidUseAssemblyStore", usesAssemblyBlobs.ToString ());
Expand All @@ -193,12 +162,8 @@ public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableL
</manifest>";
}
using (var b = CreateApkBuilder (path)) {
if (!b.GetSupportedRuntimes ().Any (x => supportedAbis == x.Abi))
Assert.Ignore ($"Runtime for {supportedAbis} was not available.");
b.ThrowOnBuildFailure = false;
Assert.AreEqual (expectedResult, b.Build (proj), "Build should have {0}.", expectedResult ? "succeeded" : "failed");
if (!expectedResult)
return;
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
//NOTE: Windows has shortened paths such as: C:\Users\myuser\ANDROI~3\ndk\PLATFO~1\AN3971~1\arch-x86\usr\lib\libc.so
if (checkMinLlvmPath && !IsWindows && !Builder.UseDotNet) {
Xamarin.Android.Tasks.NdkTools ndk = Xamarin.Android.Tasks.NdkTools.Create (AndroidNdkPath);
Expand All @@ -216,8 +181,11 @@ public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableL
}
foreach (var abi in supportedAbis.Split (new char [] { ';' })) {
var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath);
var libapp = Path.Combine (intermediate, "bundles", abi, "libmonodroid_bundle_app.so");
FileAssert.DoesNotExist (libapp);
if (!Builder.UseDotNet) {
var libapp = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath,
"bundles", abi, "libmonodroid_bundle_app.so");
Assert.IsTrue (File.Exists (libapp), abi + " libmonodroid_bundle_app.so does not exist");
}
var aotNativeLibrary = Builder.UseDotNet ?
Path.Combine (intermediate, AbiUtils.AbiToRuntimeIdentifier (abi), "aot", "UnnamedProject.dll.so") :
Path.Combine (intermediate, "aot", abi, "libaot-UnnamedProject.dll.so");
Expand All @@ -226,14 +194,19 @@ public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableL
proj.OutputPath, $"{proj.PackageName}-Signed.apk");

var helper = new ArchiveAssemblyHelper (apk, usesAssemblyBlobs);
Assert.IsTrue (helper.Exists ("assemblies/UnnamedProject.dll"), $"UnnamedProject.dll should be in the {proj.PackageName}-Signed.apk");
if (!Builder.UseDotNet) {
// BundleAssemblies=true
Assert.IsFalse (helper.Exists ("assemblies/UnnamedProject.dll"), $"UnnamedProject.dll should not be in the {proj.PackageName}-Signed.apk");
} else {
Assert.IsTrue (helper.Exists ("assemblies/UnnamedProject.dll"), $"UnnamedProject.dll should be in the {proj.PackageName}-Signed.apk");
}
using (var zipFile = ZipHelper.OpenZip (apk)) {
Assert.IsNotNull (ZipHelper.ReadFileFromZip (zipFile,
string.Format ("lib/{0}/libaot-UnnamedProject.dll.so", abi)),
$"lib/{0}/libaot-UnnamedProject.dll.so should be in the {proj.PackageName}-Signed.apk", abi);
}
}
Assert.AreEqual (expectedResult, b.Build (proj), "Second Build should have {0}.", expectedResult ? "succeeded" : "failed");
Assert.IsTrue (b.Build (proj), "Second Build should have succeeded.");
Assert.IsTrue (
b.Output.IsTargetSkipped ("_CompileJava"),
"the _CompileJava target should be skipped");
Expand All @@ -245,46 +218,39 @@ public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableL

[Test]
[TestCaseSource (nameof (AotChecks))]
[Category ("Minor"), Category ("MkBundle")]
public void BuildAotApplicationAndBundleAndÜmläüts (string supportedAbis, bool enableLLVM, bool expectedResult, bool usesAssemblyBlobs)
public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableLLVM, bool usesAssemblyBlobs)
{
var path = Path.Combine ("temp", string.Format ("BuildAotApplicationAndBundle AndÜmläüts_{0}_{1}_{2}_{3}", supportedAbis, enableLLVM, expectedResult, usesAssemblyBlobs));
var abisSanitized = supportedAbis.Replace (";", "").Replace ("-", "").Replace ("_", "");
var path = Path.Combine ("temp", string.Format ("BuildAot AndÜmläüts_{0}_{1}_{2}", abisSanitized, enableLLVM, usesAssemblyBlobs));
var proj = new XamarinAndroidApplicationProject () {
IsRelease = true,
BundleAssemblies = true,
AotAssemblies = true,
PackageName = "com.xamarin.buildaotappandbundlewithspecialchars",
};
proj.SetProperty ("AndroidNdkDirectory", AndroidNdkPath);
proj.SetProperty (KnownProperties.TargetFrameworkVersion, "v5.1");
proj.SetAndroidSupportedAbis (supportedAbis);
proj.SetProperty ("EnableLLVM", enableLLVM.ToString ());
proj.SetProperty ("AndroidUseAssemblyStore", usesAssemblyBlobs.ToString ());
using (var b = CreateApkBuilder (path)) {
if (!b.GetSupportedRuntimes ().Any (x => supportedAbis == x.Abi))
Assert.Ignore ($"Runtime for {supportedAbis} was not available.");
b.ThrowOnBuildFailure = false;
Assert.AreEqual (expectedResult, b.Build (proj), "Build should have {0}.", expectedResult ? "succeeded" : "failed");
if (!expectedResult)
return;
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
foreach (var abi in supportedAbis.Split (new char [] { ';' })) {
var libapp = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath,
"bundles", abi, "libmonodroid_bundle_app.so");
Assert.IsTrue (File.Exists (libapp), abi + " libmonodroid_bundle_app.so does not exist");
var assemblies = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath,
"aot", abi, "libaot-UnnamedProject.dll.so");
Assert.IsTrue (File.Exists (assemblies), "{0} libaot-UnnamedProject.dll.so does not exist", abi);
var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath);
var aotNativeLibrary = Builder.UseDotNet ?
Path.Combine (intermediate, AbiUtils.AbiToRuntimeIdentifier (abi), "aot", "UnnamedProject.dll.so") :
Path.Combine (intermediate, "aot", abi, "libaot-UnnamedProject.dll.so");
FileAssert.Exists (aotNativeLibrary);
var apk = Path.Combine (Root, b.ProjectDirectory,
proj.OutputPath, $"{proj.PackageName}-Signed.apk");

var helper = new ArchiveAssemblyHelper (apk, usesAssemblyBlobs);
Assert.IsFalse (helper.Exists ("assemblies/UnnamedProject.dll"), $"UnnamedProject.dll should not be in the {proj.PackageName}-Signed.apk");
Assert.IsTrue (helper.Exists ("assemblies/UnnamedProject.dll"), $"UnnamedProject.dll should be in the {proj.PackageName}-Signed.apk");
using (var zipFile = ZipHelper.OpenZip (apk)) {
Assert.IsNotNull (ZipHelper.ReadFileFromZip (zipFile,
string.Format ("lib/{0}/libaot-UnnamedProject.dll.so", abi)),
$"lib/{0}/libaot-UnnamedProject.dll.so should be in the {proj.PackageName}-Signed.apk", abi);
}
}
Assert.AreEqual (expectedResult, b.Build (proj), "Second Build should have {0}.", expectedResult ? "succeeded" : "failed");
Assert.IsTrue (b.Build (proj), "Second Build should have succeeded.");
Assert.IsTrue (
b.Output.IsTargetSkipped ("_CompileJava"),
"the _CompileJava target should be skipped");
Expand Down Expand Up @@ -441,7 +407,6 @@ public static void Foo () {
}

[Test]
[Ignore ("Ignore while investigating/fixing.")]
[Category ("LLVM")]
public void NoSymbolsArgShouldReduceAppSize ([Values ("", "Hybrid")] string androidAotMode, [Values (false, true)] bool skipDebugSymbols)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ public void XA5104AndroidNdkNotFound (string androidNdkDirectory)
IntermediateAssemblyDir = path
};

Assert.IsFalse (task2.Execute (), "Task should fail!");
BuildErrorEventArgs error2 = errors [1];
Assert.AreEqual (error1.Message, error2.Message, "Aot and MakeBundleNativeCodeExternal should produce the same error messages.");
if (androidNdkDirectory == "DoesNotExist") {
Assert.IsFalse (task2.Execute (), "Task should fail!");
BuildErrorEventArgs error2 = errors [1];
Assert.AreEqual (error1.Message, error2.Message, "Aot and MakeBundleNativeCodeExternal should produce the same error messages.");
} else {
Assert.IsTrue (task2.Execute (), "Aot task should succeed with null or empty NDK!");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ projects. .NET 5 projects will not import this file.
<Aot
Condition="'$(AotAssemblies)' == 'True'"
AndroidAotMode="$(AndroidAotMode)"
AndroidNdkDirectory="$(_AndroidNdkDirectory)"
AndroidNdkDirectory="$(AndroidNdkDirectory)"
AndroidBinUtilsDirectory="$(AndroidBinUtilsDirectory)"
AndroidApiLevel="$(_AndroidApiLevel)"
ManifestFile="$(IntermediateOutputPath)android\AndroidManifest.xml"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ projects.
LatestSupportedJavaVersion="$(LatestSupportedJavaVersion)"
ReferenceAssemblyPaths="@(_AndroidApiInfoDirectories)">
<Output TaskParameter="CommandLineToolsPath" PropertyName="_AndroidToolsDirectory" />
<Output TaskParameter="AndroidNdkPath" PropertyName="AndroidNdkDirectory" Condition=" '$(AndroidNdkDirectory)' == '' " />
<Output TaskParameter="AndroidSdkPath" PropertyName="AndroidSdkDirectory" Condition=" '$(AndroidSdkDirectory)' == '' " />
<Output TaskParameter="JavaSdkPath" PropertyName="JavaSdkDirectory" Condition=" '$(JavaSdkDirectory)' == '' " />
<Output TaskParameter="AndroidNdkPath" PropertyName="_AndroidNdkDirectory" />
Expand Down
1 change: 1 addition & 0 deletions tests/Mono.Android-Tests/Mono.Android-Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Import Project="..\..\Configuration.props" />
<PropertyGroup>
<TargetFrameworkVersion>$(AndroidLatestStableFrameworkVersion)</TargetFrameworkVersion>
<AndroidNdkDirectory></AndroidNdkDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Import Project="..\..\..\Configuration.props" />
<PropertyGroup>
<TargetFrameworkVersion>$(AndroidFrameworkVersion)</TargetFrameworkVersion>
<AndroidNdkDirectory></AndroidNdkDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PlotDataLabelSuffix>-$(TestsFlavor)NET6</PlotDataLabelSuffix>
<WarningsAsErrors>IL2037</WarningsAsErrors>
<AndroidUseNegotiateAuthentication>true</AndroidUseNegotiateAuthentication>
<AndroidNdkDirectory></AndroidNdkDirectory>
<!--
TODO: Fix excluded tests
For $(EnableLLVM), InetAccess excluded due to: https://github.com/dotnet/runtime/issues/56315
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Import Project="..\..\..\Configuration.props" />
<PropertyGroup>
<TargetFrameworkVersion>$(AndroidFrameworkVersion)</TargetFrameworkVersion>
<AndroidNdkDirectory></AndroidNdkDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Import Project="..\..\..\Configuration.props" />
<PropertyGroup>
<TargetFrameworkVersion>$(AndroidFrameworkVersion)</TargetFrameworkVersion>
<AndroidNdkDirectory></AndroidNdkDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down
2 changes: 0 additions & 2 deletions tools/xabuild/XABuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,6 @@ static void CreateConfig (XABuildPaths paths)
SetProperty (toolsets, "TargetFrameworkRootPath", paths.FrameworksDirectory + Path.DirectorySeparatorChar); //NOTE: Must include trailing \
if (!string.IsNullOrEmpty (paths.AndroidSdkDirectory))
SetProperty (toolsets, "AndroidSdkDirectory", paths.AndroidSdkDirectory);
if (!string.IsNullOrEmpty (paths.AndroidNdkDirectory))
SetProperty (toolsets, "AndroidNdkDirectory", paths.AndroidNdkDirectory);

var projectImportSearchPaths = toolsets.SelectSingleNode ("projectImportSearchPaths");
var searchPaths = projectImportSearchPaths.SelectSingleNode ($"searchPaths[@os='{paths.SearchPathsOS}']") as XmlElement;
Expand Down

0 comments on commit 538bf6b

Please sign in to comment.