Support for auto discovery and download of runtimes.

This commit is contained in:
Paul Knopf 2019-04-13 13:58:38 -04:00
parent b58be97185
commit cb67be5a5a
10 changed files with 454 additions and 143 deletions

View file

@ -0,0 +1,52 @@
using System;
using System.IO;
using Qml.Net.Runtimes;
namespace Qml.Net.Tests
{
public class BaseRuntimeManagerTests : BaseTests
{
// ReSharper disable MemberCanBePrivate.Global
protected readonly string _runtimeUserDirectory;
protected readonly string _runtimeExecutableDirectory;
protected readonly string _runtimeCurrentDirectory;
// ReSharper restore MemberCanBePrivate.Global
private readonly string _tmpDirectory;
private readonly Func<string> _oldRuntimeUserDirectory;
private readonly Func<string> _oldRuntimeExecutableDirectory;
private readonly Func<string> _oldRuntimeCurrentDirectory;
public BaseRuntimeManagerTests()
{
_tmpDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().Replace("-", ""));
Directory.CreateDirectory(_tmpDirectory);
_runtimeUserDirectory = Path.Combine(_tmpDirectory, "user");
_runtimeExecutableDirectory = Path.Combine(_tmpDirectory, "executable");
_runtimeCurrentDirectory = Path.Combine(_tmpDirectory, "current");
Directory.CreateDirectory(_runtimeUserDirectory);
Directory.CreateDirectory(_runtimeExecutableDirectory);
Directory.CreateDirectory(_runtimeCurrentDirectory);
_oldRuntimeUserDirectory = RuntimeManager.GetRuntimeUserDirectory;
_oldRuntimeExecutableDirectory = RuntimeManager.GetRuntimeExecutableDirectory;
_oldRuntimeCurrentDirectory = RuntimeManager.GetRuntimeCurrentDirectory;
RuntimeManager.GetRuntimeUserDirectory = () => _runtimeUserDirectory;
RuntimeManager.GetRuntimeExecutableDirectory = () => _runtimeExecutableDirectory;
RuntimeManager.GetRuntimeCurrentDirectory = () => _runtimeCurrentDirectory;
}
public override void Dispose()
{
Directory.Delete(_tmpDirectory, true);
RuntimeManager.GetRuntimeUserDirectory = _oldRuntimeUserDirectory;
RuntimeManager.GetRuntimeExecutableDirectory = _oldRuntimeExecutableDirectory;
RuntimeManager.GetRuntimeCurrentDirectory = _oldRuntimeCurrentDirectory;
base.Dispose();
}
}
}

View file

