diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2017-12-20 08:04:18 -0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2017-12-20 08:04:18 -0800 |
commit | d288158e746a5350bb104f0b57abbe8f83485845 (patch) | |
tree | 9bc5a761b6db1c962262c4bd73705671f4f15081 /scalaplugin | |
parent | 5f200e4a1037c2ce477096a8da58561e86a58f30 (diff) | |
parent | 9d19d740c5b387704e08fb89412b8318549a4fc5 (diff) | |
download | mill-d288158e746a5350bb104f0b57abbe8f83485845.tar.gz mill-d288158e746a5350bb104f0b57abbe8f83485845.tar.bz2 mill-d288158e746a5350bb104f0b57abbe8f83485845.zip |
Merge branch 'master' of github.com:lihaoyi/mill
Diffstat (limited to 'scalaplugin')
14 files changed, 759 insertions, 23 deletions
diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala b/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala index 637906a5..c89254b0 100644 --- a/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala +++ b/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala @@ -6,7 +6,7 @@ import java.net.URLClassLoader import java.util.Optional import ammonite.ops._ -import coursier.{Cache, Fetch, MavenRepository, Repository, Resolution} +import coursier.{Cache, Fetch, MavenRepository, Repository, Resolution, Module => CoursierModule} import mill.define.Worker import mill.eval.{PathRef, Result} import mill.util.Ctx @@ -159,7 +159,7 @@ object Lib{ scalaVersion: String, scalaBinaryVersion: String, deps: Seq[Dep], - sources: Boolean = false): Seq[PathRef] = { + sources: Boolean = false): Result[Seq[PathRef]] = { val flattened = deps.map{ case Dep.Java(dep) => dep case Dep.Scala(dep) => @@ -171,15 +171,30 @@ object Lib{ val fetch = Fetch.from(repositories, Cache.fetch()) val resolution = start.process.run(fetch).unsafePerformSync - val sourceOrJar = - if (sources) resolution.classifiersArtifacts(Seq("sources")) - else resolution.artifacts - val localArtifacts: Seq[File] = scalaz.concurrent.Task - .gatherUnordered(sourceOrJar.map(Cache.file(_).run)) - .unsafePerformSync - .flatMap(_.toOption) - - localArtifacts.map(p => PathRef(Path(p), quick = true)) + val errs = resolution.metadataErrors + if(errs.nonEmpty) { + val header = + s"""| + |Resolution failed for ${errs.length} modules: + |-------------------------------------------- + |""".stripMargin + + val errLines = errs.map { + case ((module, vsn), errMsgs) => s" ${module.trim}:$vsn \n\t" + errMsgs.mkString("\n\t") + }.mkString("\n") + val msg = header + errLines + "\n" + Result.Failure(msg) + } else { + val sourceOrJar = + if (sources) resolution.classifiersArtifacts(Seq("sources")) + else resolution.artifacts + val localArtifacts: Seq[File] = scalaz.concurrent.Task + .gatherUnordered(sourceOrJar.map(Cache.file(_).run)) + .unsafePerformSync + .flatMap(_.toOption) + + localArtifacts.map(p => PathRef(Path(p), quick = true)) + } } def scalaCompilerIvyDeps(scalaVersion: String) = Seq( Dep.Java("org.scala-lang", "scala-compiler", scalaVersion), @@ -195,4 +210,4 @@ object Lib{ "#!/usr/bin/env sh", "exec java -jar \"$0\" \"$@\"" ) -}
\ No newline at end of file +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala b/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala index 9f9425a4..949682ca 100644 --- a/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala +++ b/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala @@ -2,7 +2,7 @@ package mill package scalaplugin import ammonite.ops._ -import coursier.{Cache, MavenRepository, Repository, Resolution} +import coursier.{Cache, MavenRepository, Repository} import mill.define.Task import mill.define.Task.{Module, TaskModule} import mill.eval.{PathRef, Result} @@ -49,7 +49,8 @@ trait TestScalaModule extends ScalaModule with TaskModule { } } } -trait ScalaModule extends Module with TaskModule{ outer => + +trait ScalaModule extends Module with TaskModule { outer => def defaultCommandName() = "run" trait Tests extends TestScalaModule{ def scalaVersion = outer.scalaVersion() @@ -101,6 +102,7 @@ trait ScalaModule extends Module with TaskModule{ outer => sources ) } + def externalCompileDepClasspath: T[Seq[PathRef]] = T{ Task.traverse(projectDeps)(_.externalCompileDepClasspath)().flatten ++ resolveDeps( @@ -115,6 +117,7 @@ trait ScalaModule extends Module with TaskModule{ outer => sources = true )() } + /** * Things that need to be on the classpath in order for this code to compile; * might be less than the runtime classpath @@ -140,8 +143,9 @@ trait ScalaModule extends Module with TaskModule{ outer => Seq(dep) ) classpath match { - case Seq(single) => PathRef(single.path, quick = true) - case Seq() => throw new Exception(dep + " resolution failed") + case Result.Success(Seq(single)) => PathRef(single.path, quick = true) + case Result.Success(Seq()) => throw new Exception(dep + " resolution failed") + case f: Result.Failure => throw new Exception(dep + s" resolution failed.\n + ${f.msg}") case _ => throw new Exception(dep + " resolution resulted in more than one file") } } @@ -195,6 +199,7 @@ trait ScalaModule extends Module with TaskModule{ outer => (runDepClasspath().filter(_.path.ext != "pom") ++ Seq(resources(), compile().classes)).map(_.path).filter(exists) } + def assembly = T{ createAssembly(assemblyClasspath(), prependShellScript = prependShellScript()) } @@ -202,7 +207,34 @@ trait ScalaModule extends Module with TaskModule{ outer => def classpath = T{ Seq(resources(), compile().classes) } def jar = T{ - createJar(Seq(resources(), compile().classes).map(_.path).filter(exists), mainClass()) + createJar( + Seq(resources(), compile().classes).map(_.path).filter(exists), + mainClass() + ) + } + + def docsJar = T { + val outDir = T.ctx().dest + + val javadocDir = outDir / 'javadoc + mkdir(javadocDir) + + val options = { + val files = ls.rec(sources().path).filter(_.isFile).map(_.toNIO.toString) + files ++ Seq("-d", javadocDir.toNIO.toString, "-usejavacp") + } + + subprocess( + "scala.tools.nsc.ScalaDoc", + compileDepClasspath().filterNot(_.path.ext == "pom").map(_.path), + options = options + ) + + createJar(Seq(javadocDir))(outDir / "javadoc.jar") + } + + def sourcesJar = T { + createJar(Seq(sources(), resources()).map(_.path).filter(exists))(T.ctx().dest / "sources.jar") } def run() = T.command{ @@ -222,6 +254,79 @@ trait ScalaModule extends Module with TaskModule{ outer => ) } } + +trait PublishModule extends ScalaModule { outer => + import mill.scalaplugin.publish._ + + def publishName: T[String] = basePath.last.toString + def publishVersion: T[String] = "0.0.1-SNAPSHOT" + def pomSettings: T[PomSettings] + + // publish artifact with name "mill_2.12.4" instead of "mill_2.12" + def publishWithFullScalaVersion: Boolean = false + + def artifactScalaVersion: T[String] = T { + if (publishWithFullScalaVersion) scalaVersion() + else scalaBinaryVersion() + } + + def pom = T { + val dependencies = + ivyDeps().map(Artifact.fromDep(_, scalaVersion(), scalaBinaryVersion())) + val pom = Pom(artifact(), dependencies, publishName(), pomSettings()) + + val pomPath = T.ctx().dest / s"${publishName()}_${artifactScalaVersion()}-${publishVersion()}.pom" + write.over(pomPath, pom) + PathRef(pomPath) + } + + def ivy = T { + val dependencies = + ivyDeps().map(Artifact.fromDep(_, scalaVersion(), scalaBinaryVersion())) + val ivy = Ivy(artifact(), dependencies) + val ivyPath = T.ctx().dest / "ivy.xml" + write.over(ivyPath, ivy) + PathRef(ivyPath) + } + + def artifact: T[Artifact] = T { + Artifact(pomSettings().organization, s"${publishName()}_${artifactScalaVersion()}", publishVersion()) + } + + def publishLocal(): define.Command[Unit] = T.command { + LocalPublisher.publish( + jar = jar().path, + sourcesJar = sourcesJar().path, + docsJar = docsJar().path, + pom = pom().path, + ivy = ivy().path, + artifact = artifact() + ) + } + + def sonatypeUri: String = "https://oss.sonatype.org/service/local" + + def sonatypeSnapshotUri: String = "https://oss.sonatype.org/content/repositories/snapshots" + + def publish(credentials: String, gpgPassphrase: String): define.Command[Unit] = T.command { + val baseName = s"${publishName()}_${artifactScalaVersion()}-${publishVersion()}" + val artifacts = Seq( + jar().path -> s"${baseName}.jar", + sourcesJar().path -> s"${baseName}-sources.jar", + docsJar().path -> s"${baseName}-javadoc.jar", + pom().path -> s"${baseName}.pom" + ) + new SonatypePublisher( + sonatypeUri, + sonatypeSnapshotUri, + credentials, + gpgPassphrase, + T.ctx().log + ).publish(artifacts, artifact()) + } + +} + trait SbtScalaModule extends ScalaModule { outer => def basePath: Path override def sources = T.source{ basePath / 'src / 'main / 'scala } diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/Ivy.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/Ivy.scala new file mode 100644 index 00000000..5b2276e1 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/Ivy.scala @@ -0,0 +1,53 @@ +package mill.scalaplugin.publish + +import scala.xml.PrettyPrinter + +object Ivy { + + val head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + def apply( + artifact: Artifact, + dependencies: Seq[Dependency] + ): String = { + val xml = + <ivy-module version="2.0" xmlns:e="http://ant.apache.org/ivy/extra"> + <info + organisation={artifact.group} module={artifact.id} revision={artifact.version} status="release"> + <description/> + </info> + <configurations> + <conf name="pom" visibility="public" description=""/> + <conf extends="runtime" name="test" visibility="public" description=""/> + <conf name="provided" visibility="public" description=""/> + <conf name="optional" visibility="public" description=""/> + <conf name="compile" visibility="public" description=""/> + <conf extends="compile" name="runtime" visibility="public" description=""/> + </configurations> + + <publications> + <artifact name={artifact.id} type="pom" ext="pom" conf="pom"/> + <artifact name={artifact.id} type="jar" ext="jar" conf="compile"/> + <artifact name={artifact.id} type="src" ext="jar" conf="compile" e:classifier="sources"/> + <artifact name={artifact.id} type="doc" ext="jar" conf="compile" e:classifier="javadoc"/> + </publications> + <dependencies>{dependencies.map(renderDependency)}</dependencies> + </ivy-module> + + val pp = new PrettyPrinter(120, 4) + head + pp.format(xml).replaceAll(">", ">") + } + + private def renderDependency(dep: Dependency) = { + val scope = scopeToConf(dep.scope) + <dependency org={dep.artifact.group} name={dep.artifact.id} rev={dep.artifact.version} conf={s"$scope->default(compile)"}></dependency> + } + + private def scopeToConf(s: Scope): String = s match { + case Scope.Compile => "compile" + case Scope.Provided => "provided" + case Scope.Test => "test" + case Scope.Runtime => "runtime" + } + +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/JsonFormatters.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/JsonFormatters.scala new file mode 100644 index 00000000..e4aed0ea --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/JsonFormatters.scala @@ -0,0 +1,11 @@ +package mill.scalaplugin.publish + +import upickle.default.{ReadWriter => RW} + +trait JsonFormatters { + implicit lazy val artifactFormat: RW[Artifact] = upickle.default.macroRW + implicit lazy val developerFormat: RW[Developer] = upickle.default.macroRW + implicit lazy val licenseFormat: RW[License] = upickle.default.macroRW + implicit lazy val scmFormat: RW[SCM] = upickle.default.macroRW + implicit lazy val pomSettingsFormat: RW[PomSettings] = upickle.default.macroRW +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/LocalPublisher.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/LocalPublisher.scala new file mode 100644 index 00000000..acec6249 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/LocalPublisher.scala @@ -0,0 +1,33 @@ +package mill.scalaplugin.publish + +import ammonite.ops._ + +object LocalPublisher { + + private val root: Path = home / ".ivy2" / "local" + + def publish(jar: Path, + sourcesJar: Path, + docsJar: Path, + pom: Path, + ivy: Path, + artifact: Artifact): Unit = { + val releaseDir = root / artifact.group / artifact.id / artifact.version + writeFiles( + jar -> releaseDir / "jars" / s"${artifact.id}.jar", + sourcesJar -> releaseDir / "srcs" / s"${artifact.id}-sources.jar", + docsJar -> releaseDir / "docs" / s"${artifact.id}-javadoc.jar", + pom -> releaseDir / "poms" / s"${artifact.id}.pom", + ivy -> releaseDir / "ivys" / "ivy.xml" + ) + } + + private def writeFiles(fromTo: (Path, Path)*): Unit = { + fromTo.foreach { + case (from, to) => + mkdir(to / up) + cp.over(from, to) + } + } + +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/Pom.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/Pom.scala new file mode 100644 index 00000000..fab6c624 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/Pom.scala @@ -0,0 +1,88 @@ +package mill.scalaplugin.publish + +import scala.xml.{Elem, NodeSeq, PrettyPrinter} + +object Pom { + + val head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + //TODO - not only jar packaging support? + def apply(artifact: Artifact, + dependencies: Seq[Dependency], + name: String, + pomSettings: PomSettings): String = { + val xml = + <project + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" + xmlns ="http://maven.apache.org/POM/4.0.0"> + + <modelVersion>4.0.0</modelVersion> + <name>{name}</name> + <groupId>{artifact.group}</groupId> + <artifactId>{artifact.id}</artifactId> + <packaging>jar</packaging> + <description>{pomSettings.description}</description> + + <version>{artifact.version}</version> + <url>{pomSettings.url}</url> + <licenses> + {pomSettings.licenses.map(renderLicense)} + </licenses> + <scm> + <url>{pomSettings.scm.url}</url> + <connection>{pomSettings.scm.connection}</connection> + </scm> + <developers> + {pomSettings.developers.map(renderDeveloper)} + </developers> + <dependencies> + {dependencies.map(renderDependency)} + </dependencies> + </project> + + val pp = new PrettyPrinter(120, 4) + head + pp.format(xml) + } + + private def renderLicense(l: License): Elem = { + <license> + <name>{l.name}</name> + <url>{l.url}</url> + <distribution>{l.distribution}</distribution> + </license> + } + + private def renderDeveloper(d: Developer): Elem = { + <developer> + <id>{d.id}</id> + <name>{d.name}</name> + { + d.organization.map { org => + <organization>{org}</organization> + }.getOrElse(NodeSeq.Empty) + } + { + d.organizationUrl.map { orgUrl => + <organizationUrl>{orgUrl}</organizationUrl> + }.getOrElse(NodeSeq.Empty) + } + </developer> + } + + private def renderDependency(d: Dependency): Elem = { + val scope = d.scope match { + case Scope.Compile => NodeSeq.Empty + case Scope.Provided => <scope>provided</scope> + case Scope.Test => <scope>test</scope> + case Scope.Runtime => <scope>runtime</scope> + } + <dependency> + <groupId>{d.artifact.group}</groupId> + <artifactId>{d.artifact.id}</artifactId> + <version>{d.artifact.version}</version> + {scope} + </dependency> + } + +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypeHttpApi.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypeHttpApi.scala new file mode 100644 index 00000000..abf65cf6 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypeHttpApi.scala @@ -0,0 +1,130 @@ +package mill.scalaplugin.publish + +import java.util.Base64 + +import upickle.json + +import scala.concurrent.duration._ +import scalaj.http.{BaseHttp, HttpOptions, HttpRequest, HttpResponse} + +object PatientHttp + extends BaseHttp( + options = Seq( + HttpOptions.connTimeout(5.seconds.toMillis.toInt), + HttpOptions.readTimeout(1.minute.toMillis.toInt), + HttpOptions.followRedirects(false) + ) + ) + +class SonatypeHttpApi(uri: String, credentials: String) { + + private val base64Creds = base64(credentials) + + private val commonHeaders = Seq( + "Authorization" -> s"Basic ${base64Creds}", + "Accept" -> "application/json", + "Content-Type" -> "application/json" + ) + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles.html + def getStagingProfileUri(groupId: String): String = { + val response = withRetry( + PatientHttp(s"${uri}/staging/profiles").headers(commonHeaders)) + + val resourceUri = + json + .read(response.body)("data") + .arr + .find(profile => profile("name").str == groupId) + .map(_("resourceURI").str.toString) + + resourceUri.getOrElse( + throw new RuntimeException( + s"Could not find staging profile for groupId: ${groupId}") + ) + } + + def getStagingRepoState(stagingRepoId: String): String = { + val response = PatientHttp(s"${uri}/staging/repository/${stagingRepoId}") + .option(HttpOptions.readTimeout(60000)) + .headers(commonHeaders) + .asString + + json.read(response.body)("type").str.toString + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_start.html + def createStagingRepo(profileUri: String, groupId: String): String = { + val response = withRetry(PatientHttp(s"${profileUri}/start") + .headers(commonHeaders) + .postData( + s"""{"data": {"description": "fresh staging profile for ${groupId}"}}""")) + + json.read(response.body)("data")("stagedRepositoryId").str.toString + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_finish.html + def closeStagingRepo(profileUri: String, repositoryId: String): Boolean = { + val response = withRetry( + PatientHttp(s"${profileUri}/finish") + .headers(commonHeaders) + .postData( + s"""{"data": {"stagedRepositoryId": "${repositoryId}", "description": "closing staging repository"}}""" + )) + + response.code == 201 + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_promote.html + def promoteStagingRepo(profileUri: String, repositoryId: String): Boolean = { + val response = withRetry( + PatientHttp(s"${profileUri}/promote") + .headers(commonHeaders) + .postData( + s"""{"data": {"stagedRepositoryId": "${repositoryId}", "description": "promote staging repository"}}""" + )) + + response.code == 201 + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_drop.html + def dropStagingRepo(profileUri: String, repositoryId: String): Boolean = { + val response = withRetry( + PatientHttp(s"${profileUri}/drop") + .headers(commonHeaders) + .postData( + s"""{"data": {"stagedRepositoryId": "${repositoryId}", "description": "drop staging repository"}}""" + )) + + response.code == 201 + } + + private val uploadTimeout = 5.minutes.toMillis.toInt + + def upload(uri: String, data: Array[Byte]): HttpResponse[String] = { + PatientHttp(uri) + .option(HttpOptions.readTimeout(uploadTimeout)) + .method("PUT") + .headers( + "Content-Type" -> "application/binary", + "Authorization" -> s"Basic ${base64Creds}" + ) + .put(data) + .asString + } + + private def withRetry(request: HttpRequest, + retries: Int = 10): HttpResponse[String] = { + val resp = request.asString + if (resp.is5xx && retries > 0) { + Thread.sleep(500) + withRetry(request, retries - 1) + } else { + resp + } + } + + private def base64(s: String) = + new String(Base64.getEncoder.encode(s.getBytes)) + +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala new file mode 100644 index 00000000..90a39745 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala @@ -0,0 +1,148 @@ +package mill.scalaplugin.publish + +import java.math.BigInteger +import java.security.MessageDigest + +import ammonite.ops._ +import mill.util.Logger + +import scalaj.http.HttpResponse + +class SonatypePublisher(uri: String, + snapshotUri: String, + credentials: String, + gpgPassphrase: String, + log: Logger) { + + private val api = new SonatypeHttpApi(uri, credentials) + + def publish(artifacts: Seq[(Path, String)], artifact: Artifact): Unit = { + val signedArtifacts = artifacts ++ artifacts.map { + case (file, name) => + poorMansSign(file, gpgPassphrase) -> s"${name}.asc" + } + + val signedArtifactsWithDigest = signedArtifacts.flatMap { + case (file, name) => + val content = read.bytes(file) + + Seq( + name -> content, + (name + ".md5") -> md5hex(content), + (name + ".sha1") -> sha1hex(content) + ) + } + + val publishPath = Seq( + artifact.group.replace(".", "/"), + artifact.id, + artifact.version + ).mkString("/") + + if (artifact.isSnapshot) + publishSnapshot(publishPath, signedArtifactsWithDigest, artifact) + else + publishRelease(publishPath, signedArtifactsWithDigest, artifact) + } + + private def publishSnapshot(publishPath: String, + payloads: Seq[(String, Array[Byte])], + artifact: Artifact): Unit = { + val baseUri: String = snapshotUri + "/" + publishPath + + val publishResults = payloads.map { + case (fileName, data) => + log.info(s"Uploading ${fileName}") + val resp = api.upload(s"${baseUri}/${fileName}", data) + resp + } + reportPublishResults(publishResults, artifact) + } + + private def publishRelease(publishPath: String, + payloads: Seq[(String, Array[Byte])], + artifact: Artifact): Unit = { + val profileUri = api.getStagingProfileUri(artifact.group) + val stagingRepoId = + api.createStagingRepo(profileUri, artifact.group) + val baseUri = + s"${uri}/staging/deployByRepositoryId/${stagingRepoId}/${publishPath}" + + val publishResults = payloads.map { + case (fileName, data) => + log.info(s"Uploading ${fileName}") + api.upload(s"${baseUri}/${fileName}", data) + } + reportPublishResults(publishResults, artifact) + + log.info("Closing staging repository") + api.closeStagingRepo(profileUri, stagingRepoId) + + log.info("Waiting for staging repository to close") + awaitRepoStatus("closed", stagingRepoId) + + log.info("Promoting staging repository") + api.promoteStagingRepo(profileUri, stagingRepoId) + + log.info("Waiting for staging repository to release") + awaitRepoStatus("released", stagingRepoId) + + log.info("Dropping staging repository") + api.dropStagingRepo(profileUri, stagingRepoId) + + log.info(s"Published ${artifact.id} successfully") + } + + private def reportPublishResults(publishResults: Seq[HttpResponse[String]], + artifact: Artifact) = { + if (publishResults.forall(_.is2xx)) { + log.info(s"Published ${artifact.id} to Sonatype") + } else { + val errors = publishResults.filterNot(_.is2xx).map { response => + s"Code: ${response.code}, message: ${response.body}" + } + throw new RuntimeException( + s"Failed to publish ${artifact.id} to Sonatype. Errors: \n${errors.mkString("\n")}" + ) + } + } + + private def awaitRepoStatus(status: String, + stagingRepoId: String, + attempts: Int = 20): Unit = { + def isRightStatus = + api.getStagingRepoState(stagingRepoId).equalsIgnoreCase(status) + var attemptsLeft = attempts + + while (attemptsLeft > 0 && !isRightStatus) { + Thread.sleep(3000) + attemptsLeft -= 1 + if (attemptsLeft == 0) { + throw new RuntimeException( + s"Couldn't wait for staging repository to be ${status}. Failing") + } + } + } + + // http://central.sonatype.org/pages/working-with-pgp-signatures.html#signing-a-file + private def poorMansSign(file: Path, passphrase: String): Path = { + val fileName = file.toString + import ammonite.ops.ImplicitWd._ + %("gpg", "--yes", "-a", "-b", "--passphrase", passphrase, fileName) + Path(fileName + ".asc") + } + + private def md5hex(bytes: Array[Byte]): Array[Byte] = + hexArray(md5.digest(bytes)).getBytes + + private def sha1hex(bytes: Array[Byte]): Array[Byte] = + hexArray(sha1.digest(bytes)).getBytes + + private def md5 = MessageDigest.getInstance("md5") + + private def sha1 = MessageDigest.getInstance("sha1") + + private def hexArray(arr: Array[Byte]) = + String.format("%0" + (arr.length << 1) + "x", new BigInteger(1, arr)) + +} diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/package.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/package.scala new file mode 100644 index 00000000..1b405b90 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/package.scala @@ -0,0 +1,3 @@ +package mill.scalaplugin + +package object publish extends JsonFormatters diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/settings.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/settings.scala new file mode 100644 index 00000000..e13825ab --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/settings.scala @@ -0,0 +1,70 @@ +package mill.scalaplugin.publish + +import mill.scalaplugin.Dep + +case class Artifact(group: String, id: String, version: String) { + def isSnapshot: Boolean = version.endsWith("-SNAPSHOT") +} + +object Artifact { + + def fromDep(dep: Dep, scalaFull: String, scalaBin: String): Dependency = { + dep match { + case Dep.Java(dep) => + Dependency( + Artifact(dep.module.organization, dep.module.name, dep.version), + Scope.Compile) + case Dep.Scala(dep) => + Dependency(Artifact(dep.module.organization, + s"${dep.module.name}_${scalaBin}", + dep.version), + Scope.Compile) + case Dep.Point(dep) => + Dependency(Artifact(dep.module.organization, + s"${dep.module.name}_${scalaFull}", + dep.version), + Scope.Compile) + } + } +} + +sealed trait Scope +object Scope { + case object Compile extends Scope + case object Provided extends Scope + case object Runtime extends Scope + case object Test extends Scope +} + +case class Dependency( + artifact: Artifact, + scope: Scope +) + +case class License( + name: String, + url: String, + distribution: String = "repo" +) + +case class SCM( + url: String, + connection: String +) + +case class Developer( + id: String, + name: String, + url: String, + organization: Option[String] = None, + organizationUrl: Option[String] = None +) + +case class PomSettings( + description: String, + organization: String, + url: String, + licenses: Seq[License], + scm: SCM, + developers: Seq[Developer] +) diff --git a/scalaplugin/src/test/resource/resolve-deps/src/main/scala/Main.scala b/scalaplugin/src/test/resource/resolve-deps/src/main/scala/Main.scala new file mode 100644 index 00000000..5dcbe39a --- /dev/null +++ b/scalaplugin/src/test/resource/resolve-deps/src/main/scala/Main.scala @@ -0,0 +1,3 @@ +object Main { + println("ResolveDeps Main: hello world!") +} diff --git a/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala b/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala index deaa766e..42d93c2a 100644 --- a/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala +++ b/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala @@ -2,20 +2,36 @@ package mill.scalaplugin import ammonite.ops.ImplicitWd._ import ammonite.ops._ -import mill.define.{Cross,Task} +import mill.define.{Cross, Task} import mill.discover.Discovered import mill.eval.Result +import mill.scalaplugin.publish._ import utest._ import mill.util.JsonFormatters._ object AcyclicBuild{ val acyclic = for(crossVersion <- Cross("2.10.6", "2.11.8", "2.12.3", "2.12.4")) - yield new SbtScalaModule{outer => + yield new SbtScalaModule with PublishModule {outer => def basePath = AcyclicTests.workspacePath - def organization = "com.lihaoyi" - def name = "acyclic" + def publishName = "acyclic" + def publishVersion = "0.1.7" + + def pomSettings = PomSettings( + description = publishName(), + organization = "com.lihaoyi", + url = "https://github.com/lihaoyi/acyclic", + licenses = Seq( + License("MIT license", "http://www.opensource.org/licenses/mit-license.php") + ), + scm = SCM( + "git://github.com/lihaoyi/acyclic.git", + "scm:git://github.com/lihaoyi/acyclic.git" + ), + developers = Seq( + Developer("lihaoyi", "Li Haoyi","https://github.com/lihaoyi") + ) + ) - def version = "0.1.7" def scalaVersion = crossVersion def ivyDeps = Seq( Dep.Java("org.scala-lang", "scala-compiler", scalaVersion()) diff --git a/scalaplugin/src/test/scala/mill/scalaplugin/HelloWorldTests.scala b/scalaplugin/src/test/scala/mill/scalaplugin/HelloWorldTests.scala index a5e52330..41783efb 100644 --- a/scalaplugin/src/test/scala/mill/scalaplugin/HelloWorldTests.scala +++ b/scalaplugin/src/test/scala/mill/scalaplugin/HelloWorldTests.scala @@ -7,6 +7,7 @@ import mill.define.{Target, Task} import mill.discover.Discovered import mill.discover.Mirror.LabelledTarget import mill.eval.Result +import mill.scalaplugin.publish._ import sbt.internal.inc.CompileFailed import utest._ @@ -29,6 +30,26 @@ object HelloWorldFatalWarnings extends HelloWorldModule { def scalacOptions = T(Seq("-Ywarn-unused", "-Xfatal-warnings")) } +object HelloWorldWithPublish extends HelloWorldModule with PublishModule { + def publishName = "hello-world" + def publishVersion = "0.0.1" + + def pomSettings = PomSettings( + organization = "com.lihaoyi", + description = "hello world ready for real world publishing", + url = "https://github.com/lihaoyi/hello-world-publish", + licenses = Seq( + License("Apache License, Version 2.0", + "http://www.apache.org/licenses/LICENSE-2.0")), + scm = SCM( + "https://github.com/lihaoyi/hello-world-publish", + "scm:git:https://github.com/lihaoyi/hello-world-publish" + ), + developers = + Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")) + ) +} + object HelloWorldTests extends TestSuite { val srcPath = pwd / 'scalaplugin / 'src / 'test / 'resource / "hello-world" @@ -98,7 +119,8 @@ object HelloWorldTests extends TestSuite { val outPath = result.classes.path val analysisFile = result.analysisFile val outputFiles = ls.rec(outPath) - val expectedClassfiles = compileClassfiles(outputPath / 'compile / 'classes) + val expectedClassfiles = + compileClassfiles(outputPath / 'compile / 'classes) assert( outPath == outputPath / 'compile / 'classes, exists(analysisFile), diff --git a/scalaplugin/src/test/scala/mill/scalaplugin/ResolveDepsTests.scala b/scalaplugin/src/test/scala/mill/scalaplugin/ResolveDepsTests.scala new file mode 100644 index 00000000..cd6bd40f --- /dev/null +++ b/scalaplugin/src/test/scala/mill/scalaplugin/ResolveDepsTests.scala @@ -0,0 +1,39 @@ +package mill.scalaplugin + +import coursier.Cache +import coursier.maven.MavenRepository +import mill.eval.Result.{Failure, Success} +import mill.eval.{PathRef, Result} +import utest._ + +object ResolveDepsTests extends TestSuite { + val repos = Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")) + + def evalDeps(deps: Seq[Dep]): Result[Seq[PathRef]] = Lib.resolveDependencies(repos, "2.12.4", "2.12", deps) + + val tests = Tests { + 'resolveValidDeps - { + val deps = Seq(Dep("com.lihaoyi", "pprint", "0.5.3")) + val Success(paths) = evalDeps(deps) + assert(paths.nonEmpty) + } + + 'errOnInvalidOrgDeps - { + val deps = Seq(Dep("xxx.yyy.invalid", "pprint", "0.5.3")) + val Failure(errMsg) = evalDeps(deps) + assert(errMsg.contains("xxx.yyy.invalid")) + } + + 'errOnInvalidVersionDeps - { + val deps = Seq(Dep("com.lihaoyi", "pprint", "invalid.version.num")) + val Failure(errMsg) = evalDeps(deps) + assert(errMsg.contains("invalid.version.num")) + } + + 'errOnPartialSuccess - { + val deps = Seq(Dep("com.lihaoyi", "pprint", "0.5.3"), Dep("fake", "fake", "fake")) + val Failure(errMsg) = evalDeps(deps) + assert(errMsg.contains("fake")) + } + } +} |