1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.cling.invoker.mvn;
20
21 import java.io.FileNotFoundException;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.util.ArrayList;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.function.Consumer;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 import java.util.stream.Collectors;
33
34 import org.apache.maven.InternalErrorException;
35 import org.apache.maven.Maven;
36 import org.apache.maven.api.Constants;
37 import org.apache.maven.api.MonotonicClock;
38 import org.apache.maven.api.annotations.Nullable;
39 import org.apache.maven.api.cli.InvokerRequest;
40 import org.apache.maven.api.cli.Logger;
41 import org.apache.maven.api.cli.mvn.MavenOptions;
42 import org.apache.maven.api.services.BuilderProblem;
43 import org.apache.maven.api.services.Lookup;
44 import org.apache.maven.api.services.Sources;
45 import org.apache.maven.api.services.ToolchainsBuilder;
46 import org.apache.maven.api.services.ToolchainsBuilderRequest;
47 import org.apache.maven.api.services.ToolchainsBuilderResult;
48 import org.apache.maven.api.services.model.ModelProcessor;
49 import org.apache.maven.api.toolchain.PersistedToolchains;
50 import org.apache.maven.cling.event.ExecutionEventLogger;
51 import org.apache.maven.cling.invoker.CliUtils;
52 import org.apache.maven.cling.invoker.LookupContext;
53 import org.apache.maven.cling.invoker.LookupInvoker;
54 import org.apache.maven.cling.transfer.ConsoleMavenTransferListener;
55 import org.apache.maven.cling.transfer.QuietMavenTransferListener;
56 import org.apache.maven.cling.transfer.SimplexTransferListener;
57 import org.apache.maven.cling.transfer.Slf4jMavenTransferListener;
58 import org.apache.maven.exception.DefaultExceptionHandler;
59 import org.apache.maven.exception.ExceptionHandler;
60 import org.apache.maven.exception.ExceptionSummary;
61 import org.apache.maven.execution.DefaultMavenExecutionRequest;
62 import org.apache.maven.execution.ExecutionListener;
63 import org.apache.maven.execution.MavenExecutionRequest;
64 import org.apache.maven.execution.MavenExecutionResult;
65 import org.apache.maven.execution.ProfileActivation;
66 import org.apache.maven.execution.ProjectActivation;
67 import org.apache.maven.jline.MessageUtils;
68 import org.apache.maven.lifecycle.LifecycleExecutionException;
69 import org.apache.maven.logging.LoggingExecutionListener;
70 import org.apache.maven.logging.MavenTransferListener;
71 import org.apache.maven.project.MavenProject;
72 import org.apache.maven.toolchain.model.ToolchainModel;
73 import org.eclipse.aether.DefaultRepositoryCache;
74 import org.eclipse.aether.transfer.TransferListener;
75
76 import static java.util.Comparator.comparing;
77
78
79
80
81 public class MavenInvoker extends LookupInvoker<MavenContext> {
82 public MavenInvoker(Lookup protoLookup, @Nullable Consumer<LookupContext> contextConsumer) {
83 super(protoLookup, contextConsumer);
84 }
85
86 @Override
87 protected MavenContext createContext(InvokerRequest invokerRequest) {
88 return new MavenContext(
89 invokerRequest, true, (MavenOptions) invokerRequest.options().orElse(null));
90 }
91
92 @Override
93 protected int execute(MavenContext context) throws Exception {
94 MavenExecutionRequest request = prepareMavenExecutionRequest();
95 toolchains(context, request);
96 populateRequest(context, context.lookup, request);
97 return doExecute(context, request);
98 }
99
100 protected MavenExecutionRequest prepareMavenExecutionRequest() throws Exception {
101
102 DefaultMavenExecutionRequest mavenExecutionRequest = new DefaultMavenExecutionRequest();
103 mavenExecutionRequest.setRepositoryCache(new DefaultRepositoryCache());
104 mavenExecutionRequest.setInteractiveMode(true);
105 mavenExecutionRequest.setCacheTransferError(false);
106 mavenExecutionRequest.setIgnoreInvalidArtifactDescriptor(true);
107 mavenExecutionRequest.setIgnoreMissingArtifactDescriptor(true);
108 mavenExecutionRequest.setRecursive(true);
109 mavenExecutionRequest.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_FAST);
110 mavenExecutionRequest.setStartInstant(MonotonicClock.now());
111 mavenExecutionRequest.setLoggingLevel(MavenExecutionRequest.LOGGING_LEVEL_INFO);
112 mavenExecutionRequest.setDegreeOfConcurrency(1);
113 mavenExecutionRequest.setBuilderId("singlethreaded");
114 return mavenExecutionRequest;
115 }
116
117 @Override
118 protected void lookup(MavenContext context) throws Exception {
119 if (context.maven == null) {
120 super.lookup(context);
121 context.maven = context.lookup.lookup(Maven.class);
122 }
123 }
124
125 @Override
126 protected void postCommands(MavenContext context) throws Exception {
127 super.postCommands(context);
128
129 Logger logger = context.logger;
130 if (context.options().relaxedChecksums().orElse(false)) {
131 logger.info("Disabling strict checksum verification on all artifact downloads.");
132 } else if (context.options().strictChecksums().orElse(false)) {
133 logger.info("Enabling strict checksum verification on all artifact downloads.");
134 }
135 }
136
137 protected void toolchains(MavenContext context, MavenExecutionRequest request) throws Exception {
138 Path userToolchainsFile = null;
139 if (context.options().altUserToolchains().isPresent()) {
140 userToolchainsFile =
141 context.cwd.resolve(context.options().altUserToolchains().get());
142
143 if (!Files.isRegularFile(userToolchainsFile)) {
144 throw new FileNotFoundException(
145 "The specified user toolchains file does not exist: " + userToolchainsFile);
146 }
147 } else {
148 String userToolchainsFileStr =
149 context.protoSession.getEffectiveProperties().get(Constants.MAVEN_USER_TOOLCHAINS);
150 if (userToolchainsFileStr != null) {
151 userToolchainsFile = context.cwd.resolve(userToolchainsFileStr);
152 }
153 }
154
155 Path installationToolchainsFile = null;
156 if (context.options().altInstallationToolchains().isPresent()) {
157 installationToolchainsFile = context.cwd.resolve(
158 context.options().altInstallationToolchains().get());
159
160 if (!Files.isRegularFile(installationToolchainsFile)) {
161 throw new FileNotFoundException(
162 "The specified installation toolchains file does not exist: " + installationToolchainsFile);
163 }
164 } else {
165 String installationToolchainsFileStr =
166 context.protoSession.getEffectiveProperties().get(Constants.MAVEN_INSTALLATION_TOOLCHAINS);
167 if (installationToolchainsFileStr != null) {
168 installationToolchainsFile = context.installationDirectory
169 .resolve(installationToolchainsFileStr)
170 .normalize();
171 }
172 }
173
174 request.setInstallationToolchainsFile(
175 installationToolchainsFile != null ? installationToolchainsFile.toFile() : null);
176 request.setUserToolchainsFile(userToolchainsFile != null ? userToolchainsFile.toFile() : null);
177
178 ToolchainsBuilderRequest toolchainsRequest = ToolchainsBuilderRequest.builder()
179 .session(context.protoSession)
180 .installationToolchainsSource(
181 installationToolchainsFile != null && Files.isRegularFile(installationToolchainsFile)
182 ? Sources.fromPath(installationToolchainsFile)
183 : null)
184 .userToolchainsSource(
185 userToolchainsFile != null && Files.isRegularFile(userToolchainsFile)
186 ? Sources.fromPath(userToolchainsFile)
187 : null)
188 .build();
189
190 context.eventSpyDispatcher.onEvent(toolchainsRequest);
191
192 context.logger.debug("Reading installation toolchains from '" + installationToolchainsFile + "'");
193 context.logger.debug("Reading user toolchains from '" + userToolchainsFile + "'");
194
195 ToolchainsBuilderResult toolchainsResult =
196 context.lookup.lookup(ToolchainsBuilder.class).build(toolchainsRequest);
197
198 context.eventSpyDispatcher.onEvent(toolchainsResult);
199
200 context.effectiveToolchains = toolchainsResult.getEffectiveToolchains();
201
202 if (toolchainsResult.getProblems().hasWarningProblems()) {
203 int totalProblems = toolchainsResult.getProblems().totalProblemsReported();
204 context.logger.info("");
205 context.logger.info(String.format(
206 "%s %s encountered while building the effective toolchains (use -e to see details)",
207 totalProblems, (totalProblems == 1) ? "problem was" : "problems were"));
208
209 if (context.options().showErrors().orElse(false)) {
210 for (BuilderProblem problem :
211 toolchainsResult.getProblems().problems().toList()) {
212 context.logger.warn(problem.getMessage() + " @ " + problem.getLocation());
213 }
214 }
215
216 context.logger.info("");
217 }
218 }
219
220 @Override
221 protected void populateRequest(MavenContext context, Lookup lookup, MavenExecutionRequest request)
222 throws Exception {
223 super.populateRequest(context, lookup, request);
224 if (context.invokerRequest.rootDirectory().isEmpty()) {
225
226 request.setMultiModuleProjectDirectory(
227 context.invokerRequest.topDirectory().toFile());
228 request.setRootDirectory(context.invokerRequest.topDirectory());
229 }
230
231 request.setToolchains(
232 Optional.ofNullable(context.effectiveToolchains).map(PersistedToolchains::getToolchains).stream()
233 .flatMap(List::stream)
234 .map(ToolchainModel::new)
235 .collect(Collectors.groupingBy(ToolchainModel::getType)));
236
237 request.setNoSnapshotUpdates(context.options().suppressSnapshotUpdates().orElse(false));
238 request.setGoals(context.options().goals().orElse(List.of()));
239 request.setReactorFailureBehavior(determineReactorFailureBehaviour(context));
240 request.setRecursive(!context.options().nonRecursive().orElse(!request.isRecursive()));
241 request.setOffline(context.options().offline().orElse(request.isOffline()));
242 request.setUpdateSnapshots(context.options().updateSnapshots().orElse(false));
243 request.setGlobalChecksumPolicy(determineGlobalChecksumPolicy(context));
244
245 Path pom = determinePom(context, lookup);
246 if (pom != null) {
247 request.setPom(pom.toFile());
248 if (pom.getParent() != null) {
249 request.setBaseDirectory(pom.getParent().toFile());
250 }
251
252
253 if (context.invokerRequest.rootDirectory().isEmpty()) {
254 Path rootDirectory = CliUtils.findMandatoryRoot(context.invokerRequest.topDirectory());
255 request.setMultiModuleProjectDirectory(rootDirectory.toFile());
256 request.setRootDirectory(rootDirectory);
257 }
258 }
259
260 request.setTransferListener(determineTransferListener(
261 context, context.options().noTransferProgress().orElse(false)));
262 request.setExecutionListener(determineExecutionListener(context));
263
264 request.setResumeFrom(context.options().resumeFrom().orElse(null));
265 request.setResume(context.options().resume().orElse(false));
266 request.setMakeBehavior(determineMakeBehavior(context));
267 request.setCacheNotFound(context.options().cacheArtifactNotFound().orElse(true));
268 request.setCacheTransferError(false);
269
270 if (context.options().strictArtifactDescriptorPolicy().orElse(false)) {
271 request.setIgnoreMissingArtifactDescriptor(false);
272 request.setIgnoreInvalidArtifactDescriptor(false);
273 } else {
274 request.setIgnoreMissingArtifactDescriptor(true);
275 request.setIgnoreInvalidArtifactDescriptor(true);
276 }
277
278 request.setIgnoreTransitiveRepositories(
279 context.options().ignoreTransitiveRepositories().orElse(false));
280
281 performProjectActivation(context, request.getProjectActivation());
282 performProfileActivation(context, request.getProfileActivation());
283
284
285
286
287
288
289
290
291
292 if (context.options().threads().isPresent()) {
293 int degreeOfConcurrency =
294 calculateDegreeOfConcurrency(context.options().threads().get());
295 if (degreeOfConcurrency > 1) {
296 request.setBuilderId("multithreaded");
297 request.setDegreeOfConcurrency(degreeOfConcurrency);
298 }
299 }
300
301
302
303
304 if (context.options().builder().isPresent()) {
305 request.setBuilderId(context.options().builder().get());
306 }
307 }
308
309 protected Path determinePom(MavenContext context, Lookup lookup) {
310 Path current = context.cwd.get();
311 if (context.options().alternatePomFile().isPresent()) {
312 current = context.cwd.resolve(context.options().alternatePomFile().get());
313 }
314 ModelProcessor modelProcessor =
315 lookup.lookupOptional(ModelProcessor.class).orElse(null);
316 if (modelProcessor != null) {
317 return modelProcessor.locateExistingPom(current);
318 } else {
319 return Files.isRegularFile(current) ? current : null;
320 }
321 }
322
323 protected String determineReactorFailureBehaviour(MavenContext context) {
324 if (context.options().failFast().isPresent()) {
325 return MavenExecutionRequest.REACTOR_FAIL_FAST;
326 } else if (context.options().failAtEnd().isPresent()) {
327 return MavenExecutionRequest.REACTOR_FAIL_AT_END;
328 } else if (context.options().failNever().isPresent()) {
329 return MavenExecutionRequest.REACTOR_FAIL_NEVER;
330 } else {
331 return MavenExecutionRequest.REACTOR_FAIL_FAST;
332 }
333 }
334
335 protected String determineGlobalChecksumPolicy(MavenContext context) {
336 if (context.options().strictChecksums().orElse(false)) {
337 return MavenExecutionRequest.CHECKSUM_POLICY_FAIL;
338 } else if (context.options().relaxedChecksums().orElse(false)) {
339 return MavenExecutionRequest.CHECKSUM_POLICY_WARN;
340 } else {
341 return null;
342 }
343 }
344
345 protected ExecutionListener determineExecutionListener(MavenContext context) {
346 ExecutionListener listener = new ExecutionEventLogger(context.invokerRequest.messageBuilderFactory());
347 if (context.eventSpyDispatcher != null) {
348 listener = context.eventSpyDispatcher.chainListener(listener);
349 }
350 return new LoggingExecutionListener(listener, determineBuildEventListener(context));
351 }
352
353 protected TransferListener determineTransferListener(MavenContext context, boolean noTransferProgress) {
354 boolean quiet = context.options().quiet().orElse(false);
355 boolean logFile = context.options().logFile().isPresent();
356 boolean quietCI = context.invokerRequest.ciInfo().isPresent()
357 && !context.options().forceInteractive().orElse(false);
358
359 TransferListener delegate;
360 if (quiet || noTransferProgress || quietCI) {
361 delegate = new QuietMavenTransferListener();
362 } else if (context.interactive && !logFile) {
363 if (context.simplexTransferListener == null) {
364 SimplexTransferListener simplex = new SimplexTransferListener(new ConsoleMavenTransferListener(
365 context.invokerRequest.messageBuilderFactory(),
366 context.terminal.writer(),
367 context.invokerRequest.effectiveVerbose()));
368 context.closeables.add(simplex);
369 context.simplexTransferListener = simplex;
370 }
371 delegate = context.simplexTransferListener;
372 } else {
373 delegate = new Slf4jMavenTransferListener();
374 }
375 return new MavenTransferListener(delegate, determineBuildEventListener(context));
376 }
377
378 protected String determineMakeBehavior(MavenContext context) {
379 if (context.options().alsoMake().isPresent()
380 && context.options().alsoMakeDependents().isEmpty()) {
381 return MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
382 } else if (context.options().alsoMake().isEmpty()
383 && context.options().alsoMakeDependents().isPresent()) {
384 return MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
385 } else if (context.options().alsoMake().isPresent()
386 && context.options().alsoMakeDependents().isPresent()) {
387 return MavenExecutionRequest.REACTOR_MAKE_BOTH;
388 } else {
389 return null;
390 }
391 }
392
393 protected void performProjectActivation(MavenContext context, ProjectActivation projectActivation) {
394 if (context.options().projects().isPresent()
395 && !context.options().projects().get().isEmpty()) {
396 List<String> optionValues = context.options().projects().get();
397 for (final String optionValue : optionValues) {
398 for (String token : optionValue.split(",")) {
399 String selector = token.trim();
400 boolean active = true;
401 if (!selector.isEmpty()) {
402 if (selector.charAt(0) == '-' || selector.charAt(0) == '!') {
403 active = false;
404 selector = selector.substring(1);
405 } else if (token.charAt(0) == '+') {
406 selector = selector.substring(1);
407 }
408 }
409 boolean optional = false;
410 if (!selector.isEmpty() && selector.charAt(0) == '?') {
411 optional = true;
412 selector = selector.substring(1);
413 }
414 projectActivation.addProjectActivation(selector, active, optional);
415 }
416 }
417 }
418 }
419
420 protected void performProfileActivation(MavenContext context, ProfileActivation profileActivation) {
421 if (context.options().activatedProfiles().isPresent()
422 && !context.options().activatedProfiles().get().isEmpty()) {
423 List<String> optionValues = context.options().activatedProfiles().get();
424 for (final String optionValue : optionValues) {
425 for (String token : optionValue.split(",")) {
426 String profileId = token.trim();
427 boolean active = true;
428 if (!profileId.isEmpty()) {
429 if (profileId.charAt(0) == '-' || profileId.charAt(0) == '!') {
430 active = false;
431 profileId = profileId.substring(1);
432 } else if (token.charAt(0) == '+') {
433 profileId = profileId.substring(1);
434 }
435 }
436 boolean optional = false;
437 if (!profileId.isEmpty() && profileId.charAt(0) == '?') {
438 optional = true;
439 profileId = profileId.substring(1);
440 }
441 profileActivation.addProfileActivation(profileId, active, optional);
442 }
443 }
444 }
445 }
446
447 protected int doExecute(MavenContext context, MavenExecutionRequest request) throws Exception {
448 context.eventSpyDispatcher.onEvent(request);
449
450 MavenExecutionResult result;
451 try {
452 result = context.maven.execute(request);
453 context.eventSpyDispatcher.onEvent(result);
454 } finally {
455 context.eventSpyDispatcher.close();
456 }
457
458 if (result.hasExceptions()) {
459 ExceptionHandler handler = new DefaultExceptionHandler();
460 Map<String, String> references = new LinkedHashMap<>();
461 List<MavenProject> failedProjects = new ArrayList<>();
462
463 for (Throwable exception : result.getExceptions()) {
464 ExceptionSummary summary = handler.handleException(exception);
465 logSummary(context, summary, references, "");
466
467 if (exception instanceof LifecycleExecutionException lifecycleExecutionException) {
468 failedProjects.add(lifecycleExecutionException.getProject());
469 }
470 }
471
472 context.logger.error("");
473
474 if (!context.options().showErrors().orElse(false)) {
475 context.logger.error("To see the full stack trace of the errors, re-run Maven with the '"
476 + MessageUtils.builder().strong("-e") + "' switch");
477 }
478 if (!context.invokerRequest.effectiveVerbose()) {
479 context.logger.error("Re-run Maven using the '"
480 + MessageUtils.builder().strong("-X") + "' switch to enable verbose output");
481 }
482
483 if (!references.isEmpty()) {
484 context.logger.error("");
485 context.logger.error("For more information about the errors and possible solutions"
486 + ", please read the following articles:");
487
488 for (Map.Entry<String, String> entry : references.entrySet()) {
489 context.logger.error(MessageUtils.builder().strong(entry.getValue()) + " " + entry.getKey());
490 }
491 }
492
493 if (result.canResume()) {
494 logBuildResumeHint(context, "mvn [args] -r");
495 } else if (!failedProjects.isEmpty()) {
496 List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
497
498
499 failedProjects.sort(comparing(sortedProjects::indexOf));
500
501 MavenProject firstFailedProject = failedProjects.get(0);
502 if (!firstFailedProject.equals(sortedProjects.get(0))) {
503 String resumeFromSelector = getResumeFromSelector(sortedProjects, firstFailedProject);
504 logBuildResumeHint(context, "mvn [args] -rf " + resumeFromSelector);
505 }
506 }
507
508 if (context.options().failNever().orElse(false)) {
509 context.logger.info("Build failures were ignored.");
510 return 0;
511 } else {
512 return 1;
513 }
514 } else {
515 return 0;
516 }
517 }
518
519 protected void logBuildResumeHint(MavenContext context, String resumeBuildHint) {
520 context.logger.error("");
521 context.logger.error("After correcting the problems, you can resume the build with the command");
522 context.logger.error(
523 MessageUtils.builder().a(" ").strong(resumeBuildHint).toString());
524 }
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543 protected String getResumeFromSelector(List<MavenProject> mavenProjects, MavenProject firstFailedProject) {
544 boolean hasOverlappingArtifactId = mavenProjects.stream()
545 .filter(project -> firstFailedProject.getArtifactId().equals(project.getArtifactId()))
546 .count()
547 > 1;
548
549 if (hasOverlappingArtifactId) {
550 return firstFailedProject.getGroupId() + ":" + firstFailedProject.getArtifactId();
551 }
552
553 return ":" + firstFailedProject.getArtifactId();
554 }
555
556 protected static final Pattern NEXT_LINE = Pattern.compile("\r?\n");
557
558 protected static final Pattern LAST_ANSI_SEQUENCE = Pattern.compile("(\u001B\\[[;\\d]*[ -/]*[@-~])[^\u001B]*$");
559
560 protected static final String ANSI_RESET = "\u001B\u005Bm";
561
562 protected void logSummary(
563 MavenContext context, ExceptionSummary summary, Map<String, String> references, String indent) {
564 String referenceKey = "";
565
566 if (summary.getReference() != null && !summary.getReference().isEmpty()) {
567 referenceKey =
568 references.computeIfAbsent(summary.getReference(), k -> "[Help " + (references.size() + 1) + "]");
569 }
570
571 String msg = summary.getMessage();
572
573 if (!referenceKey.isEmpty()) {
574 if (msg.indexOf('\n') < 0) {
575 msg += " -> " + MessageUtils.builder().strong(referenceKey);
576 } else {
577 msg += "\n-> " + MessageUtils.builder().strong(referenceKey);
578 }
579 }
580
581 String[] lines = NEXT_LINE.split(msg);
582 String currentColor = "";
583
584 for (int i = 0; i < lines.length; i++) {
585
586 String line = currentColor + lines[i];
587
588
589 Matcher matcher = LAST_ANSI_SEQUENCE.matcher(line);
590 String nextColor = "";
591 if (matcher.find()) {
592 nextColor = matcher.group(1);
593 if (ANSI_RESET.equals(nextColor)) {
594
595 nextColor = "";
596 }
597 }
598
599
600 line = indent + line + ("".equals(nextColor) ? "" : ANSI_RESET);
601
602 if ((i == lines.length - 1)
603 && (context.options().showErrors().orElse(false)
604 || (summary.getException() instanceof InternalErrorException))) {
605 context.logger.error(line, summary.getException());
606 } else {
607 context.logger.error(line);
608 }
609
610 currentColor = nextColor;
611 }
612
613 indent += " ";
614
615 for (ExceptionSummary child : summary.getChildren()) {
616 logSummary(context, child, references, indent);
617 }
618 }
619 }