diff --git a/src/NuGetForUnity.Packager/Assets/NuGet.meta b/src/NuGetForUnity.Packager/Assets/NuGet.meta index 27037b55..6f1b622e 100644 --- a/src/NuGetForUnity.Packager/Assets/NuGet.meta +++ b/src/NuGetForUnity.Packager/Assets/NuGet.meta @@ -1,5 +1,9 @@ fileFormatVersion: 2 +<<<<<<<< HEAD:src/NuGetForUnity.Packager/Assets/NuGet.meta guid: dbf134857daf7df428aa31cdd055514f +======== +guid: c1f8f59114e0f0994ade28a7e9e164f0 +>>>>>>>> 650e551 (feat: Add support for native runtimes #419):src/NuGetForUnity.Tests/Assets/PlayTests.meta folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/src/NuGetForUnity.Tests/Assets/PlayTests.meta b/src/NuGetForUnity.Tests/Assets/PlayTests.meta new file mode 100644 index 00000000..27037b55 --- /dev/null +++ b/src/NuGetForUnity.Tests/Assets/PlayTests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dbf134857daf7df428aa31cdd055514f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/NuGetForUnity.Tests/Assets/PlayTests/NuGetPlayTests.cs b/src/NuGetForUnity.Tests/Assets/PlayTests/NuGetPlayTests.cs new file mode 100644 index 00000000..754ced7d --- /dev/null +++ b/src/NuGetForUnity.Tests/Assets/PlayTests/NuGetPlayTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections; +using System.Runtime.InteropServices; +using NugetForUnity; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +[assembly: PrebuildSetup(typeof(NuGetPlayTests))] +[assembly: PostBuildCleanup(typeof(NuGetPlayTests))] + +/// +/// Play mode tests allow us to install NuGet packages with Native code before play mode starts, then when play mode +/// runs the native libraries are available for use. +/// +/// There seems to be some Unity internals that prevents an edit mode test from adding a Native library and finding it +/// in the same run. +/// +public class NuGetPlayTests : IPrebuildSetup, IPostBuildCleanup +{ + readonly NugetPackageIdentifier sqlite = new NugetPackageIdentifier("SQLitePCLRaw.lib.e_sqlite3", "2.0.7"); + + /// + /// This is the version number of sqlite with periods replaced with zeros. + /// + /// Note: Version of the SQLite library does not match the NuGet package version + /// + private readonly int _expectedVersion = 3035005; + + [UnityTest] + public IEnumerator InstallAndRunSqlite() + { + yield return new WaitForFixedUpdate(); + + // Test the actual library by calling a "extern" method + var version = sqlite3_libversion_number(); + Assert.That(version, Is.EqualTo(_expectedVersion)); + } + + /// + /// Call to the SQLite native file, this actually tests loading and access the library when called. + /// + /// On windows the call to sqlite3_libversion returns a garbled string so use the integer + /// + /// + [DllImport("e_sqlite3")] + private static extern int sqlite3_libversion_number(); + + public void Setup() + { + try + { + sqlite3_libversion_number(); + Assert.Fail("e_sqlite3 dll loaded, but should not be available"); + } + catch (DllNotFoundException) + { + } + + // For windows we end up importing two identical DLLs temporarily, this causes an error log that NUnit detects + // and would fail the test if we don't tell it to ignore the Failing messages + NugetHelper.LoadNugetConfigFile(); + NugetHelper.LoadSettingFile(); + LogAssert.ignoreFailingMessages = true; + NugetHelper.InstallIdentifier(sqlite); + Assert.IsTrue(NugetHelper.IsInstalled(sqlite), "The package was NOT installed: {0} {1}", sqlite.Id, + sqlite.Version); + } + + public void Cleanup() + { + NugetHelper.Uninstall(sqlite); + Assert.IsFalse(NugetHelper.IsInstalled(sqlite), "The packages are STILL installed: {0} {1}", sqlite.Id, + sqlite.Version); + } +} \ No newline at end of file diff --git a/src/NuGetForUnity.Tests/Assets/PlayTests/NuGetPlayTests.cs.meta b/src/NuGetForUnity.Tests/Assets/PlayTests/NuGetPlayTests.cs.meta new file mode 100644 index 00000000..065de322 --- /dev/null +++ b/src/NuGetForUnity.Tests/Assets/PlayTests/NuGetPlayTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bc1d9162435c540d8802e86434f9aac5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/NuGetForUnity.Tests/Assets/PlayTests/PlayTest.asmdef b/src/NuGetForUnity.Tests/Assets/PlayTests/PlayTest.asmdef new file mode 100644 index 00000000..352337c6 --- /dev/null +++ b/src/NuGetForUnity.Tests/Assets/PlayTests/PlayTest.asmdef @@ -0,0 +1,22 @@ +{ + "name": "PlayTest", + "rootNamespace": "", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "NuGetForUnity" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/src/NuGetForUnity.Tests/Assets/PlayTests/PlayTest.asmdef.meta b/src/NuGetForUnity.Tests/Assets/PlayTests/PlayTest.asmdef.meta new file mode 100644 index 00000000..f590490d --- /dev/null +++ b/src/NuGetForUnity.Tests/Assets/PlayTests/PlayTest.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 60bcaf86670c11973aea764b696160ac +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs index 177d4d10..668c7912 100644 --- a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs +++ b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs @@ -996,6 +996,25 @@ public void TestSourceCodePackageInstall(string packageId, string packageVersion Assert.IsFalse(InstalledPackagesManager.IsInstalled(package, false), "The package is STILL installed: {0} {1}", package.Id, package.Version); } + [Test] + [TestCase("win7-x64", BuildTarget.StandaloneWindows64)] + [TestCase("win7-x86", BuildTarget.StandaloneWindows)] + [TestCase("win-x64", BuildTarget.StandaloneWindows64)] + [TestCase("win-x86", BuildTarget.StandaloneWindows)] + [TestCase("linux-x64", BuildTarget.StandaloneLinux64)] + [TestCase("osx-x64", BuildTarget.StandaloneOSX)] + public void NativeSettingsTest(string key, BuildTarget buildTarget) + { + Settings.CreateDefault(NugetHelper.SettingsFilePath); + // Call load settings directly to ensure we're testing the deserialised file + var settings = Settings.Load(NugetHelper.SettingsFilePath); + var nativeRuntimes = settings.NativeRuntimesMappings; + + Assert.IsTrue(nativeRuntimes.ContainsKey(key), $"Native mappings is missing {key}"); + Assert.IsTrue(nativeRuntimes[key].Contains(buildTarget), + $"Native mapping for {key} is missing build target {buildTarget}"); + } + private static void ConfigureNugetConfig(InstallMode installMode) { var nugetConfigFile = ConfigurationManager.NugetConfigFile; diff --git a/src/NuGetForUnity/Editor/Helper/FileSystemHelper.cs b/src/NuGetForUnity/Editor/Helper/FileSystemHelper.cs index 75335272..801b77bd 100644 --- a/src/NuGetForUnity/Editor/Helper/FileSystemHelper.cs +++ b/src/NuGetForUnity/Editor/Helper/FileSystemHelper.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; using JetBrains.Annotations; using UnityEngine; @@ -104,7 +105,14 @@ internal static void DeleteDirectory([NotNull] string directoryPath, bool log) directoryInfo.Attributes = FileAttributes.Normal; // delete the directory - directoryInfo.Delete(true); + try + { + directoryInfo.Delete(true); + } + catch (UnauthorizedAccessException e) when (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && e.Message.Contains(".dll")) + { + Debug.LogError("Windows was unable to delete all the files, if you are using Native files this is because the file is in use by Unity. Please restart Unity to remove all the files"); + } } /// @@ -119,7 +127,14 @@ internal static void DeleteFile([NotNull] string filePath) } File.SetAttributes(filePath, FileAttributes.Normal); - File.Delete(filePath); + try + { + File.Delete(filePath); + } + catch (UnauthorizedAccessException e) when (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && e.Message.Contains(".dll")) + { + Debug.LogError("Windows was unable to delete all the files, if you are using Native files this is because the file is in use by Unity. Please restart Unity to remove all the files"); + } } /// diff --git a/src/NuGetForUnity/Editor/Ui/MetaFileHandler.cs b/src/NuGetForUnity/Editor/Ui/MetaFileHandler.cs new file mode 100644 index 00000000..a92a4954 --- /dev/null +++ b/src/NuGetForUnity/Editor/Ui/MetaFileHandler.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace NugetForUnity +{ + public class MetaFileHandler : AssetPostprocessor + { + private const string RuntimesFolderName = "runtimes"; + private const string NativeFolderName = "native"; + private const string AssetsFolderName = "Assets"; + + /// + /// Used to mark an asset as already processed by this class. + /// + private const string ProcessedLabel = "NuGetForUnity"; + + private static readonly List NotObsoleteBuildTargets = typeof(BuildTarget) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(fieldInfo => fieldInfo.GetCustomAttribute(typeof(ObsoleteAttribute)) == null) + .Select(fieldInfo => (BuildTarget)fieldInfo.GetValue(null)) + .ToList(); + + private enum ProcessState + { + Success, + Failure, + AlreadyProcessed + } + + private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths) + { + if (importedAssets.Length == 0) return; + + if (NugetHelper.NugetConfigFile == null) + { + NugetHelper.LoadNugetConfigFile(); + } + + if (NugetHelper.SettingsFile == null) + { + NugetHelper.LoadSettingFile(); + } + + var pathPrefix = NugetHelper.NugetConfigFile.RepositoryPath.Substring( + NugetHelper.NugetConfigFile.RepositoryPath.IndexOf(AssetsFolderName, StringComparison.Ordinal)); + var nugetForUnityAssets = importedAssets.Where(path => path.StartsWith(pathPrefix)).ToList(); + + if (nugetForUnityAssets.Count == 0) return; + + var runtimeAssetsFiles = nugetForUnityAssets + .Where(path => path.Contains(RuntimesFolderName)) + .Where(path => path.Contains(NativeFolderName)) + .Where(File.Exists) + .ToList(); + var runtimeResults = runtimeAssetsFiles.Select(path => (path, HandleRuntime(path))).ToList(); + + LogResults("Handle Runtimes", runtimeResults); + } + + private static void LogResults(string type, IList<(string, ProcessState)> results) + { + var successes = results.Where(r => ProcessState.Success.Equals(r.Item2)).Select(r => r.Item1).ToList(); + var failures = results.Where(r => ProcessState.Failure.Equals(r.Item2)).Select(r => r.Item1).ToList(); + + if (successes.Count > 0 && NugetHelper.NugetConfigFile.Verbose) + { + NugetHelper.LogVerbose("NuGetForUnity: {0} successfully configured: {1}", type, + string.Join(",", successes)); + } + + if (failures.Count > 0) + { + Debug.LogError($"NuGetForUnity: {type} failed to configure {string.Join(",", failures)}"); + } + } + + /// + /// Given the load the for the asset it's associated + /// with + /// + /// Path to the assets + /// The loaded PluginImporter + /// True if the plugin importer for the asset could be loaded + private static bool GetPluginImporter(string assetFilePath, out PluginImporter plugin) + { + var assetPath = assetFilePath; + plugin = AssetImporter.GetAtPath(assetPath) as PluginImporter; + + if (plugin == null) + { + Debug.LogWarning($"Failed to import plugin at {assetPath}"); + return false; + } + + NugetHelper.LogVerbose("Plugin loaded for file: {0}", assetPath); + + return true; + } + + /// + /// Check the labels on the asset to determine if we've already processed it. Calls to + /// trigger call backs to this class to occur so not checking + /// if an asset has already been processed triggers an infinite loop that Unity detects and logs as an error + /// + /// Asset to check + /// + private static bool AlreadyProcessed(UnityEngine.Object asset) + { + return AssetDatabase.GetLabels(asset).ToList().Contains(ProcessedLabel); + } + + /// + /// Extract platform information from the assetFilePath + /// + /// Path to an asset + /// List of compatible Unity build targets + /// Platform i.e. Linux/OSX/Windows + /// Architecture i.e. x86_64 + /// True if the platform & architecture are supported + private static bool GetPlatform(string assetFilePath, out List compatibleTargets, + out string platform, out string architecture) + { + var platformFolder = + assetFilePath.Substring(0, assetFilePath.IndexOf(NativeFolderName, StringComparison.Ordinal)); + var platformString = new DirectoryInfo(platformFolder).Name; + + var platformAndArch = platformString.Split('-'); + + platform = platformString; + architecture = platformAndArch[1]; + return NugetHelper.SettingsFile.NativeRuntimesMappings.TryGetValue(platformString, out compatibleTargets); + } + + /// + /// Import and set compatability on a native file. + /// + /// Unsupported platforms/architectures parent folder is deleted + /// Set all excludes for all non-deprecated BuildTargets + /// Set all compatability for all non-deprecated BuildTargets + /// + /// + /// Path to an asset + /// True if it was able to update the asset + private static ProcessState HandleRuntime(string assetFilePath) + { + if (!GetPluginImporter(assetFilePath, out var plugin)) return ProcessState.Failure; + if (AlreadyProcessed(plugin)) return ProcessState.AlreadyProcessed; + + if (!GetPlatform(assetFilePath, out var compatibleTargets, out var platform, out var arch)) + { + var platformFolder = + assetFilePath.Substring(0, assetFilePath.IndexOf(NativeFolderName, StringComparison.Ordinal)); + NugetHelper.LogVerbose("Runtime {0} is not supported", platformFolder); + Directory.Delete(platformFolder, true); + return ProcessState.Failure; + } + + var incompatibleTargets = NotObsoleteBuildTargets.Except(compatibleTargets).ToList(); + + NugetHelper.LogVerbose( + "Runtime {0} of asset {1} setting compatability to {2}, incompatibility to {3}", + platform, + assetFilePath, + string.Join(",", compatibleTargets), + string.Join(",", incompatibleTargets)); + + incompatibleTargets.ForEach(target => plugin.SetExcludeFromAnyPlatform(target, true)); + compatibleTargets.ForEach(target => plugin.SetExcludeFromAnyPlatform(target, false)); + + incompatibleTargets.ForEach(target => plugin.SetCompatibleWithPlatform(target, false)); + compatibleTargets.ForEach(target => plugin.SetCompatibleWithPlatform(target, true)); + + plugin.SetCompatibleWithEditor(true); + // Set the editor architecture to prevent loading both 32 & 64 windows DLLs + switch (arch) + { + case "x64": + plugin.SetEditorData("CPU", "x86_64"); + break; + case "x86": + plugin.SetEditorData("CPU", "x86"); + break; + default: + Debug.LogError($"Unsupported architecture {arch} for {assetFilePath}"); + return ProcessState.Failure; + } + + AssetDatabase.SetLabels(plugin, new[] { ProcessedLabel }); + + // Persist and reload the change to the meta file + plugin.SaveAndReimport(); + NugetHelper.LogVerbose("Runtime {0} of asset {1} compatability set", platform, assetFilePath); + return ProcessState.Success; + } + } +} \ No newline at end of file diff --git a/src/NuGetForUnity/Editor/Ui/MetaFileHandler.cs.meta b/src/NuGetForUnity/Editor/Ui/MetaFileHandler.cs.meta new file mode 100644 index 00000000..392ede03 --- /dev/null +++ b/src/NuGetForUnity/Editor/Ui/MetaFileHandler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ddc3963d9ff54764ae2b2e56e0ef788d +timeCreated: 1648039013 \ No newline at end of file diff --git a/src/NuGetForUnity/Editor/Ui/Settings.cs b/src/NuGetForUnity/Editor/Ui/Settings.cs new file mode 100644 index 00000000..8dd6fdae --- /dev/null +++ b/src/NuGetForUnity/Editor/Ui/Settings.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.IO; +using System.Xml.Linq; +using UnityEditor; +using UnityEngine; + +namespace NugetForUnity +{ + public class Settings + { + /// + /// Gets or sets the mapping from NuGet runtimes folder naming convention to Unity BuildTargets + /// + public Dictionary> NativeRuntimesMappings { get; set; } + + public void Save(string filepath) + { + XDocument configFile = new XDocument(); + + XElement nativeRuntimesMapping = new XElement("nativeRuntimesMapping"); + foreach (var mapping in NativeRuntimesMappings) + { + var platform = new XElement("platform"); + platform.Add(new XAttribute("name", mapping.Key)); + + foreach (var target in mapping.Value) + { + var buildTarget = new XElement("buildTarget"); + buildTarget.Add(new XAttribute("name", target.ToString())); + platform.Add(buildTarget); + } + + nativeRuntimesMapping.Add(platform); + } + + XElement settings = new XElement("settings"); + settings.Add(nativeRuntimesMapping); + configFile.Add(settings); + + bool fileExists = File.Exists(filepath); + // remove the read only flag on the file, if there is one. + if (fileExists) + { + FileAttributes attributes = File.GetAttributes(filepath); + if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + attributes &= ~FileAttributes.ReadOnly; + File.SetAttributes(filepath, attributes); + } + } + + configFile.Save(filepath); + } + + public static Settings Load(string filepath) + { + Settings settingsFile = DefaultSettings(); + XDocument file = XDocument.Load(filepath); + + XElement nativeRuntimesMapping = file.Root.Element("nativeRuntimesMapping"); + if (nativeRuntimesMapping != null) + { + settingsFile.NativeRuntimesMappings = new Dictionary>(); + var platforms = nativeRuntimesMapping.Elements("platform"); + foreach (var platform in platforms) + { + var platformName = platform.Attribute("name").Value; + var buildTargets = new List(); + var targets = platform.Elements("buildTarget"); + + foreach (var target in targets) + { + var targetName = target.Attribute("name").Value; + BuildTarget parsedTarget; + if (BuildTarget.TryParse(targetName, true, out parsedTarget)) + { + buildTargets.Add(parsedTarget); + } + else + { + Debug.LogWarning(string.Format("{0} of {1} not found", targetName, platformName)); + } + } + + settingsFile.NativeRuntimesMappings.Add(platformName, buildTargets); + } + } + + return settingsFile; + } + + public static Settings CreateDefault(string filepath) + { + var settings = DefaultSettings(); + settings.Save(filepath); + + return settings; + } + + private static Settings DefaultSettings() + { + var settings = new Settings(); + + var nativeRuntimes = new Dictionary> + { + { "win7-x64", new List() { BuildTarget.StandaloneWindows64 } }, + { "win7-x86", new List() { BuildTarget.StandaloneWindows } }, + { "win-x64", new List() { BuildTarget.StandaloneWindows64 } }, + { "win-x86", new List() { BuildTarget.StandaloneWindows } }, + { "linux-x64", new List() { BuildTarget.StandaloneLinux64 } }, + { "osx-x64", new List() { BuildTarget.StandaloneOSX } } + }; + + settings.NativeRuntimesMappings = nativeRuntimes; + + return settings; + } + } +} \ No newline at end of file diff --git a/src/NuGetForUnity/Editor/Ui/Settings.cs.meta b/src/NuGetForUnity/Editor/Ui/Settings.cs.meta new file mode 100644 index 00000000..ab112e01 --- /dev/null +++ b/src/NuGetForUnity/Editor/Ui/Settings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9b843fff1874478c890dcea11fbda40d +timeCreated: 1638795981 \ No newline at end of file diff --git a/src/NuGetForUnity/NuGetForUnity.asmdef.meta b/src/NuGetForUnity/NuGetForUnity.asmdef.meta new file mode 100644 index 00000000..677f53a5 --- /dev/null +++ b/src/NuGetForUnity/NuGetForUnity.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1e78d62627da293fb8dd09b1ccabcc62 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: