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