예제 #1
0
        public void DependencyOrderWhenParallelAndSkipping(
            TargetCollection targets,
            TestConsole console,
            int clock,
            int buildStartTime,
            int test1StartTime,
            int test2StartTime)
        {
            "Given a target that takes a long time to start up"
            .x(() => Ensure(ref targets).Add(CreateTarget(
                                                 "build",
                                                 () => {
                Thread.Sleep(TimeSpan.FromSeconds(1));         // a weak way to encourage the tests to run first
                buildStartTime = Interlocked.Increment(ref clock);
            })));

            "And a second target which depends on the first target"
            .x(() => targets.Add(CreateTarget("test1", new[] { "build" }, () => test1StartTime = Interlocked.Increment(ref clock))));

            "And a third target which depends on the first target"
            .x(() => targets.Add(CreateTarget("test2", new[] { "build" }, () => test2StartTime = Interlocked.Increment(ref clock))));

            "When I run all the targets with parallelism, skipping dependencies"
            .x(() => targets.RunAsync(new List <string> {
                "--parallel", "--skip-dependencies", "test1", "test2", "build"
            }, console = new TestConsole()));

            "Then the first target is run first"
            .x(() => Assert.Equal(1, buildStartTime));

            "And the other targets are run later"
            .x(() => Assert.Equal(5, test1StartTime + test2StartTime));
        }
예제 #2
0
        public void WithInputs(TargetCollection targets, List <int> inputsReceived)
        {
            "Given a target with inputs 1 and 2"
            .x(() => Ensure(ref targets).Add(CreateTarget("default", new[] { 1, 2 }, input => Ensure(ref inputsReceived).Add(input))));

            "When I run the target"
            .x(() => targets.RunAsync(new List <string>(), default, default, default));
예제 #3
0
        public void DoubleTransitiveDependency(TargetCollection targets, List <string> ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => Ensure(ref ran).Add("first"))));

            "And a second target which depends on the first target"
            .x(() => targets.Add(CreateTarget("second", new[] { "first" }, () => Ensure(ref ran).Add("second"))));

            "And a third target which depends on the first target and the second target"
            .x(() => targets.Add(CreateTarget("third", new[] { "first", "second" }, () => Ensure(ref ran).Add("third"))));

            "When I run the third target"
            .x(() => targets.RunAsync(new List <string> {
                "third"
            }, default));

            "Then all targets are run"
            .x(() => Assert.Equal(3, ran.Count));

            "And the first target is run first"
            .x(() => Assert.Equal("first", ran[0]));

            "And the second target is run second"
            .x(() => Assert.Equal("second", ran[1]));

            "And the third target is run third"
            .x(() => Assert.Equal("third", ran[2]));
        }
예제 #4
0
        public void NotExistentDependencies(TargetCollection targets, TestConsole console, bool anyRan, Exception exception)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => anyRan = true)));

            "And a second target which depends on the first target and a non-existent target"
            .x(() => targets.Add(CreateTarget("second", new[] { "first", "non-existing" }, () => anyRan = true)));

            "And a third target which depends on the second target and another non-existent target"
            .x(() => targets.Add(CreateTarget("third", new[] { "second", "also-non-existing" }, () => anyRan = true)));

            "When I run the third target"
            .x(async() => exception = await Record.ExceptionAsync(() => targets.RunAsync(new List <string> {
                "third"
            }, console = new TestConsole())));

            "Then the operation fails"
            .x(() => Assert.NotNull(exception));

            "And I am told that the first non-existent target could not be found"
            .x(() => Assert.Contains("non-existing, required by second", exception.Message));

            "And I am told that the second non-existent target could not be found"
            .x(() => Assert.Contains("also-non-existing, required by third", exception.Message));

            "And the other targets are not run"
            .x(() => Assert.False(anyRan));
        }
예제 #5
0
        public void MultipleNonExistent(
            TargetCollection targets, TestConsole console, bool existing, Exception exception)
        {
            "Given an existing target"
            .x(() => Ensure(ref targets).Add(CreateTarget(nameof(existing), () => existing = true)));

            "When I run that target and two non-existent targets"
            .x(async() => exception = await Record.ExceptionAsync(() => targets.RunAsync(
                                                                      new List <string> {
                nameof(existing), "non-existing", "also-non-existing"
            },
                                                                      console = new TestConsole())));

            "Then the operation fails"
            .x(() => Assert.NotNull(exception));

            "Then I am told that the first non-existent target could not be found"
            .x(() => Assert.Contains("non-existing", exception.Message));

            "Then I am told that the second non-existent target could not be found"
            .x(() => Assert.Contains("also-non-existing", exception.Message));

            "And the existing target is not run"
            .x(() => Assert.False(existing));
        }
