/// <summary> /// Creates new or appends to existing output file. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that describes content generated by a template. /// </param> /// <param name="content"> /// A <see cref="String"/> that contains content generated by a template. /// </param> /// <param name="host"> /// An <see cref="ITextTemplatingEngineHost"/> object hosting the <paramref name="transformation"/>. /// </param> /// <param name="transformation"> /// <see cref="TextTransformation"/> object generated by T4 based on top-level .tt file. /// </param> /// <remarks> /// Multiple outputs can be combined in a single output file during code /// generation. This allows user to customize a composite code generator /// to generate output with required granularity without having to modify /// the generator itself. /// </remarks> public void Append(OutputInfo output, string content, ITextTemplatingEngineHost host, TextTransformation transformation) { if (output == null) { throw new ArgumentNullException("output"); } if (host == null) { throw new ArgumentNullException("host"); } string previousDirectory = Environment.CurrentDirectory; Environment.CurrentDirectory = Path.GetDirectoryName(host.TemplateFile); try { Validate(output); if (string.IsNullOrEmpty(output.File)) { this.AppendToStandardOutput(output, content, host, transformation); } else { this.AppendToOutputFile(output, content, host); } } finally { Environment.CurrentDirectory = previousDirectory; } }
/// <summary> /// Determines absolute path to the output <see cref="OutputInfo.File"/>. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that describes content generated by a template. /// </param> /// <param name="host"> /// An <see cref="ITextTemplatingEngineHost"/> object hosting the <see cref="TextTransformation"/>. /// </param> /// <returns> /// If <see cref="OutputInfo.File"/> contains absolute path, returns its value. Otherwise, /// if <see cref="OutputInfo.Project"/> is specified, resolves relative <see cref="OutputInfo.File"/> /// path based on the location of the project, otherwise resolves the path based on the location /// of the <see cref="ITextTemplatingEngineHost.TemplateFile"/>. /// </returns> private static string GetFullFilePath(OutputInfo output, ITextTemplatingEngineHost host) { // Does file contain absolute path already? if (Path.IsPathRooted(output.File)) { return(output.File); } // Resolve relative path string baseFile = string.IsNullOrEmpty(output.Project) ? host.TemplateFile : output.Project; string baseDirectory = Path.GetDirectoryName(baseFile); string fullPath = Path.Combine(baseDirectory, output.File); return(Path.GetFullPath(fullPath)); }
/// <summary> /// Finds project item collection for the output file in Visual Studio solution. /// </summary> /// <param name="output"> /// An <see cref="OutputFile"/> that needs to be added to the solution. /// </param> /// <param name="projects"> /// All <see cref="Project"/>s in the current <see cref="Solution"/>. /// </param> /// <param name="template"> /// A <see cref="ProjectItem"/> that represents T4 template being transformed. /// </param> /// <returns>A <see cref="ProjectItems"/> collection where the generated file should be added.</returns> private static ProjectItems FindProjectItemCollection(OutputFile output, IEnumerable <Project> projects, ProjectItem template) { ProjectItems collection; // collection to which output file needs to be added string relativePath; // path from the collection to the file string basePath; // absolute path to the directory to which an item is being added if (!string.IsNullOrEmpty(output.Project)) { // If output file needs to be added to another project Project project = projects.First(p => OutputInfo.SamePath(GetPath(p), output.Project)); collection = project.ProjectItems; relativePath = GetRelativePath(GetPath(project), output.File); basePath = Path.GetDirectoryName(GetPath(project)); } else if (output.PreserveExistingFile || !OutputInfo.SamePath(Path.GetDirectoryName(output.File), Environment.CurrentDirectory)) { // If output file needs to be added to another folder of the current project collection = template.ContainingProject.ProjectItems; relativePath = GetRelativePath(GetPath(template.ContainingProject), output.File); basePath = Path.GetDirectoryName(GetPath(template.ContainingProject)); } else { // Add the output file to the list of children of the template file collection = template.ProjectItems; relativePath = GetRelativePath(template.get_FileNames(1), output.File); basePath = Path.GetDirectoryName(template.get_FileNames(1)); } // make sure that all folders in the file path exist in the project. if (relativePath.StartsWith("." + Path.DirectorySeparatorChar, StringComparison.Ordinal)) { // Remove leading .\ from the path relativePath = relativePath.Substring(relativePath.IndexOf(Path.DirectorySeparatorChar) + 1); while (relativePath.Contains(Path.DirectorySeparatorChar)) { string folderName = relativePath.Substring(0, relativePath.IndexOf(Path.DirectorySeparatorChar)); ProjectItem folder = AddFolder(collection, folderName, basePath); collection = folder.ProjectItems; relativePath = relativePath.Substring(folderName.Length + 1); basePath = Path.Combine(basePath, folderName); } } return(collection); }
/// <summary> /// Validates that <paramref name="output"/> properties are consistent /// with properties of the <paramref name="previousOutput"/>. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that represents the generated output content. /// </param> /// <param name="previousOutput"> /// An <see cref="OutputInfo"/> object that represents previously generated output content. /// </param> private static void Validate(OutputInfo output, OutputInfo previousOutput) { if (!OutputInfo.SamePath(previousOutput.Project, GetFullProjectPath(output))) { throw new TransformationException("Project name doesn't match previously generated value"); } if (previousOutput.Encoding != output.Encoding) { throw new TransformationException("Encoding doesn't match previously generated value"); } if (previousOutput.BuildAction != output.BuildAction) { throw new TransformationException("Build action doesn't match previously generated value"); } if (previousOutput.CopyToOutputDirectory != output.CopyToOutputDirectory) { throw new TransformationException("CopyToOutputDirectory doesn't match previously generated value"); } if (previousOutput.CustomTool != output.CustomTool) { throw new TransformationException("CustomTool doesn't match previously generated value"); } if (previousOutput.CustomToolNamespace != output.CustomToolNamespace) { throw new TransformationException("CustomToolNamespace doesn't match previously generated value"); } foreach (KeyValuePair <string, string> buildProperty in previousOutput.BuildProperties) { string previousValue; bool propertyExists = output.BuildProperties.TryGetValue(buildProperty.Key, out previousValue); if (propertyExists && buildProperty.Value != previousValue) { throw new TransformationException("Build property doesn't match previously generated value"); } } if (previousOutput.PreserveExistingFile != output.PreserveExistingFile) { throw new TransformationException("PreserveExistingFile doesn't match previously generated value"); } }
/// <summary> /// This method is a part of T4 Toolbox infrastructure. Don't call it in your code. /// </summary> /// <param name="content"> /// Generated content. /// </param> /// <param name="output"> /// An <see cref="OutputInfo"/> object that specifies how the content must be saved. /// </param> /// <param name="errors"> /// A collection of <see cref="CompilerError"/> objects that represent errors and warnings /// that occurred while generating this content. /// </param> internal static void Render(string content, OutputInfo output, CompilerErrorCollection errors) { //if (output.PreserveExistingFile == true) //{ // output.PreserveExistingFile = false; // CompilerError error = new CompilerError(); // error.IsWarning = true; // // PreserveExistingFile depends on where it is put (ie before Render) - not atomic. Too risky. // // this.PreserveExistingFile = true in a Template would be safer. // // But I prefer explict method, as it is quite clear. // error.ErrorText = "PreserveExistingFile is deprecated. Always use before Render() in Generator, or in template itself, or use RenderToFileIfNotExists(filename) instead."; // error.FileName = Host.TemplateFile; // errors.Add(error); //} TransformationContext.ReportErrors(errors); TransformationContext.outputManager.Append(output, content, Host, Transformation); }
/// <summary> /// Performs validation tasks that require accessing Visual Studio automation model. /// </summary> /// <param name="outputFiles"> /// <see cref="OutputFile"/>s that need to be added to the solution. /// </param> /// <param name="projects"> /// All <see cref="Project"/>s in the current solution. /// </param> /// <remarks> /// Most of the output validation is done on the fly, by the <see cref="OutputManager.Append"/> /// method. This method performs the remaining validations that access Visual /// Studio automation model and cannot cross <see cref="bAppDomain"/> boundries. /// </remarks> private static void Validate(IEnumerable <OutputFile> outputFiles, IEnumerable <Project> projects) { foreach (OutputFile outputFile in outputFiles) { if (string.IsNullOrEmpty(outputFile.Project)) { continue; } // Make sure that project is included in the solution bool projectInSolution = projects.Any(p => OutputInfo.SamePath(GetPath(p), outputFile.Project)); if (!projectInSolution) { throw new TransformationException( string.Format(CultureInfo.CurrentCulture, "Target project {0} does not belong to the solution", outputFile.Project)); } } }
/// <summary> /// Appends generated <paramref name="content"/> to standard output of the <paramref name="transformation"/>. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that describes content generated by a template. /// </param> /// <param name="content"> /// A <see cref="String"/> that contains content generated by a template. /// </param> /// <param name="host"> /// An <see cref="ITextTemplatingEngineHost"/> object hosting the <paramref name="transformation"/>. /// </param> /// <param name="transformation"> /// <see cref="TextTransformation"/> object generated by T4 based on top-level .tt file. /// </param> private void AppendToStandardOutput(OutputInfo output, string content, ITextTemplatingEngineHost host, TextTransformation transformation) { // If some content was already written to the standard output if (this.standardOutput != null) { Validate(output, this.standardOutput); } transformation.Write(content); host.SetOutputEncoding(output.Encoding, false); if (this.standardOutput == null) { this.standardOutput = new OutputInfo(); this.standardOutput.Project = GetFullProjectPath(output); this.standardOutput.Encoding = output.Encoding; } this.standardOutput.AppendReferences(output.References); }
/// <summary> /// Appends generated <paramref name="content"/> to the specified <see cref="OutputInfo.File"/>. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that describes content generated by a template. /// </param> /// <param name="content"> /// A <see cref="String"/> that contains content generated by a template. /// </param> /// <param name="host"> /// An <see cref="ITextTemplatingEngineHost"/> object hosting the transformation. /// </param> private void AppendToOutputFile(OutputInfo output, string content, ITextTemplatingEngineHost host) { // If some content was already generated for this file string filePath = GetFullFilePath(output, host); OutputFile outputFile = this.outputFiles.FirstOrDefault(o => OutputInfo.SamePath(o.File, filePath)); if (outputFile != null) { // Verify that output properties match Validate(output, outputFile); } else { // Otherwise, create a new output container outputFile = new OutputFile(); outputFile.File = filePath; outputFile.Project = GetFullProjectPath(output); outputFile.Encoding = output.Encoding; outputFile.BuildAction = output.BuildAction; outputFile.CustomTool = output.CustomTool; outputFile.CustomToolNamespace = output.CustomToolNamespace; outputFile.CopyToOutputDirectory = output.CopyToOutputDirectory; outputFile.PreserveExistingFile = output.PreserveExistingFile; this.outputFiles.Add(outputFile); } outputFile.Content.Append(content); outputFile.AppendBuildProperties(output.BuildProperties); outputFile.AppendReferences(output.References); }
/// <summary> /// Validates that <paramref name="output"/> properties are consistent /// with properties of the <paramref name="previousOutput"/>. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that represents the generated output content. /// </param> /// <param name="previousOutput"> /// An <see cref="OutputInfo"/> object that represents previously generated output content. /// </param> private static void Validate(OutputInfo output, OutputInfo previousOutput) { if (!OutputInfo.SamePath(previousOutput.Project, GetFullProjectPath(output))) { throw new TransformationException("Project name doesn't match previously generated value"); } if (previousOutput.Encoding != output.Encoding) { throw new TransformationException("Encoding doesn't match previously generated value"); } if (previousOutput.BuildAction != output.BuildAction) { throw new TransformationException("Build action doesn't match previously generated value"); } if (previousOutput.CopyToOutputDirectory != output.CopyToOutputDirectory) { throw new TransformationException("CopyToOutputDirectory doesn't match previously generated value"); } if (previousOutput.CustomTool != output.CustomTool) { throw new TransformationException("CustomTool doesn't match previously generated value"); } if (previousOutput.CustomToolNamespace != output.CustomToolNamespace) { throw new TransformationException("CustomToolNamespace doesn't match previously generated value"); } foreach (KeyValuePair<string, string> buildProperty in previousOutput.BuildProperties) { string previousValue; bool propertyExists = output.BuildProperties.TryGetValue(buildProperty.Key, out previousValue); if (propertyExists && buildProperty.Value != previousValue) { throw new TransformationException("Build property doesn't match previously generated value"); } } if (previousOutput.PreserveExistingFile != output.PreserveExistingFile) { throw new TransformationException("PreserveExistingFile doesn't match previously generated value"); } }
/// <summary> /// Determines absolute path to the <see cref="OutputInfo.Project"/> file. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that describes content generated by a template. /// </param> /// <returns> /// An absolute path to the <see cref="OutputInfo.Project"/>, if specified. Otherwise, returns /// an empty string. /// </returns> private static string GetFullProjectPath(OutputInfo output) { if (string.IsNullOrEmpty(output.Project)) { return string.Empty; } return Path.GetFullPath(output.Project); }
/// <summary> /// Determines absolute path to the output <see cref="OutputInfo.File"/>. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that describes content generated by a template. /// </param> /// <param name="host"> /// An <see cref="ITextTemplatingEngineHost"/> object hosting the <see cref="TextTransformation"/>. /// </param> /// <returns> /// If <see cref="OutputInfo.File"/> contains absolute path, returns its value. Otherwise, /// if <see cref="OutputInfo.Project"/> is specified, resolves relative <see cref="OutputInfo.File"/> /// path based on the location of the project, otherwise resolves the path based on the location /// of the <see cref="ITextTemplatingEngineHost.TemplateFile"/>. /// </returns> private static string GetFullFilePath(OutputInfo output, ITextTemplatingEngineHost host) { // Does file contain absolute path already? if (Path.IsPathRooted(output.File)) { return output.File; } // Resolve relative path string baseFile = string.IsNullOrEmpty(output.Project) ? host.TemplateFile : output.Project; string baseDirectory = Path.GetDirectoryName(baseFile); string fullPath = Path.Combine(baseDirectory, output.File); return Path.GetFullPath(fullPath); }
/// <summary> /// This method is a part of T4 Toolbox infrastructure. Don't call it in your code. /// </summary> /// <param name="content"> /// Generated content. /// </param> /// <param name="output"> /// An <see cref="OutputInfo"/> object that specifies how the content must be saved. /// </param> /// <param name="errors"> /// A collection of <see cref="CompilerError"/> objects that represent errors and warnings /// that occurred while generating this content. /// </param> internal static void Render(string content, OutputInfo output, CompilerErrorCollection errors) { TransformationContext.ReportErrors(errors); TransformationContext.outputManager.Append(output, content, Host, Transformation); }
/// <summary> /// Deletes output files that were not generated by the current session. /// </summary> /// <param name="outputFiles"> /// A read-only collection of <see cref="OutputFile"/> objects. /// </param> /// <param name="solution"> /// Current Visual Studio <see cref="Solution"/>. /// </param> /// <param name="template"> /// A <see cref="ProjectItem"/> that represents T4 template being transformed. /// </param> private static void DeleteOldOutputs(IEnumerable <OutputFile> outputFiles, Solution solution, ProjectItem template) { // If previous transformation produced a log of generated ouptut files string logFile = GetLogFileName(); if (File.Exists(logFile)) { // Delete all files recorded in the log that were not regenerated foreach (string line in File.ReadAllLines(logFile)) { string relativePath = line.Trim(); // Skip blank lines if (relativePath.Length == 0) { continue; } // Skip comments if (relativePath.StartsWith("//", StringComparison.OrdinalIgnoreCase)) { continue; } string absolutePath = Path.GetFullPath(relativePath); // Skip the file if it was regenerated during current transformation if (outputFiles.Any(output => OutputInfo.SamePath(output.File, absolutePath))) { continue; } // The file wasn't regenerated, delete it from the solution, source control and file storage ProjectItem projectItem = solution.FindProjectItem(absolutePath); if (projectItem != null) { DeleteProjectItem(projectItem); } } } // Also delete all project items nested under template if they weren't regenerated string templateFileName = Path.GetFileNameWithoutExtension(Host.TemplateFile); foreach (ProjectItem childProjectItem in template.ProjectItems) { // Skip the file if it has the same name as the template file. This will prevent constant // deletion and adding of the main output file to the project, which is slow and may require // the user to check the file out unnecessarily. if (templateFileName == Path.GetFileNameWithoutExtension(childProjectItem.Name)) { continue; } // If the file wan't regenerated, delete it from the the solution, source control and file storage if (!outputFiles.Any(o => OutputInfo.SamePath(o.File, childProjectItem.get_FileNames(1)))) { childProjectItem.Delete(); } } }
/// <summary> /// Validates <paramref name="output"/> properties. /// </summary> /// <param name="output"> /// An <see cref="OutputInfo"/> object that represents the generated output content. /// </param> private static void Validate(OutputInfo output) { // If content needs to be written to the standard output of the transformation if (string.IsNullOrEmpty(output.File)) { if (!string.IsNullOrEmpty(output.Project)) { throw new TransformationException("Cannot move standard transformation output file"); } if (!string.IsNullOrEmpty(output.BuildAction)) { throw new TransformationException("Cannot specify BuildAction for standard template output."); } if (output.CopyToOutputDirectory != CopyToOutputDirectory.DoNotCopy) { throw new TransformationException("Cannot specify CopyToOutputDirectory for standard template output."); } if (output.CustomTool != null) { throw new TransformationException("Cannot specify CustomTool for standard template output."); } if (output.CustomToolNamespace != null) { throw new TransformationException("Cannot specify CustomToolNamespace for standard template output."); } if (output.BuildProperties.Count > 0) { throw new TransformationException("Cannot specify BuildProperties for standard template output."); } if (output.PreserveExistingFile == true) { throw new TransformationException("PreserveExistingFile cannot be set for standard template output"); } } // If content needs to be written to another project if (!string.IsNullOrEmpty(output.Project)) { // Make sure the project file exists string projectPath = GetFullProjectPath(output); if (!File.Exists(projectPath)) { throw new TransformationException( string.Format(CultureInfo.CurrentCulture, "Target project {0} does not exist", projectPath)); } } }