diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fd80bb6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "files.associations": { + "*.buildschema.json": "jsonc" + }, + "json.schemas": [ + { + "fileMatch": [ + "/*.buildschema.json" + ], + "url": "https://raw.githubusercontent.com/mhutch/MonoDevelop.MSBuildEditor/main/MonoDevelop.MSBuild/Schemas/buildschema.json" + } + ], + "cSpell.words": [ + "Preprocess", + "reimplementation" + ] +} diff --git a/Directory.Build.props b/Directory.Build.props index f7580d1..7162a68 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,6 +19,10 @@ + + + + true diff --git a/Mono.TextTemplating.Build/Messages.Designer.cs b/Mono.TextTemplating.Build/Messages.Designer.cs deleted file mode 100644 index ff23bfb..0000000 --- a/Mono.TextTemplating.Build/Messages.Designer.cs +++ /dev/null @@ -1,289 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Mono.TextTemplating.Build { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Messages { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Messages() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Mono.TextTemplating.Build.Messages", typeof(Messages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to T4 build state format has changed. All T4 files will be reprocessed.. - /// - internal static string BuildStateFormatChanged { - get { - return ResourceManager.GetString("BuildStateFormatChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to load T4 build state. All T4 files will be reprocessed.. - /// - internal static string BuildStateLoadFailed { - get { - return ResourceManager.GetString("BuildStateLoadFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to save T4 build state. All T4 files will be reprocessed on next run.. - /// - internal static string BuildStateSaveFailed { - get { - return ResourceManager.GetString("BuildStateSaveFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Directive processor '{0}' could not be split into 'name!class!assembly' components. - /// - internal static string DirectiveProcessorDoesNotHaveThreeValues { - get { - return ResourceManager.GetString("DirectiveProcessorDoesNotHaveThreeValues", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Directive processor '{0}' is missing component '{1}'. - /// - internal static string DirectiveProcessorMissingComponent { - get { - return ResourceManager.GetString("DirectiveProcessorMissingComponent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Directive processor '{0}' has no 'Assembly' metadata. - /// - internal static string DirectiveProcessorNoAssembly { - get { - return ResourceManager.GetString("DirectiveProcessorNoAssembly", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Internal exception. Please report at https://github.com/mono/t4. - ///{0}. - /// - internal static string InternalException { - get { - return ResourceManager.GetString("InternalException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Loaded state file '{0}'. - /// - internal static string LoadedStateFile { - get { - return ResourceManager.GetString("LoadedStateFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Template parameter '{0}' has neither 'Value' metadata nor a value encoded in the name. - /// - internal static string ParameterNoValue { - get { - return ResourceManager.GetString("ParameterNoValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to T4 engine encountered a recoverable internal error. Although it should not affect output, please report it as it may affect reliability and performance.. - /// - internal static string RecoverableInternalError { - get { - return ResourceManager.GetString("RecoverableInternalError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating all templates: default namespace has changed. - /// - internal static string RegeneratingAllDefaultNamespaceChanged { - get { - return ResourceManager.GetString("RegeneratingAllDefaultNamespaceChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating all templates: directive processors have changed. - /// - internal static string RegeneratingAllDirectiveProcessorsChanged { - get { - return ResourceManager.GetString("RegeneratingAllDirectiveProcessorsChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating all templates: include paths have changed. - /// - internal static string RegeneratingAllIncludePathsChanged { - get { - return ResourceManager.GetString("RegeneratingAllIncludePathsChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating all templates: intermediate directory has changed. - /// - internal static string RegeneratingAllIntermediateDirChanged { - get { - return ResourceManager.GetString("RegeneratingAllIntermediateDirChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating all templates: parameters have have changed. - /// - internal static string RegeneratingAllParametersChanged { - get { - return ResourceManager.GetString("RegeneratingAllParametersChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating all templates: reference paths have changed. - /// - internal static string RegeneratingAllReferencePathsChanged { - get { - return ResourceManager.GetString("RegeneratingAllReferencePathsChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating preprocessed template '{0}': output file '{1}' was not found. - /// - internal static string RegeneratingPreprocessedOutputFileMissing { - get { - return ResourceManager.GetString("RegeneratingPreprocessedOutputFileMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating preprocessed template '{0}': output file '{1}' is older than dependency '{2}'. - /// - internal static string RegeneratingPreprocessedOutputFileOlderThanDependency { - get { - return ResourceManager.GetString("RegeneratingPreprocessedOutputFileOlderThanDependency", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating preprocessed template '{0}': output file '{1}' is older than template file. - /// - internal static string RegeneratingPreprocessedOutputFileOlderThanTemplate { - get { - return ResourceManager.GetString("RegeneratingPreprocessedOutputFileOlderThanTemplate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating transform template '{0}': output file '{1}' was not found. - /// - internal static string RegeneratingTransformMissingOutputFile { - get { - return ResourceManager.GetString("RegeneratingTransformMissingOutputFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating transform template '{0}': output file '{1}' is older than dependency '{2}'. - /// - internal static string RegeneratingTransformOutputFileOlderThanDependency { - get { - return ResourceManager.GetString("RegeneratingTransformOutputFileOlderThanDependency", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating transform template '{0}': output file '{1}' is older than template file. - /// - internal static string RegeneratingTransformOutputFileOlderThanTemplate { - get { - return ResourceManager.GetString("RegeneratingTransformOutputFileOlderThanTemplate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regenerating transform templates: assembly references have changed. - /// - internal static string RegeneratingTransformsAsmRefsChanged { - get { - return ResourceManager.GetString("RegeneratingTransformsAsmRefsChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Skipping preprocessed template '{0}': output '{1}' is up to date. - /// - internal static string SkippingPreprocessedOutputUpToDate { - get { - return ResourceManager.GetString("SkippingPreprocessedOutputUpToDate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Skipping transform template '{0}': output '{1}' is up to date. - /// - internal static string SkippingTransformUpToDate { - get { - return ResourceManager.GetString("SkippingTransformUpToDate", resourceCulture); - } - } - } -} diff --git a/Mono.TextTemplating.Build/Messages.resx b/Mono.TextTemplating.Build/Messages.resx index a1d6685..14e8328 100644 --- a/Mono.TextTemplating.Build/Messages.resx +++ b/Mono.TextTemplating.Build/Messages.resx @@ -127,19 +127,19 @@ Failed to save T4 build state. All T4 files will be reprocessed on next run. - Directive processor '{0}' could not be split into 'name!class!assembly' components - - - Directive processor '{0}' is missing component '{1}' + Directive processor '{0}' has encoded form with incorrect number of components Directive processor '{0}' has no 'Assembly' metadata + + Directive processor '{0}' has no 'Class' metadata + Loaded state file '{0}' - - Template parameter '{0}' has neither 'Value' metadata nor a value encoded in the name + + Template argument '{0}' has no 'Value' metadata T4 engine encountered a recoverable internal error. Although it should not affect output, please report it as it may affect reliability and performance. diff --git a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj index 7a7344c..ab1cf0b 100644 --- a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj +++ b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.csproj @@ -38,8 +38,19 @@ + + + PrepareResources;$(CompileDependsOn) + + - - + diff --git a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.props b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.props index 1d2a0a6..b67947a 100644 --- a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.props +++ b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.props @@ -5,10 +5,25 @@ - + False + + False + + + False + + + False + + + False + + + False + MSBuild:TransformAll diff --git a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.targets b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.targets index 55fc490..1316dba 100644 --- a/Mono.TextTemplating.Build/Mono.TextTemplating.Build.targets +++ b/Mono.TextTemplating.Build/Mono.TextTemplating.Build.targets @@ -27,18 +27,18 @@ + DependsOnTargets="_SetExplicitTransformProperties;TransformTemplatesCore" /> + DependsOnTargets="_SetTransformOnBuildProperties;TransformTemplatesCore" /> @@ -66,7 +66,7 @@ They run regardless of whether we have any items to transform, as they may want to inject items to be transformed. --> - (); 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"); buildState.TransformTemplates.Add (new TemplateBuildState.TransformTemplate { @@ -140,10 +144,7 @@ public override bool Execute () } //TODO - //IntermediateDirectory //RequiredAssemblies - //GeneratedTemplates - //PreprocessedTemplates //settings.Debug //settings.Log //metadata to override output name, class name and namespace @@ -167,16 +168,27 @@ bool AddParameters (TemplateBuildState buildState) foreach (var par in ParameterValues) { string paramName = par.ItemSpec; + string processorName, directiveName, paramVal; - string paramVal = par.GetMetadata ("Value"); - string processorName, directiveName; + if (TemplateGenerator.TryParseParameter (paramName, out processorName, out directiveName, out string parsedName, out paramVal)) { + paramName = parsedName; + } + + // metadata overrides encoded values. todo: warn when this happens? + if (par.TryGetMetadata ("Value", out string valueMetadata)) { + paramVal = valueMetadata; + } - if (!string.IsNullOrEmpty (paramVal)) { - processorName = par.GetMetadata ("Processor"); - directiveName = par.GetMetadata ("Directive"); + if (par.TryGetMetadata ("Processor", out string processorMetadata)) { + processorName = processorMetadata; } - else if (!TemplateGenerator.TryParseParameter (paramName, out processorName, out directiveName, out paramName, out paramVal)) { - Log.LogErrorFromResources (nameof(Messages.ParameterNoValue), par); + + if (par.TryGetMetadata ("Directive", out string directiveMetadata)) { + directiveName = directiveMetadata; + } + + if(paramVal is null) { + Log.LogWarningFromResources (nameof(Messages.ArgumentNoValue), par); success = false; continue; } @@ -205,44 +217,46 @@ bool AddDirectiveProcessors (TemplateBuildState buildState) foreach (var dirItem in DirectiveProcessors) { var name = dirItem.ItemSpec; - var className = dirItem.GetMetadata ("Class"); + string className = null, assembly = null; - if (className != null) { - var assembly = dirItem.GetMetadata ("Assembly") ?? dirItem.GetMetadata ("Codebase"); - if (string.IsNullOrEmpty (assembly)) { - Log.LogErrorFromResources (nameof(Messages.DirectiveProcessorNoAssembly), name); - hasErrors = true; + if (name.IndexOf('!') > -1) { + var split = name.Split ('!'); + if (split.Length != 3) { + Log.LogErrorFromResources (nameof(Messages.DirectiveProcessorDoesNotHaveThreeValues), name); + return false; } + //empty values for these are fine; they may get set through metadata + name = split[0]; + className = split[1]; + assembly = split[2]; + } - buildState.DirectiveProcessors.Add (new TemplateBuildState.DirectiveProcessor { - Name = name, - Class = className, - Assembly = assembly - }); - continue; + if (dirItem.TryGetMetadata ("Class", out string classMetadata)) { + className = classMetadata; + } + + if (dirItem.TryGetMetadata ("Codebase", out string codebaseMetadata)) { + assembly = codebaseMetadata; + } + + if (dirItem.TryGetMetadata ("Assembly", out string assemblyMetadata)) { + assembly = assemblyMetadata; } - var split = name.Split ('!'); - if (split.Length != 3) { - Log.LogErrorFromResources (nameof(Messages.DirectiveProcessorDoesNotHaveThreeValues), name); + if (string.IsNullOrEmpty (className)) { + Log.LogErrorFromResources (nameof(Messages.DirectiveProcessorNoClass), name); hasErrors = true; - continue; } - for (int i = 0; i < 3; i++) { - string s = split[i]; - if (string.IsNullOrEmpty (s)) { - string kind = i == 0 ? "name" : (i == 1 ? "class" : "assembly"); - Log.LogErrorFromResources (nameof(Messages.DirectiveProcessorMissingComponent), kind, name); - hasErrors = true; - continue; - } + if (string.IsNullOrEmpty (assembly)) { + Log.LogErrorFromResources (nameof(Messages.DirectiveProcessorNoAssembly), name); + hasErrors = true; } buildState.DirectiveProcessors.Add (new TemplateBuildState.DirectiveProcessor { - Name = split[0], - Class = split[1], - Assembly = split[2] + Name = name, + Class = className, + Assembly = assembly }); } @@ -297,4 +311,19 @@ void SaveBuildState (TemplateBuildState buildState, string filePath, MessagePack } } } + + static class TaskItemExtensions + { + public static bool TryGetMetadata (this ITaskItem item, string name, out string value) + { + var potentialValue = item.GetMetadata (name); + if (potentialValue?.Length > 0) { + value = potentialValue; + return true; + } + + value = null; + return false; + } + } } diff --git a/Mono.TextTemplating.Build/readme.md b/Mono.TextTemplating.Build/readme.md new file mode 100644 index 0000000..6a7304e --- /dev/null +++ b/Mono.TextTemplating.Build/readme.md @@ -0,0 +1,126 @@ +# T4.BuildTools + +`T4.BuildTools` is a set of MSBuild tasks and targets for for processing T4 templates, a general-purpose way to generate text or code files using C#. + +It's part of [Mono.TextTemplating](https://github.com/mono/t4), a modern open-source reimplementation of the Visual Studio T4 text templating engine. + +## Usage + +These targets introduce two new MSBuild item types: `T4Transform` and `T4Preprocess`. They are processed automatically during the build and when saving the template file in Visual Studio. + +> **NOTE**: These items are processed using the `Mono.TextTemplating` T4 engine and host. Host-specific templates will not have access to the Visual Studio T4 host. + +`T4Transform` items are transformed when saving the template in Visual Studio and when explicitly invoking the `TransformTemplate` MSBuild target. The `TransformOnBuild` property can be set to transform the templates on every build by calling this target automatically. The build is incremental by default, so a template will only be transformed if its input files are newer than its output. + +`T4Preprocess` items are preprocessed into a class that can be instantiated and executed from project code. The preprocessed class is generated in the intermediate output directory and included in the build automatically, similar to a source generator. + +### Customizing the Build + +Template transformation can be customized using a range of MSBuild properties, items and metadata. + +### Properties + +| Property | Description +| -- | -- +| `TransformOnBuild` | Set this to `True` to automatically transform `T4Transform` items on build. `T4Preprocess` items are transformed on build regardless of this setting unless legacy preprocessing is enabled. +| `T4DefaultNamespace` | Sets the namespace to be used when generating T4 classes. Defaults to the project's `$(RootNamespace)`. +| `TransformOutOfDateOnly` | Setting this to `False` will disable the incremental build and force all template to be re-transformed. + +### Items + +| Item | Description +| -- | -- +| `T4Argument` | Pass a parameter value to the T4 templates, optionally scoped to a directive processor and/or directive. This may use `Value` metadata, with optional `Processor` and/or `Directive` metadata, or it may encode the value and processor into the `Include` with the `=` or `!!!` formats used by the CLI `t4 -a` option. +| `DirectiveProcessor` | Register a custom directive processor by name. The `Class` and `Assembly` may be provided as metadata, or encoded into the `Include` with the `!!` format used by the CLI `t4 --dp` option. +| `T4ReferencePath` | Adds a search directory for resolving assembly references in `T4Transform` templates. Affects `<#@assembly#>` directives and calls to the host's `ResolveAssemblyReference(...)` method. +|`T4IncludePath` | Adds a search directory for resolving `<#@include#>` directives in `T4Transform` and `T4Preprocess` templates. For `T4Transform` items, this also affects calls to the host's `LoadIncludeText(...)` method. +| `T4AssemblyReference` | Additional assemblies to be referenced when processing `T4Transform` items. May be a absolute path, or relative to the project or the `T4ReferencePath` directories. + +Both `T4Argument` and `DirectiveProcessor` items support either setting metadata via MSBuild metadata or encoding it into the `Include` in the same format supported by the options of the `t4` CLI tool. If both encoded metadata and MSBuild metadata are present, the MSBuild metadata takes precedence. + +> **NOTE:** Encoded metadata is supported for convenience, and to support parsing `T4Argument` items from the `T4Arguments` property. However, using MSBuild metadata is preferred when possible as it allows simplified manipulation of the items. + +For example, the following `T4Argument` items are equivalent: + +```xml + + + + +``` + +As are the following `T4Argument` items: + +```xml + + + + + +``` + +Similarly, the following `DirectiveProcessor` items are equivalent: + +```xml + + + + +``` + +### Item Metadata + +`T4Transform` items have optional metadata that can be used to control the path used for the generated output. These can also be used for `T4Preprocess` items when using legacy preprocessing. + +| 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. +| `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 + +There also are several properties that are intended to be passed in when invoking the target on the CLI and should not be used in project/targets files: + +| Property | Description +| -- | -- +| `TransformFile` | Semicolon-separated list of template filenames. If `TransformFile` is set when invoking the `TextTransform` target explicitly, then only the templates specified by it will be transformed. +| `T4Arguments` | Semicolon-separated list of `T4Argument` items. + +For example: + +```bash +dotnet msbuild -t:TransformTemplates -p:TransformFile=Foo.cs -p:T4Arguments="Foo=1;Bar=2" +``` + +## Target Extensibility + +The `TransformTemplatesCore` target is provided as an extension points for custom targets that require running before or after template processing. This target runs when automatic or explicit transformation takes place, whereas the `TransformTemplates` target only runs when invoked explicitly. + +The outputs of the transformed and preprocessed templates are available as `GeneratedTemplateOutput` and `PreprocessedTemplateOutput` items respectively, and assemblies referenced by preprocessed templates are available as `T4RequiredAssemblies` items. + + These items are only available after the transform targets have run, so are not available in the MSBuild project at evaluation time. To access them in custom targets, use `AfterTargets="TransformTemplatesCore"` to order your target after template transformation. + +For example: + +```xml + + + + + +``` + +## Legacy Compatibility + +The following properties, items and metadata are provided for partial backwards compatibility with the Visual Studio [Microsoft.TextTemplating](https://learn.microsoft.com/en-us/visualstudio/modeling/code-generation-in-a-build-process) MSBuild targets. + +| Kind | Name | Description +| -- | -- |-- +| Item | `T4ParameterValues` | Equivalent to `T4Argument` item +| Property | `UseLegacyT4Preprocessing` | Place preprocessed templates beside the template files instead of dynamically injecting them into the build +| Property | `IncludeFolders` | Equivalent to `T4IncludePath` items +| Property | `PreprocessTemplateDefaultNamespace` | Equivalent to `T4DefaultNamespace` property +| Property | `AfterTransform` | List of targets to run before template transformation. Use `BeforeTargets="TransformTemplatesCore"` instead. +| Property | `AfterTransform` | List of targets to run after template transformation. Use `AfterTargets="TransformTemplatesCore"` instead. +| Metadata | `DirectiveProcessor.Codebase` | Equivalent to `Assembly` metadata +| Metadata | `T4Transform.OutputFilePath` | Equivalent to `OutputDirectory` metadata