2022-11-28 13:55:05 +08:00
|
|
|
|
/*
|
2022-07-03 22:34:07 +08:00
|
|
|
|
MIT License
|
|
|
|
|
|
Copyright (c) 2019 Laurent Kempé
|
|
|
|
|
|
https://github.com/laurentkempe/DynamicRun/blob/master/LICENSE
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Reflection;
|
2022-10-14 02:31:31 +08:00
|
|
|
|
using System.Runtime.InteropServices;
|
2022-07-03 22:34:07 +08:00
|
|
|
|
using Microsoft.CodeAnalysis;
|
|
|
|
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
|
|
|
|
using Microsoft.CodeAnalysis.Text;
|
2022-08-16 00:30:56 +08:00
|
|
|
|
using SingleFileExtractor.Core;
|
2022-07-03 22:34:07 +08:00
|
|
|
|
|
2022-12-06 15:50:17 +08:00
|
|
|
|
namespace MinecraftClient.Scripting.DynamicRun.Builder
|
2022-07-03 22:34:07 +08:00
|
|
|
|
{
|
2022-10-02 18:31:08 +08:00
|
|
|
|
internal class Compiler
|
2022-07-03 22:34:07 +08:00
|
|
|
|
{
|
2022-10-21 00:29:46 +08:00
|
|
|
|
public CompileResult Compile(string filepath, string fileName, List<string> additionalAssemblies)
|
2022-07-03 22:34:07 +08:00
|
|
|
|
{
|
2022-10-02 18:31:08 +08:00
|
|
|
|
using var peStream = new MemoryStream();
|
2022-10-21 00:29:46 +08:00
|
|
|
|
var result = GenerateCode(filepath, fileName, additionalAssemblies).Emit(peStream);
|
2022-07-03 22:34:07 +08:00
|
|
|
|
|
2022-10-02 18:31:08 +08:00
|
|
|
|
if (!result.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
|
2022-07-03 22:34:07 +08:00
|
|
|
|
|
2022-10-02 18:31:08 +08:00
|
|
|
|
return new CompileResult()
|
|
|
|
|
|
{
|
|
|
|
|
|
Assembly = null,
|
|
|
|
|
|
HasCompiledSucecssfully = false,
|
|
|
|
|
|
Failures = failures.ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2022-12-06 15:50:17 +08:00
|
|
|
|
|
2022-10-02 18:31:08 +08:00
|
|
|
|
peStream.Seek(0, SeekOrigin.Begin);
|
2022-07-03 22:34:07 +08:00
|
|
|
|
|
2022-10-02 18:31:08 +08:00
|
|
|
|
return new CompileResult()
|
|
|
|
|
|
{
|
|
|
|
|
|
Assembly = peStream.ToArray(),
|
|
|
|
|
|
HasCompiledSucecssfully = true,
|
|
|
|
|
|
Failures = null
|
|
|
|
|
|
};
|
2022-07-03 22:34:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-10-21 00:29:46 +08:00
|
|
|
|
private static CSharpCompilation GenerateCode(string sourceCode, string fileName, List<string> additionalAssemblies)
|
2022-07-03 22:34:07 +08:00
|
|
|
|
{
|
|
|
|
|
|
var codeString = SourceText.From(sourceCode);
|
2026-03-24 17:43:14 +00:00
|
|
|
|
var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest);
|
2022-07-03 22:34:07 +08:00
|
|
|
|
|
|
|
|
|
|
var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options);
|
2022-12-06 15:50:17 +08:00
|
|
|
|
|
2022-10-21 00:29:46 +08:00
|
|
|
|
var references = new List<MetadataReference>();
|
2022-07-03 22:34:07 +08:00
|
|
|
|
|
2022-10-21 00:29:46 +08:00
|
|
|
|
// Find if any additional assembly DLL exists in the base directory where the .exe exists.
|
2022-12-06 15:50:17 +08:00
|
|
|
|
foreach (var assembly in additionalAssemblies)
|
2022-10-21 00:29:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
var dllPath = Path.Combine(AppContext.BaseDirectory, assembly);
|
2022-12-06 15:50:17 +08:00
|
|
|
|
if (File.Exists(dllPath))
|
2022-10-21 00:29:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
references.Add(MetadataReference.CreateFromFile(dllPath));
|
|
|
|
|
|
// Store the reference in our Assembly Resolver for future reference.
|
|
|
|
|
|
AssemblyResolver.AddAssembly(Assembly.LoadFile(dllPath).FullName!, dllPath);
|
|
|
|
|
|
}
|
2022-12-06 15:50:17 +08:00
|
|
|
|
else
|
2022-10-21 00:29:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
ConsoleIO.WriteLogLine($"[Script Error] {assembly} is defined in script, but cannot find DLL! Script may not run.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-08-16 00:30:56 +08:00
|
|
|
|
|
2022-10-14 02:31:31 +08:00
|
|
|
|
#pragma warning disable IL3000 // We determine if we are in a self-contained binary by checking specifically if the Assembly file path is null.
|
|
|
|
|
|
|
|
|
|
|
|
var SystemPrivateCoreLib = typeof(object).Assembly.Location; // System.Private.CoreLib
|
|
|
|
|
|
var SystemConsole = typeof(Console).Assembly.Location; // System.Console
|
|
|
|
|
|
var MinecraftClientDll = typeof(Program).Assembly.Location; // The path to MinecraftClient.dll
|
2022-12-06 15:50:17 +08:00
|
|
|
|
|
2022-10-14 02:31:31 +08:00
|
|
|
|
// We're on a self-contained binary, so we need to extract the executable to get the assemblies.
|
2023-03-28 21:06:56 +08:00
|
|
|
|
if (string.IsNullOrEmpty(MinecraftClientDll))
|
2022-08-16 00:30:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
// Create a temporary file to copy the executable to.
|
2023-03-28 21:06:56 +08:00
|
|
|
|
var executablePath = Environment.ProcessPath;
|
2026-03-24 01:25:50 +00:00
|
|
|
|
if (executablePath is null)
|
|
|
|
|
|
throw new InvalidOperationException("Cannot determine the process path for self-contained scripting extraction.");
|
2023-03-28 21:06:56 +08:00
|
|
|
|
var tempPath = Path.Combine(Path.GetTempPath(), "mcc-scripting");
|
|
|
|
|
|
Directory.CreateDirectory(tempPath);
|
|
|
|
|
|
|
|
|
|
|
|
var tempFile = Path.Combine(tempPath, "mcc-executable");
|
|
|
|
|
|
var useExisting = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if we already have the executable in the temporary path.
|
|
|
|
|
|
foreach (var file in Directory.EnumerateFiles(tempPath))
|
2022-08-16 00:30:56 +08:00
|
|
|
|
{
|
2023-03-28 21:06:56 +08:00
|
|
|
|
if (file.EndsWith("mcc-executable"))
|
|
|
|
|
|
{
|
2024-07-14 01:46:43 +08:00
|
|
|
|
// Check if the file is the same as the current executable.
|
|
|
|
|
|
if (File.ReadAllBytes(file).SequenceEqual(File.ReadAllBytes(executablePath)))
|
|
|
|
|
|
{
|
|
|
|
|
|
useExisting = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If not, refresh the cache.
|
|
|
|
|
|
File.Delete(file);
|
2023-03-28 21:06:56 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(executablePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new FileNotFoundException("[Script Error] Could not locate the current folder of MCC for scripting.");
|
|
|
|
|
|
}
|
2022-08-16 00:30:56 +08:00
|
|
|
|
|
2023-03-28 21:06:56 +08:00
|
|
|
|
// Copy the executable to a temporary path.
|
|
|
|
|
|
if (!useExisting)
|
|
|
|
|
|
File.Copy(executablePath, tempFile);
|
|
|
|
|
|
|
|
|
|
|
|
// Access the contents of the executable.
|
2026-03-21 20:07:20 +01:00
|
|
|
|
using ExecutableReader executableReader = new(tempFile);
|
|
|
|
|
|
if (!executableReader.IsSingleFile)
|
|
|
|
|
|
throw new InvalidOperationException("[Script Error] The executable is not a single-file bundle.");
|
|
|
|
|
|
|
|
|
|
|
|
var files = executableReader.Bundle.Files;
|
2023-03-28 21:06:56 +08:00
|
|
|
|
|
|
|
|
|
|
Stream? assemblyStream;
|
|
|
|
|
|
|
|
|
|
|
|
var assemblyrefs = Assembly.GetEntryAssembly()?.GetReferencedAssemblies().ToList()!;
|
|
|
|
|
|
assemblyrefs.Add(new("MinecraftClient"));
|
|
|
|
|
|
assemblyrefs.Add(new("System.Private.CoreLib"));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var refs in assemblyrefs) {
|
|
|
|
|
|
var loadedAssembly = Assembly.Load(refs);
|
|
|
|
|
|
if (string.IsNullOrEmpty(loadedAssembly.Location)) {
|
|
|
|
|
|
// Check if we can access the file from the executable.
|
|
|
|
|
|
var reference = files.FirstOrDefault(x =>
|
2026-03-21 20:07:20 +01:00
|
|
|
|
Path.GetFileNameWithoutExtension(x.RelativePath) == refs.Name);
|
|
|
|
|
|
var refCount = files.Count(x => Path.GetFileNameWithoutExtension(x.RelativePath) == refs.Name);
|
2023-03-28 21:06:56 +08:00
|
|
|
|
if (refCount > 1) {
|
|
|
|
|
|
// Safety net for the case where the assembly is referenced multiple times.
|
|
|
|
|
|
// Should not happen normally, but we can make exceptions when it does happen.
|
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
|
"[Script Error] Too many references to the same assembly. Assembly name: " + refs.Name);
|
|
|
|
|
|
}
|
2022-10-02 18:31:08 +08:00
|
|
|
|
|
refactor: convert == null / != null to is null / is not null in 31 files
Replace old-style null comparisons with modern C# pattern matching
syntax across Commands, Protocol, Mapping, ChatBots, Physics,
Inventory, Logger, CommandHandler, Scripting, Crypto, and other modules.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 00:42:59 +00:00
|
|
|
|
if (reference is null) {
|
2023-03-28 21:06:56 +08:00
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
|
"[Script Error] The executable does not contain a referenced assembly. Assembly name: " + refs.Name);
|
2022-08-16 00:30:56 +08:00
|
|
|
|
}
|
2023-03-28 21:06:56 +08:00
|
|
|
|
|
2026-03-21 20:07:20 +01:00
|
|
|
|
assemblyStream = reference.AsStream();
|
2023-03-28 21:06:56 +08:00
|
|
|
|
references.Add(MetadataReference.CreateFromStream(assemblyStream!));
|
|
|
|
|
|
continue;
|
2022-08-16 00:30:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-28 21:06:56 +08:00
|
|
|
|
references.Add(MetadataReference.CreateFromFile(loadedAssembly.Location));
|
2022-08-16 00:30:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2022-10-14 02:31:31 +08:00
|
|
|
|
references.Add(MetadataReference.CreateFromFile(SystemPrivateCoreLib));
|
|
|
|
|
|
references.Add(MetadataReference.CreateFromFile(SystemConsole));
|
|
|
|
|
|
references.Add(MetadataReference.CreateFromFile(MinecraftClientDll));
|
2022-08-16 00:30:56 +08:00
|
|
|
|
Assembly.GetEntryAssembly()?.GetReferencedAssemblies().ToList().ForEach(a => references.Add(MetadataReference.CreateFromFile(Assembly.Load(a).Location)));
|
|
|
|
|
|
}
|
|
|
|
|
|
#pragma warning restore IL3000
|
2022-10-14 02:31:31 +08:00
|
|
|
|
|
2022-07-03 22:34:07 +08:00
|
|
|
|
return CSharpCompilation.Create($"{fileName}.dll",
|
2022-08-16 00:30:56 +08:00
|
|
|
|
new[] { parsedSyntaxTree },
|
2022-10-02 18:31:08 +08:00
|
|
|
|
references: references,
|
|
|
|
|
|
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
|
2022-07-03 22:34:07 +08:00
|
|
|
|
optimizationLevel: OptimizationLevel.Release,
|
|
|
|
|
|
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));
|
|
|
|
|
|
}
|
2022-10-02 18:31:08 +08:00
|
|
|
|
|
|
|
|
|
|
internal struct CompileResult
|
|
|
|
|
|
{
|
2022-07-03 22:34:07 +08:00
|
|
|
|
internal byte[]? Assembly;
|
|
|
|
|
|
internal bool HasCompiledSucecssfully;
|
|
|
|
|
|
internal List<Diagnostic>? Failures;
|
2022-10-02 18:31:08 +08:00
|
|
|
|
public CompileResult(bool hasCompiledSucecssfully, List<Diagnostic>? failures, byte[]? assembly)
|
|
|
|
|
|
{
|
2022-07-03 22:34:07 +08:00
|
|
|
|
HasCompiledSucecssfully = hasCompiledSucecssfully;
|
|
|
|
|
|
Failures = failures;
|
|
|
|
|
|
Assembly = assembly;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-11-28 13:55:05 +08:00
|
|
|
|
}
|