Skip to content

Compare local artifacts to those from maven central #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
target
/report
12 changes: 12 additions & 0 deletions report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh

set -e

rm -r report || true
mkdir report

find . -name "reproducible-builds-report.md" | sort | xargs cat > report/index.md
find . -name "reproducible-builds-diffoscope-output-*" | sort | while read line ; do cp -r $line report ; done

markdown report/index.md > report/index.html

210 changes: 131 additions & 79 deletions src/main/scala/ReproducibleBuildsPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.nio.charset.Charset
import java.nio.file.Files

import scala.concurrent.duration._
import gigahorse.GigahorseSupport
import gigahorse.{GigahorseSupport, StatusError}
import sbt.{io => _, _}
import sbt.Keys._
import sbt.Classpaths._
Expand Down Expand Up @@ -40,98 +40,151 @@ object ReproducibleBuildsPlugin extends AutoPlugin {

val reproducibleBuildsCertification = taskKey[File]("Create a Reproducible Builds certification")
val reproducibleBuildsCheckCertification = taskKey[Unit]("Download and compare Reproducible Builds certifications")
val reproducibleBuildsCheckMavenCentral = taskKey[File]("Compare Reproducible Build certifications against those published on Maven Central")

val bzztNetResolver = Resolver.url("repo.bzzt.net", url("http://repo.bzzt.net:8000"))(Patterns().withArtifactPatterns(Vector(
// We default to a Maven-style pattern with host and timestamp to reduce naming collisions, and branch if populated
"[organisation]/[module](_[scalaVersion])(_[sbtVersion])/([branch]/)[revision]/[artifact]-[revision](-[classifier])(-[host])(-[timestamp]).[ext]"
)))

lazy val ourCertification = Def.task[Certification] {
Certification(
organization.value,
reproducibleBuildsPackageName.value,
version.value,
scmInfo.value,
(packagedArtifacts in Compile).value,
(scalaVersion in artifactName).value,
(scalaBinaryVersion in artifactName).value,
sbtVersion.value
)
}

lazy val ourCertificationFile = Def.task[File] {
val certification = ourCertification.value

val targetDirPath = crossTarget.value
val targetFilePath = targetDirPath.toPath.resolve(targetFilename(certification.artifactId, certification.version, certification.classifier))

Files.write(targetFilePath, certification.asPropertyString.getBytes(Charset.forName("UTF-8")))

targetFilePath.toFile
}

sealed trait ArtifactUrlTarget
case object MavenCentral extends ArtifactUrlTarget
case object PublishToPrefix extends ArtifactUrlTarget

def artifactUrl(target: ArtifactUrlTarget, ext: String) = Def.task {
import scala.collection.JavaConverters._
val extraModuleAttributes = {
val scalaVer = Map("scalaVersion" -> scalaBinaryVersion.value)
if (sbtPlugin.value) scalaVer + ("sbtVersion" -> sbtBinaryVersion.value)
else scalaVer
}.asJava

val pattern: String = target match {
case MavenCentral =>
"http://repo1.maven.org/maven2/[organisation]/[module](_[scalaVersion])/[revision]/[artifact](_[scalaVersion])-[revision](-[classifier]).[ext]"
case PublishToPrefix =>
val pattern = (publishTo in ReproducibleBuilds).value.getOrElse(bzztNetResolver).asInstanceOf[URLRepository].patterns.artifactPatterns.head
pattern.substring(0, pattern.lastIndexOf("/") + 1)
}

IvyPatternHelper.substitute(
pattern,
organization.value.replace('.', '/'),
reproducibleBuildsPackageName.value,
version.value,
reproducibleBuildsPackageName.value,
ext,
ext,
"compile",
extraModuleAttributes,
null
)
}

override lazy val projectSettings = Seq(
publishCertification := true,
hostname := InetAddress.getLocalHost.getHostName,
resolvers += bzztNetResolver,
packageBin in Compile := postProcessJar((packageBin in Compile).value),
reproducibleBuildsPackageName := moduleName.value,
reproducibleBuildsCertification := {
val certification = Certification(
organization.value,
reproducibleBuildsPackageName.value,
version.value,
scmInfo.value,
(packagedArtifacts in Compile).value,
(scalaVersion in artifactName).value,
(scalaBinaryVersion in artifactName).value,
sbtVersion.value
)

val targetDirPath = crossTarget.value
val targetFilePath = targetDirPath.toPath.resolve(targetFilename(certification.artifactId, certification.version, certification.classifier))

Files.write(targetFilePath, certification.asPropertyString.getBytes(Charset.forName("UTF-8")))

targetFilePath.toFile
},
reproducibleBuildsCertification := ourCertificationFile.value,
artifact in ReproducibleBuilds := Artifact(reproducibleBuildsPackageName.value, "buildinfo", "buildinfo"),
packagedArtifacts ++= {
val generatedArtifact = Map(
(artifact in ReproducibleBuilds).value ->
{
val certification = Certification(
organization.value,
reproducibleBuildsPackageName.value,
version.value,
scmInfo.value,
(packagedArtifacts in Compile).value,
(scalaVersion in artifactName).value,
(scalaBinaryVersion in artifactName).value,
sbtVersion.value
)

val targetDirPath = crossTarget.value
val targetFilePath = targetDirPath.toPath.resolve(targetFilename(certification.artifactId, certification.version, certification.classifier))

Files.write(targetFilePath, certification.asPropertyString.getBytes(Charset.forName("UTF-8")))

targetFilePath.toFile
}
(artifact in ReproducibleBuilds).value -> ourCertificationFile.value
)

if (publishCertification.value) generatedArtifact else Map.empty[Artifact, File]
},
reproducibleBuildsCheckMavenCentral := {
val ourArtifacts = (packagedArtifacts in Compile).value
val url = artifactUrl(MavenCentral,"buildinfo").value

val log = streams.value.log
log.info(s"Downloading certification from [$url]")
val targetDirPath = crossTarget.value

val report: Future[String] = checkVerification(ourCertification.value, uri(url))
.flatMap(result => {
showResult(log, result)
if (result.ok) Future.successful(result.asMarkdown)
else Future.sequence({
result.verdicts
.collect { case (filename: String, m: Mismatch) => {
val ext = filename.substring(filename.lastIndexOf('.') + 1)
val mavenArtifactUrl = artifactUrl(MavenCentral, "").value + ext

val artifactName = mavenArtifactUrl.substring(mavenArtifactUrl.lastIndexOf('/') + 1)

val ourArtifact = ourArtifacts.collect { case (art, file) if art.`type` == ext => file }.toList match {
case List() => throw new IllegalStateException(s"Did not find local artifact for $artifactName ($ext)")
case List(artifact) => artifact
case multiple => throw new IllegalStateException(s"Found multiple artifacts for $ext")
}

http.run(GigahorseSupport.url(mavenArtifactUrl)).map { entity =>
val mavenCentralArtifactsPath = targetDirPath.toPath.resolve("mavenCentralArtifact")
mavenCentralArtifactsPath.toFile.mkdirs()
val mavenCentralArtifact = mavenCentralArtifactsPath.resolve(artifactName)
Files.write(
mavenCentralArtifact,
entity.bodyAsByteBuffer.array()
)
val diffoscopeOutputDir = targetDirPath.toPath.resolve(s"reproducible-builds-diffoscope-output-$artifactName")
val cmd = s"diffoscope --html-dir $diffoscopeOutputDir $ourArtifact $mavenCentralArtifact"
new ProcessBuilder(
"diffoscope",
"--html-dir",
diffoscopeOutputDir.toFile.getAbsolutePath,
ourArtifact.getAbsolutePath,
mavenCentralArtifact.toFile.getAbsolutePath
).start().waitFor()
log.info(s"Running '$cmd' for a detailed report on the differences")
s"See the [diffoscope report](reproducible-builds-diffoscope-output-$artifactName/index.html) for a detailed explanation " +
" of the differences between the freshly built artifact and the one published to Maven Central"
}.recover {
case s: StatusError if s.status == 404 =>
s"Unfortunately no artifact was found at $mavenArtifactUrl to diff against."
}
}
}
}).map { verdicts => result.asMarkdown + "\n\n" + verdicts.mkString("", "\n\n", "\n\n") }
})

val targetFilePath = targetDirPath.toPath.resolve("reproducible-builds-report.md")

Files.write(targetFilePath, Await.result(report, 30.seconds).getBytes(Charset.forName("UTF-8")))

targetFilePath.toFile
},
reproducibleBuildsCheckCertification := {
val ours = Certification(
organization.value,
reproducibleBuildsPackageName.value,
version.value,
scmInfo.value,
(packagedArtifacts in Compile).value,
(scalaVersion in artifactName).value,
(scalaBinaryVersion in artifactName).value,
sbtVersion.value
)
val ours = ourCertification.value
val groupId = organization.value
// TODO also check against the 'official' published buildinfo
val pattern = (publishTo in ReproducibleBuilds).value.getOrElse(bzztNetResolver).asInstanceOf[URLRepository].patterns.artifactPatterns.head
val prefixPattern = pattern.substring(0, pattern.lastIndexOf("/") + 1)
import scala.collection.JavaConverters._
val extraModuleAttributes = {
val scalaVer = Map("scalaVersion" -> scalaBinaryVersion.value)
if (sbtPlugin.value) scalaVer + ("sbtVersion" -> sbtBinaryVersion.value)
else scalaVer
}.asJava

val prefix = IvyPatternHelper.substitute(
prefixPattern,
organization.value.replace('.', '/'),
reproducibleBuildsPackageName.value,
version.value,
reproducibleBuildsPackageName.value,
"buildinfo",
"buildinfo",
"compile",
extraModuleAttributes,
null
)
val prefix = artifactUrl(PublishToPrefix, "buildinfo").value
val log = streams.value.log
log.info(s"Discovering certifications at [$prefix]")
// TODO add Accept header to request JSON-formatted
Expand All @@ -147,13 +200,7 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
Future.sequence(results)
}.map { resultList =>
log.info(s"Processed ${resultList.size} results. ${resultList.count(_.ok)} matching attestations, ${resultList.filterNot(_.ok).size} mismatches");
resultList.foreach { result =>
log.info(s"${result.uri}:")
log.info("- " + (if (result.ok) "OK" else "NOT OK"))
result.verdicts.foreach {
case (filename, verdict) => log.info(s"- $filename: $verdict")
}
}
resultList.foreach { result => showResult(log, result) }
}
Await.result(done, 30.seconds)
},
Expand Down Expand Up @@ -218,6 +265,9 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
publishM2 := publishTask(publishM2Configuration).value
))