예제 #6
0
        public void NestedDependencies(TargetCollection targets, TestConsole console, List <string> ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => Ensure(ref ran).Add("first"))));

            "And a second target which depends on the first target"
            .x(() => targets.Add(CreateTarget("second", new[] { "first" }, () => Ensure(ref ran).Add("second"))));

            "And a third target which depends on the second target"
            .x(() => targets.Add(CreateTarget("third", new[] { "second" }, () => Ensure(ref ran).Add("third"))));

            "When I run the third target"
            .x(() => targets.RunAsync(new List <string> {
                "third"
            }, console = new TestConsole()));

            "Then all targets are run"
            .x(() => Assert.Equal(3, ran.Count));

            "And the first target is run first"
            .x(() => Assert.Equal("first", ran[0]));

            "And the second target is run second"
            .x(() => Assert.Equal("second", ran[1]));

            "And the third target is run third"
            .x(() => Assert.Equal("third", ran[2]));
        }
예제 #7
0
        public void Default(TargetCollection targets, bool @default, bool other)
        {
            "Given a default target"
            .x(() => Ensure(ref targets).Add(CreateTarget("default", () => @default = true)));

            "And another target"
            .x(() => targets.Add(CreateTarget(nameof(other), () => other = true)));

            "When I run without specifying any target names"
            .x(() => targets.RunAsync(new List <string>(), default, default));
예제 #8
0
        public void MixingCase(TargetCollection targets, bool first, bool second)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => first = true)));

            "And another target which depends on the first target with a different case"
            .x(() => targets.Add(CreateTarget("second", new[] { "FIRST" }, () => second = true)));

            "When I run the second target with a different case"
            .x(() => targets.RunAsync(new[] { "SECOND" }, default, default, default));
예제 #9
0
        public void WithoutInputs(TargetCollection targets, bool ran)
        {
            "Given a target with missing inputs"
            .x(() => Ensure(ref targets).Add(CreateTarget("default", Enumerable.Empty <object>(), input => ran = true)));

            "When I run the target"
            .x(() => targets.RunAsync(new List <string>(), new TestConsole()));

            "Then the target is not run"
            .x(() => Assert.False(ran));
        }
예제 #10
0
        public void DryRun(TargetCollection targets, bool ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("target", () => ran = true)));

            "When I run the target specifying a dry run"
            .x(() => targets.RunAsync(new List <string> {
                "target", "-n"
            }, default));

            "Then the target is not run"
            .x(() => Assert.False(ran));
        }
예제 #11
0
        public void FlatDependencies(TargetCollection targets, List <string> ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => Ensure(ref ran).Add("first"))));

            "And a second target"
            .x(() => targets.Add(CreateTarget("second", () => Ensure(ref ran).Add("second"))));

            "And a third target which depends on the first and second target"
            .x(() => targets.Add(CreateTarget("third", new[] { "first", "second" }, () => Ensure(ref ran).Add("third"))));

            "When I run the third target"
            .x(() => targets.RunAsync(new List <string> {
                "third"
            }, default, default, default));
예제 #12
0
        public void SelfDependency(TargetCollection targets, Exception exception)
        {
            "Given a target which depends on itself"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", new[] { "first" })));

            "When I run the target"
            .x(async() => exception = await Record.ExceptionAsync(() => targets.RunAsync(new List <string> {
                "first"
            }, null)));

            "Then the operation fails"
            .x(() => Assert.NotNull(exception));

            "And I am told that the circular dependency was detected"
            .x(() => Assert.Contains("first -> first", exception.Message));
        }
