1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.artifact.buildinfo;
20
21 import java.io.BufferedWriter;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.OutputStreamWriter;
25 import java.io.PrintWriter;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.Files;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Properties;
32
33 import org.apache.maven.artifact.Artifact;
34 import org.apache.maven.artifact.factory.ArtifactFactory;
35 import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
36 import org.apache.maven.plugin.MojoExecutionException;
37 import org.apache.maven.plugins.annotations.Component;
38 import org.apache.maven.plugins.annotations.Mojo;
39 import org.apache.maven.plugins.annotations.Parameter;
40 import org.apache.maven.project.MavenProject;
41 import org.apache.maven.shared.utils.PropertyUtils;
42 import org.apache.maven.shared.utils.StringUtils;
43 import org.apache.maven.shared.utils.logging.MessageUtils;
44 import org.eclipse.aether.RepositorySystem;
45 import org.eclipse.aether.RepositorySystemSession;
46 import org.eclipse.aether.repository.RemoteRepository;
47
48 import static org.apache.maven.plugins.artifact.buildinfo.BuildInfoWriter.getArtifactFilename;
49
50
51
52
53
54
55
56 @Mojo(name = "compare", threadSafe = true)
57 public class CompareMojo extends AbstractBuildinfoMojo {
58
59
60
61
62
63
64
65
66
67
68
69 @Parameter(property = "reference.repo", defaultValue = "central")
70 private String referenceRepo;
71
72
73
74
75
76 @Parameter(property = "compare.aggregate.only", defaultValue = "false")
77 private boolean aggregateOnly;
78
79 @Component
80 private ArtifactFactory artifactFactory;
81
82
83
84
85 @Component
86 private RepositorySystem repoSystem;
87
88
89
90
91 @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
92 private RepositorySystemSession repoSession;
93
94
95
96
97 @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
98 private List<RemoteRepository> remoteRepos;
99
100
101
102
103
104 @Parameter(property = "compare.fail", defaultValue = "true")
105 private boolean fail;
106
107 @Component
108 private ArtifactRepositoryLayout artifactRepositoryLayout;
109
110 @Override
111 public void execute(Map<Artifact, String> artifacts) throws MojoExecutionException {
112 getLog().info("Checking against reference build from " + referenceRepo + "...");
113 checkAgainstReference(artifacts, session.getProjects().size() == 1);
114 }
115
116 @Override
117 protected void skip(MavenProject last) throws MojoExecutionException {
118 if (aggregateOnly) {
119 return;
120 }
121
122
123 checkAgainstReference(generateBuildinfo(true), true);
124 }
125
126
127
128
129
130
131
132
133 private void checkAgainstReference(Map<Artifact, String> artifacts, boolean mono) throws MojoExecutionException {
134 MavenProject root = mono ? project : getExecutionRoot();
135 File referenceDir = new File(root.getBuild().getDirectory(), "reference");
136 referenceDir.mkdirs();
137
138
139 File referenceBuildinfo = downloadOrCreateReferenceBuildinfo(mono, artifacts, referenceDir);
140
141
142 compareWithReference(artifacts, referenceBuildinfo);
143 }
144
145 private File downloadOrCreateReferenceBuildinfo(boolean mono, Map<Artifact, String> artifacts, File referenceDir)
146 throws MojoExecutionException {
147 RemoteRepository repo = createReferenceRepo();
148
149 ReferenceBuildinfoUtil rmb = new ReferenceBuildinfoUtil(
150 getLog(),
151 referenceDir,
152 artifacts,
153 artifactFactory,
154 repoSystem,
155 repoSession,
156 artifactHandlerManager,
157 rtInformation);
158
159 return rmb.downloadOrCreateReferenceBuildinfo(repo, project, buildinfoFile, mono);
160 }
161
162 private void compareWithReference(Map<Artifact, String> artifacts, File referenceBuildinfo)
163 throws MojoExecutionException {
164 Properties actual = BuildInfoWriter.loadOutputProperties(buildinfoFile);
165 Properties reference = BuildInfoWriter.loadOutputProperties(referenceBuildinfo);
166
167 int ok = 0;
168 List<String> okFilenames = new ArrayList<>();
169 List<String> koFilenames = new ArrayList<>();
170 List<String> diffoscopes = new ArrayList<>();
171 List<String> ignored = new ArrayList<>();
172 File referenceDir = referenceBuildinfo.getParentFile();
173 for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) {
174 Artifact artifact = entry.getKey();
175 String prefix = entry.getValue();
176 if (prefix == null) {
177
178 ignored.add(getArtifactFilename(artifact));
179 continue;
180 }
181
182 String[] checkResult = checkArtifact(artifact, prefix, reference, actual, referenceDir);
183 String filename = checkResult[0];
184 String diffoscope = checkResult[1];
185
186 if (diffoscope == null) {
187 ok++;
188 okFilenames.add(filename);
189 } else {
190 koFilenames.add(filename);
191 diffoscopes.add(diffoscope);
192 }
193 }
194
195 int ko = artifacts.size() - ok - ignored.size();
196 int missing = reference.size() / 3 ;
197
198 if (ko + missing > 0) {
199 getLog().error("Reproducible Build output summary: "
200 + MessageUtils.buffer().success(ok + " files ok")
201 + ", " + MessageUtils.buffer().failure(ko + " different")
202 + ((missing == 0) ? "" : (", " + MessageUtils.buffer().failure(missing + " missing")))
203 + ((ignored.isEmpty()) ? "" : (", " + MessageUtils.buffer().warning(ignored.size() + " ignored"))));
204 getLog().error("see "
205 + MessageUtils.buffer()
206 .project("diff " + relative(referenceBuildinfo) + " " + relative(buildinfoFile))
207 .toString());
208 getLog().error("see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
209 } else {
210 getLog().info("Reproducible Build output summary: "
211 + MessageUtils.buffer().success(ok + " files ok")
212 + ((ignored.isEmpty()) ? "" : (", " + MessageUtils.buffer().warning(ignored.size() + " ignored"))));
213 }
214
215
216 File buildcompare = new File(
217 buildinfoFile.getParentFile(), buildinfoFile.getName().replaceFirst(".buildinfo$", ".buildcompare"));
218 try (PrintWriter p = new PrintWriter(new BufferedWriter(
219 new OutputStreamWriter(Files.newOutputStream(buildcompare.toPath()), StandardCharsets.UTF_8)))) {
220 p.println("version=" + project.getVersion());
221 p.println("ok=" + ok);
222 p.println("ko=" + ko);
223 p.println("ignored=" + ignored.size());
224 p.println("okFiles=\"" + StringUtils.join(okFilenames.iterator(), " ") + '"');
225 p.println("koFiles=\"" + StringUtils.join(koFilenames.iterator(), " ") + '"');
226 p.println("ignoredFiles=\"" + StringUtils.join(ignored.iterator(), " ") + '"');
227 Properties ref = PropertyUtils.loadOptionalProperties(referenceBuildinfo);
228 String v = ref.getProperty("java.version");
229 if (v != null) {
230 p.println("reference_java_version=\"" + v + '"');
231 }
232 v = ref.getProperty("os.name");
233 if (v != null) {
234 p.println("reference_os_name=\"" + v + '"');
235 }
236 for (String diffoscope : diffoscopes) {
237 p.print("# ");
238 p.println(diffoscope);
239 }
240 getLog().info("Reproducible Build output comparison saved to " + buildcompare);
241 } catch (IOException e) {
242 throw new MojoExecutionException("Error creating file " + buildcompare, e);
243 }
244
245 copyAggregateToRoot(buildcompare);
246
247 if (fail && (ko + missing > 0)) {
248 throw new MojoExecutionException("Build artifacts are different from reference");
249 }
250 }
251
252
253 private String[] checkArtifact(
254 Artifact artifact, String prefix, Properties reference, Properties actual, File referenceDir) {
255 String actualFilename = (String) actual.remove(prefix + ".filename");
256 String actualLength = (String) actual.remove(prefix + ".length");
257 String actualSha512 = (String) actual.remove(prefix + ".checksums.sha512");
258
259 String referencePrefix = findPrefix(reference, artifact.getGroupId(), actualFilename);
260 String referenceLength = (String) reference.remove(referencePrefix + ".length");
261 String referenceSha512 = (String) reference.remove(referencePrefix + ".checksums.sha512");
262 reference.remove(referencePrefix + ".groupId");
263
264 String issue = null;
265 if (!actualLength.equals(referenceLength)) {
266 issue = "size";
267 } else if (!actualSha512.equals(referenceSha512)) {
268 issue = "sha512";
269 }
270
271 if (issue != null) {
272 String diffoscope = diffoscope(artifact, referenceDir);
273 getLog().error(issue + " mismatch " + MessageUtils.buffer().strong(actualFilename) + ": investigate with "
274 + MessageUtils.buffer().project(diffoscope));
275 return new String[] {actualFilename, diffoscope};
276 }
277 return new String[] {actualFilename, null};
278 }
279
280 private String diffoscope(Artifact a, File referenceDir) {
281 File actual = a.getFile();
282
283
284 File reference = new File(new File(referenceDir, a.getGroupId()), getRepositoryFilename(a));
285 if (actual == null) {
286 return "missing file for " + a.getId() + " reference = " + relative(reference) + " actual = null";
287 }
288 return "diffoscope " + relative(reference) + " " + relative(actual);
289 }
290
291 private String getRepositoryFilename(Artifact a) {
292 String path = artifactRepositoryLayout.pathOf(a);
293 return path.substring(path.lastIndexOf('/'));
294 }
295
296 private String relative(File file) {
297 File basedir = getExecutionRoot().getBasedir();
298 int length = basedir.getPath().length();
299 String path = file.getPath();
300 return path.substring(length + 1);
301 }
302
303 private static String findPrefix(Properties reference, String actualGroupId, String actualFilename) {
304 for (String name : reference.stringPropertyNames()) {
305 if (name.endsWith(".filename") && actualFilename.equals(reference.getProperty(name))) {
306 String prefix = name.substring(0, name.length() - ".filename".length());
307 if (actualGroupId.equals(reference.getProperty(prefix + ".groupId"))) {
308 reference.remove(name);
309 return prefix;
310 }
311 }
312 }
313 return null;
314 }
315
316 private RemoteRepository createReferenceRepo() throws MojoExecutionException {
317 if (referenceRepo.contains("::")) {
318
319 int index = referenceRepo.indexOf("::");
320 String id = referenceRepo.substring(0, index);
321 String url = referenceRepo.substring(index + 2);
322 return createDeploymentArtifactRepository(id, url);
323 } else if (referenceRepo.contains(":")) {
324
325 return createDeploymentArtifactRepository("reference", referenceRepo);
326 }
327
328
329 for (RemoteRepository repo : remoteRepos) {
330 if (referenceRepo.equals(repo.getId())) {
331 return repo;
332 }
333 }
334 throw new MojoExecutionException("Could not find repository with id = " + referenceRepo);
335 }
336
337 private static RemoteRepository createDeploymentArtifactRepository(String id, String url) {
338 return new RemoteRepository.Builder(id, "default", url).build();
339 }
340 }