initial commit
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Build results
|
||||
[Bb]in/
|
||||
.bin/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
[Pp]ublish/
|
||||
*.log
|
||||
|
||||
# IDE files
|
||||
.vs/
|
||||
.vscode/
|
||||
.vshistory/
|
||||
.idea/
|
||||
.editorconfig
|
||||
*.user
|
||||
*.DotSettings
|
||||
|
||||
# temp files
|
||||
.old*/
|
||||
old/
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
16
CSharpScript.sln
Normal file
16
CSharpScript.sln
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpScript", "CSharpScript\CSharpScript.csproj", "{4A25F563-E6D4-4A4D-82B4-21A1E7FADC8A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4A25F563-E6D4-4A4D-82B4-21A1E7FADC8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4A25F563-E6D4-4A4D-82B4-21A1E7FADC8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A25F563-E6D4-4A4D-82B4-21A1E7FADC8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A25F563-E6D4-4A4D-82B4-21A1E7FADC8A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
23
CSharpScript/CSharpScript.csproj
Normal file
23
CSharpScript/CSharpScript.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<SatelliteResourceLanguages>none</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||
<PackageReference Include="Tomlyn" Version="2.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="CSharpScript.toml.default">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
18
CSharpScript/CSharpScript.toml.default
Normal file
18
CSharpScript/CSharpScript.toml.default
Normal file
@@ -0,0 +1,18 @@
|
||||
# where to search for .cs files
|
||||
src_dir = "src"
|
||||
|
||||
# namespace and class with method Main
|
||||
main_class = "Script.Program"
|
||||
|
||||
# where compiled files will be cached
|
||||
cache_dir = "cache/CSharpScript"
|
||||
|
||||
# comppiled file name
|
||||
assembly_file_name = "Program.dll"
|
||||
|
||||
# paths of dll files that are referenced in your source code
|
||||
# Placeholders:
|
||||
# %RUNTIME% - directory where System.* libraries are stored
|
||||
references = [
|
||||
"%RUNTIME%/netstandard.dll"
|
||||
]
|
||||
191
CSharpScript/Program.cs
Normal file
191
CSharpScript/Program.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Tomlyn;
|
||||
|
||||
namespace CSharpScript;
|
||||
|
||||
public class Config
|
||||
{
|
||||
public string src_dir { get; set; }
|
||||
public string main_class { get; set; }
|
||||
public string cache_dir { get; set; } = "cache/CSharpScript";
|
||||
public string assembly_file_name { get; set; }
|
||||
|
||||
public List<string> references { get; set; } = ["%RUNTIME%/netstandard.dll"];
|
||||
}
|
||||
|
||||
public static class Program
|
||||
{
|
||||
private const string CONFIG_PATH = "CSharpScript.toml";
|
||||
|
||||
private static readonly TomlSerializerOptions TomlSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
IndentSize = 4,
|
||||
MaxDepth = 64,
|
||||
DefaultIgnoreCondition = TomlIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
static string PathFixSeparators(string path)
|
||||
{
|
||||
char sep = Path.DirectorySeparatorChar;
|
||||
char notSep = sep == '\\' ? '/' : '\\';
|
||||
return path.Replace(notSep, sep);
|
||||
}
|
||||
|
||||
static int Main(string[] args)
|
||||
{
|
||||
if (!File.Exists(CONFIG_PATH))
|
||||
{
|
||||
Console.WriteLine($"Error: Config '{CONFIG_PATH}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var conf = TomlSerializer.Deserialize<Config>(File.ReadAllText(CONFIG_PATH), TomlSerializerOptions)
|
||||
?? throw new Exception("can't deserialize config " + CONFIG_PATH);
|
||||
|
||||
// find sources
|
||||
var csFiles = Directory.GetFiles(PathFixSeparators(conf.src_dir), "*.cs").ToList();
|
||||
Console.WriteLine("Source files:");
|
||||
csFiles.ForEach(Console.WriteLine);
|
||||
Console.WriteLine("");
|
||||
|
||||
// try load cached assembly
|
||||
string cachedAssemblyPath = Path.GetFullPath(
|
||||
Path.Combine(
|
||||
PathFixSeparators(conf.cache_dir),
|
||||
conf.assembly_file_name));
|
||||
if (File.Exists(cachedAssemblyPath))
|
||||
{
|
||||
var sourceFileUpdateTime = csFiles.Select(File.GetLastWriteTime).Max();
|
||||
var cachedAssemblyUpdateTime = File.GetLastWriteTime(cachedAssemblyPath);
|
||||
Console.WriteLine($"Sources in '{conf.src_dir}' were updated at {sourceFileUpdateTime:yyyy.MM.dd-HH:mm:ss}");
|
||||
Console.WriteLine($"Assembly '{cachedAssemblyPath}' was cached at {cachedAssemblyUpdateTime:yyyy.MM.dd-HH:mm:ss}");
|
||||
// sources weren't changed
|
||||
if (sourceFileUpdateTime <= cachedAssemblyUpdateTime)
|
||||
{
|
||||
Console.WriteLine($"Loading cached assembly '{cachedAssemblyPath}'");
|
||||
var assembly = Assembly.LoadFile(cachedAssemblyPath);
|
||||
return ExecAssemblyMain(assembly, conf, args);
|
||||
}
|
||||
Console.WriteLine("Cached assembly is obsolete");
|
||||
}
|
||||
|
||||
// compile sources
|
||||
var sw = Stopwatch.StartNew();
|
||||
var compiler = InitCompiler(conf);
|
||||
Console.WriteLine($"Initialized in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
Console.WriteLine("Compiling...");
|
||||
var compResult = compiler.Compile(csFiles);
|
||||
Console.WriteLine($"Compiled in {sw.ElapsedMilliseconds}s");
|
||||
sw.Stop();
|
||||
return compResult.Match(
|
||||
noChanges => throw new NotImplementedException(noChanges.ToString()),
|
||||
success =>
|
||||
{
|
||||
PrintDiagnostics(success.Diagnostics);
|
||||
Console.WriteLine();
|
||||
var assembly = SaveAndLoadCompiledAssembly(success, cachedAssemblyPath);
|
||||
return ExecAssemblyMain(assembly, conf, args);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
PrintDiagnostics(error.Diagnostics);
|
||||
Console.WriteLine("Compilation failed.");
|
||||
return 2;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static RoslynWrapper InitCompiler(Config conf)
|
||||
{
|
||||
Console.WriteLine("\nInitializing compiler...");
|
||||
|
||||
Console.WriteLine("Referenced assemblies:");
|
||||
List<string> referenceAssemblyFilePaths = AppDomain.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
|
||||
.Select(a => a.Location)
|
||||
.ToList();
|
||||
referenceAssemblyFilePaths.ForEach(Console.WriteLine);
|
||||
|
||||
var systemDllPath = referenceAssemblyFilePaths
|
||||
.Find(p => Path.GetFileName(p).StartsWith("System.Private."))
|
||||
?? throw new Exception("can't find location of System.dll in loaded assenmblies");
|
||||
string runtimeAssembliesDir = Path.GetDirectoryName(systemDllPath) ?? ".";
|
||||
foreach (var path_ in conf.references)
|
||||
{
|
||||
string path = Path.GetFullPath(
|
||||
path_.Replace("%RUNTIME%", runtimeAssembliesDir));
|
||||
referenceAssemblyFilePaths.Add(path);
|
||||
Console.WriteLine(path);
|
||||
}
|
||||
|
||||
var referencedAssemblies = referenceAssemblyFilePaths
|
||||
.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p))
|
||||
.ToList();
|
||||
Console.WriteLine();
|
||||
|
||||
var compiler = new RoslynWrapper(conf.assembly_file_name, referencedAssemblies);
|
||||
compiler.CompilationOptions = compiler.CompilationOptions
|
||||
.WithOutputKind(OutputKind.ConsoleApplication)
|
||||
.WithMainTypeName(conf.main_class);
|
||||
return compiler;
|
||||
}
|
||||
|
||||
private static void PrintDiagnostics(IEnumerable<Diagnostic> diagnostics)
|
||||
{
|
||||
Console.WriteLine("Diagnostic messages:");
|
||||
foreach (var d in diagnostics)
|
||||
{
|
||||
var span = d.Location.GetLineSpan();
|
||||
var start = span.StartLinePosition;
|
||||
string? locationString = null;
|
||||
if (!string.IsNullOrEmpty(span.Path))
|
||||
locationString = $"{span.Path}:{start.Line + 1}:{start.Character + 1} ";
|
||||
Console.WriteLine($"{locationString}{d.Severity} {d.Id}: {d.GetMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Assembly SaveAndLoadCompiledAssembly(CompilationSuccess success, string filePath)
|
||||
{
|
||||
Console.WriteLine($"Saving compiled assembly to '{filePath}'");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? ".");
|
||||
File.WriteAllBytes(filePath, success.AssemblyBytes);
|
||||
Console.WriteLine("Loading compiled assembly from memory");
|
||||
var assembly = Assembly.Load(success.AssemblyBytes);
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static int ExecAssemblyMain(Assembly assembly, Config conf, string[] args)
|
||||
{
|
||||
MethodInfo? entryPoint = assembly.EntryPoint;
|
||||
if (entryPoint == null)
|
||||
throw new Exception($"No entry point found in assembly {assembly} ");
|
||||
|
||||
// Main()
|
||||
object?[]? parameters = null;
|
||||
// Main(string[] args)
|
||||
if (entryPoint.GetParameters().Length == 1)
|
||||
{
|
||||
parameters = [ args ];
|
||||
}
|
||||
|
||||
string? argsStr = null;
|
||||
if (args.Length != 0)
|
||||
{
|
||||
argsStr = "'" + string.Join("', '", args) + "'";
|
||||
}
|
||||
Console.WriteLine($"Executing {conf.assembly_file_name} {conf.main_class}.Main({argsStr})\n");
|
||||
object? result = entryPoint.Invoke(null, parameters);
|
||||
if (result is int retcode)
|
||||
return retcode;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
173
CSharpScript/RoslynWrapper.cs
Normal file
173
CSharpScript/RoslynWrapper.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using OneOf;
|
||||
|
||||
namespace CSharpScript;
|
||||
|
||||
// Based on https://github.com/tugberkugurlu/DotNetSamples/blob/master/csharp/RoslynCompileSample/RoslynCompileSample/Program.cs
|
||||
|
||||
public enum CompilationStatus
|
||||
{
|
||||
Compiled, NoChanges, Error
|
||||
}
|
||||
|
||||
public record CompilationNoChanges;
|
||||
public record CompilationSuccess(IList<Diagnostic> Diagnostics, byte[] AssemblyBytes);
|
||||
public record CompilationError(IList<Diagnostic> Diagnostics);
|
||||
|
||||
|
||||
public class RoslynWrapper
|
||||
{
|
||||
public readonly string AssemblyName;
|
||||
|
||||
public List<MetadataReference> References;
|
||||
|
||||
public CSharpParseOptions ParseOptions = new(
|
||||
languageVersion: LanguageVersion.Latest);
|
||||
|
||||
public CSharpCompilationOptions CompilationOptions = new(
|
||||
outputKind: OutputKind.DynamicallyLinkedLibrary);
|
||||
|
||||
|
||||
private record CachedSourceFile(DateTime EditTime, CSharpSyntaxTree SyntaxTree);
|
||||
|
||||
private Dictionary<string, CachedSourceFile> _cachedSourceFiles = new();
|
||||
|
||||
private CSharpCompilation? _cachedCompilation;
|
||||
|
||||
|
||||
public RoslynWrapper(string assemblyName, List<MetadataReference> references)
|
||||
{
|
||||
AssemblyName = assemblyName;
|
||||
References = references;
|
||||
}
|
||||
|
||||
|
||||
public OneOf<CompilationNoChanges, CompilationSuccess, CompilationError> Compile(IEnumerable<string> filePaths)
|
||||
{
|
||||
if(_cachedCompilation is null)
|
||||
CreateCompilation(filePaths);
|
||||
else if (UpdateCompilation(filePaths) == false)
|
||||
return new CompilationNoChanges();
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
var result = _cachedCompilation!.Emit(ms);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
return new CompilationSuccess(result.Diagnostics, ms.ToArray());
|
||||
}
|
||||
|
||||
var errors = result.Diagnostics
|
||||
.Where(d =>
|
||||
d.IsWarningAsError ||
|
||||
d.Severity == DiagnosticSeverity.Error)
|
||||
.ToList();
|
||||
return new CompilationError(errors);
|
||||
}
|
||||
|
||||
|
||||
private static SourceText ReadSourceText(string filePath)
|
||||
{
|
||||
using var fileStream = File.OpenRead(filePath);
|
||||
return SourceText.From(fileStream);
|
||||
}
|
||||
|
||||
private void CreateCompilation(IEnumerable<string> filePaths)
|
||||
{
|
||||
Dictionary<string, CachedSourceFile> sourceFiles = new();
|
||||
List<SyntaxTree> syntaxTrees = new();
|
||||
foreach (var filePath in filePaths)
|
||||
{
|
||||
var editTime = File.GetLastWriteTime(filePath);
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(ReadSourceText(filePath), ParseOptions, filePath);
|
||||
syntaxTrees.Add(syntaxTree);
|
||||
sourceFiles.Add(filePath, new CachedSourceFile(editTime, (CSharpSyntaxTree)syntaxTree));
|
||||
}
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
AssemblyName,
|
||||
syntaxTrees: syntaxTrees,
|
||||
references: References,
|
||||
options: CompilationOptions);
|
||||
|
||||
// assign fields only when compilation is initialized successfully
|
||||
_cachedCompilation = compilation;
|
||||
_cachedSourceFiles = sourceFiles;
|
||||
}
|
||||
|
||||
/// <returns>false if source code wasn't changed since last cached compilation</returns>
|
||||
private bool UpdateCompilation(IEnumerable<string> filePaths)
|
||||
{
|
||||
if (_cachedCompilation is null)
|
||||
throw new Exception("can't update compilation because it was not cached");
|
||||
|
||||
Dictionary<string, CachedSourceFile> cachedSourceFilesTemp = new(_cachedSourceFiles);
|
||||
List<(CachedSourceFile old, CachedSourceFile upd)> sourceFilesChanged = new();
|
||||
List<CachedSourceFile> sourceFilesNotChanged = new();
|
||||
List<CachedSourceFile> sourceFilesAdded = new();
|
||||
List<CachedSourceFile> sourceFilesRemoved = new();
|
||||
|
||||
// populate sourceFilesChanged, sourceFilesNotChanged, sourceFilesAdded
|
||||
foreach (var filePath in filePaths)
|
||||
{
|
||||
var editTime = File.GetLastWriteTime(filePath);
|
||||
// source file is cached
|
||||
if(_cachedSourceFiles.TryGetValue(filePath, out var cachedFile))
|
||||
{
|
||||
cachedSourceFilesTemp.Remove(filePath);
|
||||
|
||||
// file wasn't edited
|
||||
if (editTime == cachedFile.EditTime)
|
||||
{
|
||||
sourceFilesNotChanged.Add(cachedFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
// file was edited
|
||||
var updatedSyntaxTree = cachedFile.SyntaxTree.WithChangedText(ReadSourceText(filePath));
|
||||
var updatedFile = new CachedSourceFile(editTime, (CSharpSyntaxTree)updatedSyntaxTree);
|
||||
sourceFilesChanged.Add(new(cachedFile, updatedFile));
|
||||
}
|
||||
|
||||
// source file is new
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(ReadSourceText(filePath), ParseOptions, filePath);
|
||||
sourceFilesAdded.Add(new CachedSourceFile(editTime, (CSharpSyntaxTree)syntaxTree));
|
||||
}
|
||||
|
||||
// at this point all wiles that are left in cachedSourceFilesTemp are not present in filePaths
|
||||
sourceFilesRemoved.AddRange(cachedSourceFilesTemp.Values);
|
||||
|
||||
// source files are the same as in cached compilation, nothing to update
|
||||
int changedFilesCount = sourceFilesChanged.Count + sourceFilesAdded.Count + sourceFilesRemoved.Count;
|
||||
if (changedFilesCount == 0)
|
||||
return false;
|
||||
|
||||
Dictionary<string, CachedSourceFile> updatedCachedSourceFiles = new();
|
||||
foreach (var csf in sourceFilesNotChanged)
|
||||
updatedCachedSourceFiles.Add(csf.SyntaxTree.FilePath, csf);
|
||||
foreach (var csf in sourceFilesAdded)
|
||||
updatedCachedSourceFiles.Add(csf.SyntaxTree.FilePath, csf);
|
||||
foreach (var tupl in sourceFilesChanged)
|
||||
updatedCachedSourceFiles.Add(tupl.upd.SyntaxTree.FilePath, tupl.upd);
|
||||
|
||||
var updatedCompilation = _cachedCompilation
|
||||
.RemoveSyntaxTrees(sourceFilesRemoved.Select(csf => csf.SyntaxTree))
|
||||
.AddSyntaxTrees(sourceFilesAdded.Select(csf => csf.SyntaxTree));
|
||||
foreach (var (old, upd) in sourceFilesChanged)
|
||||
{
|
||||
updatedCompilation = updatedCompilation.ReplaceSyntaxTree(old.SyntaxTree, upd.SyntaxTree);
|
||||
}
|
||||
|
||||
// assign fields only when compilation is modified successfully
|
||||
_cachedCompilation = updatedCompilation;
|
||||
_cachedSourceFiles = updatedCachedSourceFiles;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user