add r2r for launcher and game assemblies
Some checks failed
Build / Compute Version (push) Successful in 7s
Build / Build Nuget package (SharedCringe) (push) Successful in 3m59s
Build / Build Nuget package (CringeBootstrap.Abstractions) (push) Successful in 4m12s
Build / Build Nuget package (NuGet) (push) Successful in 4m8s
Build / Build Nuget package (CringePlugins) (push) Successful in 4m33s
Build / Build Launcher (push) Failing after 4m38s
Some checks failed
Build / Compute Version (push) Successful in 7s
Build / Build Nuget package (SharedCringe) (push) Successful in 3m59s
Build / Build Nuget package (CringeBootstrap.Abstractions) (push) Successful in 4m12s
Build / Build Nuget package (NuGet) (push) Successful in 4m8s
Build / Build Nuget package (CringePlugins) (push) Successful in 4m33s
Build / Build Launcher (push) Failing after 4m38s
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<TieredPGO>true</TieredPGO>
|
<TieredPGO>true</TieredPGO>
|
||||||
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CringeBootstrap.Abstractions\CringeBootstrap.Abstractions.csproj" />
|
<ProjectReference Include="..\CringeBootstrap.Abstractions\CringeBootstrap.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\CringeLauncher\CringeLauncher.csproj" ExcludeAssets="compile" />
|
<ProjectReference Include="..\CringeLauncher\CringeLauncher.csproj" ExcludeAssets="compile" />
|
||||||
|
<ProjectReference Include="..\CringeLauncher\CringeLauncher.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
248
CringeBootstrap/CrossGen/CrossGenService.cs
Normal file
248
CringeBootstrap/CrossGen/CrossGenService.cs
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using NuGet;
|
||||||
|
using NuGet.Deps;
|
||||||
|
using NuGet.Versioning;
|
||||||
|
|
||||||
|
namespace CringeBootstrap.CrossGen;
|
||||||
|
|
||||||
|
internal class CrossGenService(string gameDirectoryPath, string cachePath)
|
||||||
|
{
|
||||||
|
private readonly ImmutableHashSet<string> _excludedAssemblies = ImmutableHashSet.Create(
|
||||||
|
StringComparer.OrdinalIgnoreCase,
|
||||||
|
"VRage.NativeAftermath.dll" // managed C++ is not supported
|
||||||
|
);
|
||||||
|
|
||||||
|
// assembly with game version constant so hash always changes with game updates
|
||||||
|
private const string CacheKeyFileName = "SpaceEngineers.Game.dll";
|
||||||
|
|
||||||
|
private readonly string _crossGenCachePath = Directory.CreateDirectory(Path.Join(cachePath, "R2R")).FullName;
|
||||||
|
|
||||||
|
public void CleanCache()
|
||||||
|
{
|
||||||
|
foreach (var directory in Directory.EnumerateDirectories(_crossGenCachePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, true);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||||
|
Console.WriteLine("Failed to clean previous crossgen cache");
|
||||||
|
Console.ResetColor();
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run crossgen and return path to game assemblies directory
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The path to game assemblies directory either original or R2R</returns>
|
||||||
|
public string RunCrossGen()
|
||||||
|
{
|
||||||
|
var cacheDirectory = Path.Join(_crossGenCachePath, GetCacheKey());
|
||||||
|
if (Directory.Exists(cacheDirectory))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Crossgen cache hit");
|
||||||
|
return cacheDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Starting coldstart crossgen");
|
||||||
|
|
||||||
|
CleanCache();
|
||||||
|
|
||||||
|
var crossGenPath = DownloadCrossGenAsync().GetAwaiter().GetResult();
|
||||||
|
if (crossGenPath is null)
|
||||||
|
return gameDirectoryPath;
|
||||||
|
|
||||||
|
var inputAssemblies = CollectInputAssemblies();
|
||||||
|
ImmutableHashSet<string> references = [..CollectFrameworkReferencesAsync().GetAwaiter().GetResult(), ..inputAssemblies];
|
||||||
|
references = references.WithComparer(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(cacheDirectory);
|
||||||
|
|
||||||
|
for (var index = 0; index < inputAssemblies.Length; index++)
|
||||||
|
{
|
||||||
|
var inputAssembly = inputAssemblies[index];
|
||||||
|
var inputReferences = references.Remove(inputAssembly);
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo(crossGenPath, [
|
||||||
|
"--targetos:windows",
|
||||||
|
"--targetarch:x64",
|
||||||
|
"--Ot",
|
||||||
|
..inputReferences.SelectMany(x => new [] { "-r", x }),
|
||||||
|
"--out", Path.Join(cacheDirectory, Path.GetFileName(inputAssembly)),
|
||||||
|
inputAssembly
|
||||||
|
])
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine($"Running crossgen... {index/(inputAssemblies.Length-1.0):P0}");
|
||||||
|
|
||||||
|
using var process = Process.Start(startInfo);
|
||||||
|
|
||||||
|
var outputStringBuilder = new StringBuilder();
|
||||||
|
var errorStringBuilder = new StringBuilder();
|
||||||
|
if (process is not null)
|
||||||
|
{
|
||||||
|
process.OutputDataReceived += (_, args) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(args.Data))
|
||||||
|
outputStringBuilder.AppendLine(args.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.ErrorDataReceived += (_, args) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(args.Data))
|
||||||
|
errorStringBuilder.AppendLine(args.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
process.WaitForExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process is not null && process.ExitCode == 0) continue;
|
||||||
|
|
||||||
|
string? logFilePath = null;
|
||||||
|
if (process is not null)
|
||||||
|
{
|
||||||
|
logFilePath = Path.Join(cachePath, $"{Path.GetFileName(inputAssembly)}.log");
|
||||||
|
File.WriteAllText(logFilePath, outputStringBuilder.ToString());
|
||||||
|
File.AppendAllText(logFilePath, errorStringBuilder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
LogCrossGenException($"Crossgen failed! {(logFilePath is not null ? $"Log saved to: {logFilePath}" : string.Empty)} Skipping crossgen", new Exception($"Crossgen failed for {inputAssembly}"));
|
||||||
|
CleanCache();
|
||||||
|
return gameDirectoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
|
Console.WriteLine("Crossgen finished");
|
||||||
|
Console.ResetColor();
|
||||||
|
return cacheDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ImmutableArray<string>> CollectFrameworkReferencesAsync()
|
||||||
|
{
|
||||||
|
var dotnetPacksPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet", "shared");
|
||||||
|
if (!Directory.Exists(dotnetPacksPath))
|
||||||
|
throw new Exception($"Dotnet shared packs not found in {dotnetPacksPath}");
|
||||||
|
|
||||||
|
const string runtimePackId = "Microsoft.NETCore.App";
|
||||||
|
const string desktopPackId = "Microsoft.WindowsDesktop.App";
|
||||||
|
|
||||||
|
return
|
||||||
|
[
|
||||||
|
..await CollectFrameworkReferencesFromPackAsync(dotnetPacksPath, runtimePackId),
|
||||||
|
..await CollectFrameworkReferencesFromPackAsync(dotnetPacksPath, desktopPackId)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask<ImmutableArray<string>> CollectFrameworkReferencesFromPackAsync(string packsPath, string packId)
|
||||||
|
{
|
||||||
|
var packDirPath = Path.Join(packsPath, packId, Environment.Version.ToString());
|
||||||
|
var packPath = Path.Join(packDirPath, $"{packId}.deps.json");
|
||||||
|
if (!File.Exists(packPath))
|
||||||
|
throw new Exception($"Dotnet shared pack {packId} not found in {packPath}");
|
||||||
|
|
||||||
|
await using var stream = File.OpenRead(packPath);
|
||||||
|
var ((runtimeFramework, _), _, targets, _) = await DependencyManifestSerializer.DeserializeAsync(stream);
|
||||||
|
var (_, runtime, _) = targets[runtimeFramework].Values.First();
|
||||||
|
|
||||||
|
return [..runtime!.Keys.Select(b => Path.Join(packDirPath, b))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImmutableArray<string> CollectInputAssemblies()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
..Directory.EnumerateFiles(gameDirectoryPath, "*.dll")
|
||||||
|
.Where(IsManagedAssembly)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsManagedAssembly(string path)
|
||||||
|
{
|
||||||
|
if (_excludedAssemblies.Contains(Path.GetFileName(path)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AssemblyName.GetAssemblyName(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> DownloadCrossGenAsync()
|
||||||
|
{
|
||||||
|
const string nugetUrl = "https://api.nuget.org/v3/index.json";
|
||||||
|
const string toolName = "crossgen2.exe";
|
||||||
|
const string packageId = "Microsoft.NETCore.App.Crossgen2.win-x64";
|
||||||
|
var nugetCachePath = Path.Join(cachePath, "x64", $"net{Environment.Version.Major}.{Environment.Version.Minor}");
|
||||||
|
|
||||||
|
var packagePath = Directory.CreateDirectory(Path.Join(nugetCachePath, packageId, Environment.Version.ToString()));
|
||||||
|
var toolPath = Path.Join(packagePath.FullName, "tools", toolName);
|
||||||
|
if (File.Exists(toolPath))
|
||||||
|
return toolPath;
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = await NuGetClient.CreateFromIndexUrlAsync(nugetUrl, httpClient);
|
||||||
|
|
||||||
|
if (!packagePath.Exists) packagePath.Create();
|
||||||
|
|
||||||
|
await using var stream =
|
||||||
|
await client!.GetPackageContentStreamAsync(packageId, new NuGetVersion(Environment.Version));
|
||||||
|
await using var memStream = new MemoryStream();
|
||||||
|
await stream.CopyToAsync(memStream);
|
||||||
|
memStream.Position = 0;
|
||||||
|
using var archive = new ZipArchive(memStream, ZipArchiveMode.Read);
|
||||||
|
archive.ExtractToDirectory(packagePath.FullName);
|
||||||
|
|
||||||
|
if (!File.Exists(toolPath))
|
||||||
|
{
|
||||||
|
LogCrossGenException("Failed to find crossgen", new FileNotFoundException("Failed to find crossgen", toolPath));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
LogCrossGenException("Failed to extract crossgen", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogCrossGenException("Failed to download crossgen", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogCrossGenException(string message, Exception e)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.WriteLine(message);
|
||||||
|
Console.ResetColor();
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCacheKey()
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(Path.Join(gameDirectoryPath, CacheKeyFileName));
|
||||||
|
return Convert.ToHexStringLower(SHA256.HashData(stream));
|
||||||
|
}
|
||||||
|
}
|
@@ -9,14 +9,17 @@ namespace CringeBootstrap;
|
|||||||
public class GameDirectoryAssemblyLoadContext : AssemblyLoadContext, ICoreLoadContext
|
public class GameDirectoryAssemblyLoadContext : AssemblyLoadContext, ICoreLoadContext
|
||||||
{
|
{
|
||||||
private readonly string _dir;
|
private readonly string _dir;
|
||||||
|
private readonly string _unmanagedAssembliesDir;
|
||||||
|
|
||||||
private static readonly ImmutableHashSet<string> ReferenceAssemblies = ["netstandard"];
|
private static readonly ImmutableHashSet<string> ReferenceAssemblies = ["netstandard"];
|
||||||
// Assembly simple names are case-insensitive per the runtime behavior
|
// Assembly simple names are case-insensitive per the runtime behavior
|
||||||
// (see SimpleNameToFileNameMapTraits for the TPA lookup hash).
|
// (see SimpleNameToFileNameMapTraits for the TPA lookup hash).
|
||||||
private readonly Dictionary<string, string> _assemblyNames = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, string> _assemblyNames = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public GameDirectoryAssemblyLoadContext(string dir) : base("CringeBootstrap")
|
public GameDirectoryAssemblyLoadContext(string dir, string unmanagedAssembliesDir) : base("CringeBootstrap")
|
||||||
{
|
{
|
||||||
_dir = dir;
|
_dir = dir;
|
||||||
|
_unmanagedAssembliesDir = unmanagedAssembliesDir;
|
||||||
var files = Directory.GetFiles(dir, "*.dll");
|
var files = Directory.GetFiles(dir, "*.dll");
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
@@ -73,7 +76,8 @@ public class GameDirectoryAssemblyLoadContext : AssemblyLoadContext, ICoreLoadCo
|
|||||||
return base.LoadUnmanagedDll(unmanagedDllName);
|
return base.LoadUnmanagedDll(unmanagedDllName);
|
||||||
|
|
||||||
// prefer System32 over ours
|
// prefer System32 over ours
|
||||||
ReadOnlySpan<string> dirs = [Environment.SystemDirectory, _dir];
|
// avoid using _dir because it may be a crossgen directory without unmanaged assemblies
|
||||||
|
ReadOnlySpan<string> dirs = [Environment.SystemDirectory, _unmanagedAssembliesDir];
|
||||||
foreach (var dir in dirs)
|
foreach (var dir in dirs)
|
||||||
{
|
{
|
||||||
var path = Path.Join(dir, unmanagedDllName);
|
var path = Path.Join(dir, unmanagedDllName);
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
|
||||||
using System.Reflection.Metadata;
|
using System.Reflection.Metadata;
|
||||||
using System.Runtime.Loader;
|
using System.Runtime.Loader;
|
||||||
using CringeBootstrap;
|
using CringeBootstrap;
|
||||||
using CringeBootstrap.Abstractions;
|
using CringeBootstrap.Abstractions;
|
||||||
|
using CringeBootstrap.CrossGen;
|
||||||
using Velopack;
|
using Velopack;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -41,15 +41,45 @@ AssemblyLoadContext.Default.Resolving += (loadContext, name) =>
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
var dir = Path.GetDirectoryName(args[0])!;
|
var dir = Path.GetDirectoryName(args[0])!;
|
||||||
var context = new GameDirectoryAssemblyLoadContext(dir);
|
var gameDir = dir;
|
||||||
|
|
||||||
|
var customEntrypoint = Environment.GetEnvironmentVariable("DOTNET_BOOTSTRAP_ENTRYPOINT");
|
||||||
|
|
||||||
|
if (
|
||||||
|
#if !DEBUG // disable crossgen for plugins userdev, but leave for debug
|
||||||
|
customEntrypoint is null &&
|
||||||
|
#endif
|
||||||
|
!args.Contains("--skip-crossgen", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var cacheDir = Directory.CreateDirectory(Path.Join(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"CringeLauncher", "cache"));
|
||||||
|
|
||||||
|
var crossGenService = new CrossGenService(gameDir, cacheDir.FullName);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dir = crossGenService.RunCrossGen();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.WriteLine("Crossgen encountered a fatal error and will be skipped for this session.");
|
||||||
|
Console.ResetColor();
|
||||||
|
Console.WriteLine(e);
|
||||||
|
|
||||||
|
crossGenService.CleanCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new GameDirectoryAssemblyLoadContext(dir, gameDir);
|
||||||
|
|
||||||
// a list of assemblies which are not in the game binaries but reference them
|
// a list of assemblies which are not in the game binaries but reference them
|
||||||
context.AddDependencyOverride("CringeLauncher");
|
context.AddDependencyOverride("CringeLauncher");
|
||||||
context.AddDependencyOverride("CringePlugins");
|
context.AddDependencyOverride("CringePlugins");
|
||||||
context.AddDependencyOverride("EOSSDK");
|
context.AddDependencyOverride("EOSSDK");
|
||||||
|
|
||||||
var entrypoint = Environment.GetEnvironmentVariable("DOTNET_BOOTSTRAP_ENTRYPOINT") ??
|
var entrypoint = customEntrypoint ?? "CringeLauncher.Launcher, CringeLauncher";
|
||||||
"CringeLauncher.Launcher, CringeLauncher";
|
|
||||||
if (!TypeName.TryParse(entrypoint, out var entrypointName) ||
|
if (!TypeName.TryParse(entrypoint, out var entrypointName) ||
|
||||||
entrypointName.AssemblyName is null)
|
entrypointName.AssemblyName is null)
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user