Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.DotNet.Build.Tasks.Templating
{
/// <summary>
/// <para>
/// Generates a new file at <see cref="OutputPath"/>.
/// </para>
/// <para>
/// The <see cref="TemplateFile"/> can define variables for substitution using <see cref="Properties"/>.
/// </para>
/// <example>
/// The input file might look like this:
/// <code>
/// 2 + 2 = ${Sum}
/// </code>
/// When the task is invoked like this, it will produce "2 + 2 = 4"
/// <code>
/// &lt;GenerateFileFromTemplate Properties="Sum=4;OtherValue=123;" ... &gt;
/// </code>
/// </example>
/// </summary>
public class GenerateFileFromTemplate : Task
{
/// <summary>
/// The template file using the variable syntax <c>${VarName}</c>.
/// If your template file needs to output this format, you can escape the dollar sign with a backtick e.g. <c>`${NotReplaced}</c>.
/// </summary>
[Required]
public string TemplateFile { get; set; }

/// <summary>
/// The destination for the generated file.
/// </summary>
[Required]
[Output]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "Output" is not correct, it's an input parameter. It's not returned by this task.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I think it may be more correct to set the value to the value escaped by https://github.com/aspnet/BuildTools/blame/main/src/Internal.AspNetCore.BuildTasks/GenerateFileFromTemplate.cs#L55.

We could probably also separate it out into a separate out parameter like ResolvedOutputPath or EscapedOutputPath but so far we haven't run into any issues in our builds due to this being an Output parameter so I would prefer if we don't block on this change and file followup issues to address.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path is never set by this code. Marking it output is incorrect, since it doesn't actually output anything. We should not be checking in new, incorrect, code.

public string OutputPath { get; set; }

/// <summary>
/// Key=Value pairs of values, separated by semicolons e.g. <c>Properties="Sum=4;OtherValue=123;"</c>.
/// </summary>
[Required]
public string[] Properties { get; set; }

public override bool Execute()
{
string outputPath = Path.GetFullPath(OutputPath.Replace('\\', '/'));

if (!File.Exists(TemplateFile))
{
Log.LogError($"File {TemplateFile} does not exist");
return false;
}

IDictionary<string, string> values = MSBuildListSplitter.GetNamedProperties(Properties, Log);
string template = File.ReadAllText(TemplateFile);

string result = Replace(template, values);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
File.WriteAllText(outputPath, result);

return !Log.HasLoggedErrors;
}

public string Replace(string template, IDictionary<string, string> values)
{
StringBuilder sb = new();
Comment thread
JunTaoLuo marked this conversation as resolved.
StringBuilder varNameSb = new();
int line = 1;
for (int i = 0; i < template.Length; i++)
{
char templateChar = template[i];
char nextTemplateChar = i + 1 >= template.Length
? '\0'
: template[i + 1];
Comment thread
JunTaoLuo marked this conversation as resolved.

// count lines in the template file
if (templateChar == '\n')
{
line++;
}

if (templateChar == '`' && (nextTemplateChar == '$' || nextTemplateChar == '`'))
{
// skip the backtick for known escape characters
i++;
sb.Append(nextTemplateChar);
continue;
}

if (templateChar != '$' || nextTemplateChar != '{')
{
// variables begin with ${. Moving on.
sb.Append(templateChar);
continue;
}

varNameSb.Clear();
i += 2;
for (; i < template.Length; i++)
{
templateChar = template[i];
if (templateChar != '}')
{
varNameSb.Append(templateChar);
}
else
{
// Found the end of the variable substitution
string varName = varNameSb.ToString();
if (values.TryGetValue(varName, out string value))
{
sb.Append(value);
}
else
{
Log.LogWarning(null, null, null, TemplateFile,
line, 0, 0, 0,
message: $"No property value is available for '{varName}'");
Comment thread
JunTaoLuo marked this conversation as resolved.
}

varNameSb.Clear();
break;
}
}

if (varNameSb.Length > 0)
{
Log.LogWarning(null, null, null, TemplateFile,
line, 0, 0, 0,
message: "Expected closing bracket for variable placeholder. No substitution will be made.");
sb.Append("${").Append(varNameSb.ToString());
}
}

return sb.ToString();
}
}
}

43 changes: 43 additions & 0 deletions src/Microsoft.DotNet.Build.Tasks.Templating/MSBuildListSplitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.Build.Utilities;

namespace Microsoft.DotNet.Build.Tasks.Templating
{
internal static class MSBuildListSplitter
{
public static IDictionary<string, string> GetNamedProperties(string[] input, TaskLoggingHelper log)
{
Dictionary<string, string> values = new(StringComparer.OrdinalIgnoreCase);
if (input == null)
{
return values;
}

foreach (string item in input)
{
int splitIdx = item.IndexOf('=');
if (splitIdx < 0)
{
log.LogWarning($"Property: {item} does not have a valid '=' separator");
continue;
Comment thread
JunTaoLuo marked this conversation as resolved.
}

string key = item.Substring(0, splitIdx).Trim();
if (string.IsNullOrEmpty(key))
{
log.LogWarning($"Property: {item} does not have a valid property name");
continue;
}
Comment thread
dougbu marked this conversation as resolved.

string value = item.Substring(splitIdx + 1);
values[key] = value;
}

return values;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. -->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/fyi @markwilkie four projects in this repo use a different header:

<!-- Copyright (c)  Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information. -->

They should probably be made consistent.

<Project Sdk="Microsoft.NET.Sdk">
Comment thread
JunTaoLuo marked this conversation as resolved.

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
Comment thread
JunTaoLuo marked this conversation as resolved.
<Description>Templating task package</Description>
<PackageTags>Arcade Build Tool Templating</PackageTags>
<IncludeSymbols>false</IncludeSymbols>
<IncludeSource>false</IncludeSource>
<IsPackable>true</IsPackable>
<BuildOutputTargetFolder>tools\</BuildOutputTargetFolder>
<DevelopmentDependency>true</DevelopmentDependency>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<EnableDefaultNoneItems>false</EnableDefaultNoneItems>
</PropertyGroup>

<ItemGroup>
<None Include="build/**/*.*" Pack="true">
<PackagePath>build</PackagePath>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkVersion)" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCoreVersion)" />
</ItemGroup>

<Import Project="$(RepoRoot)eng\BuildTask.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. -->
<Project>

<PropertyGroup>
<MicrosoftDotNetBuildTasksTemplatingAssembly>$(MSBuildThisFileDirectory)..\tools\netstandard2.0\Microsoft.DotNet.Build.Tasks.Templating.dll</MicrosoftDotNetBuildTasksTemplatingAssembly>
</PropertyGroup>

<UsingTask TaskName="Microsoft.DotNet.Build.Tasks.Templating.GenerateFileFromTemplate" AssemblyFile="$(MicrosoftDotNetBuildTasksTemplatingAssembly)" />

</Project>