Skip to content

Instantly share code, notes, and snippets.

@fubar-coder
Last active October 28, 2024 22:41
Show Gist options
  • Save fubar-coder/804c7ae5d58c5f5a6e52807ca4cc7723 to your computer and use it in GitHub Desktop.
Save fubar-coder/804c7ae5d58c5f5a6e52807ca4cc7723 to your computer and use it in GitHub Desktop.
Artifact resolver using TeamCity syntax

Artifact resolver

This resolves the artifacts using the path syntax used by TeamCity, excluding the variable replacement functionality.

LICENSE

MIT

Usage

You need to install Microsoft.Extensions.FileSystemGlobbing and add the source files below.

var config = ArtifactResolver.LoadConfiguration([
  "app/**/*.dll => ./",
  "-:app/**/*.Tests.dll"
]);

var resolver = new ArtifactResolver(config);
var transformed = resolver.Transform(new DirectoryInfoWrapper(new DirectoryInfo(rootDir)));
ArtifactResolver.Copyto(transformed, "/tmp/your-output-dir");

The code above copies all DLL files from below the app directory, exception the unit test DLLs, to the target directory (/tmp/your-output-dir).

You can combine multiple transformations, because the original paths are always retained.

namespace FubarDev.ArtifactResolver;
public abstract record ArtifactConfigurationItem;
public record ArtifactInclude(string Pattern) : ArtifactConfigurationItem
{
public string? TargetDirectory { get; init; }
}
public record ArtifactExclude(string Pattern) : ArtifactConfigurationItem;
using System.Text.RegularExpressions;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
namespace FubarDev.ArtifactResolver;
public class ArtifactResolver
{
private static readonly Regex _moveRegex = new Regex(@"^\s*(?<mode>[+-]:)?\s*(?<source>(?![+-]:).+?)(\s*=>\s*(?<target>.+?))?\s*$", RegexOptions.Compiled);
private readonly string[] _excludePatterns;
private readonly List<ArtifactInclude> _includes;
public ArtifactResolver(IEnumerable<ArtifactConfigurationItem> configuration)
{
var includes = new List<ArtifactInclude>();
var excludes = new List<ArtifactExclude>();
foreach (var item in configuration)
{
switch (item)
{
case ArtifactInclude include:
includes.Add(include);
break;
case ArtifactExclude exclude:
excludes.Add(exclude);
break;
}
}
_includes = includes;
_excludePatterns = excludes.Select(x => x.Pattern).ToArray();
}
public static IEnumerable<ArtifactConfigurationItem> LoadConfiguration(IEnumerable<string> lines)
{
foreach (var line in lines)
{
var match = _moveRegex.Match(line);
if (!match.Success)
{
throw new InvalidOperationException($"Ungültige Struktur: {line}");
}
var mode = match.Groups["mode"].Value;
var source = match.Groups["source"].Value.Trim();
var target = match.Groups["target"].Value.Trim();
if (source.EndsWith("=>"))
{
throw new InvalidOperationException($"Ungültige Struktur: {line}");
}
if (mode == "-:")
{
if (!string.IsNullOrEmpty(target))
{
throw new InvalidOperationException($"Ungültige Struktur: {line}");
}
yield return new ArtifactExclude(source);
}
else
{
var targetDir = string.IsNullOrEmpty(target) ? null : target;
if (targetDir != null)
{
targetDir = targetDir.Replace('\\', '/');
if (targetDir.StartsWith("./"))
{
targetDir = targetDir[1..];
}
if (!targetDir.EndsWith("/"))
{
targetDir += "/";
}
if (!targetDir.StartsWith("/"))
{
targetDir = "/" + targetDir;
}
}
yield return new ArtifactInclude(source)
{
TargetDirectory = targetDir,
};
}
}
}
public DirectoryInfoBase Transform(DirectoryInfoBase sourceDirectory)
{
var result = new InMemoryDir(string.Empty, string.Empty);
foreach (var (matcher, targetDir) in CreateMatchers(sourceDirectory))
{
var matchResult = matcher.Execute(sourceDirectory);
var transfer = new List<(string Source, string Target)>();
if (string.IsNullOrEmpty(targetDir))
{
foreach (var match in matchResult.Files)
{
transfer.Add((match.Path, match.Path));
}
}
else
{
foreach (var match in matchResult.Files)
{
var relativePath = match.Stem ?? Path.GetFileName(match.Path);
var targetPath = targetDir + relativePath;
transfer.Add((match.Path, targetPath));
}
}
foreach (var (source, target) in transfer)
{
var sourceInfo = FindFile(sourceDirectory, source);
result.AddFile(target, sourceInfo.FullName);
}
}
return result;
}
public static void CopyTo(DirectoryInfoBase root, string target)
{
Directory.CreateDirectory(target);
foreach (var item in root.EnumerateFileSystemInfos())
{
if (item is DirectoryInfoBase dirItem)
{
CopyTo(dirItem, $"{target}/{dirItem.Name}");
}
else if (item is FileInfoBase fileItem)
{
System.IO.File.Copy(fileItem.FullName, $"{target}/{fileItem.Name}", true);
}
else
{
throw new NotSupportedException();
}
}
}
private static FileInfoBase FindFile(DirectoryInfoBase root, string path)
{
return (FileInfoBase?) Find(root, path) ?? throw new InvalidOperationException();
}
private static FileSystemInfoBase? Find(DirectoryInfoBase root, string path)
{
var parts = path.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
var dir = root;
foreach (var part in parts.Take(parts.Length - 1))
{
dir = dir.GetDirectory(part) ?? throw new InvalidOperationException();
}
var lastPart = parts[^1];
return dir.EnumerateFileSystemInfos()
.FirstOrDefault(x => string.Equals(x.Name, lastPart, StringComparison.OrdinalIgnoreCase));
}
private IEnumerable<(Matcher Matcher, string? TargetDir)> CreateMatchers(DirectoryInfoBase sourceDirectory)
{
var includesByTargetDirs = _includes
.GroupBy(x => x.TargetDirectory ?? string.Empty);
foreach (var group in includesByTargetDirs)
{
var matcher = new Matcher(StringComparison.OrdinalIgnoreCase);
foreach (var include in group)
{
var hasGlob = include.Pattern.Contains('*') || include.Pattern.Contains('?');
if (!hasGlob && Find(sourceDirectory, include.Pattern) is DirectoryInfoBase)
{
matcher.AddInclude(include.Pattern + "/**");
}
else
{
matcher.AddInclude(include.Pattern);
}
}
matcher.AddExcludePatterns(_excludePatterns);
var targetDir = string.IsNullOrEmpty(group.Key) ? null : group.Key;
yield return (matcher, targetDir);
}
}
private class InMemoryFile(string name, string fullName, InMemoryDir parentDir) : FileInfoBase
{
public override string Name => name;
public override string FullName => fullName;
public override DirectoryInfoBase ParentDirectory => parentDir;
}
private class InMemoryDir(string name, string fullName) : DirectoryInfoBase
{
private readonly InMemoryDir? _parentDirectory;
private readonly List<FileSystemInfoBase?> _items = [];
private readonly Dictionary<string, (InMemoryDir Info, int Index)> _subDirectories =
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, (FileInfoBase Info, int Index)> _files =
new(StringComparer.OrdinalIgnoreCase);
public InMemoryDir(string name, string fullName, InMemoryDir? parentDir)
: this(name, fullName)
{
_parentDirectory = parentDir;
}
public override string Name => name;
public override string FullName => fullName;
public override DirectoryInfoBase? ParentDirectory => _parentDirectory;
internal void AddItem(FileSystemInfoBase item)
{
if (item is FileInfoBase file)
{
if (_files.TryGetValue(file.Name, out var oldItem))
{
_items[oldItem.Index] = null;
}
_files[file.Name] = (file, _items.Count);
_items.Add(file);
}
else if (item is InMemoryDir dir)
{
if (_subDirectories.TryGetValue(dir.Name, out var oldItem))
{
_items[oldItem.Index] = null;
}
_subDirectories[dir.Name] = (dir, _items.Count);
_items.Add(dir);
}
else
{
throw new NotSupportedException();
}
}
internal void AddFile(string targetPath, string sourcePath)
{
if (string.IsNullOrEmpty(targetPath))
{
throw new InvalidOperationException();
}
var dir = this;
var parts = targetPath.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
var basePath = string.IsNullOrEmpty(dir.FullName) ? string.Empty : dir.FullName + "/";
foreach (var part in parts.Take(parts.Length - 1))
{
if (dir.GetDirectory(part) is not InMemoryDir subDir)
{
subDir = new InMemoryDir(part, basePath + part, dir);
dir.AddItem(subDir);
}
dir = subDir;
basePath += part + "/";
}
var fileName = parts[^1];
dir.AddItem(new InMemoryFile(fileName, sourcePath, dir));
}
public override IEnumerable<FileSystemInfoBase> EnumerateFileSystemInfos()
{
return _items.Where(x => x != null)!;
}
public override DirectoryInfoBase? GetDirectory(string path)
{
return _subDirectories.TryGetValue(path, out var item) ? item.Info : null;
}
public override FileInfoBase? GetFile(string path)
{
return _files.TryGetValue(path, out var item) ? item.Info : null;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment