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