예제 #13
0
        public void WithInputs(TargetCollection targets, List <int> inputsReceived)
        {
            "Given a target with inputs 1 and 2"
            .x(() => Ensure(ref targets).Add(CreateTarget("default", new[] { 1, 2 }, input => Ensure(ref inputsReceived).Add(input))));

            "When I run the target"
            .x(() => targets.RunAsync(new List <string>(), new TestConsole()));

            "Then the target is run twice"
            .x(() => Assert.Equal(2, inputsReceived.Count));

            "And target was run with 1 first"
            .x(() => Assert.Equal(1, inputsReceived[0]));

            "And target was run with 2 second"
            .x(() => Assert.Equal(2, inputsReceived[1]));
        }
예제 #14
0
        public void Default(TargetCollection targets, TestConsole console, bool @default, bool other)
        {
            "Given a default target"
            .x(() => Ensure(ref targets).Add(CreateTarget("default", () => @default = true)));

            "And another target"
            .x(() => targets.Add(CreateTarget(nameof(other), () => other = true)));

            "When I run without specifying any target names"
            .x(() => targets.RunAsync(new List <string>(), console = new TestConsole()));

            "Then the default target is run"
            .x(() => Assert.True(@default));

            "But the other target is not run"
            .x(() => Assert.False(other));
        }
예제 #15
0
        public void SkippingDependencies(TargetCollection targets, TestConsole console, List <string> ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => Ensure(ref ran).Add("first"))));

            "And a second target which depends on the first target and a non-existent target"
            .x(() => targets.Add(CreateTarget("second", new[] { "first", "non-existent" }, () => Ensure(ref ran).Add("second"))));

            "When I run the second target, skipping dependencies"
            .x(() => targets.RunAsync(new List <string> {
                "second", "-s"
            }, console = new TestConsole()));

            "Then the second target is run"
            .x(() => Assert.Contains("second", ran));

            "But the first target is not run"
            .x(() => Assert.DoesNotContain("first", ran));
        }
예제 #16
0
        public void UnknownOptions(TargetCollection targets, bool ran, Exception exception)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("target", () => ran = true)));

            "When I run the target specifying unknown options"
            .x(async() => exception = await Record.ExceptionAsync(() => targets.RunAsync(new List <string> {
                "target", "-b", "-z"
            }, default)));

            "Then the operation fails"
            .x(() => Assert.NotNull(exception));

            "Then I am told that the option is unknown"
            .x(() => Assert.Contains("Unknown options -b -z", exception.Message));

            "Then I am told how to get help"
            .x(() => Assert.Contains(". \"--help\" for usage", exception.Message));

            "And the target is not run"
            .x(() => Assert.False(ran));
        }
예제 #17
0
        public void DoubleDependency(TargetCollection targets, TestConsole console, List <string> ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => Ensure(ref ran).Add("first"))));

            "And a second target which depends on the first target twice"
            .x(() => targets.Add(CreateTarget("second", new[] { "first", "first" }, () => Ensure(ref ran).Add("second"))));

            "When I run the second target"
            .x(() => targets.RunAsync(new List <string> {
                "second"
            }, console = new TestConsole()));

            "Then both targets are run once"
            .x(() => Assert.Equal(2, ran.Count));

            "And the first target is run first"
            .x(() => Assert.Equal("first", ran[0]));

            "And the second target is run second"
            .x(() => Assert.Equal("second", ran[1]));
        }
예제 #18
0
        public void DependencyOrderWhenSkipping(TargetCollection targets, TestConsole console, List <string> ran)
        {
            "Given a target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", () => Ensure(ref ran).Add("first"))));

            "And a second target which depends on the first target"
            .x(() => targets.Add(CreateTarget("second", new[] { "first" }, () => Ensure(ref ran).Add("second"))));

            "When I run the second and first targets, skipping dependencies"
            .x(() => targets.RunAsync(new List <string> {
                "--skip-dependencies", "second", "first"
            }, console = new TestConsole()));

            "Then all targets are run"
            .x(() => Assert.Equal(2, ran.Count));

            "And the first target is run first"
            .x(() => Assert.Equal("first", ran[0]));

            "And the second target is run second"
            .x(() => Assert.Equal("second", ran[1]));
        }