@ -0,0 +1,79 @@
using System.IO;
using System.Linq;
using FluentAssertions;
using Qml.Net.Runtimes;
using Xunit;
namespace Qml.Net.Tests
{
public class RuntimeManagerDiscoveryTests : BaseRuntimeManagerTests
{
[Theory]
[InlineData(RuntimeManager.RuntimeSearchLocation.UserDirectory)]
public void Can_find_qt_runtimes(RuntimeManager.RuntimeSearchLocation runtimeSearchLocation)
{
var directory = Path.Combine(RuntimeManager.GetPotentialRuntimesDirectories(runtimeSearchLocation).Single());
var found = RuntimeManager.FindQtRuntime(
RuntimeManager.GetPotentialRuntimesDirectories(runtimeSearchLocation),
"qt-version",
RuntimeTarget.Windows64);
found.Should().BeNullOrEmpty();
File.WriteAllText(Path.Combine(directory, "version.txt"), "qt-version-win-x64");
found = RuntimeManager.FindQtRuntime(
RuntimeManager.GetPotentialRuntimesDirectories(runtimeSearchLocation),
"qt-version",
RuntimeTarget.Windows64);
found.Should().Be(directory);
File.Delete(Path.Combine(directory, "version.txt"));
var nestedRuntimeDirectory = Path.Combine(directory, "qt-version-win-x64");
Directory.CreateDirectory(nestedRuntimeDirectory);
File.WriteAllText(Path.Combine(nestedRuntimeDirectory, "version.txt"), "qt-version-win-x64");
found = RuntimeManager.FindQtRuntime(
RuntimeManager.GetPotentialRuntimesDirectories(runtimeSearchLocation),
"qt-version",
RuntimeTarget.Windows64);
found.Should().Be(nestedRuntimeDirectory);
}
[Fact]
public void Can_find_runtimes_in_proper_order()
{
File.WriteAllText(Path.Combine(_runtimeCurrentDirectory, "version.txt"), "qt-version-win-x64");
var found = RuntimeManager.FindQtRuntime(
RuntimeManager.GetPotentialRuntimesDirectories(),
"qt-version",
RuntimeTarget.Windows64);
found.Should().Be(_runtimeCurrentDirectory);
File.WriteAllText(Path.Combine(_runtimeUserDirectory, "version.txt"), "qt-version-win-x64");
found = RuntimeManager.FindQtRuntime(
RuntimeManager.GetPotentialRuntimesDirectories(),
"qt-version",
RuntimeTarget.Windows64);
found.Should().Be(_runtimeUserDirectory);
File.WriteAllText(Path.Combine(_runtimeExecutableDirectory, "version.txt"), "qt-version-win-x64");
found = RuntimeManager.FindQtRuntime(
RuntimeManager.GetPotentialRuntimesDirectories(),
"qt-version",
RuntimeTarget.Windows64);
found.Should().Be(_runtimeExecutableDirectory);
}
}
}

View file

