initial commit

This commit is contained in:
2026-06-12 01:49:40 +05:00
commit a5f124a9db
6 changed files with 450 additions and 0 deletions

29
.gitignore vendored Normal file
View 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
View 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

View 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>

View 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
View 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;
}
}

View 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;
}
}