예제 #19
0
        public void CircularDependency(TargetCollection targets, Exception exception)
        {
            "Given a target which depends on a third target"
            .x(() => Ensure(ref targets).Add(CreateTarget("first", new[] { "third" })));

            "And a second target which depends on the first target"
            .x(() => targets.Add(CreateTarget("second", new[] { "first" })));

            "And a third target which depends on the second target"
            .x(() => targets.Add(CreateTarget("third", new[] { "second" })));

            "When I run the third target"
            .x(async() => exception = await Record.ExceptionAsync(() => targets.RunAsync(new List <string> {
                "third"
            }, default)));

            "Then the operation fails"
            .x(() => Assert.NotNull(exception));

            "And I am told that the circular dependency was detected"
            .x(() => Assert.Contains("first -> third -> second -> first", exception.Message));
        }
예제 #20
0
 public static Task RunTargetsAsync(IEnumerable <string> args) =>
 targets.RunAsync(args, new SystemConsole());
예제 #21
0
    public async Task <int> OnExecuteAsync()
    {
        Exception error = default;

        try
        {
            NeedMono             = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
            TestFlagsNonParallel = "-parallel none -maxthreads 1 ";
            // TestFlagsNonParallel = "-parallel none -maxthreads 1 -preenumeratetheories ";
            TestFlagsParallel = "";

            // Find the folder with the solution file
            BaseFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            while (true)
            {
                if (Directory.Exists(Path.Combine(BaseFolder, ".git")))
                {
                    break;
                }

                BaseFolder = Path.GetDirectoryName(BaseFolder);
                if (BaseFolder == null)
                {
                    throw new InvalidOperationException("Could not locate a solution file in the directory hierarchy");
                }
            }

            ConsoleRunnerExe   = Path.Combine(BaseFolder, "src", "xunit.v3.runner.console", "bin", ConfigurationText, "net472", "merged", "xunit.v3.runner.console.exe");
            ConsoleRunner32Exe = Path.Combine(BaseFolder, "src", "xunit.v3.runner.console", "bin", ConfigurationText + "_x86", "net472", "merged", "xunit.v3.runner.console.x86.exe");

            // Dependent folders
            PackageOutputFolder = Path.Combine(BaseFolder, "artifacts", "packages");
            Directory.CreateDirectory(PackageOutputFolder);

            TestOutputFolder = Path.Combine(BaseFolder, "artifacts", "test");
            Directory.CreateDirectory(TestOutputFolder);

            // Parse the targets
            var targetNames = Targets.Select(x => x.ToString()).ToList();

            // Turn off test parallelization in CI, for more repeatable test timing
            if (Targets.Contains(BuildTarget.CI))
            {
                TestFlagsParallel = TestFlagsNonParallel;
            }

            // Find target classes
            var targetCollection = new TargetCollection();
            var targets
                = Assembly.GetExecutingAssembly()
                  .ExportedTypes
                  .Select(x => new { type = x, attr = x.GetCustomAttribute <TargetAttribute>() })
                  .Where(x => x.attr != null);

            foreach (var target in targets)
            {
                var method = target.type.GetRuntimeMethod("OnExecute", new[] { typeof(BuildContext) });

                if (method == null)
                {
                    targetCollection.Add(new Target(target.attr.TargetName, target.attr.DependentTargets));
                }
                else
                {
                    targetCollection.Add(new ActionTarget(target.attr.TargetName, target.attr.DependentTargets, async() =>
                    {
                        var sw = Stopwatch.StartNew();

                        try
                        {
                            await(Task) method.Invoke(null, new[] { this });
                        }
                        finally
                        {
                            if (Timing)
                            {
                                WriteLineColor(ConsoleColor.Cyan, $"TIMING: Target '{target.attr.TargetName}' took {sw.Elapsed}{Environment.NewLine}");
                            }
                        }
                    }));
                }
            }

            var swTotal = Stopwatch.StartNew();

            // Let Bullseye run the target(s)
            await targetCollection.RunAsync(targetNames, SkipDependencies, dryRun : false, parallel : false, new NullLogger(), _ => false);

            WriteLineColor(ConsoleColor.Green, $"==> Build success! <=={Environment.NewLine}");

            if (Timing)
            {
                WriteLineColor(ConsoleColor.Cyan, $"TIMING: Build took {swTotal.Elapsed}{Environment.NewLine}");
            }

            return(0);
        }
        catch (Exception ex)
        {
            error = ex;
            while (error is TargetInvocationException || error is TargetFailedException)
            {
                error = error.InnerException;
            }
        }

        Console.WriteLine();

        if (error is NonZeroExitCodeException nonZeroExit)
        {
            WriteLineColor(ConsoleColor.Red, "==> Build failed! <==");
            return(nonZeroExit.ExitCode);
        }

        WriteLineColor(ConsoleColor.Red, $"==> Build failed! An unhandled exception was thrown <==");
        Console.WriteLine(error.ToString());
        return(-1);
    }
