Lottie-Windows/LottieGen/MSBuildTask/LottieGen.cs

414 строки
15 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace LottieGen.Task
{
/// <summary>
/// An MsBuild task for translating Lottie files to code.
/// </summary>
/// <remarks>
/// Ideally this would run LottieGen in process, however
/// MsBuild still runs on .NET Framework which is missing support
/// for some .NET 5 features needed by the LottieGen code.
/// So for now this is a wrapper around LottieGen.exe, which
/// can be packaged with this Task as a self-contained
/// executable.
/// When MsBuild updates to .NET 5, this task can contain the
/// LottieGen code and can call it in-process.
/// </remarks>
public sealed class LottieGen : Microsoft.Build.Utilities.ToolTask
{
readonly List<string> _outputFiles = new List<string>();
bool _nextEventIsOutputFile;
protected override string ToolName => "LottieGen.exe";
// WARNING: Nullable value types are not supported by MSBuild and will result in an error
// when passed into the task through an MSBuild project file.
/// <summary>
/// The Lottie file to process.
/// </summary>
[Required]
public string? InputFile { get; set; }
/// <summary>
/// cs, cppcx, or cppwinrt.
/// </summary>
[Required]
public string? Language { get; set; }
/// <summary>
/// Specifies additional interfaces that the generated code
/// will claim to implement.
/// </summary>
public ITaskItem[]? AdditionalInterface { get; set; }
/// <summary>
/// Disables optimization done by the code generator. This is
/// useful when the generated code is going to be hacked on.
/// </summary>
public bool DisableCodeGenOptimizer { get; set; }
/// <summary>
/// Disables optimization of the translation from Lottie to
/// Windows code.Mainly used to detect bugs in the optimizer.
/// </summary>
public bool DisableTranslationOptimizer { get; set; }
/// <summary>
/// Generates properties for each distinct color of fills and
/// strokes so that the colors in the animation can be modified
/// at runtime.
/// </summary>
public bool GenerateColorBindings { get; set; }
/// <summary>
/// Generates code that extends DependencyObject.This is useful
/// to allow XAML binding to properties in the Lottie source.
/// </summary>
public bool GenerateDependencyObject { get; set; }
/// <summary>
/// The lowest UAP version on which the result must run.Defaults
/// to 7. Must be 7 or higher. Code will be generated that will
/// run down to this version.If less than TargetUapVersion,
/// extra code will be generated if necessary to support the
/// lower versions.
/// </summary>
public uint MinimumUapVersion { get; set; }
/// <summary>
/// Specifies the namespace for the generated code. Defaults to
/// AnimatedVisuals.
/// </summary>
public string? Namespace { get; set; }
/// <summary>
/// Specifies the output folder for the generated files.If not
/// specified the files will be written to the current directory.
/// </summary>
[Required]
public string? OutputFolder { get; set; }
/// <summary>
/// Contains the items that were produced by the task.
/// </summary>
[Output]
public ITaskItem[] OutputFiles { get; private set; } = new ITaskItem[0];
/// <summary>
/// Makes the generated class public rather than internal. Ignored
/// for c++.
/// </summary>
public bool Public { get; set; }
/// <summary>
/// Cppwinrt only, specifies the root namespace of the consuming
/// project. Affects the names used to reference files generated
/// by cppwinrt.exe.
/// </summary>
public string? RootNamespace { get; set; }
/// <summary>
/// Fails on any parsing or translation issue. If not specified,
/// a best effort will be made to create valid output, and any
/// issues will be reported to STDOUT.
/// </summary>
public bool StrictMode { get; set; }
/// <summary>
/// The target UAP version on which the result will run.Must be 7
/// or higher and >= MinimumUapVersion.This value determines the
/// minimum SDK version required to compile the generated code.
/// If not specified, defaults to the latest UAP version.
/// </summary>
public uint TargetUapVersion { get; set; }
/// <summary>
/// Prevents any information from being included that could change
/// from run to run with the same inputs, for example tool version
/// numbers, file paths, and dates.This is designed to enable
/// testing of the tool by diffing the outputs.
/// </summary>
public bool TestMode { get; set; }
/// <summary>
/// Generates code for a particular WinUI version. Defaults to 2.4.
/// </summary>
public string? WinUIVersion { get; set; }
public override bool Execute()
{
var result = base.Execute();
if (result)
{
// Set the [Output]. We infer the output based on the
// language, the input file, and the output folder. This
// is currently a best guess and doesn't deal with the fact
// the input file might contain a wildcard, or that the namespace
// has been overridden, etc.
switch (GetNormalizedLanguage())
{
case Lang.CSharp:
OutputFiles = new ITaskItem[]
{
new TaskItem($"{Path.Combine(OutputFolder, InputFile)}.cs"),
};
break;
case Lang.Cppwinrt:
{
var outputFileBase = Path.Combine(OutputFolder, $"AnimatedVisuals.{InputFile}");
OutputFiles = new ITaskItem[]
{
new TaskItem($"{outputFileBase}.idl"),
new TaskItem($"{outputFileBase}.cpp"),
new TaskItem($"{outputFileBase}.h"),
};
}
break;
case Lang.Cx:
{
var outputFileBase = Path.Combine(OutputFolder, $"AnimatedVisuals.{InputFile}");
OutputFiles = new ITaskItem[]
{
new TaskItem($"{outputFileBase}.cpp"),
new TaskItem($"{outputFileBase}.h"),
};
}
break;
default:
throw new InvalidOperationException();
}
}
else
{
OutputFiles = new ITaskItem[0];
}
return result;
}
protected override string GenerateCommandLineCommands()
{
var args = new List<string>();
AddArg(nameof(InputFile), InputFile!);
AddArg(nameof(Language), Language!);
if (AdditionalInterface is not null)
{
foreach (var value in AdditionalInterface)
{
AddArg("additionalInterface", value.ItemSpec);
}
}
AddOptionalBool(nameof(DisableCodeGenOptimizer), DisableCodeGenOptimizer);
AddOptionalBool(nameof(DisableTranslationOptimizer), DisableTranslationOptimizer);
AddOptionalBool(nameof(GenerateColorBindings), GenerateColorBindings);
AddOptionalBool(nameof(GenerateDependencyObject), GenerateDependencyObject);
AddOptional(nameof(MinimumUapVersion), MinimumUapVersion > 0 ? MinimumUapVersion : null);
AddOptional(nameof(Namespace), Namespace);
AddArg(nameof(OutputFolder), OutputFolder!);
AddOptionalBool(nameof(Public), Public);
AddOptional(nameof(RootNamespace), RootNamespace);
AddOptionalBool(nameof(StrictMode), StrictMode);
AddOptional(nameof(TargetUapVersion), TargetUapVersion > 0 ? TargetUapVersion : null);
AddOptional(nameof(WinUIVersion), WinUIVersion);
var result = string.Join(" ", args);
return result;
void AddOptional(string parameterName, object? value)
{
if (value is not null)
{
AddArg(parameterName, value.ToString());
}
}
void AddOptionalBool(string parameterName, bool value)
{
if (value)
{
args.Add($"-{parameterName}");
}
}
void AddArg(string parameterName, string value)
{
args.Add($"-{parameterName}");
args.Add(EscapeCmdLineArg(value));
}
}
/// <summary>
/// Escapes a command-line argument to ensure that it can be passed through to another application without mangling.
/// </summary>
/// <remarks>
/// The string is wrapped in quotes to ensure that any spaces are considered part of the value. Backslashes and quotes
/// are escaped to ensure they are not interpreted as metacharacters.
/// </remarks>
/// <param name="arg">The argument to escape.</param>
/// <returns>The escaped argument.</returns>
string EscapeCmdLineArg(string arg)
{
// From MSDN "Everyone quotes command line arguments the wrong way"
StringBuilder result = new StringBuilder();
// Wrap the arg in quotes
result.Append('\"');
for (int i = 0; ; ++i)
{
int numBackslashes = 0;
while (i < arg.Length && arg[i] == '\\')
{
++i;
++numBackslashes;
}
if (i >= arg.Length)
{
// End of string.
// Doubling any trailing backslashes gives us an even number followed by our final double-quote.
// Cmdline will replace each pair with a single backslash (back to where we started), but treat
// the quote as a metacharacter and remove it.
result.Append('\\', numBackslashes * 2);
break;
}
else if (arg[i] == '"')
{
// Found a double-quote in the string.
// Doubling any preceding backslashes and adding one gives us an odd number followed by the double-quote.
// Cmdline will replace each pair with a single backslash and convert the ending \" to a literal quote.
result.Append('\\', (numBackslashes * 2) + 1);
result.Append(arg[i]);
}
else
{
// In all other cases, just pass through the characters as-is.
result.Append('\\', numBackslashes);
result.Append(arg[i]);
}
}
result.Append('\"');
return result.ToString();
}
// Provides the default path to the tool. Ignored if
// the <ToolPath/> property is set.
// By default we expect the tool to be in the same directory
// as the assembly that this class is in.
protected override string GenerateFullPathToTool()
=> Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), ToolName);
protected override bool ValidateParameters()
{
var hasErrors = false;
if (InputFile is null)
{
Log.LogError($"{nameof(InputFile)} not specified.");
hasErrors = true;
}
else
{
if (!File.Exists(InputFile))
{
Log.LogError($"{nameof(InputFile)} \"{InputFile}\" not found.");
}
}
switch (GetNormalizedLanguage())
{
case Lang.Cppwinrt:
case Lang.CSharp:
case Lang.Cx:
break;
default:
Log.LogError($"Unrecognized language \"{Language!}\".");
hasErrors = true;
break;
}
if (WinUIVersion is not null)
{
Version version;
if (!Version.TryParse(WinUIVersion, out version))
{
Log.LogError($"Invalid version string \"{WinUIVersion!}\".");
hasErrors = true;
}
}
return !hasErrors;
}
protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)
{
base.LogEventsFromTextOutput(singleLine, messageImportance);
if (_nextEventIsOutputFile)
{
_outputFiles.Add(singleLine.Trim());
}
// Read the line to determine what LottieGen is up to.
// This allows us to find out what files were written.
_nextEventIsOutputFile =
singleLine.StartsWith("CX header for class ") ||
singleLine.StartsWith("CX source for class ") ||
singleLine.StartsWith("C# code for class ") ||
singleLine.StartsWith("Cppwinrt header for class ") ||
singleLine.StartsWith("Cppwinrt source for class ") ||
singleLine.StartsWith("Cppwinrt IDL for class ");
}
Lang GetNormalizedLanguage()
{
var lang = Language?.ToLowerInvariant();
switch (lang)
{
case "cs":
return Lang.CSharp;
case "cppwinrt":
return Lang.Cppwinrt;
case "cx":
return Lang.Cx;
default:
return Lang.Unknown;
}
}
enum Lang
{
Unknown,
CSharp,
Cx,
Cppwinrt,
}
}
}