private def showResult(log: Logger, result: VerificationResult): Unit =
log.info(result.asMarkdown)

private def gpgPluginSettings =
if (gpgPluginOnClasspath) GpgHelpers.settings
else Seq.empty
Expand All @@ -242,6 +292,8 @@ object ReproducibleBuildsPlugin extends AutoPlugin {
http.run(GigahorseSupport.url(uri.toASCIIString)).map { entity =>
val theirs = Certification(entity.bodyAsString)
VerificationResult(uri, ourSums, theirs.checksums)
}.recover {
case e: StatusError if e.status == 404 => VerificationResult(uri, ourSums, Seq.empty)
}
}

Expand Down
40 changes: 33 additions & 7 deletions src/main/scala/VerificationResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,49 @@ package net.bzzt.reproduciblebuilds

import java.net.URI

sealed trait Verdict {
val description: String
}
case object Match extends Verdict {
val description = "Match"
}
case object MissingInTheirs extends Verdict {
val description = "Missing in theirs but present in ours"
}
case object MissingInOurs extends Verdict {
val description = "Missing in ours but present in theirs"
}
case class Mismatch(our: Checksum, their: Checksum) extends Verdict {
val description = s"Mismatch: our ${our.hexChecksum} did not match their ${their.hexChecksum}"
}