@ -66,6 +66,12 @@ namespace Qml.Net.Tests
}
}
[Fact]
public void Test()
{
var potential = RuntimeManager.GetPotentialRuntimesDirectories();
}
public void Dispose()
{
if (Directory.Exists(_tempDirectory))

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Qml.Net.Runtimes
{
internal class Chmod
internal static class Chmod
{
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string pathname, int mode);

View file

@ -4,6 +4,7 @@ namespace Qml.Net.Runtimes
{
LinuxX64,
OSX64,
Windows64
Windows64,
Unsupported
}
}

View file

@ -0,0 +1,161 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Qml.Net.Runtimes
{
public static partial class RuntimeManager
{
public static void ConfigureRuntimeDirectory(string directory)
{
if (string.IsNullOrEmpty(directory))
{
throw new ArgumentNullException(nameof(directory));
}
if (!Directory.Exists(directory))
{
throw new Exception("The directory doesn't exist.");
}
var versionFile = Path.Combine(directory, "version.txt");
if (!File.Exists(versionFile))
{
throw new Exception("The version.txt file doesn't exist in the directory.");
}
var version = File.ReadAllText(versionFile).TrimEnd(Environment.NewLine.ToCharArray());
var expectedVersion = $"{QmlNetConfig.QtBuildVersion}-{RuntimeTargetToString(GetCurrentRuntimeTarget())}";
if (version != expectedVersion)
{
throw new Exception($"The version of the runtime directory was {versionFile}, but expected {expectedVersion}");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var pluginsDirectory = Path.Combine(directory, "qt", "plugins");
if (!Directory.Exists(pluginsDirectory))
{
throw new Exception($"Plugins directory didn't exist: {pluginsDirectory}");
}
Environment.SetEnvironmentVariable("QT_PLUGIN_PATH", pluginsDirectory);
var qmlDirectory = Path.Combine(directory, "qt", "qml");
if (!Directory.Exists(qmlDirectory))
{
throw new Exception($"QML directory didn't exist: {qmlDirectory}");
}
Environment.SetEnvironmentVariable("QML2_IMPORT_PATH", qmlDirectory);
var libDirectory = Path.Combine(directory, "qt", "lib");
if (!Directory.Exists(libDirectory))
{
throw new Exception($"The lib directory didn't exist: {libDirectory}");
}
var preloadPath = Path.Combine(libDirectory, "preload.txt");
if (!File.Exists(preloadPath))
{
throw new Exception($"The preload.txt file didn't exist: {preloadPath}");
}
var libsToPreload = File.ReadAllLines(preloadPath).Where(x => !string.IsNullOrEmpty(x))
.Select(x => Path.Combine(libDirectory, x))
.ToList();
var platformLoader = NetNativeLibLoader.Loader.PlatformLoaderBase.SelectPlatformLoader();
foreach (var libToPreload in libsToPreload)
{
var libHandler = platformLoader.LoadLibrary(libToPreload);
if (libHandler == IntPtr.Zero)
{
throw new Exception($"Unabled to preload library: {libToPreload}");
}
}
return;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var pluginsDirectory = Path.Combine(directory, "qt", "plugins");
if (!Directory.Exists(pluginsDirectory))
{
throw new Exception($"Plugins directory didn't exist: {pluginsDirectory}");
}
Environment.SetEnvironmentVariable("QT_PLUGIN_PATH", pluginsDirectory);
var qmlDirectory = Path.Combine(directory, "qt", "qml");
if (!Directory.Exists(qmlDirectory))
{
throw new Exception($"QML directory didn't exist: {qmlDirectory}");
}
Environment.SetEnvironmentVariable("QML2_IMPORT_PATH", qmlDirectory);
var libDirectory = Path.Combine(directory, "qt", "lib");
if (!Directory.Exists(libDirectory))
{
throw new Exception($"The lib directory didn't exist: {libDirectory}");
}
var preloadPath = Path.Combine(libDirectory, "preload.txt");
if (!File.Exists(preloadPath))
{
throw new Exception($"The preload.txt file didn't exist: {preloadPath}");
}
var libsToPreload = File.ReadAllLines(preloadPath).Where(x => !string.IsNullOrEmpty(x))
.Select(x => Path.Combine(libDirectory, x))
.ToList();
var platformLoader = NetNativeLibLoader.Loader.PlatformLoaderBase.SelectPlatformLoader();
foreach (var libToPreload in libsToPreload)
{
var libHandler = platformLoader.LoadLibrary(libToPreload);
if (libHandler == IntPtr.Zero)
{
throw new Exception($"Unabled to preload library: {libToPreload}");
}
}
return;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var pluginsDirectory = Path.Combine(directory, "qt", "plugins");
if (!Directory.Exists(pluginsDirectory))
{
throw new Exception($"Plugins directory didn't exist: {pluginsDirectory}");
}
Environment.SetEnvironmentVariable("QT_PLUGIN_PATH", pluginsDirectory);
var qmlDirectory = Path.Combine(directory, "qt", "qml");
if (!Directory.Exists(qmlDirectory))
{
throw new Exception($"QML directory didn't exist: {qmlDirectory}");
}
Environment.SetEnvironmentVariable("QML2_IMPORT_PATH", qmlDirectory);
var binDirectory = Path.Combine(directory, "qt", "bin");
if (!Directory.Exists(binDirectory))
{
throw new Exception($"The bin directory didn't exist: {binDirectory}");
}
Environment.SetEnvironmentVariable("PATH", $"{binDirectory};{Environment.GetEnvironmentVariable("PATH")}");
var preloadPath = Path.Combine(binDirectory, "preload.txt");
if (!File.Exists(preloadPath))
{
throw new Exception($"The preload.txt file didn't exist: {preloadPath}");
}
return;
}
throw new Exception("Unknown platform, can't configure runtime directory");
}
}
}

View file

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Qml.Net.Runtimes
{
public static partial class RuntimeManager
{
[Flags]
public enum RuntimeSearchLocation
{
UserDirectory = 1,
ExecutableDirectory = 1 << 1,
CurrentDirectory = 1 << 2,
All = UserDirectory | ExecutableDirectory | CurrentDirectory
}
internal static Func<string> GetRuntimeUserDirectory = () =>
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".qmlnet-qt-runtimes");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(Environment.GetEnvironmentVariable("%HOMEDRIVE%%HOMEPATH%"), ".qmlnet-qt-runtimes");
}
throw new Exception("Unknown platform, can't get user runtimes directory");
};
internal static Func<string> GetRuntimeExecutableDirectory = () => Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
internal static Func<string> GetRuntimeCurrentDirectory = () => Path.Combine(Directory.GetCurrentDirectory(), "qmlnet-qt-runtimes");
public static string[] GetPotentialRuntimesDirectories(RuntimeSearchLocation runtimes = RuntimeSearchLocation.All)
{
var result = new List<string>();
// Order matters, most important location first.
if (runtimes.HasFlag(RuntimeSearchLocation.ExecutableDirectory))
{
result.Add(Path.Combine(GetRuntimeExecutableDirectory()));
}
if (runtimes.HasFlag(RuntimeSearchLocation.UserDirectory))
{
result.Add(GetRuntimeUserDirectory());
}
if (runtimes.HasFlag(RuntimeSearchLocation.CurrentDirectory))
{
result.Add(GetRuntimeCurrentDirectory());
}
return result.ToArray();
}
public static string FindQtRuntime(string[] runtimeDirectories, string qtVersion, RuntimeTarget target)
{
if (runtimeDirectories.Length == 0)
{
return null;
}
var expectedVersion = $"{qtVersion}-{RuntimeTargetToString(target)}";
foreach (var potentialRuntimeDirectory in runtimeDirectories)
{
if (!Directory.Exists(potentialRuntimeDirectory))
{
// Obviously nothing here...
continue;
}
// Maybe this directory contains a Qt runtime?
var versionPath = Path.Combine(potentialRuntimeDirectory, "version.txt");
if (File.Exists(versionPath))
{
var version = File.ReadAllText(versionPath).Trim(Environment.NewLine.ToCharArray());
if (version == expectedVersion)
{
// Found it!
return potentialRuntimeDirectory;
}
}
// Maybe there are nested directories of runtimes?
var nestedDirectory = Path.Combine(potentialRuntimeDirectory, expectedVersion);
if (Directory.Exists(nestedDirectory))
{
versionPath = Path.Combine(nestedDirectory, "version.txt");
if (File.Exists(versionPath))
{
var version = File.ReadAllText(versionPath).Trim(Environment.NewLine.ToCharArray());
if (version == expectedVersion)
{
// Found it!
return nestedDirectory;
}
}
}
}
return null;
}
}
}

