diff --git a/Mono.TextTemplating.Build.Tests/Mono.TextTemplating.Build.Tests.csproj b/Mono.TextTemplating.Build.Tests/Mono.TextTemplating.Build.Tests.csproj index 9f81497..d309a71 100644 --- a/Mono.TextTemplating.Build.Tests/Mono.TextTemplating.Build.Tests.csproj +++ b/Mono.TextTemplating.Build.Tests/Mono.TextTemplating.Build.Tests.csproj @@ -21,7 +21,7 @@ and loads first, thereby breaking loading of MSBuild assemblies. Force-upgrade it. --> - + diff --git a/Mono.TextTemplating.Build/BuildSpecificCodeCompilationContext.cs b/Mono.TextTemplating.Build/BuildSpecificCodeCompilationContext.cs new file mode 100644 index 0000000..d73d5fd --- /dev/null +++ b/Mono.TextTemplating.Build/BuildSpecificCodeCompilationContext.cs @@ -0,0 +1,16 @@ +using Mono.TextTemplating.CodeCompilation; + +namespace Mono.TextTemplating.Build +{ + public sealed class BuildSpecificCodeCompilationContext : ICodeCompilationContext + { + + readonly string _compilerSearchPath; + public BuildSpecificCodeCompilationContext (string compilerSearchPath) + { + _compilerSearchPath = compilerSearchPath; + } + + public string CompilerSearchPath => _compilerSearchPath; + } +} \ No newline at end of file diff --git a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj index 81b90d2..4c81069 100644 --- a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj +++ b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj @@ -34,7 +34,7 @@ - + diff --git a/Mono.TextTemplating.Build/T4.BuildTools.targets b/Mono.TextTemplating.Build/T4.BuildTools.targets index 3839b1b..b6bcfc9 100644 --- a/Mono.TextTemplating.Build/T4.BuildTools.targets +++ b/Mono.TextTemplating.Build/T4.BuildTools.targets @@ -84,6 +84,7 @@ IncludePaths="@(T4IncludePath)" ParameterValues="@(T4Argument)" ReferencePaths="@(T4ReferencePath)" + RoslynTargetsPath="$(RoslynTargetsPath)" TransformTemplates="@(T4Transform)" PreprocessTemplates="@(T4Preprocess)" AssemblyReferences="@(T4AssemblyReference)" diff --git a/Mono.TextTemplating.Build/TextTransform.cs b/Mono.TextTemplating.Build/TextTransform.cs index 79a4e02..4afd1a3 100644 --- a/Mono.TextTemplating.Build/TextTransform.cs +++ b/Mono.TextTemplating.Build/TextTransform.cs @@ -10,6 +10,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Mono.TextTemplating.CodeCompilation; namespace Mono.TextTemplating.Build { @@ -37,6 +38,11 @@ public TextTransform () : base (Messages.ResourceManager) { } public string PreprocessTargetRuntimeIdentifier { get; set; } + /// + /// The path to roslyn which will be used for this build. + /// + public string RoslynTargetsPath { get; set; } + [Required] public string IntermediateDirectory { get; set; } @@ -130,7 +136,9 @@ public override bool Execute () } } - TextTransformProcessor.Process (Log, previousBuildState, buildState, PreprocessOnly); + ICodeCompilationContext context = RoslynTargetsPath != null ? new BuildSpecificCodeCompilationContext(RoslynTargetsPath) : new DefaultCodeCompilationContext (); + + TextTransformProcessor.Process (Log, previousBuildState, buildState, PreprocessOnly, context); if (buildState.TransformTemplates != null) { TransformTemplateOutput = new ITaskItem[buildState.TransformTemplates.Count]; diff --git a/Mono.TextTemplating.Build/TextTransformProcessor.cs b/Mono.TextTemplating.Build/TextTransformProcessor.cs index 3854cd2..2c68126 100644 --- a/Mono.TextTemplating.Build/TextTransformProcessor.cs +++ b/Mono.TextTemplating.Build/TextTransformProcessor.cs @@ -12,12 +12,13 @@ using Microsoft.Build.Utilities; using Microsoft.VisualStudio.TextTemplating; +using Mono.TextTemplating.CodeCompilation; namespace Mono.TextTemplating.Build { static class TextTransformProcessor { - public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previousBuildState, TemplateBuildState buildState, bool preprocessOnly) + public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previousBuildState, TemplateBuildState buildState, bool preprocessOnly, ICodeCompilationContext context) { (var transforms, var preprocessed) = buildState.GetStaleAndNewTemplates (previousBuildState, preprocessOnly, new WriteTimeCache ().GetWriteTime, taskLog); @@ -50,7 +51,7 @@ public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previo } string outputContent; - (outputFile, outputContent) = generator.ProcessTemplateAsync (pt, inputFile, inputContent, outputFile, settings).Result; + (outputFile, outputContent) = generator.ProcessTemplateAsync (pt, inputFile, inputContent, outputFile, settings, context).Result; if (generator.Errors.HasErrors) { return; diff --git a/Mono.TextTemplating.Tests/ProcessingTests.cs b/Mono.TextTemplating.Tests/ProcessingTests.cs index 2cb8b75..223c41a 100644 --- a/Mono.TextTemplating.Tests/ProcessingTests.cs +++ b/Mono.TextTemplating.Tests/ProcessingTests.cs @@ -29,7 +29,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; - +using Mono.TextTemplating.CodeCompilation; using Xunit; namespace Mono.TextTemplating.Tests @@ -150,7 +150,7 @@ public async Task SetOutputExtension () var gen = new TemplateGenerator (); var pt = gen.ParseTemplate (inputFile, inputContent); TemplateSettings settings = TemplatingEngine.GetSettings (gen, pt); - (outputName, _) = await gen.ProcessTemplateAsync (pt, inputFile, inputContent, outputName, settings); + (outputName, _) = await gen.ProcessTemplateAsync (pt, inputFile, inputContent, outputName, settings, new DefaultCodeCompilationContext()); Assert.Equal ("hello.cfg", outputName); } diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs index 4235e06..da96513 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs @@ -45,6 +45,37 @@ public static bool IsLangVersionArg (string arg) => return $"-langversion:{ToString (runtime.RuntimeLangVersion)}"; } + public static CSharpLangVersion ParseLangVersionOutput(string stdOut){ + // first remove any lines which are not numbers, and handle the default of something similar to "13.0 (default)" + var lines = stdOut.Split(new[] { Environment.NewLine, " " }, StringSplitOptions.RemoveEmptyEntries); + // now find any numbers remaining - these are the versions and 1-5 are ints and greater than 6.0 are floats + var version = lines.Where(l => float.TryParse(l, out _)) + .Select(float.Parse) + .ToArray(); + + // if we have any numbers, return the highest one + if(version.Length > 0){ + return version.Max() switch { + 1 => CSharpLangVersion.v5_0, + 2 => CSharpLangVersion.v6_0, + 3 => CSharpLangVersion.v7_0, + 4 => CSharpLangVersion.v7_1, + 5 => CSharpLangVersion.v7_2, + 6 => CSharpLangVersion.v7_3, + 7 => CSharpLangVersion.v8_0, + 8 => CSharpLangVersion.v9_0, + 9 => CSharpLangVersion.v10_0, + 10 => CSharpLangVersion.v11_0, + 11 => CSharpLangVersion.v12_0, + 12 => CSharpLangVersion.v13_0, + _ => CSharpLangVersion.Latest + }; + } + + // we didn't find any numbers, so return the latest version + return CSharpLangVersion.Latest; + } + //https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history public static CSharpLangVersion FromNetCoreSdkVersion (SemVersion sdkVersion) => sdkVersion switch { diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/DefaultCodeCompilationContext.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/DefaultCodeCompilationContext.cs new file mode 100644 index 0000000..07c1ae0 --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/DefaultCodeCompilationContext.cs @@ -0,0 +1,11 @@ +using System; +using System.IO; + +namespace Mono.TextTemplating.CodeCompilation +{ + public class DefaultCodeCompilationContext : ICodeCompilationContext + { + public string CompilerSearchPath { get; } = Path.GetDirectoryName (typeof (object).Assembly.Location); + + } +} \ No newline at end of file diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/ICodeCompilationContext.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/ICodeCompilationContext.cs new file mode 100644 index 0000000..116db07 --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/ICodeCompilationContext.cs @@ -0,0 +1,10 @@ +namespace Mono.TextTemplating.CodeCompilation +{ + public interface ICodeCompilationContext + { + /// + /// The directory to search for the compiler in. + /// + string CompilerSearchPath { get; } + } +} \ No newline at end of file diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/RuntimeInfo.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/RuntimeInfo.cs index 3a00fac..9307950 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/RuntimeInfo.cs +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/RuntimeInfo.cs @@ -85,7 +85,9 @@ class RuntimeInfo public string RefAssembliesDir { get; } public string RuntimeFacadesDir { get; } - public static RuntimeInfo GetRuntime () + public static RuntimeInfo GetRuntime () => GetRuntime (new DefaultCodeCompilationContext ()); + + public static RuntimeInfo GetRuntime (ICodeCompilationContext context) { if (Type.GetType ("Mono.Runtime") != null) { @@ -93,7 +95,7 @@ public static RuntimeInfo GetRuntime () } else if (RuntimeInformation.FrameworkDescription.StartsWith (".NET Framework", StringComparison.OrdinalIgnoreCase)) { - return GetNetFrameworkRuntime (); + return GetNetFrameworkRuntime (context); } else { @@ -123,24 +125,56 @@ static RuntimeInfo GetMonoRuntime () ); } - static RuntimeInfo GetNetFrameworkRuntime () + static RuntimeInfo GetNetFrameworkRuntime (ICodeCompilationContext context) { - var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); - var csc = Path.Combine (runtimeDir, "csc.exe"); + var csc = Path.Combine (context.CompilerSearchPath, "csc.exe"); if (!File.Exists (csc)) { return FromError (RuntimeKind.NetFramework, "Could not find csc in host .NET Framework installation"); } - return new RuntimeInfo ( - RuntimeKind.NetFramework, - runtimeDir: runtimeDir, - // we don't really care about the version if it's not .net core - runtimeVersion: new Version ("4.7.2"), - refAssembliesDir: null, - runtimeFacadesDir: runtimeDir, - cscPath: csc, - cscMaxLangVersion: CSharpLangVersion.v5_0, - runtimeLangVersion: CSharpLangVersion.v5_0 - ); + + // now, determine the csc max lang version, just run csc -langversion:? and parse the output + var psi = new System.Diagnostics.ProcessStartInfo (csc, "-langversion:?") { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + psi.RedirectStandardOutput = true; + using (var p = System.Diagnostics.Process.Start (psi)) { + p.WaitForExit (); + var output = p.StandardOutput.ReadToEnd (); + if (output.Contains ("error CS2008")) { + // then we have a basic csc that doesn't support -langversion, probably net4 + return new RuntimeInfo ( + RuntimeKind.NetFramework, + runtimeDir: context.CompilerSearchPath, + // we don't really care about the version if it's not .net core + runtimeVersion: new Version ("4.7.2"), + refAssembliesDir: null, + runtimeFacadesDir: context.CompilerSearchPath, + cscPath: csc, + cscMaxLangVersion: CSharpLangVersion.v5_0, + runtimeLangVersion: CSharpLangVersion.v5_0 + ); + } + + var maxLangVersion = CSharpLangVersionHelper.ParseLangVersionOutput (output); + + // now that we know the max lang version, we need to find the runtime dir, which if it's msbuild, it's gonna be one dir up from CompilerSearchPath + // C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Roslyn -> C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin + + return new RuntimeInfo ( + RuntimeKind.NetFramework, + runtimeDir: Directory.GetParent (context.CompilerSearchPath).FullName, + // we don't really care about the version if it's not .net core + runtimeVersion: new Version ("4.7.2"), + refAssembliesDir: null, + runtimeFacadesDir: context.CompilerSearchPath, + cscPath: csc, + cscMaxLangVersion: maxLangVersion, + runtimeLangVersion: maxLangVersion + ); + } } static RuntimeInfo GetDotNetCoreSdk () @@ -193,8 +227,6 @@ static RuntimeInfo GetDotNetCoreSdk () out _ ); - - return new RuntimeInfo ( RuntimeKind.NetCore, runtimeDir: runtimeDir, diff --git a/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs b/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs index 9cd4839..33ff3e4 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs @@ -33,6 +33,7 @@ using Microsoft.VisualStudio.TextTemplating; using System.Threading; using System.Threading.Tasks; +using Mono.TextTemplating.CodeCompilation; namespace Mono.TextTemplating { @@ -119,7 +120,22 @@ void InitializeForRun (string inputFileName = null, string outputFileName = null public bool ProcessTemplate (string inputFile, string outputFile) => ProcessTemplateAsync (inputFile, outputFile, CancellationToken.None).Result; - public async Task ProcessTemplateAsync (string inputFile, string outputFile, CancellationToken token = default) + public async Task ProcessTemplateAsync (string inputFile, string outputFile) + => await ProcessTemplateAsync (inputFile, outputFile, CancellationToken.None).ConfigureAwait (false); + + public async Task ProcessTemplateAsync (string inputFile, string outputFile, CancellationToken token) + => await ProcessTemplateAsync (inputFile, outputFile, new DefaultCodeCompilationContext(), token).ConfigureAwait (false); + + internal async Task<(string outputFile, string outputContent)> ProcessTemplateAsync (ParsedTemplate pt, string inputFile, string inputContent, string outputFile, TemplateSettings settings) + { + InitializeForRun (inputFileName: inputFile, outputFileName: outputFile); + + var outputContent = await Engine.ProcessTemplateAsync (pt, inputContent, settings, this, new DefaultCodeCompilationContext()).ConfigureAwait (false); + + return (OutputFile, outputContent); + } + + public async Task ProcessTemplateAsync (string inputFile, string outputFile, ICodeCompilationContext context, CancellationToken token) { if (string.IsNullOrEmpty (inputFile)) throw new ArgumentNullException (nameof (inputFile)); @@ -263,10 +279,20 @@ public string PreprocessTemplate ( string outputFileName, TemplateSettings settings, CancellationToken token = default) + => await ProcessTemplateAsync (pt, inputFileName, inputContent, outputFileName, settings, new DefaultCodeCompilationContext (), token).ConfigureAwait (false); + + public async Task<(string fileName, string content)> ProcessTemplateAsync ( + ParsedTemplate pt, + string inputFileName, + string inputContent, + string outputFileName, + TemplateSettings settings, + ICodeCompilationContext context, + CancellationToken token = default) { InitializeForRun (inputFileName, outputFileName); - var outputContent = await Engine.ProcessTemplateAsync (pt, inputContent, settings, this, token).ConfigureAwait (false); + var outputContent = await Engine.ProcessTemplateAsync (pt, inputContent, settings, this, context, token).ConfigureAwait (false); return (OutputFile, outputContent); } diff --git a/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs b/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs index 7a00f75..e448f04 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs @@ -59,10 +59,10 @@ internal void SetCompilerFunc (Func cr createCompilerFunc = createCompiler; } - CodeCompilation.CodeCompiler GetOrCreateCompiler () + CodeCompilation.CodeCompiler GetOrCreateCompiler (ICodeCompilationContext context) { if (cachedCompiler == null) { - var runtime = RuntimeInfo.GetRuntime (); + var runtime = RuntimeInfo.GetRuntime (context); if (runtime.Error != null) { throw new TemplatingEngineException (runtime.Error); } @@ -79,13 +79,13 @@ public string ProcessTemplate (string content, ITextTemplatingEngineHost host) public async Task ProcessTemplateAsync (string content, ITextTemplatingEngineHost host, CancellationToken token = default) { - using var tpl = await CompileTemplateAsync (content, host, token).ConfigureAwait (false); + using var tpl = await CompileTemplateAsync (content, host, new DefaultCodeCompilationContext(), token).ConfigureAwait (false); return tpl?.Process (); } - public async Task ProcessTemplateAsync (ParsedTemplate pt, string content, TemplateSettings settings, ITextTemplatingEngineHost host, CancellationToken token = default) + public async Task ProcessTemplateAsync (ParsedTemplate pt, string content, TemplateSettings settings, ITextTemplatingEngineHost host, ICodeCompilationContext context, CancellationToken token = default) { - var tpl = await CompileTemplateAsync (pt, content, host, settings, token).ConfigureAwait (false); + var tpl = await CompileTemplateAsync (pt, content, host, settings, context, token).ConfigureAwait (false); using (tpl?.template) { return tpl?.template.Process (); } @@ -189,9 +189,12 @@ internal static string PreprocessTemplateInternal (ParsedTemplate pt, string con [Obsolete("Use CompileTemplateAsync")] public CompiledTemplate CompileTemplate (string content, ITextTemplatingEngineHost host) - => CompileTemplateAsync (content, host, CancellationToken.None).Result; + => CompileTemplateAsync (content, host, new DefaultCodeCompilationContext(), CancellationToken.None).Result; public async Task CompileTemplateAsync (string content, ITextTemplatingEngineHost host, CancellationToken token) + => await CompileTemplateAsync (content, host, new DefaultCodeCompilationContext(), token).ConfigureAwait (false); + + public async Task CompileTemplateAsync (string content, ITextTemplatingEngineHost host, ICodeCompilationContext context, CancellationToken token) { if (content == null) throw new ArgumentNullException (nameof (content)); @@ -204,7 +207,7 @@ public async Task CompileTemplateAsync (string content, ITextT return null; } - var tpl = await CompileTemplateInternal (pt, content, host, null, token).ConfigureAwait (false); + var tpl = await CompileTemplateInternal (pt, content, host, null, context, token).ConfigureAwait (false); return tpl?.template; } @@ -224,7 +227,7 @@ public CompiledTemplate CompileTemplate ( out string[] references, TemplateSettings settings = null) { - var result = CompileTemplateAsync (pt, content, host, settings, CancellationToken.None).Result; + var result = CompileTemplateAsync (pt, content, host, settings, new DefaultCodeCompilationContext(), CancellationToken.None).Result; references = result?.references; return result?.template; } @@ -234,14 +237,17 @@ public CompiledTemplate CompileTemplate ( string content, ITextTemplatingEngineHost host, TemplateSettings settings = null, + ICodeCompilationContext context = null, CancellationToken token = default) { if (pt == null) throw new ArgumentNullException (nameof (pt)); if (host == null) throw new ArgumentNullException (nameof (host)); + if (content == null) + context = new DefaultCodeCompilationContext(); - return CompileTemplateInternal (pt, content, host, settings, token); + return CompileTemplateInternal (pt, content, host, settings,context, token); } async Task<(CompiledTemplate template, string[] references)?> CompileTemplateInternal ( @@ -249,6 +255,7 @@ public CompiledTemplate CompileTemplate ( string content, ITextTemplatingEngineHost host, TemplateSettings settings, + ICodeCompilationContext context, CancellationToken token ) { @@ -274,7 +281,7 @@ CancellationToken token return null; } - (var results, var assembly) = await CompileCode (references, settings, ccu, token).ConfigureAwait (false); + (var results, var assembly) = await CompileCode (references, settings, ccu, context, token).ConfigureAwait (false); if (results.Errors.HasErrors) { host.LogErrors (pt.Errors); host.LogErrors (results.Errors); @@ -288,7 +295,7 @@ CancellationToken token return (compiledTemplate, references); } - async Task<(CompilerResults, CompiledAssemblyData)> CompileCode (IEnumerable references, TemplateSettings settings, CodeCompileUnit ccu, CancellationToken token) + async Task<(CompilerResults, CompiledAssemblyData)> CompileCode (IEnumerable references, TemplateSettings settings, CodeCompileUnit ccu, ICodeCompilationContext context, CancellationToken token) { string sourceText; var genOptions = new CodeGeneratorOptions (); @@ -300,7 +307,7 @@ CancellationToken token CompiledAssemblyData compiledAssembly = null; // this may throw, so do it before writing source files - var compiler = GetOrCreateCompiler (); + var compiler = GetOrCreateCompiler (context); // GetTempFileName guarantees that the returned file name is unique, but // there are no equivalent for directories, so we create a directory