diff --git a/src/CSScriptLib/src/CSScriptLib/CSScript.cs b/src/CSScriptLib/src/CSScriptLib/CSScript.cs index 9f55df8b..4ff9ecc9 100644 --- a/src/CSScriptLib/src/CSScriptLib/CSScript.cs +++ b/src/CSScriptLib/src/CSScriptLib/CSScript.cs @@ -62,6 +62,8 @@ public static string GetCacheDirectory(string file) /// public class Settings { + internal const string DefaultEncodingName = "default"; + /// /// Loads and returns the settings instance. /// diff --git a/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj b/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj index 98e6ee46..d07225b4 100644 --- a/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj +++ b/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj @@ -122,6 +122,7 @@ CS-Script now uses separate temporary directories for better isolation: + diff --git a/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs b/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs index b7cb8768..9ee5204f 100644 --- a/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs +++ b/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs @@ -220,6 +220,49 @@ override protected (byte[] asm, byte[] pdb, Project project) Compile(string scri return scriptCache[scriptHash]; } + ///////////////////////////////////// + var lightParser = new CSharpParser(scriptFile ?? tempScriptFile, true); + + if (lightParser.Precompilers.Any()) + { + var actualSources = sources.ToList(); + + var precompiliationResult = base.PrecompileScript(scriptFile ?? tempScriptFile, lightParser); + + if (precompiliationResult != null) + { + tempScriptFile ??= CSScript.GetScriptTempFile(); + File.WriteAllText(tempScriptFile, precompiliationResult.Content); + actualSources[0] = tempScriptFile; + } + + int index = 1; + foreach (string file in sources.Skip(1)) + { + if (!file.EndsWith(Globals.InjectedAttributesPrefix)) + { + var code = File.ReadAllText(file); + + var precompResult = base.PrecompileImportedScript(code, lightParser); + if (precompResult != null) + { + var newFile = file.ChangeExtension(".g.cs"); + File.WriteAllText(newFile, precompResult.Content); + actualSources[index] = newFile; + } + } + index++; + } + + actualSources.AddRange(precompiliationResult?.NewIncludes ?? []); + sources = actualSources.Distinct().ToArray(); + + if (precompiliationResult?.NewIncludes?.Any() == true) + { + refs = refs.Concat(precompiliationResult.NewReferences).ToArray(); + } + } + (byte[] asm, byte[] pdb) result = CompileAssemblyFromFileBatch_with_Csc(sources, refs, info?.AssemblyFile, this.IsDebug, info); if (IsCachingEnabled) diff --git a/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs b/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs index 1b360470..1131c2a3 100644 --- a/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs +++ b/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs @@ -68,30 +68,6 @@ // namespace CSScriptLib { - static class localExtensions - { - public static (string file, int line) Translate(this Dictionary<(int, int), (string, int)> mapping, int line) - { - foreach ((int start, int end) range in mapping.Keys) - if (range.start <= line && line <= range.end) - { - (string file, int lineOffset) = mapping[range]; - return (file, line - range.start + lineOffset); - } - - return ("", 0); - } - - static public string[] SeparateUsingsFromCode(this string code) - { - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CompilationUnitSyntax root = tree.GetCompilationUnitRoot(); - int pos = root.Usings.FullSpan.End; - - return new[] { code.Substring(0, pos).TrimEnd(), code.Substring(pos) }; - } - } - /// /// /// @@ -208,13 +184,32 @@ override protected (byte[] asm, byte[] pdb, Project project) Compile(string scri //////////////////////////////////////// var mapping = new Dictionary<(int, int), (string, int)>(); + var lightParser = new CSharpParser(scriptText, false); + List extraRefs = []; - if (scriptFile == null && new CSharpParser(scriptText, false).Imports.Any()) + if (scriptFile == null && (lightParser.Imports.Any() || lightParser.Precompilers.Any())) { tempScriptFile = CSScript.GetScriptTempFile(); File.WriteAllText(tempScriptFile, scriptText); } + if (lightParser.Precompilers.Any()) + { + var precompiliationResult = base.PrecompileScript(scriptFile ?? tempScriptFile, lightParser); + + if (precompiliationResult != null) + { + tempScriptFile ??= CSScript.GetScriptTempFile(); + File.WriteAllText(tempScriptFile, precompiliationResult.Content); + scriptText = precompiliationResult.Content; + + if (precompiliationResult?.NewIncludes?.Any() == true) + { + extraRefs.AddRange(precompiliationResult.NewReferences); + } + } + } + if (scriptFile == null && tempScriptFile == null) { // if (!DisableReferencingFromCode && info?.CodeKind != SourceCodeKind.Script) @@ -247,9 +242,17 @@ override protected (byte[] asm, byte[] pdb, Project project) Compile(string scri { var parts = File.ReadAllText(file).SeparateUsingsFromCode(); var usings = parts[0].GetLines(); - var code = parts[1].GetLines(); + var code = parts[1]; + + if (!file.EndsWith(Globals.InjectedAttributesPrefix) && lightParser.Precompilers.Any()) + { + var precompResult = base.PrecompileImportedScript(code, lightParser); - importedSources[file] = (usings.Count(), code); + if (precompResult != null) + code = precompResult.Content; + } + + importedSources[file] = (usings.Count(), code.GetLines()); add_code(file, usings, 0); } @@ -320,7 +323,7 @@ void add_code(string file, string[] codeLines, int lineOffset) var explicitRefs = this.refAssemblies.Except(refs); // from code - foreach (var asm in refs.Concat(explicitRefs)) + foreach (var asm in refs.Concat(explicitRefs).Concat(extraRefs.Select(x => Assembly.LoadFrom(x)))) { var metadata = ToMetadata(asm); if (metadata != null) @@ -671,4 +674,28 @@ public override IEvaluator Reset(bool referenceDomainAssemblies = true) return this; } } + + static class localExtensions + { + public static (string file, int line) Translate(this Dictionary<(int, int), (string, int)> mapping, int line) + { + foreach ((int start, int end) range in mapping.Keys) + if (range.start <= line && line <= range.end) + { + (string file, int lineOffset) = mapping[range]; + return (file, line - range.start + lineOffset); + } + + return ("", 0); + } + + static public string[] SeparateUsingsFromCode(this string code) + { + SyntaxTree tree = CSharpSyntaxTree.ParseText(code); + CompilationUnitSyntax root = tree.GetCompilationUnitRoot(); + int pos = root.Usings.FullSpan.End; + + return new[] { code.Substring(0, pos).TrimEnd(), code.Substring(pos) }; + } + } } \ No newline at end of file diff --git a/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs b/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs index cae8a3e8..e770d1a7 100644 --- a/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs +++ b/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs @@ -33,17 +33,22 @@ //using Microsoft.CodeAnalysis; //using Microsoft.CodeAnalysis.CSharp.Scripting using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.Loader; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Scripting; using csscript; using CSScripting; using CSScripting.CodeDom; @@ -1028,6 +1033,141 @@ public IEvaluator ReferenceAssembliesFromCode(string code, params string[] searc return this; } + Dictionary precompilersCache = new(); + + internal PrecompilationContext PrecompileImportedScript(string scriptCode, CSharpParser primaryScript) + { + string[] searchDirs = [this.GetType().Assembly.Location().GetDirName(), .. primaryScript.ExtraSearchDirs]; + + var retval = new PrecompilationContext { SearchDirs = searchDirs }; + + Hashtable contextData = new() + { + ["NewDependencies"] = retval.NewDependencies, + ["NewSearchDirs"] = retval.NewSearchDirs, + ["NewReferences"] = retval.NewReferences, + ["NewIncludes"] = retval.NewIncludes, + ["NewCompilerOptions"] = "", + ["SearchDirs"] = retval.SearchDirs, + }; + + var content = scriptCode; + var modified = false; + + foreach (string file in primaryScript.Precompilers) + { + var precompilerPath = Precompiler.FindImlementationFile(file, searchDirs); + if (!precompilersCache.ContainsKey(precompilerPath)) + throw new Exception("Precompiler " + file + " cache cannot be loaded."); // but it must exist since the script precompilers have been processed already + + var precompiler = precompilersCache[precompilerPath]; + + bool result = ApplyPrecompilation(null, retval, contextData, ref content, precompilerPath); + + if (result) + { + retval.Content = content; + retval.NewDependencies.Add(file); + modified = true; + } + } + return modified ? retval : null; + } + + internal PrecompilationContext PrecompileScript(string script, CSharpParser parser) + { + string[] searchDirs = [this.GetType().Assembly.Location().GetDirName(), .. parser.ExtraSearchDirs]; + + var retval = new PrecompilationContext { SearchDirs = searchDirs }; + + Hashtable contextData = new() + { + ["NewDependencies"] = retval.NewDependencies, + ["NewSearchDirs"] = retval.NewSearchDirs, + ["NewReferences"] = retval.NewReferences, + ["NewIncludes"] = retval.NewIncludes, + ["NewCompilerOptions"] = "", + ["SearchDirs"] = retval.SearchDirs, + }; + + var content = parser.Code; + var modified = false; + + foreach (string file in parser.Precompilers) + { + var precompilerPath = Precompiler.FindImlementationFile(file, searchDirs); + + using (SimpleAsmProbing.For(searchDirs)) + { + if (!precompilersCache.ContainsKey(precompilerPath)) + { + (byte[] asm, byte[] pdb, Project project) precompilerInfo = Compile(null, file, null); + + var precompilerAsm = Assembly.Load(precompilerInfo.asm, precompilerInfo.pdb); + + var precompilerType = precompilerAsm.GetTypes().FirstOrDefault(x => x.Name.EndsWith("Precompiler")); + if (precompilerType == null) + throw new Exception("Precompiler " + file + " cannot be loaded. CreateInstance returned null."); + + var methods = precompilerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + .Where(x => x.Name == "Compile"); + + precompilersCache[precompilerPath] = + ( + method: methods.FirstOrDefault(x => x.GetParameters().Count() == 1), + legacyMethod: methods.FirstOrDefault(x => x.GetParameters().Count() == 4) + ); + } + + bool result = ApplyPrecompilation(script, retval, contextData, ref content, precompilerPath); + + if (result) + { + retval.Content = content; + retval.NewDependencies.Add(file); + modified = true; + } + } + } + return modified ? retval : null; + } + + private bool ApplyPrecompilation(string script, PrecompilationContext retval, Hashtable contextData, ref string content, string precompilerPath) + { + var (method, legacyMethod) = precompilersCache[precompilerPath]; + + bool result; + + if (method != null) + { + // bool Compile(dynamic context) bool + // Compile(PrecompilationContext context) + object compiler = null; + if (!method.IsStatic) + compiler = Activator.CreateInstance(method.DeclaringType); + + retval.Content = content; + + result = (bool)method.Invoke(compiler, [retval]); + + if (result) + content = retval.Content; + } + else + { + // public static bool Compile(ref string scriptCode, string + // scriptFile, bool isPrimaryScript, Hashtable context) + var compile = (Precompiler.CompileMethod)Delegate.CreateDelegate(typeof(Precompiler.CompileMethod), legacyMethod); + + result = compile(ref content, + script, + IsPrimaryScript: script != null, + contextData); + } + + return result; + } + /// /// References the given assembly by the assembly path. /// diff --git a/src/Tests.CSScriptLib/Evaluator.Api.Test.cs b/src/Tests.CSScriptLib/Evaluator.Api.Test.cs index 78496b34..e61f03d3 100644 --- a/src/Tests.CSScriptLib/Evaluator.Api.Test.cs +++ b/src/Tests.CSScriptLib/Evaluator.Api.Test.cs @@ -65,13 +65,13 @@ public API_Roslyn() string testTempFile(string fileName, [CallerMemberName] string caller = null) { var rootDir = "TestData".PathJoin(nameof(API_Roslyn), caller).GetFullPath().EnsureDir(); - if (fileName == "asm.hidden-from-xunit-dll-file") - { - File.AppendAllText( - @"D:\dev\cs-script\src\Tests.CSScriptLib\bin\Debug\net10.0\TestData\test-error.log", - rootDir.PathJoin(fileName) + Environment.NewLine); - Debugger.Launch(); // asm.dll is a special file name that xUnit locks just because it was present in the local dir. So avoid using it in the tests. - } + // if (fileName == "asm.hidden-from-xunit-dll-file") + // { + // File.AppendAllText( + // @"D:\dev\cs-script\src\Tests.CSScriptLib\bin\Debug\net10.0\TestData\test-error.log", + // rootDir.PathJoin(fileName) + Environment.NewLine); + // Debugger.Launch(); // asm.dll is a special file name that xUnit locks just because it was present in the local dir. So avoid using it in the tests. + // } return Path.Combine(rootDir, fileName); } diff --git a/src/Tests.CSScriptLib/Evaluator.CodeDom.Test.cs b/src/Tests.CSScriptLib/Evaluator.CodeDom.Test.cs index 7a2410cd..f0e6ccc9 100644 --- a/src/Tests.CSScriptLib/Evaluator.CodeDom.Test.cs +++ b/src/Tests.CSScriptLib/Evaluator.CodeDom.Test.cs @@ -290,5 +290,221 @@ public int Add(int a, int b) // dynamic script = asm.CreateObject("*"); // var result = script.Sum(7, 3); // } + + [Fact] + public void Precompiler_for_script_code() + { + var pre = testTempFile("precompiler.cs"); + + File.WriteAllText(pre, @"using System; + using System.Collections; + public class Sample_Precompiler + { + public static bool Compile(ref string scriptCode, string scriptFile, bool isPrimaryScript, Hashtable context) + { + scriptCode = scriptCode.Replace(""Hello World"", ""Hello World!!!""); + return true; + } + }"); + + dynamic script = CSScript.CodeDomEvaluator + .LoadCode($@"//css_precompiler {pre} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + var result = script.foo(); + + Assert.Equal("Hello World!!!", result); + } + + [Fact] + public void Precompiler_alt_signature() + { + var pre = testTempFile("precompiler.cs"); + + File.WriteAllText(pre, $@"//css_ref {typeof(PrecompilationContext).Assembly.Location} + using System; + using System.Collections; + public class Sample_Precompiler + {{ + public bool Compile(csscript.PrecompilationContext context) + {{ + context.Content = context.Content.Replace(""Hello World"", ""Hello World!!!""); + return true; + }} + }}"); + + CSScript.EvaluatorConfig.DebugBuild = true; + + dynamic script = CSScript.CodeDomEvaluator + .LoadCode($@"//css_precompiler {pre} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + var result = script.foo(); + + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_alt_signature2() + { + var pre = testTempFile("precompiler.cs"); + + File.WriteAllText(pre, @"using System; + using System.Collections; + public class Sample_Precompiler + { + public bool Compile(dynamic context) + { + context.Content = context.Content.Replace(""Hello World"", ""Hello World!!!""); + return true; + } + }"); + + CSScript.EvaluatorConfig.DebugBuild = true; + + dynamic script = CSScript.CodeDomEvaluator + .LoadCode($@"//css_precompiler {pre} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + var result = script.foo(); + + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_imported_scripts() + { + var pre = testTempFile("precompiler.cs"); + var importedScript = testTempFile("imported.cs"); + + File.WriteAllText(pre, + @"using System; + using System.Collections; + public class Sample_Precompiler + { + public bool Compile(csscript.PrecompilationContext context) + { + context.Content = context.Content.Replace(""Hello World"", ""Hello World!!!""); + return true; + } + }"); + + File.WriteAllText(importedScript, + @"public class Utils + { + public static string foo()=> ""Hello World""; + }"); + + dynamic script = CSScript.CodeDomEvaluator + .LoadCode($@"//css_precompiler {pre} + //css_include {importedScript} + public class Script + {{ + public string foo() + => ""Hello World""; + public string foo2() + => Utils.foo(); + }}"); + + var result = script.foo(); + Assert.Contains("Hello World!!!", result); + + result = script.foo2(); + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_imported_scripts2() + { + var pre = testTempFile("precompiler.cs"); + var importedScript = testTempFile("imported.cs"); + + File.WriteAllText(pre, + @"using System; + using System.Collections; + public class Sample_Precompiler + { + public static bool Compile(ref string code, string scriptFile, bool isPrimaryScript, Hashtable context) + { + code = code.Replace(""Hello World"", ""Hello World!!!""); + return true; + } + }"); + + File.WriteAllText(importedScript, + @"public class Utils + { + public static string foo()=> ""Hello World""; + }"); + + // CSScript.EvaluatorConfig.DebugBuild = true; + + dynamic script = CSScript.CodeDomEvaluator + .LoadCode($@"//css_precompiler {pre} + //css_include {importedScript} + public class Script + {{ + public string foo() + => ""Hello World""; + public string foo2() + => Utils.foo(); + }}"); + + var result = script.foo(); + Assert.Contains("Hello World!!!", result); + + result = script.foo2(); + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_for_script_file() + { + var preFile = testTempFile("precompiler.cs"); + var scriptFile = testTempFile("primary_script.cs"); + + File.WriteAllText(preFile, $@"using System; + using System.Collections; + public class Sample_Precompiler + {{ + public static bool Compile(ref string scriptCode, string scriptFile, bool isPrimaryScript, Hashtable context) + {{ + scriptCode = scriptCode.Replace(""Hello World"", ""Hello World!!!""); + return true; + }} + }}"); + + File.WriteAllText(scriptFile, $@"//css_precompiler {preFile} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + dynamic script = CSScript.CodeDomEvaluator + .LoadFile(scriptFile); + + var result = script.foo(); + Assert.Contains("Hello World!!!", result); + + var scriptAsm = CSScript.CodeDomEvaluator + .CompileAssemblyFromFile(scriptFile, scriptFile + ".dll"); + dynamic script2 = Assembly.LoadFrom(scriptAsm).CreateObject("*"); + + result = script2.foo(); + Assert.Contains("Hello World!!!", result); + } } } \ No newline at end of file diff --git a/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs b/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs index 8dc206e5..193cf813 100644 --- a/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs +++ b/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs @@ -152,6 +152,222 @@ public void call_LoadMethod() Assert.Equal(5, result[1]); } + [Fact] + public void Precompiler_for_script_code() + { + var pre = testTempFile("precompiler.cs"); + + File.WriteAllText(pre, $@"//css_ref {typeof(PrecompilationContext).Assembly.Location} + using System; + using System.Collections; + public class Sample_Precompiler + {{ + public static bool Compile(ref string scriptCode, string scriptFile, bool isPrimaryScript, Hashtable context) + {{ + scriptCode = scriptCode.Replace(""Hello World"", ""Hello World!!!""); + return true; + }} + }}"); + + dynamic script = CSScript.RoslynEvaluator + .LoadCode($@"//css_precompiler {pre} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + var result = script.foo(); + + Assert.Equal("Hello World!!!", result); + } + + [Fact] + public void Precompiler_alt_signature2() + { + var pre = testTempFile("precompiler.cs"); + + File.WriteAllText(pre, $@"using System; + using System.Collections; + public class Sample_Precompiler + {{ + public bool Compile(dynamic context) + {{ + context.Content = context.Content.Replace(""Hello World"", ""Hello World!!!""); + return true; + }} + }}"); + + CSScript.EvaluatorConfig.DebugBuild = true; + + dynamic script = CSScript.RoslynEvaluator + .LoadCode($@"//css_precompiler {pre} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + var result = script.foo(); + + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_alt_signature() + { + var pre = testTempFile("precompiler.cs"); + + File.WriteAllText(pre, $@"using System; + using System.Collections; + public class Sample_Precompiler + {{ + public bool Compile(dynamic context) + {{ + context.Content = context.Content.Replace(""Hello World"", ""Hello World!!!""); + return true; + }} + }}"); + + CSScript.EvaluatorConfig.DebugBuild = true; + + dynamic script = CSScript.RoslynEvaluator + .LoadCode($@"//css_precompiler {pre} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + var result = script.foo(); + + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_imported_scripts() + { + var pre = testTempFile("precompiler.cs"); + var importedScript = testTempFile("imported.cs"); + + File.WriteAllText(pre, + @"using System; + using System.Collections; + public class Sample_Precompiler + { + public bool Compile(dynamic context) + { + context.Content = context.Content.Replace(""Hello World"", ""Hello World!!!""); + return true; + } + }"); + + File.WriteAllText(importedScript, + @"public class Utils + { + public static string foo()=> ""Hello World""; + }"); + + dynamic script = CSScript.RoslynEvaluator + .LoadCode($@"//css_precompiler {pre} + //css_include {importedScript} + public class Script + {{ + public string foo() + => ""Hello World""; + public string foo2() + => Utils.foo(); + }}"); + + var result = script.foo(); + Assert.Contains("Hello World!!!", result); + + result = script.foo2(); + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_imported_scripts2() + { + var pre = testTempFile("precompiler.cs"); + var importedScript = testTempFile("imported.cs"); + + File.WriteAllText(pre, + @"using System; + using System.Collections; + public class Sample_Precompiler + { + public static bool Compile(ref string code, string scriptFile, bool isPrimaryScript, Hashtable context) + { + code = code.Replace(""Hello World"", ""Hello World!!!""); + return true; + } + }"); + + File.WriteAllText(importedScript, + @"public class Utils + { + public static string foo()=> ""Hello World""; + }"); + + // CSScript.EvaluatorConfig.DebugBuild = true; + + dynamic script = CSScript.RoslynEvaluator + .LoadCode($@"//css_precompiler {pre} + //css_include {importedScript} + public class Script + {{ + public string foo() + => ""Hello World""; + public string foo2() + => Utils.foo(); + }}"); + + var result = script.foo(); + Assert.Contains("Hello World!!!", result); + + result = script.foo2(); + Assert.Contains("Hello World!!!", result); + } + + [Fact] + public void Precompiler_for_script_file() + { + var preFile = testTempFile("precompiler.cs"); + var scriptFile = testTempFile("primary_script.cs"); + + File.WriteAllText(preFile, $@"using System; + using System.Collections; + public class Sample_Precompiler + {{ + public static bool Compile(ref string scriptCode, string scriptFile, bool isPrimaryScript, Hashtable context) + {{ + scriptCode = scriptCode.Replace(""Hello World"", ""Hello World!!!""); + return true; + }} + }}"); + + File.WriteAllText(scriptFile, $@"//css_precompiler {preFile} + public class Script + {{ + public string foo() + => ""Hello World""; + }}"); + + dynamic script = CSScript.RoslynEvaluator + .LoadFile(scriptFile); + + var result = script.foo(); + Assert.Contains("Hello World!!!", result); + + var scriptAsm = CSScript.RoslynEvaluator + .CompileAssemblyFromFile(scriptFile, scriptFile + ".dll"); + dynamic script2 = Assembly.LoadFrom(scriptAsm).CreateObject("*"); + + result = script2.foo(); + Assert.Contains("Hello World!!!", result); + } + public class Host : IScriptHost { public void WriteLine(string message) diff --git a/src/Tests.cscs/FileParserTests.cs b/src/Tests.cscs/FileParserTests.cs index d7d64489..f2bd97b3 100644 --- a/src/Tests.cscs/FileParserTests.cs +++ b/src/Tests.cscs/FileParserTests.cs @@ -2,11 +2,11 @@ using System.IO; using System.Linq; using System.Reflection; +using static System.Reflection.BindingFlags; using csscript; using CSScripting; using CSScriptLib; using Xunit; -using static System.Reflection.BindingFlags; namespace Misc { @@ -19,7 +19,7 @@ namespace Misc /// public class TestFolder { - public static string root = Assembly.GetExecutingAssembly().Location.GetDirName().PathJoin("test").EnsureDir(); + public static string root = Assembly.GetExecutingAssembly().Location.GetDirName().PathJoin("test", "TestFolder").EnsureDir(); public TestFolder() { diff --git a/src/cscs/Precompiler.cs b/src/cscs/Precompiler.cs index 3838901e..06409026 100644 --- a/src/cscs/Precompiler.cs +++ b/src/cscs/Precompiler.cs @@ -1,10 +1,16 @@ -using CSScripting; using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.CodeAnalysis; +using CSScripting; +using CSScriptLib; namespace csscript { @@ -51,11 +57,58 @@ public class PrecompilationContext /// public string[] SearchDirs = new string[0]; + /// + /// Gets or sets the precompilation content associated with this instance. + /// public string Content; + + /// + /// Gets or sets the path to the script file used for processing. + /// + /// This property should contain a valid file path. Ensure that the file exists and is + /// accessible before attempting to use it. public string scriptFile; + + /// + /// Gets or sets a value indicating whether this script is the primary script for the associated entity. + /// public bool IsPrimaryScript; } + class Precompiler + { + public delegate bool CompileMethod(ref string content, string scriptFile, bool IsPrimaryScript, Hashtable context); + + public static string FindImlementationFile(string file, string[] searchDirs) + { + string retval = FindFile(file, searchDirs); + + if (retval == null && !Path.HasExtension(file)) + { + retval = FindFile(file + ".cs", searchDirs) ?? + FindFile(file + ".dll", searchDirs); + } + + return retval; + } + + public static string FindFile(string file, string[] searchDirs) + { + if (File.Exists(file)) + { + return Path.GetFullPath(file); + } + else if (!Path.IsPathRooted(file)) + { + foreach (string dir in searchDirs) + if (File.Exists(Path.Combine(dir, file))) + return Path.Combine(dir, file); + } + + return null; + } + } + internal class DefaultPrecompiler { public static bool Compile(ref string code, string scriptFile, bool IsPrimaryScript, Hashtable context) @@ -343,7 +396,7 @@ internal static Result Process(string content, string consoleEncoding) } } - if (!autoCodeInjected && entryPointInjectionPos != -1 && !Utils.IsNullOrWhiteSpace(lineText)) + if (!autoCodeInjected && entryPointInjectionPos != -1 && lineText.HasText()) { bracket_count += lineText.Split('{').Length - 1; bracket_count -= lineText.Split('}').Length - 1; diff --git a/src/cscs/Services.Roslyn.cs b/src/cscs/Services.Roslyn.cs index 8d981644..a52721a8 100644 --- a/src/cscs/Services.Roslyn.cs +++ b/src/cscs/Services.Roslyn.cs @@ -51,7 +51,7 @@ public static CompilerResults CompileAssemblyFromFileBatch_with_roslyn(CompilerP .EnsureDir(); string firstScript = fileNames.First(); - string attr_file = fileNames.FirstOrDefault(x => x.EndsWith(".attr.g.cs", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".attr.g.vb", StringComparison.OrdinalIgnoreCase)); + string attr_file = fileNames.FirstOrDefault(x => x.EndsWith(Globals.InjectedAttributesPrefix) || x.EndsWith(".attr.g.vb")); string dbg_inject_file = fileNames.FirstOrDefault(x => x.GetFileName().StartsWith("dbg.inject.", StringComparison.OrdinalIgnoreCase)); string single_source = build_dir.PathJoin(firstScript.GetFileName()); diff --git a/src/cscs/Utils/CoreExtensions.cs b/src/cscs/Utils/CoreExtensions.cs index 48bc6cf6..19ecaa86 100644 --- a/src/cscs/Utils/CoreExtensions.cs +++ b/src/cscs/Utils/CoreExtensions.cs @@ -1,19 +1,15 @@ -using csscript; -using CSScripting; -using CSScripting.CodeDom; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.Serialization; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using CSScripting; #if class_lib @@ -375,7 +371,7 @@ public static string EnquoteArg(this string text, char quotationCharacter = '"') internal static bool IsTopLevelStatement(this SyntaxTree code) => //code.GetCompilationUnitRoot().Members.OfType().Any(); - code.GetRoot().DescendantNodes().OfType().Any(); + code.GetRoot().DescendantNodes().OfType().Any(); /// /// Determines whether the specified assembly path corresponds to a runtime assembly. diff --git a/src/cscs/Utils/Globals.cs b/src/cscs/Utils/Globals.cs index 29ca8256..64a0e687 100644 --- a/src/cscs/Utils/Globals.cs +++ b/src/cscs/Utils/Globals.cs @@ -43,6 +43,7 @@ internal static void DbgLog(string line, string context = null) public static bool DefaultRoslynCompilationToScript { get; set; } = true; static internal string DynamicWrapperClassName = "DynamicClass"; + static internal string InjectedAttributesPrefix = ".attr.g.cs"; static internal string RootClassName = "css_root"; // Roslyn still does not support anything else but `Submission#0` (17 Jul 2019) [update] // Roslyn now does support alternative class names (1 Jan 2020) diff --git a/src/cscs/Utils/Utils.cs b/src/cscs/Utils/Utils.cs index 2278c6bb..ca4ffd72 100644 --- a/src/cscs/Utils/Utils.cs +++ b/src/cscs/Utils/Utils.cs @@ -1336,7 +1336,7 @@ internal static Dictionary> LoadPrecompilers(ExecuteOptions if (precompilerFile != "" && precompilerFile != noDefaultPrecompilerSwitch) { - string sourceFile = FindImlementationFile(precompilerFile, options.searchDirs) + string sourceFile = Precompiler.FindImlementationFile(precompilerFile, options.searchDirs) ?? throw new ApplicationException("Cannot find Precompiler file " + precompilerFile); Assembly asm; @@ -1391,35 +1391,6 @@ internal static Dictionary> LoadPrecompilers(ExecuteOptions return retval; } - public static string FindFile(string file, string[] searchDirs) - { - if (File.Exists(file)) - { - return Path.GetFullPath(file); - } - else if (!Path.IsPathRooted(file)) - { - foreach (string dir in searchDirs) - if (File.Exists(Path.Combine(dir, file))) - return Path.Combine(dir, file); - } - - return null; - } - - public static string FindImlementationFile(string file, string[] searchDirs) - { - string retval = FindFile(file, searchDirs); - - if (retval == null && !Path.HasExtension(file)) - { - retval = FindFile(file + ".cs", searchDirs) ?? - FindFile(file + ".dll", searchDirs); - } - - return retval; - } - static void kill(string proc_name) { try @@ -1457,7 +1428,7 @@ internal static int GenerateCompilationContext(CSharpParser parser, ExecuteOptio { if (file != "") { - sb.Append(FindImlementationFile(file, options.searchDirs)) + sb.Append(Precompiler.FindImlementationFile(file, options.searchDirs)) .Append(','); } } diff --git a/src/cscs/csscript.cs b/src/cscs/csscript.cs index bb07d0de..b7f51c56 100644 --- a/src/cscs/csscript.cs +++ b/src/cscs/csscript.cs @@ -1896,7 +1896,7 @@ string Compile(string scriptFileName) CompilerResults results; if (generateExe) { - var exeCompatibleIjections = filesToInject.Where(x => !x.EndsWith(".attr.g.cs")).ToArray(); + var exeCompatibleIjections = filesToInject.Where(x => !x.EndsWith(Globals.InjectedAttributesPrefix)).ToArray(); filesToCompile = filesToCompile.ConcatWith(exeCompatibleIjections); results = CompileAssembly(compiler, compilerParams, filesToCompile); }