#nullable disable // The adapter is compiled by the SDK; the value protocol is shared with the worker. // Only this source adapter enters the persistent build node. The real task DLL // and its dependencies are loaded by a single-use dotnet process. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text; using AtomUI.Build.Tasks.Isolation; using Microsoft.Build.Framework; #if !ATOMUI_BUILD_TASK_HOST using Microsoft.Build.Utilities; #endif namespace AtomUI.Build.Tasks.Isolation { // The protocol contains values only. No task-defined types or MSBuild runtime // objects cross the process boundary, including item implementations. [DataContract] public sealed class TaskRequest { [DataMember] public string TaskName { get; set; } [DataMember] public string ProjectFile; [DataMember] public int Line; [DataMember] public int Column; [DataMember] public Dictionary Properties = new Dictionary(); [DataMember] public Dictionary Items = new Dictionary(); } [DataContract] public sealed class TaskResponse { [DataMember] public bool Success; [DataMember] public Dictionary Properties = new Dictionary(); [DataMember] public Dictionary Items = new Dictionary(); [DataMember] public List Diagnostics = new List(); } [DataContract] public sealed class TaskWireItem : ITaskItem { [DataMember] public string ItemSpec { get; set; } [DataMember] public string FullPath; [DataMember] public Dictionary Metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); public int MetadataCount { get { return Metadata.Count; } } public ICollection MetadataNames { get { return Metadata.Keys.ToArray(); } } public string GetMetadata(string name) { string value; if (Metadata.TryGetValue(name, out value)) return value; return name.Equals("FullPath", StringComparison.OrdinalIgnoreCase) ? FullPath ?? string.Empty : string.Empty; } public void SetMetadata(string name, string value) { Metadata[name] = value; } public void RemoveMetadata(string name) { Metadata.Remove(name); } public IDictionary CloneCustomMetadata() { return new Dictionary(Metadata, StringComparer.OrdinalIgnoreCase); } public void CopyMetadataTo(ITaskItem destination) { foreach (var pair in Metadata) if (string.IsNullOrEmpty(destination.GetMetadata(pair.Key))) { var literalDestination = destination as ITaskItem2; if (literalDestination != null) literalDestination.SetMetadataValueLiteral(pair.Key, pair.Value); else destination.SetMetadata(pair.Key, pair.Value); } } [OnDeserialized] private void OnDeserialized(StreamingContext context) { Metadata = new Dictionary(Metadata, StringComparer.OrdinalIgnoreCase); } public static TaskWireItem FromItem(ITaskItem item) { var result = new TaskWireItem { ItemSpec = item.ItemSpec, FullPath = item.GetMetadata("FullPath") }; foreach (DictionaryEntry entry in item.CloneCustomMetadata()) result.Metadata.Add((string)entry.Key, (string)entry.Value); return result; } } [DataContract] public sealed class TaskDiagnostic { [DataMember] public string Kind; [DataMember] public string Message; [DataMember] public string Subcategory; [DataMember] public string Code; [DataMember] public string HelpKeyword; [DataMember] public string File; [DataMember] public int Line; [DataMember] public int Column; [DataMember] public int EndLine; [DataMember] public int EndColumn; [DataMember] public int Importance; } public static class TaskWire { public static void Write(string path, T value) { using (var stream = File.Create(path)) Serializer().WriteObject(stream, value); } public static T Read(string json) { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) return (T)Serializer().ReadObject(stream); } public static void WriteResponse(TaskResponse response) { using (var stream = Console.OpenStandardOutput()) Serializer().WriteObject(stream, response); } private static DataContractJsonSerializer Serializer() { return new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); } } } #if !ATOMUI_BUILD_TASK_HOST namespace AtomUI.Build.Tasks { public abstract class IsolatedBuildTask : Task, ICancelableTask { [Required] public string TaskAssembly { get; set; } public string DotNetPath { get; set; } private readonly object processGate = new object(); private Process process; private bool cancelled; public void Cancel() { lock (processGate) { cancelled = true; if (process != null && !process.HasExited) process.Kill(); } } public override bool Execute() { string directory = Path.Combine(Path.GetTempPath(), "atomui-build-task-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(directory); try { return ExecuteProcess(Path.Combine(directory, "request.json")); } catch (Exception error) { Log.LogErrorFromException(error, true); return false; } finally { lock (processGate) { if (process != null) { // A failed read or cancellation must not let a worker outlive // the invocation and keep the toolset locked in the background. if (!process.HasExited) { process.Kill(); process.WaitForExit(); } process.Dispose(); process = null; } } Directory.Delete(directory, true); } } private bool ExecuteProcess(string projectFile) { var parameters = GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.DeclaringType == GetType()).ToArray(); var outputs = parameters.Where(p => p.IsDefined(typeof(OutputAttribute), true)).ToArray(); var request = new TaskRequest { TaskName = GetType().Name, ProjectFile = BuildEngine.ProjectFileOfTaskNode, Line = BuildEngine.LineNumberOfTaskNode, Column = BuildEngine.ColumnNumberOfTaskNode }; foreach (var parameter in parameters.Except(outputs)) { object value = parameter.GetValue(this, null); if (value == null) continue; if (parameter.PropertyType == typeof(ITaskItem[])) request.Items.Add(parameter.Name, ((ITaskItem[])value).Select(TaskWireItem.FromItem).ToArray()); else request.Properties.Add(parameter.Name, Convert.ToString(value, CultureInfo.InvariantCulture)); } TaskWire.Write(projectFile, request); string arguments = Quote(Path.GetFullPath(TaskAssembly)) + " " + Quote(projectFile); var start = new ProcessStartInfo { FileName = string.IsNullOrWhiteSpace(DotNetPath) ? "dotnet" : DotNetPath, Arguments = arguments, WorkingDirectory = Directory.GetCurrentDirectory(), UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; lock (processGate) { if (cancelled) return false; process = Process.Start(start); } var standardOutput = process.StandardOutput.ReadToEndAsync(); var standardError = process.StandardError.ReadToEndAsync(); process.WaitForExit(); string outputText = standardOutput.GetAwaiter().GetResult(); string errorText = standardError.GetAwaiter().GetResult(); foreach (string line in errorText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) Log.LogMessageFromText(line, MessageImportance.High); if (process.ExitCode != 0) { Log.LogError("AtomUI build task {0} exited with code {1}. {2}", GetType().Name, process.ExitCode, outputText); return false; } TaskResponse result = TaskWire.Read(outputText); foreach (TaskDiagnostic diagnostic in result.Diagnostics) { if (diagnostic.Kind == "error") Log.LogError(diagnostic.Subcategory, diagnostic.Code, diagnostic.HelpKeyword, diagnostic.File, diagnostic.Line, diagnostic.Column, diagnostic.EndLine, diagnostic.EndColumn, diagnostic.Message); else if (diagnostic.Kind == "warning") Log.LogWarning(diagnostic.Subcategory, diagnostic.Code, diagnostic.HelpKeyword, diagnostic.File, diagnostic.Line, diagnostic.Column, diagnostic.EndLine, diagnostic.EndColumn, diagnostic.Message); else Log.LogMessage((MessageImportance)diagnostic.Importance, "{0}", diagnostic.Message); } if (!result.Success) return false; foreach (var output in outputs) { if (output.PropertyType == typeof(ITaskItem[])) { // Keep the original task's literal ITaskItem semantics, including // custom metadata named Identity. TaskItem would reinterpret both. output.SetValue(this, result.Items[output.Name].Cast().ToArray(), null); } else output.SetValue(this, Convert.ChangeType(result.Properties[output.Name], output.PropertyType, CultureInfo.InvariantCulture), null); } return !Log.HasLoggedErrors; } private static string Quote(string path) { if (path.IndexOf('"') >= 0) throw new ArgumentException("A process path cannot contain a quote.", "path"); return "\"" + path + "\""; } } } namespace AtomUI.Build.Tasks { public sealed class ValidateRegistrationToolchainTask : IsolatedBuildTask { public string LinkerAssembly { get; set; } = string.Empty; public string NativeCompiler { get; set; } = string.Empty; } public sealed class PrepareTypeMapLinkTask : IsolatedBuildTask { public string SdkVersion { get; set; } = string.Empty; public string WasmSdkTasksPath { get; set; } = string.Empty; public string BackendAssembly { get; set; } = string.Empty; public string LinkerAssembly { get; set; } = string.Empty; public string Capability { get; set; } = string.Empty; public string ReceiptPath { get; set; } = string.Empty; public string SignaturePath { get; set; } = string.Empty; public string LinkSemaphore { get; set; } = string.Empty; public string OutputDirectory { get; set; } = string.Empty; public string Configuration { get; set; } = string.Empty; public ITaskItem[] Inputs { get; set; } = Array.Empty(); public ITaskItem[] Assemblies { get; set; } = Array.Empty(); public ITaskItem[] LinkOptions { get; set; } = Array.Empty(); public ITaskItem[] LinkItems { get; set; } = Array.Empty(); } public sealed class VerifyTypeMapReceiptTask : IsolatedBuildTask { public string ReceiptPath { get; set; } = string.Empty; public string SignaturePath { get; set; } = string.Empty; public bool BindReceipt { get; set; } public bool RequireConsumedAssemblies { get; set; } public ITaskItem[] ConsumedAssemblies { get; set; } = Array.Empty(); } public sealed class ResolveRegistrationToolsTask : IsolatedBuildTask { public ITaskItem[] Candidates { get; set; } = Array.Empty(); public bool IncludeBackend { get; set; } [Output] public string GeneratorAssembly { get; set; } = string.Empty; [Output] public string BackendAssembly { get; set; } = string.Empty; } public sealed class GenerateThemeAssetWrappersTask : IsolatedBuildTask { [Required] public ITaskItem[] ThemeAssets { get; set; } = new ITaskItem[0]; [Required] public string OutputDirectory { get; set; } = string.Empty; [Required] public string AssemblyName { get; set; } = string.Empty; [Required] public string GeneratedCodePath { get; set; } = string.Empty; [Output] public ITaskItem[] GeneratedAssets { get; set; } = new ITaskItem[0]; } public sealed class ExportLanguageTemplatesTask : IsolatedBuildTask { [Required] public ITaskItem[] SourceFiles { get; set; } = Array.Empty(); [Required] public string TargetLanguage { get; set; } = string.Empty; public string OutputRootDirectory { get; set; } [Output] public ITaskItem[] ExportedFiles { get; set; } = Array.Empty(); } public sealed class PrepareLanguagePackageAssetsTask : IsolatedBuildTask { [Required] public string PackageId { get; set; } = string.Empty; [Required] public ITaskItem[] LanguageFiles { get; set; } = Array.Empty(); public ITaskItem[] SourceLanguageFiles { get; set; } = Array.Empty(); public string ExpectedLanguage { get; set; } public string MinimumTargetState { get; set; } = "final"; public bool RequireVerifiedContract { get; set; } [Output] public ITaskItem[] PreparedLanguageFiles { get; set; } = Array.Empty(); } public sealed class PrepareLanguagePackageTask : IsolatedBuildTask { [Required] public string PackageId { get; set; } = string.Empty; [Required] public ITaskItem[] LanguageFiles { get; set; } = Array.Empty(); public ITaskItem[] SourceLanguageFiles { get; set; } = Array.Empty(); public ITaskItem[] PackageFiles { get; set; } = Array.Empty(); [Required] public string OutputManifestPath { get; set; } = string.Empty; public string ExpectedLanguage { get; set; } public string MinimumTargetState { get; set; } = "final"; public bool RequireVerifiedContract { get; set; } [Output] public ITaskItem[] PreparedLanguageFiles { get; set; } = Array.Empty(); } public sealed class GenerateLanguagePackagePropsTask : IsolatedBuildTask { [Required] public string PackageId { get; set; } = string.Empty; [Required] public ITaskItem[] LanguageFiles { get; set; } = Array.Empty(); [Required] public string OutputPath { get; set; } = string.Empty; public bool RequireTargetLanguage { get; set; } = true; public string SourceKind { get; set; } = "StaticLanguagePack"; } } #endif