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.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.io.PrintWriter;
27 import java.nio.charset.StandardCharsets;
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
49
50
51
52
53
54 @Mojo(name = "compare", threadSafe = true)
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 @Component
78 private ArtifactFactory artifactFactory;
79
80
81
82
83 @Component
84 private RepositorySystem repoSystem;
85
86
87
88
89 @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
90 private RepositorySystemSession repoSession;
91
92
93
94
95 @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
96 private List<RemoteRepository> remoteRepos;
97
98 @Component
99 private ArtifactRepositoryLayout artifactRepositoryLayout;
100
101 @Override
102 public void execute(Map<Artifact, String> artifacts) throws MojoExecutionException {
103 getLog().info("Checking against reference build from " + referenceRepo + "...");
104 checkAgainstReference(artifacts, reactorProjects.size() == 1);
105 }
106
107 @Override
108 protected void skip(MavenProject last) throws MojoExecutionException {
109 if (aggregateOnly) {
110 return;
111 }
112
113
114 checkAgainstReference(generateBuildinfo(true), true);
115 }
116
117
118
119
120
121
122
123
124 private void checkAgainstReference(Map<Artifact, String> artifacts, boolean mono) throws MojoExecutionException {
125 MavenProject root = mono ? project : getExecutionRoot();
126 File referenceDir = new File(root.getBuild().getDirectory(), "reference");
127 referenceDir.mkdirs();
128
129
130 File referenceBuildinfo = downloadOrCreateReferenceBuildinfo(mono, artifacts, referenceDir);
131
132
133 compareWithReference(artifacts, referenceBuildinfo);
134 }
135
136 private File downloadOrCreateReferenceBuildinfo(boolean mono, Map<Artifact, String> artifacts, File referenceDir)
137 throws MojoExecutionException {
138 RemoteRepository repo = createReferenceRepo();
139
140 ReferenceBuildinfoUtil rmb = new ReferenceBuildinfoUtil(
141 getLog(),
142 referenceDir,
143 artifacts,
144 artifactFactory,
145 repoSystem,
146 repoSession,
147 artifactHandlerManager,
148 rtInformation);
149
150 return rmb.downloadOrCreateReferenceBuildinfo(repo, project, buildinfoFile, mono);
151 }
152
153 private void compareWithReference(Map<Artifact, String> artifacts, File referenceBuildinfo)
154 throws MojoExecutionException {
155 Properties actual = BuildInfoWriter.loadOutputProperties(buildinfoFile);
156 Properties reference = BuildInfoWriter.loadOutputProperties(referenceBuildinfo);
157
158 int ok = 0;
159 List<String> okFilenames = new ArrayList<>();
160 List<String> koFilenames = new ArrayList<>();
161 List<String> diffoscopes = new ArrayList<>();
162 File referenceDir = referenceBuildinfo.getParentFile();
163 for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) {
164 Artifact artifact = entry.getKey();
165 String prefix = entry.getValue();
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;
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 getLog().error("see "
189 + MessageUtils.buffer()
190 .project("diff " + relative(referenceBuildinfo) + " " + relative(buildinfoFile))
191 .toString());
192 getLog().error("see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
193 } else {
194 getLog().info("Reproducible Build output summary: "
195 + MessageUtils.buffer().success(ok + " files ok"));
196 }
197
198
199 File buildcompare = new File(
200 buildinfoFile.getParentFile(), buildinfoFile.getName().replaceFirst(".buildinfo$", ".buildcompare"));
201 try (PrintWriter p = new PrintWriter(new BufferedWriter(
202 new OutputStreamWriter(new FileOutputStream(buildcompare), StandardCharsets.UTF_8)))) {
203 p.println("version=" + project.getVersion());
204 p.println("ok=" + ok);
205 p.println("ko=" + ko);
206 p.println("okFiles=\"" + StringUtils.join(okFilenames.iterator(), " ") + '"');
207 p.println("koFiles=\"" + StringUtils.join(koFilenames.iterator(), " ") + '"');
208 Properties ref = PropertyUtils.loadOptionalProperties(referenceBuildinfo);
209 String v = ref.getProperty("java.version");
210 if (v != null) {
211 p.println("reference_java_version=\"" + v + '"');
212 }
213 v = ref.getProperty("os.name");
214 if (v != null) {
215 p.println("reference_os_name=\"" + v + '"');
216 }
217 for (String diffoscope : diffoscopes) {
218 p.print("# ");
219 p.println(diffoscope);
220 }
221 getLog().info("Reproducible Build output comparison saved to " + buildcompare);
222 } catch (IOException e) {
223 throw new MojoExecutionException("Error creating file " + buildcompare, e);
224 }
225
226 copyAggregateToRoot(buildcompare);
227
228 if (ko + missing > 0) {
229 throw new MojoExecutionException("Build artifacts are different from reference");
230 }
231 }
232
233
234 private String[] checkArtifact(
235 Artifact artifact, String prefix, Properties reference, Properties actual, File referenceDir) {
236 String actualFilename = (String) actual.remove(prefix + ".filename");
237 String actualLength = (String) actual.remove(prefix + ".length");
238 String actualSha512 = (String) actual.remove(prefix + ".checksums.sha512");
239
240 String referencePrefix = findPrefix(reference, actualFilename);
241 String referenceLength = (String) reference.remove(referencePrefix + ".length");
242 String referenceSha512 = (String) reference.remove(referencePrefix + ".checksums.sha512");
243
244 String issue = null;
245 if (!actualLength.equals(referenceLength)) {
246 issue = "size";
247 } else if (!actualSha512.equals(referenceSha512)) {
248 issue = "sha512";
249 }
250
251 if (issue != null) {
252 String diffoscope = diffoscope(artifact, referenceDir);
253 getLog().error(issue + " mismatch " + MessageUtils.buffer().strong(actualFilename) + ": investigate with "
254 + MessageUtils.buffer().project(diffoscope));
255 return new String[] {actualFilename, diffoscope};
256 }
257 return new String[] {actualFilename, null};
258 }
259
260 private String diffoscope(Artifact a, File referenceDir) {
261 File actual = a.getFile();
262
263
264 File reference = new File(referenceDir, getRepositoryFilename(a));
265 if ((actual == null) || (reference == null)) {
266 return "missing file for " + a.getId() + " reference = "
267 + (reference == null ? "null" : relative(reference)) + " actual = "
268 + (actual == null ? "null" : relative(actual));
269 }
270 return "diffoscope " + relative(reference) + " " + relative(actual);
271 }
272
273 private String getRepositoryFilename(Artifact a) {
274 String path = artifactRepositoryLayout.pathOf(a);
275 return path.substring(path.lastIndexOf('/'));
276 }
277
278 private String relative(File file) {
279 File basedir = getExecutionRoot().getBasedir();
280 int length = basedir.getPath().length();
281 String path = file.getPath();
282 return path.substring(length + 1);
283 }
284
285 private static String findPrefix(Properties reference, String actualFilename) {
286 for (String name : reference.stringPropertyNames()) {
287 if (name.endsWith(".filename") && actualFilename.equals(reference.getProperty(name))) {
288 reference.remove(name);
289 return name.substring(0, name.length() - ".filename".length());
290 }
291 }
292 return null;
293 }
294
295 private RemoteRepository createReferenceRepo() throws MojoExecutionException {
296 if (referenceRepo.contains("::")) {
297
298 int index = referenceRepo.indexOf("::");
299 String id = referenceRepo.substring(0, index);
300 String url = referenceRepo.substring(index + 2);
301 return createDeploymentArtifactRepository(id, url);
302 } else if (referenceRepo.contains(":")) {
303
304 return createDeploymentArtifactRepository("reference", referenceRepo);
305 }
306
307
308 for (RemoteRepository repo : remoteRepos) {
309 if (referenceRepo.equals(repo.getId())) {
310 return repo;
311 }
312 }
313 throw new MojoExecutionException("Could not find repository with id = " + referenceRepo);
314 }
315
316 private static RemoteRepository createDeploymentArtifactRepository(String id, String url) {
317 return new RemoteRepository.Builder(id, "default", url).build();
318 }
319 }