case class VerificationResult(
uri: URI,
ourSums: Map[String, Checksum],
remoteSums: Map[String, Checksum],
) {
def verdicts: Seq[(String, String)] =
def asMarkdown = {
val artifactName = uri.toASCIIString.substring(uri.toASCIIString.lastIndexOf('/') + 1)
s"""# `$artifactName`: ${if (ok) "OK" else "NOT OK"}
|
|${verdicts.map { case (filename, verdict) => s"- $filename: ${verdict.description}" }.mkString("\n")}
""".stripMargin
}

/**
* filename -> verdict
*/
def verdicts: Seq[(String, Verdict)] =
ourSums.map {
case (key, value) => (key, verdict(key, value))
}.toSeq ++
ourSums.keySet.diff(remoteSums.keySet).map { missingInTheirs => (missingInTheirs, "Missing in their checksums but present in ours") } ++
remoteSums.keySet.diff(ourSums.keySet).map { missingInOurs => (missingInOurs, "Missing in our checksums but present in theirs") }
remoteSums.keySet.diff(ourSums.keySet).map { missingInOurs => (missingInOurs, MissingInOurs) }

def verdict(filename: String, ourSum: Checksum): String = remoteSums.get(filename) match {
case None => "Not found in remote attestation"
def verdict(filename: String, ourSum: Checksum): Verdict = remoteSums.get(filename) match {
case None => MissingInTheirs
case Some(checksum) =>
if (checksum == ourSum) "Match"
else s"Mismatch: our ${ourSum.hexChecksum} did not match their ${checksum.hexChecksum}"
if (checksum == ourSum) Match
else Mismatch(ourSum, checksum)
}

def ok = ourSums == remoteSums
Expand Down