예제 #22
0
 public static Task RunTargetsAsync(IEnumerable <string> args) =>
 targets.RunAsync(args);
예제 #23
0
    async Task <int> OnExecuteAsync()
    {
        Exception error = default;

        try
        {
            NeedMono = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

            // Find the folder with the solution file
            BaseFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            while (true)
            {
                if (Directory.GetFiles(BaseFolder, "*.sln").Count() != 0)
                {
                    break;
                }

                BaseFolder = Path.GetDirectoryName(BaseFolder);
                if (BaseFolder == null)
                {
                    throw new InvalidOperationException("Could not locate a solution file in the directory hierarchy");
                }
            }

            // Dependent folders
            PackageOutputFolder = Path.Combine(BaseFolder, "artifacts", "packages");
            Directory.CreateDirectory(PackageOutputFolder);

            TestOutputFolder = Path.Combine(BaseFolder, "artifacts", "test");
            Directory.CreateDirectory(TestOutputFolder);

            var homeFolder = NeedMono
                                ? Environment.GetEnvironmentVariable("HOME")
                                : Environment.GetEnvironmentVariable("USERPROFILE");

            var nuGetCliFolder = Path.Combine(homeFolder, ".nuget", "cli", NuGetVersion);
            Directory.CreateDirectory(nuGetCliFolder);

            NuGetExe = Path.Combine(nuGetCliFolder, "nuget.exe");
            NuGetUrl = $"https://dist.nuget.org/win-x86-commandline/v{NuGetVersion}/nuget.exe";

            // Parse the targets and Bullseye-specific arguments
            var bullseyeArguments = Targets.Select(x => x.ToString());
            if (SkipDependencies)
            {
                bullseyeArguments = bullseyeArguments.Append("--skip-dependencies");
            }

            // Find target classes
            var targetCollection = new TargetCollection();
            var targets
                = Assembly.GetExecutingAssembly()
                  .ExportedTypes
                  .Select(x => new { type = x, attr = x.GetCustomAttribute <TargetAttribute>() })
                  .Where(x => x.attr != null);

            foreach (var target in targets)
            {
                var method = target.type.GetRuntimeMethod("OnExecute", new[] { typeof(BuildContext) });

                if (method == null)
                {
                    targetCollection.Add(new Target(target.attr.TargetName, target.attr.DependentTargets));
                }
                else
                {
                    targetCollection.Add(new ActionTarget(target.attr.TargetName, target.attr.DependentTargets, () => (Task)method.Invoke(null, new[] { this })));
                }
            }

            // Let Bullseye run the target(s)
            await targetCollection.RunAsync(bullseyeArguments.ToList(), SkipDependencies, false, false, new NullLogger(), null);

            return(0);
        }
        catch (Exception ex)
        {
            error = ex;
            while (error is TargetInvocationException || error is TargetFailedException)
            {
                error = error.InnerException;
            }
        }

        Console.WriteLine();

        if (error is NonZeroExitCodeException nonZeroExit)
        {
            WriteLineColor(ConsoleColor.Red, "==> Build failed! <==");
            return(nonZeroExit.ExitCode);
        }

        WriteLineColor(ConsoleColor.Red, $"==> Build failed! An unhandled exception was thrown <==");
        Console.WriteLine(error.ToString());
        return(-1);
    }
예제 #24
0
 /// <summary>
 /// Runs the previously specified targets.
 /// In most cases, <see cref="RunTargetsAndExitAsync(IEnumerable{string}, Func{Exception, bool})"/> should be used instead of this method.
 /// This method should only be used if continued code execution after running targets is specifically required.
 /// </summary>
 /// <param name="args">The command line arguments.</param>
 /// <param name="messageOnly">
 /// A predicate that is called when an exception is thrown.
 /// Return <c>true</c> to display only the exception message instead instead of the full exception details.
 /// </param>
 /// <returns>A <see cref="Task"/> that represents the asynchronous running of the targets.</returns>
 public static Task RunTargetsWithoutExitingAsync(IEnumerable <string> args, Func <Exception, bool> messageOnly) =>
 targets.RunAsync(args, messageOnly);
