001/* 002 * JDrupes Builder 003 * Copyright (C) 2025, 2026 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 com.google.common.flogger.FluentLogger; 022import static com.google.common.flogger.LazyArgs.lazy; 023import java.io.PrintStream; 024import java.nio.file.Path; 025import java.util.Collection; 026import java.util.EnumSet; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Properties; 030import java.util.concurrent.CompletableFuture; 031import java.util.concurrent.ExecutorService; 032import java.util.concurrent.Executors; 033import java.util.concurrent.atomic.AtomicBoolean; 034import java.util.stream.Collectors; 035import java.util.stream.Stream; 036import org.apache.commons.cli.CommandLine; 037import org.jdrupes.builder.api.BuildContext; 038import org.jdrupes.builder.api.BuildException; 039import org.jdrupes.builder.api.ConfigurationException; 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.StatusLine; 047import org.jdrupes.builder.core.console.SplitConsole; 048 049/// A context for building. 050/// 051public class DefaultBuildContext implements BuildContext { 052 053 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 054 @SuppressWarnings("PMD.FieldNamingConventions") 055 private static final ScopedValue<AtomicBoolean> providerInvocationAllowed 056 = ScopedValue.newInstance(); 057 private final FutureStreamCache cache; 058 private ExecutorService executor 059 = Executors.newVirtualThreadPerTaskExecutor(); 060 private final Path buildRoot; 061 private final Properties jdbldProperties; 062 private final CommandLine commandLine; 063 private final AwaitableCounter executingFutureStreams 064 = new AwaitableCounter(); 065 private final SplitConsole console; 066 private final CompletableFuture<AbstractRootProject> buildProject 067 = new CompletableFuture<>(); 068 @SuppressWarnings("PMD.FieldNamingConventions") 069 private static final ScopedValue<RequestChainLink> requestChainEnd 070 = ScopedValue.newInstance(); 071 072 static { 073 ScopedValueContext.add(requestChainEnd); 074 } 075 076 /// A link in the call chain. 077 /// 078 /// @param previous the previous 079 /// @param invocation the invocation 080 /// 081 public record RequestChainLink(RequestChainLink previous, 082 ProviderInvocation<?> invocation) { 083 } 084 085 /// Instantiates a new default build. By default, the build uses 086 /// a virtual thread per task executor. 087 /// 088 /* default */ DefaultBuildContext(Path buildRoot, 089 Properties jdbldProperties, CommandLine commandLine) { 090 this.buildRoot = buildRoot; 091 this.jdbldProperties = jdbldProperties; 092 this.commandLine = commandLine; 093 cache = new FutureStreamCache(); 094 console = SplitConsole.open(); 095 } 096 097 /// Returns the executor service used by this build to create futures. 098 /// 099 /// @return the executor service 100 /// 101 public ExecutorService executor() { 102 return executor; 103 } 104 105 /// Sets the executor service used by this build to create futures. 106 /// 107 /// @param executor the executor 108 /// 109 public void executor(ExecutorService executor) { 110 this.executor = executor; 111 } 112 113 /// Executing future streams. 114 /// 115 /// @return the awaitable counter 116 /// 117 public AwaitableCounter executingFutureStreams() { 118 return executingFutureStreams; 119 } 120 121 /// Returns the build root. 122 /// 123 /// @return the path 124 /// 125 public Path buildRoot() { 126 return buildRoot; 127 } 128 129 @Override 130 public CommandLine commandLine() { 131 return commandLine; 132 } 133 134 @Override 135 public String property(String name, String defaultValue) { 136 return jdbldProperties.getProperty(name, 137 defaultValue); 138 } 139 140 /// Start request chain. 141 /// 142 /// @param carriers the carriers 143 /// @return the scoped value. carrier 144 /// 145 public ScopedValue.Carrier startRequestChain(ScopedValue.Carrier carriers) { 146 if (requestChainEnd.isBound()) { 147 throw new ConfigurationException() 148 .message("Request chain is already bound."); 149 } 150 return carriers.where(requestChainEnd, 151 new RequestChainLink(null, ProviderInvocation.LAUNCH)); 152 } 153 154 /// Return a carrier with this context available from [#context] and 155 /// the provider invocation allowed flag set. 156 /// 157 /// @param carrier the carrier 158 /// @return the augmented carrier 159 /// 160 /* default */ ScopedValue.Carrier inScopeForProviderCall() { 161 return ScopedValue 162 .where(providerInvocationAllowed, new AtomicBoolean(true)); 163 } 164 165 /* default */ SplitConsole console() { 166 return console; 167 } 168 169 @Override 170 public StatusLine statusLine() { 171 return FutureStream.statusLine.orElse(SplitConsole.nullStatusLine()); 172 } 173 174 @Override 175 public PrintStream out() { 176 return console().out(); 177 } 178 179 @Override 180 public PrintStream error() { 181 return console().err(); 182 } 183 184 @Override 185 public <T extends Resource> Stream<T> resources(ResourceProvider provider, 186 ResourceRequest<T> request) { 187 // Normalize request, non-project providers don't get intends 188 var invocation = new ProviderInvocation<>(provider, 189 provider instanceof Project || request.uses().isEmpty() ? request 190 : request.using(EnumSet.noneOf(Intent.class))); 191 return inScopeForProviderCall() 192 .call(() -> inResourcesContext(invocation)); 193 } 194 195 @SuppressWarnings({ "PMD.AvoidSynchronizedStatement" }) 196 private <T extends Resource> Stream<T> inResourcesContext( 197 ProviderInvocation<T> invocation) { 198 if (invocation.provider() instanceof Project) { 199 // As a project's provide only delegates to other providers 200 // it is inefficient to invoke it asynchronously. Nevertheless, 201 // SPI must be invoked lazily. 202 var snapshot = ScopedValueContext.snapshot(); 203 return LazyCollectionStream.of( 204 () -> snapshot.where(providerInvocationAllowed, 205 new AtomicBoolean(true)).call(() -> invokeSpi(invocation))); 206 } 207 if (!invocation.request().type().equals(CleanlinessType)) { 208 return cache.computeIfAbsent(invocation, 209 k -> new FutureStream<T>(k)).stream(); 210 } 211 212 // Special handling for cleanliness. Clean one by one... 213 synchronized (executor) { 214 // Await completion of all generating threads 215 try { 216 executingFutureStreams().await(0); 217 } catch (InterruptedException e) { 218 throw new BuildException().cause(e); 219 } 220 } 221 var result = invokeSpi(invocation).stream(); 222 // Purge cached results from provider 223 cache.purge(invocation.provider()); 224 return result; 225 } 226 227 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 228 private <T extends Resource> Collection<T> 229 invokeSpi(ProviderInvocation<T> invocation) { 230 return ScopedValue.where(requestChainEnd, new RequestChainLink( 231 requestChainEnd.orElseThrow(() -> new ConfigurationException() 232 .cause(new IllegalStateException()) 233 .message("No request chain end")), 234 invocation)).call(() -> { 235 logger.atFinest().log("Request chain: %s", 236 lazy(() -> requestChain() 237 .stream().map(ProviderInvocation::toString) 238 .collect(Collectors.joining(" ≪ ")))); 239 var prev = requestChainEnd.get().previous; 240 while (prev != null) { 241 if (invocation.equals(prev.invocation())) { 242 throw new BuildException().message("Request loop: %s", 243 requestChain().stream() 244 .map(ProviderInvocation::toString) 245 .collect(Collectors.joining(" ≪ "))); 246 } 247 prev = prev.previous; 248 } 249 return ((AbstractProvider) invocation.provider()).toSpi() 250 .provide(invocation.request()); 251 }); 252 } 253 254 /* default */ List<ProviderInvocation<?>> requestChain() { 255 var cur = requestChainEnd.isBound() ? requestChainEnd.get() : null; 256 List<ProviderInvocation<?>> result = new LinkedList<>(); 257 while (cur != null) { 258 result.add(cur.invocation); 259 cur = cur.previous; 260 } 261 return result; 262 } 263 264 /// Checks if is provider invocation is allowed. Clears the 265 /// allowed flag to also detect nested invocations. 266 /// 267 /// @return true, if is provider invocation allowed 268 /// 269 public static boolean isProviderInvocationAllowed() { 270 return providerInvocationAllowed.isBound() 271 && providerInvocationAllowed.get().getAndSet(false); 272 } 273 274 @Override 275 public void close() { 276 executor.shutdownNow(); 277 console.close(); 278 } 279 280 /* default */ CompletableFuture<AbstractRootProject> buildProject() { 281 return buildProject; 282 } 283}