@@ -5,7 +5,7 @@ import java.nio.charset.Charset
5
5
import java .nio .file .Files
6
6
7
7
import scala .concurrent .duration ._
8
- import gigahorse .GigahorseSupport
8
+ import gigahorse .{ GigahorseSupport , StatusError }
9
9
import sbt .{io => _ , _ }
10
10
import sbt .Keys ._
11
11
import sbt .Classpaths ._
@@ -40,98 +40,151 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
40
40
41
41
val reproducibleBuildsCertification = taskKey[File ](" Create a Reproducible Builds certification" )
42
42
val reproducibleBuildsCheckCertification = taskKey[Unit ](" Download and compare Reproducible Builds certifications" )
43
+ val reproducibleBuildsCheckMavenCentral = taskKey[File ](" Compare Reproducible Build certifications against those published on Maven Central" )
43
44
44
45
val bzztNetResolver = Resolver .url(" repo.bzzt.net" , url(" http://repo.bzzt.net:8000" ))(Patterns ().withArtifactPatterns(Vector (
45
46
// We default to a Maven-style pattern with host and timestamp to reduce naming collisions, and branch if populated
46
47
" [organisation]/[module](_[scalaVersion])(_[sbtVersion])/([branch]/)[revision]/[artifact]-[revision](-[classifier])(-[host])(-[timestamp]).[ext]"
47
48
)))
48
49
50
+ lazy val ourCertification = Def .task[Certification ] {
51
+ Certification (
52
+ organization.value,
53
+ reproducibleBuildsPackageName.value,
54
+ version.value,
55
+ scmInfo.value,
56
+ (packagedArtifacts in Compile ).value,
57
+ (scalaVersion in artifactName).value,
58
+ (scalaBinaryVersion in artifactName).value,
59
+ sbtVersion.value
60
+ )
61
+ }
62
+
63
+ lazy val ourCertificationFile = Def .task[File ] {
64
+ val certification = ourCertification.value
65
+
66
+ val targetDirPath = crossTarget.value
67
+ val targetFilePath = targetDirPath.toPath.resolve(targetFilename(certification.artifactId, certification.version, certification.classifier))
68
+
69
+ Files .write(targetFilePath, certification.asPropertyString.getBytes(Charset .forName(" UTF-8" )))
70
+
71
+ targetFilePath.toFile
72
+ }
73
+
74
+ sealed trait ArtifactUrlTarget
75
+ case object MavenCentral extends ArtifactUrlTarget
76
+ case object PublishToPrefix extends ArtifactUrlTarget
77
+
78
+ def artifactUrl (target : ArtifactUrlTarget , ext : String ) = Def .task {
79
+ import scala .collection .JavaConverters ._
80
+ val extraModuleAttributes = {
81
+ val scalaVer = Map (" scalaVersion" -> scalaBinaryVersion.value)
82
+ if (sbtPlugin.value) scalaVer + (" sbtVersion" -> sbtBinaryVersion.value)
83
+ else scalaVer
84
+ }.asJava
85
+
86
+ val pattern : String = target match {
87
+ case MavenCentral =>
88
+ " http://repo1.maven.org/maven2/[organisation]/[module](_[scalaVersion])/[revision]/[artifact](_[scalaVersion])-[revision](-[classifier]).[ext]"
89
+ case PublishToPrefix =>
90
+ val pattern = (publishTo in ReproducibleBuilds ).value.getOrElse(bzztNetResolver).asInstanceOf [URLRepository ].patterns.artifactPatterns.head
91
+ pattern.substring(0 , pattern.lastIndexOf(" /" ) + 1 )
92
+ }
93
+
94
+ IvyPatternHelper .substitute(
95
+ pattern,
96
+ organization.value.replace('.' , '/' ),
97
+ reproducibleBuildsPackageName.value,
98
+ version.value,
99
+ reproducibleBuildsPackageName.value,
100
+ ext,
101
+ ext,
102
+ " compile" ,
103
+ extraModuleAttributes,
104
+ null
105
+ )
106
+ }
107
+
49
108
override lazy val projectSettings = Seq (
50
109
publishCertification := true ,
51
110
hostname := InetAddress .getLocalHost.getHostName,
52
111
resolvers += bzztNetResolver,
53
112
packageBin in Compile := postProcessJar((packageBin in Compile ).value),
54
113
reproducibleBuildsPackageName := moduleName.value,
55
- reproducibleBuildsCertification := {
56
- val certification = Certification (
57
- organization.value,
58
- reproducibleBuildsPackageName.value,
59
- version.value,
60
- scmInfo.value,
61
- (packagedArtifacts in Compile ).value,
62
- (scalaVersion in artifactName).value,
63
- (scalaBinaryVersion in artifactName).value,
64
- sbtVersion.value
65
- )
66
-
67
- val targetDirPath = crossTarget.value
68
- val targetFilePath = targetDirPath.toPath.resolve(targetFilename(certification.artifactId, certification.version, certification.classifier))
69
-
70
- Files .write(targetFilePath, certification.asPropertyString.getBytes(Charset .forName(" UTF-8" )))
71
-
72
- targetFilePath.toFile
73
- },
114
+ reproducibleBuildsCertification := ourCertificationFile.value,
74
115
artifact in ReproducibleBuilds := Artifact (reproducibleBuildsPackageName.value, " buildinfo" , " buildinfo" ),
75
116
packagedArtifacts ++= {
76
117
val generatedArtifact = Map (
77
- (artifact in ReproducibleBuilds ).value ->
78
- {
79
- val certification = Certification (
80
- organization.value,
81
- reproducibleBuildsPackageName.value,
82
- version.value,
83
- scmInfo.value,
84
- (packagedArtifacts in Compile ).value,
85
- (scalaVersion in artifactName).value,
86
- (scalaBinaryVersion in artifactName).value,
87
- sbtVersion.value
88
- )
89
-
90
- val targetDirPath = crossTarget.value
91
- val targetFilePath = targetDirPath.toPath.resolve(targetFilename(certification.artifactId, certification.version, certification.classifier))
92
-
93
- Files .write(targetFilePath, certification.asPropertyString.getBytes(Charset .forName(" UTF-8" )))
94
-
95
- targetFilePath.toFile
96
- }
118
+ (artifact in ReproducibleBuilds ).value -> ourCertificationFile.value
97
119
)
98
120
99
121
if (publishCertification.value) generatedArtifact else Map .empty[Artifact , File ]
100
122
},
123
+ reproducibleBuildsCheckMavenCentral := {
124
+ val ourArtifacts = (packagedArtifacts in Compile ).value
125
+ val url = artifactUrl(MavenCentral ," buildinfo" ).value
126
+
127
+ val log = streams.value.log
128
+ log.info(s " Downloading certification from [ $url] " )
129
+ val targetDirPath = crossTarget.value
130
+
131
+ val report : Future [String ] = checkVerification(ourCertification.value, uri(url))
132
+ .flatMap(result => {
133
+ showResult(log, result)
134
+ if (result.ok) Future .successful(result.asMarkdown)
135
+ else Future .sequence({
136
+ result.verdicts
137
+ .collect { case (filename : String , m : Mismatch ) => {
138
+ val ext = filename.substring(filename.lastIndexOf('.' ) + 1 )
139
+ val mavenArtifactUrl = artifactUrl(MavenCentral , " " ).value + ext
140
+
141
+ val artifactName = mavenArtifactUrl.substring(mavenArtifactUrl.lastIndexOf('/' ) + 1 )
142
+
143
+ val ourArtifact = ourArtifacts.collect { case (art, file) if art.`type` == ext => file }.toList match {
144
+ case List () => throw new IllegalStateException (s " Did not find local artifact for $artifactName ( $ext) " )
145
+ case List (artifact) => artifact
146
+ case multiple => throw new IllegalStateException (s " Found multiple artifacts for $ext" )
147
+ }
148
+
149
+ http.run(GigahorseSupport .url(mavenArtifactUrl)).map { entity =>
150
+ val mavenCentralArtifactsPath = targetDirPath.toPath.resolve(" mavenCentralArtifact" )
151
+ mavenCentralArtifactsPath.toFile.mkdirs()
152
+ val mavenCentralArtifact = mavenCentralArtifactsPath.resolve(artifactName)
153
+ Files .write(
154
+ mavenCentralArtifact,
155
+ entity.bodyAsByteBuffer.array()
156
+ )
157
+ val diffoscopeOutputDir = targetDirPath.toPath.resolve(s " reproducible-builds-diffoscope-output- $artifactName" )
158
+ val cmd = s " diffoscope --html-dir $diffoscopeOutputDir $ourArtifact $mavenCentralArtifact"
159
+ new ProcessBuilder (
160
+ " diffoscope" ,
161
+ " --html-dir" ,
162
+ diffoscopeOutputDir.toFile.getAbsolutePath,
163
+ ourArtifact.getAbsolutePath,
164
+ mavenCentralArtifact.toFile.getAbsolutePath
165
+ ).start().waitFor()
166
+ log.info(s " Running ' $cmd' for a detailed report on the differences " )
167
+ s " See the [diffoscope report](reproducible-builds-diffoscope-output- $artifactName/index.html) for a detailed explanation " +
168
+ " of the differences between the freshly built artifact and the one published to Maven Central"
169
+ }.recover {
170
+ case s : StatusError if s.status == 404 =>
171
+ s " Unfortunately no artifact was found at $mavenArtifactUrl to diff against. "
172
+ }
173
+ }
174
+ }
175
+ }).map { verdicts => result.asMarkdown + " \n\n " + verdicts.mkString(" " , " \n\n " , " \n\n " ) }
176
+ })
177
+
178
+ val targetFilePath = targetDirPath.toPath.resolve(" reproducible-builds-report.md" )
179
+
180
+ Files .write(targetFilePath, Await .result(report, 30 .seconds).getBytes(Charset .forName(" UTF-8" )))
181
+
182
+ targetFilePath.toFile
183
+ },
101
184
reproducibleBuildsCheckCertification := {
102
- val ours = Certification (
103
- organization.value,
104
- reproducibleBuildsPackageName.value,
105
- version.value,
106
- scmInfo.value,
107
- (packagedArtifacts in Compile ).value,
108
- (scalaVersion in artifactName).value,
109
- (scalaBinaryVersion in artifactName).value,
110
- sbtVersion.value
111
- )
185
+ val ours = ourCertification.value
112
186
val groupId = organization.value
113
- // TODO also check against the 'official' published buildinfo
114
- val pattern = (publishTo in ReproducibleBuilds ).value.getOrElse(bzztNetResolver).asInstanceOf [URLRepository ].patterns.artifactPatterns.head
115
- val prefixPattern = pattern.substring(0 , pattern.lastIndexOf(" /" ) + 1 )
116
- import scala .collection .JavaConverters ._
117
- val extraModuleAttributes = {
118
- val scalaVer = Map (" scalaVersion" -> scalaBinaryVersion.value)
119
- if (sbtPlugin.value) scalaVer + (" sbtVersion" -> sbtBinaryVersion.value)
120
- else scalaVer
121
- }.asJava
122
-
123
- val prefix = IvyPatternHelper .substitute(
124
- prefixPattern,
125
- organization.value.replace('.' , '/' ),
126
- reproducibleBuildsPackageName.value,
127
- version.value,
128
- reproducibleBuildsPackageName.value,
129
- " buildinfo" ,
130
- " buildinfo" ,
131
- " compile" ,
132
- extraModuleAttributes,
133
- null
134
- )
187
+ val prefix = artifactUrl(PublishToPrefix , " buildinfo" ).value
135
188
val log = streams.value.log
136
189
log.info(s " Discovering certifications at [ $prefix] " )
137
190
// TODO add Accept header to request JSON-formatted
@@ -147,13 +200,7 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
147
200
Future .sequence(results)
148
201
}.map { resultList =>
149
202
log.info(s " Processed ${resultList.size} results. ${resultList.count(_.ok)} matching attestations, ${resultList.filterNot(_.ok).size} mismatches " );
150
- resultList.foreach { result =>
151
- log.info(s " ${result.uri}: " )
152
- log.info(" - " + (if (result.ok) " OK" else " NOT OK" ))
153
- result.verdicts.foreach {
154
- case (filename, verdict) => log.info(s " - $filename: $verdict" )
155
- }
156
- }
203
+ resultList.foreach { result => showResult(log, result) }
157
204
}
158
205
Await .result(done, 30 .seconds)
159
206
},
@@ -218,6 +265,9 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
218
265
publishM2 := publishTask(publishM2Configuration).value
219
266
))
220
267
268
+ private def showResult (log : Logger , result : VerificationResult ): Unit =
269
+ log.info(result.asMarkdown)
270
+
221
271
private def gpgPluginSettings =
222
272
if (gpgPluginOnClasspath) GpgHelpers .settings
223
273
else Seq .empty
@@ -242,6 +292,8 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
242
292
http.run(GigahorseSupport .url(uri.toASCIIString)).map { entity =>
243
293
val theirs = Certification (entity.bodyAsString)
244
294
VerificationResult (uri, ourSums, theirs.checksums)
295
+ }.recover {
296
+ case e : StatusError if e.status == 404 => VerificationResult (uri, ourSums, Seq .empty)
245
297
}
246
298
}
247
299
0 commit comments