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.getUserProperties().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.getUserProperties().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 SimplexTransferListener simplex = new SimplexTransferListener(new ConsoleMavenTransferListener(
364 context.invokerRequest.messageBuilderFactory(),
365 context.terminal.writer(),
366 context.invokerRequest.effectiveVerbose()));
367 context.closeables.add(simplex);
368 delegate = simplex;
369 } else {
370 delegate = new Slf4jMavenTransferListener();
371 }
372 return new MavenTransferListener(delegate, determineBuildEventListener(context));
373 }
374
375 protected String determineMakeBehavior(MavenContext context) {
376 if (context.options().alsoMake().isPresent()
377 && context.options().alsoMakeDependents().isEmpty()) {
378 return MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
379 } else if (context.options().alsoMake().isEmpty()
380 && context.options().alsoMakeDependents().isPresent()) {
381 return MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
382 } else if (context.options().alsoMake().isPresent()
383 && context.options().alsoMakeDependents().isPresent()) {
384 return MavenExecutionRequest.REACTOR_MAKE_BOTH;
385 } else {
386 return null;
387 }
388 }
389
390 protected void performProjectActivation(MavenContext context, ProjectActivation projectActivation) {
391 if (context.options().projects().isPresent()
392 && !context.options().projects().get().isEmpty()) {
393 List<String> optionValues = context.options().projects().get();
394 for (final String optionValue : optionValues) {
395 for (String token : optionValue.split(",")) {
396 String selector = token.trim();
397 boolean active = true;
398 if (!selector.isEmpty()) {
399 if (selector.charAt(0) == '-' || selector.charAt(0) == '!') {
400 active = false;
401 selector = selector.substring(1);
402 } else if (token.charAt(0) == '+') {
403 selector = selector.substring(1);
404 }
405 }
406 boolean optional = false;
407 if (!selector.isEmpty() && selector.charAt(0) == '?') {
408 optional = true;
409 selector = selector.substring(1);
410 }
411 projectActivation.addProjectActivation(selector, active, optional);
412 }
413 }
414 }
415 }
416
417 protected void performProfileActivation(MavenContext context, ProfileActivation profileActivation) {
418 if (context.options().activatedProfiles().isPresent()
419 && !context.options().activatedProfiles().get().isEmpty()) {
420 List<String> optionValues = context.options().activatedProfiles().get();
421 for (final String optionValue : optionValues) {
422 for (String token : optionValue.split(",")) {
423 String profileId = token.trim();
424 boolean active = true;
425 if (!profileId.isEmpty()) {
426 if (profileId.charAt(0) == '-' || profileId.charAt(0) == '!') {
427 active = false;
428 profileId = profileId.substring(1);
429 } else if (token.charAt(0) == '+') {
430 profileId = profileId.substring(1);
431 }
432 }
433 boolean optional = false;
434 if (!profileId.isEmpty() && profileId.charAt(0) == '?') {
435 optional = true;
436 profileId = profileId.substring(1);
437 }
438 profileActivation.addProfileActivation(profileId, active, optional);
439 }
440 }
441 }
442 }
443
444 protected int doExecute(MavenContext context, MavenExecutionRequest request) throws Exception {
445 context.eventSpyDispatcher.onEvent(request);
446
447 MavenExecutionResult result;
448 try {
449 result = context.maven.execute(request);
450 context.eventSpyDispatcher.onEvent(result);
451 } finally {
452 context.eventSpyDispatcher.close();
453 }
454
455 if (result.hasExceptions()) {
456 ExceptionHandler handler = new DefaultExceptionHandler();
457 Map<String, String> references = new LinkedHashMap<>();
458 List<MavenProject> failedProjects = new ArrayList<>();
459
460 for (Throwable exception : result.getExceptions()) {
461 ExceptionSummary summary = handler.handleException(exception);
462 logSummary(context, summary, references, "");
463
464 if (exception instanceof LifecycleExecutionException lifecycleExecutionException) {
465 failedProjects.add(lifecycleExecutionException.getProject());
466 }
467 }
468
469 context.logger.error("");
470
471 if (!context.options().showErrors().orElse(false)) {
472 context.logger.error("To see the full stack trace of the errors, re-run Maven with the '"
473 + MessageUtils.builder().strong("-e") + "' switch");
474 }
475 if (!context.invokerRequest.effectiveVerbose()) {
476 context.logger.error("Re-run Maven using the '"
477 + MessageUtils.builder().strong("-X") + "' switch to enable verbose output");
478 }
479
480 if (!references.isEmpty()) {
481 context.logger.error("");
482 context.logger.error("For more information about the errors and possible solutions"
483 + ", please read the following articles:");
484
485 for (Map.Entry<String, String> entry : references.entrySet()) {
486 context.logger.error(MessageUtils.builder().strong(entry.getValue()) + " " + entry.getKey());
487 }
488 }
489
490 if (result.canResume()) {
491 logBuildResumeHint(context, "mvn [args] -r");
492 } else if (!failedProjects.isEmpty()) {
493 List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
494
495
496 failedProjects.sort(comparing(sortedProjects::indexOf));
497
498 MavenProject firstFailedProject = failedProjects.get(0);
499 if (!firstFailedProject.equals(sortedProjects.get(0))) {
500 String resumeFromSelector = getResumeFromSelector(sortedProjects, firstFailedProject);
501 logBuildResumeHint(context, "mvn [args] -rf " + resumeFromSelector);
502 }
503 }
504
505 if (context.options().failNever().orElse(false)) {
506 context.logger.info("Build failures were ignored.");
507 return 0;
508 } else {
509 return 1;
510 }
511 } else {
512 return 0;
513 }
514 }
515
516 protected void logBuildResumeHint(MavenContext context, String resumeBuildHint) {
517 context.logger.error("");
518 context.logger.error("After correcting the problems, you can resume the build with the command");
519 context.logger.error(
520 MessageUtils.builder().a(" ").strong(resumeBuildHint).toString());
521 }
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540 protected String getResumeFromSelector(List<MavenProject> mavenProjects, MavenProject firstFailedProject) {
541 boolean hasOverlappingArtifactId = mavenProjects.stream()
542 .filter(project -> firstFailedProject.getArtifactId().equals(project.getArtifactId()))
543 .count()
544 > 1;
545
546 if (hasOverlappingArtifactId) {
547 return firstFailedProject.getGroupId() + ":" + firstFailedProject.getArtifactId();
548 }
549
550 return ":" + firstFailedProject.getArtifactId();
551 }
552
553 protected static final Pattern NEXT_LINE = Pattern.compile("\r?\n");
554
555 protected static final Pattern LAST_ANSI_SEQUENCE = Pattern.compile("(\u001B\\[[;\\d]*[ -/]*[@-~])[^\u001B]*$");
556
557 protected static final String ANSI_RESET = "\u001B\u005Bm";
558
559 protected void logSummary(
560 MavenContext context, ExceptionSummary summary, Map<String, String> references, String indent) {
561 String referenceKey = "";
562
563 if (summary.getReference() != null && !summary.getReference().isEmpty()) {
564 referenceKey =
565 references.computeIfAbsent(summary.getReference(), k -> "[Help " + (references.size() + 1) + "]");
566 }
567
568 String msg = summary.getMessage();
569
570 if (!referenceKey.isEmpty()) {
571 if (msg.indexOf('\n') < 0) {
572 msg += " -> " + MessageUtils.builder().strong(referenceKey);
573 } else {
574 msg += "\n-> " + MessageUtils.builder().strong(referenceKey);
575 }
576 }
577
578 String[] lines = NEXT_LINE.split(msg);
579 String currentColor = "";
580
581 for (int i = 0; i < lines.length; i++) {
582
583 String line = currentColor + lines[i];
584
585
586 Matcher matcher = LAST_ANSI_SEQUENCE.matcher(line);
587 String nextColor = "";
588 if (matcher.find()) {
589 nextColor = matcher.group(1);
590 if (ANSI_RESET.equals(nextColor)) {
591
592 nextColor = "";
593 }
594 }
595
596
597 line = indent + line + ("".equals(nextColor) ? "" : ANSI_RESET);
598
599 if ((i == lines.length - 1)
600 && (context.options().showErrors().orElse(false)
601 || (summary.getException() instanceof InternalErrorException))) {
602 context.logger.error(line, summary.getException());
603 } else {
604 context.logger.error(line);
605 }
606
607 currentColor = nextColor;
608 }
609
610 indent += " ";
611
612 for (ExceptionSummary child : summary.getChildren()) {
613 logSummary(context, child, references, indent);
614 }
615 }
616 }