using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.Cryptography; namespace EMPlannerVerificationHost; internal static class PluginPackagingChecks { public static void Run() { string repositoryRoot = Directory.GetCurrentDirectory(); string scriptPath = Path.Combine(repositoryRoot, "ClumsyPilot", "scripts", "Publish-ClumsyPilotPlugin.ps1"); Verification.True(File.Exists(scriptPath), "plugin publish script exists"); string managedDll = Path.Combine(AppContext.BaseDirectory, "ClumsyPilot.dll"); Verification.True(File.Exists(managedDll), "verification host has managed ClumsyPilot.dll"); VerifyBuildOutputMetadata(); string packageRoot = Path.Combine(Path.GetTempPath(), "em-planner-plugin-package-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(packageRoot); try { Publish(scriptPath, managedDll, packageRoot); VerifyPackage(repositoryRoot, packageRoot); Publish(scriptPath, managedDll, packageRoot); VerifyPackage(repositoryRoot, packageRoot); AssertNoStaleSiblingDirectories(packageRoot); } finally { if (Directory.Exists(packageRoot)) Directory.Delete(packageRoot, true); } } private static void Publish(string scriptPath, string managedDll, string packageRoot) { var startInfo = new ProcessStartInfo { FileName = "powershell.exe", WorkingDirectory = Directory.GetCurrentDirectory(), UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, }; startInfo.ArgumentList.Add("-NoProfile"); startInfo.ArgumentList.Add("-ExecutionPolicy"); startInfo.ArgumentList.Add("Bypass"); startInfo.ArgumentList.Add("-File"); startInfo.ArgumentList.Add(scriptPath); startInfo.ArgumentList.Add("-ManagedDll"); startInfo.ArgumentList.Add(managedDll); startInfo.ArgumentList.Add("-OutputDirectory"); startInfo.ArgumentList.Add(packageRoot); using (var process = new Process { StartInfo = startInfo }) { process.Start(); string standardOutput = process.StandardOutput.ReadToEnd(); string standardError = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) { throw new InvalidOperationException("plugin publish exited " + process.ExitCode + ": " + standardOutput + standardError); } } } private static void VerifyPackage(string repositoryRoot, string packageRoot) { string pluginsDirectory = Path.Combine(packageRoot, "plugins"); Verification.True(Directory.Exists(pluginsDirectory), "plugins directory exists"); var relativeFiles = new List(); string[] files = Directory.GetFiles(pluginsDirectory, "*", SearchOption.AllDirectories); for (int index = 0; index < files.Length; index++) relativeFiles.Add(Path.GetRelativePath(packageRoot, files[index]).Replace('\\', '/')); relativeFiles.Sort(StringComparer.Ordinal); Verification.Equal("plugins/ClumsyPilot.dll|plugins/TrajectoryPlanningVisualization.dll|" + "plugins/licenses/OSQP-LICENSE.txt|plugins/licenses/OSQP-NOTICE.txt|plugins/licenses/OSQP-VERSION.txt|" + "plugins/osqp.dll", string.Join("|", relativeFiles), "plugin tree contains exactly the managed DLLs, native DLL, and OSQP license files"); string managedPlugin = Path.Combine(pluginsDirectory, "ClumsyPilot.dll"); AssemblyName managedAssembly = AssemblyName.GetAssemblyName(managedPlugin); Verification.True(!string.IsNullOrWhiteSpace(managedAssembly.Name), "packaged ClumsyPilot.dll is managed"); string visualizationPlugin = Path.Combine(pluginsDirectory, "TrajectoryPlanningVisualization.dll"); AssemblyName visualizationAssembly = AssemblyName.GetAssemblyName(visualizationPlugin); Verification.Equal("TrajectoryPlanningVisualization", visualizationAssembly.Name, "packaged visualization DLL has the expected managed assembly name"); string nativePlugin = Path.Combine(pluginsDirectory, "osqp.dll"); Verification.Equal((ushort)0x8664, ReadPeMachine(nativePlugin), "packaged OSQP binary is x64"); Verification.Equal(ReadExpectedOsqpHash(repositoryRoot), GetSha256(nativePlugin), "packaged OSQP hash matches SHA256SUMS"); } private static void AssertNoStaleSiblingDirectories(string packageRoot) { string parentDirectory = Path.GetDirectoryName(packageRoot) ?? throw new InvalidOperationException("temporary package root has no parent directory"); string prefix = Path.GetFileName(packageRoot); string[] matchingDirectories = Directory.GetDirectories(parentDirectory, prefix + "*"); Verification.Equal(1, matchingDirectories.Length, "publish leaves no stale sibling staging directory"); Verification.Equal(packageRoot, matchingDirectories[0], "publish keeps only the requested temporary test root"); } private static ushort ReadPeMachine(string path) { byte[] bytes = File.ReadAllBytes(path); Verification.True(bytes.Length > 0x40 && bytes[0] == 'M' && bytes[1] == 'Z', "OSQP has DOS header"); int peOffset = BitConverter.ToInt32(bytes, 0x3c); Verification.True(peOffset >= 0 && peOffset + 6 <= bytes.Length && bytes[peOffset] == 'P' && bytes[peOffset + 1] == 'E', "OSQP has PE header"); return BitConverter.ToUInt16(bytes, peOffset + 4); } private static string ReadExpectedOsqpHash(string repositoryRoot) { string manifestPath = Path.Combine(repositoryRoot, "ClumsyPilot", "ThirdParty", "OSQP", "SHA256SUMS"); string[] tokens = File.ReadAllText(manifestPath).Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); Verification.True(tokens.Length >= 2 && string.Equals(tokens[1], "win-x64/osqp.dll", StringComparison.Ordinal), "OSQP hash manifest describes the pinned x64 runtime"); return tokens[0].ToLowerInvariant(); } private static string GetSha256(string path) { return Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant(); } private static void VerifyBuildOutputMetadata() { string outputDirectory = AppContext.BaseDirectory; Verification.True(File.Exists(Path.Combine(outputDirectory, "osqp.dll")), "build output contains the pinned OSQP runtime"); Verification.True(File.Exists(Path.Combine(outputDirectory, "licenses", "OSQP-LICENSE.txt")), "build output contains the OSQP license"); Verification.True(File.Exists(Path.Combine(outputDirectory, "licenses", "OSQP-NOTICE.txt")), "build output contains the OSQP notice"); Verification.True(File.Exists(Path.Combine(outputDirectory, "licenses", "OSQP-VERSION.txt")), "build output contains the OSQP version"); } }