147 lines
6.8 KiB
C#
147 lines
6.8 KiB
C#
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>();
|
|
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/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 DLL, 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 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");
|
|
}
|
|
}
|