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