/
TychoMavenLifecycleParticipant.java
253 lines (222 loc) · 11.3 KB
/
TychoMavenLifecycleParticipant.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
/*******************************************************************************
* Copyright (c) 2008, 2020 Sonatype Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sonatype Inc. - initial API and implementation
* Bachmann electronic GmbH - Bug 457314 - handle null as tycho version
* Christoph Läubrich - Bug 569829 - TychoMavenLifecycleParticipant should respect fail-at-end flag / error output is missing
*******************************************************************************/
package org.eclipse.tycho.core.maven;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.apache.maven.AbstractMavenLifecycleParticipant;
import org.apache.maven.MavenExecutionException;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.Logger;
import org.eclipse.tycho.ReactorProject;
import org.eclipse.tycho.artifacts.DependencyResolutionException;
import org.eclipse.tycho.core.osgitools.BundleReader;
import org.eclipse.tycho.core.osgitools.DefaultBundleReader;
import org.eclipse.tycho.core.osgitools.DefaultReactorProject;
import org.eclipse.tycho.core.shared.BuildFailureException;
import org.eclipse.tycho.core.utils.TychoVersion;
import org.eclipse.tycho.resolver.TychoResolver;
@Component(role = AbstractMavenLifecycleParticipant.class, hint = "TychoMavenLifecycleListener")
public class TychoMavenLifecycleParticipant extends AbstractMavenLifecycleParticipant {
private static final String TYCHO_GROUPID = "org.eclipse.tycho";
private static final Set<String> TYCHO_PLUGIN_IDS = new HashSet<>(Arrays.asList("tycho-maven-plugin",
"tycho-p2-director-plugin", "tycho-p2-plugin", "tycho-p2-publisher-plugin", "tycho-p2-repository-plugin",
"tycho-packaging-plugin", "tycho-pomgenerator-plugin", "tycho-source-plugin", "tycho-surefire-plugin",
"tycho-versions-plugin", "tycho-compiler-plugin"));
private static final String P2_USER_AGENT_KEY = "p2.userAgent";
private static final String P2_USER_AGENT_VALUE = "tycho/";
@Requirement
private BundleReader bundleReader;
@Requirement
private TychoResolver resolver;
@Requirement
private PlexusContainer plexus;
@Requirement
private Logger log;
public TychoMavenLifecycleParticipant() {
// needed for plexus
}
// needed for unit tests
protected TychoMavenLifecycleParticipant(Logger log) {
this.log = log;
}
@Override
public void afterProjectsRead(MavenSession session) throws MavenExecutionException {
try {
if (disableLifecycleParticipation(session)) {
return;
}
List<MavenProject> projects = session.getProjects();
validate(projects);
// setting this system property to let EF figure out where the traffic
// is coming from (#467418)
System.setProperty(P2_USER_AGENT_KEY, P2_USER_AGENT_VALUE + TychoVersion.getTychoVersion());
configureComponents(session);
for (MavenProject project : projects) {
resolver.setupProject(session, project, DefaultReactorProject.adapt(project));
}
resolveProjects(session, projects);
} catch (BuildFailureException e) {
// build failure is not an internal (unexpected) error, so avoid printing a stack
// trace by wrapping it in MavenExecutionException
throw new MavenExecutionException(e.getMessage(), e);
}
}
private void validate(List<MavenProject> projects) throws MavenExecutionException {
validateConsistentTychoVersion(projects);
validateUniqueBaseDirs(projects);
}
private void resolveProjects(MavenSession session, List<MavenProject> projects) {
List<ReactorProject> reactorProjects = DefaultReactorProject.adapt(session);
MavenExecutionRequest request = session.getRequest();
boolean failFast = MavenExecutionRequest.REACTOR_FAIL_FAST.equals(request.getReactorFailureBehavior());
Map<MavenProject, BuildFailureException> resolutionErrors = new ConcurrentHashMap<>();
Consumer<MavenProject> resolveProject = project -> {
if (failFast && !resolutionErrors.isEmpty()) {
//short circuit
return;
}
try {
resolver.resolveProject(session, project, reactorProjects);
} catch (BuildFailureException e) {
resolutionErrors.put(project, e);
if (failFast) {
throw e;
}
}
};
int degreeOfConcurrency = request.getDegreeOfConcurrency();
Predicate<MavenProject> takeWhile = Predicate.not(p -> failFast && !resolutionErrors.isEmpty());
if (degreeOfConcurrency > 1) {
ForkJoinPool executor = new ForkJoinPool(degreeOfConcurrency);
ForkJoinTask<?> future = executor.submit(() -> {
projects.parallelStream().takeWhile(takeWhile).forEach(resolveProject);
});
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
throw new RuntimeException("resolve dependencies failed", cause);
} finally {
executor.shutdown();
}
} else {
projects.stream().takeWhile(takeWhile).forEach(resolveProject);
}
reportResolutionErrors(resolutionErrors, projects, failFast);
}
private void reportResolutionErrors(Map<MavenProject, BuildFailureException> resolutionErrors,
List<MavenProject> projects, boolean failFast) {
if (resolutionErrors.isEmpty()) {
return;
}
if (resolutionErrors.size() == 1 || failFast) {
//The idea is if user want to fail-fast he would expect to get exactly one error (the first one),
//while if parallel execution is enabled it might report an (incomplete) list of other failures that happened due to the parallel processing.
throw resolutionErrors.values().iterator().next();
}
DependencyResolutionException exception = new DependencyResolutionException(
String.format("Cannot resolve dependencies of %d/%d projects, see log for details",
resolutionErrors.size(), projects.size()));
resolutionErrors.values().forEach(exception::addSuppressed);
resolutionErrors.forEach((project, error) -> log.error(project.getName() + ": " + error.getMessage()));
throw exception;
}
protected void validateConsistentTychoVersion(List<MavenProject> projects) throws MavenExecutionException {
Map<String, Set<MavenProject>> versionToProjectsMap = new HashMap<>();
for (MavenProject project : projects) {
for (Plugin plugin : project.getBuild().getPlugins()) {
if (TYCHO_GROUPID.equals(plugin.getGroupId()) && TYCHO_PLUGIN_IDS.contains(plugin.getArtifactId())) {
String version = plugin.getVersion();
// Skip checking plug ins that do not have a version
if (version == null) {
continue;
}
log.debug(
TYCHO_GROUPID + ":" + plugin.getArtifactId() + ":" + version + " configured in " + project);
Set<MavenProject> projectSet = versionToProjectsMap.get(version);
if (projectSet == null) {
projectSet = new LinkedHashSet<>();
versionToProjectsMap.put(version, projectSet);
}
projectSet.add(project);
}
}
}
if (versionToProjectsMap.size() > 1) {
List<String> versions = new ArrayList<>(versionToProjectsMap.keySet());
Collections.sort(versions);
log.error("Several versions of tycho plugins are configured " + versions + ":");
for (String version : versions) {
log.error(version + ":");
for (MavenProject project : versionToProjectsMap.get(version)) {
log.error("\t" + project.toString());
}
}
throw new MavenExecutionException("All tycho plugins configured in one reactor must use the same version",
projects.get(0).getFile());
}
}
private void validateUniqueBaseDirs(List<MavenProject> projects) throws MavenExecutionException {
// we store intermediate build results in the target/ folder and use the baseDir as unique key
// so multiple modules in the same baseDir would lead to irreproducible/unexpected results
// e.g. with mvn clean. This should really not be supported by maven core
Set<File> baseDirs = new HashSet<>();
for (MavenProject project : projects) {
File basedir = project.getBasedir();
if (baseDirs.contains(basedir)) {
throw new MavenExecutionException(
"Multiple modules within the same basedir are not supported: " + basedir, project.getFile());
} else {
baseDirs.add(basedir);
}
}
}
private static final Set<String> CLEAN_PHASES = Set.of("pre-clean", "clean", "post-clean");
private boolean disableLifecycleParticipation(MavenSession session) {
// command line property to disable Tycho lifecycle participant
return "maven".equals(session.getUserProperties().get("tycho.mode"))
|| session.getUserProperties().containsKey("m2e.version")
// disable for 'clean-only' builds. Consider that Maven can be invoked without explicit goals, if default goals are specified
|| (!session.getGoals().isEmpty() && CLEAN_PHASES.containsAll(session.getGoals()));
}
private void configureComponents(MavenSession session) {
// TODO why does the bundle reader need to cache stuff in the local maven repository?
File localRepository = new File(session.getLocalRepository().getBasedir());
((DefaultBundleReader) bundleReader).setLocationRepository(localRepository);
}
}