001/* 002 * JDrupes Builder 003 * Copyright (C) 2025 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.builder.core; 020 021import java.io.PrintStream; 022import java.nio.file.Path; 023import java.util.EnumSet; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.Properties; 029import java.util.concurrent.CompletableFuture; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.ExecutorService; 032import java.util.concurrent.Executors; 033import java.util.concurrent.Future; 034import java.util.concurrent.atomic.AtomicBoolean; 035import java.util.stream.Collectors; 036import java.util.stream.Stream; 037import org.apache.commons.cli.CommandLine; 038import org.jdrupes.builder.api.BuildContext; 039import org.jdrupes.builder.api.BuildException; 040import org.jdrupes.builder.api.Intent; 041import org.jdrupes.builder.api.Project; 042import org.jdrupes.builder.api.Resource; 043import org.jdrupes.builder.api.ResourceProvider; 044import org.jdrupes.builder.api.ResourceRequest; 045import static org.jdrupes.builder.api.ResourceType.CleanlinessType; 046import org.jdrupes.builder.api.RootProject; 047import org.jdrupes.builder.api.StatusLine; 048import org.jdrupes.builder.core.console.SplitConsole; 049 050/// A context for building. 051/// 052@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.TooManyMethods" }) 053public class DefaultBuildContext implements BuildContext { 054 055 @SuppressWarnings("PMD.FieldNamingConventions") 056 private static final ScopedValue<AtomicBoolean> providerInvocationAllowed 057 = ScopedValue.newInstance(); 058 private final FutureStreamCache cache; 059 private ExecutorService executor 060 = Executors.newVirtualThreadPerTaskExecutor(); 061 private final Path buildRoot; 062 private final Properties jdbldProperties; 063 private final CommandLine commandLine; 064 private final AwaitableCounter executingFutureStreams 065 = new AwaitableCounter(); 066 private final SplitConsole console; 067 @SuppressWarnings("PMD.FieldNamingConventions") 068 private static final ScopedValue< 069 DefaultBuildContext> scopedBuildContext = ScopedValue.newInstance(); 070 private final CompletableFuture<AbstractRootProject> buildProject 071 = new CompletableFuture<>(); 072 @SuppressWarnings("PMD.FieldNamingConventions") 073 private static final ScopedValue<CallChainLink> callChainEnd 074 = ScopedValue.newInstance(); 075 private final Map<ProviderInvocation<?>, CallChainLink> callChains 076 = new ConcurrentHashMap<>(); 077 078 static { 079 ScopedValueContext.add(scopedBuildContext, callChainEnd); 080 } 081 082 /// A link in the call chain. 083 /// 084 /// @param previous the previous 085 /// @param invocation the invocation 086 /// 087 public record CallChainLink(CallChainLink previous, 088 ProviderInvocation<?> invocation) { 089 } 090 091 /// Instantiates a new default build. By default, the build uses 092 /// a virtual thread per task executor. 093 /// 094 /* default */ DefaultBuildContext(Path buildRoot, 095 Properties jdbldProperties, CommandLine commandLine) { 096 this.buildRoot = buildRoot; 097 this.jdbldProperties = jdbldProperties; 098 this.commandLine = commandLine; 099 cache = new FutureStreamCache(); 100 console = SplitConsole.open(); 101 } 102 103 /// Returns the executor service used by this build to create futures. 104 /// 105 /// @return the executor service 106 /// 107 public ExecutorService executor() { 108 return executor; 109 } 110 111 /// Sets the executor service used by this build to create futures. 112 /// 113 /// @param executor the executor 114 /// 115 public void executor(ExecutorService executor) { 116 this.executor = executor; 117 } 118 119 /// Executing future streams. 120 /// 121 /// @return the awaitable counter 122 /// 123 public AwaitableCounter executingFutureStreams() { 124 return executingFutureStreams; 125 } 126 127 /// Returns the build root. 128 /// 129 /// @return the path 130 /// 131 public Path buildRoot() { 132 return buildRoot; 133 } 134 135 @Override 136 public CommandLine commandLine() { 137 return commandLine; 138 } 139 140 @Override 141 public String property(String name, String defaultValue) { 142 return jdbldProperties.getProperty(name, 143 defaultValue); 144 } 145 146 /// Returns the context. 147 /// 148 /// @return the optional 149 /// 150 public static Optional<DefaultBuildContext> context() { 151 if (scopedBuildContext.isBound()) { 152 return Optional.of(scopedBuildContext.get()); 153 } 154 return Optional.empty(); 155 } 156 157 /// Return a carrier with this context available from [#context]. 158 /// 159 /// @return the carrier 160 /// 161 public ScopedValue.Carrier inScope() { 162 return ScopedValue.where(scopedBuildContext, this); 163 } 164 165 /// Return a carrier with this context available from [#context] and 166 /// the provider invocation allowed flag set. 167 /// 168 /// @param carrier the carrier 169 /// @return the augmented carrier 170 /// 171 /* default */ ScopedValue.Carrier inScopeForProviderCall() { 172 return inScope() 173 .where(providerInvocationAllowed, new AtomicBoolean(true)); 174 } 175 176 /* default */ SplitConsole console() { 177 return console; 178 } 179 180 @Override 181 public StatusLine statusLine() { 182 return FutureStream.statusLine.orElse(SplitConsole.nullStatusLine()); 183 } 184 185 @Override 186 public PrintStream out() { 187 return console().out(); 188 } 189 190 @Override 191 public PrintStream error() { 192 return console().err(); 193 } 194 195 @Override 196 public <T extends Resource> Stream<T> resources(ResourceProvider provider, 197 ResourceRequest<T> request) { 198 // Normalize request, non-project providers don't get intends 199 var invocation = new ProviderInvocation<>(provider, 200 provider instanceof Project || request.uses().isEmpty() ? request 201 : request.using(EnumSet.noneOf(Intent.class))); 202 return ScopedValue.where(scopedBuildContext, this) 203 .where(providerInvocationAllowed, new AtomicBoolean(true)) 204 .where(callChainEnd, new CallChainLink( 205 callChainEnd.isBound() ? callChainEnd.get() 206 : new CallChainLink(null, ProviderInvocation.LAUNCH), 207 invocation)) 208 .call(() -> inResourcesContext(invocation)); 209 } 210 211 @SuppressWarnings({ "PMD.AvoidSynchronizedStatement", 212 "PMD.AvoidInstantiatingObjectsInLoops" }) 213 private <T extends Resource> Stream<T> inResourcesContext( 214 ProviderInvocation<T> invocation) { 215 var prev = callChainEnd().previous; 216 while (prev != null) { 217 if (invocation.equals(prev.invocation())) { 218 throw new BuildException().message("Request loop: %s", 219 callChain().stream().map(ProviderInvocation::toString) 220 .collect(Collectors.joining(" ≪ "))); 221 } 222 prev = prev.previous; 223 } 224 callChains.put(invocation, callChainEnd()); 225 if (invocation.provider() instanceof Project) { 226 // As a project's provide only delegates to other providers 227 // it is inefficient to invoke it asynchronously. Besides, it 228 // leads to recursive invocations of the project's deploy 229 // method too easily and results in a loop detection without 230 // there really being a loop. 231 return ((AbstractProvider) invocation.provider()).toSpi() 232 .provide(invocation.request()); 233 } 234 if (!invocation.request().type().equals(CleanlinessType)) { 235 return cache.computeIfAbsent( 236 invocation, k -> new FutureStream<T>(this, k)).stream(); 237 } 238 239 // Special handling for cleanliness. Clean one by one... 240 synchronized (executor) { 241 // Await completion of all generating threads 242 try { 243 executingFutureStreams().await(0); 244 } catch (InterruptedException e) { 245 throw new BuildException().cause(e); 246 } 247 } 248 var result = ((AbstractProvider) invocation.provider()).toSpi() 249 .provide(invocation.request()); 250 // Purge cached results from provider 251 cache.purge(invocation.provider()); 252 return result; 253 } 254 255 /// Returns the end of the current call chain. 256 /// 257 /// @return the call chain link 258 /// 259 /* default */ CallChainLink callChainEnd() { 260 return callChainEnd 261 .orElseThrow(() -> new IllegalStateException("No call chain")); 262 } 263 264 /* default */ List<ProviderInvocation<?>> callChain() { 265 var cur = callChainEnd(); 266 List<ProviderInvocation<?>> result = new LinkedList<>(); 267 while (cur != null) { 268 result.add(cur.invocation); 269 cur = cur.previous; 270 } 271 return result; 272 } 273 274 /* default */ List<ProviderInvocation<?>> 275 callChain(ProviderInvocation<?> running) { 276 var cur = callChains.get(running); 277 List<ProviderInvocation<?>> result = new LinkedList<>(); 278 while (cur != null) { 279 result.add(cur.invocation); 280 cur = cur.previous; 281 } 282 return result; 283 } 284 285 /// Checks if is provider invocation is allowed. Clears the 286 /// allowed flag to also detect nested invocations. 287 /// 288 /// @return true, if is provider invocation allowed 289 /// 290 public static boolean isProviderInvocationAllowed() { 291 return providerInvocationAllowed.isBound() 292 && providerInvocationAllowed.get().getAndSet(false); 293 } 294 295 @Override 296 public void close() { 297 executor.shutdownNow(); 298 console.close(); 299 } 300 301 /* default */ Future<AbstractRootProject> buildProject() { 302 return buildProject; 303 } 304 305 /// Creates and initializes the root project and the sub projects. 306 /// Adds the sub projects to the root project automatically. This 307 /// method should be used if the launcher detects the sub projects 308 /// e.g. by reflection and the root project does not add its sub 309 /// projects itself. 310 /// 311 /// @param buildRoot the build root 312 /// @param rootProject the root project 313 /// @param subprojects the sub projects 314 /// @param jdbldProps the builder properties 315 /// @param commandLine the command line 316 /// @return the root project 317 /// 318 public static AbstractRootProject createProjects( 319 Path buildRoot, Class<? extends RootProject> rootProject, 320 List<Class<? extends Project>> subprojects, 321 Properties jdbldProps, CommandLine commandLine) { 322 try { 323 return ScopedValue 324 .where(scopedBuildContext, 325 new DefaultBuildContext(buildRoot, jdbldProps, commandLine)) 326 .call(() -> { 327 var result = (AbstractRootProject) rootProject 328 .getConstructor().newInstance(); 329 result.unlockProviders(); 330 subprojects.forEach(result::project); 331 scopedBuildContext.get().buildProject.complete(result); 332 return result; 333 }); 334 } catch (SecurityException | NegativeArraySizeException 335 | IllegalArgumentException | ReflectiveOperationException e) { 336 throw new IllegalArgumentException(e); 337 } 338 } 339}