You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Symbols can be tagged with the appropriate metadata by setting the Symbols=true attribute on the CreateArtifact task. Referencing the namespace that the symbols will be uploaded to from the symbol store config will allow accessing them through the api/v1/symbols route. Hashing for symbols is compatible with symstore.exe, but is handled by a custom implementation in SymStore.cs. [CL 35971608 by ben marsh in ue5-main branch]
283 lines
8.6 KiB
C#
283 lines
8.6 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using EpicGames.Core;
|
|
using EpicGames.Horde;
|
|
using EpicGames.Horde.Artifacts;
|
|
using EpicGames.Horde.Commits;
|
|
using EpicGames.Horde.Storage;
|
|
using EpicGames.Horde.Storage.Nodes;
|
|
using EpicGames.Horde.Streams;
|
|
using EpicGames.Horde.Symbols;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
#nullable enable
|
|
|
|
namespace AutomationTool.Tasks
|
|
{
|
|
/// <summary>
|
|
/// Parameters for a <see cref="CreateArtifactTask"/>.
|
|
/// </summary>
|
|
public class CreateArtifactTaskParameters
|
|
{
|
|
/// <summary>
|
|
/// Name of the artifact
|
|
/// </summary>
|
|
[TaskParameter]
|
|
public string Name { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The artifact type. Determines the permissions and expiration policy for the artifact.
|
|
/// </summary>
|
|
[TaskParameter]
|
|
public string Type { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Description for the artifact. Will be shown through the Horde dashboard.
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public string? Description { get; set; }
|
|
|
|
/// <summary>
|
|
/// Base path for the uploaded files. All the tagged files must be under this directory. Defaults to the workspace root directory.
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public string? BaseDir { get; set; }
|
|
|
|
/// <summary>
|
|
/// Stream containing the artifact.
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public string? StreamId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Commit for the uploaded artifact.
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public string? Commit { get; set; }
|
|
|
|
/// <summary>
|
|
/// Files to include in the artifact.
|
|
/// </summary>
|
|
[TaskParameter(ValidationType = TaskParameterValidationType.FileSpec)]
|
|
public string Files { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Queryable keys for this artifact, separated by semicolons.
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public string? Keys { get; set; }
|
|
|
|
/// <summary>
|
|
/// Other metadata for the artifact, separated by semicolons.
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public string? Metadata { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether to add aliases for symbol files
|
|
/// </summary>
|
|
[TaskParameter(Optional = true)]
|
|
public bool Symbols { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uploads an artifact to Horde
|
|
/// </summary>
|
|
[TaskElement("CreateArtifact", typeof(CreateArtifactTaskParameters))]
|
|
public class CreateArtifactTask : BgTaskImpl
|
|
{
|
|
readonly CreateArtifactTaskParameters _parameters;
|
|
|
|
/// <summary>
|
|
/// Constructor.
|
|
/// </summary>
|
|
/// <param name="parameters">Parameters for this task.</param>
|
|
public CreateArtifactTask(CreateArtifactTaskParameters parameters)
|
|
=> _parameters = parameters;
|
|
|
|
/// <summary>
|
|
/// ExecuteAsync the task.
|
|
/// </summary>
|
|
/// <param name="job">Information about the current job.</param>
|
|
/// <param name="buildProducts">Set of build products produced by this node.</param>
|
|
/// <param name="tagNameToFileSet">Mapping from tag names to the set of files they include.</param>
|
|
public override async Task ExecuteAsync(JobContext job, HashSet<FileReference> buildProducts, Dictionary<string, HashSet<FileReference>> tagNameToFileSet)
|
|
{
|
|
ArtifactName name = new ArtifactName(_parameters.Name);
|
|
ArtifactType type = new ArtifactType(_parameters.Type);
|
|
List<string> keys = (_parameters.Keys ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
|
List<string> metadata = (_parameters.Metadata ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
|
|
|
// Add keys for the job that's executing
|
|
string? jobId = Environment.GetEnvironmentVariable("UE_HORDE_JOBID");
|
|
if (!String.IsNullOrEmpty(jobId))
|
|
{
|
|
keys.Add($"job:{jobId}");
|
|
|
|
string? stepId = Environment.GetEnvironmentVariable("UE_HORDE_STEPID");
|
|
if (!String.IsNullOrEmpty(stepId))
|
|
{
|
|
keys.Add($"job:{jobId}/step:{stepId}");
|
|
}
|
|
}
|
|
|
|
// Figure out the current change and stream id
|
|
StreamId streamId;
|
|
if (!String.IsNullOrEmpty(_parameters.StreamId))
|
|
{
|
|
streamId = new StreamId(_parameters.StreamId);
|
|
}
|
|
else
|
|
{
|
|
string? streamIdEnvVar = Environment.GetEnvironmentVariable("UE_HORDE_STREAMID");
|
|
if (!String.IsNullOrEmpty(streamIdEnvVar))
|
|
{
|
|
streamId = new StreamId(streamIdEnvVar);
|
|
}
|
|
else
|
|
{
|
|
throw new AutomationException("Missing UE_HORDE_STREAMID environment variable; unable to determine current stream.");
|
|
}
|
|
}
|
|
|
|
CommitId commitId;
|
|
if (!String.IsNullOrEmpty(_parameters.Commit))
|
|
{
|
|
commitId = new CommitId(_parameters.Commit);
|
|
}
|
|
else
|
|
{
|
|
int change = CommandUtils.P4Env.Changelist;
|
|
if (change > 0)
|
|
{
|
|
commitId = CommitId.FromPerforceChange(CommandUtils.P4Env.Changelist);
|
|
}
|
|
else
|
|
{
|
|
throw new AutomationException("Unknown changelist. Please run with -P4.");
|
|
}
|
|
}
|
|
|
|
// Resolve the files to include
|
|
DirectoryReference baseDir = ResolveDirectory(_parameters.BaseDir);
|
|
List<FileReference> files = BgTaskImpl.ResolveFilespec(baseDir, _parameters.Files, tagNameToFileSet).ToList();
|
|
|
|
bool validFiles = true;
|
|
foreach (FileReference file in files)
|
|
{
|
|
if (!file.IsUnderDirectory(baseDir))
|
|
{
|
|
Logger.LogError("Artifact file {File} is not under {BaseDir}", file, baseDir);
|
|
validFiles = false;
|
|
}
|
|
}
|
|
|
|
if (!validFiles)
|
|
{
|
|
throw new AutomationException($"Unable to create artifact {name} with given file list.");
|
|
}
|
|
|
|
// Create the new artifact
|
|
IHordeClient hordeClient = CommandUtils.ServiceProvider.GetRequiredService<IHordeClient>();
|
|
IArtifact artifact = await hordeClient.Artifacts.AddAsync(name, type, _parameters.Description, streamId, commitId, keys, metadata);
|
|
Logger.LogInformation("Creating artifact {ArtifactId} '{ArtifactName}' ({ArtifactType}) with namespace {NamespaceId}, ref {RefName} ({Link})", artifact.Id, name, type, artifact.NamespaceId, artifact.RefName, $"{hordeClient.ServerUrl}/api/v1/storage/{artifact.NamespaceId}/refs/{artifact.RefName}");
|
|
|
|
// Upload the files
|
|
IStorageClient storage = hordeClient.CreateStorageClient(artifact.NamespaceId);
|
|
Stopwatch timer = Stopwatch.StartNew();
|
|
|
|
IHashedBlobRef<DirectoryNode> rootRef;
|
|
await using (IBlobWriter blobWriter = storage.CreateBlobWriter(artifact.RefName))
|
|
{
|
|
rootRef = await blobWriter.WriteFilesAsync(baseDir, files);
|
|
}
|
|
await storage.WriteRefAsync(artifact.RefName, rootRef);
|
|
|
|
Logger.LogInformation("Uploaded artifact {ArtifactId} in {Time:n1}s", artifact.Id, timer.Elapsed.TotalSeconds);
|
|
|
|
// Tag any uploaded symbols
|
|
if (_parameters.Symbols)
|
|
{
|
|
foreach (FileReference file in files)
|
|
{
|
|
string? hash = await SymStore.GetHashAsync(file, Logger, default);
|
|
if (hash != null)
|
|
{
|
|
string path = file.MakeRelativeTo(baseDir);
|
|
|
|
FileEntry? fileEntry = await FindFileAsync(rootRef, path);
|
|
if (fileEntry == null)
|
|
{
|
|
Logger.LogWarning("Unable to find file {Path} in uploaded data.", path);
|
|
}
|
|
else
|
|
{
|
|
string fileName = file.GetFileName().ToUpperInvariant();
|
|
string alias = $"sym:{fileName}/{hash}/{fileName}";
|
|
Logger.LogInformation("Adding symbol alias: {Alias}", alias);
|
|
await storage.AddAliasAsync(alias, fileEntry.Target.Handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static async Task<FileEntry?> FindFileAsync(IBlobRef<DirectoryNode> rootDir, string path, CancellationToken cancellationToken = default)
|
|
{
|
|
string[] fragments = path.Split('/', '\\');
|
|
if (fragments.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
IBlobRef<DirectoryNode> directoryRef = rootDir;
|
|
for (int idx = 0;; idx++)
|
|
{
|
|
DirectoryNode directory = await directoryRef.ReadBlobAsync(cancellationToken);
|
|
if (idx + 1 < fragments.Length)
|
|
{
|
|
if (directory.TryGetDirectoryEntry(fragments[idx], out DirectoryEntry? directoryEntry))
|
|
{
|
|
directoryRef = directoryEntry.Handle;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (directory.TryGetFileEntry(fragments[idx], out FileEntry? fileEntry))
|
|
{
|
|
return fileEntry;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void Write(XmlWriter writer)
|
|
=> Write(writer, _parameters);
|
|
|
|
/// <inheritdoc/>
|
|
public override IEnumerable<string> FindConsumedTagNames()
|
|
=> FindTagNamesFromFilespec(_parameters.Files);
|
|
|
|
/// <inheritdoc/>
|
|
public override IEnumerable<string> FindProducedTagNames()
|
|
=> Enumerable.Empty<string>();
|
|
}
|
|
}
|