diff --git a/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs b/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs index 56c2d23..d9a4586 100644 --- a/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs +++ b/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Linq; using Xunit; @@ -35,7 +36,7 @@ public void TransformOnBuild () var instance = project.Build ("Build"); - var generatedFilePath = project.DirectoryPath["foo.txt"].AssertTextStartsWith("Hello 2019!"); + var generatedFilePath = project.DirectoryPath["foo.txt"].AssertTextStartsWith ("Hello 2019!"); instance.AssertSingleItem ("GeneratedTemplates", withFullPath: generatedFilePath); instance.AssertNoItems ("PreprocessedTemplates"); @@ -71,6 +72,82 @@ public void PreprocessLegacy () instance.AssertNoItems ("GeneratedTemplates"); } + [Fact] + public void PreprocessLegacyWithExtension () + { + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("PreprocessTemplateWithExtension") + .WithProperty ("UseLegacyT4Preprocessing", "true"); + + var instance = project.Build ("TransformTemplates"); + + var generatedFilePath = project.DirectoryPath["foo.g.cs"].AssertTextStartsWith ("//--------"); + + instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); + instance.AssertNoItems ("GeneratedTemplates"); + } + + [Fact] + public void PreprocessLegacyNamespaceFromRelativePath () + { + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("PreprocessTemplateFromRelativePath") + .WithProperty ("UseLegacyT4Preprocessing", "true"); + + var instance = project.Build ("TransformTemplates"); + + var generatedFilePath = project.DirectoryPath["Nested/Template/Folder/foo.cs"] + .AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateFromRelativePath.Nested.Template.Folder {", + "partial class foo" + ); + + instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); + instance.AssertNoItems ("GeneratedTemplates"); + } + + [Fact] + public void PreprocessLegacyMetadata () + { + // Arrange + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("PreprocessTemplateMetadata") + .WithProperty ("UseLegacyT4Preprocessing", "true"); + + var outputDirectory = project.DirectoryPath["Demo/Output/OutputDirectory.cs"]; + var outputFileName = project.DirectoryPath["Demo/OutputFileNameTest.cs"]; + var outputFileNameAndOutputDirectory = project.DirectoryPath["Demo/Output/OutputDirectoryAndFileNameTest.cs"]; + + // Act + var instance = project.Build ("TransformTemplates"); + + // Assert + outputDirectory.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo.Output {", + "partial class OutputDirectory" + ); + + outputFileName.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo {", + "partial class OutputFileNameTest" + ); + + outputFileNameAndOutputDirectory.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo.Output {", + "partial class OutputDirectoryAndFileNameTest" + ); + + instance.AssertNoItems ("GeneratedTemplates"); + } + [Fact] public void PreprocessOnBuild () { @@ -93,6 +170,50 @@ public void PreprocessOnBuild () .AssertAssemblyContainsType ("PreprocessTemplate.foo"); } + [Fact] + public void PreprocessOnBuildWithExtension () + { + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("PreprocessTemplateWithExtension"); + + project.Restore (); + + var instance = project.Build ("Build"); + var objDir = project.DirectoryPath["obj", "Debug", "netstandard2.0"]; + + var generatedFilePath = instance.GetIntermediateDirFile ("TextTransform", "foo.g.cs") + .AssertTextStartsWith ("//--------"); + + instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); + instance.AssertNoItems ("GeneratedTemplates"); + + instance.GetTargetPath () + .AssertFileName ("PreprocessTemplateWithExtension.dll") + .AssertAssemblyContainsType ("PreprocessTemplateWithExtension.foo"); + } + + [Fact] + public void PreprocessOnBuildNamespaceFromRelativePath () + { + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("PreprocessTemplateFromRelativePath"); + + project.Restore (); + + var instance = project.Build ("Build"); + var objDir = project.DirectoryPath["obj", "Debug", "netstandard2.0"]; + + var generatedFilePath = instance.GetIntermediateDirFile ("TextTransform", "Nested", "Template", "Folder", "foo.cs") + .AssertTextStartsWith ("//--------"); + + instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); + instance.AssertNoItems ("GeneratedTemplates"); + + instance.GetTargetPath () + .AssertFileName ("PreprocessTemplateFromRelativePath.dll") + .AssertAssemblyContainsType ("PreprocessTemplateFromRelativePath.Nested.Template.Folder.foo"); + } + [Fact] public void PreprocessOnDesignTimeBuild () { @@ -120,13 +241,13 @@ public void IncrementalTransform () project.Restore (); - var fooGenerated = project.DirectoryPath ["foo.txt"]; - var fooTemplate = project.DirectoryPath ["foo.tt"]; - var barGenerated = project.DirectoryPath ["bar.txt"]; - var barTemplate = project.DirectoryPath ["bar.tt"]; - var includeFile = project.DirectoryPath ["helper.ttinclude"]; + var fooGenerated = project.DirectoryPath["foo.txt"]; + var fooTemplate = project.DirectoryPath["foo.tt"]; + var barGenerated = project.DirectoryPath["bar.txt"]; + var barTemplate = project.DirectoryPath["bar.tt"]; + var includeFile = project.DirectoryPath["helper.ttinclude"]; - void ExecuteAndValidate() + void ExecuteAndValidate () { var instance = project.Build ("TransformTemplates"); diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt new file mode 100644 index 0000000..b608e03 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello World diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj new file mode 100644 index 0000000..d69d1c8 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj @@ -0,0 +1,14 @@ + + + + + netstandard2.0 + + + + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt new file mode 100644 index 0000000..5cf6231 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello Item Metadata OutputDirectory diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt new file mode 100644 index 0000000..8811756 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello Item Metadata OutputDirectory And OutputDFileName diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt new file mode 100644 index 0000000..38f7b8e --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello Item Metadata OutputDFileName diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj new file mode 100644 index 0000000..0417b2b --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj @@ -0,0 +1,23 @@ + + + + + netstandard2.0 + + + + + Demo/Output + + + Demo/OutputFileNameTest.cs + + + Demo/Output + OutputDirectoryAndFileNameTest.cs + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj new file mode 100644 index 0000000..4cf2553 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj @@ -0,0 +1,14 @@ + + + + + netstandard2.0 + + + + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt new file mode 100644 index 0000000..6e129f9 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt @@ -0,0 +1,3 @@ +<#@ template language="C#" #> +<#@ output extension=".g.cs" #> +Hello World diff --git a/Mono.TextTemplating.Build/T4.BuildTools.targets b/Mono.TextTemplating.Build/T4.BuildTools.targets index 3839b1b..fd21a09 100644 --- a/Mono.TextTemplating.Build/T4.BuildTools.targets +++ b/Mono.TextTemplating.Build/T4.BuildTools.targets @@ -76,6 +76,7 @@ <_T4PreprocessOnly>False <_T4PreprocessOnly Condition="'$(_T4TransformKind)'=='DesignTime' Or ('$(_T4TransformKind)'=='OnBuild' And '$(TransformOnBuild)'=='False')">True <_T4IntermediateTemplateOutputDir Condition="'$(_T4IntermediateTemplateOutputDir)'==''">$(IntermediateOutputPath)TextTransform\ + <_T4ProjectDirectory Condition="'$(_T4ProjectDirectory)'==''">$(MSBuildProjectDirectory) diff --git a/Mono.TextTemplating.Build/TemplateBuildState.cs b/Mono.TextTemplating.Build/TemplateBuildState.cs index 6a2a1ab..ff2e49e 100644 --- a/Mono.TextTemplating.Build/TemplateBuildState.cs +++ b/Mono.TextTemplating.Build/TemplateBuildState.cs @@ -228,6 +228,8 @@ public class TransformTemplate public string OutputFile { get; set; } [Key (2)] public List Dependencies { get; set; } + [Key (3)] + public bool IgnoreExtensionTemplateDirective { get; set; } public bool IsStale (Func getFileWriteTime, TaskLoggingHelper logger) { @@ -266,6 +268,10 @@ public class PreprocessedTemplate public List Dependencies { get; set; } [Key (3)] public List References { get; set; } + [Key (4)] + public string Namespace { get; set; } + [Key (5)] + public bool IgnoreExtensionTemplateDirective { get; set; } public bool IsStale (Func getFileWriteTime, TaskLoggingHelper logger) { diff --git a/Mono.TextTemplating.Build/TextTransform.cs b/Mono.TextTemplating.Build/TextTransform.cs index 79a4e02..84240a8 100644 --- a/Mono.TextTemplating.Build/TextTransform.cs +++ b/Mono.TextTemplating.Build/TextTransform.cs @@ -40,6 +40,9 @@ public TextTransform () : base (Messages.ResourceManager) { } [Required] public string IntermediateDirectory { get; set; } + [Required] + public string ProjectDirectory { get; set; } + [Output] public ITaskItem [] RequiredAssemblies { get; set; } @@ -54,6 +57,7 @@ public override bool Execute () bool success = true; Directory.CreateDirectory (IntermediateDirectory); + Directory.CreateDirectory (ProjectDirectory); string buildStateFilename = Path.Combine (IntermediateDirectory, "t4-build-state.msgpack"); @@ -101,16 +105,33 @@ public override bool Execute () foreach (var ppt in PreprocessTemplates) { string inputFile = ppt.ItemSpec; string outputFile; + + // Metadata only supported for legacy processing. + bool hasOutputFileName = false; if (UseLegacyPreprocessingMode) { - //TODO: OutputFilePath, OutputFileName - outputFile = Path.ChangeExtension (inputFile, ".cs"); + if (!ppt.TryGetMetadata ("OutputFileName", out outputFile)) { + var name = Path.GetFileNameWithoutExtension (inputFile); + outputFile = Path.ChangeExtension (name, ".cs"); + } else { + hasOutputFileName = true; + } + + // If set, it is relative to the ProjectDirectory. + if (ppt.TryGetMetadata ("OutputDirectory", out var outputDirectory)) { + outputFile = Path.Combine (ProjectDirectory, outputDirectory, outputFile); + } else { // otherwise use the same directory as the template. + var parentDir = GetDirectoryFullPath (inputFile); + outputFile = Path.Combine (parentDir, outputFile); + } } else { - //FIXME: this could cause collisions. generate a path based on relative path and link metadata outputFile = Path.Combine (IntermediateDirectory, Path.ChangeExtension (inputFile, ".cs")); } + buildState.PreprocessTemplates.Add (new TemplateBuildState.PreprocessedTemplate { InputFile = inputFile, - OutputFile = outputFile + OutputFile = outputFile, + Namespace = CalculateNamespace (outputFile), + IgnoreExtensionTemplateDirective = hasOutputFileName }); } } @@ -118,14 +139,28 @@ public override bool Execute () if (TransformTemplates != null) { buildState.TransformTemplates = new List (); foreach (var tt in TransformTemplates) { - //TODO: OutputFilePath, OutputFileName - //var outputFilePathMetadata = tt.TryGetMetadata("OutputFilePath"); - //var outputFileNameMetadata = tt.TryGetMetadata("OutputFileName"); string inputFile = tt.ItemSpec; - string outputFile = Path.ChangeExtension (inputFile, ".txt"); + + bool hasOutputFileName = false; + if (!tt.TryGetMetadata ("OutputFileName", out var outputFile)) { + var name = Path.GetFileNameWithoutExtension(inputFile); + outputFile = Path.ChangeExtension (name, ".txt"); + } else { + hasOutputFileName = true; + } + + // If set, it is relative to the ProjectDirectory. + if (tt.TryGetMetadata ("OutputDirectory", out var outputDirectory)) { + outputFile = Path.Combine(ProjectDirectory, outputDirectory, outputFile); + } else { // otherwise use the same directory as the template. + var parentDir = GetDirectoryFullPath(inputFile); + outputFile = Path.Combine(parentDir, outputFile); + } + buildState.TransformTemplates.Add (new TemplateBuildState.TransformTemplate { InputFile = inputFile, - OutputFile = outputFile + OutputFile = outputFile, + IgnoreExtensionTemplateDirective = hasOutputFileName }); } } @@ -173,6 +208,24 @@ static TaskItem ConstructOutputItem (string outputFile, string inputFile, List { string inputFile = transform.InputFile; - string outputFile = Path.ChangeExtension (inputFile, ".txt"); var pt = LoadTemplate (generator, inputFile, out var inputContent); TemplateSettings settings = TemplatingEngine.GetSettings (generator, pt); + if (!transform.IgnoreExtensionTemplateDirective && !string.IsNullOrEmpty(settings.Extension)) { + transform.OutputFile = Path.ChangeExtension (transform.OutputFile, settings.Extension); + } + if (parameterMap != null) { AddCoercedSessionParameters (generator, pt, parameterMap); } @@ -49,14 +52,12 @@ public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previo return; } - string outputContent; - (outputFile, outputContent) = generator.ProcessTemplateAsync (pt, inputFile, inputContent, outputFile, settings).Result; + (var outputFile, var outputContent) = generator.ProcessTemplateAsync (pt, inputFile, inputContent, transform.OutputFile, settings).Result; if (generator.Errors.HasErrors) { return; } - transform.OutputFile = outputFile; transform.Dependencies = new List (generator.IncludedFiles); transform.Dependencies.AddRange (generator.CapturedReferences); @@ -80,25 +81,23 @@ public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previo var pt = LoadTemplate (generator, inputFile, out var inputContent); TemplateSettings settings = TemplatingEngine.GetSettings (generator, pt); + if (!preprocess.IgnoreExtensionTemplateDirective && !string.IsNullOrEmpty(settings.Extension)) { + preprocess.OutputFile = Path.ChangeExtension (preprocess.OutputFile, settings.Extension); + } + settings.RelativeLinePragmas = true; settings.RelativeLinePragmasBaseDirectory = Path.GetDirectoryName (preprocess.OutputFile); settings.CodeGenerationOptions.UseRemotingCallContext = buildState.PreprocessTargetRuntimeIdentifier == ".NETFramework"; - // FIXME: make these configurable, take relative path into account - settings.Namespace = buildState.DefaultNamespace; - settings.Name = Path.GetFileNameWithoutExtension (preprocess.InputFile); + settings.Namespace = preprocess.Namespace; + settings.Name = GetFileNameWithoutExtension (preprocess.OutputFile); generator.Errors.AddRange (pt.Errors); if (generator.Errors.HasErrors) { return; } - //FIXME: escaping - //FIXME: namespace name based on relative path and link metadata - string preprocessClassName = Path.GetFileNameWithoutExtension (inputFile); - settings.Name = preprocessClassName; - var outputContent = generator.PreprocessTemplate (pt, inputFile, inputContent, settings, out var references); if (generator.Errors.HasErrors) { @@ -178,9 +177,20 @@ static ParsedTemplate LoadTemplate (MSBuildTemplateGenerator generator, string f return generator.ParseTemplate (filename, inputContent); } + static string GetFileNameWithoutExtension(string path) + { + var fileName = Path.GetFileName (path); + var firstDotIndex = fileName.IndexOf('.'); + return firstDotIndex != -1 + ? fileName.Substring(0, firstDotIndex) + : fileName; + } + static void WriteOutput (MSBuildTemplateGenerator generator, string outputFile, string outputContent, Encoding encoding) { try { + var parentDir = Path.GetDirectoryName(outputFile); + Directory.CreateDirectory(parentDir); File.WriteAllText (outputFile, outputContent, encoding ?? new UTF8Encoding (encoderShouldEmitUTF8Identifier: false)); } catch (IOException ex) { diff --git a/Mono.TextTemplating.Build/readme.md b/Mono.TextTemplating.Build/readme.md index 6a7304e..a155f96 100644 --- a/Mono.TextTemplating.Build/readme.md +++ b/Mono.TextTemplating.Build/readme.md @@ -74,7 +74,7 @@ Similarly, the following `DirectiveProcessor` items are equivalent: | Metadata | Description | -- | -- -| `OutputDirectory`| Set an output directory for the file generated by the template. If this is not set, it defaults to the directory containing the template file. It is evaluated relative to the project directory, not relative to the template file. If the directory does not exist, it wil; be created. +| `OutputDirectory`| Set an output directory for the file generated by the template. If this is not set, it defaults to the directory containing the template file. It is evaluated relative to the project directory, not relative to the template file. If the directory does not exist, it will; be created. | `OutputFileName`| Set a filename to be used for the template output instead of deriving one from the template filename. If this is set, it will be the exact name used for the generated file. Any `<#@extension..#>` directive present in the template file will be ignored, and no other extension will be added. This filename may include directory components, and is evaluated relative to the template directory, which defaults to the directory containing the template file. ### CLI Properties diff --git a/Mono.TextTemplating.Tests/TestDataPath.cs b/Mono.TextTemplating.Tests/TestDataPath.cs index 971e33c..f4dd299 100644 --- a/Mono.TextTemplating.Tests/TestDataPath.cs +++ b/Mono.TextTemplating.Tests/TestDataPath.cs @@ -85,6 +85,21 @@ public TestDataPath AssertTextStartsWith (string value, StringComparison compari return this; } + /// + /// Assert that the file exists and contains the given value + /// + public TestDataPath AssertContainsText (StringComparison comparison, params string[] values) + { + AssertFileExists (); + var text = File.ReadAllText (path); + Assert.Multiple(() => { + foreach (var value in values) { + Assert.Contains (value, text, comparison); + } + }); + return this; + } + static string TrimEndingDirectorySeparator (string path) #if NETCOREAPP3_0_OR_GREATER => Path.TrimEndingDirectorySeparator (path);