diff --git a/src/DotNetCI/.editorconfig b/.editorconfig similarity index 100% rename from src/DotNetCI/.editorconfig rename to .editorconfig diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index f46db64..cde73ac 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -25,5 +25,5 @@ jobs: with: dotnet-version: 3.1.301 - name: Build Tool Info - run: dotnet run --project src/DotNetCI/src/DotNetCI.Tool/ + run: dotnet run --project src/DotNetCI.Tool/ diff --git a/src/DotNetCI/Directory.Build.props b/Directory.Build.props similarity index 100% rename from src/DotNetCI/Directory.Build.props rename to Directory.Build.props diff --git a/src/DotNetCI/Directory.Build.targets b/Directory.Build.targets similarity index 100% rename from src/DotNetCI/Directory.Build.targets rename to Directory.Build.targets diff --git a/src/DotNetCI/DotNetCI.sln b/DotNetCI.sln similarity index 84% rename from src/DotNetCI/DotNetCI.sln rename to DotNetCI.sln index cb690c3..ea241f0 100644 --- a/src/DotNetCI/DotNetCI.sln +++ b/DotNetCI.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCI.Engine", "src\DotN EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCI.Config", "src\DotNetCI.Config\DotNetCI.Config.csproj", "{11C6B3A4-E3DB-4F79-AF24-728A188519E2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCI.Extensions", "src\DotNetCI.Extensions\DotNetCI.Extensions.csproj", "{C4D2280C-F5ED-4D54-AEA5-42D3FDC5382D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {11C6B3A4-E3DB-4F79-AF24-728A188519E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {11C6B3A4-E3DB-4F79-AF24-728A188519E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {11C6B3A4-E3DB-4F79-AF24-728A188519E2}.Release|Any CPU.Build.0 = Release|Any CPU + {C4D2280C-F5ED-4D54-AEA5-42D3FDC5382D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4D2280C-F5ED-4D54-AEA5-42D3FDC5382D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4D2280C-F5ED-4D54-AEA5-42D3FDC5382D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4D2280C-F5ED-4D54-AEA5-42D3FDC5382D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -52,6 +58,7 @@ Global {A9343D80-0366-4BEC-B637-2E6BCD66252F} = {16DED6D7-9309-4A88-B499-39A6C8AE33A6} {0CE9842E-C869-4AFC-85B8-34C55FDEDD26} = {16DED6D7-9309-4A88-B499-39A6C8AE33A6} {11C6B3A4-E3DB-4F79-AF24-728A188519E2} = {16DED6D7-9309-4A88-B499-39A6C8AE33A6} + {C4D2280C-F5ED-4D54-AEA5-42D3FDC5382D} = {16DED6D7-9309-4A88-B499-39A6C8AE33A6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0DCA34AB-D2D4-419E-815F-82AAE2B03C45} diff --git a/src/DotNetCI/Nuget.config b/Nuget.config similarity index 100% rename from src/DotNetCI/Nuget.config rename to Nuget.config diff --git a/src/DotNetCI/Packages.props b/Packages.props similarity index 90% rename from src/DotNetCI/Packages.props rename to Packages.props index 879e7e0..8a63b1f 100644 --- a/src/DotNetCI/Packages.props +++ b/Packages.props @@ -6,6 +6,7 @@ + diff --git a/dnci.yml b/dnci.yml new file mode 100644 index 0000000..7a62328 --- /dev/null +++ b/dnci.yml @@ -0,0 +1,23 @@ +variables: + - DOTNET_CLI_TELEMETRY_OPTOUT 1 + - DOTNET_SVCUTIL_TELEMETRY_OPTOUT 1 + - DOTNET_SKIP_FIRST_TIME_EXPERIENCE 1 + - DOTNET_NOLOGO 1 + - POWERSHELL_TELEMETRY_OPTOUT 1 + - POWERSHELL_UPDATECHECK_OPTOUT 1 + - DOTNET_CLI_UI_LANGUAGE ru +tasks: + - name: Hello! It's me! + definitions: + - src/DotNetCI/ + jobs: + - name: restore + configuration: Release + - name: Hello! It's me2! + definitions: + - src/DotNetCI.Config + - src/DotNetCI.Engine + jobs: + - name: restore + - configuration: Release + diff --git a/src/DotNetCI/src/Directory.Build.props b/src/Directory.Build.props similarity index 100% rename from src/DotNetCI/src/Directory.Build.props rename to src/Directory.Build.props diff --git a/src/DotNetCI.Config/CIConfig.cs b/src/DotNetCI.Config/CIConfig.cs new file mode 100644 index 0000000..cbdad72 --- /dev/null +++ b/src/DotNetCI.Config/CIConfig.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DotNetCI.Config +{ + internal class CIConfig + { + public IEnumerable Variables { get; set; } = new List(); + public RunOptions RunOptions { get; set; } = new RunOptions(); + public IEnumerable Tasks { get; set; } = new List(); + } +} diff --git a/src/DotNetCI.Config/CIConfigRepository.cs b/src/DotNetCI.Config/CIConfigRepository.cs new file mode 100644 index 0000000..64cfc49 --- /dev/null +++ b/src/DotNetCI.Config/CIConfigRepository.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Bullseye; +using DotNetCI.Extensions; +using SharpYaml.Serialization; + +namespace DotNetCI.Config +{ + public class CIConfigRepository + { + private const char SEPARATOR = ' '; + + public Dictionary Variables { get; } + public IEnumerable Tasks { get; } + public Options Options { get; } + + private CIConfigRepository(Dictionary envVars, IEnumerable tasks, Options options) + { + Variables = envVars; + Tasks = tasks; + Options = options; + } + + public static async Task CreateAsync(string sourcePath, bool throwsExceptions = true) + { + CIConfig config = await ReadCIConfigFileAsync(sourcePath, throwsExceptions); + Dictionary enviroinmentVariables = ParseEnviroinmentVariables(config.Variables, throwsExceptions); + Options options = GetOptions(config.RunOptions); + + return new CIConfigRepository(enviroinmentVariables, config.Tasks, options); + } + + private static Options GetOptions(RunOptions options) + { + return DefaultConfig + .RunOptions + .MutableClone(x => + { + x.Clear = options.Clear ?? x.Clear; + x.DryRun = options.DryRun ?? x.DryRun; + x.Host = options.Host ?? x.Host; + x.ListDependencies = options.ListDependencies ?? x.ListDependencies; + x.ListInputs = options.ListInputs ?? x.ListInputs; + x.ListTargets = options.ListTargets ?? x.ListTargets; + x.ListTree = options.ListTree ?? x.ListTree; + x.NoColor = options.NoColor ?? x.NoColor; + x.Parallel = options.Parallel ?? x.Parallel; + x.SkipDependencies = options.SkipDependencies ?? x.SkipDependencies; + x.Verbose = options.Verbose ?? x.Verbose; + }).TransferTo(options => + { + return new Options + { +#pragma warning disable CS8629 // Тип значения, допускающего NULL, может быть NULL. + Clear = options.Clear.Value, + DryRun = options.DryRun.Value, + Host = options.Host.Value, + ListDependencies = options.ListDependencies.Value, + ListInputs = options.ListInputs.Value, + ListTargets = options.ListTargets.Value, + ListTree = options.ListTree.Value, + NoColor = options.NoColor.Value, + Parallel = options.Parallel.Value, + SkipDependencies = options.SkipDependencies.Value, + Verbose = options.Verbose.Value +#pragma warning restore CS8629 // Тип значения, допускающего NULL, может быть NULL. + }; + }); + } + private static Dictionary ParseEnviroinmentVariables(IEnumerable dataSet, bool throwsExceptions) + { + Dictionary result = DefaultConfig + .EnviroinmentVariables + .ToDictionary(x => x.Key, x => x.Value); + + foreach (string data in dataSet) + { + string[] dataPair = data.Split(SEPARATOR); + + if (dataPair.Count() != 2) + { + if (throwsExceptions) + { + throw new InvalidDataException($"Can't set variable {data}"); + } + } + else + { + result[dataPair[0]] = dataPair[1]; + } + } + + return result; + } + + private static async Task ReadCIConfigFileAsync(string sourcePath, bool throwsExceptions) + { + try + { + string content = string.Empty; + using (StreamReader reader = new StreamReader(sourcePath)) + { + content = await reader.ReadToEndAsync(); + } + Serializer serializer = new Serializer(new SerializerSettings() + { + NamingConvention = new NameConvention() + }); + return serializer.Deserialize(content); + } + catch + { + if (throwsExceptions) + { + throw; + } + + return new CIConfig(); + } + } + } +} diff --git a/src/DotNetCI.Config/CIJobConfig.cs b/src/DotNetCI.Config/CIJobConfig.cs new file mode 100644 index 0000000..3f292ec --- /dev/null +++ b/src/DotNetCI.Config/CIJobConfig.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace DotNetCI.Config +{ + public class CIJobConfig + { + [Required] + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public IEnumerable Filters { get; set; } = new List(); + [Required] + public string Configuration { get; set; } + public IEnumerable Depends { get; set; } = new List(); + } +} diff --git a/src/DotNetCI.Config/CITaskConfig.cs b/src/DotNetCI.Config/CITaskConfig.cs new file mode 100644 index 0000000..a272121 --- /dev/null +++ b/src/DotNetCI.Config/CITaskConfig.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace DotNetCI.Config +{ + public class CITaskConfig + { + [Required] + public string? Name { get; set; } + public string? Description { get; set; } + public IEnumerable Definitions { get; set; } = new List(); + public IEnumerable Jobs { get; set; } = new List(); + } +} diff --git a/src/DotNetCI.Config/DefaultConfig.cs b/src/DotNetCI.Config/DefaultConfig.cs new file mode 100644 index 0000000..f4e280b --- /dev/null +++ b/src/DotNetCI.Config/DefaultConfig.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Bullseye; + +namespace DotNetCI.Config +{ + internal static class DefaultConfig + { + public static Dictionary EnviroinmentVariables => new Dictionary + { + { "DOTNET_CLI_TELEMETRY_OPTOUT", "1" }, + { "DOTNET_SVCUTIL_TELEMETRY_OPTOUT", "1" }, + { "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1" }, + { "DOTNET_NOLOGO", "1" }, + { "POWERSHELL_TELEMETRY_OPTOUT", "1" }, + { "POWERSHELL_UPDATECHECK_OPTOUT", "1" }, + { "DOTNET_CLI_UI_LANGUAGE", "ru" }, + }; + public static RunOptions RunOptions => new RunOptions + { + Host = Host.Unknown, + Verbose = false, + SkipDependencies = false, + Parallel = false, + NoColor = false, + ListTargets = false, + ListInputs = false, + ListDependencies = false, + DryRun = false, + Clear = false, + ListTree = false + }; + } +} diff --git a/src/DotNetCI.Config/DotNetCI.Config.csproj b/src/DotNetCI.Config/DotNetCI.Config.csproj new file mode 100644 index 0000000..93a458c --- /dev/null +++ b/src/DotNetCI.Config/DotNetCI.Config.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp3.1 + + + + + + + + + + diff --git a/src/DotNetCI.Config/NameConvention.cs b/src/DotNetCI.Config/NameConvention.cs new file mode 100644 index 0000000..437e4c2 --- /dev/null +++ b/src/DotNetCI.Config/NameConvention.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using DotNetCI.Extensions; +using SharpYaml.Serialization; + +namespace DotNetCI.Config +{ + internal class NameConvention : IMemberNamingConvention + { + public StringComparer Comparer { get; } + + public NameConvention() + { + Comparer = StringComparer.Ordinal; + } + + public string Convert(string name) + { + return name.ToLower(); + } + } +} diff --git a/src/DotNetCI.Config/RunOptions.cs b/src/DotNetCI.Config/RunOptions.cs new file mode 100644 index 0000000..e1fd2eb --- /dev/null +++ b/src/DotNetCI.Config/RunOptions.cs @@ -0,0 +1,25 @@ +using System; +using Bullseye; + +namespace DotNetCI.Config +{ + internal class RunOptions : ICloneable + { + public Host? Host { get; set; } + public bool? Verbose { get; set; } + public bool? SkipDependencies { get; set; } + public bool? Parallel { get; set; } + public bool? NoColor { get; set; } + public bool? ListTargets { get; set; } + public bool? ListInputs { get; set; } + public bool? ListDependencies { get; set; } + public bool? DryRun { get; set; } + public bool? Clear { get; set; } + public bool? ListTree { get; set; } + + public object Clone() + { + return MemberwiseClone(); + } + } +} diff --git a/src/DotNetCI.Engine/DotNetBuilder.cs b/src/DotNetCI.Engine/DotNetBuilder.cs new file mode 100644 index 0000000..9c62cd7 --- /dev/null +++ b/src/DotNetCI.Engine/DotNetBuilder.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bullseye; + +namespace DotNetCI.Engine +{ + public class DotNetBuilder + { + private readonly Options _runOptions; + private readonly ICollection _tasks; + + public DotNetBuilder(Options runOptions) + { + _runOptions = runOptions; + _tasks = new List(); + } + + public DotNetBuilder AddCiTask(TCiTask task) + where TCiTask : ICiTask + { + _tasks.Add(task); + return this; + } + + public DotNetBuilder AddCiTaskRange(IEnumerable tasks) + where TCiTask : ICiTask + { + foreach (TCiTask task in tasks) + { + _ = AddCiTask(task); + } + + return this; + } + + public async Task ExecuteTargetsAsync() + { + List runTargetsTasks = new List(); + foreach (ICiTask task in _tasks) + { + Targets targets = new Targets(); + + foreach (ICiJob job in task.Jobs) + { + targets.Add(job.Name, async () => await job.ExecuteJobAsync()); + } + + runTargetsTasks.Add(targets.RunWithoutExitingAsync(task.Jobs.Select(x => x.Name), _runOptions).ConfigureAwait(false)); + } + + foreach (ConfiguredTaskAwaitable runTargetTask in runTargetsTasks) + { + await runTargetTask; + } + } + } +} diff --git a/src/DotNetCI.Engine/DotNetCI.Engine.csproj b/src/DotNetCI.Engine/DotNetCI.Engine.csproj new file mode 100644 index 0000000..be8a7f1 --- /dev/null +++ b/src/DotNetCI.Engine/DotNetCI.Engine.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/src/DotNetCI.Engine/ICiJob.cs b/src/DotNetCI.Engine/ICiJob.cs new file mode 100644 index 0000000..6bfdd35 --- /dev/null +++ b/src/DotNetCI.Engine/ICiJob.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetCI.Engine +{ + public interface ICiJob + { + string Name { get; } + string? Description { get; } + + Task ExecuteJobAsync(); + } +} diff --git a/src/DotNetCI.Engine/ICiTask.cs b/src/DotNetCI.Engine/ICiTask.cs new file mode 100644 index 0000000..f0699bd --- /dev/null +++ b/src/DotNetCI.Engine/ICiTask.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace DotNetCI.Engine +{ + public interface ICiTask + { + string Name { get; } + string Description { get; } + JobsCollection Jobs { get; } + + ICiTask AddJob(TJob job) + where TJob : ICiJob; + + Task ExecuteJobsAsync(params string[] jobs); + } +} diff --git a/src/DotNetCI.Engine/JobsCollection.cs b/src/DotNetCI.Engine/JobsCollection.cs new file mode 100644 index 0000000..06dc480 --- /dev/null +++ b/src/DotNetCI.Engine/JobsCollection.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace DotNetCI.Engine +{ + public class JobsCollection : IReadOnlyCollection, IEnumerable + { + private readonly ICollection _jobs; + + public int Count => _jobs.Count; + + public ICiJob? this[string name] => _jobs.FirstOrDefault(x => x.Name == name); + + public JobsCollection() + { + _jobs = new List(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _jobs.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return _jobs.GetEnumerator(); + } + + public JobsCollection AddJob(TJob job) + where TJob : ICiJob + { + _jobs.Add(job); + return this; + } + } +} diff --git a/src/DotNetCI/src/DotNetCI.Engine/CliWrapCommandExtensions.cs b/src/DotNetCI.Extensions/CliWrapCommandExtensions.cs similarity index 54% rename from src/DotNetCI/src/DotNetCI.Engine/CliWrapCommandExtensions.cs rename to src/DotNetCI.Extensions/CliWrapCommandExtensions.cs index e43f989..7569d2c 100644 --- a/src/DotNetCI/src/DotNetCI.Engine/CliWrapCommandExtensions.cs +++ b/src/DotNetCI.Extensions/CliWrapCommandExtensions.cs @@ -2,13 +2,13 @@ using System.IO; using CliWrap; -namespace DotNetCI.Engine +namespace DotNetCI.Extensions { - internal static class CliWrapCommandExtensions + public static class CliWrapCommandExtensions { internal static Stream _stdout = Console.OpenStandardOutput(); internal static Stream _stderr = Console.OpenStandardOutput(); - internal static Command ToConsole(this Command command) => command | (_stdout, _stderr); + public static Command ToConsole(this Command command) => command | (_stdout, _stderr); } } diff --git a/src/DotNetCI/src/DotNetCI.Engine/DotNetCI.Engine.csproj b/src/DotNetCI.Extensions/DotNetCI.Extensions.csproj similarity index 82% rename from src/DotNetCI/src/DotNetCI.Engine/DotNetCI.Engine.csproj rename to src/DotNetCI.Extensions/DotNetCI.Extensions.csproj index e9c00d2..8a074f5 100644 --- a/src/DotNetCI/src/DotNetCI.Engine/DotNetCI.Engine.csproj +++ b/src/DotNetCI.Extensions/DotNetCI.Extensions.csproj @@ -4,7 +4,6 @@ netcoreapp3.1 - diff --git a/src/DotNetCI.Extensions/ICloneableExtensions.cs b/src/DotNetCI.Extensions/ICloneableExtensions.cs new file mode 100644 index 0000000..5b6d449 --- /dev/null +++ b/src/DotNetCI.Extensions/ICloneableExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace DotNetCI.Extensions +{ + public static class ICloneableExtensions + { + public static T Clone(this T obj) + where T: ICloneable + { + return (T)obj.Clone(); + } + + public static T MutableClone(this T obj, Action mutation) + where T : ICloneable + { + T mutable = obj.Clone(); + mutation?.Invoke(mutable); + return mutable; + } + + public static TReciever TransferTo(this TOwner owner, Func? transfer) + where TOwner : ICloneable + { + if (transfer == null) + { +#pragma warning disable CS8603 // Возможно, возврат ссылки, допускающей значение NULL. + return default; +#pragma warning restore CS8603 // Возможно, возврат ссылки, допускающей значение NULL. + } + return transfer.Invoke(owner); + } + } +} diff --git a/src/DotNetCI.Extensions/IEnumerableExtensions.cs b/src/DotNetCI.Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..ca84f2f --- /dev/null +++ b/src/DotNetCI.Extensions/IEnumerableExtensions.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace DotNetCI.Extensions +{ + public static class IEnumerableExtensions + { + public static string StringJoinWithPrefix(this IEnumerable collection, string separator = "", string prefix = "") + { + return prefix + string.Join(separator, collection); + } + } +} diff --git a/src/DotNetCI.Extensions/StringExtensions.cs b/src/DotNetCI.Extensions/StringExtensions.cs new file mode 100644 index 0000000..49e3fc8 --- /dev/null +++ b/src/DotNetCI.Extensions/StringExtensions.cs @@ -0,0 +1,23 @@ +using System; + +namespace DotNetCI.Extensions +{ + public static class StringExtensions + { + private static readonly char[] _boundary = new[] { '_' }; + public static string ToFirstLetterUpper(this string @string) + { + if (string.IsNullOrEmpty(@string)) + { + return @string; + } + string[] segments = @string.ToLower().Trim(_boundary).Split(_boundary, StringSplitOptions.RemoveEmptyEntries); + string result = string.Empty; + foreach (string segment in segments) + { + result += segment.Substring(0, 1).ToUpperInvariant() + result.Substring(1); + } + return result; + } + } +} diff --git a/src/DotNetCI.Tool/CiJobs/BuildJob.cs b/src/DotNetCI.Tool/CiJobs/BuildJob.cs new file mode 100644 index 0000000..c538b9f --- /dev/null +++ b/src/DotNetCI.Tool/CiJobs/BuildJob.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using CliWrap.Buffered; +using DotNetCI.Engine; +using DotNetCI.Extensions; +using static CliWrap.Cli; + +namespace DotNetCI.Tool.CiJobs +{ + public class BuildJob : ICiJob + { + private readonly CancellationToken _cancellationToken; + private readonly string _dotnetPath; + private readonly string _definition; + private readonly string _configuration; + + public string Name => "build"; + public string? Description => "Build"; + + public BuildJob(string dotnetPath, string definition, string configuration = "Release", CancellationToken cancellationToken = default) + { + _cancellationToken = cancellationToken; + _dotnetPath = dotnetPath; + _definition = definition; + _configuration = configuration; + } + + public async Task ExecuteJobAsync() + { + BufferedCommandResult cmd = await Wrap(_dotnetPath) + .WithArguments($"build {_definition} -noLogo -c {_configuration}") + .ToConsole() + .ExecuteBufferedAsync(_cancellationToken).Task.ConfigureAwait(false); + } + } +} diff --git a/src/DotNetCI.Tool/CiJobs/DefaultJob.cs b/src/DotNetCI.Tool/CiJobs/DefaultJob.cs new file mode 100644 index 0000000..56891cc --- /dev/null +++ b/src/DotNetCI.Tool/CiJobs/DefaultJob.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using DotNetCI.Engine; + +namespace DotNetCI.Tool.CiJobs +{ + public class DefaultJob : ICiJob + { + public string Name => "default"; + + public string? Description => string.Empty; + + public async Task ExecuteJobAsync() + { + await Task.Run(() => Console.WriteLine("CI Tool launched.")); + } + } +} diff --git a/src/DotNetCI.Tool/CiJobs/ResotoreJob.cs b/src/DotNetCI.Tool/CiJobs/ResotoreJob.cs new file mode 100644 index 0000000..7ac0b5f --- /dev/null +++ b/src/DotNetCI.Tool/CiJobs/ResotoreJob.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CliWrap.Buffered; +using DotNetCI.Engine; +using DotNetCI.Extensions; +using static CliWrap.Cli; + +namespace DotNetCI.Tool.CiJobs +{ + public class ResotoreJob : ICiJob + { + private readonly CancellationToken _cancellationToken; + private readonly string _dotnetPath; + private readonly IEnumerable _definitions; + private readonly string _configuration; + + public string Name => "restore"; + public string Description => "Nuget restore tool"; + + public ResotoreJob(string dotnetPath, IEnumerable definitions, string configuration = "Release", CancellationToken cancellationToken = default) + { + _cancellationToken = cancellationToken; + _dotnetPath = dotnetPath; + _definitions = (definitions?.Any() ?? false) + ? definitions + : new List { string.Empty }; + _configuration = configuration; + } + + public async Task ExecuteJobAsync() + { + bool isPublicRelease = bool.Parse(Environment.GetEnvironmentVariable("NBGV_PublicRelease") ?? "false"); + foreach (string definition in _definitions) + { + BufferedCommandResult cmd = await Wrap(_dotnetPath) + .WithArguments( + $"msbuild {definition} -noLogo " + + "-t:Restore " + + "-p:RestoreForce=true " + + "-p:RestoreIgnoreFailedSources=True " + + $"-p:Configuration={_configuration} " + + // for Nerdbank.GitVersioning + $"-p:PublicRelease={isPublicRelease} " + ) + .ToConsole() + .ExecuteBufferedAsync(_cancellationToken).Task.ConfigureAwait(false); + } + } + } +} diff --git a/src/DotNetCI.Tool/CiJobs/UnitTestsJob.cs b/src/DotNetCI.Tool/CiJobs/UnitTestsJob.cs new file mode 100644 index 0000000..e9c4251 --- /dev/null +++ b/src/DotNetCI.Tool/CiJobs/UnitTestsJob.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using CliWrap.Buffered; +using DotNetCI.Engine; +using DotNetCI.Extensions; +using static CliWrap.Cli; + +namespace DotNetCI.Tool.CiJobs +{ + class UnitTestsJob : ICiJob + { + private readonly CancellationToken _cancellationToken; + private readonly string _dotnetPath; + private readonly string _definition; + private readonly string _configuration; + private readonly string _filter; + + public string Name => "unit_test"; + public string? Description => "Unit tests"; + + public UnitTestsJob(string dotnetPath, string definition, IEnumerable? filter = null, string configuration = "Release", CancellationToken cancellationToken = default) + { + _cancellationToken = cancellationToken; + _dotnetPath = dotnetPath; + _definition = definition; + _configuration = configuration; + _filter = filter?.StringJoinWithPrefix("|", "--filter ") ?? string.Empty; + } + + public async Task ExecuteJobAsync() + { + string resultsDirectory = Path.GetFullPath(Path.Combine("artifacts", "tests", "unit", "output")); + if (!Directory.Exists(resultsDirectory)) + { + Directory.CreateDirectory(resultsDirectory); + } + BufferedCommandResult cmd = await Wrap(_dotnetPath) + .WithArguments( + $"test {_definition}" + + _filter + " " + + "--nologo " + + "--no-restore " + + $"--collect:\"XPlat Code Coverage\" --results-directory {resultsDirectory} " + + $"--logger trx;LogFileName=\"{Path.Combine(resultsDirectory, "tests.trx").Replace("\"", "\\\"")}\" " + + $"-c {_configuration} " + + "-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=json,cobertura" + ) + .ToConsole() + .ExecuteBufferedAsync(_cancellationToken).Task.ConfigureAwait(false); + + MoveAttachmentsToResultsDirectory(resultsDirectory, cmd.StandardOutput); + TryRemoveTestsOutputDirectories(resultsDirectory); + + // Removes all files in inner folders, workaround of https://github.com/microsoft/vstest/issues/2334 + static void TryRemoveTestsOutputDirectories(string resultsDirectory) + { + foreach (string directory in Directory.EnumerateDirectories(resultsDirectory)) + { + try + { + Directory.Delete(directory, recursive: true); + } + catch { } + } + } + + // Removes guid from tests output path, workaround of https://github.com/microsoft/vstest/issues/2378 + static void MoveAttachmentsToResultsDirectory(string resultsDirectory, string output) + { + Regex attachmentsRegex = new Regex( + $@"Attachments:(?(?[\s]+[^\n]+{Regex.Escape(resultsDirectory)}[^\n]+[\n])+)", + RegexOptions.Singleline | RegexOptions.CultureInvariant); + Match match = attachmentsRegex.Match(output); + if (match.Success) + { + string regexPaths = match.Groups["filepaths"].Value.Trim('\n', ' ', '\t', '\r'); + string[] paths = regexPaths.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); + if (paths.Length > 0) + { + foreach (string path in paths) + { + File.Move(path, Path.Combine(resultsDirectory, Path.GetFileName(path)), overwrite: true); + } + Directory.Delete(Path.GetDirectoryName(paths[0]), true); + } + } + } + } + } +} diff --git a/src/DotNetCI.Tool/CiTask.cs b/src/DotNetCI.Tool/CiTask.cs new file mode 100644 index 0000000..e08fc8e --- /dev/null +++ b/src/DotNetCI.Tool/CiTask.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using DotNetCI.Engine; + +namespace DotNetCI.Tool +{ + internal class CiTask : ICiTask + { + public string Name { get; } + + public string Description { get; } + + public JobsCollection Jobs { get; } + + public CiTask(string name, string description = "") + { + Name = name; + Description = description; + Jobs = new JobsCollection(); + } + + public ICiTask AddJob(TJob job) where TJob : ICiJob + { + Jobs.AddJob(job); + + return this; + } + + public async Task ExecuteJobsAsync(params string[] jobs) + { + foreach (string job in jobs) + { + ICiJob? ciJob = Jobs[job]; + if (ciJob != null) + { + await ciJob.ExecuteJobAsync(); + } + } + } + } +} diff --git a/src/DotNetCI/src/DotNetCI.Tool/DotNetCI.Tool.csproj b/src/DotNetCI.Tool/DotNetCI.Tool.csproj similarity index 92% rename from src/DotNetCI/src/DotNetCI.Tool/DotNetCI.Tool.csproj rename to src/DotNetCI.Tool/DotNetCI.Tool.csproj index 2d26d6a..7f17a2a 100644 --- a/src/DotNetCI/src/DotNetCI.Tool/DotNetCI.Tool.csproj +++ b/src/DotNetCI.Tool/DotNetCI.Tool.csproj @@ -20,6 +20,7 @@ + diff --git a/src/DotNetCI.Tool/Program.cs b/src/DotNetCI.Tool/Program.cs new file mode 100644 index 0000000..8b27588 --- /dev/null +++ b/src/DotNetCI.Tool/Program.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using DotNetCI.Config; +using DotNetCI.Engine; +using DotNetCI.Tool.CiJobs; + +namespace DotNetCI.Tool +{ + + internal static class Program + { + private static readonly string _configFile = "dnci.yml"; + + private static string? _dotnetPath; + private static CancellationToken _cancellationToken; + + private static async Task Main( + string[] arguments, + CancellationToken cancellationToken + ) + { + Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); + _cancellationToken = cancellationToken; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + string configFilePath = Path.Combine(Environment.CurrentDirectory, _configFile); + Console.WriteLine($"Try find {_configFile}\n"); + if (!File.Exists(configFilePath)) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($"{_configFile} not found! Create and configure it on repository root!"); + return 1; + } + Stopwatch stopWatch = new Stopwatch(); + try + { + _dotnetPath = await TryFindDotNetPathAsync(stopWatch); + if (string.IsNullOrEmpty(_dotnetPath)) + { + throw new FileNotFoundException("Can't find dotnet execution file."); + } + CIConfigRepository configRepository = await ReadConfigFileAsync(configFilePath, stopWatch); + SetEnviroinmentVariables(configRepository.Variables, stopWatch); + + DotNetBuilder dotnetBuilder = new DotNetBuilder(configRepository.Options); + dotnetBuilder.AddCiTaskRange(GetTasks(configRepository.Tasks)); + await dotnetBuilder.ExecuteTargetsAsync(); + + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($"Fatal error: {AggregateFatalErrorMessage(ex)}\n"); + return -1; + } + finally + { + stopWatch.Stop(); + Console.ResetColor(); + Console.WriteLine($"Elapsed time: {(float)stopWatch.ElapsedTicks / (float)Stopwatch.Frequency}"); + } + + return 0; + } + + private static string AggregateFatalErrorMessage(Exception? ex) + { + if (ex == null) + { + return string.Empty; + } + string result = ex.Message; + + return result.TrimEnd('.') + ". " + AggregateFatalErrorMessage(ex.InnerException); + } + + private static async Task ReadConfigFileAsync(string configFilePath, Stopwatch stopwatch) + { + Console.WriteLine($"Read {_configFile} file..."); + stopwatch.Start(); + CIConfigRepository configRepository = await CIConfigRepository.CreateAsync(configFilePath, true); + stopwatch.Stop(); + Console.WriteLine(GetElapsedTimeMessage(stopwatch)); + stopwatch.Reset(); + + return configRepository; + } + + private static void SetEnviroinmentVariables(Dictionary variables, Stopwatch stopwatch) + { + Console.WriteLine($"Set enviroinment variables..."); + stopwatch.Start(); + foreach (KeyValuePair @var in variables) + { + Console.WriteLine($"{var.Key} = {var.Value}"); + Environment.SetEnvironmentVariable(var.Key, var.Value); + } + stopwatch.Stop(); + Console.WriteLine(GetElapsedTimeMessage(stopwatch)); + } + + private static string GetElapsedTimeMessage(Stopwatch stopwatch) + { + return $"Done! Elapsed time: {(float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency}\n"; + } + + private static IEnumerable GetTasks(IEnumerable tasksConfig) + { + List result = new List(); + foreach (CITaskConfig taskConfig in tasksConfig) + { + ICiTask ciTask = new CiTask(taskConfig.Name, taskConfig.Description); + foreach (CIJobConfig jobConfig in taskConfig.Jobs) + { + ICiJob newJob = jobConfig.Name switch + { + "restore" => new ResotoreJob(_dotnetPath, taskConfig.Definitions, jobConfig.Configuration, _cancellationToken), + _ => new DefaultJob() + }; + + ciTask.AddJob(newJob); + } + + result.Add(ciTask); + } + + return result; + } + + private static string? TryFindDotNetPath() + { + string dotnet = "dotnet"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + dotnet += ".exe"; + } + + ProcessModule mainModule = Process.GetCurrentProcess().MainModule; + if (!string.IsNullOrEmpty(mainModule?.FileName) + && Path.GetFileName(mainModule.FileName)!.Equals(dotnet, StringComparison.OrdinalIgnoreCase)) + { + return mainModule.FileName; + } + + string? environmentVariable = Environment.GetEnvironmentVariable("DOTNET_ROOT"); + if (!string.IsNullOrEmpty(environmentVariable)) + { + return Path.Combine(environmentVariable, dotnet); + } + + string? paths = Environment.GetEnvironmentVariable("PATH"); + if (paths == null) + { + return null; + } + + foreach (string path in paths.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) + { + string fullPath = Path.Combine(path, dotnet); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + return null; + } + + private static async Task TryFindDotNetPathAsync(Stopwatch stopwatch) + { + Console.Write("Try find dotnet path ..."); + stopwatch.Start(); + string? dotnetPath = await Task.Run(() => TryFindDotNetPath()); + stopwatch.Stop(); + Console.WriteLine(GetElapsedTimeMessage(stopwatch)); + stopwatch.Reset(); + return dotnetPath; + } + + //private static async Task SetEnviroinmentVariablesAsync(string path) + //{ + // Stopwatch stopWatch = new Stopwatch(); + // stopWatch.Start(); + // const string env = "ENVIROINMENT VAR: "; + // if (File.Exists(path)) + // { + // Console.WriteLine($"{env}File \"{path}\" found.\nSet enviroinment variables from {path} ->"); + // } + // else + // { + // Console.ForegroundColor = ConsoleColor.DarkYellow; + // Console.WriteLine($"{env}Cant find \"{path}\" file.\nSet deffault enviroinment variables ->"); + // Console.ResetColor(); + // } + + // foreach (KeyValuePair item in await LoadEnviroinmentVars(path)) + // { + // Environment.SetEnvironmentVariable(item.Key, item.Value); + + // Console.WriteLine($" {env}\"{item.Key}\"=\"{item.Value}\" -> Done."); + // } + // stopWatch.Stop(); + // Console.WriteLine($"{env}Done. Elapsed time: {((float)stopWatch.ElapsedTicks / (float)Stopwatch.Frequency).ToString("0.0000")}s."); + //} + + //private static async Task SetToolOptionsAsync(string path) + //{ + // Stopwatch stopWatch = new Stopwatch(); + // stopWatch.Start(); + // const string tool = "TOOL OPTIONS: "; + + // if (File.Exists(path)) + // { + // Console.WriteLine($"{tool}File \"{path}\" found.\nSet DotNetBuildTool options from {path} ->"); + // } + // else + // { + // Console.ForegroundColor = ConsoleColor.DarkRed; + // Console.WriteLine($"{tool}Cant find \"{path}\" file.\nSorry, command line arguments not Implement yet."); + // Console.ResetColor(); + // if (_errorIfToolConfigNotExists) + // { + // throw new Exception($"{tool}Can't configure build tool."); + // } + // } + // BuilderOptions result = new BuilderOptions(); + // await foreach (KeyValuePair item in LoadToolConfig(path)) + // { + // PropertyInfo? property = typeof(BuilderOptions) + // .GetProperties() + // .FirstOrDefault(x => x.Name.ToUpper() == item.Key.ToUpper()); + // if (property == null) + // { + // Console.ForegroundColor = ConsoleColor.DarkYellow; + // Console.WriteLine($" {tool}Can't find and set: \"{item.Key}\" -> Done."); + // Console.ResetColor(); + + // } + // else + // { + // bool isParsed = Enum.TryParse(property.PropertyType.Name.ToUpper(), out OptionType optionType); + // if (!isParsed) + // { + // optionType = property.PropertyType.IsEnum ? OptionType.ENUM : OptionType.Unknown; + // } + // try + // { + // object givenObj = optionType switch + // { + // OptionType.BOOLEAN => ParseBool(item.Value), + // OptionType.STRING => item.Value, + // OptionType.ENUM => ParseEnum(property.PropertyType, item.Value), + // _ => throw new ArgumentException($"Unknown type: {property.PropertyType}") + // }; + // property.SetValue(result, givenObj); + // } + // catch (ArgumentException ex) + // { + // throw new ArgumentException($"{item.Key} option can't be parsed", ex); + // } + // Console.WriteLine($" {tool}{item.Key} \"{item.Value}\" -> Done."); + // } + // } + // stopWatch.Stop(); + // Console.WriteLine($"{tool}Done. Elapsed time: {((float)stopWatch.ElapsedTicks / (float)Stopwatch.Frequency).ToString("0.0000")}s."); + // return result; + + // bool ParseBool(string str) + // { + // return str switch + // { + // "0" => false, + // "1" => true, + // _ => throw new ArgumentException($"Can't parse value: {str}") + // }; + // } + + // object ParseEnum(Type enumType, string str) + // { + // return Enum.Parse(enumType, str); + // } + //} + } +} diff --git a/src/DotNetCI/src/DotNetCI.Config/BaseConfig.cs b/src/DotNetCI/src/DotNetCI.Config/BaseConfig.cs deleted file mode 100644 index 1e30488..0000000 --- a/src/DotNetCI/src/DotNetCI.Config/BaseConfig.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace DotNetCI.Config -{ - internal static class BaseConfig - { - public static async IAsyncEnumerable> LoadFromFile(string path, char separator = '=') - { - IList lines = new List(); - - using (Stream fs = new FileStream(path, FileMode.Open)) - { - using (StreamReader sr = new StreamReader(fs)) - { - while (!sr.EndOfStream) - { - lines.Add(await sr.ReadLineAsync()); - } - - foreach (string? line in lines) - { - string[] linePair = line?.Split(separator, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; - - if (linePair.Count() == 2) - { - yield return new KeyValuePair(linePair[0].Trim(), linePair[1].Trim()); - } - } - } - } - } - } -} diff --git a/src/DotNetCI/src/DotNetCI.Config/DotNetCI.Config.csproj b/src/DotNetCI/src/DotNetCI.Config/DotNetCI.Config.csproj deleted file mode 100644 index cb63190..0000000 --- a/src/DotNetCI/src/DotNetCI.Config/DotNetCI.Config.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - netcoreapp3.1 - - - diff --git a/src/DotNetCI/src/DotNetCI.Config/EniroinmentConfig.cs b/src/DotNetCI/src/DotNetCI.Config/EniroinmentConfig.cs deleted file mode 100644 index 4486ecb..0000000 --- a/src/DotNetCI/src/DotNetCI.Config/EniroinmentConfig.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace DotNetCI.Config -{ - public static class EniroinmentConfig - { - private static readonly Dictionary _enviroinmentVariables = new Dictionary() - { - { "DOTNET_CLI_TELEMETRY_OPTOUT", "1" }, - { "DOTNET_SVCUTIL_TELEMETRY_OPTOUT", "1" }, - { "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1" }, - { "DOTNET_NOLOGO", "1" }, - { "POWERSHELL_TELEMETRY_OPTOUT", "1" }, - { "POWERSHELL_UPDATECHECK_OPTOUT", "1" }, - { "DOTNET_CLI_UI_LANGUAGE", "ru" }, - }; - - public static async Task> LoadEnviroinmentVars(string path) - { - Dictionary result = _enviroinmentVariables - .ToDictionary(x => x.Key, x => x.Value); - if (!File.Exists(path)) - { - return result; - } - - await foreach (KeyValuePair item in BaseConfig.LoadFromFile(path)) - { - result[item.Key] = item.Value; - } - - return result; - } - } -} diff --git a/src/DotNetCI/src/DotNetCI.Config/ToolConfig.cs b/src/DotNetCI/src/DotNetCI.Config/ToolConfig.cs deleted file mode 100644 index 6bb3f7f..0000000 --- a/src/DotNetCI/src/DotNetCI.Config/ToolConfig.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace DotNetCI.Config -{ - public static class ToolConfig - { - private const char SEPARATOR = ' '; - public static async IAsyncEnumerable> LoadToolConfig(string path) - { - if (!File.Exists(path)) - { - yield break; - } - - await foreach (KeyValuePair item in BaseConfig.LoadFromFile(path, SEPARATOR)) - { - yield return new KeyValuePair(item.Key, item.Value); - } - } - } -} diff --git a/src/DotNetCI/src/DotNetCI.Engine/BuilderOptions.cs b/src/DotNetCI/src/DotNetCI.Engine/BuilderOptions.cs deleted file mode 100644 index e64846f..0000000 --- a/src/DotNetCI/src/DotNetCI.Engine/BuilderOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bullseye; - -namespace DotNetCI.Engine -{ - public class BuilderOptions - { - public bool Clear { get; set; } - public bool DryRun { get; set; } - public Host Host { get; set; } - public bool Parallel { get; set; } - public bool SkipDependencies { get; set; } - public bool Verbose { get; set; } - public string? FileOrFolder { get; set; } - public bool ErrorIfTargetsNotExists { get; set; } - public bool PrintInfo { get; set; } - public string? Configuration { get; set; } - } -} diff --git a/src/DotNetCI/src/DotNetCI.Engine/DotNetBuilder.cs b/src/DotNetCI/src/DotNetCI.Engine/DotNetBuilder.cs deleted file mode 100644 index f361b53..0000000 --- a/src/DotNetCI/src/DotNetCI.Engine/DotNetBuilder.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Bullseye; -using CliWrap; -using CliWrap.Buffered; -using static Bullseye.Targets; - -namespace DotNetCI.Engine -{ - public class DotNetBuilder - { - private readonly string _dotnetPath; - private readonly BuilderOptions _options; - - private DotNetBuilder(BuilderOptions options, string? dotnetPath, CancellationToken cancellationToken = default) - { - _options = options; - _dotnetPath = dotnetPath ?? string.Empty; - - InitTargets(cancellationToken); - } - - public static async Task CreatBuilderAsync(BuilderOptions options, CancellationToken cancellationToken = default) - { - return new DotNetBuilder(options, await TryFindDotNetPathAsync(), cancellationToken); - } - - public async Task ExecuteTargetsAsync(string[] targets) - { - Options options = new Options - { - Clear = _options.Clear, - DryRun = _options.DryRun, - Host = _options.Host, - ListDependencies = false, - ListInputs = false, - ListTargets = false, - ListTree = false, - NoColor = false, - Parallel = _options.Parallel, - SkipDependencies = _options.SkipDependencies, - Verbose = _options.Verbose - }; - await RunTargetsWithoutExitingAsync(targets, options).ConfigureAwait(false); - } - - private void InitTargets(CancellationToken cancellationToken = default) - { - Target( - "restore", - async () => - { - bool isPublicRelease = bool.Parse(Environment.GetEnvironmentVariable("NBGV_PublicRelease") ?? "false"); - BufferedCommandResult cmd = await Cli.Wrap(_dotnetPath).WithArguments( - $"msbuild {_options.FileOrFolder} -noLogo " + - "-t:Restore " + - "-p:RestoreForce=true " + - "-p:RestoreIgnoreFailedSources=True " + - $"-p:Configuration={_options.Configuration} " + - // for Nerdbank.GitVersioning - $"-p:PublicRelease={isPublicRelease} " - ) - .ToConsole() - .ExecuteBufferedAsync(cancellationToken).Task.ConfigureAwait(false); - }); - - Target( - "build", - async () => - { - BufferedCommandResult cmd = await Cli.Wrap(_dotnetPath).WithArguments( - $"build {_options} -noLogo -c {_options.Configuration}" - ) - .ToConsole() - .ExecuteBufferedAsync(cancellationToken).Task.ConfigureAwait(false); - }); - - Target( - "unit_test", - async () => - { - string resultsDirectory = Path.GetFullPath(Path.Combine("artifacts", "tests", "unit", "output")); - if (!Directory.Exists(resultsDirectory)) - { - Directory.CreateDirectory(resultsDirectory); - } - BufferedCommandResult cmd = await Cli.Wrap(_dotnetPath) - .WithArguments( - $"test " + - "--filter FullyQualifiedName~Unit " + - "--nologo " + - "--no-restore " + - $"--collect:\"XPlat Code Coverage\" --results-directory {resultsDirectory} " + - $"--logger trx;LogFileName=\"{Path.Combine(resultsDirectory, "tests.trx").Replace("\"", "\\\"")}\" " + - $"-c {_options.Configuration} " + - "-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=json,cobertura" - ) - .ToConsole() - .ExecuteBufferedAsync(cancellationToken).Task.ConfigureAwait(false); - - MoveAttachmentsToResultsDirectory(resultsDirectory, cmd.StandardOutput); - TryRemoveTestsOutputDirectories(resultsDirectory); - - // Removes all files in inner folders, workaround of https://github.com/microsoft/vstest/issues/2334 - static void TryRemoveTestsOutputDirectories(string resultsDirectory) - { - foreach (string directory in Directory.EnumerateDirectories(resultsDirectory)) - { - try - { - Directory.Delete(directory, recursive: true); - } - catch { } - } - } - - // Removes guid from tests output path, workaround of https://github.com/microsoft/vstest/issues/2378 - static void MoveAttachmentsToResultsDirectory(string resultsDirectory, string output) - { - Regex attachmentsRegex = new Regex( - $@"Attachments:(?(?[\s]+[^\n]+{Regex.Escape(resultsDirectory)}[^\n]+[\n])+)", - RegexOptions.Singleline | RegexOptions.CultureInvariant); - Match match = attachmentsRegex.Match(output); - if (match.Success) - { - string regexPaths = match.Groups["filepaths"].Value.Trim('\n', ' ', '\t', '\r'); - string[] paths = regexPaths.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); - if (paths.Length > 0) - { - foreach (string path in paths) - { - File.Move(path, Path.Combine(resultsDirectory, Path.GetFileName(path)), overwrite: true); - } - Directory.Delete(Path.GetDirectoryName(paths[0]), true); - } - } - } - }); - - Target("default", () => - { - Console.WriteLine("-- use \"restore\" and/or \"build\" and/or \"unit_test\" in command line."); - }); - } - - private static string? TryFindDotNetPath() - { - string dotnet = "dotnet"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - dotnet += ".exe"; - } - - ProcessModule mainModule = Process.GetCurrentProcess().MainModule; - if (!string.IsNullOrEmpty(mainModule?.FileName) - && Path.GetFileName(mainModule.FileName)!.Equals(dotnet, StringComparison.OrdinalIgnoreCase)) - { - return mainModule.FileName; - } - - string? environmentVariable = Environment.GetEnvironmentVariable("DOTNET_ROOT"); - if (!string.IsNullOrEmpty(environmentVariable)) - { - return Path.Combine(environmentVariable, dotnet); - } - - string? paths = Environment.GetEnvironmentVariable("PATH"); - if (paths == null) - { - return null; - } - - foreach (string path in paths.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) - { - string fullPath = Path.Combine(path, dotnet); - if (File.Exists(fullPath)) - { - return fullPath; - } - } - return null; - } - - private static async Task TryFindDotNetPathAsync() - { - return await Task.Run(() => TryFindDotNetPath()); - } - } -} diff --git a/src/DotNetCI/src/DotNetCI.Engine/OptionType.cs b/src/DotNetCI/src/DotNetCI.Engine/OptionType.cs deleted file mode 100644 index f00e4e8..0000000 --- a/src/DotNetCI/src/DotNetCI.Engine/OptionType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DotNetCI.Engine -{ - public enum OptionType - { - STRING, - BOOLEAN, - ENUM, - Unknown - } -} diff --git a/src/DotNetCI/src/DotNetCI.Tool/Program.cs b/src/DotNetCI/src/DotNetCI.Tool/Program.cs deleted file mode 100644 index d2f03ec..0000000 --- a/src/DotNetCI/src/DotNetCI.Tool/Program.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using DotNetCI.Engine; -using static DotNetCI.Config.EniroinmentConfig; -using static DotNetCI.Config.ToolConfig; - -namespace DotNetCI.Tool -{ - internal static class Program - { - private static readonly string _enviroinmentVarsConfigFile = ".dnbuildvars"; - private static readonly string _dotnetBuildToolConfig = ".dnbuild"; - - private static bool _errorIfToolConfigNotExists; - - private static async Task Main( - string[] arguments, - CancellationToken cancellationToken, - bool errorIfToolConfigNotExists = false - ) - { - Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - _errorIfToolConfigNotExists = errorIfToolConfigNotExists; - string enviroinmentVarsConfigPath = Path.Combine(Environment.CurrentDirectory, _enviroinmentVarsConfigFile); - string buildToolConfigPath = Path.Combine(Environment.CurrentDirectory, _dotnetBuildToolConfig); - Task< BuilderOptions> setToolOptionsTask = SetToolOptionsAsync(buildToolConfigPath); - Task setEnvVarsTask = SetEnviroinmentVariablesAsync(enviroinmentVarsConfigPath); - - BuilderOptions options = await setToolOptionsTask; - await setEnvVarsTask; - - DotNetBuilder builder = await DotNetBuilder.CreatBuilderAsync(options, cancellationToken); - await builder.ExecuteTargetsAsync(arguments); - } - - private static async Task SetEnviroinmentVariablesAsync(string path) - { - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - const string env = "ENVIROINMENT VAR: "; - if (File.Exists(path)) - { - Console.WriteLine($"{env}File \"{path}\" found.\nSet enviroinment variables from {path} ->"); - } - else - { - Console.ForegroundColor = ConsoleColor.DarkYellow; - Console.WriteLine($"{env}Cant find \"{path}\" file.\nSet deffault enviroinment variables ->"); - Console.ResetColor(); - } - - foreach (KeyValuePair item in await LoadEnviroinmentVars(path)) - { - Environment.SetEnvironmentVariable(item.Key, item.Value); - - Console.WriteLine($" {env}\"{item.Key}\"=\"{item.Value}\" -> Done."); - } - stopWatch.Stop(); - Console.WriteLine($"{env}Done. Elapsed time: {((float)stopWatch.ElapsedTicks / (float)Stopwatch.Frequency).ToString("0.0000")}s."); - } - - private static async Task SetToolOptionsAsync(string path) - { - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - const string tool = "TOOL OPTIONS: "; - - if (File.Exists(path)) - { - Console.WriteLine($"{tool}File \"{path}\" found.\nSet DotNetBuildTool options from {path} ->"); - } - else - { - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.WriteLine($"{tool}Cant find \"{path}\" file.\nSorry, command line arguments not Implement yet."); - Console.ResetColor(); - if (_errorIfToolConfigNotExists) - { - throw new Exception($"{tool}Can't configure build tool."); - } - } - BuilderOptions result = new BuilderOptions(); - await foreach (KeyValuePair item in LoadToolConfig(path)) - { - PropertyInfo? property = typeof(BuilderOptions) - .GetProperties() - .FirstOrDefault(x => x.Name.ToUpper() == item.Key.ToUpper()); - if (property == null) - { - Console.ForegroundColor = ConsoleColor.DarkYellow; - Console.WriteLine($" {tool}Can't find and set: \"{item.Key}\" -> Done."); - Console.ResetColor(); - - } - else - { - bool isParsed = Enum.TryParse(property.PropertyType.Name.ToUpper(), out OptionType optionType); - if (!isParsed) - { - optionType = property.PropertyType.IsEnum ? OptionType.ENUM : OptionType.Unknown; - } - try - { - object givenObj = optionType switch - { - OptionType.BOOLEAN => ParseBool(item.Value), - OptionType.STRING => item.Value, - OptionType.ENUM => ParseEnum(property.PropertyType, item.Value), - _ => throw new ArgumentException($"Unknown type: {property.PropertyType}") - }; - property.SetValue(result, givenObj); - } - catch (ArgumentException ex) - { - throw new ArgumentException($"{item.Key} option can't be parsed", ex); - } - Console.WriteLine($" {tool}{item.Key} \"{item.Value}\" -> Done."); - } - } - stopWatch.Stop(); - Console.WriteLine($"{tool}Done. Elapsed time: {((float)stopWatch.ElapsedTicks / (float)Stopwatch.Frequency).ToString("0.0000")}s."); - return result; - - bool ParseBool(string str) - { - return str switch - { - "0" => false, - "1" => true, - _ => throw new ArgumentException($"Can't parse value: {str}") - }; - } - - object ParseEnum(Type enumType, string str) - { - return Enum.Parse(enumType, str); - } - } - } -}