예제 #25
0
 /// <summary>
 /// Runs the previously specified targets.
 /// In most cases, <see cref="RunTargetsAndExitAsync(IEnumerable{string}, Func{Exception, bool}, string)"/> should be used instead of this method.
 /// This method should only be used if continued code execution after running targets is specifically required.
 /// </summary>
 /// <param name="args">The command line arguments.</param>
 /// <param name="messageOnly">
 /// A predicate that is called when an exception is thrown.
 /// Return <c>true</c> to display only the exception message instead instead of the full exception details.
 /// </param>
 /// <param name="logPrefix">
 /// The prefix to use for log messages.
 /// If not specified or <c>null</c>, the name of the entry assembly will be used, as returned by <see cref="System.Reflection.Assembly.GetEntryAssembly"/>.
 /// If the entry assembly is <c>null</c>, the default prefix of "Bullseye" is used.
 /// </param>
 /// <returns>A <see cref="Task"/> that represents the asynchronous running of the targets.</returns>
 public static Task RunTargetsWithoutExitingAsync(IEnumerable <string> args, Func <Exception, bool> messageOnly = null, string logPrefix = null) =>
 targets.RunAsync(args, messageOnly, logPrefix);
예제 #26
0
    public async Task <int> OnExecuteAsync()
    {
        Exception error = default;

        try
        {
            NeedMono             = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
            TestFlagsNonParallel = "-parallel none -maxthreads 1";
            // TestFlagsParallel = "-parallel all -maxthreads 16";
            TestFlagsParallel = "-parallel collections -maxthreads 16";

            // Find the folder with the solution file
            BaseFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            while (true)
            {
                if (Directory.GetFiles(BaseFolder, "*.sln").Count() != 0)
                {
                    break;
                }

                BaseFolder = Path.GetDirectoryName(BaseFolder);
                if (BaseFolder == null)
                {
                    throw new InvalidOperationException("Could not locate a solution file in the directory hierarchy");
                }
            }

            // Dependent folders
            PackageOutputFolder = Path.Combine(BaseFolder, "artifacts", "packages");
            Directory.CreateDirectory(PackageOutputFolder);

            TestOutputFolder = Path.Combine(BaseFolder, "artifacts", "test");
            Directory.CreateDirectory(TestOutputFolder);

            // Parse the targets
            var targetNames = Targets.Select(x => x.ToString()).ToList();

            // Turn off test parallelization in CI, for more repeatable test timing
            if (Targets.Contains(BuildTarget.CI))
            {
                TestFlagsParallel = TestFlagsNonParallel;
            }

            // Find target classes
            var targetCollection = new TargetCollection();

            foreach (var target in Assembly.GetExecutingAssembly()
                     .ExportedTypes
                     .Select(x => new { type = x, attr = x.GetCustomAttribute <TargetAttribute>() })
                     .Where(x => x.attr != null))
            {
                var method = target.type.GetRuntimeMethod("OnExecute", new[] { typeof(BuildContext) });

                if (method == null)
                {
                    targetCollection.Add(new Target(target.attr.TargetName, target.attr.DependentTargets));
                }
                else
                {
                    targetCollection.Add(new ActionTarget(target.attr.TargetName, target.attr.DependentTargets, () => (Task)method.Invoke(null, new[] { this })));
                }
            }

            // Let Bullseye run the target(s)
            await targetCollection.RunAsync(targetNames, SkipDependencies, dryRun : false, parallel : false, new NullLogger(), _ => false);

            return(0);
        }
        catch (Exception ex)
        {
            error = ex;
            while (error is TargetInvocationException || error is TargetFailedException)
            {
                error = error.InnerException;
            }
        }

        Console.WriteLine();

        if (error is NonZeroExitCodeException nonZeroExit)
        {
            WriteLineColor(ConsoleColor.Red, "==> Build failed! <==");
            return(nonZeroExit.ExitCode);
        }

        WriteLineColor(ConsoleColor.Red, $"==> Build failed! An unhandled exception was thrown <==");
        Console.WriteLine(error.ToString());
        return(-1);
    }