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