Nuke.Cola
Loading...
Searching...
No Matches
Plugins.cs
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Linq.Expressions;
5using System.Reflection;
6using System.Threading.Tasks;
7using Microsoft.Build.Tasks;
8using Nuke.Common;
9using Nuke.Common.IO;
10using Nuke.Common.Utilities;
11using Nuke.Common.Utilities.Collections;
12using Serilog;
13
15
16/// <summary>
17/// Delegate for the `Execute` entrypoint NUKE provides for build classes
18/// </summary>
19public delegate int MainExecute<T>(params Expression<Func<T, Target>>[] defaultTargetExpressions) where T : NukeBuild, new();
20
21/// <summary>
22/// This class contains the main entry point for builds supporting plugins.
23/// </summary>
24public static class Plugins
25{
26 private const string OutputBuildClass = nameof(OutputBuildClass);
27
28 private const string ExecuteWithPlugins = nameof(ExecuteWithPlugins);
29 private const string ReuseCompiled = nameof(ReuseCompiled);
30
31 private static string? GetCSharpName(Type type)
32 => type.Namespace == null ? type.Name : type.FullName;
33
34 private static bool AttemptReuseCompiledPlugins()
35 => EnvironmentInfo.HasArgument(ReuseCompiled)
36 || EnvironmentInfo.GetVariable<int>("REUSE_COMPILED") == 1;
37
38 /// <summary>
39 /// Use this instead of the regular Execute method in your main function if you want to support
40 /// build plugins.
41 /// </summary>
42 /// <param name="defaultExecute">
43 /// If no build plugins are found, the provided delegate will be executed instead
44 /// </param>
45 /// <typeparam name="T">The type of the main build class</typeparam>
46 /// <returns>The error code or 0 on success</returns>
47 /// <remarks>
48 /// If plugins are found they're collected into an intermediate C# script which defines a
49 /// build class inheriting from the provided main build class and implementing all the build
50 /// interfaces defined by each plugin. This however also means build interfaces cannot have
51 /// non-default-implemented members, so they behave more like composition in this case.
52 ///
53 /// The main NUKE execute method is then called from within this intermediate class.
54 /// These plugins can then interact with the main build targets if they can reference to
55 /// the main build assembly, either directly or more elegantly through a Nuget package.
56 ///
57 /// Use `--ReuseCompiledPlugins` argument or `REUSE_COMPILED
58 /// </remarks>
59 public static int Execute<T>(MainExecute<T> defaultExecute) where T : NukeBuild, new()
60 {
61 BuildContext context = new(NukeBuild.TemporaryDirectory, NukeBuild.RootDirectory);
62 var intermediateScriptPath = context.Temporary / "Intermediate.csx";
63 var intermediateAssembliesRoot = context.Temporary / "IntermediateAssemblies";
64 var noPluginsFoundIndicator = context.Temporary / "NoPlugins.txt";
65
66 if (AttemptReuseCompiledPlugins())
67 {
68 if (noPluginsFoundIndicator.FileExists())
69 {
70 $"Skipped building build plugins entirely. Executing build from {typeof(T).Name}".Log();
71 return defaultExecute();
72 }
73
74 if (intermediateScriptPath.FileExists())
75 {
76 var (dllPath, _, _) = DotnetCommon.GetDllLocationOfScript(
77 intermediateScriptPath,
78 intermediateAssembliesRoot
79 );
80 if (dllPath.FileExists())
81 {
82 "Reusing plugins which has been already compiled.".Log();
83 return ExecuteExternal(dllPath);
84 }
85 }
86 }
87
88 noPluginsFoundIndicator.ExistingFile()?.DeleteFile();
89
90 var engines = new IProvidePlugins[]
91 {
95 };
96
97 foreach(var engine in engines)
98 {
99 engine.InitializeEngine(context);
100 }
101
102 var sources = engines
103 .SelectMany(e => e.GatherPlugins(context))
104 .ForEachLazy(p => $"Using plugin {p.SourcePath}".Log())
105 .ToList();
106
107 if (sources.IsEmpty())
108 {
109 $"No build plugins were found. Executing build from {typeof(T).Name}".Log();
110 noPluginsFoundIndicator.WriteAllText("1");
111 return defaultExecute();
112 }
113
114 sources.AsParallel().ForAll(s => s.Compile(context));
115
116 var buildInterfaces = sources.SelectMany(s => s.BuildInterfaces);
117 var assemblyPaths = buildInterfaces
118 .DistinctBy(i => i.ToString());
119
120 var dllRefs = string.Join(
121 Environment.NewLine,
122 assemblyPaths
123 .Where(p => p.Source != null)
124 .Select(p => p.ImportViaSource
125 ? $"\n// dll: {p.Interface.Assembly.Location}\n#load \"{p}\""
126 : $"#r \"{p}\""
127 )
128 );
129 var interfaces = string.Join(", ", buildInterfaces.Select(i => GetCSharpName(i.Interface)));
130 var baseName = GetCSharpName(typeof(T));
131 var currentAssembly = Assembly.GetEntryAssembly()?.Location;
132
133 Assert.NotNull(currentAssembly);
134
135 var intermediateScriptSource =
136 $$"""
137 #r "nuget: System.Linq.Expressions, 4.3.0"
138 #r "{{currentAssembly}}"
139 {{dllRefs}}
140
141 public class {{OutputBuildClass}} : {{baseName}}, {{interfaces}}
142 {
143 public static int {{ExecuteWithPlugins}}() => Execute<{{OutputBuildClass}}>();
144 }
145 """;
146
147 File.WriteAllText(intermediateScriptPath, intermediateScriptSource);
148 // intermediateAssembliesRoot.CreateOrCleanDirectory();
149
150 "Preparing intermediate assembly".Log();
151 return ExecuteExternal(
152 DotnetCommon.CompileScript(intermediateScriptPath, intermediateAssembliesRoot)
153 );
154 }
155
156 private static int ExecuteExternal(AbsolutePath dllPath)
157 {
158 var intermediateAssembly = Assembly.LoadFrom(dllPath);
159
160 var intermediateClass = intermediateAssembly.GetTypes().First(t => t.Name == OutputBuildClass);
161 return (int) intermediateClass?.GetMethod(ExecuteWithPlugins)?.Invoke(null, null)!;
162 }
163}
Gather build plugins defined as single file C# scripts following the file name format *....
static AbsolutePath CompileScript(AbsolutePath scriptPath, AbsolutePath outputDirIn)
Compiles a C# script into a DLL with dotnet-script, which needs to be installed at least into the con...
Gather build plugins defined as a .NET project following the file name format *.Nuke....
Gather build interfaces from main assembly which should be implicitly included in the main build clas...
This class contains the main entry point for builds supporting plugins.
Definition Plugins.cs:25
static int Execute< T >(MainExecute< T > defaultExecute)
Use this instead of the regular Execute method in your main function if you want to support build plu...
Definition Plugins.cs:59
Implementations of this interface must represent a plugin system which might have a distinct logic an...
record class BuildContext(AbsolutePath Temporary, AbsolutePath Root)
Local paths for plugin discovery and compilation.
delegate int MainExecute< T >(params Expression< Func< T, Target > >[] defaultTargetExpressions)
Delegate for the Execute entrypoint NUKE provides for build classes.