View file

@ -1,4 +1,6 @@
using System;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
@ -7,7 +9,7 @@ using System.Threading;
namespace Qml.Net.Runtimes
{
public static class RuntimeManager
public static partial class RuntimeManager
{
public delegate string BuildRuntimeUrlDelegate(string qtVersion, RuntimeTarget target);
@ -26,6 +28,8 @@ namespace Qml.Net.Runtimes
return "linux-x64";
case RuntimeTarget.OSX64:
return "osx-x64";
case RuntimeTarget.Unsupported:
throw new Exception("Unsupported target");
default:
throw new Exception($"Unknown target {target}");
}
@ -33,13 +37,16 @@ namespace Qml.Net.Runtimes
public delegate void ExtractTarGZStreamDelegate(Stream stream, string destinationDirectory);
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
public static ExtractTarGZStreamDelegate ExtractTarGZStream = Tar.ExtractTarFromGzipStream;
// ReSharper disable once MemberCanBePrivate.Global
public static RuntimeTarget GetCurrentRuntimeTarget()
{
if (IntPtr.Size != 8)
{
throw new Exception("Only 64bit supported");
return RuntimeTarget.Unsupported;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -57,7 +64,7 @@ namespace Qml.Net.Runtimes
return RuntimeTarget.OSX64;
}
throw new Exception("Unknown OS platform");
return RuntimeTarget.Unsupported;
}
public static void DownloadRuntimeToDirectory(
@ -94,7 +101,7 @@ namespace Qml.Net.Runtimes
});
}
public static void GetUrlStream(string url, Action<Stream> action)
private static void GetUrlStream(string url, Action<Stream> action)
{
var syncContext = SynchronizationContext.Current;
try
@ -112,147 +119,40 @@ namespace Qml.Net.Runtimes
}
}
public static void ConfigureRuntimeDirectory(string directory)
public static string FindSuitableQtRuntime(RuntimeSearchLocation runtimeSearchLocation = RuntimeSearchLocation.All)
{
if (string.IsNullOrEmpty(directory))
var potentials = GetPotentialRuntimesDirectories(runtimeSearchLocation);
return FindQtRuntime(potentials, QmlNetConfig.QtBuildVersion, GetCurrentRuntimeTarget());
}
public static void DiscoverOrDownloadSuitableQtRuntime(RuntimeSearchLocation runtimeSearchLocation = RuntimeSearchLocation.All)
{
var suitableRuntime = FindSuitableQtRuntime(runtimeSearchLocation);
if (!string.IsNullOrEmpty(suitableRuntime))
{
throw new ArgumentNullException(nameof(directory));
// Found one!
ConfigureRuntimeDirectory(suitableRuntime);
return;
}
if (!Directory.Exists(directory))
var currentTarget = GetCurrentRuntimeTarget();
var version = $"{QmlNetConfig.QtBuildVersion}-{RuntimeTargetToString(currentTarget)}";
// Let's try to download and install the Qt runtime into the users directory.
var destinationDirectory = Path.Combine(GetPotentialRuntimesDirectories(RuntimeSearchLocation.UserDirectory).Single(), version);
var destinationTmpDirectory = $"{destinationDirectory}-{Guid.NewGuid().ToString().Replace("-", "")}";
Directory.CreateDirectory(destinationTmpDirectory);
DownloadRuntimeToDirectory(QmlNetConfig.QtBuildVersion, currentTarget, destinationTmpDirectory);
if (Directory.Exists(destinationDirectory))
{
throw new Exception("The directory doesn't exist.");
Directory.Delete(destinationDirectory, true);
}
var versionFile = Path.Combine(directory, "version.txt");
Directory.Move(destinationTmpDirectory, destinationDirectory);
if (!File.Exists(versionFile))
{
throw new Exception("The version.txt file doesn't exist in the directory.");
}
var version = File.ReadAllText(versionFile).TrimEnd(Environment.NewLine.ToCharArray());
var expectedVersion = $"{QmlNetConfig.QtBuildVersion}-{RuntimeTargetToString(GetCurrentRuntimeTarget())}";
if (version != expectedVersion)
{
throw new Exception($"The version of the runtime directory was {versionFile}, but expected {expectedVersion}");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var pluginsDirectory = Path.Combine(directory, "qt", "plugins");
if (!Directory.Exists(pluginsDirectory))
{
throw new Exception($"Plugins directory didn't exist: {pluginsDirectory}");
}
Environment.SetEnvironmentVariable("QT_PLUGIN_PATH", pluginsDirectory);
var qmlDirectory = Path.Combine(directory, "qt", "qml");
if (!Directory.Exists(qmlDirectory))
{
throw new Exception($"QML directory didn't exist: {qmlDirectory}");
}
Environment.SetEnvironmentVariable("QML2_IMPORT_PATH", qmlDirectory);
var libDirectory = Path.Combine(directory, "qt", "lib");
if (!Directory.Exists(libDirectory))
{
throw new Exception($"The lib directory didn't exist: {libDirectory}");
}
var preloadPath = Path.Combine(libDirectory, "preload.txt");
if (!File.Exists(preloadPath))
{
throw new Exception($"The preload.txt file didn't exist: {preloadPath}");
}
var libsToPreload = File.ReadAllLines(preloadPath).Where(x => !string.IsNullOrEmpty(x))
.Select(x => Path.Combine(libDirectory, x))
.ToList();
var platformLoader = NetNativeLibLoader.Loader.PlatformLoaderBase.SelectPlatformLoader();
foreach (var libToPreload in libsToPreload)
{
var libHandler = platformLoader.LoadLibrary(libToPreload);
if (libHandler == IntPtr.Zero)
{
throw new Exception($"Unabled to preload library: {libToPreload}");
}
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var pluginsDirectory = Path.Combine(directory, "qt", "plugins");
if (!Directory.Exists(pluginsDirectory))
{
throw new Exception($"Plugins directory didn't exist: {pluginsDirectory}");
}
Environment.SetEnvironmentVariable("QT_PLUGIN_PATH", pluginsDirectory);
var qmlDirectory = Path.Combine(directory, "qt", "qml");
if (!Directory.Exists(qmlDirectory))
{
throw new Exception($"QML directory didn't exist: {qmlDirectory}");
}
Environment.SetEnvironmentVariable("QML2_IMPORT_PATH", qmlDirectory);
var libDirectory = Path.Combine(directory, "qt", "lib");
if (!Directory.Exists(libDirectory))
{
throw new Exception($"The lib directory didn't exist: {libDirectory}");
}
var preloadPath = Path.Combine(libDirectory, "preload.txt");
if (!File.Exists(preloadPath))
{
throw new Exception($"The preload.txt file didn't exist: {preloadPath}");
}
var libsToPreload = File.ReadAllLines(preloadPath).Where(x => !string.IsNullOrEmpty(x))
.Select(x => Path.Combine(libDirectory, x))
.ToList();
var platformLoader = NetNativeLibLoader.Loader.PlatformLoaderBase.SelectPlatformLoader();
foreach (var libToPreload in libsToPreload)
{
var libHandler = platformLoader.LoadLibrary(libToPreload);
if (libHandler == IntPtr.Zero)
{
throw new Exception($"Unabled to preload library: {libToPreload}");
}
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var pluginsDirectory = Path.Combine(directory, "qt", "plugins");
if (!Directory.Exists(pluginsDirectory))
{
throw new Exception($"Plugins directory didn't exist: {pluginsDirectory}");
}
Environment.SetEnvironmentVariable("QT_PLUGIN_PATH", pluginsDirectory);
var qmlDirectory = Path.Combine(directory, "qt", "qml");
if (!Directory.Exists(qmlDirectory))
{
throw new Exception($"QML directory didn't exist: {qmlDirectory}");
}
Environment.SetEnvironmentVariable("QML2_IMPORT_PATH", qmlDirectory);
var binDirectory = Path.Combine(directory, "qt", "bin");
if (!Directory.Exists(binDirectory))
{
throw new Exception($"The bin directory didn't exist: {binDirectory}");
}
Environment.SetEnvironmentVariable("PATH", $"{binDirectory};{Environment.GetEnvironmentVariable("PATH")}");
var preloadPath = Path.Combine(binDirectory, "preload.txt");
if (!File.Exists(preloadPath))
{
throw new Exception($"The preload.txt file didn't exist: {preloadPath}");
}
}
ConfigureRuntimeDirectory(destinationDirectory);
}
}
}

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Qml.Net.Runtimes
{
internal class Symlink
internal static class Symlink
{
[DllImport("libc")]
private static extern int symlink(string path1, string path2);

View file

@ -10,7 +10,7 @@ using Newtonsoft.Json.Schema;
namespace Qml.Net.Runtimes
{
internal class Tar
internal static class Tar
{
public enum EntryType : byte
{
@ -31,7 +31,7 @@ namespace Qml.Net.Runtimes
// ReSharper restore UnusedMember.Global
}
public static Header ReadHeader(Stream stream)
private static Header ReadHeader(Stream stream)
{
var buffer = new byte[512];
var bytesRead = stream.Read(buffer, 0, buffer.Length);
@ -70,7 +70,7 @@ namespace Qml.Net.Runtimes
return header;
}
public class Header
private class Header
{
public EntryType EntryType { get; set; }