From d34eeb3be992bf82fe3d3d2d88b7dcaaad9c2e88 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Thu, 16 Jun 2022 18:05:23 +0200 Subject: [PATCH] Remove publish module and command (#2446) The publish module has been migrated to https://github.com/coursier/publish some time ago (and doesn't use okhttp anymore there). For the publish command, there's no 100% equivalent substitute. Scala CLI (https://github.com/VirtusLab/scala-cli) got a publish command recently, offering similar features. --- build.sc | 29 +- .../main/scala/coursier/cli/Coursier.scala | 1 - .../scala/coursier/cli/publish/Hooks.scala | 137 --- .../scala/coursier/cli/publish/Input.scala | 144 --- .../scala/coursier/cli/publish/Manual.scala | 116 --- .../scala/coursier/cli/publish/Publish.scala | 307 ------ .../coursier/cli/publish/PublishError.scala | 37 - .../cli/publish/PublishRepository.scala | 141 --- .../coursier/cli/publish/PublishTasks.scala | 121 --- .../coursier/cli/publish/conf/Conf.scala | 109 --- .../coursier/cli/publish/conf/Developer.scala | 25 - .../publish/conf/OrganizationDetails.scala | 39 - .../cli/publish/options/ChecksumOptions.scala | 9 - .../publish/options/DirectoryOptions.scala | 9 - .../cli/publish/options/MetadataOptions.scala | 37 - .../cli/publish/options/PublishOptions.scala | 56 -- .../publish/options/RepositoryOptions.scala | 41 - .../publish/options/SignatureOptions.scala | 8 - .../options/SinglePackageOptions.scala | 26 - .../cli/publish/options/package.scala | 8 - .../cli/publish/params/ChecksumParams.scala | 38 - .../cli/publish/params/DirectoryParams.scala | 66 -- .../cli/publish/params/MetadataParams.scala | 104 --- .../cli/publish/params/PublishParams.scala | 288 ------ .../cli/publish/params/RepositoryParams.scala | 228 ----- .../cli/publish/params/SignatureParams.scala | 21 - .../publish/params/SinglePackageParams.scala | 106 --- .../cli/publish/params/SonatypeParams.scala | 5 - .../coursier/cli/publish/params/package.scala | 8 - .../cli/publish/sonatype/Sonatype.scala | 288 ------ .../publish/sonatype/SonatypeOptions.scala | 29 - .../cli/publish/sonatype/SonatypeParams.scala | 101 -- .../cli/publish/util/DeleteOnExit.scala | 57 -- .../scala/coursier/cli/publish/util/Git.scala | 29 - .../cli/publish/version/Options.scala | 17 - .../cli/publish/version/Version.scala | 99 -- .../main/scala/coursier/publish/Content.scala | 41 - .../coursier/publish/MavenMetadata.scala | 319 ------- .../src/main/scala/coursier/publish/Pom.scala | 404 -------- .../coursier/publish/bintray/BintrayApi.scala | 120 --- .../coursier/publish/checksum/Checksum.scala | 26 - .../publish/checksum/ChecksumType.scala | 36 - .../coursier/publish/checksum/Checksums.scala | 124 --- .../checksum/logger/BatchChecksumLogger.scala | 20 - .../checksum/logger/ChecksumLogger.scala | 15 - .../logger/InteractiveChecksumLogger.scala | 46 - .../coursier/publish/checksum/package.scala | 5 - .../main/scala/coursier/publish/dir/Dir.scala | 133 --- .../coursier/publish/dir/DirContent.scala | 47 - .../publish/dir/logger/BatchDirLogger.scala | 17 - .../publish/dir/logger/DirLogger.scala | 13 - .../dir/logger/InteractiveDirLogger.scala | 36 - .../coursier/publish/download/Download.scala | 40 - .../publish/download/FileDownload.scala | 49 - .../publish/download/OkhttpDownload.scala | 99 -- .../download/logger/DownloadLogger.scala | 11 - .../logger/SimpleDownloadLogger.scala | 31 - .../coursier/publish/fileset/FileSet.scala | 203 ---- .../coursier/publish/fileset/Group.scala | 875 ------------------ .../scala/coursier/publish/fileset/Path.scala | 12 - .../publish/logging/OutputFrame.scala | 223 ----- .../publish/logging/ProgressLogger.scala | 169 ---- .../main/scala/coursier/publish/sbt/Sbt.scala | 124 --- .../coursier/publish/signing/GpgSigner.scala | 115 --- .../coursier/publish/signing/NopSigner.scala | 22 - .../coursier/publish/signing/Signer.scala | 133 --- .../signing/logger/BatchSignerLogger.scala | 15 - .../logger/InteractiveSignerLogger.scala | 44 - .../publish/signing/logger/SignerLogger.scala | 20 - .../coursier/publish/signing/package.scala | 5 - .../publish/sonatype/OkHttpClientUtil.scala | 127 --- .../publish/sonatype/SonatypeApi.scala | 321 ------- .../sonatype/logger/BatchSonatypeLogger.scala | 27 - .../logger/InteractiveSonatypeLogger.scala | 44 - .../sonatype/logger/SonatypeLogger.scala | 11 - .../coursier/publish/upload/DummyUpload.scala | 16 - .../coursier/publish/upload/FileUpload.scala | 47 - .../upload/HttpURLConnectionUpload.scala | 143 --- .../publish/upload/OkhttpUpload.scala | 122 --- .../coursier/publish/upload/Upload.scala | 133 --- .../upload/logger/BatchUploadLogger.scala | 34 - .../logger/InteractiveUploadLogger.scala | 53 -- .../publish/upload/logger/UploadLogger.scala | 15 - project/deps.sc | 1 - 84 files changed, 1 insertion(+), 7569 deletions(-) delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/Hooks.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/Input.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/Manual.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/Publish.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/PublishError.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/PublishRepository.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/PublishTasks.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/conf/Conf.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/conf/Developer.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/conf/OrganizationDetails.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/ChecksumOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/DirectoryOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/MetadataOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/PublishOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/RepositoryOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/SignatureOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/SinglePackageOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/options/package.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/ChecksumParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/DirectoryParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/MetadataParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/PublishParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/RepositoryParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/SignatureParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/SinglePackageParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/SonatypeParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/params/package.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/sonatype/Sonatype.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeOptions.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeParams.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/util/DeleteOnExit.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/util/Git.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/version/Options.scala delete mode 100644 modules/cli/src/main/scala/coursier/cli/publish/version/Version.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/Content.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/MavenMetadata.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/Pom.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/bintray/BintrayApi.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/Checksum.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/ChecksumType.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/Checksums.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/logger/BatchChecksumLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/logger/ChecksumLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/logger/InteractiveChecksumLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/checksum/package.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/dir/Dir.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/dir/DirContent.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/dir/logger/BatchDirLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/dir/logger/DirLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/dir/logger/InteractiveDirLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/download/Download.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/download/FileDownload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/download/OkhttpDownload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/download/logger/DownloadLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/download/logger/SimpleDownloadLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/fileset/FileSet.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/fileset/Group.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/fileset/Path.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/logging/OutputFrame.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/logging/ProgressLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/sbt/Sbt.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/GpgSigner.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/NopSigner.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/Signer.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/logger/BatchSignerLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/logger/InteractiveSignerLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/logger/SignerLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/signing/package.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/sonatype/OkHttpClientUtil.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/sonatype/SonatypeApi.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/sonatype/logger/BatchSonatypeLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/sonatype/logger/InteractiveSonatypeLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/sonatype/logger/SonatypeLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/DummyUpload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/FileUpload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/HttpURLConnectionUpload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/OkhttpUpload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/Upload.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/logger/BatchUploadLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/logger/InteractiveUploadLogger.scala delete mode 100644 modules/publish/src/main/scala/coursier/publish/upload/logger/UploadLogger.scala diff --git a/build.sc b/build.sc index f3cd99262e..5be1ffaada 100644 --- a/build.sc +++ b/build.sc @@ -54,7 +54,6 @@ object cache extends Module { object js extends Cross[CacheJs](ScalaVersions.all: _*) } object launcher extends Cross[Launcher](ScalaVersions.all ++ Seq(ScalaVersions.scala211): _*) -object publish extends Cross[Publish](ScalaVersions.all: _*) object env extends Cross[Env](ScalaVersions.all: _*) object `launcher-native_03` extends LauncherNative03 object `launcher-native_040M2` extends LauncherNative040M2 @@ -173,8 +172,6 @@ object `cli-tests` extends CliTests object web extends Web -def publish0 = publish - class UtilJvm(val crossScalaVersion: String) extends UtilJvmBase { def ivyDeps = super.ivyDeps() ++ Agg( Deps.jsoup @@ -268,29 +265,6 @@ class Launcher(val crossScalaVersion: String) extends LauncherBase { def noProguardResourceBootstrap = `bootstrap-launcher`.resourceAssembly() } -class Publish(val crossScalaVersion: String) extends CrossSbtModule with CsModule - with CoursierPublishModule with CsMima { - def artifactName = "coursier-publish" - def moduleDeps = Seq( - core.jvm(), - cache.jvm() - ) - def ivyDeps = super.ivyDeps() ++ Agg( - Deps.argonautShapeless, - Deps.catsCore, - Deps.collectionCompat, - Deps.okhttp - ) - def mimaPreviousVersions = T { - val previous = super.mimaPreviousVersions() - if (crossScalaVersion.startsWith("2.13.")) - // this module wasn't published in 2.13 when coursier was built with sbt - previous.filter(_ != "2.0.16") - else - previous - } -} - class Env(val crossScalaVersion: String) extends CrossSbtModule with CsModule with CoursierPublishModule with CsMima { def mimaPreviousVersions = T { @@ -542,8 +516,7 @@ trait Cli extends CsModule with CoursierPublishModule with Launchers { install(cliScalaVersion), jvm(cliScalaVersion), launcherModule(cliScalaVersion), - `proxy-setup`, - publish0(cliScalaVersion) + `proxy-setup` ) def ivyDeps = super.ivyDeps() ++ Agg( Deps.argonautShapeless, diff --git a/modules/cli/src/main/scala/coursier/cli/Coursier.scala b/modules/cli/src/main/scala/coursier/cli/Coursier.scala index 844bd1b8b2..48eaa9059d 100644 --- a/modules/cli/src/main/scala/coursier/cli/Coursier.scala +++ b/modules/cli/src/main/scala/coursier/cli/Coursier.scala @@ -39,7 +39,6 @@ object Coursier extends CommandsEntryPoint { jvm.JavaHome, launch.Launch, install.List, - publish.Publish, resolve.Resolve, search.Search, setup.Setup, diff --git a/modules/cli/src/main/scala/coursier/cli/publish/Hooks.scala b/modules/cli/src/main/scala/coursier/cli/publish/Hooks.scala deleted file mode 100644 index 0aa060768f..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/Hooks.scala +++ /dev/null @@ -1,137 +0,0 @@ -package coursier.cli.publish - -import java.io.PrintStream -import java.util.concurrent.ScheduledExecutorService - -import coursier.maven.MavenRepository -import coursier.publish.bintray.BintrayApi -import coursier.publish.fileset.FileSet -import coursier.publish.sonatype.SonatypeApi -import coursier.publish.sonatype.logger.{BatchSonatypeLogger, InteractiveSonatypeLogger} -import coursier.util.Task - -import scala.concurrent.duration.DurationInt - -trait Hooks { - - type T >: Null - - def beforeUpload(fileSet: FileSet, isSnapshot: Boolean): Task[T] = - Task.point(null) - def repository(t: T, repo: PublishRepository, isSnapshot: Boolean): Option[MavenRepository] = - None - def afterUpload(t: T): Task[Unit] = - Task.point(()) - -} - -object Hooks { - - private final class Sonatype( - repo: PublishRepository.Sonatype, - api: SonatypeApi, - out: PrintStream, - verbosity: Int, - batch: Boolean, - es: ScheduledExecutorService - ) extends Hooks { - - val logger = - if (batch) - new BatchSonatypeLogger(out, verbosity) - else - InteractiveSonatypeLogger.create(out, verbosity) - - type T = Option[(SonatypeApi.Profile, String)] - - override def beforeUpload(fileSet0: FileSet, isSnapshot: Boolean): Task[T] = - if (isSnapshot) - Task.point(None) - else - for { - - p <- PublishTasks.sonatypeProfile(fileSet0, api, logger) - - _ = { - if (verbosity >= 2) - out.println(s"Selected Sonatype profile ${p.name} (id: ${p.id}, uri: ${p.uri})") - else if (verbosity >= 1) - out.println(s"Selected Sonatype profile ${p.name} (id: ${p.id})") - else if (verbosity >= 0) - out.println(s"Selected Sonatype profile ${p.name}") - } - - id <- api.createStagingRepository(p, "create staging repository") - - } yield Some((p, id)) - - override def repository( - t: T, - repo0: PublishRepository, - isSnapshot: Boolean - ): Option[MavenRepository] = - t.map { - case (_, repoId) => - repo.releaseRepoOf(repoId) - } - - override def afterUpload(profileRepoIdOpt: T): Task[Unit] = - profileRepoIdOpt match { - case None => Task.point(()) - case Some((profile, repoId)) => - // TODO Print sensible error messages if anything goes wrong here (commands to finish promoting, etc.) - for { - _ <- api.sendCloseStagingRepositoryRequest(profile, repoId, "closing repository") - _ <- api.waitForStatus(profile.id, repoId, "closed", 20, 3.seconds, 1.5, es) - _ <- api.sendPromoteStagingRepositoryRequest(profile, repoId, "promoting repository") - _ <- api.waitForStatus(profile.id, repoId, "released", 20, 3.seconds, 1.5, es) - _ <- api.sendDropStagingRepositoryRequest(profile, repoId, "dropping repository") - } yield () - } - } - - private final class Bintray( - api: BintrayApi, - subject: String, - repo: String, - package0: String, - licenses: Seq[String], - vcsUrl: String - ) extends Hooks { - - type T = Object - - override def beforeUpload(fileSet: FileSet, isSnapshot: Boolean): Task[Object] = - for { - _ <- api.createRepositoryIfNeeded(subject, repo) - _ <- api.createPackageIfNeeded(subject, repo, package0, licenses, vcsUrl) - } yield null - - } - - def dummy: Hooks = - new Hooks { - type T = Object - } - - def sonatype( - repo: PublishRepository.Sonatype, - api: SonatypeApi, - out: PrintStream, - verbosity: Int, - batch: Boolean, - es: ScheduledExecutorService - ): Hooks = - new Sonatype(repo, api, out, verbosity, batch, es) - - def bintray( - api: BintrayApi, - subject: String, - repo: String, - package0: String, - licenses: Seq[String], - vcsUrl: String - ): Hooks = - new Bintray(api, subject, repo, package0, licenses, vcsUrl) - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/Input.scala b/modules/cli/src/main/scala/coursier/cli/publish/Input.scala deleted file mode 100644 index 1b07e62c55..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/Input.scala +++ /dev/null @@ -1,144 +0,0 @@ -package coursier.cli.publish - -import java.io.{File, PrintStream} -import java.nio.file.{Files, Paths} -import java.time.Instant -import java.util.concurrent.ExecutorService - -import coursier.{Repositories, dependencyString} -import coursier.cache.{Cache, CacheLogger} -import coursier.cli.publish.params.PublishParams -import coursier.cli.publish.util.DeleteOnExit -import coursier.publish.dir.Dir -import coursier.publish.dir.logger.{BatchDirLogger, InteractiveDirLogger} -import coursier.publish.fileset.FileSet -import coursier.publish.sbt.Sbt -import coursier.util.Task - -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, ExecutionContext} - -object Input { - - def manualPackageFileSetOpt(params: PublishParams, now: Instant): Task[Option[FileSet]] = - if (params.singlePackage.`package`) - Task.fromEither( - Manual.manualPackageFileSet(params.singlePackage, params.metadata, now) - .map(Some(_)) - ) - else - Task.point(None) - - def dirFileSet( - params: PublishParams, - out: PrintStream - ): Task[FileSet] = - params - .directory - .directories - .map { d => - val logger = - if (params.batch) - new BatchDirLogger(out, params.dirName(d), params.verbosity) - else - InteractiveDirLogger.create(out, params.dirName(d), params.verbosity) - Dir.read(d, logger) - } - // the logger will have to be shared if this is to be parallelized - .foldLeft(Task.point(FileSet.empty)) { (acc, t) => - for { - a <- acc - extra <- t - } yield a ++ extra - } - - def sbtCsPublishJarTask(cache: Cache[Task]): Task[File] = - Task.delay { - val files = coursier.Fetch(cache) - .addRepositories(Repositories.sbtPlugin("releases")) - .addDependencies( - dep"io.get-coursier:sbt-cs-publish;scalaVersion=2.12;sbtVersion=1.0:0.1.1" - ) - .run() - - files match { - case Seq() => ??? - case Seq(jar) => jar - case other => ??? - } - } - - def sbtFileSet( - params: PublishParams, - now: Instant, - out: PrintStream, - deleteOnExit: DeleteOnExit, - maybeReadCurrentDir: Boolean, - pool: ExecutorService - ): Task[FileSet] = { - - val actualSbtDirectoriesTask = { - val checkSbtDirs = maybeReadCurrentDir && - params.directory.directories.isEmpty && - params.directory.sbtDirectories.isEmpty - if (checkSbtDirs) { - val cwd = Paths.get(".") - Task.delay(Sbt.isSbtProject(cwd)).map { - case true => - Seq(cwd) - case false => - Nil - } - } - else - Task.point(params.directory.sbtDirectories) - } - - actualSbtDirectoriesTask.flatMap { actualSbtDirectories => - actualSbtDirectories - .map { sbtDir => - for { - sbtStructureJar <- sbtCsPublishJarTask(params.cache.cache(pool, CacheLogger.nop)) - t <- Task.delay { - val sbt = new Sbt( - sbtDir.toFile, - sbtStructureJar, - ExecutionContext.global, - params.sbtOutputFrame, - params.verbosity, - interactive = !params.batch - ) - val tmpDir = Files.createTempDirectory("coursier-publish-sbt-") - deleteOnExit(tmpDir) - val f = sbt.publishTo(tmpDir.toFile) - // meh, blocking from a task… - Await.result(f, Duration.Inf) - val dirLogger = - if (params.batch) - new BatchDirLogger( - out, - params.dirName(tmpDir, Some("temporary directory")), - params.verbosity - ) - else - InteractiveDirLogger.create( - out, - params.dirName(tmpDir, Some("temporary directory")), - params.verbosity - ) - Dir.read(tmpDir, dirLogger) - } - fs <- t - } yield fs - } - // DirLogger will have to be shared to parallelize this - .foldLeft(Task.point(FileSet.empty)) { (acc, t) => - for { - a <- acc - extra <- t - } yield a ++ extra - } - } - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/Manual.scala b/modules/cli/src/main/scala/coursier/cli/publish/Manual.scala deleted file mode 100644 index 1907f56f1a..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/Manual.scala +++ /dev/null @@ -1,116 +0,0 @@ -package coursier.cli.publish - -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.time.Instant - -import coursier.publish.fileset.{FileSet, Path} -import coursier.cli.publish.params.{MetadataParams, SinglePackageParams} -import coursier.core.{ModuleName, Organization} -import coursier.publish.{Content, Pom} - -object Manual { - - private def pomModuleVersion( - params: SinglePackageParams, - metadata: MetadataParams, - now: Instant - ): Either[PublishError.InvalidArguments, (Content, Organization, ModuleName, String)] = - (metadata.organization, metadata.name, metadata.version) match { - case (Some(org), Some(name), Some(ver)) => - val content = params.pomOpt match { - case Some(path) => - Content.File(path) - case None => - val pomStr = Pom.create( - org, - name, - ver, - dependencies = metadata.dependencies.getOrElse(Nil).map { - case (org0, name0, ver0) => - (org0, name0, ver0, None) - } - ) - Content.InMemory(now, pomStr.getBytes(StandardCharsets.UTF_8)) - } - - Right((content, org, name, ver)) - - case (orgOpt, nameOpt, verOpt) => - params.pomOpt match { - case None => - Left(new PublishError.InvalidArguments( - "Either specify organization / name / version, or pass a POM file." - )) - case Some(path) => - val s = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) - - val elem = scala.xml.XML.loadString(s) // can throw… - val xml = coursier.core.compatibility.xmlFromElem(elem) - - val pomOrError = - for { - _ <- - (if (xml.label == "project") Right(()) else Left("Project definition not found")) - proj <- coursier.maven.Pom.project(xml) - } yield proj - - pomOrError match { - case Left(err) => - Left(new PublishError.InvalidArguments(s"Error parsing $path: $err")) - case Right(proj) => - val org = orgOpt.getOrElse(proj.module.organization) - val name = nameOpt.getOrElse(proj.module.name) - val ver = verOpt.getOrElse(proj.version) - val content = - if (metadata.isEmpty) - Content.File(path) - else { - - var elem0 = elem - elem0 = orgOpt.fold(elem0)(Pom.overrideOrganization(_, elem0)) - elem0 = nameOpt.fold(elem0)(Pom.overrideModuleName(_, elem0)) - elem0 = verOpt.fold(elem0)(Pom.overrideVersion(_, elem0)) - - val pomStr = Pom.print(elem0) - Content.InMemory(now, pomStr.getBytes(StandardCharsets.UTF_8)) - } - - Right((content, org, name, ver)) - } - } - } - - def manualPackageFileSet( - params: SinglePackageParams, - metadata: MetadataParams, - now: Instant - ): Either[PublishError.InvalidArguments, FileSet] = - pomModuleVersion(params, metadata, now).map { - case (pom, org, name, ver) => - val dir = Path(org.value.split('.').toSeq ++ Seq(name.value, ver)) - - val jarOpt = params - .jarOpt - .map { path => - (dir / s"${name.value}-$ver.jar", Content.File(path)) - } - - val artifacts = params - .artifacts - .map { - case (classifier, ext, path) => - val suffix = - if (classifier.isEmpty) "" - else "-" + classifier.value - (dir / s"${name.value}-$ver$suffix.${ext.value}", Content.File(path)) - } - - FileSet( - Seq((dir / s"${name.value}-$ver.pom", pom)) ++ - jarOpt.toSeq ++ - artifacts - ) - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/Publish.scala b/modules/cli/src/main/scala/coursier/cli/publish/Publish.scala deleted file mode 100644 index 3bc3f51228..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/Publish.scala +++ /dev/null @@ -1,307 +0,0 @@ -package coursier.cli.publish - -import java.io.{File, PrintStream} -import java.net.URI -import java.nio.file.Paths -import java.time.Instant -import java.util.concurrent.ScheduledExecutorService - -import caseapp._ -import coursier.cache.internal.ThreadUtil -import coursier.publish.checksum.{ChecksumType, Checksums} -import coursier.publish.fileset.{FileSet, Group} -import coursier.cli.publish.options.PublishOptions -import coursier.cli.publish.params.PublishParams -import coursier.publish.upload._ -import coursier.cli.CoursierCommand -import coursier.cli.publish.util.{DeleteOnExit, Git} -import coursier.cli.util.Guard -import coursier.maven.MavenRepository -import coursier.publish.download.{Download, FileDownload, OkhttpDownload} -import coursier.util.{Sync, Task} - -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, ExecutionContext} - -object Publish extends CoursierCommand[PublishOptions] { - override def hidden: Boolean = true - - val defaultChecksums = Seq(ChecksumType.MD5, ChecksumType.SHA1) - - private def repoParams( - repo: MavenRepository, - parallel: Boolean = false, - dummyUpload: Boolean = false, - urlSuffix: String = "" - ): (Upload, Download, MavenRepository, Boolean) = { - - val (upload, download, repo0, isLocal) = - // TODO Accept .\ too on Windows? - if (!repo.root.contains("://") && repo.root.contains(File.separatorChar)) { - val p = Paths.get(repo.root).toAbsolutePath - (FileUpload(p), FileDownload(p), repo.withRoot("."), true) - } - else if (repo.root.startsWith("file:")) { - val p = Paths.get(new URI(repo.root)).toAbsolutePath - (FileUpload(p), FileDownload(p), repo.withRoot("."), true) - } - else if (repo.root.startsWith("http://") || repo.root.startsWith("https://")) { - val pool = Sync.fixedThreadPool(if (parallel) 4 else 1) // sizing, shutdown, … - val upload = - if (parallel) - OkhttpUpload.create(pool, expect100Continue = true, urlSuffix) - else - HttpURLConnectionUpload.create(pool, urlSuffix) - (upload, OkhttpDownload.create(pool), repo, false) - } - else - throw new PublishError.UnrecognizedRepositoryFormat(repo.root) - - val actualUpload = - if (dummyUpload) DummyUpload(upload) - else upload - - (actualUpload, download, repo0, isLocal) - } - - private def isSnapshot(fs: FileSet): Task[Boolean] = { - val versions = Group.split(fs).collect { - case m: Group.Module => m.version - } - val snapshotMap = versions.groupBy(_.endsWith("SNAPSHOT")) - if (snapshotMap.size >= 2) - Task.fail(new Exception("Cannot push both snapshot and non-snapshot artifacts")) - else - Task.point(!snapshotMap.contains(false)) - } - - def publish(params: PublishParams, out: PrintStream, es: ScheduledExecutorService): Task[Unit] = { - - val deleteOnExit = new DeleteOnExit(params.verbosity) - - val now = Instant.now() - - params.maybeWarnSigner(out) - - val hooks = params.hooks(out, es) - - for { - - _ <- params.initSigner - - manualPackageFileSetOpt <- Input.manualPackageFileSetOpt(params, now) - dirFileSet0 <- Input.dirFileSet(params, out) - sbtFileSet0 <- Input.sbtFileSet( - params, - now, - out, - deleteOnExit, - manualPackageFileSetOpt.isEmpty, - es - ) - fileSet0 = { - val fileSets = manualPackageFileSetOpt.toSeq ++ Seq(dirFileSet0, sbtFileSet0) - fileSets.foldLeft(FileSet.empty)(_ ++ _) - } - _ = { - if (params.verbosity >= 2) { - System.err.println(s"Initial file set (${fileSet0.elements.length} elements)") - for ((p, _) <- fileSet0.elements) - System.err.println(" " + p.repr) - System.err.println() - } - } - _ <- { - if (fileSet0.isEmpty) - Task.fail(new PublishError.NoInput) - else - Task.point(()) - } - - updateFileSet0 <- { - val scmDomainPath = { - val dir = params.directory.directories.headOption - .orElse(params.directory.sbtDirectories.headOption) - .map(_.toFile) - .getOrElse(new File(".")) - if (params.metadata.git.getOrElse(params.repository.gitHub)) - Git(dir) - else - None - } - val distMgmtRepo = - if (params.repository.gitHub) - scmDomainPath.flatMap { - case ("github.com", path) if path.count(_ == '/') == 1 => - val owner = path.takeWhile(_ != '/') - Some(("github", owner, s"https://maven.pkg.github.com/$path")) - case _ => None - } - else - None - fileSet0.updateMetadata( - params.metadata.organization, - params.metadata.name, - params.metadata.version, - params.metadata.licenses, - params.metadata.developersOpt, - params.metadata.homePage, - scmDomainPath, - distMgmtRepo, - now - ) - } - - isSnapshot0 <- isSnapshot(updateFileSet0) - - (_, readDownload, readRepo, _) = - repoParams(params.repository.repository.readRepo(isSnapshot0)) - - fileSet1 <- { - if (params.metadata.mavenMetadata.getOrElse(!params.repository.gitHub)) - PublishTasks.updateMavenMetadata( - updateFileSet0, - now, - readDownload, - readRepo, - params.downloadLogger(out), - params.repository.snapshotVersioning - ) - else - Task.point { - PublishTasks.clearMavenMetadata(updateFileSet0) - } - } - - // re-init signer (e.g. in case gpg-agent cleared its cache since the first init) - _ <- params.initSigner - - withSignatures <- params - .signer - .signatures( - fileSet1, - now, - ChecksumType.all.map(_.extension).toSet, - Set("maven-metadata.xml"), - params.signerLogger(out) - ) - .flatMap { - case Left((path, _, msg)) => Task.fail(new Exception( - s"Failed to sign $path: $msg" - )) - case Right(fs) => Task.point(fileSet1 ++ fs) - } - - finalFileSet <- { - val checksums = params.checksum.checksumsOpt.getOrElse { - if (params.repository.gitHub || params.repository.bintray) - Nil - else - defaultChecksums - } - if (checksums.isEmpty) - Task.point( - Checksums.clear(ChecksumType.all, withSignatures) - ) - else - Checksums( - checksums, - withSignatures, - now, - params.checksumLogger(out) - ).map(withSignatures ++ _) - } - - sortedFinalFileSet <- finalFileSet.order - - _ = { - if (params.verbosity >= 2) { - System.err.println(s"Writing / pushing ${sortedFinalFileSet.elements.length} elements:") - for ((f, _) <- sortedFinalFileSet.elements) - System.err.println(s" ${f.repr}") - } - } - - hooksData <- hooks.beforeUpload(sortedFinalFileSet, isSnapshot0) - - retainedRepo = hooks.repository(hooksData, params.repository.repository, isSnapshot0) - .getOrElse(params.repository.repository.repo(isSnapshot0)) - - parallel = params.parallel.getOrElse(!params.repository.gitHub) - urlSuffix = params.urlSuffixOpt.getOrElse(if (params.repository.bintray) ";publish=1" else "") - - (upload, _, repo, isLocal) = repoParams( - retainedRepo, - parallel = parallel, - dummyUpload = params.dummy, - urlSuffix = urlSuffix - ) - - res <- upload.uploadFileSet( - repo, - sortedFinalFileSet, - params.uploadLogger(out, isLocal), - parallel = parallel - ) - _ <- { - if (res.isEmpty) - Task.point(()) - else - Task.fail(new PublishError.UploadingError(repo, res)) - } - - _ <- hooks.afterUpload(hooksData) - } yield - if (params.verbosity >= 0) { - val actualReadRepo = params.repository.repository.checkResultsRepo(isSnapshot0) - val modules = Group.split(sortedFinalFileSet) - .collect { case m: Group.Module => m } - .sortBy(m => (m.organization.value, m.name.value, m.version)) - out.println("\n \ud83d\udc40 Check results at") - for (m <- modules) { - val base = actualReadRepo.root + m.baseDir.map("/" + _).mkString - out.println(s" $base") - } - // TODO If publishing releases to Sonatype and not promoting, print message about how to promote things. - // TODO If publishing releases to Sonatype, print message about Maven Central sync. - } - } - - def run(options: PublishOptions, args: RemainingArgs): Unit = { - - Guard() - - val params = PublishParams(options, args.all).toEither match { - case Left(errors) => - for (err <- errors.toList) - System.err.println(err) - sys.exit(1) - case Right(p) => p - } - - val es = ThreadUtil.fixedScheduledThreadPool(params.cache.parallel) - val ec = ExecutionContext.fromExecutorService(es) - - val task = publish(params, System.err, es) - val f = task.attempt.future()(ec) - val res = Await.result(f, Duration.Inf) - - res match { - case Left(err: PublishError) if params.verbosity <= 1 => - System.err.println(err.message) - sys.exit(1) - - // Kind of meh to catch those here. - // These errors should be returned via Either-s or other and handled explicitly. - case Left(err: Upload.Error) if params.verbosity <= 1 => - System.err.println(err.getMessage) - sys.exit(1) - - case Left(e) => - throw e - - case Right(()) => - // normal exit - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/PublishError.scala b/modules/cli/src/main/scala/coursier/cli/publish/PublishError.scala deleted file mode 100644 index 6cabccdb2b..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/PublishError.scala +++ /dev/null @@ -1,37 +0,0 @@ -package coursier.cli.publish - -import coursier.publish.fileset.{FileSet, Path} -import coursier.publish.upload.Upload -import coursier.maven.MavenRepository -import coursier.publish.Content - -abstract class PublishError(val message: String, cause: Throwable = null) - extends Exception(message, cause) - -object PublishError { - - final class InvalidArguments(message: String) - extends PublishError(message) - - final class UnrecognizedRepositoryFormat(repo: String) - extends PublishError( - s"Unrecognized repository format: $repo (expected repository starting with / | ./ | http:// | https:// )" - ) - - final class NoInput - extends PublishError("No input specified, e.g. via --dir or --sbt or --jar") - - final class UploadingError( - repo: MavenRepository, - errors: Seq[(Path, Content, Upload.Error)] - ) extends PublishError( - errors - .map { - case (p, _, err) => - s"Error uploading ${p.repr} to ${repo.root}: ${err.getMessage}" - } - .mkString(System.lineSeparator()), - errors.headOption.map(_._3).orNull - ) - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/PublishRepository.scala b/modules/cli/src/main/scala/coursier/cli/publish/PublishRepository.scala deleted file mode 100644 index 7c028eaa6a..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/PublishRepository.scala +++ /dev/null @@ -1,141 +0,0 @@ -package coursier.cli.publish - -import coursier.core.Authentication -import coursier.maven.MavenRepository - -sealed abstract class PublishRepository extends Product with Serializable { - def snapshotRepo: MavenRepository - def releaseRepo: MavenRepository - def readSnapshotRepo: MavenRepository - def readReleaseRepo: MavenRepository - - final def repo(isSnapshot: Boolean): MavenRepository = - if (isSnapshot) - snapshotRepo - else - releaseRepo - final def readRepo(isSnapshot: Boolean): MavenRepository = - if (isSnapshot) - readSnapshotRepo - else - readReleaseRepo - def checkResultsRepo(isSnapshot: Boolean): MavenRepository = - readRepo(isSnapshot) - - def withAuthentication(auth: Authentication): PublishRepository -} - -object PublishRepository { - - final case class Simple( - snapshotRepo: MavenRepository, - readRepoOpt: Option[MavenRepository] = None - ) extends PublishRepository { - def releaseRepo: MavenRepository = snapshotRepo - def readSnapshotRepo: MavenRepository = readRepoOpt.getOrElse(snapshotRepo) - def readReleaseRepo: MavenRepository = readSnapshotRepo - - def withAuthentication(auth: Authentication): Simple = - copy( - snapshotRepo = snapshotRepo.withAuthentication(Some(auth)), - readRepoOpt = readRepoOpt.map(_.withAuthentication(Some(auth))) - ) - } - - final case class Bintray( - user: String, - repository: String, - package0: String, - apiKey: String, - overrideAuthOpt: Option[Authentication] - ) extends PublishRepository { - - def authentication: Authentication = - overrideAuthOpt.getOrElse(Authentication(user, apiKey)) - - def releaseRepo: MavenRepository = - MavenRepository( - s"https://api.bintray.com/maven/$user/$repository/$package0", - authentication = Some(authentication) - ) - def snapshotRepo: MavenRepository = - releaseRepo - - def readReleaseRepo: MavenRepository = - MavenRepository(s"https://dl.bintray.com/$user/$repository") - def readSnapshotRepo: MavenRepository = - readReleaseRepo - - def withAuthentication(auth: Authentication): Bintray = - copy(overrideAuthOpt = Some(auth)) - - } - - final case class GitHub( - username: String, - repo: String, - token: String, - overrideAuthOpt: Option[Authentication] - ) extends PublishRepository { - - def releaseRepo: MavenRepository = - MavenRepository( - s"https://maven.pkg.github.com/$username/$repo", - authentication = overrideAuthOpt.orElse(Some(Authentication(username, token))) - ) - def snapshotRepo: MavenRepository = - releaseRepo - - def readReleaseRepo: MavenRepository = - releaseRepo - def readSnapshotRepo: MavenRepository = - releaseRepo - - def withAuthentication(auth: Authentication): GitHub = - copy(overrideAuthOpt = Some(auth)) - - override def toString: String = - Iterator(username, repo, "****", overrideAuthOpt) - .mkString("GitHub(", ", ", ")") - } - - final case class Sonatype(base: MavenRepository) extends PublishRepository { - - def snapshotRepo: MavenRepository = - base.withRoot(s"${base.root}/content/repositories/snapshots") - def releaseRepo: MavenRepository = - base.withRoot(s"$restBase/staging/deploy/maven2") - def releaseRepoOf(repoId: String): MavenRepository = - base.withRoot(s"$restBase/staging/deployByRepositoryId/$repoId") - def readSnapshotRepo: MavenRepository = - snapshotRepo - def readReleaseRepo: MavenRepository = - base.withRoot(s"${base.root}/content/repositories/releases") - - override def checkResultsRepo(isSnapshot: Boolean): MavenRepository = - if (isSnapshot) - super.checkResultsRepo(isSnapshot) - else - base.withRoot(s"${base.root}/content/repositories/public") - - def restBase: String = - s"${base.root}/service/local" - - def withAuthentication(auth: Authentication): Sonatype = - copy( - base = base.withAuthentication(Some(auth)) - ) - } - - def gitHub(username: String, repo: String, token: String): PublishRepository = - GitHub(username, repo, token, None) - - def bintray( - user: String, - repository: String, - package0: String, - apiKey: String - ): PublishRepository = - Bintray(user, repository, package0, apiKey, None) - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/PublishTasks.scala b/modules/cli/src/main/scala/coursier/cli/publish/PublishTasks.scala deleted file mode 100644 index 746dd071b6..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/PublishTasks.scala +++ /dev/null @@ -1,121 +0,0 @@ -package coursier.cli.publish - -import java.time.Instant - -import coursier.maven.MavenRepository -import coursier.publish.download.Download -import coursier.publish.download.logger.DownloadLogger -import coursier.publish.fileset.{FileSet, Group} -import coursier.publish.sonatype.SonatypeApi -import coursier.publish.sonatype.logger.SonatypeLogger -import coursier.util.Task - -object PublishTasks { - - def updateMavenMetadata( - fs: FileSet, - now: Instant, - download: Download, - repository: MavenRepository, - logger: DownloadLogger, - withMavenSnapshotVersioning: Boolean - ): Task[FileSet] = { - - val groups = Group.split(fs) - - for { - groups0 <- Group.addOrUpdateMavenMetadata(groups, now) - fromRepo <- Group.downloadMavenMetadata( - groups.collect { case m: Group.Module => (m.organization, m.name) }, - download, - repository, - logger - ) - metadata <- Group.mergeMavenMetadata( - fromRepo ++ groups0.collect { case m: Group.MavenMetadata => m }, - now - ) - groups1 = groups0.flatMap { - case _: Group.MavenMetadata => Nil - case m => Seq(m) - } ++ metadata - groups2 <- Task.gather.gather { - groups1.map { - case m: Group.Module if m.version.endsWith("SNAPSHOT") && !m.version.contains("+") => - if (withMavenSnapshotVersioning) - Group.downloadSnapshotVersioningMetadata(m, download, repository, logger).flatMap { - m0 => - m0.addSnapshotVersioning(now, Set("md5", "sha1", "asc")) // meh second arg - } - else - Task.point(m.clearSnapshotVersioning) - case other => - Task.point(other) - } - } - res <- Task.fromEither(Group.merge(groups2).left.map(msg => new Exception(msg))) - } yield res - } - - def clearMavenMetadata(fs: FileSet): FileSet = { - - val groups = Group.split(fs) - - val updatedGroups = groups.flatMap { - case _: Group.MavenMetadata => Nil - case other => Seq(other) - } - - Group.mergeUnsafe(updatedGroups) - } - - def sonatypeProfile( - fs: FileSet, - api: SonatypeApi, - logger: SonatypeLogger - ): Task[SonatypeApi.Profile] = { - - val groups = Group.split(fs) - val orgs = groups.map(_.organization).distinct - - api.listProfiles(logger).flatMap { profiles => - val m = orgs.map { org => - val validProfiles = - profiles.filter(p => org.value == p.name || org.value.startsWith(p.name + ".")) - val profileOpt = - if (validProfiles.isEmpty) - None - else - Some(validProfiles.minBy(_.name.length)) - org -> profileOpt - } - - val noProfiles = m.collect { - case (org, None) => org - } - - if (noProfiles.isEmpty) { - val m0 = m.collect { - case (org, Some(p)) => org -> p - } - - val grouped = m0.groupBy(_._2) - - if (grouped.size > 1) - Task.fail(new Exception( - s"Cannot publish to several Sonatype profiles at once (${grouped.keys.toVector.map(_.name).sorted})" - )) - else { - assert(grouped.size == 1) - Task.point(grouped.head._1) - } - } - else - Task.fail(new Exception( - s"No Sonatype profile found to publish under organization(s) ${noProfiles.map(_.value).sorted.mkString(", ")}" - )) - } - - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/conf/Conf.scala b/modules/cli/src/main/scala/coursier/cli/publish/conf/Conf.scala deleted file mode 100644 index 2cdbc9c863..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/conf/Conf.scala +++ /dev/null @@ -1,109 +0,0 @@ -package coursier.cli.publish.conf - -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Path} - -import argonaut._ -import argonaut.Argonaut._ -import coursier.core.Organization -import coursier.publish.Pom.License - -final case class Conf( - organization: OrganizationDetails, - version: Option[String], - homePage: Option[String], - licenses: Option[List[License]], - developers: Option[List[coursier.publish.Pom.Developer]] -) - -object Conf { - - private implicit val licenseDecoder: DecodeJson[License] = - DecodeJson { c => - c.as[String].flatMap { l => - l.split(":", 2) match { - case Array(id, url) => - DecodeResult.ok(License(id, url)) - case Array(id) => - License.map.get(id) match { - case None => - DecodeResult.fail(s"Unrecognized license: '$id'", c.history) - case Some(l) => - DecodeResult.ok(l) - } - } - } - } - - private final case class SimpleFields( - version: Option[String] = None, - ver: Option[String] = None, - homePage: Option[String] = None, - home: Option[String] = None, - developers: Option[List[Developer]] = None, - licenses: Option[List[License]] = None - ) { - def versionOpt = - version.orElse(ver) - def homePageOpt = - homePage.orElse(home) - } - - private object SimpleFields { - import argonaut.ArgonautShapeless._ - implicit val decoder = DecodeJson.of[SimpleFields] - } - - private val orgDetailsDecoder: DecodeJson[Option[OrganizationDetails]] = - DecodeJson { c => - - c.as(SimpleFields.decoder).flatMap { s => - - val orgField = c.downField("organization").success - .orElse(c.downField("organisation").success) - .orElse(c.downField("org").success) - - orgField match { - case None => DecodeResult.ok(Option.empty[OrganizationDetails]) - case Some(c) => - val organizationAsString = orgField - .flatMap(_.as[String].toOption) - .map { s => - OrganizationDetails(Some(Organization(s)), None) - } - def organizationAsObj = orgField - .flatMap(_.as(OrganizationDetails.decoder).toOption) - - organizationAsString - .orElse(organizationAsObj) match { - case None => - DecodeResult.fail("Malformed organization field", c.history) - case Some(org) => DecodeResult.ok(Option(org)) - } - } - } - } - - implicit val decoder: DecodeJson[Conf] = - DecodeJson { c => - for { - s <- c.as(SimpleFields.decoder) - orgDetailsOpt <- c.as(orgDetailsDecoder) - } yield Conf( - orgDetailsOpt.getOrElse(OrganizationDetails.empty), - s.versionOpt, - s.homePageOpt, - s.licenses, - s.developers.map(_.map(_.get)) - ) - } - - def load(path: Path): Either[String, Conf] = { - - // TODO catch errors and report them here - val s = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) - - s.decodeEither(decoder) - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/conf/Developer.scala b/modules/cli/src/main/scala/coursier/cli/publish/conf/Developer.scala deleted file mode 100644 index 77092b23c7..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/conf/Developer.scala +++ /dev/null @@ -1,25 +0,0 @@ -package coursier.cli.publish.conf - -import argonaut._ - -final case class Developer( - id: String, - name: String, - url: String, - email: Option[String] = None -) { - def get: coursier.publish.Pom.Developer = - coursier.publish.Pom.Developer( - id, - name, - url, - email - ) -} - -object Developer { - - import argonaut.ArgonautShapeless._ - implicit val decoder = DecodeJson.of[Developer] - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/conf/OrganizationDetails.scala b/modules/cli/src/main/scala/coursier/cli/publish/conf/OrganizationDetails.scala deleted file mode 100644 index 2f52477120..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/conf/OrganizationDetails.scala +++ /dev/null @@ -1,39 +0,0 @@ -package coursier.cli.publish.conf - -import argonaut._ -import argonaut.Argonaut._ -import coursier.core.Organization - -final case class OrganizationDetails( - organization: Option[Organization], - url: Option[String] -) - -object OrganizationDetails { - - val empty = OrganizationDetails(None, None) - - implicit val decoder: DecodeJson[OrganizationDetails] = - DecodeJson { c => - - if (c.focus.isObject) { - - val nameOpt = c.field("name").success - .orElse(c.field("value").success) - .map(_.as[String].map(s => Option(Organization(s)))) - .getOrElse(DecodeResult.ok(None)) - - val urlOpt = c.field("url").success - .map(_.as[String].map(Option(_))) - .getOrElse(DecodeResult.ok(None)) - - for { - n <- nameOpt - u <- urlOpt - } yield OrganizationDetails(n, u) - } - else - DecodeResult.fail("Organization: not an object", c.history) - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/ChecksumOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/ChecksumOptions.scala deleted file mode 100644 index 9692850731..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/ChecksumOptions.scala +++ /dev/null @@ -1,9 +0,0 @@ -package coursier.cli.publish.options - -// format: off -final case class ChecksumOptions( - - checksums: Option[List[String]] = None - -) -// format: on diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/DirectoryOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/DirectoryOptions.scala deleted file mode 100644 index fd19f56c87..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/DirectoryOptions.scala +++ /dev/null @@ -1,9 +0,0 @@ -package coursier.cli.publish.options - -// format: off -final case class DirectoryOptions( - dir: List[String] = Nil, - sbtDir: List[String] = Nil, - sbt: Boolean = false -) -// format: on diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/MetadataOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/MetadataOptions.scala deleted file mode 100644 index bf535e803d..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/MetadataOptions.scala +++ /dev/null @@ -1,37 +0,0 @@ -package coursier.cli.publish.options - -import caseapp._ - -// format: off -final case class MetadataOptions( - - @Name("org") - @Name("O") - organization: Option[String] = None, - - @Name("N") - name: Option[String] = None, - - @Name("V") - version: Option[String] = None, - - @Name("dep") - @Name("d") - dependency: List[String] = Nil, - - license: List[String] = Nil, - - home: Option[String] = None, - - @HelpMessage("Read metadata from git") - git: Option[Boolean] = None, - - mavenMetadata: Option[Boolean] = None - -) -// format: on - -object MetadataOptions { - implicit val parser = Parser[MetadataOptions] - implicit val help = caseapp.core.help.Help[MetadataOptions] -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/PublishOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/PublishOptions.scala deleted file mode 100644 index 05448e16c7..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/PublishOptions.scala +++ /dev/null @@ -1,56 +0,0 @@ -package coursier.cli.publish.options - -import caseapp._ -import coursier.cli.options.CacheOptions - -// format: off -@HelpMessage("[Experimental] Publish an artifact to a maven repository.") -final case class PublishOptions( - - @Recurse - repositoryOptions: RepositoryOptions = RepositoryOptions(), - - @Recurse - metadataOptions: MetadataOptions = MetadataOptions(), - - @Recurse - singlePackageOptions: SinglePackageOptions = SinglePackageOptions(), - - @Recurse - directoryOptions: DirectoryOptions = DirectoryOptions(), - - @Recurse - checksumOptions: ChecksumOptions = ChecksumOptions(), - - @Recurse - signatureOptions: SignatureOptions = SignatureOptions(), - - @Recurse - cacheOptions: CacheOptions = CacheOptions(), - - @Name("q") - quiet: Option[Boolean] = None, - - @Name("v") - verbose: Int @@ Counter = Tag.of(0), - - dummy: Boolean = false, - - @HelpMessage("Disable interactive output") - batch: Option[Boolean] = None, - - conf: Option[String] = None, - - sbtOutputFrame: Int = 10, - - parallelUpload: Option[Boolean] = None, - - urlSuffix: Option[String] = None - -) -// format: on - -object PublishOptions { - implicit val parser = Parser[PublishOptions] - implicit val help = caseapp.core.help.Help[PublishOptions] -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/RepositoryOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/RepositoryOptions.scala deleted file mode 100644 index dad962c9db..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/RepositoryOptions.scala +++ /dev/null @@ -1,41 +0,0 @@ -package coursier.cli.publish.options - -import caseapp._ - -// format: off -final case class RepositoryOptions( - - @Name("r") - @Name("repo") - @Name("dest") - repository: Option[String] = None, - - @HelpMessage("Repository to read maven-metadata.xml files from") - readFrom: Option[String] = None, - - auth: Option[String] = None, - - sonatype: Option[Boolean] = None, - - github: Option[String] = None, - - bintray: Option[String] = None, - bintrayApiKey: Option[String] = None, - bintrayLicense: List[String] = Nil, - bintrayVcsUrl: Option[String] = None, - - snapshotVersioning: Boolean = true - -) { - // format: on - - override def toString: String = - copy(auth = auth.map(_ => "****")) - .productIterator - .mkString("RepositoryOptions(", ", ", ")") -} - -object RepositoryOptions { - implicit val parser = Parser[RepositoryOptions] - implicit val help = caseapp.core.help.Help[RepositoryOptions] -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/SignatureOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/SignatureOptions.scala deleted file mode 100644 index 297d9f076d..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/SignatureOptions.scala +++ /dev/null @@ -1,8 +0,0 @@ -package coursier.cli.publish.options - -// format: off -final case class SignatureOptions( - gpg: Option[Boolean] = None, - gpgKey: Option[String] = None -) -// format: on diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/SinglePackageOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/SinglePackageOptions.scala deleted file mode 100644 index 469bd687fb..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/SinglePackageOptions.scala +++ /dev/null @@ -1,26 +0,0 @@ -package coursier.cli.publish.options - -import caseapp._ - -// format: off -final case class SinglePackageOptions( - - jar: Option[String] = None, - - @Name("P") - pom: Option[String] = None, - - @Name("A") - @ValueDescription("classifier:/path/to/file") - artifact: List[String] = Nil, - - @HelpMessage("Force creation of a single package (default: true if --jar or --pom or --artifact specified, else false)") - `package`: Option[Boolean] = None - -) -// format: on - -object SinglePackageOptions { - implicit val parser = Parser[SinglePackageOptions] - implicit val help = caseapp.core.help.Help[SinglePackageOptions] -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/options/package.scala b/modules/cli/src/main/scala/coursier/cli/publish/options/package.scala deleted file mode 100644 index c43ffe5abd..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/options/package.scala +++ /dev/null @@ -1,8 +0,0 @@ -package coursier.cli.publish - -/** Mostly contains case classes defining command-line options. - * - * These case classes are passed as is to case-app. Field names directly correspond to option names - * in particular. - */ -package object options diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/ChecksumParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/ChecksumParams.scala deleted file mode 100644 index 850ab3b67f..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/ChecksumParams.scala +++ /dev/null @@ -1,38 +0,0 @@ -package coursier.cli.publish.params - -import cats.data.{Validated, ValidatedNel} -import cats.implicits._ -import coursier.publish.checksum.ChecksumType -import coursier.cli.publish.options.ChecksumOptions - -final case class ChecksumParams( - checksumsOpt: Option[Seq[ChecksumType]] -) - -object ChecksumParams { - - def apply(options: ChecksumOptions): ValidatedNel[String, ChecksumParams] = { - - val checksumsOptV = - options.checksums match { - case None => - Validated.validNel(None) - case Some(list) => - list - .flatMap(_.split(',')) - .map(_.trim) - .filter(_.nonEmpty) - .traverse { s => - Validated.fromEither(ChecksumType.parse(s)) - .toValidatedNel - } - .map(Some(_)) - } - - checksumsOptV.map { checksumsOpt => - ChecksumParams( - checksumsOpt - ) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/DirectoryParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/DirectoryParams.scala deleted file mode 100644 index 6fdbe950ec..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/DirectoryParams.scala +++ /dev/null @@ -1,66 +0,0 @@ -package coursier.cli.publish.params - -import java.nio.file.{Files, Path, Paths} - -import cats.data.{Validated, ValidatedNel} -import cats.implicits._ -import coursier.publish.dir.Dir -import coursier.cli.publish.options.DirectoryOptions -import coursier.publish.sbt.Sbt - -final case class DirectoryParams( - directories: Seq[Path], - sbtDirectories: Seq[Path] -) - -object DirectoryParams { - def apply(options: DirectoryOptions, args: Seq[String]): ValidatedNel[String, DirectoryParams] = { - - val dirsV = options.dir.traverse { d => - val dir0 = Paths.get(d) - if (Files.exists(dir0)) - if (Files.isDirectory(dir0)) - Validated.validNel(dir0) - else - Validated.invalidNel(s"$d not a directory") - else - Validated.invalidNel(s"$d not found") - } - - val sbtDirsV = ((if (options.sbt) List(".") else Nil) ::: options.sbtDir).traverse { d => - val dir0 = Paths.get(d) - if (Files.exists(dir0)) - if (Files.isDirectory(dir0)) { - val buildProps = dir0.resolve("project/build.properties") - if (Files.exists(buildProps)) - Validated.validNel(dir0) - else - Validated.invalidNel(s"project/build.properties not found under sbt directory $d") - } - else - Validated.invalidNel(s"$d not a directory") - else - Validated.invalidNel(s"$d not found") - } - - val extraV = args - .toList - .traverse { a => - val p = Paths.get(a) - if (Sbt.isSbtProject(p)) - Validated.validNel((None, Some(p))) - else if (Dir.isRepository(p)) - Validated.validNel((Some(p), None)) - else - Validated.invalidNel(s"$a is neither an sbt project or a local repository") - } - - (dirsV, sbtDirsV, extraV).mapN { - case (dirs, sbtDirs, extra) => - DirectoryParams( - dirs ++ extra.flatMap(_._1), - sbtDirs ++ extra.flatMap(_._2) - ) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/MetadataParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/MetadataParams.scala deleted file mode 100644 index 9eb238e047..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/MetadataParams.scala +++ /dev/null @@ -1,104 +0,0 @@ -package coursier.cli.publish.params - -import cats.data.{Validated, ValidatedNel} -import cats.implicits._ -import coursier.cli.publish.options.MetadataOptions -import coursier.core.{ModuleName, Organization} -import coursier.parse.DependencyParser -import coursier.publish.Pom.{Developer, License} - -final case class MetadataParams( - organization: Option[Organization], - name: Option[ModuleName], - version: Option[String], - licenses: Option[Seq[License]], - homePage: Option[String], - // TODO Support full-fledged coursier.Dependency? - dependencies: Option[Seq[(Organization, ModuleName, String)]], - developersOpt: Option[Seq[Developer]], - git: Option[Boolean], - mavenMetadata: Option[Boolean] -) { - def isEmpty: Boolean = - organization.isEmpty && - name.isEmpty && - version.isEmpty && - licenses.isEmpty && - homePage.isEmpty && - dependencies.isEmpty && - developersOpt.isEmpty -} - -object MetadataParams { - def apply( - options: MetadataOptions, - defaultScalaVersion: String - ): ValidatedNel[String, MetadataParams] = { - - // TODO Check for invalid character? emptiness? - val organization = options.organization.map(Organization(_)) - val name = options.name.map(ModuleName(_)) - val version = options.version - val dependenciesV = - if (options.dependency.forall(_.trim.isEmpty)) - Validated.validNel(None) - else - options - .dependency - .map(_.trim) - .filter(_.nonEmpty) - .traverse { s => - DependencyParser.moduleVersion(s, defaultScalaVersion) match { - case Left(err) => - Validated.invalidNel(err) - case Right((mod, _)) if mod.attributes.nonEmpty => - Validated.invalidNel(s"Dependency $s: attributes not supported for now") - case Right((mod, ver)) => - Validated.validNel((mod.organization, mod.name, ver)) - } - } - .map(Some(_)) - - val licensesV = - if (options.license.forall(_.trim.isEmpty)) - Validated.validNel(None) - else - options - .license - .map(_.trim) - .filter(_.nonEmpty) - .traverse { s => - s.split(":", 2) match { - case Array(id, url) => - Validated.validNel(License(id, url)) - case Array(id) => - License.map.get(id) match { - case None => - Validated.invalidNel( - s"Unrecognized license '$id', please pass an URL for it like --license $id:https://…" - ) - case Some(license) => - Validated.validNel(license) - } - } - } - .map(Some(_)) - - val homePageOpt = options.home.map(_.trim).filter(_.nonEmpty) - - (dependenciesV, licensesV).mapN { - (dependencies, licenses) => - MetadataParams( - organization, - name, - version, - licenses, - homePageOpt, - dependencies, - None, // developer not accepted from the command-line for now - options.git, - options.mavenMetadata - ) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/PublishParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/PublishParams.scala deleted file mode 100644 index 8aceae4a3c..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/PublishParams.scala +++ /dev/null @@ -1,288 +0,0 @@ -package coursier.cli.publish.params - -import java.io.PrintStream -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Path, Paths} -import java.time.Instant -import java.util.concurrent.{ScheduledExecutorService, TimeUnit} - -import caseapp.Tag -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.implicits._ -import coursier.cache.loggers.RefreshLogger -import coursier.cli.params.CacheParams -import coursier.cli.publish.{Hooks, PublishRepository} -import coursier.cli.publish.conf.Conf -import coursier.cli.publish.options.PublishOptions -import coursier.publish.Content -import coursier.publish.bintray.BintrayApi -import coursier.publish.checksum.logger.{ - BatchChecksumLogger, - ChecksumLogger, - InteractiveChecksumLogger -} -import coursier.publish.download.logger.{DownloadLogger, SimpleDownloadLogger} -import coursier.publish.signing.{GpgSigner, NopSigner, Signer} -import coursier.publish.signing.logger.{BatchSignerLogger, InteractiveSignerLogger, SignerLogger} -import coursier.publish.sonatype.SonatypeApi -import coursier.publish.upload.logger.{BatchUploadLogger, InteractiveUploadLogger, UploadLogger} -import coursier.util.Task -import okhttp3.OkHttpClient - -final case class PublishParams( - repository: RepositoryParams, - metadata: MetadataParams, - singlePackage: SinglePackageParams, - directory: DirectoryParams, - checksum: ChecksumParams, - signature: SignatureParams, - cache: CacheParams, - verbosity: Int, - dummy: Boolean, - batch: Boolean, - sbtOutputFrame: Option[Int], - parallel: Option[Boolean], - urlSuffixOpt: Option[String] -) { - def withConf(conf: Conf): PublishParams = { - - var p = this - - for (o <- conf.organization.organization if p.metadata.organization.isEmpty) - p = p.copy( - metadata = p.metadata.copy(organization = Some(o)) - ) - - // TODO Take conf.organization.url into account - - for (v <- conf.version if p.metadata.version.isEmpty) - p = p.copy( - metadata = p.metadata.copy(version = Some(v)) - ) - - for (url <- conf.homePage if p.metadata.homePage.isEmpty) - p = p.copy( - metadata = p.metadata.copy(homePage = Some(url)) - ) - - for (licenses <- conf.licenses if p.metadata.licenses.isEmpty) - p = p.copy( - metadata = p.metadata.copy(licenses = Some(licenses)) - ) - - for (developers <- conf.developers if p.metadata.developersOpt.isEmpty) - p = p.copy( - metadata = p.metadata.copy(developersOpt = Some(developers)) - ) - - p - } - - def dirName(dir: Path, short: Option[String] = None): String = - if (verbosity >= 2) - dir.normalize().toAbsolutePath.toString - else - short.getOrElse(dir.getFileName.toString) - - lazy val signer: Signer = - if (signature.gpg) { - val key = signature.gpgKeyOpt match { - case None => GpgSigner.Key.Default - case Some(id) => GpgSigner.Key.Id(id) - } - GpgSigner(key) - } - else - NopSigner - - def maybeWarnSigner(out: PrintStream): Unit = - (repository.repository, signer) match { - case (_: PublishRepository.Sonatype, NopSigner) => - out.println("Warning: --sonatype passed, but signing not enabled, trying to proceed anyway") - case _ => - } - - // Signing dummy stuff to trigger any gpg dialog, before our signer logger is set up. - // The gpg dialog and our logger seem to conflict else, leaving the terminal in a bad state. - def initSigner: Task[Unit] = - signer - .sign(Content.InMemory(Instant.EPOCH, "hello".getBytes(StandardCharsets.UTF_8))) - .flatMap { - case Left(msg) => - Task.fail(new Exception(s"Failed to sign: $msg")) - case Right(_) => Task.point(()) - } - - def hooks(out: PrintStream, es: ScheduledExecutorService): Hooks = - repository.repository match { - case repo: PublishRepository.Sonatype => - // this can't be shutdown anyway - val client = new OkHttpClient.Builder() - // Sonatype can be quite slow - .readTimeout(60L, TimeUnit.SECONDS) - .build() - val authentication = repository.repository.snapshotRepo.authentication - if (authentication.isEmpty && verbosity >= 0) - out.println("Warning: no Sonatype credentials passed, trying to proceed anyway") - val api = SonatypeApi( - client, - repo.restBase, - repository.repository.snapshotRepo.authentication, - verbosity - ) - Hooks.sonatype(repo, api, out, verbosity, batch, es) - - case repo: PublishRepository.Bintray => - // this can't be shutdown anyway - val client = new OkHttpClient.Builder() - // just in case - .readTimeout(60L, TimeUnit.SECONDS) - .build() - val authentication = repository.repository.snapshotRepo.authentication - if (authentication.isEmpty && verbosity >= 0) - out.println("Warning: no Bintray credentials passed, trying to proceed anyway") // ??? - val api = - BintrayApi(client, "https://api.bintray.com", Some(repo.authentication), verbosity) - Hooks.bintray( - api, - repo.user, - repo.repository, - repo.package0, - if (repository.bintrayLicenses.isEmpty) Seq("Apache-2.0") /* FIXME */ - else repository.bintrayLicenses, - repository.bintrayVcsUrlOpt.getOrElse( - s"https://bintray.com/${repo.user}/${repo.repository}/${repo.package0}" - ) - ) - - case _ => - Hooks.dummy - } - - def downloadLogger(out: PrintStream): DownloadLogger = - new SimpleDownloadLogger(out, verbosity) - - def signerLogger(out: PrintStream): SignerLogger = - if (batch) - new BatchSignerLogger(out, verbosity) - else - InteractiveSignerLogger.create(out, verbosity) - - def checksumLogger(out: PrintStream): ChecksumLogger = - if (batch) - new BatchChecksumLogger(out, verbosity) - else - InteractiveChecksumLogger.create(out, verbosity) - - def uploadLogger(out: PrintStream, isLocal: Boolean): UploadLogger = - if (batch) - new BatchUploadLogger(out, dummy, isLocal) - else - InteractiveUploadLogger.create(out, dummy, isLocal) - -} - -object PublishParams { - def apply(options: PublishOptions, args: Seq[String]): ValidatedNel[String, PublishParams] = { - - // FIXME Get from options - val defaultScalaVersion = scala.util.Properties.versionNumberString - - val repositoryV = RepositoryParams(options.repositoryOptions) - val metadataV = MetadataParams(options.metadataOptions, defaultScalaVersion) - val singlePackageV = SinglePackageParams(options.singlePackageOptions) - val directoryV = DirectoryParams(options.directoryOptions, args) - val checksumV = ChecksumParams(options.checksumOptions) - val signatureV = SignatureParams(options.signatureOptions) - val cacheV = options.cacheOptions.params - - val verbosityV = - (options.quiet, Tag.unwrap(options.verbose)) match { - case (Some(true), 0) => - Validated.validNel(-1) - case (Some(true), n) => - assert(n > 0) - Validated.invalidNel("Cannot specify both --quiet and --verbose") - case (_, n) => - Validated.validNel(n) - } - - val sbtOutputFrame = - Some(options.sbtOutputFrame).filter(_ > 0) - - val dummy = options.dummy - val batch = options.batch.getOrElse { - RefreshLogger.defaultFallbackMode - } - - val res = ( - repositoryV, - metadataV, - singlePackageV, - directoryV, - checksumV, - signatureV, - cacheV, - verbosityV - ).mapN { - (repository, metadata, singlePackage, directory, checksum, signature, cache, verbosity) => - PublishParams( - repository, - metadata, - singlePackage, - directory, - checksum, - signature, - cache, - verbosity, - dummy, - batch, - sbtOutputFrame, - options.parallelUpload, - options.urlSuffix - ) - } - - // TODO Actually take conf file into account beforehand - // So that e.g. its repository is taken into account and we do not default to sonatype here - res.withEither { e => - for { - p <- e - // TODO Warn about ignored fields in conf file? - confOpt <- options.conf match { - case None => - val loadDefaultIfExists = !p.singlePackage.`package` && - p.directory.directories.isEmpty && - p.directory.sbtDirectories.forall(_ == Paths.get(".")) - if (loadDefaultIfExists) { - val default = Paths.get("publish.json") - val projectDefault = Paths.get("project/publish.json") - if (Files.isRegularFile(default)) - Conf.load(default) - .left.map(NonEmptyList.of(_)) - .map(Some(_)) - else if (Files.isRegularFile(projectDefault)) - Conf.load(projectDefault) - .left.map(NonEmptyList.of(_)) - .map(Some(_)) - else - Right(None) - } - else - Right(None) - case Some(c) => - val p = Paths.get(c) - if (Files.exists(p)) - if (Files.isRegularFile(p)) - Conf.load(p) - .left.map(NonEmptyList.of(_)) - .map(Some(_)) - else - Left(NonEmptyList.of(s"Conf file $c is not a file")) - else - Left(NonEmptyList.of(s"Conf file $c not found")) - } - } yield confOpt.fold(p)(p.withConf) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/RepositoryParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/RepositoryParams.scala deleted file mode 100644 index 851be40e33..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/RepositoryParams.scala +++ /dev/null @@ -1,228 +0,0 @@ -package coursier.cli.publish.params - -import cats.data.{Validated, ValidatedNel} -import cats.implicits._ -import coursier.cli.publish.PublishRepository -import coursier.cli.publish.options.RepositoryOptions -import coursier.core.Authentication -import coursier.maven.MavenRepository -import coursier.parse.RepositoryParser - -final case class RepositoryParams( - repository: PublishRepository, - snapshotVersioning: Boolean, - gitHub: Boolean, - bintray: Boolean, - bintrayLicenses: Seq[String], - bintrayVcsUrlOpt: Option[String] -) - -object RepositoryParams { - - val sonatypeBase = "https://oss.sonatype.org" - - def apply(options: RepositoryOptions): ValidatedNel[String, RepositoryParams] = { - - // FIXME Take repo from conf file into account here - val sonatype = - options.sonatype.getOrElse( - options.repository.isEmpty && options.github.isEmpty && options.bintray.isEmpty - ) // or .getOrElse(false)? - - def defaultRepositoryV = { - val repositoryV = - options.repository match { - case None => - Validated.invalidNel("No repository specified, and --sonatype option not specified") - case Some(repoUrl) => - RepositoryParser.repository(repoUrl, maybeFile = true) match { - case Left(err) => - Validated.invalidNel(err) - case Right(m: MavenRepository) => - Validated.validNel(m) - case Right(_) => - Validated.invalidNel(s"$repoUrl: non-maven repositories not supported") - } - } - - val readRepositoryOptV = - options.readFrom match { - case None => - Validated.validNel(None) - case Some(repoUrl) => - RepositoryParser.repository(repoUrl, maybeFile = true) match { - case Left(err) => - Validated.invalidNel(err) - case Right(m: MavenRepository) => - Validated.validNel(Some(m)) - case Right(_) => - Validated.invalidNel(s"$repoUrl: non-maven repositories not supported") - } - } - - (repositoryV, readRepositoryOptV).mapN { - (repo, readRepoOpt) => - PublishRepository.Simple(repo, readRepoOpt) - } - } - - def fromGitHub(ghCredentials: String): ValidatedNel[String, PublishRepository] = { - - val (ghRepo, ghTokenOpt) = - ghCredentials.split(":", 2) match { - case Array(user) => (user, None) - case Array(user, token) => (user, Some(token)) - } - - val ghUserRepoV = - ghRepo.split("/", 2) match { - case Array(user, repo) => Validated.validNel((user, repo)) - case _ => - Validated.invalidNel(s"Invalid GitHub repository: '$ghRepo' (expected 'user/repo')") - } - - val ghTokenV = - ghTokenOpt match { - case Some(token) => Validated.validNel(token) - case None => - Option(System.getenv("GH_TOKEN")) match { - case Some(token) => Validated.validNel(token) - case None => Validated.invalidNel("No GitHub token specified") - } - } - - (ghUserRepoV, ghTokenV).mapN { - case ((user, repo), token) => - PublishRepository.gitHub(user, repo, token) - } - } - - def fromBintray( - repo: String, - apiKey: Option[String] - ): ValidatedNel[String, PublishRepository] = { - - val paramsV = repo.split("/", 3) match { - case Array(user, repo0, package0) => - Validated.validNel((user, repo0, package0)) - case Array(user, repo0) => - Validated.validNel((user, repo0, "default")) - case Array(user) => - Validated.validNel((user, "maven", "default")) - case _ => - Validated.invalidNel( - s"Invalid bintray repository: '$repo' (expected 'user/repository/package')" - ) - } - - val apiKeyV = apiKey match { - case None => - Validated.invalidNel( - "No Bintray API key specified (--bintray-api-key or BINTRAY_API_KEY in the environment)" - ) - case Some(key) => Validated.validNel(key) - } - - (paramsV, apiKeyV).mapN { - case ((user, repo0, package0), key) => - PublishRepository.bintray(user, repo0, package0, key) - } - } - - def fromSonatype = - if (options.repository.nonEmpty || options.readFrom.nonEmpty) - Validated.invalidNel("Cannot specify --repository or --read-from along with --sonatype") - else - Validated.validNel( - PublishRepository.Sonatype(MavenRepository(sonatypeBase)) - ) - - val repositoryV = - options.github.map(fromGitHub) - .orElse { - options.bintray - .map { repo => - val apiKey = options.bintrayApiKey.orElse(Option(System.getenv("BINTRAY_API_KEY"))) - fromBintray(repo, apiKey) - } - } - .getOrElse { - if (sonatype) - fromSonatype - else - defaultRepositoryV - } - - def authFromEnv(userVar: String, passVar: String) = { - val userV = Option(System.getenv(userVar)) match { - case None => Validated.invalidNel(s"User environment variable $userVar not set") - case Some(u) => Validated.validNel(u) - } - val passV = Option(System.getenv(passVar)) match { - case None => Validated.invalidNel(s"Password environment variable $passVar not set") - case Some(u) => Validated.validNel(u) - } - (userV, passV).mapN { - (user, pass) => - Some(Authentication(user, pass)) - } - } - - val credentialsV = options.auth match { - case None => - if (sonatype) - authFromEnv("SONATYPE_USERNAME", "SONATYPE_PASSWORD") - else - Validated.validNel(None) - case Some(s) => - def handleAuth(auth: String) = - auth.split(":", 2) match { - case Array(user, pass) => - Validated.validNel(Some(Authentication(user, pass))) - case _ => - Validated.invalidNel( - "Malformed --auth argument (expected user:password, or env:USER_ENV_VAR:PASSWORD_ENV_VAR)" - ) - } - - if (s.startsWith("env:")) - if (s.contains(":")) - s.split(":", 2) match { - case Array(userVar, passVar) => - authFromEnv(userVar, passVar) - case _ => - // should not happen - ??? - } - else { - val varName = s.stripPrefix("env:") - Option(System.getenv(varName)) match { - case None => - Validated.invalidNel(s"Authentication environment variable $varName not set") - case Some(v) => - handleAuth(v) - } - } - else if (s.startsWith("file:")) - // TODO - ??? - else - handleAuth(s) - } - - val snapshotVersioning = options.snapshotVersioning - - (repositoryV, credentialsV).mapN { - (repository, credentials) => - val repo = credentials.fold(repository)(repository.withAuthentication) - RepositoryParams( - repo, - snapshotVersioning, - options.github.nonEmpty, - options.bintray.nonEmpty, - options.bintrayLicense, - options.bintrayVcsUrl - ) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/SignatureParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/SignatureParams.scala deleted file mode 100644 index d94946835d..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/SignatureParams.scala +++ /dev/null @@ -1,21 +0,0 @@ -package coursier.cli.publish.params - -import cats.data.{Validated, ValidatedNel} -import coursier.cli.publish.options.SignatureOptions - -final case class SignatureParams( - gpg: Boolean, - gpgKeyOpt: Option[String] -) - -object SignatureParams { - def apply(options: SignatureOptions): ValidatedNel[String, SignatureParams] = - // check here that the passed gpg key exists? - Validated.validNel( - SignatureParams( - // TODO Adjust default value if --sonatype is passed - options.gpg.getOrElse(options.gpgKey.nonEmpty), - options.gpgKey - ) - ) -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/SinglePackageParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/SinglePackageParams.scala deleted file mode 100644 index 11f41931a4..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/SinglePackageParams.scala +++ /dev/null @@ -1,106 +0,0 @@ -package coursier.cli.publish.params - -import java.nio.file.{Files, Path, Paths} - -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.implicits._ -import coursier.cli.publish.options.SinglePackageOptions -import coursier.core.{Classifier, Extension} - -final case class SinglePackageParams( - jarOpt: Option[Path], - pomOpt: Option[Path], - artifacts: Seq[(Classifier, Extension, Path)], - `package`: Boolean -) - -object SinglePackageParams { - - private def q = "\"" - - def apply(options: SinglePackageOptions): ValidatedNel[String, SinglePackageParams] = { - - // FIXME This does some I/O (not reflected in return type) - - def fileExtensionV(path: String): ValidatedNel[String, (Path, String)] = - fileV(path).withEither(_.flatMap { p => - val name = p.getFileName.toString - val idx = name.lastIndexOf('.') - if (idx < 0) - Left(NonEmptyList.one( - s"$path has no extension, specify one by passing it with classifier:extension:$path" - )) - else { - val ext = name.drop(idx + 1) - if (ext.isEmpty) - Left(NonEmptyList.one( - s"$path extension is empty, specify one by passing it with classifier:extension:$path" - )) - else - Right((p, ext)) - } - }) - - def fileV(path: String): ValidatedNel[String, Path] = { - val p = Paths.get(path) - if (!Files.exists(p)) - Validated.invalidNel(s"not found: $path") - else if (!Files.isRegularFile(p)) - Validated.invalidNel(s"not a regular file: $path") - else - Validated.validNel(p) - } - - def fileOptV(pathOpt: Option[String]): ValidatedNel[String, Option[Path]] = - pathOpt match { - case None => - Validated.validNel(None) - case Some(path) => - fileV(path).map(Some(_)) - } - - val jarOptV = fileOptV(options.jar) - val pomOptV = fileOptV(options.pom) - - val artifactsV = options.artifact.traverse { s => - s.split(":", 3) match { - case Array(strClassifier, strExtension, path) => - fileV(path).map((Classifier(strClassifier), Extension(strExtension), _)) - case Array(strClassifier, path) => - fileExtensionV(path).map { - case (p, ext) => - (Classifier(strClassifier), Extension(ext), p) - } - case _ => - Validated.invalidNel( - s"Malformed artifact argument: $s (expected: ${q}classifier:/path/to/artifact$q)" - ) - } - } - - val packageV = options.`package` match { - case Some(true) => - Validated.validNel(true) - case Some(false) => - if (options.jar.nonEmpty || options.pom.nonEmpty || options.artifact.nonEmpty) - Validated.invalidNel( - "Cannot specify --package=false along with --pom or --jar or --artifact" - ) - else - Validated.validNel(false) - case None => - val p = options.jar.nonEmpty || options.pom.nonEmpty || options.artifact.nonEmpty - Validated.validNel(p) - } - - (jarOptV, pomOptV, artifactsV, packageV).mapN { - (jarOpt, pomOpt, artifacts, package0) => - SinglePackageParams( - jarOpt, - pomOpt, - artifacts, - package0 - ) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/SonatypeParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/SonatypeParams.scala deleted file mode 100644 index 099e491b63..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/SonatypeParams.scala +++ /dev/null @@ -1,5 +0,0 @@ -package coursier.cli.publish.params - -final case class SonatypeParams( - restBase: String -) diff --git a/modules/cli/src/main/scala/coursier/cli/publish/params/package.scala b/modules/cli/src/main/scala/coursier/cli/publish/params/package.scala deleted file mode 100644 index a89cd4a4fa..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/params/package.scala +++ /dev/null @@ -1,8 +0,0 @@ -package coursier.cli.publish - -/** Reworked / validated command-line options. - * - * This package mostly contains case classes, build out of the ones from [[coursier.cli.options]], - * usually after some validation. - */ -package object params diff --git a/modules/cli/src/main/scala/coursier/cli/publish/sonatype/Sonatype.scala b/modules/cli/src/main/scala/coursier/cli/publish/sonatype/Sonatype.scala deleted file mode 100644 index f1de1bc374..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/sonatype/Sonatype.scala +++ /dev/null @@ -1,288 +0,0 @@ -package coursier.cli.publish.sonatype - -import java.util.concurrent.TimeUnit - -import caseapp._ -import coursier.publish.sonatype.SonatypeApi -import coursier.util.Task -import okhttp3.OkHttpClient - -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, ExecutionContext} - -object Sonatype extends CaseApp[SonatypeOptions] { - - def listProfiles( - params: SonatypeParams, - api: SonatypeApi - ): Task[Option[Seq[SonatypeApi.Profile]]] = - if (params.raw) - for { - json <- api.rawListProfiles() - _ <- Task.delay { - println(json.spaces2) - } - profiles <- Task.fromEither(api.decodeListProfilesResponse(json)) - } yield Some(profiles) - else - for { - profiles <- api.listProfiles() - _ <- Task.delay { - for (p <- profiles) - println(s"Profile ${p.name}\n id: ${p.id}\n URL: ${p.uri}") - } - } yield Some(profiles) - - def list( - params: SonatypeParams, - api: SonatypeApi, - profileIdOpt: Option[String] - ): Task[Option[Seq[SonatypeApi.Repository]]] = - if (params.raw) - for { - json <- api.rawListProfileRepositories(profileIdOpt) - _ <- Task.delay(println(json.spaces2)) - repositories <- Task.fromEither(api.decodeListProfileRepositoriesResponse(json)) - } yield Some(repositories) - else - for { - repositories <- api.listProfileRepositories(profileIdOpt) - _ <- Task.delay { - println("Repositories" + profileIdOpt.fold("")(p => s" of profile $p")) - for (r <- repositories if !params.cleanList || !r.id.startsWith("central_bundles-")) { - val extra = profileIdOpt.fold(s", profile: ${r.profileName}")(_ => "") - println(s" ${r.id} (${r.`type`}" + extra + ")") - } - } - } yield Some(repositories) - - def create(params: SonatypeParams, api: SonatypeApi, profile: SonatypeApi.Profile): Task[Unit] = - if (params.raw) - for { - json <- api.rawCreateStagingRepository(profile, params.description.getOrElse("")) - _ <- Task.delay(println(json.spaces2)) - } yield () - else - for { - repoId <- api.createStagingRepository(profile, params.description.getOrElse("")) - _ <- Task.delay(println(s"Created repository $repoId")) - } yield () - - def close( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile, - repoId: String - ): Task[Unit] = - for { - _ <- api.sendCloseStagingRepositoryRequest(profile, repoId, params.description.getOrElse("")) - _ <- Task.delay(println(s"Closed repository $repoId")) - } yield () - - def promote( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile, - repoId: String - ): Task[Unit] = - for { - _ <- - api.sendPromoteStagingRepositoryRequest(profile, repoId, params.description.getOrElse("")) - _ <- Task.delay(println(s"Promoted repository $repoId")) - } yield () - - def drop( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile, - repoId: String - ): Task[Unit] = - for { - _ <- api.sendDropStagingRepositoryRequest(profile, repoId, params.description.getOrElse("")) - _ <- Task.delay(println(s"Dropped repository $repoId")) - } yield () - - def maybeListProfiles( - params: SonatypeParams, - api: SonatypeApi - ): Task[Option[Seq[SonatypeApi.Profile]]] = - if (params.listProfiles) - listProfiles(params, api) - else if (params.needListProfiles) - api.listProfiles().map(Some(_)) - else - Task.point(None) - - def maybeList( - params: SonatypeParams, - api: SonatypeApi, - profileIdOpt: Option[String] - ): Task[Option[Seq[SonatypeApi.Repository]]] = - if (params.list) - list(params, api, profileIdOpt) - else if (params.needListRepositories) - api.listProfileRepositories(profileIdOpt).map(Some(_)) - else - Task.point(None) - - def maybeCreate( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile - ): Task[Unit] = - if (params.create) - create(params, api, profile) - else - Task.point(()) - - def maybeClose( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile, - repoId: String - ): Task[Unit] = - if (params.close) - close(params, api, profile, repoId) - else - Task.point(()) - - def maybePromote( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile, - repoId: String - ): Task[Unit] = - if (params.promote) - promote(params, api, profile, repoId) - else - Task.point(()) - - def maybeDrop( - params: SonatypeParams, - api: SonatypeApi, - profile: SonatypeApi.Profile, - repoId: String - ): Task[Unit] = - if (params.drop) - drop(params, api, profile, repoId) - else - Task.point(()) - - def run(options: SonatypeOptions, remainingArgs: RemainingArgs): Unit = { - - if (remainingArgs.all.nonEmpty) - sys.error(s"unexpected arguments: ${remainingArgs.all.mkString(", ")}") - - val params = SonatypeParams(options).toEither match { - case Left(errs) => - errs.toList.foreach(println) - sys.exit(1) - case Right(p) => p - } - - val client = new OkHttpClient.Builder() - // Sonatype can be quite slow… - .readTimeout(60L, TimeUnit.SECONDS) - .build() - - val api = SonatypeApi(client, params.base, params.authentication, verbosity = params.verbosity) - - def profileTask(profiles: Seq[SonatypeApi.Profile], idOrName: Either[String, String]) = { - - val profileOpt = idOrName match { - case Left(id) => profiles.find(_.id == id) - case Right(name) => profiles.find(_.name == name) - } - - profileOpt match { - case None => Task.fail(new Exception(s"Profile ${idOrName.merge} not found")) - case Some(p) => Task.delay { - if (idOrName.isRight) - Console.err.println(s"Profile id of ${idOrName.merge}: ${p.id}") - p - } - } - } - - val t = for { - profiles <- maybeListProfiles(params, api).map(_.getOrElse(Nil)) - - repositoriesOpt <- { - - val profileIdOptTask = params.profileIdOpt match { - case Some(id) => Task.point(Some(id)) - case None => - params.profileNameOpt match { - case Some(name) => - profileTask(profiles, Right(name)) - .map(p => Some(p.id)) - case None => - Task.point(None) - } - } - - profileIdOptTask - .flatMap(maybeList(params, api, _)) - } - - profileOpt <- { - - val proceed = params.create || params.close || params.promote || params.drop - - if (proceed) - (params.profileIdOpt, params.profileNameOpt) match { - case (Some(id), _) => - profileTask(profiles, Left(id)) - .map(Some(_)) - - case (None, Some(name)) => - profileTask(profiles, Right(name)) - .map(Some(_)) - - case (None, None) => - val profileOpt = for { - repoId <- params.repositoryIdOpt - repositories <- repositoriesOpt - r <- repositories.find(_.id == repoId) - p <- profiles.find(_.id == r.profileId) - } yield p - - Task.point(profileOpt) - } - else - Task.point(None) - } - _ <- { - profileOpt match { - case None => - if (params.create) - Task.fail(new Exception("No profile")) - else - Task.point(()) - case Some(profile) => - maybeCreate(params, api, profile) - } - } - _ <- { - (profileOpt, params.repositoryIdOpt) match { - case (Some(profile), Some(repoId)) => - for { - _ <- maybeClose(params, api, profile, repoId) - _ <- maybePromote(params, api, profile, repoId) - _ <- maybeDrop(params, api, profile, repoId) - } yield () - case _ => - if (params.close || params.promote || params.drop) { - val msg = - s"No profile or repo (repositoriesOpt: $repositoriesOpt, profiles: ${profiles.mkString(", ")})" - Task.fail(new Exception(msg)) - } - else - Task.point(()) - } - } - } yield () - - Await.result(t.future()(ExecutionContext.global), Duration.Inf) - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeOptions.scala b/modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeOptions.scala deleted file mode 100644 index 3fe34a882e..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeOptions.scala +++ /dev/null @@ -1,29 +0,0 @@ -package coursier.cli.publish.sonatype - -import caseapp._ - -final case class SonatypeOptions( - raw: Boolean = false, - profile: Option[String] = None, - profileId: Option[String] = None, - @Name("repo") - repository: Option[String] = None, - description: String = "", - listProfiles: Boolean = false, - list: Option[Boolean] = None, - cleanList: Option[Boolean] = None, - create: Boolean = false, - close: Boolean = false, - promote: Boolean = false, - drop: Boolean = false, - base: Option[String] = None, - user: Option[String] = None, - password: Option[String] = None, - @Name("v") - verbose: Int @@ Counter = Tag.of(0) -) - -object SonatypeOptions { - implicit val parser = Parser[SonatypeOptions] - implicit val help = caseapp.core.help.Help[SonatypeOptions] -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeParams.scala b/modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeParams.scala deleted file mode 100644 index 6fba9ad801..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/sonatype/SonatypeParams.scala +++ /dev/null @@ -1,101 +0,0 @@ -package coursier.cli.publish.sonatype - -import caseapp.Tag -import cats.data.{Validated, ValidatedNel} -import cats.implicits._ -import coursier.core.Authentication - -final case class SonatypeParams( - raw: Boolean, - listProfiles: Boolean, - list: Boolean, - cleanList: Boolean, - profileIdOpt: Option[String], - profileNameOpt: Option[String], - repositoryIdOpt: Option[String], - create: Boolean, - close: Boolean, - promote: Boolean, - drop: Boolean, - description: Option[String], - base: String, - authentication: Option[Authentication], - verbosity: Int -) { - def needListProfiles: Boolean = - profileIdOpt.isEmpty && (profileNameOpt.nonEmpty || repositoryIdOpt.nonEmpty) - def needListRepositories: Boolean = - profileIdOpt.isEmpty && repositoryIdOpt.nonEmpty -} - -object SonatypeParams { - def apply(options: SonatypeOptions): ValidatedNel[String, SonatypeParams] = { - - val description = Some(options.description).filter(_.nonEmpty) - - val list = options.list.orElse(options.cleanList).getOrElse(false) - - val checkActionsV = { - val missingAction = !options.listProfiles && - !list && - !options.create && - !options.close && - !options.promote && - !options.drop - if (missingAction) - Validated.invalidNel( - "No action specified (pass either one of --list-profiles, --list, --create, --close, --drop, or --promote)" - ) - else if (options.create && options.profileId.isEmpty && options.profile.isEmpty) - Validated.invalidNel("Profile id or name required to create a repository") - else if ((options.close || options.promote || options.drop) && options.repository.isEmpty) - Validated.invalidNel("Repository required to close, promote, or drop") - else - Validated.validNel(()) - } - - // FIXME this will duplicate error messages (re-uses the same Validated - - val authV = (options.user, options.password) match { - case (None, None) => - val userOpt = Option(System.getenv("SONATYPE_USERNAME")) - val passwordOpt = Option(System.getenv("SONATYPE_PASSWORD")) - (userOpt, passwordOpt) match { - case (Some(u), Some(p)) => - Validated.validNel(Some(Authentication(u, p))) - case _ => - // should we allow no authentication somehow? - Validated.invalidNel( - "No authentication specified (either pass --user and --password, or set SONATYPE_USERNAME and SONATYPE_PASSWORD in the environment)" - ) - } - case (Some(u), Some(p)) => - Validated.validNel(Some(Authentication(u, p))) - case (Some(_), None) => - Validated.invalidNel("User specified, but no password passed") - case (None, Some(_)) => - Validated.invalidNel("Password specified, but no user passed") - } - - (checkActionsV, authV).mapN { - (_, auth) => - SonatypeParams( - options.raw, - options.listProfiles, - list, - options.cleanList.getOrElse(!options.raw), - options.profileId, - options.profile, - options.repository, - options.create, - options.close, - options.promote, - options.drop, - description, - options.base.getOrElse("https://oss.sonatype.org/service/local"), - auth, - Tag.unwrap(options.verbose) - ) - } - } -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/util/DeleteOnExit.scala b/modules/cli/src/main/scala/coursier/cli/publish/util/DeleteOnExit.scala deleted file mode 100644 index 5ec849b65a..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/util/DeleteOnExit.scala +++ /dev/null @@ -1,57 +0,0 @@ -package coursier.cli.publish.util - -import java.nio.file.{Files, Path} - -import scala.jdk.CollectionConverters._ - -final class DeleteOnExit(verbosity: Int) { - - private def deleteRecursiveIfExists(f: Path): Unit = { - - if (Files.isDirectory(f)) { - var s: java.util.stream.Stream[Path] = null - try { - s = Files.list(f) - s.iterator() - .asScala - .foreach(deleteRecursiveIfExists) - } - finally if (s != null) - s.close() - } - - Files.deleteIfExists(f) - } - - @volatile private var addedHook = false - private val deleteOnExitLock = new Object - private var deleteOnExit0 = List.empty[Path] - - def apply(f: Path): Unit = { - - if (!addedHook) - deleteOnExitLock.synchronized { - if (!addedHook) { - Runtime.getRuntime.addShutdownHook( - new Thread("coursier-publish-delete-on-exit") { - setDaemon(true) - override def run() = - deleteOnExitLock.synchronized { - for (p <- deleteOnExit0.distinct if Files.exists(p)) { - if (verbosity >= 1) - Console.err.println(s"Cleaning up $p") - deleteRecursiveIfExists(p) - } - } - } - ) - addedHook = true - } - } - - deleteOnExitLock.synchronized { - deleteOnExit0 = f :: deleteOnExit0 - } - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/util/Git.scala b/modules/cli/src/main/scala/coursier/cli/publish/util/Git.scala deleted file mode 100644 index b138f665cd..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/util/Git.scala +++ /dev/null @@ -1,29 +0,0 @@ -package coursier.cli.publish.util - -import java.io.File - -object Git { - - def remoteUrl(dir: File): String = - // from https://github.com/sbt/sbt-git/blob/f8caf9365be380cf101e9605af159b5e7f842d0c/src/main/scala/com/typesafe/sbt/git/JGit.scala#L100 - scala.sys.process.Process(Seq("git", "ls-remote", "--get-url", "origin")).lineStream_!.head - - def apply(dir: File): Option[(String, String)] = { - - // from https://github.com/sbt/sbt-git/blob/f8caf9365be380cf101e9605af159b5e7f842d0c/src/main/scala/com/typesafe/sbt/SbtGit.scala#L125-L130 - val user = """(?:[^@\/]+@)?""" - val domain = """([^\/]+)""" - val gitPath = """(.*)\.git\/?""" - val unauthenticated = raw"""(?:git|https?|ftps?)\:\/\/$domain\/$gitPath""".r - val ssh = raw"""ssh\:\/\/$user$domain\/$gitPath""".r - val headlessSSH = raw"""$user$domain:$gitPath""".r - - remoteUrl(dir) match { - case unauthenticated(domain0, repo) => Some((domain0, repo)) - case ssh(domain0, repo) => Some((domain0, repo)) - case headlessSSH(domain0, repo) => Some((domain0, repo)) - case _ => None - } - } - -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/version/Options.scala b/modules/cli/src/main/scala/coursier/cli/publish/version/Options.scala deleted file mode 100644 index a6cb655fba..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/version/Options.scala +++ /dev/null @@ -1,17 +0,0 @@ -package coursier.cli.publish.version - -import caseapp._ - -// format: off -final case class Options( - @HelpMessage("Check if the current version is a snapshot one") - isSnapshot: Boolean = false, - @Name("q") // hmm, doesn't work - quiet: Boolean = false -) -// format: on - -object Options { - implicit val parser = Parser[Options] - implicit val help = caseapp.core.help.Help[Options] -} diff --git a/modules/cli/src/main/scala/coursier/cli/publish/version/Version.scala b/modules/cli/src/main/scala/coursier/cli/publish/version/Version.scala deleted file mode 100644 index 6c1da0de86..0000000000 --- a/modules/cli/src/main/scala/coursier/cli/publish/version/Version.scala +++ /dev/null @@ -1,99 +0,0 @@ -package coursier.cli.publish.version - -import java.io.{BufferedReader, File, InputStreamReader} - -import caseapp._ - -object Version extends CaseApp[Options] { - - private def output(cmd: Seq[String], dir: File): String = { - // adapted from https://stackoverflow.com/a/16714180/3714539 - val b = new ProcessBuilder(cmd: _*).directory(dir) - val p = b.start() - val reader = new BufferedReader(new InputStreamReader(p.getInputStream)) - val builder = new StringBuilder - var line: String = null - while ({ - line = reader.readLine() - line != null - }) { - builder.append(line) - builder.append(System.getProperty("line.separator")) - } - val retCode = p.waitFor() - if (retCode == 0) - builder.toString - else - throw new Exception(s"Command ${cmd.mkString(" ")} exited with code $retCode") - } - - def version(dir: File): Either[String, String] = { - - val tag = output(Seq("git", "describe", "--tags", "--match", "v[0-9]*", "--abbrev=0"), dir).trim - - if (tag.isEmpty) - Left("No git tag like v[0-9]* found") - else { - val dist = - output(Seq("git", "rev-list", "--count", s"$tag...HEAD"), dir).trim.toInt // can throw… - - if (dist == 0) - Right(tag) - else { - - val previousVersion = tag.stripPrefix("v") - - // Tweak coursier.core.Version.Tokenizer to help here? - - val versionOpt = - if (previousVersion.forall(c => c == '.' || c.isDigit)) { - val l = previousVersion.split('.') - Some((l.init :+ (l.last.toInt + 1).toString).mkString(".") + "-SNAPSHOT") - } - else { - val idx = previousVersion.indexOf("-M") - if (idx < 0) - None - else - Some(previousVersion.take(idx) + "-SNAPSHOT") - } - - versionOpt.toRight { - s"Don't know how to handle version $previousVersion" - } - } - } - } - - def run(options: Options, remainingArgs: RemainingArgs): Unit = { - - val dir = remainingArgs.all match { - case Seq() => new File(".") - case Seq(path) => new File(path) - case other => - Console.err.println( - s"Too many arguments specified: ${other.mkString(" ")}\nExpected 0 or 1 argument." - ) - sys.exit(1) - } - - version(dir) match { - case Left(msg) => - Console.err.println(msg) - sys.exit(1) - case Right(v) => - if (options.isSnapshot) { - val retCode = - if (v.endsWith("-SNAPSHOT")) - 0 - else - 1 - if (!options.quiet) - Console.err.println(v) - sys.exit(retCode) - } - else - println(v) - } - } -} diff --git a/modules/publish/src/main/scala/coursier/publish/Content.scala b/modules/publish/src/main/scala/coursier/publish/Content.scala deleted file mode 100644 index 9b5652eb9a..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/Content.scala +++ /dev/null @@ -1,41 +0,0 @@ -package coursier.publish - -import java.nio.file.{Files, Path} -import java.time.Instant - -import coursier.util.Task - -/** Content of a file, either on disk or in memory. - */ -sealed abstract class Content extends Product with Serializable { - def lastModifiedTask: Task[Instant] - // TODO Support chunked reading - def contentTask: Task[Array[Byte]] - - def pathOpt: Option[Path] = None -} - -object Content { - - final case class File(path: Path) extends Content { - def lastModifiedTask: Task[Instant] = - Task.delay { - Files.getLastModifiedTime(path) - .toInstant - } - def contentTask: Task[Array[Byte]] = - Task.delay { - Files.readAllBytes(path) - } - override def pathOpt: Option[Path] = - Some(path) - } - - final case class InMemory(lastModified: Instant, content: Array[Byte]) extends Content { - def lastModifiedTask: Task[Instant] = - Task.point(lastModified) - def contentTask: Task[Array[Byte]] = - Task.point(content) - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/MavenMetadata.scala b/modules/publish/src/main/scala/coursier/publish/MavenMetadata.scala deleted file mode 100644 index 88dc699016..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/MavenMetadata.scala +++ /dev/null @@ -1,319 +0,0 @@ -package coursier.publish - -import java.time.{Instant, LocalDateTime, ZoneOffset} -import java.time.format.DateTimeFormatter -import java.util.Locale - -import coursier.core.{ModuleName, Organization} - -import scala.util.Try -import scala.xml.{Elem, Node} - -object MavenMetadata { - - final case class Info( - latest: Option[String], - release: Option[String], - versions: Seq[String], - lastUpdated: Option[LocalDateTime] - ) - - private val lastUpdatedPattern = DateTimeFormatter.ofPattern("yyyyMMddHHmmss") - val timestampPattern = DateTimeFormatter.ofPattern("yyyyMMdd.HHmmss") - - def create( - org: Organization, - name: ModuleName, - latest: Option[String], - release: Option[String], - versions: Seq[String], - lastUpdated: LocalDateTime - ): Elem = - - {org.value} - {name.value} - - {latest.fold()(v => {v})} - {release.fold()(v => {v})} - {versions.map(v => {v})} - {lastUpdated.format(lastUpdatedPattern)} - - - - def create( - org: Organization, - name: ModuleName, - latest: Option[String], - release: Option[String], - versions: Seq[String], - lastUpdated: Instant - ): Elem = - create( - org, - name, - latest, - release, - versions, - lastUpdated.atOffset(ZoneOffset.UTC).toLocalDateTime - ) - - def isReleaseVersion(version: String): Boolean = - // kind of flaky… - version - .toLowerCase(Locale.ROOT) - .replace("final", "") - .forall(c => c.isDigit || c == '.' || c == '-') - - def update( - content: Elem, - setOrg: Option[Organization], - setName: Option[ModuleName], - setLatest: Option[String], - setRelease: Option[String], - addVersions: Seq[String], - setLastUpdated: Option[LocalDateTime] - ): Elem = - // FIXME Only updates previous tags for now, doesn't add those that need to be added - content.copy( - child = content.child.map { - case n if n.label == "groupId" => - setOrg.fold(n)(g => {g.value}) - case n if n.label == "artifactId" => - setName.fold(n)(name => {name.value}) - case n: Elem if n.label == "versioning" => - n.copy( - child = n.child.map { - case n if n.label == "latest" => - setLatest.fold(n)(v => {v}) - case n if n.label == "release" => - setRelease.fold(n)(v => {v}) - case n: Elem if n.label == "versions" => - val found = n - .child - .collect { - case n if n.label == "version" => n.text - } - .toSet - val toAdd = addVersions.filter(!found(_)) - n.copy( - child = n.child ++ toAdd.map { v => - {v} - } - ) - case n if n.label == "lastUpdated" => - setLastUpdated.fold(n)(t => - {t.format(lastUpdatedPattern)} - ) - case n => n - } - ) - case n => n - } - ) - - def createSnapshotVersioning( - org: Organization, - name: ModuleName, - version: String, - snapshotVersioning: (LocalDateTime, Int), - now: Instant, - artifacts: Seq[(Option[String], String, String, LocalDateTime)] - ) = - - {org.value} - {name.value} - {version} - - - {snapshotVersioning._1.format(timestampPattern)} - {snapshotVersioning._2} - - {now.atOffset(ZoneOffset.UTC).toLocalDateTime.format(lastUpdatedPattern)} - - { - artifacts.map { - case (classifierOpt, ext, value, updated) => - - {classifierOpt.fold[Seq[Node]](Nil)(c => Seq({c}))} - {ext} - {value} - {updated.format(lastUpdatedPattern)} - - } - } - - - - - def snapshotVersioningBuildNumber(elem: Elem) = - elem.child.collectFirst { - case n: Elem if n.label == "versioning" => - n.child.collectFirst { - case n if n.label == "snapshot" => - n.child.collectFirst { - case n if n.label == "buildNumber" => - Try(n.text.toInt).toOption - }.flatten - }.flatten - }.flatten - - def currentSnapshotVersioning(elem: Elem) = - elem.child.collectFirst { - case n: Elem if n.label == "versioning" => - n.child.collectFirst { - case n if n.label == "snapshot" => - val buildNumOpt = n.child.collectFirst { - case n if n.label == "buildNumber" => - Try(n.text.toInt).toOption - }.flatten - val timestampOpt = n.child.collectFirst { - case n if n.label == "timestamp" => - Try(LocalDateTime.parse(n.text, timestampPattern)).toOption - }.flatten - - (buildNumOpt, timestampOpt) match { - case (Some(n), Some(ts)) => Some((n, ts)) - case (None, None) => None - case _ => ??? // Report via return type - } - }.flatten - }.flatten - - def updateSnapshotVersioning( - content: Elem, - setOrg: Option[Organization], - setName: Option[ModuleName], - setVersion: Option[String], - setSnapshotVersioning: Option[(LocalDateTime, Int)], - setLastUpdated: Option[LocalDateTime], - addArtifacts: Seq[(Option[String], String, String, LocalDateTime)] - ): Elem = - // FIXME Only updates previous tags for now, doesn't add those that need to be added - content.copy( - child = content.child.map { - case n if n.label == "groupId" => - setOrg.fold(n)(g => {g.value}) - case n if n.label == "artifactId" => - setName.fold(n)(name => {name.value}) - case n if n.label == "version" => - setVersion.fold(n)(v => {v}) - case n: Elem if n.label == "versioning" => - n.copy( - child = n.child.map { - case n if n.label == "snapshot" => - setSnapshotVersioning.fold(n) { - case (dt, num) => - - {dt.format(timestampPattern)} - {num} - - } - case n: Elem if n.label == "snapshotVersions" => - val m = addArtifacts.map { - case (c, ext, ver, lm) => - (c, ext) -> (ver, lm) - }.toMap - - val keep = n.child.flatMap { - case n if n.label == "snapshotVersion" => - val classifierOpt = n.child.collectFirst { - case n0 if n0.label == "classifier" => n0.text - } - val extension = n.child.collectFirst { - case n0 if n0.label == "extension" => n0.text - }.getOrElse("") - if (m.contains((classifierOpt, extension))) - Nil - else - Seq(n) - case n => Seq(n) - } - - - { - addArtifacts.map { - case (c, ext, ver, lm) => - - {c.fold(Seq.empty[Elem])(c0 => Seq({c0}))} - {ext} - {ver} - {lm.format(lastUpdatedPattern)} - - } ++ keep - } - - - case n if n.label == "lastUpdated" => - setLastUpdated.fold(n)(t => - {t.format(lastUpdatedPattern)} - ) - case n => n - } - ) - case n => n - } - ) - - def info(elem: Elem): Info = { - - val latestOpt = elem - .child - .collectFirst { - case n: Elem if n.label == "versioning" => - n.child.collectFirst { - case n0 if n0.label == "latest" => - n0.text - } - } - .flatten - - val releaseOpt = elem - .child - .collectFirst { - case n: Elem if n.label == "versioning" => - n.child.collectFirst { - case n0 if n0.label == "release" => - n0.text - } - } - .flatten - - val versions = elem - .child - .collectFirst { - case n: Elem if n.label == "versioning" => - n.child.collectFirst { - case n0: Elem if n0.label == "versions" => - n0.child.collect { - case v if v.label == "version" => v.text - } - } - } - .flatten - .toSeq - .flatten - - val lastUpdatedOpt = elem - .child - .collectFirst { - case n: Elem if n.label == "versioning" => - n.child.collectFirst { - case n0 if n0.label == "lastUpdated" => - // format error throw here… - LocalDateTime.from(lastUpdatedPattern.parse(n0.text)) - } - } - .flatten - - Info( - latestOpt, - releaseOpt, - versions, - lastUpdatedOpt - ) - } - - def print(elem: Elem): String = - Pom.print(elem) - -} diff --git a/modules/publish/src/main/scala/coursier/publish/Pom.scala b/modules/publish/src/main/scala/coursier/publish/Pom.scala deleted file mode 100644 index 827aea1244..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/Pom.scala +++ /dev/null @@ -1,404 +0,0 @@ -package coursier.publish - -import coursier.core.{Configuration, ModuleName, Organization, Type} - -import scala.collection.mutable -import scala.xml.{Elem, Node, NodeSeq} - -object Pom { - - // TODO Check https://github.com/lihaoyi/mill/pull/144/files - final case class License(name: String, url: String) - - object License { - def apache2: License = - License("Apache-2.0", "https://spdx.org/licenses/Apache-2.0.html") - - lazy val all = Seq( - apache2 - ) - - lazy val map = all.map(l => l.name -> l).toMap - } - - final case class Scm( - url: String, - connection: String, - developerConnection: String - ) - - object Scm { - def gitHub(org: String, project: String): Scm = - Scm( - s"https://github.com/$org/$project.git", - s"scm:git:github.com/$org/$project.git", - s"scm:git:git@github.com:$org/$project.git" - ) - } - - // FIXME What's mandatory? What's not? - final case class Developer( - id: String, - name: String, - url: String, - mail: Option[String] - ) - - def create( - organization: Organization, - moduleName: ModuleName, - version: String, - packaging: Option[Type] = None, - description: Option[String] = None, - url: Option[String] = None, - name: Option[String] = None, - // TODO Accept full-fledged coursier.Dependency - dependencies: Seq[(Organization, ModuleName, String, Option[Configuration])] = Nil, - license: Option[License] = None, - scm: Option[Scm] = None, - developers: Seq[Developer] = Nil - ): String = { - - val nodes = new mutable.ListBuffer[NodeSeq] - - nodes ++= Seq( - 4.0.0, - {organization.value}, - {moduleName.value}, - {version} - ) - - for (p <- packaging) - nodes += {p.value} - - for (u <- url) - nodes += {u} - - for (d <- description) - nodes += {d} - - for (n <- name) - nodes += {n} - - nodes += { - val urlNodeOpt = url.fold[NodeSeq](Nil)(u => {u}) - - {organization.value} - {urlNodeOpt} - - } - - for (l <- license) - nodes += - - - {l.name} - {l.url} - repo - - - - for (s <- scm) - nodes += - - {s.url} - {s.connection} - {s.developerConnection} - - - if (developers.nonEmpty) - nodes += - - { - developers.map { d => - - {d.id} - {d.name} - {d.url} - - // + optional mail - } - } - - - if (dependencies.nonEmpty) - nodes += - - { - dependencies.map { - case (depOrg, depName, ver, confOpt) => - - {depOrg.value} - {depName.value} - {ver} - {confOpt.fold[NodeSeq](Nil)(c => {c})} - - } - } - - - print( - - {nodes.result()} - - ) - } - - private def addOrUpdate(content: Elem, label: String)(update: Option[Node] => Node): Elem = { - - // assumes there's at most one child with this label… - - val found = content.child.exists(_.label == label) - - val updatedChildren = - if (found) - content.child.map { - case n if n.label == label => - update(Some(n)) - case n => - n - } - else - content.child :+ update(None) - - content.copy( - child = updatedChildren - ) - } - - def overrideOrganization(organization: Organization, content: Elem): Elem = { - - val content0 = addOrUpdate(content, "groupId") { _ => - {organization.value} - } - - addOrUpdate(content0, "organization") { - case Some(elem0: Elem) => - addOrUpdate(elem0, "name") { _ => - {organization.value} - } - case _ => - - {organization.value} - - } - } - - def overrideModuleName(name: ModuleName, content: Elem): Elem = - addOrUpdate(content, "artifactId") { _ => - {name.value} - } - - def overrideVersion(version: String, content: Elem): Elem = - addOrUpdate(content, "version") { _ => - {version} - } - - def overrideHomepage(url: String, content: Elem): Elem = { - - val content0 = addOrUpdate(content, "url") { _ => - {url} - } - - addOrUpdate(content0, "organization") { - case Some(elem0: Elem) => - addOrUpdate(elem0, "url") { _ => - {url} - } - case _ => - - {url} - - } - } - - def overrideScm(domain: String, path: String, content: Elem): Elem = - addOrUpdate(content, "scm") { - case Some(elem0: Elem) => - var elem1 = addOrUpdate(elem0, "url") { _ => - https://{domain}/{path} - } - elem1 = addOrUpdate(elem1, "connection") { _ => - scm:git:https://{domain}/{path}.git - } - addOrUpdate(elem1, "developerConnection") { _ => - scm:git:git@{domain}:{path}.git - } - case _ => - - https://{domain}/{path} - scm:git:https://{domain}/{path}.git - scm:git:git@{domain}:{path}.git - - } - - def overrideDistributionManagementRepository( - id: String, - name: String, - url: String, - content: Elem - ): Elem = - addOrUpdate(content, "distributionManagement") { - case Some(elem0: Elem) => - addOrUpdate(elem0, "repository") { _ => - - {id} - {name} - {url} - - } - case _ => - - - {id} - {name} - {url} - - - } - - def overrideLicenses(licenses: Seq[License], content: Elem): Elem = - addOrUpdate(content, "licenses") { _ => - { - licenses.map { l => - - {l.name} - {l.url} - repo - - } - } - } - - def overrideDevelopers(developers: Seq[Developer], content: Elem): Elem = - addOrUpdate(content, "developers") { _ => - { - developers.map { dev => - - {dev.id} - {dev.name} - { - dev.mail match { - case None => - - case Some(mail) => - {mail} - } - } - {dev.url} - - } - } - } - - def transformDependency( - content: Elem, - from: (Organization, ModuleName), - to: (Organization, ModuleName) - ): Elem = { - - def adjustGroupArtifactIds(n: Elem): Elem = { - - val orgOpt = n.child.collectFirst { - case n if n.label == "groupId" => Organization(n.text) - } - val nameOpt = n.child.collectFirst { - case n if n.label == "artifactId" => ModuleName(n.text) - } - - if (orgOpt.contains(from._1) && nameOpt.contains(from._2)) - n.copy( - child = n.child.map { - case n if n.label == "groupId" => - {to._1.value} - case n if n.label == "artifactId" => - {to._2.value} - case n => n - } - ) - else - n - } - - // TODO Adjust dependencyManagement section too? - - content.copy( - child = content.child.map { - case n: Elem if n.label == "dependencies" => - n.copy( - child = n.child.map { - case n: Elem if n.label == "dependency" => - val n0 = adjustGroupArtifactIds(n) - n0.copy( - child = n0.child.map { - case n: Elem if n.label == "exclusions" => - n.copy( - child = n.child.map { - case n: Elem if n.label == "exclusion" => - adjustGroupArtifactIds(n) - case n => n - } - ) - case n => n - } - ) - case n => n - } - ) - case n => n - } - ) - } - - def transformDependencyVersion( - content: Elem, - org: Organization, - name: ModuleName, - fromVersion: String, - toVersion: String - ): Elem = { - - def adjustVersion(n: Elem): Elem = { - - val orgOpt = n.child.collectFirst { - case n if n.label == "groupId" => Organization(n.text) - } - val nameOpt = n.child.collectFirst { - case n if n.label == "artifactId" => ModuleName(n.text) - } - - if (orgOpt.contains(org) && nameOpt.contains(name)) - n.copy( - child = n.child.map { - case n if n.label == "version" && n.text.trim == fromVersion => - {toVersion} - case n => n - } - ) - else - n - } - - // TODO Adjust dependencyManagement section too? - - content.copy( - child = content.child.map { - case n: Elem if n.label == "dependencies" => - n.copy( - child = n.child.map { - case n: Elem if n.label == "dependency" => - adjustVersion(n) - case n => n - } - ) - case n => n - } - ) - } - - def print(elem: Elem): String = { - val printer = new scala.xml.PrettyPrinter(Int.MaxValue, 2) - """""" + '\n' + printer.format(elem) - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/bintray/BintrayApi.scala b/modules/publish/src/main/scala/coursier/publish/bintray/BintrayApi.scala deleted file mode 100644 index a7093414b6..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/bintray/BintrayApi.scala +++ /dev/null @@ -1,120 +0,0 @@ -package coursier.publish.bintray - -import java.io.FileNotFoundException - -import argonaut._ -import argonaut.Argonaut._ -import coursier.core.Authentication -import coursier.publish.sonatype.OkHttpClientUtil -import coursier.util.Task -import okhttp3.OkHttpClient - -final case class BintrayApi( - client: OkHttpClient, - base: String, - authentication: Option[Authentication], - verbosity: Int -) { - - private val clientUtil = OkHttpClientUtil(client, authentication, verbosity) - - def getRepository(subject: String, repo: String): Task[Option[Json]] = - clientUtil.get[Json](s"$base/repos/$subject/$repo") // escaping? - .attempt - .flatMap { - case Left(_: FileNotFoundException) => - Task.point(None) - case Right(json) => - Task.point(Some(json)) - case Left(e) => - Task.fail(e) - } - - def createRepository( - subject: String, - repo: String - ): Task[Json] = - clientUtil.get[Json]( - s"$base/repos/$subject/$repo", - post = Some( - clientUtil.postBody( - BintrayApi.CreateRepositoryRequest(repo, "maven") - )(BintrayApi.CreateRepositoryRequest.encoder) - ) - ) - - def createRepositoryIfNeeded( - subject: String, - repo: String - ): Task[Boolean] = - getRepository(subject, repo).flatMap { - case None => createRepository(subject, repo).map(_ => true) - case Some(_) => Task.point(false) - } - - def getPackage(subject: String, repo: String, package0: String): Task[Option[Json]] = - clientUtil.get[Json](s"$base/packages/$subject/$repo/$package0") // escaping? - .attempt - .flatMap { - case Left(_: FileNotFoundException) => - Task.point(None) - case Right(json) => - Task.point(Some(json)) - case Left(e) => - Task.fail(e) - } - - def createPackage( - subject: String, - repo: String, - package0: String, - licenses: Seq[String], - vcsUrl: String - ): Task[Json] = - clientUtil.get[Json]( - s"$base/packages/$subject/$repo", - post = Some( - clientUtil.postBody( - BintrayApi.CreatePackageRequest(package0, licenses.toList, vcsUrl) - )(BintrayApi.CreatePackageRequest.encoder) - ) - ) - - def createPackageIfNeeded( - subject: String, - repo: String, - package0: String, - licenses: Seq[String], - vcsUrl: String - ): Task[Boolean] = - getPackage(subject, repo, package0).flatMap { - case None => createPackage(subject, repo, package0, licenses, vcsUrl).map(_ => true) - case Some(_) => Task.point(false) - } - -} - -object BintrayApi { - - private final case class CreatePackageRequest( - name: String, - licenses: List[String], - vcs_url: String - ) - - private object CreatePackageRequest { - import argonaut.ArgonautShapeless._ - implicit val encoder = EncodeJson.of[CreatePackageRequest] - } - - private final case class CreateRepositoryRequest( - name: String, - `type`: String - ) - - private object CreateRepositoryRequest { - import argonaut.ArgonautShapeless._ - implicit val encoder = EncodeJson.of[CreateRepositoryRequest] - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/Checksum.scala b/modules/publish/src/main/scala/coursier/publish/checksum/Checksum.scala deleted file mode 100644 index 6973cbf542..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/Checksum.scala +++ /dev/null @@ -1,26 +0,0 @@ -package coursier.publish.checksum - -import java.math.BigInteger -import java.security.MessageDigest - -/** A… checksum - */ -final case class Checksum(`type`: ChecksumType, value: BigInteger) { - assert(`type`.validValue(value)) - def repr: String = - String.format(s"%0${`type`.size}x", value) -} - -object Checksum { - - def compute(`type`: ChecksumType, content: Array[Byte]): Checksum = { - - val md = MessageDigest.getInstance(`type`.name) - md.update(content) - val digest = md.digest() - val calculatedSum = new BigInteger(1, digest) - - Checksum(`type`, calculatedSum) - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/ChecksumType.scala b/modules/publish/src/main/scala/coursier/publish/checksum/ChecksumType.scala deleted file mode 100644 index a548b80fa3..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/ChecksumType.scala +++ /dev/null @@ -1,36 +0,0 @@ -package coursier.publish.checksum - -import java.math.BigInteger -import java.util.Locale - -/** A type of checksum. - * - * @param name - * @param extension: - * extension of this checksum type, without a prefix `"."` - * @param size: - * size of hexadecimal string representation - size in bits is this one times 4 - */ -sealed abstract class ChecksumType( - val name: String, - val extension: String, - val size: Int -) extends Product with Serializable { - private val firstInvalidValue = BigInteger.valueOf(16L).pow(size) - def validValue(value: BigInteger): Boolean = - value.compareTo(BigInteger.ZERO) >= 0 && - value.compareTo(firstInvalidValue) < 0 -} - -object ChecksumType { - case object SHA1 extends ChecksumType("sha-1", "sha1", 40) - case object MD5 extends ChecksumType("md5", "md5", 32) - - val all = Seq(SHA1, MD5) - val map = all.map(c => c.name -> c).toMap - - def parse(s: String): Either[String, ChecksumType] = - map - .get(s.toLowerCase(Locale.ROOT)) - .toRight(s"Unrecognized checksum: $s") -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/Checksums.scala b/modules/publish/src/main/scala/coursier/publish/checksum/Checksums.scala deleted file mode 100644 index 37dabac828..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/Checksums.scala +++ /dev/null @@ -1,124 +0,0 @@ -package coursier.publish.checksum - -import java.nio.charset.StandardCharsets -import java.time.Instant - -import coursier.publish.Content -import coursier.publish.checksum.logger.ChecksumLogger -import coursier.publish.fileset.FileSet -import coursier.util.Task - -object Checksums { - - def clear(types: Seq[ChecksumType], fs: FileSet): FileSet = { - val extensions = types.map("." + _.extension) - FileSet( - fs.elements.filter { - case (p, _) => - p.elements - .lastOption - .forall(n => !extensions.exists(n.endsWith)) - } - ) - } - - /** Compute the missing checksums in a [[FileSet]]. - * - * @param types: - * checksum types to check / compute - * @param fileSet: - * initial [[FileSet]], can optionally contain some already calculated checksum - * @param now: - * last modified time for the added checksum files - * @return - * a [[FileSet]] of the missing checksum files - */ - def apply( - types: Seq[ChecksumType], - fileSet: FileSet, - now: Instant, - logger: => ChecksumLogger - ): Task[FileSet] = { - - // separate base files from existing checksums - val filesOrChecksums = fileSet - .elements - .map { - case (path, content) => - types - .collectFirst { - case t if path.elements.lastOption.exists(_.endsWith("." + t.extension)) => - (path.mapLast(_.stripSuffix("." + t.extension)), t) - } - .toLeft((path, content)) - } - - val checksums = filesOrChecksums - .collect { - case Left(e) => e - } - .toSet - - val files = filesOrChecksums - .collect { - case Right(p) => p - } - - val missing = - for { - type0 <- types - (path, content) <- files - if !checksums((path, type0)) - } yield (type0, path, content) - - // compute missing checksum files - def checksumFilesTask(id: Object, logger0: ChecksumLogger) = Task.gather.gather { - for ((type0, path, content) <- missing) - yield { - val checksumPath = path.mapLast(_ + "." + type0.extension) - val doSign = content.contentTask.map { b => - val checksum = Checksum.compute(type0, b) - (checksumPath, Content.InMemory(now, checksum.repr.getBytes(StandardCharsets.UTF_8))) - } - for { - _ <- Task.delay(logger0.computing(id, type0, checksumPath.repr)) - a <- doSign.attempt - _ <- Task.delay(logger0.computed(id, type0, checksumPath.repr, a.left.toOption)) - res <- Task.fromEither(a) - } yield res - } - }.map { elements => - FileSet(elements) - } - - val missingFs = FileSet( - missing.map { - case (type0, path, content) => - val checksumPath = path.mapLast(_ + "." + type0.extension) - (checksumPath, content) // kind of meh… content isn't used here anyway - } - ) - - val before = Task.delay { - val id = new Object - val logger0 = logger - logger0.start() - logger0.computingSet(id, missingFs) - (id, logger0) - } - - def after(id: Object, logger0: ChecksumLogger) = Task.delay { - logger0.computedSet(id, missingFs) - logger0.stop() - } - - for { - idLogger <- before - (id, logger0) = idLogger - a <- checksumFilesTask(id, logger0).attempt - _ <- after(id, logger0) - res <- Task.fromEither(a) - } yield res - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/logger/BatchChecksumLogger.scala b/modules/publish/src/main/scala/coursier/publish/checksum/logger/BatchChecksumLogger.scala deleted file mode 100644 index ac33a3dcfb..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/logger/BatchChecksumLogger.scala +++ /dev/null @@ -1,20 +0,0 @@ -package coursier.publish.checksum.logger - -import java.io.PrintStream - -import coursier.publish.checksum.ChecksumType - -final class BatchChecksumLogger(out: PrintStream, verbosity: Int) extends ChecksumLogger { - override def computing(id: Object, type0: ChecksumType, path: String): Unit = - if (verbosity >= 0) - out.println(s"Computing ${type0.name} checksum of ${path.repr}") - override def computed( - id: Object, - type0: ChecksumType, - path: String, - errorOpt: Option[Throwable] - ): Unit = { - if (verbosity >= 0) - out.println(s"Computed ${type0.name} checksum of ${path.repr}") - } -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/logger/ChecksumLogger.scala b/modules/publish/src/main/scala/coursier/publish/checksum/logger/ChecksumLogger.scala deleted file mode 100644 index 668c2fa5b6..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/logger/ChecksumLogger.scala +++ /dev/null @@ -1,15 +0,0 @@ -package coursier.publish.checksum.logger - -import coursier.publish.checksum.ChecksumType -import coursier.publish.fileset.FileSet - -trait ChecksumLogger { - def computingSet(id: Object, fs: FileSet): Unit = () - def computing(id: Object, type0: ChecksumType, path: String): Unit = () - def computed(id: Object, type0: ChecksumType, path: String, errorOpt: Option[Throwable]): Unit = - () - def computedSet(id: Object, fs: FileSet): Unit = () - - def start(): Unit = () - def stop(keep: Boolean = true): Unit = () -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/logger/InteractiveChecksumLogger.scala b/modules/publish/src/main/scala/coursier/publish/checksum/logger/InteractiveChecksumLogger.scala deleted file mode 100644 index 3ccecbf1be..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/logger/InteractiveChecksumLogger.scala +++ /dev/null @@ -1,46 +0,0 @@ -package coursier.publish.checksum.logger - -import java.io.{OutputStream, OutputStreamWriter, Writer} - -import coursier.publish.checksum.ChecksumType -import coursier.publish.fileset.FileSet -import coursier.publish.logging.ProgressLogger - -final class InteractiveChecksumLogger(out: Writer, verbosity: Int) extends ChecksumLogger { - - private val underlying = new ProgressLogger[Object]( - "Computed", - "checksums", - out - ) - - override def computingSet(id: Object, fs: FileSet): Unit = - underlying.processingSet(id, Some(fs.elements.length)) - override def computing(id: Object, type0: ChecksumType, path: String): Unit = { - if (verbosity >= 2) - out.write(s"Computing ${type0.name} checksum of ${path.repr}" + System.lineSeparator()) - underlying.processing(path, id) - } - override def computed( - id: Object, - type0: ChecksumType, - path: String, - errorOpt: Option[Throwable] - ): Unit = { - if (verbosity >= 2) - out.write(s"Computed ${type0.name} checksum of ${path.repr}" + System.lineSeparator()) - underlying.processed(path, id, errorOpt.nonEmpty) - } - override def computedSet(id: Object, fs: FileSet): Unit = - underlying.processedSet(id) - - override def start(): Unit = - underlying.start() - override def stop(keep: Boolean): Unit = - underlying.stop(keep) -} - -object InteractiveChecksumLogger { - def create(out: OutputStream, verbosity: Int): InteractiveChecksumLogger = - new InteractiveChecksumLogger(new OutputStreamWriter(out), verbosity) -} diff --git a/modules/publish/src/main/scala/coursier/publish/checksum/package.scala b/modules/publish/src/main/scala/coursier/publish/checksum/package.scala deleted file mode 100644 index 74b1d51186..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/checksum/package.scala +++ /dev/null @@ -1,5 +0,0 @@ -package coursier.publish - -/** All things related to checksums. - */ -package object checksum diff --git a/modules/publish/src/main/scala/coursier/publish/dir/Dir.scala b/modules/publish/src/main/scala/coursier/publish/dir/Dir.scala deleted file mode 100644 index df8479c06a..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/dir/Dir.scala +++ /dev/null @@ -1,133 +0,0 @@ -package coursier.publish.dir - -import java.nio.file.{Files, Path} - -import coursier.publish.fileset.{FileSet, Path => FsPath} -import coursier.publish.Content -import coursier.publish.dir.logger.DirLogger -import coursier.util.Task - -import scala.collection.compat.immutable.LazyList -import scala.jdk.CollectionConverters._ - -object Dir { - - def fileSet(dir: Path, logger: DirLogger): Task[FileSet] = { - - def files(f: Path): LazyList[Path] = - if (Files.isRegularFile(f)) { - logger.element(dir, f) - LazyList(f) - } - else if (Files.isDirectory(f)) { - var s: java.util.stream.Stream[Path] = null - try { - s = Files.list(f) - s.iterator() - .asScala - .toVector - .to(LazyList) - .flatMap(files) - } - finally if (s != null) - s.close() - } - else - // ??? - LazyList.empty - - Task.delay { - val dir0 = dir.normalize().toAbsolutePath - val elems = files(dir0).toVector.map { f => - val p = FsPath(dir0.relativize(f).iterator().asScala.map(_.toString).toVector) - val content = Content.File(f) - (p, content) - } - FileSet(elems) - } - } - - def isRepository(dir: Path): Boolean = { - - def isMetadata(f: Path): Boolean = { - val name = f.getFileName.toString - name.endsWith(".pom") || name.endsWith(".xml") - } - - // Some(false) if this directory or any of its sub-directories: - // - contains files, - // - but none of them looks like metadata (*.pom or *.xml) - // Some(true) if this directory or any of its sub-directories: - // - contains files, - // - and all that do have files that look like metadata (*.pom or *.xml) - // None else (no files found, only directories). - def validate(f: Path): Option[Boolean] = { - - val (dirs, files) = { - var s: java.util.stream.Stream[Path] = null - try { - s = Files.list(f) - s.iterator().asScala.toVector.partition(Files.isDirectory(_)) - } - finally if (s != null) - s.close() - } - - val checkFiles = - if (files.isEmpty) - None - else - Some(files.exists(isMetadata)) - - // there should be a monoid for that… - - checkFiles match { - case Some(false) => checkFiles - case _ => - val checkDirs = - dirs.foldLeft(Option.empty[Boolean]) { - (acc, dir) => - acc match { - case Some(false) => acc - case _ => - validate(dir) match { - case r @ Some(_) => r - case None => acc - } - } - } - - checkDirs match { - case Some(_) => checkDirs - case None => checkFiles - } - } - } - - Files.isDirectory(dir) && { - validate(dir).getOrElse(false) - } - } - - def read(dir: Path, logger: => DirLogger): Task[FileSet] = { - - val before = Task.delay { - val logger0 = logger - logger0.start() - logger0.reading(dir) - logger0 - } - def after(count: Int, logger0: DirLogger) = Task.delay { - logger0.read(dir, count) - logger0.stop() - } - - for { - logger0 <- before - a <- fileSet(dir, logger0).attempt - _ <- after(a.toOption.fold(0)(_.elements.length), logger0) - fs <- Task.fromEither(a) - } yield fs - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/dir/DirContent.scala b/modules/publish/src/main/scala/coursier/publish/dir/DirContent.scala deleted file mode 100644 index 8dd715585f..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/dir/DirContent.scala +++ /dev/null @@ -1,47 +0,0 @@ -package coursier.publish.dir - -import coursier.publish.Content - -final case class DirContent(elements: Seq[(String, Content)]) { - def ++(other: DirContent): DirContent = { - // complexity possibly not too optimal… (removeAll iterates on all elements) - val cleanedUp = other.elements.map(_._1).foldLeft(this)(_.removeAll(_)) - DirContent(cleanedUp.elements ++ other.elements) - } - def filterOutExtension(extension: String): DirContent = { - val suffix = "." + extension - DirContent(elements.filter(!_._1.endsWith(suffix))) - } - def isEmpty: Boolean = - elements.isEmpty - def remove(name: String): DirContent = - copy( - elements = elements.filter { - case (n, _) => n != name && !n.startsWith(name + ".") - } - ) - - /** Removes anything looking like a checksum or signature related to `path` */ - def removeAll(name: String): DirContent = { - - val prefix = name + "." - val (remove, keep) = elements.partition { - case (n, _) => - n == name || n.startsWith(prefix) - } - - if (remove.isEmpty) - this - else - DirContent(keep) - } - - def update(name: String, content: Content): DirContent = - ++(DirContent(Seq(name -> content))) -} - -object DirContent { - - val empty = DirContent(Nil) - -} diff --git a/modules/publish/src/main/scala/coursier/publish/dir/logger/BatchDirLogger.scala b/modules/publish/src/main/scala/coursier/publish/dir/logger/BatchDirLogger.scala deleted file mode 100644 index 38cdc8efad..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/dir/logger/BatchDirLogger.scala +++ /dev/null @@ -1,17 +0,0 @@ -package coursier.publish.dir.logger - -import java.io.PrintStream -import java.nio.file.Path - -final class BatchDirLogger(out: PrintStream, dirName: String, verbosity: Int) extends DirLogger { - - override def reading(dir: Path): Unit = - if (verbosity >= 0) - out.println(s"Reading $dirName") - override def element(dir: Path, file: Path): Unit = - if (verbosity >= 0) - out.println(s"Found $file") - override def read(dir: Path, elements: Int): Unit = - if (verbosity >= 0) - out.println(s"Found $elements elements in $dirName") -} diff --git a/modules/publish/src/main/scala/coursier/publish/dir/logger/DirLogger.scala b/modules/publish/src/main/scala/coursier/publish/dir/logger/DirLogger.scala deleted file mode 100644 index 41f7a8e157..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/dir/logger/DirLogger.scala +++ /dev/null @@ -1,13 +0,0 @@ -package coursier.publish.dir.logger - -import java.nio.file.Path - -trait DirLogger { - // dir should be removed… - def reading(dir: Path): Unit = () - def element(dir: Path, file: Path): Unit = () - def read(dir: Path, elements: Int): Unit = () - - def start(): Unit = () - def stop(keep: Boolean = true): Unit = () -} diff --git a/modules/publish/src/main/scala/coursier/publish/dir/logger/InteractiveDirLogger.scala b/modules/publish/src/main/scala/coursier/publish/dir/logger/InteractiveDirLogger.scala deleted file mode 100644 index aaab64d8fa..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/dir/logger/InteractiveDirLogger.scala +++ /dev/null @@ -1,36 +0,0 @@ -package coursier.publish.dir.logger - -import java.io.{OutputStream, OutputStreamWriter} -import java.nio.file.Path - -import coursier.publish.logging.ProgressLogger - -final class InteractiveDirLogger(out: OutputStreamWriter, dirName: String, verbosity: Int) - extends DirLogger { - - private val underlying = new ProgressLogger[String]( - "Read", - s"files from $dirName", - out, - doneEmoji = Some("\ud83d\udd0d") - ) - - override def reading(dir: Path): Unit = - underlying.processingSet(dirName, None) - override def element(dir: Path, file: Path): Unit = { - underlying.processing(file.toString, dirName) - underlying.processed(file.toString, dirName, false) - } - override def read(dir: Path, elements: Int): Unit = - underlying.processedSet(dirName) - - override def start(): Unit = - underlying.start() - override def stop(keep: Boolean): Unit = - underlying.stop(keep) -} - -object InteractiveDirLogger { - def create(out: OutputStream, dirName: String, verbosity: Int): DirLogger = - new InteractiveDirLogger(new OutputStreamWriter(out), dirName, verbosity) -} diff --git a/modules/publish/src/main/scala/coursier/publish/download/Download.scala b/modules/publish/src/main/scala/coursier/publish/download/Download.scala deleted file mode 100644 index f8bfefbda5..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/download/Download.scala +++ /dev/null @@ -1,40 +0,0 @@ -package coursier.publish.download - -import java.time.Instant - -import coursier.core.Authentication -import coursier.publish.download.logger.DownloadLogger -import coursier.util.Task - -trait Download { - def downloadIfExists( - url: String, - authentication: Option[Authentication], - logger: DownloadLogger - ): Task[Option[(Option[Instant], Array[Byte])]] -} - -object Download { - - sealed abstract class Error(val transient: Boolean, message: String, cause: Throwable = null) - extends Exception(message, cause) - - object Error { - final class HttpError( - url: String, - code: Int, - headers: Map[String, Seq[String]], - response: String - ) extends Error(transient = code / 100 == 5, s"$url: HTTP $code\n$response") - final class Unauthorized(url: String, realm: Option[String]) - extends Error(transient = false, s"Unauthorized ($url, ${realm.getOrElse("[no realm]")})") - final class DownloadError(url: String, exception: Throwable) - extends Error(transient = false, s"Download error for $url", exception) - final class FileException(exception: Throwable) extends Error( - transient = false, - "I/O error", - exception - ) // can some exceptions be transient? - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/download/FileDownload.scala b/modules/publish/src/main/scala/coursier/publish/download/FileDownload.scala deleted file mode 100644 index 869eb511cc..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/download/FileDownload.scala +++ /dev/null @@ -1,49 +0,0 @@ -package coursier.publish.download - -import java.nio.file.{Files, Path} -import java.time.Instant - -import coursier.core.Authentication -import coursier.publish.download.logger.DownloadLogger -import coursier.util.Task - -import scala.util.control.NonFatal - -/** Copies - * @param base - */ -final case class FileDownload(base: Path) extends Download { - private val base0 = base.normalize() - def downloadIfExists( - url: String, - authentication: Option[Authentication], - logger: DownloadLogger - ): Task[Option[(Option[Instant], Array[Byte])]] = { - - val p = base0.resolve(url).normalize() - if (p.startsWith(base0)) - Task.delay { - logger.downloadingIfExists(url) - val res = - try if (Files.isRegularFile(p)) { - val lastModified = Files.getLastModifiedTime(p).toInstant - Right(Some((Some(lastModified), Files.readAllBytes(p)))) - } - else - Right(None) - catch { - case NonFatal(e) => - Left(e) - } - logger.downloadedIfExists( - url, - res.toOption.flatMap(_.map(_._2.length)), - res.left.toOption.map(e => new Download.Error.FileException(e)) - ) - - Task.fromEither(res) - }.flatMap(identity) - else - Task.fail(new Exception(s"Invalid path: $url (base: $base0, p: $p)")) - } -} diff --git a/modules/publish/src/main/scala/coursier/publish/download/OkhttpDownload.scala b/modules/publish/src/main/scala/coursier/publish/download/OkhttpDownload.scala deleted file mode 100644 index 4ae574d6b7..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/download/OkhttpDownload.scala +++ /dev/null @@ -1,99 +0,0 @@ -package coursier.publish.download - -import java.time.Instant -import java.util.concurrent.ExecutorService - -import coursier.core.Authentication -import coursier.publish.download.logger.DownloadLogger -import coursier.util.Task -import okhttp3.internal.http.HttpDate -import okhttp3.{OkHttpClient, Request, Response} - -import scala.jdk.CollectionConverters._ -import scala.util.{Failure, Success, Try} - -final case class OkhttpDownload(client: OkHttpClient, pool: ExecutorService) extends Download { - - import OkhttpDownload.TryOps - - def downloadIfExists( - url: String, - authentication: Option[Authentication], - logger: DownloadLogger - ): Task[Option[(Option[Instant], Array[Byte])]] = { - - // FIXME Some duplication with upload below… - - val request = { - val b = new Request.Builder() - .url(url) - .get() - - // Handling this ourselves rather than via client.setAuthenticator / com.squareup.okhttp.Authenticator - for (auth <- authentication; (k, v) <- auth.allHttpHeaders) - b.addHeader(k, v) - - b.build() - } - - Task.schedule(pool) { - logger.downloadingIfExists(url) - - val res = Try { - var response: Response = null - - try { - response = client.newCall(request).execute() - - if (response.isSuccessful) { - val lastModifiedOpt = Option(response.header("Last-Modified")).map { s => - HttpDate.parse(s).toInstant - } - Right(Some((lastModifiedOpt, response.body().bytes()))) - } - else { - val code = response.code() - if (code / 100 == 4) - Right(None) - else { - val content = Try(response.body().string()).getOrElse("") - Left(new Download.Error.HttpError( - url, - code, - response.headers().toMultimap.asScala.mapValues(_.asScala.toList).iterator.toMap, - content - )) - } - } - } - finally if (response != null) - response.body().close() - }.toEither.flatMap(identity) - - logger.downloadedIfExists( - url, - res.toOption.flatMap(_.map(_._2.length)), - res.left.toOption.map(e => new Download.Error.DownloadError(url, e)) - ) - - Task.fromEither(res) - }.flatMap(identity) - } - -} - -object OkhttpDownload { - - // for 2.11 - private[publish] implicit class TryOps[T](private val t: Try[T]) { - def toEither: Either[Throwable, T] = - t match { - case Success(t) => Right(t) - case Failure(e) => Left(e) - } - } - - def create(pool: ExecutorService): Download = - // Seems we can't even create / shutdown the client thread pool (via its Dispatcher)… - OkhttpDownload(new OkHttpClient, pool) -} diff --git a/modules/publish/src/main/scala/coursier/publish/download/logger/DownloadLogger.scala b/modules/publish/src/main/scala/coursier/publish/download/logger/DownloadLogger.scala deleted file mode 100644 index cace8432a6..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/download/logger/DownloadLogger.scala +++ /dev/null @@ -1,11 +0,0 @@ -package coursier.publish.download.logger - -trait DownloadLogger { - - def checking(url: String): Unit = () - def checked(url: String, exists: Boolean, errorOpt: Option[Throwable]): Unit = () - - def downloadingIfExists(url: String): Unit = () - def downloadedIfExists(url: String, size: Option[Long], errorOpt: Option[Throwable]): Unit = () - -} diff --git a/modules/publish/src/main/scala/coursier/publish/download/logger/SimpleDownloadLogger.scala b/modules/publish/src/main/scala/coursier/publish/download/logger/SimpleDownloadLogger.scala deleted file mode 100644 index e3780fabea..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/download/logger/SimpleDownloadLogger.scala +++ /dev/null @@ -1,31 +0,0 @@ -package coursier.publish.download.logger - -import java.io.PrintStream - -final class SimpleDownloadLogger(out: PrintStream, verbosity: Int) extends DownloadLogger { - - override def downloadingIfExists(url: String): Unit = { - if (verbosity >= 2) - out.println(s"Trying to download $url") - } - - override def downloadedIfExists( - url: String, - size: Option[Long], - errorOpt: Option[Throwable] - ): Unit = - if (verbosity >= 2) { - val msg = - if (size.isEmpty) - s"Not found : $url (ignored)" - else if (errorOpt.isEmpty) - s"Downloaded $url" - else - s"Failed to download $url" - out.println(msg) - } - else if (verbosity >= 1) - if (size.nonEmpty) - out.println(s"Downloaded $url") - -} diff --git a/modules/publish/src/main/scala/coursier/publish/fileset/FileSet.scala b/modules/publish/src/main/scala/coursier/publish/fileset/FileSet.scala deleted file mode 100644 index 037c578a3a..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/fileset/FileSet.scala +++ /dev/null @@ -1,203 +0,0 @@ -package coursier.publish.fileset - -import java.time.Instant - -import coursier.publish.Content -import coursier.core.{ModuleName, Organization} -import coursier.publish.Pom.{Developer, License} -import coursier.util.Task - -import scala.collection.compat._ - -final case class FileSet(elements: Seq[(Path, Content)]) { - def ++(other: FileSet): FileSet = { - // complexity possibly not too optimal… (removeAll iterates on all elements) - val cleanedUp = other.elements.map(_._1).foldLeft(this)(_.removeAll(_)) - FileSet(cleanedUp.elements ++ other.elements) - } - def filterOutExtension(extension: String): FileSet = { - val suffix = "." + extension - FileSet(elements.filter(_._1.elements.lastOption.forall(!_.endsWith(suffix)))) - } - def isEmpty: Boolean = - elements.isEmpty - - /** Removes anything looking like a checksum or signature related to `path` */ - def removeAll(path: Path): FileSet = { - - val prefix = path.repr + "." - val (remove, keep) = elements.partition { - case (p, _) => - p == path || p.repr.startsWith(prefix) - } - - if (remove.isEmpty) - this - else - FileSet(keep) - } - - def update(path: Path, content: Content): FileSet = - ++(FileSet(Seq(path -> content))) - - def updateMetadata( - org: Option[Organization], - name: Option[ModuleName], - version: Option[String], - licenses: Option[Seq[License]], - developers: Option[Seq[Developer]], - homePage: Option[String], - gitDomainPath: Option[(String, String)], - distMgmtRepo: Option[(String, String, String)], - now: Instant - ): Task[FileSet] = { - - val split = Group.split(this) - - val adjustOrgName = - if (org.isEmpty && name.isEmpty) - Task.point(split) - else { - val map = split.map { - case m: Group.Module => - (m.organization, m.name) -> (org.getOrElse(m.organization), name.getOrElse(m.name)) - case m: Group.MavenMetadata => - (m.organization, m.name) -> (org.getOrElse(m.organization), name.getOrElse(m.name)) - }.toMap - - Task.gather.gather { - split.map { m => - m.transform(map, now) - } - } - } - - val adjustVersion: Task[Seq[Group]] = - version match { - case Some(ver) => - adjustOrgName.flatMap { groups => - val map = groups - .collect { - case m: Group.Module => (m.organization, m.name) -> (m.version -> ver) - } - .toMap - Task.sync.gather { - groups.map { group => - group.transformVersion(map, now) - } - } - } - case None => - adjustOrgName - } - - adjustVersion.flatMap { l => - Task.gather.gather { - l.map { - case m: Group.Module => - m.updateMetadata( - org, - name, - version, - licenses, - developers, - homePage, - gitDomainPath, - distMgmtRepo, - now - ) - case m: Group.MavenMetadata => - m.updateContent( - org, - name, - version, - version.filter(!_.endsWith("SNAPSHOT")), - version.toSeq, - now - ) - } - } - }.flatMap { groups => - Group.merge(groups) match { - case Left(e) => Task.fail(new Exception(e)) - case Right(fs) => Task.point(fs) - } - } - } - - def order: Task[FileSet] = { - - val split = Group.split(this) - - def order(m: Map[Group.Module, Seq[coursier.core.Module]]): Stream[Group.Module] = - if (m.isEmpty) - Stream.empty - else { - - val (now, later) = m.partition(_._2.isEmpty) - - if (now.isEmpty) - // FIXME Report that properly - throw new Exception(s"Found cycle in input modules\n$m") - - val prefix = now - .keys - .toVector - .sortBy(_.module.toString) // sort to make output deterministic - .toStream - - val done = now.keySet.map(_.module) - - val later0 = later.mapValues(_.filterNot(done)).iterator.toMap - - prefix #::: order(later0) - } - - val sortedModulesTask = Task.gather - .gather { - split.collect { - case m: Group.Module => - m.dependenciesOpt.map((m, _)) - } - } - .map { l => - val m = l.toMap - val current = m.keySet.map(_.module) - val interDependencies = m.mapValues(_.filter(current)).iterator.toMap - order(interDependencies).toVector - } - - val mavenMetadataMap = split - .collect { - case m: Group.MavenMetadata => - m.module -> m - } - .toMap // shouldn't discard values… assert it? - - sortedModulesTask.map { sortedModules => - - val modules = sortedModules.map(_.module).toSet - - val unknownMavenMetadata = mavenMetadataMap - .view - .filterKeys(!modules(_)) - .map(_._2) - .toVector - .sortBy(_.module.toString) // sort to make output deterministic - - val modulesWithMavenMetadata = sortedModules.flatMap { m => - m +: mavenMetadataMap.get(m.module).toSeq - } - - val sortedGroups = (modulesWithMavenMetadata ++ unknownMavenMetadata) - .map(_.ordered) - Group.mergeUnsafe(sortedGroups) - } - } -} - -object FileSet { - - val empty = FileSet(Nil) - -} diff --git a/modules/publish/src/main/scala/coursier/publish/fileset/Group.scala b/modules/publish/src/main/scala/coursier/publish/fileset/Group.scala deleted file mode 100644 index 77f4494bca..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/fileset/Group.scala +++ /dev/null @@ -1,875 +0,0 @@ -package coursier.publish.fileset - -import java.nio.charset.StandardCharsets -import java.time.{Instant, ZoneOffset} - -import coursier.publish -import coursier.publish.{Content, Pom} -import coursier.publish.dir.DirContent -import coursier.core.{ModuleName, Organization, Version} -import coursier.maven.MavenRepository -import coursier.publish.Pom.{Developer, License} -import coursier.publish.download.Download -import coursier.publish.download.logger.DownloadLogger -import coursier.util.Task - -import scala.xml.{Elem, XML} - -/** A subset of a [[FileSet]], with particular semantic. - */ -sealed abstract class Group extends Product with Serializable { - - /** [[FileSet]] corresponding to this [[Group]] - */ - def fileSet: FileSet - - def organization: Organization - - /** Changes any reference to the first coordinates to the second ones. - * - * Coordinates can be our coordinates, or those of dependencies, exclusions, … - */ - def transform( - map: Map[(Organization, ModuleName), (Organization, ModuleName)], - now: Instant - ): Task[Group] - - def transformVersion( - map: Map[(Organization, ModuleName), (String, String)], - now: Instant - ): Task[Group] - - /** Ensure the files of this [[Group]] are ordered (POMs last for [[Group.Module]], etc.) */ - def ordered: Group -} - -object Group { - - /** Subset of a [[FileSet]] corresponding to a particular module. - * - * That is to the files of a particular - published - version of a given module. - */ - final case class Module( - organization: Organization, - name: ModuleName, - version: String, - snapshotVersioning: Option[String], - files: DirContent - ) extends Group { - - def module: coursier.core.Module = - coursier.core.Module(organization, name, Map.empty) - - def baseDir: Seq[String] = - organization.value.split('.').toSeq ++ Seq(name.value, version) - - def fileSet: FileSet = { - val dirPath = Path(baseDir) - FileSet( - files.elements.map { - case (n, c) => - (dirPath / n) -> c - } - ) - } - - private def stripPrefixes: Module = { - - val prefix = s"${name.value}-${snapshotVersioning.getOrElse(version)}" - - val updatedContent = DirContent( - files.elements.map { - case (n, c) => - val newName = - if (n == "maven-metadata.xml" || n.startsWith("maven-metadata.xml.")) - n - else { - assert(n.startsWith(prefix), s"nope for $n.startsWith($prefix)") - n.stripPrefix(prefix) - } - (newName, c) - } - ) - - copy(files = updatedContent) - } - - private def updateFileNames: Module = { - - val newPrefix = s"${name.value}-${snapshotVersioning.getOrElse(version)}" - - val updatedContent = DirContent( - files.elements.collect { - case (n, c) => - val newName = - if (n == "maven-metadata.xml" || n.startsWith("maven-metadata.xml.")) - n - else - s"$newPrefix$n" - (newName, c) - } - ) - - copy(files = updatedContent) - } - - private def updateOrgNameVer( - org: Option[Organization], - name: Option[ModuleName], - version: Option[String] - ): Module = { - - val base = - version match { - case Some(v) if !v.endsWith("SNAPSHOT") => - clearSnapshotVersioning - case _ => - this - } - - base - .stripPrefixes - .copy( - organization = org.getOrElse(base.organization), - name = name.getOrElse(base.name), - version = version.getOrElse(base.version) - ) - .updateFileNames - } - - /** Adjust the organization / name / version. - * - * Possibly changing those in POM or maven-metadata.xml files. - */ - def updateMetadata( - org: Option[Organization], - name: Option[ModuleName], - version: Option[String], - licenses: Option[Seq[License]], - developers: Option[Seq[Developer]], - homePage: Option[String], - gitDomainPath: Option[(String, String)], - distMgmtRepo: Option[(String, String, String)], - now: Instant - ): Task[Module] = - if ( - org.isEmpty && name.isEmpty && version.isEmpty && licenses.isEmpty && developers.isEmpty && homePage.isEmpty && gitDomainPath.isEmpty - ) - Task.point(this) - else - updateOrgNameVer(org, name, version) - .updatePom(now, licenses, developers, homePage, gitDomainPath, distMgmtRepo) - .flatMap(_.updateMavenMetadata(now)) - - def removeMavenMetadata: Module = - copy( - files = files.remove("maven-metadata.xml") - ) - - def clearSnapshotVersioning: Module = - if (snapshotVersioning.isEmpty) - this - else - stripPrefixes - .removeMavenMetadata - .copy(snapshotVersioning = None) - .updateFileNames - - def transform( - map: Map[(Organization, ModuleName), (Organization, ModuleName)], - now: Instant - ): Task[Module] = { - - val base = map.get((organization, name)) match { - case None => Task.point(this) - case Some(to) => - updateMetadata(Some(to._1), Some(to._2), None, None, None, None, None, None, now) - } - - base.flatMap { m => - m.transformPom(now) { elem => - map.foldLeft(elem) { - case (acc, (from, to)) => - Pom.transformDependency(acc, from, to) - } - } - } - } - - def transformVersion( - map: Map[(Organization, ModuleName), (String, String)], - now: Instant - ): Task[Module] = - transformPom(now) { elem => - map.foldLeft(elem) { - case (acc, ((org, name), (fromVer, toVer))) => - Pom.transformDependencyVersion(acc, org, name, fromVer, toVer) - } - } - - private def pomFileName: String = - s"${name.value}-${snapshotVersioning.getOrElse(version)}.pom" - - /** The POM file of this [[Module]], if any. - */ - def pomOpt: Option[(String, Content)] = { - val fileName = pomFileName - files - .elements - .collectFirst { - case (`fileName`, c) => - (fileName, c) - } - } - - def dependenciesOpt: Task[Seq[coursier.core.Module]] = - pomOpt match { - case None => Task.point(Nil) - case Some((_, content)) => - content.contentTask.flatMap { b => - val s = new String(b, StandardCharsets.UTF_8) - coursier.maven.MavenRepository.parseRawPomSax(s) match { - case Left(e) => - Task.fail(new Exception(s"Error parsing POM: $e")) - case Right(proj) => - Task.point(proj.dependencies.map(_._2.module)) - } - } - } - - /** Adjust the POM of this [[Module]], so that it contains the same org / name / version as this - * [[Module]]. - * - * Calling this method, or running its [[Task]], doesn't write anything on disk. The new POM - * stays in memory (via [[Content.InMemory]]). The returned [[Module]] only lives in memory. - * The only effect here is possibly reading stuff on disk. - * - * @param now: - * if the POM is edited, its last modified time. - */ - def updatePom( - now: Instant, - licenses: Option[Seq[License]], - developers: Option[Seq[Developer]], - homePage: Option[String], - gitDomainPath: Option[(String, String)], - distMgmtRepo: Option[(String, String, String)] - ): Task[Module] = - transformPom(now) { elem => - var elem0 = elem - elem0 = Pom.overrideOrganization(organization, elem0) - elem0 = Pom.overrideModuleName(name, elem0) - elem0 = Pom.overrideVersion(version, elem0) - for (l <- licenses) - elem0 = Pom.overrideLicenses(l, elem0) - for (l <- developers) - elem0 = Pom.overrideDevelopers(l, elem0) - for (h <- homePage) - elem0 = Pom.overrideHomepage(h, elem0) - for ((domain, path) <- gitDomainPath) - elem0 = Pom.overrideScm(domain, path, elem0) - for ((id, name, url) <- distMgmtRepo) - elem0 = Pom.overrideDistributionManagementRepository(id, name, url, elem0) - elem0 - } - - def transformPom(now: Instant)(f: Elem => Elem): Task[Module] = - pomOpt match { - case None => - Task.fail( - new Exception(s"No POM found (files: ${files.elements.map(_._1).mkString(", ")})") - ) - case Some((fileName, c)) => - c.contentTask.map { pomBytes => - val elem = f(XML.loadString(new String(pomBytes, StandardCharsets.UTF_8))) - - val pomContent0 = - Content.InMemory(now, Pom.print(elem).getBytes(StandardCharsets.UTF_8)) - - val updatedContent = files.update(fileName, pomContent0) - copy(files = updatedContent) - } - } - - /** Adds a maven-metadata.xml file to this module if it doesn't have one already. - * @param now: - * last modified time of the added maven-metadata.xml, if one is indeed added. - */ - def addMavenMetadata(now: Instant): Module = { - - val mavenMetadataFound = files - .elements - .exists(_._1 == "maven-metadata.xml") - - if (mavenMetadataFound) - this - else { - val updatedContent = { - val b = { - val content = coursier.publish.MavenMetadata.create( - organization, - name, - None, - None, - Nil, - now - ) - coursier.publish.MavenMetadata.print(content).getBytes(StandardCharsets.UTF_8) - } - files.update("maven-metadata.xml", Content.InMemory(now, b)) - } - - copy(files = updatedContent) - } - } - - def mavenMetadataContentOpt = files - .elements - .find(_._1 == "maven-metadata.xml") - .map(_._2) - - /** Updates the maven-metadata.xml file of this [[Module]], so that it contains the same org / - * name. - * @param now: - * if maven-metadata.xml is edited, its last modified time. - */ - def updateMavenMetadata(now: Instant): Task[Module] = - mavenMetadataContentOpt match { - case None => - Task.point(this) - - case Some(content) => - content.contentTask.map { b => - - val updatedMetadataBytes = { - val elem = XML.loadString(new String(b, StandardCharsets.UTF_8)) - val newContent = coursier.publish.MavenMetadata.update( - elem, - Some(organization), - Some(name), - None, - None, - Nil, - Some(now.atOffset(ZoneOffset.UTC).toLocalDateTime) - ) - coursier.publish.MavenMetadata.print(newContent).getBytes(StandardCharsets.UTF_8) - } - - val updatedContent = - files.update("maven-metadata.xml", Content.InMemory(now, updatedMetadataBytes)) - copy(files = updatedContent) - } - } - - def addSnapshotVersioning(now: Instant, ignoreExtensions: Set[String]): Task[Module] = { - - assert(version.endsWith("-SNAPSHOT") || version.endsWith(".SNAPSHOT")) - - val versionPrefix = version.stripSuffix("SNAPSHOT").dropRight(1) - - val initialFilePrefix = s"${name.value}-${snapshotVersioning.getOrElse(version)}" - - def updatedVersion(buildNumber: Int) = - s"$versionPrefix-${now.atOffset(ZoneOffset.UTC).toLocalDateTime.format(publish.MavenMetadata.timestampPattern)}-$buildNumber" - - def artifacts(buildNumber: Int) = { - val updatedVersion0 = updatedVersion(buildNumber) - files.elements.collect { - case (n, _) if n.startsWith(initialFilePrefix + ".") => - if (ignoreExtensions.exists(e => n.endsWith("." + e))) - Nil - else - Seq(( - None, - n.stripPrefix(initialFilePrefix + "."), - updatedVersion0, - now.atOffset(ZoneOffset.UTC).toLocalDateTime - )) - case (n, _) if n.startsWith(initialFilePrefix + "-") => - val suffix = n.stripPrefix(initialFilePrefix + "-") - val idx = suffix.indexOf('.') - if (idx < 0) - ??? - else if (ignoreExtensions.exists(e => n.endsWith("." + e))) - Nil - else { - val classifier = suffix.take(idx) - val ext = suffix.drop(idx + 1) - Seq(( - Some(classifier), - ext, - updatedVersion0, - now.atOffset(ZoneOffset.UTC).toLocalDateTime - )) - } - case (n, _) if n.startsWith("maven-metadata.xml.") => - Nil - case ("maven-metadata.xml", _) => - Nil - case (other, _) => - // unrecognized file… - ??? - }.flatten - } - - def files0(buildNumber: Int) = { - val updatedVersion0 = updatedVersion(buildNumber) - val updatedFilePrefix = s"${name.value}-$updatedVersion0" - DirContent( - files.elements.map { - case (n, c) - if n.startsWith(initialFilePrefix + ".") || n.startsWith(initialFilePrefix + "-") => - (updatedFilePrefix + n.stripPrefix(initialFilePrefix), c) - case t => - t - } - ) - } - - val content = mavenMetadataContentOpt match { - case None => - Task.point { - val buildNumber = 1 - buildNumber -> publish.MavenMetadata.createSnapshotVersioning( - organization, - name, - version, - (now.atOffset(ZoneOffset.UTC).toLocalDateTime, buildNumber), - now, - artifacts(buildNumber) - ) - } - case Some(c) => - c.contentTask.map { b => - val elem = XML.loadString(new String(b, StandardCharsets.UTF_8)) - val latestSnapshotParams = - publish.MavenMetadata.currentSnapshotVersioning(elem).getOrElse { - ??? - } - val latestSnapshotVer = - s"$versionPrefix-${latestSnapshotParams._2.atOffset(ZoneOffset.UTC).toLocalDateTime.format(publish.MavenMetadata.timestampPattern)}-${latestSnapshotParams._1}" - if (snapshotVersioning.contains(latestSnapshotVer)) - latestSnapshotParams._1 -> elem // kind of meh, this is in case the source already has snapshot ver, and the dest hasn't, so the current maven metadata only comes from the source - else { - val buildNumber = latestSnapshotParams._1 + 1 - buildNumber -> publish.MavenMetadata.updateSnapshotVersioning( - elem, - None, - None, - Some(version), - Some((now.atOffset(ZoneOffset.UTC).toLocalDateTime, buildNumber)), - Some(now.atZone(ZoneOffset.UTC).toLocalDateTime), - artifacts(buildNumber) - ) - } - } - } - - content.map { - case (buildNumber, elem) => - val b = publish.MavenMetadata.print(elem).getBytes(StandardCharsets.UTF_8) - val files1 = files0(buildNumber).update("maven-metadata.xml", Content.InMemory(now, b)) - copy( - snapshotVersioning = Some(updatedVersion(buildNumber)), - files = files1 - ) - } - } - - def ordered: Module = { - - // POM file last - // checksum before underlying file - // signatures before underlying file - - val pomFileName0 = pomFileName - val (pomFiles, other) = files.elements.partition { - case (n, _) => - n == pomFileName0 || n.startsWith(pomFileName0 + ".") - } - val sortedFiles = DirContent((pomFiles.sortBy(_._1) ++ other.sortBy(_._1)).reverse) - copy(files = sortedFiles) - } - - } - - /** Subset of a [[FileSet]] corresponding to maven-metadata.xml files. - * - * This correspond to the maven-metadata.xml file under org/name/maven-metadata.xml, not the ones - * that can be found under org/name/version/maven-metadata.xml (these are in [[Module]]). - */ - final case class MavenMetadata( - organization: Organization, - name: ModuleName, - files: DirContent - ) extends Group { - - def module: coursier.core.Module = - coursier.core.Module(organization, name, Map.empty) - - def fileSet: FileSet = { - val dirPath = Path(organization.value.split('.').toSeq ++ Seq(name.value)) - FileSet( - files.elements.map { - case (n, c) => - (dirPath / n) -> c - } - ) - } - - def xmlOpt: Option[Content] = { - val fileName = "maven-metadata.xml" - files - .elements - .collectFirst { - case (`fileName`, c) => - c - } - } - - def updateContent( - org: Option[Organization], - name: Option[ModuleName], - latest: Option[String], - release: Option[String], - addVersions: Seq[String], - now: Instant - ): Task[MavenMetadata] = - xmlOpt match { - case None => - Task.point(this) - case Some(c) => - c.contentTask.map { b => - val elem = XML.loadString(new String(b, StandardCharsets.UTF_8)) - val updated = coursier.publish.MavenMetadata.update( - elem, - org, - name, - latest, - release, - addVersions, - Some(now.atOffset(ZoneOffset.UTC).toLocalDateTime) - ) - val b0 = coursier.publish.MavenMetadata.print(updated) - .getBytes(StandardCharsets.UTF_8) - val c0 = Content.InMemory(now, b0) - copy( - files = files.update("maven-metadata.xml", c0) - ) - } - } - - def transform( - map: Map[(Organization, ModuleName), (Organization, ModuleName)], - now: Instant - ): Task[MavenMetadata] = - map.get((organization, name)) match { - case Some(to) if to != (organization, name) => - updateContent( - Some(to._1).filter(_ != organization), - Some(to._2).filter(_ != name), - None, - None, - Nil, - now - ).map { m => - m.copy( - organization = to._1, - name = to._2 - ) - } - case _ => - Task.point(this) - } - - def transformVersion( - map: Map[(Organization, ModuleName), (String, String)], - now: Instant - ): Task[MavenMetadata] = - Task.point(this) - - def ordered: MavenMetadata = { - // reverse alphabetical order should be enough here (will put checksums and signatures before underlying files) - val sortedFiles = DirContent(files.elements.sortBy(_._1).reverse) - copy(files = sortedFiles) - } - - } - - /** Identify the [[Group]] s each file of the passed [[FileSet]] correspond to. - */ - def split(fs: FileSet): Seq[Group] = { - - val byDir = fs.elements.groupBy(_._1.dropLast) - - // FIXME Plenty of unhandled errors here - - byDir.toSeq.map { - case (dir, elements) => - val canBeMavenMetadata = - elements.exists(_._1.elements.lastOption.contains("maven-metadata.xml")) && - !elements.exists(_._1.elements.lastOption.exists(_.endsWith(".pom"))) - - dir.elements.reverse match { - case Seq(ver, strName, reverseOrg @ _*) if reverseOrg.nonEmpty && !canBeMavenMetadata => - val org = Organization(reverseOrg.reverse.mkString(".")) - val name = ModuleName(strName) - val snapshotVersioningOpt = - if (ver.endsWith("SNAPSHOT")) - Some(elements.map(_._1.elements.last).filter(_.endsWith(".pom"))) - .filter(_.nonEmpty) - .map(_.minBy(_.length)) - .filter(_.startsWith(s"${name.value}-")) - .map(_.stripPrefix(s"${name.value}-").stripSuffix(".pom")) - .filter(_ != ver) - else - None - val fileNamePrefixes = { - val p = s"${name.value}-${snapshotVersioningOpt.getOrElse(ver)}" - Set(".", "-").map(p + _) - } - - def recognized(p: Path): Boolean = - p.elements.lastOption.exists(n => fileNamePrefixes.exists(n.startsWith)) || - p.elements.lastOption.contains("maven-metadata.xml") || - p.elements.lastOption.exists(_.startsWith("maven-metadata.xml.")) - - if (elements.forall(t => recognized(t._1))) { - val strippedDir = elements.map { - case (p, c) => - p.elements.last -> c - } - Module(org, name, ver, snapshotVersioningOpt, DirContent(strippedDir)) - } - else - throw new Exception( - s"Unrecognized files: ${elements.filter(t => !recognized(t._1)).map(_._1.repr).mkString(", ")}" - ) - - case Seq(strName, reverseOrg @ _*) if reverseOrg.nonEmpty && canBeMavenMetadata => - val org = Organization(reverseOrg.reverse.mkString(".")) - val name = ModuleName(strName) - - def recognized(p: Path): Boolean = - p.elements.lastOption.contains("maven-metadata.xml") || - p.elements.lastOption.exists(_.startsWith("maven-metadata.xml.")) - - if (elements.forall(t => recognized(t._1))) { - val strippedDir = elements.map { - case (p, c) => - p.elements.last -> c - } - MavenMetadata(org, name, DirContent(strippedDir)) - } - else - sys.error( - s"Unrecognized: ${dir.elements} (${elements.filter(t => !recognized(t._1))})" - ) - - case _ => - ??? - } - } - } - - /** Merge [[Group]] s as a [[FileSet]]. - * - * Can be "left" if some duplicated [[Module]] s or [[MavenMetadata]] s are found. - */ - def merge(groups: Seq[Group]): Either[String, FileSet] = { - - val duplicatedModules = groups - .collect { case m: Module => m } - .groupBy(m => (m.organization, m.name, m.version)) - .filter(_._2.lengthCompare(1) > 0) - .iterator - .toMap - - val duplicatedMeta = groups - .collect { case m: MavenMetadata => m } - .groupBy(m => (m.organization, m.name)) - .filter(_._2.lengthCompare(1) > 0) - .iterator - .toMap - - if (duplicatedModules.isEmpty && duplicatedMeta.isEmpty) - Right(groups.foldLeft(FileSet.empty)(_ ++ _.fileSet)) - else - ??? - } - - private[coursier] def mergeUnsafe(groups: Seq[Group]): FileSet = - FileSet(groups.flatMap(_.fileSet.elements)) - - /** Ensure all [[Module]] s in the passed `groups` have a corresponding [[MavenMetadata]] group. - * - * @param now: - * if new files are created, their last-modified time. - */ - def addOrUpdateMavenMetadata(groups: Seq[Group], now: Instant): Task[Seq[Group]] = { - - val modules = groups - .collect { case m: Group.Module => m } - .groupBy(m => (m.organization, m.name)) - val meta = groups - .collect { case m: Group.MavenMetadata => m } - .groupBy(m => (m.organization, m.name)) - .mapValues { - case Seq(md) => md - case l => ??? - } - .iterator - .toMap - - val a = for ((k @ (org, name), m) <- modules.toSeq) yield { - - val versions = m.map(_.version) - val latest = versions.map(Version(_)).max.repr - val releaseOpt = Some(versions.filter(publish.MavenMetadata.isReleaseVersion).map(Version(_))) - .filter(_.nonEmpty) - .map(_.max.repr) - - meta.get(k) match { - case None => - val elem = publish.MavenMetadata.create( - org, - name, - Some(latest), - releaseOpt, - versions, - now - ) - val b = publish.MavenMetadata.print(elem).getBytes(StandardCharsets.UTF_8) - val content = DirContent(Seq( - "maven-metadata.xml" -> Content.InMemory(now, b) - )) - Seq(Task.point(k -> Group.MavenMetadata(org, name, content))) - case Some(md) => - Seq(md.updateContent( - None, - None, - Some(latest), - releaseOpt, - versions, - now - ).map(k -> _)) - } - } - - Task.gather.gather(a.flatten) - .map(l => modules.values.toSeq.flatten ++ (meta ++ l.toMap).values.toSeq) - } - - def downloadMavenMetadata( - orgNames: Seq[(Organization, ModuleName)], - download: Download, - repository: MavenRepository, - logger: DownloadLogger - ): Task[Seq[MavenMetadata]] = { - - val root = repository.root + "/" - - Task.gather.gather { - orgNames.map { - case (org, name) => - val url = root + s"${org.value.split('.').mkString("/")}/${name.value}/maven-metadata.xml" - download.downloadIfExists(url, repository.authentication, logger).map(_.map { - case (lastModifiedOpt, b) => - // download and verify checksums too? - MavenMetadata( - org, - name, - DirContent( - Seq( - "maven-metadata.xml" -> Content.InMemory( - lastModifiedOpt.getOrElse(Instant.EPOCH), - b - ) - ) - ) - ) - }) - } - }.map(_.flatten) - } - - def downloadSnapshotVersioningMetadata( - m: Module, - download: Download, - repository: MavenRepository, - logger: DownloadLogger - ): Task[Module] = { - - // assert(m.snapshotVersioning.isEmpty) - - val root = repository.root + "/" - val url = root + s"${m.baseDir.mkString("/")}/maven-metadata.xml" - - download.downloadIfExists(url, repository.authentication, logger).map { - case Some((lastModifiedOpt, b)) => - m.copy( - files = m.files.update( - "maven-metadata.xml", - Content.InMemory(lastModifiedOpt.getOrElse(Instant.EPOCH), b) - ) - ) - case None => - m - } - } - - def mergeMavenMetadata( - groups: Seq[MavenMetadata], - now: Instant - ): Task[Seq[MavenMetadata]] = { - - val tasks = groups - .groupBy(m => (m.organization, m.name)) - .valuesIterator - .map { l => - val (dontKnow, withContent) = l.partition(_.xmlOpt.isEmpty) - - // dontKnow should be empty anyway… - - val merged = withContent match { - case Seq() => sys.error("can't possibly happen") - case Seq(m) => Task.point(m) - case Seq(m, others @ _*) => - m.xmlOpt.get.contentTask.flatMap { b => - val mainElem = XML.loadString(new String(b, StandardCharsets.UTF_8)) - - others.foldLeft(Task.point(mainElem)) { - case (mainElemTask, m0) => - for { - mainElem0 <- mainElemTask - b <- m0.xmlOpt.get.contentTask - } yield { - val elem = XML.loadString(new String(b, StandardCharsets.UTF_8)) - val info = publish.MavenMetadata.info(elem) - publish.MavenMetadata.update( - mainElem0, - None, - None, - info.latest, - info.release, - info.versions, - info.lastUpdated - ) - } - }.map { elem => - val b = publish.MavenMetadata.print(elem).getBytes(StandardCharsets.UTF_8) - m.copy( - files = m.files.update("maven-metadata.xml", Content.InMemory(now, b)) - ) - } - } - } - - merged.map(dontKnow :+ _) - } - .toSeq - - Task.gather.gather(tasks).map(_.flatten) - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/fileset/Path.scala b/modules/publish/src/main/scala/coursier/publish/fileset/Path.scala deleted file mode 100644 index bcf6eebac1..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/fileset/Path.scala +++ /dev/null @@ -1,12 +0,0 @@ -package coursier.publish.fileset - -final case class Path(elements: Seq[String]) { - def /(elem: String): Path = - Path(elements :+ elem) - def mapLast(f: String => String): Path = - Path(elements.dropRight(1) ++ elements.lastOption.map(f).toSeq) - def dropLast: Path = - Path(elements.init) - def repr: String = - elements.mkString("/") -} diff --git a/modules/publish/src/main/scala/coursier/publish/logging/OutputFrame.scala b/modules/publish/src/main/scala/coursier/publish/logging/OutputFrame.scala deleted file mode 100644 index 8c2b24dde1..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/logging/OutputFrame.scala +++ /dev/null @@ -1,223 +0,0 @@ -package coursier.publish.logging - -import java.io._ -import java.util.concurrent.{Executors, ScheduledFuture, TimeUnit} - -import coursier.cache.internal.Terminal.Ansi -import coursier.cache.internal.{ConsoleDim, ThreadUtil} - -import scala.util.control.NonFatal - -/** Displays the last `bufferSize` lines of `is` in the terminal via `out`, updating them along - * time. - */ -final class OutputFrame( - is: InputStream, - out: Writer, - bufferSize: Int, - preamble: Seq[String], - postamble: Seq[String] -) { - - private[this] val lines = new OutputFrame.Lines(bufferSize) - private[this] val inCaseOfErrorLines = new OutputFrame.Lines(100) // don't hard-code size? - - private val readThread: Thread = - new Thread("output-frame-read") { - setDaemon(true) - override def run() = - try { - val br = new BufferedReader(new InputStreamReader(is)) - var l: String = null - while ({ - l = br.readLine() - l != null - }) { - lines.append(l) - inCaseOfErrorLines.append(l) - } - } - catch { - case _: InterruptedException => - // normal exit - case NonFatal(e) => - System.err.println(s"Caught exception in output-frame-read") - e.printStackTrace(System.err) - } - } - - private def clear(): Unit = { - - var count = 0 - - for (_ <- preamble) { - out.clearLine(2) - out.write('\n') - count += 1 - } - - var n = 0 - while (n < bufferSize + 1) { - out.clearLine(2) - out.write('\n') - n += 1 - } - - count += n - - for (_ <- postamble) { - out.clearLine(2) - out.write('\n') - count += 1 - } - - out.up(count) - - out.flush() - } - - private def updateOutput(scrollUp: Boolean = true): Runnable = - new Runnable { - def run() = { - val width = ConsoleDim.width() - - var count = 0 - - for (l <- preamble) { - val l0 = - if (preamble.length <= width) l - else l.substring(0, width) - out.clearLine(2) - out.write(l0 + System.lineSeparator()) - count += 1 - } - - val it = lines.linesIterator() - var n = 0 - while (n < bufferSize && it.hasNext) { - val l = it.next() - // https://stackoverflow.com/a/25189932/3714539 - .replaceAll("\u001B\\[[\\d;]*[^\\d;]", "") - val l0 = - if (l.length <= width) l - else l.substring(0, width) - out.clearLine(2) - out.write(l0 + System.lineSeparator()) - n += 1 - } - - while (n < bufferSize + 1) { - out.clearLine(2) - out.write('\n') - n += 1 - } - - count += n - - for (l <- postamble) { - val l0 = - if (preamble.length <= width) l - else l.substring(0, width) - out.clearLine(2) - out.write(l0 + System.lineSeparator()) - count += 1 - } - - if (scrollUp) - out.up(count) - - out.flush() - } - } - - private val pool = Executors.newScheduledThreadPool(1, ThreadUtil.daemonThreadFactory()) - private var updateFutureOpt = Option.empty[ScheduledFuture[_]] - - private val period = 1000L / 50L - - def start(): Unit = { - assert(!pool.isShutdown) - assert(!readThread.isAlive) - assert(updateFutureOpt.isEmpty) - readThread.start() - val f = pool.scheduleAtFixedRate(updateOutput(), 0L, period, TimeUnit.MILLISECONDS) - updateFutureOpt = Some(f) - } - - def stop(keepFrame: Boolean = true, errored: Option[PrintStream] = None): Unit = { - readThread.interrupt() - updateFutureOpt.foreach(_.cancel(false)) - pool.shutdown() - pool.awaitTermination(2L * period, TimeUnit.MILLISECONDS) - errored match { - case None => - if (keepFrame) - updateOutput(scrollUp = false).run() - else - clear() - case Some(errStream) => - inCaseOfErrorLines.linesIterator().foreach(errStream.println) - } - } - -} - -object OutputFrame { - - private final class Lines(bufferSize: Int) { - - @volatile private[this] var first: Line = _ - @volatile private[this] var last: Line = _ - - // should not be called multiple times in parallel - def append(value: String): Unit = { - assert(value ne null) - val index = if (last eq null) 0L else last.index + 1L - val l = new Line(value, index) - - if (last eq null) { - assert(first eq null) - first = l - last = l - } - else { - last.setNext(l) - last = l - } - - while (last.index - first.index > bufferSize) { - // Let the former `first` be garbage-collected, if / once no more `linesIterator` reference it. - first = first.next - assert(first ne null) - } - } - - // thread safe, and can be used while append gets called - def linesIterator(): Iterator[String] = - new Iterator[String] { - var current = first - def hasNext = current ne null - def next() = { - val v = current.value - current = current.next - v - } - } - - } - - private final class Line(val value: String, val index: Long) { - - @volatile private[this] var next0: Line = _ - - def setNext(next: Line): Unit = { - assert(this.next0 eq null) - assert(!(next eq null)) - this.next0 = next - } - - def next: Line = - next0 - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/logging/ProgressLogger.scala b/modules/publish/src/main/scala/coursier/publish/logging/ProgressLogger.scala deleted file mode 100644 index e5cb45811f..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/logging/ProgressLogger.scala +++ /dev/null @@ -1,169 +0,0 @@ -package coursier.publish.logging - -import java.io.Writer -import java.lang.{Boolean => JBoolean} -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.{ConcurrentHashMap, Executors, ScheduledFuture, TimeUnit} - -import coursier.cache.internal.Terminal.Ansi -import coursier.cache.internal.ThreadUtil - -import scala.jdk.CollectionConverters._ - -/** Displays the progress of some task on a single line. - * - * With a ticker, an emoji once it's done, a summary of how many sub-tasks are done, and the total - * number of sub-tasks if it is known. - */ -final class ProgressLogger[T]( - processedMessage: String, - elementName: String, - out: Writer, - updateOnChange: Boolean = false, - doneEmoji: Option[String] = Some(Console.GREEN + "✔" + Console.RESET) -) { - - import ProgressLogger._ - - private val states = new ConcurrentHashMap[T, State] - private var printed = 0 - - private def clear(): Unit = { - - for (_ <- 1 to printed) { - out.clearLine(2) - out.down(1) - } - - out.up(printed) - out.flush() - - printed = 0 - } - - // from https://github.com/mitsuhiko/indicatif/blob/0c3c0b5cbe666402ed7d3b9366c346f6eb0228fe/src/progress.rs#L202 - private[this] val tickers = "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ " - - private def update(scrollUp: Boolean = true): Runnable = - new Runnable { - def run() = { - - clear() - - for ((_, s) <- states.asScala.toVector.sortBy(_._2.totalOpt.sum)) { - val m = s.processed.asScala.iterator.toMap - val ongoing = m.count(_._2.isLeft) - val extra = - if (ongoing > 0) { - val total = m.iterator.flatMap(_._2.left.toOption.iterator.map(_._2)).sum - if (total > 0L) { - val done = - m.iterator.flatMap(_._2.left.toOption.iterator.filter(_._2 > 0L).map(_._1)).sum - val pct = f"${100L * done.toDouble / total}%.2f %%" - s" ($pct of $ongoing on-going)" - } - else - s" ($ongoing on-going)" - } - else - "" - val doneCount = m.count(_._2.isRight) - val done = s.done.get() - val em = - if (done) - doneEmoji.fold("")(_ + " ") - else - tickers(doneCount % tickers.length) + " " - val totalPart = s.totalOpt.filter(_ => !done).fold("")(t => s" / $t") - out.write( - s" $em$processedMessage $doneCount$totalPart " + elementName + extra + - System.lineSeparator() - ) - printed += 1 - } - - if (scrollUp) - out.up(printed) - - out.flush() - } - } - - private val onChangeUpdate = update() - private val onChangeUpdateLock = new Object - private def onChange(): Unit = { - if (updateOnChange) - onChangeUpdateLock.synchronized { - onChangeUpdate.run() - } - } - - def processingSet(id: T, totalOpt: Option[Int]): Unit = { - val s = new State(totalOpt) - val previous = states.putIfAbsent(id, s) - assert(previous eq null) - onChange() - } - def processedSet(id: T): Unit = { - val s = states.get(id) - assert(s ne null, s"Found ${states.asScala.iterator.map(_._1).toList}, not $id") - val previous = s.done.getAndSet(true) - assert(!previous) - onChange() - } - - def processing(url: String, id: T): Unit = { - val s = states.get(id) - assert(s ne null, s"$id not started") - val previous = s.processed.putIfAbsent(url, Left((0, 0))) - assert(previous eq null) - onChange() - } - def progress(url: String, id: T, done: Long, total: Long): Unit = { - val s = states.get(id) - assert(s ne null, s"Found ${states.asScala.iterator.map(_._1).toList}, not $id") - val b = s.processed.put(url, Left((done, total))) - assert(b.isLeft) - onChange() - } - def processed(url: String, id: T, errored: Boolean): Unit = { - val s = states.get(id) - assert(s ne null, s"Found ${states.asScala.iterator.map(_._1).toList}, not $id") - val b = s.processed.put(url, Right(())) - assert(b.isLeft) - onChange() - } - - // FIXME Unused if updateOnChange is true - private val pool = Executors.newScheduledThreadPool(1, ThreadUtil.daemonThreadFactory()) - private var updateFutureOpt = Option.empty[ScheduledFuture[_]] - - private val period = 1000L / 50L - - def start(): Unit = { - assert(!pool.isShutdown) - assert(updateFutureOpt.isEmpty) - if (!updateOnChange) { - val f = pool.scheduleAtFixedRate(update(), 0L, period, TimeUnit.MILLISECONDS) - updateFutureOpt = Some(f) - } - } - def stop(keep: Boolean): Unit = { - updateFutureOpt.foreach(_.cancel(false)) - pool.shutdown() - pool.awaitTermination(2L * period, TimeUnit.MILLISECONDS) - if (keep) - update(scrollUp = false).run() - else - clear() - } -} - -object ProgressLogger { - - private final class State(val totalOpt: Option[Int]) { - val done = new AtomicBoolean(false) - val processed = new ConcurrentHashMap[String, Either[(Long, Long), Unit]] - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/sbt/Sbt.scala b/modules/publish/src/main/scala/coursier/publish/sbt/Sbt.scala deleted file mode 100644 index 58308d1f2e..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/sbt/Sbt.scala +++ /dev/null @@ -1,124 +0,0 @@ -package coursier.publish.sbt - -import java.io.{File, OutputStreamWriter} -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Path} - -import coursier.publish.logging.OutputFrame - -import scala.jdk.CollectionConverters._ -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -final class Sbt( - directory: File, - plugin: File, - ec: ExecutionContext, - outputFrameSizeOpt: Option[Int], - verbosity: Int, - interactive: Boolean = true -) { - - private implicit val ec0 = ec - - private val keepSbtOutput = (!interactive && verbosity >= 0) || verbosity >= 2 - - def run(sbtCommands: String): Try[Int] = { - - val processCommands = Seq("sbt", "-J-Dsbt.log.noformat=true", sbtCommands) // UTF-8… - - if (verbosity >= 2) - Console.err.println(s"Running ${processCommands.map("'" + _ + "'").mkString(" ")}") - - Try { - val b = new ProcessBuilder(processCommands.asJava) - b.directory(directory) - b.redirectInput(ProcessBuilder.Redirect.PIPE) - if (keepSbtOutput) { - b.redirectOutput(ProcessBuilder.Redirect.INHERIT) - b.redirectError(ProcessBuilder.Redirect.INHERIT) - } - else - b.redirectErrorStream(true) - - val p = b.start() - p.getOutputStream.close() - val outputFrameOpt = - if (keepSbtOutput || !interactive) - None - else - outputFrameSizeOpt.map { n => - System.out.flush() - System.err.flush() - val f = new OutputFrame( - p.getInputStream, - new OutputStreamWriter(System.err), - n, - Seq("", "--- sbt is running ---"), - Seq() - ) - f.start() - f - } - - var retCode = 0 - try retCode = p.waitFor() - finally { - val errStreamOpt = if (retCode == 0) None else Some(System.err) - outputFrameOpt.foreach(_.stop(keepFrame = false, errored = errStreamOpt)) - } - - retCode - } - } - - def publishTo(dir: File, projectsOpt: Option[Seq[String]] = None): Future[Unit] = { - - if (verbosity >= 1) - Console.err.println( - s"Publishing sbt project ${directory.getAbsolutePath} to temporary directory $dir" - ) - else if (verbosity >= 0) { - val name = directory.getName - val msg = - if (name == ".") - s"Publishing sbt project to temporary directory" - else - s"Publishing ${directory.getName} to temporary directory" - - Console.err.println(msg) - } - - Console.err.flush() - - // dir escaping a bit meh - val cmd = Seq( - s"""apply -cp "${plugin.getAbsolutePath}" sbtcspublish.SbtCsPublishPlugin""", - "+csPublish \"" + dir + "\"" - ).mkString(";", ";", "") - - run(cmd) match { - case Success(0) => - Future.successful(()) - case Success(n) => - Future.failed(new Exception(s"sbt exited with code $n")) - case Failure(e) => - Future.failed(e) - } - } - -} - -object Sbt { - - def isSbtProject(dir: Path): Boolean = - Files.isDirectory(dir) && { - val buildProps = dir.resolve("project/build.properties") - Files.isRegularFile(buildProps) && { - Try(new String(Files.readAllBytes(buildProps), StandardCharsets.UTF_8)) - .toOption - .exists(_.linesIterator.exists(_.startsWith("sbt.version="))) - } - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/GpgSigner.scala b/modules/publish/src/main/scala/coursier/publish/signing/GpgSigner.scala deleted file mode 100644 index c11dbf5e18..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/GpgSigner.scala +++ /dev/null @@ -1,115 +0,0 @@ -package coursier.publish.signing - -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Path} -import java.nio.file.attribute.{PosixFilePermission, PosixFilePermissions} - -import coursier.publish.Content -import coursier.util.Task - -import scala.jdk.CollectionConverters._ - -final case class GpgSigner( - key: GpgSigner.Key, - command: String = "gpg", - extraOptions: Seq[String] = Nil -) extends Signer { - - private def keyArgs: Seq[String] = - key match { - case GpgSigner.Key.Default => - Nil - case GpgSigner.Key.Id(id) => - Seq("--local-user", id) - } - - def sign(content: Content): Task[Either[String, String]] = { - - val pathTemporaryTask = content.pathOpt.map(p => Task.point((p, false))).getOrElse { - val p = Files.createTempFile( - "signer", - ".content", - PosixFilePermissions.asFileAttribute( - Set( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE - ).asJava - ) - ) - content.contentTask.map { b => - Files.write(p, b) - (p, true) - } - } - - pathTemporaryTask.flatMap { - case (path, temporary) => - sign0(path, temporary, content) - } - } - - private def sign0( - path: Path, - temporary: Boolean, - content: Content - ): Task[Either[String, String]] = - Task.delay { - - // inspired by https://github.com/jodersky/sbt-gpg/blob/853e608120eac830068bbb121b486b7cf06fc4b9/src/main/scala/Gpg.scala - - val dest = Files.createTempFile( - "signer", - ".asc", - PosixFilePermissions.asFileAttribute( - Set( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE - ).asJava - ) - ) - - try { - val pb = new ProcessBuilder() - .command( - Seq(command) ++ - extraOptions ++ - keyArgs ++ - Seq( - "--armor", - "--yes", - "--output", - dest.toAbsolutePath.toString, - "--detach-sign", - path.toAbsolutePath.toString - ): _* - ) - .inheritIO() - - val p = pb.start() - - val retCode = p.waitFor() - - if (retCode == 0) - Right(new String(Files.readAllBytes(dest), StandardCharsets.UTF_8)) - else - Left(s"gpg failed (return code: $retCode)") - } - finally { - // Ignore I/O errors? - Files.deleteIfExists(dest) - if (temporary) - Files.deleteIfExists(path) - } - } -} - -object GpgSigner { - - sealed abstract class Key extends Product with Serializable - - object Key { - final case class Id(id: String) extends Key - case object Default extends Key - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/NopSigner.scala b/modules/publish/src/main/scala/coursier/publish/signing/NopSigner.scala deleted file mode 100644 index b0b5aff221..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/NopSigner.scala +++ /dev/null @@ -1,22 +0,0 @@ -package coursier.publish.signing - -import java.time.Instant - -import coursier.publish.Content -import coursier.publish.fileset.{FileSet, Path} -import coursier.publish.signing.logger.SignerLogger -import coursier.util.Task - -object NopSigner extends Signer { - def sign(content: Content): Task[Either[String, String]] = - Task.point(Right("")) - - override def signatures( - fileSet: FileSet, - now: Instant, - dontSignExtensions: Set[String], - dontSignFiles: Set[String], - logger: => SignerLogger - ): Task[Either[(Path, Content, String), FileSet]] = - Task.point(Right(FileSet.empty)) -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/Signer.scala b/modules/publish/src/main/scala/coursier/publish/signing/Signer.scala deleted file mode 100644 index bea4c0a690..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/Signer.scala +++ /dev/null @@ -1,133 +0,0 @@ -package coursier.publish.signing - -import java.nio.charset.StandardCharsets -import java.time.Instant - -import coursier.publish.Content -import coursier.publish.fileset.{FileSet, Path} -import coursier.publish.signing.logger.SignerLogger -import coursier.util.Task - -/** Signs artifacts. - */ -trait Signer { - - /** Computes the signature of the passed `content`. - * - * @return - * an error message (left), or the signature file content (right), wrapped in [[Task]] - */ - def sign(content: Content): Task[Either[String, String]] - - /** Adds missing signatures in a [[FileSet]]. - * - * @param fileSet: - * [[FileSet]] to add signatures to - can optionally contain some already calculated signatures - * @param now: - * last modified time for the added signature files - * @return - * a [[FileSet]] of the missing signature files - */ - def signatures( - fileSet: FileSet, - now: Instant, - dontSignExtensions: Set[String], - dontSignFiles: Set[String], - logger: => SignerLogger - ): Task[Either[(Path, Content, String), FileSet]] = { - - val elementsOrSignatures = fileSet.elements.flatMap { - case (path, content) => - if (path.elements.lastOption.exists(n => n.endsWith(".asc"))) - // found a signature - Seq(Right(path.mapLast(_.stripSuffix(".asc")))) - else if (path.elements.lastOption.exists(n => n.contains(".asc."))) - // FIXME May not be ok if e.g. version contains .asc., like 2.0.asc.1 (this is unlikely though) - // ignored file - Nil - else - // may need to be signed - Seq(Left((path, content))) - } - - val signed = elementsOrSignatures - .collect { - case Right(path) => path - } - .toSet - - val toSign = elementsOrSignatures - .collect { - case Left((path, content)) - if !signed(path) && - !path.elements.lastOption.exists(n => - dontSignExtensions.exists(e => n.endsWith("." + e)) - ) && - !path.elements.lastOption.exists(n => - dontSignFiles(n) || dontSignFiles.exists(f => n.startsWith(f + ".")) - ) => - (path, content) - } - - def signaturesTask(id: Object, logger0: SignerLogger) = - toSign.foldLeft( - Task.point[Either[(Path, Content, String), List[(Path, Content)]]](Right(Nil)) - ) { - case (acc, (path, content)) => - for { - previous <- acc - res <- { - previous match { - case l @ Left(_) => Task.point(l) - case Right(l) => - val doSign = sign(content).map { - case Left(e) => - Left((path, content, e)) - case Right(s) => - val content = Content.InMemory(now, s.getBytes(StandardCharsets.UTF_8)) - Right((path.mapLast(_ + ".asc"), content) :: l) - } - - for { - _ <- Task.delay(logger0.signingElement(id, path)) - a <- doSign.attempt - // FIXME Left case of doSign not passed as error here - _ <- Task.delay(logger0.signedElement(id, path, a.left.toOption)) - res <- Task.fromEither(a) - } yield res - } - } - } yield res - }.map(_.map { elements => - FileSet(elements.reverse) - }) - - val toSignFs = FileSet(toSign) - - if (toSignFs.isEmpty) - Task.point(Right(FileSet.empty)) - else { - val before = Task.delay { - val id = new Object - val logger0 = logger - logger0.start() - logger0.signing(id, toSignFs) - (id, logger0) - } - - def after(id: Object, logger0: SignerLogger) = Task.delay { - logger0.signed(id, toSignFs) - logger0.stop() - } - - for { - idLogger <- before - (id, logger0) = idLogger - a <- signaturesTask(id, logger0).attempt - _ <- after(id, logger0) - res <- Task.fromEither(a) - } yield res - } - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/logger/BatchSignerLogger.scala b/modules/publish/src/main/scala/coursier/publish/signing/logger/BatchSignerLogger.scala deleted file mode 100644 index 08ad27c4e1..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/logger/BatchSignerLogger.scala +++ /dev/null @@ -1,15 +0,0 @@ -package coursier.publish.signing.logger - -import java.io.PrintStream - -import coursier.publish.fileset.Path - -final class BatchSignerLogger(out: PrintStream, verbosity: Int) extends SignerLogger { - - override def signingElement(id: Object, path: Path): Unit = - if (verbosity >= 0) - out.println(s"Signing ${path.repr}") - override def signedElement(id: Object, path: Path, excOpt: Option[Throwable]): Unit = - if (verbosity >= 0) - out.println(s"Signed ${path.repr}") -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/logger/InteractiveSignerLogger.scala b/modules/publish/src/main/scala/coursier/publish/signing/logger/InteractiveSignerLogger.scala deleted file mode 100644 index 3054bd0a07..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/logger/InteractiveSignerLogger.scala +++ /dev/null @@ -1,44 +0,0 @@ -package coursier.publish.signing.logger - -import java.io.{OutputStream, OutputStreamWriter, Writer} - -import coursier.publish.fileset.{FileSet, Path} -import coursier.publish.logging.ProgressLogger - -final class InteractiveSignerLogger(out: Writer, verbosity: Int) extends SignerLogger { - - private val underlying = new ProgressLogger[Object]( - "Signed", - "files", - out, - updateOnChange = true, - doneEmoji = Some("\u270D\uFE0F ") - ) - - override def signing(id: Object, fileSet: FileSet): Unit = { - underlying.processingSet(id, Some(fileSet.elements.length)) - } - override def signed(id: Object, fileSet: FileSet): Unit = - underlying.processedSet(id) - - override def signingElement(id: Object, path: Path): Unit = { - if (verbosity >= 2) - out.write(s"Signing ${path.repr}" + System.lineSeparator()) - underlying.processing(path.repr, id) - } - override def signedElement(id: Object, path: Path, excOpt: Option[Throwable]): Unit = { - if (verbosity >= 2) - out.write(s"Signed ${path.repr}" + System.lineSeparator()) - underlying.processed(path.repr, id, excOpt.nonEmpty) - } - - override def start(): Unit = - underlying.start() - override def stop(keep: Boolean): Unit = - underlying.stop(keep) -} - -object InteractiveSignerLogger { - def create(out: OutputStream, verbosity: Int): SignerLogger = - new InteractiveSignerLogger(new OutputStreamWriter(out), verbosity) -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/logger/SignerLogger.scala b/modules/publish/src/main/scala/coursier/publish/signing/logger/SignerLogger.scala deleted file mode 100644 index 0962067eae..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/logger/SignerLogger.scala +++ /dev/null @@ -1,20 +0,0 @@ -package coursier.publish.signing.logger - -import coursier.publish.fileset.{FileSet, Path} - -trait SignerLogger { - def signing(id: Object, fs: FileSet): Unit = () - def signingElement(id: Object, path: Path): Unit = () - def signedElement(id: Object, path: Path, excOpt: Option[Throwable]): Unit = () - def signed(id: Object, fs: FileSet): Unit = () - - def start(): Unit = () - def stop(keep: Boolean = true): Unit = () -} - -object SignerLogger { - - val nop: SignerLogger = - new SignerLogger {} - -} diff --git a/modules/publish/src/main/scala/coursier/publish/signing/package.scala b/modules/publish/src/main/scala/coursier/publish/signing/package.scala deleted file mode 100644 index 1ec07437aa..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/signing/package.scala +++ /dev/null @@ -1,5 +0,0 @@ -package coursier.publish - -/** All things signing. - */ -package object signing diff --git a/modules/publish/src/main/scala/coursier/publish/sonatype/OkHttpClientUtil.scala b/modules/publish/src/main/scala/coursier/publish/sonatype/OkHttpClientUtil.scala deleted file mode 100644 index ac4d522e48..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/sonatype/OkHttpClientUtil.scala +++ /dev/null @@ -1,127 +0,0 @@ -package coursier.publish.sonatype - -import java.io.FileNotFoundException -import java.nio.charset.StandardCharsets - -import argonaut.{DecodeJson, EncodeJson} -import argonaut.Argonaut._ -import coursier.core.Authentication -import coursier.util.Task -import okhttp3.{MediaType, OkHttpClient, Request, RequestBody} - -import scala.jdk.CollectionConverters._ -import scala.util.Try - -final case class OkHttpClientUtil( - client: OkHttpClient, - authentication: Option[Authentication], - verbosity: Int -) { - - private def request(url: String, post: Option[RequestBody] = None): Request = { - val b = new Request.Builder().url(url) - for (body <- post) - b.post(body) - - // Handling this ourselves rather than via client.setAuthenticator / com.squareup.okhttp.Authenticator - for (auth <- authentication; (k, v) <- auth.allHttpHeaders) - b.addHeader(k, v) - - // ??? - b.addHeader( - "Accept", - "application/json,application/vnd.siesta-error-v1+json,application/vnd.siesta-validation-errors-v1+json" - ) - - val r = b.build() - - if (verbosity >= 2) { - val m = r.headers().toMultimap.asScala.mapValues(_.asScala.toVector) - for ((k, l) <- m; v <- l) - System.err.println(s"$k: $v") - } - - r - } - - def postBody[B: EncodeJson](content: B): RequestBody = - RequestBody.create( - OkHttpClientUtil.mediaType, - EncodeJson.of[B].apply(content).nospaces.getBytes(StandardCharsets.UTF_8) - ) - - def create(url: String, post: Option[RequestBody] = None): Task[Unit] = { - - val t = Task.delay { - if (verbosity >= 1) - Console.err.println(s"Getting $url") - val resp = client.newCall(request(url, post)).execute() - if (verbosity >= 1) - Console.err.println(s"Done: $url") - - if (resp.code() == 201) - Task.point(()) - else - Task.fail(new Exception( - s"Failed to get $url (http status: ${resp.code()}, response: ${Try(resp.body().string()).getOrElse("")})" - )) - } - - t.flatMap(identity) - } - - def get[T: DecodeJson]( - url: String, - post: Option[RequestBody] = None, - nested: Boolean = true - ): Task[T] = { - - val t = Task.delay { - if (verbosity >= 1) - Console.err.println(s"Getting $url") - if (verbosity >= 2) - post.foreach { b => - val buf = new okio.Buffer - b.writeTo(buf) - System.err.println("Sending " + buf) - } - val resp = client.newCall(request(url, post)).execute() - if (verbosity >= 1) - Console.err.println(s"Done: $url") - - if (resp.isSuccessful) - if (nested) - resp.body().string().decodeEither(DecodeJson.of[T]) match { - case Left(e) => - Task.fail(new Exception(s"Received invalid response from $url: $e")) - case Right(t) => - Task.point(t) - } - else - resp.body().string().decodeEither[T] match { - case Left(e) => - Task.fail(new Exception(s"Received invalid response from $url: $e")) - case Right(t) => - Task.point(t) - } - else { - val msg = - s"Failed to get $url (http status: ${resp.code()}, response: ${Try(resp.body().string()).getOrElse("")})" - val notFound = resp.code() / 100 == 4 - if (notFound) - Task.fail(new FileNotFoundException(msg)) - else - Task.fail(new Exception(msg)) - } - } - - t.flatMap(identity) - } - -} - -object OkHttpClientUtil { - - private val mediaType = MediaType.parse("application/json") - -} diff --git a/modules/publish/src/main/scala/coursier/publish/sonatype/SonatypeApi.scala b/modules/publish/src/main/scala/coursier/publish/sonatype/SonatypeApi.scala deleted file mode 100644 index d1236fc85c..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/sonatype/SonatypeApi.scala +++ /dev/null @@ -1,321 +0,0 @@ -package coursier.publish.sonatype - -import java.util.concurrent.ScheduledExecutorService - -import argonaut._ -import argonaut.Argonaut._ -import coursier.core.Authentication -import coursier.publish.sonatype.logger.SonatypeLogger -import coursier.util.Task -import okhttp3.{MediaType, OkHttpClient, RequestBody} - -import scala.concurrent.duration.Duration - -final case class SonatypeApi( - client: OkHttpClient, - base: String, - authentication: Option[Authentication], - verbosity: Int, - retryOnTimeout: Int = 3 -) { - - // vaguely inspired by https://github.com/lihaoyi/mill/blob/7b4ced648ecd9b79b3a16d67552f0bb69f4dd543/scalalib/src/mill/scalalib/publish/SonatypeHttpApi.scala - // and https://github.com/xerial/sbt-sonatype/blob/583db138df2b0e7bbe58717103f2c9874fca2a74/src/main/scala/xerial/sbt/Sonatype.scala - - import SonatypeApi._ - - private def postBody[B: EncodeJson](content: B): RequestBody = - clientUtil.postBody(Json.obj("data" -> EncodeJson.of[B].apply(content))) - - private def get[T: DecodeJson]( - url: String, - post: Option[RequestBody] = None, - nested: Boolean = true - ): Task[T] = - clientUtil.get(url, post, nested)(Response.decode[T]).map(_.data) - - private val clientUtil = OkHttpClientUtil(client, authentication, verbosity) - - private def withRetry[T](task: Int => Task[T]): Task[T] = { - - def helper(attempt: Int): Task[T] = - task(attempt).attempt.flatMap { - case Left(_: java.net.SocketTimeoutException) if attempt + 1 < retryOnTimeout => - helper(attempt + 1) - case other => - Task.fromEither(other) - } - - helper(0) - } - - def listProfiles(logger: SonatypeLogger = SonatypeLogger.nop): Task[Seq[SonatypeApi.Profile]] = { - - def before(attempt: Int) = Task.delay { - logger.listingProfiles(attempt, retryOnTimeout) - } - - def after(errorOpt: Option[Throwable]) = Task.delay { - logger.listedProfiles(errorOpt) - } - - // for w/e reasons, Profiles.Profile.decode isn't implicitly picked - val task = get(s"$base/staging/profiles")(DecodeJson.ListDecodeJson(Profiles.Profile.decode)) - .map(_.map(_.profile)) - - withRetry { attempt => - for { - _ <- before(attempt) - a <- task.attempt - _ <- after(a.left.toOption) - res <- Task.fromEither(a) - } yield res - } - } - - def rawListProfiles(): Task[Json] = - get[Json](s"$base/staging/profiles") - - def decodeListProfilesResponse(json: Json): Either[Exception, Seq[SonatypeApi.Profile]] = - json.as(DecodeJson.ListDecodeJson(Profiles.Profile.decode)).toEither match { - case Left(e) => Left(new Exception(s"Error decoding response: $e")) - case Right(l) => Right(l.map(_.profile)) - } - - def listProfileRepositories(profileIdOpt: Option[String]): Task[Seq[SonatypeApi.Repository]] = - get(s"$base/staging/profile_repositories" + profileIdOpt.fold("")("/" + _))( - DecodeJson.ListDecodeJson(RepositoryResponse.decoder) - ) - .map(_.map(_.repository)) - - def rawListProfileRepositories(profileIdOpt: Option[String]): Task[Json] = - get[Json](s"$base/staging/profile_repositories" + profileIdOpt.fold("")("/" + _)) - - def decodeListProfileRepositoriesResponse(json: Json) - : Either[Exception, Seq[SonatypeApi.Repository]] = - json.as(DecodeJson.ListDecodeJson(RepositoryResponse.decoder)).toEither match { - case Left(e) => Left(new Exception(s"Error decoding response: $e")) - case Right(l) => Right(l.map(_.repository)) - } - - def createStagingRepository(profile: Profile, description: String): Task[String] = - get( - s"${profile.uri}/start", - post = Some(postBody(StartRequest(description))(StartRequest.encoder)) - )(StartResponse.decoder).map { r => - r.stagedRepositoryId - } - - def rawCreateStagingRepository(profile: Profile, description: String): Task[Json] = - get[Json]( - s"${profile.uri}/start", - post = Some(postBody(StartRequest(description))(StartRequest.encoder)) - ) - - private def stagedRepoAction( - action: String, - profile: Profile, - repositoryId: String, - description: String - ): Task[Unit] = - clientUtil.create( - s"${profile.uri}/$action", - post = Some(postBody(StagedRepositoryRequest(description, repositoryId))( - StagedRepositoryRequest.encoder - )) - ) - - def sendCloseStagingRepositoryRequest( - profile: Profile, - repositoryId: String, - description: String - ): Task[Unit] = - stagedRepoAction("finish", profile, repositoryId, description) - - def sendPromoteStagingRepositoryRequest( - profile: Profile, - repositoryId: String, - description: String - ): Task[Unit] = - stagedRepoAction("promote", profile, repositoryId, description) - - def sendDropStagingRepositoryRequest( - profile: Profile, - repositoryId: String, - description: String - ): Task[Unit] = - stagedRepoAction("drop", profile, repositoryId, description) - - def lastActivity(repositoryId: String, action: String) = - get[List[Json]](s"$base/staging/repository/$repositoryId/activity", nested = false).map { l => - l.filter { json => - json.field("name").flatMap(_.string).contains(action) - }.lastOption - } - - def waitForStatus( - profileId: String, - repositoryId: String, - status: String, - maxAttempt: Int, - initialDelay: Duration, - backoffFactor: Double, - es: ScheduledExecutorService - ): Task[Unit] = { - - // TODO Stop early in case of error (which statuses exactly???) - - def task(attempt: Int, nextDelay: Duration, totalDelay: Duration): Task[Unit] = - listProfileRepositories(Some(profileId)).flatMap { l => - l.find(_.id == repositoryId) match { - case None => - Task.fail(new Exception(s"Repository $repositoryId not found")) - case Some(repo) => - // TODO Use logger for that - System.err.println(s"Repository $repositoryId has status ${repo.`type`}") - repo.`type` match { - case `status` => - Task.point(()) - case other => - if (attempt < maxAttempt) - task(attempt + 1, backoffFactor * nextDelay, totalDelay + nextDelay) - .schedule(nextDelay, es) - else - // FIXME totalDelay doesn't include the duration of the requests themselves (only the time between) - Task.fail( - new Exception(s"Repository $repositoryId in state $other after $totalDelay") - ) - } - } - } - - task(1, initialDelay, Duration.Zero) - } - -} - -object SonatypeApi { - - final case class Profile( - id: String, - name: String, - uri: String - ) - - final case class Repository( - profileId: String, - profileName: String, - id: String, - `type`: String - ) - - def activityErrored(activity: Json): Either[List[String], Unit] = - Activity.decoder.decodeJson(activity).toEither match { - case Left(e) => ??? - case Right(a) => - val errors = a.events.filter(_.severity >= 1).map(_.name) - if (errors.isEmpty) - Right(()) - else - Left(errors) - } - - // same kind of check as sbt-sonatype - def repositoryClosed(activity: Json, repoId: String): Boolean = - Activity.decoder.decodeJson(activity).toEither match { - case Left(_) => ??? - case Right(a) => - a.events.exists { e => - e.name == "repositoryClosed" && - e.properties.exists(p => p.name == "id" && p.value == repoId) - } - } - def repositoryPromoted(activity: Json, repoId: String): Boolean = - Activity.decoder.decodeJson(activity).toEither match { - case Left(_) => ??? - case Right(a) => - a.events.exists { e => - e.name == "repositoryReleased" && - e.properties.exists(p => p.name == "id" && p.value == repoId) - } - } - - private final case class Activity(name: String, events: List[Activity.Event]) - - private object Activity { - import argonaut.ArgonautShapeless._ - final case class Event(name: String, severity: Int, properties: List[Property]) - final case class Property(name: String, value: String) - implicit val decoder = DecodeJson.of[Activity] - } - - private val mediaType = MediaType.parse("application/json") - - private final case class Response[T](data: T) - - private object Response { - import argonaut.ArgonautShapeless._ - implicit def decode[T: DecodeJson] = DecodeJson.of[Response[T]] - } - - private object Profiles { - - final case class Profile( - id: String, - name: String, - resourceURI: String - ) { - def profile = - SonatypeApi.Profile( - id, - name, - resourceURI - ) - } - - object Profile { - import argonaut.ArgonautShapeless._ - implicit val decode = DecodeJson.of[Profile] - } - } - - private final case class RepositoryResponse( - profileId: String, - profileName: String, - repositoryId: String, - `type`: String - ) { - def repository: Repository = - Repository( - profileId, - profileName, - repositoryId, - `type` - ) - } - private object RepositoryResponse { - import argonaut.ArgonautShapeless._ - implicit val decoder = DecodeJson.of[RepositoryResponse] - } - - private final case class StartRequest(description: String) - private object StartRequest { - import argonaut.ArgonautShapeless._ - implicit val encoder = EncodeJson.of[StartRequest] - } - private final case class StartResponse(stagedRepositoryId: String) - private object StartResponse { - import argonaut.ArgonautShapeless._ - implicit val decoder = DecodeJson.of[StartResponse] - } - - private final case class StagedRepositoryRequest( - description: String, - stagedRepositoryId: String - ) - private object StagedRepositoryRequest { - import argonaut.ArgonautShapeless._ - implicit val encoder = EncodeJson.of[StagedRepositoryRequest] - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/sonatype/logger/BatchSonatypeLogger.scala b/modules/publish/src/main/scala/coursier/publish/sonatype/logger/BatchSonatypeLogger.scala deleted file mode 100644 index 522809e996..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/sonatype/logger/BatchSonatypeLogger.scala +++ /dev/null @@ -1,27 +0,0 @@ -package coursier.publish.sonatype.logger - -import java.io.PrintStream - -final class BatchSonatypeLogger(out: PrintStream, verbosity: Int) extends SonatypeLogger { - override def listingProfiles(attempt: Int, total: Int): Unit = - if (verbosity >= 0) { - val extra = - if (attempt == 0) "" - else s" (attempt $attempt / $total)" - out.println("Listing Sonatype profiles..." + extra) - } - override def listedProfiles(errorOpt: Option[Throwable]): Unit = { - - val msgOpt = - if (errorOpt.isEmpty) - if (verbosity >= 1) - Some("Listed Sonatype profiles") - else - None - else - Some("Fail to list Sonatype profiles") - - for (msg <- msgOpt) - out.println(s"$msg") - } -} diff --git a/modules/publish/src/main/scala/coursier/publish/sonatype/logger/InteractiveSonatypeLogger.scala b/modules/publish/src/main/scala/coursier/publish/sonatype/logger/InteractiveSonatypeLogger.scala deleted file mode 100644 index 3e436fa1b4..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/sonatype/logger/InteractiveSonatypeLogger.scala +++ /dev/null @@ -1,44 +0,0 @@ -package coursier.publish.sonatype.logger - -import java.io.{OutputStream, OutputStreamWriter} - -import coursier.cache.internal.Terminal.Ansi - -final class InteractiveSonatypeLogger(out: OutputStreamWriter, verbosity: Int) - extends SonatypeLogger { - override def listingProfiles(attempt: Int, total: Int): Unit = - if (verbosity >= 0) { - val extra = - if (attempt == 0) "" - else s" (attempt $attempt / $total)" - out.write("Listing Sonatype profiles..." + extra) - out.flush() - } - override def listedProfiles(errorOpt: Option[Throwable]): Unit = { - if (verbosity >= 0) { - out.clearLine(2) - out.write('\n') - out.up(1) - out.flush() - } - - val msgOpt = - if (errorOpt.isEmpty) - if (verbosity >= 1) - Some("Listed Sonatype profiles") - else - None - else - Some("Fail to list Sonatype profiles") - - for (msg <- msgOpt) { - out.write(s"$msg" + System.lineSeparator()) - out.flush() - } - } -} - -object InteractiveSonatypeLogger { - def create(out: OutputStream, verbosity: Int): SonatypeLogger = - new InteractiveSonatypeLogger(new OutputStreamWriter(out), verbosity) -} diff --git a/modules/publish/src/main/scala/coursier/publish/sonatype/logger/SonatypeLogger.scala b/modules/publish/src/main/scala/coursier/publish/sonatype/logger/SonatypeLogger.scala deleted file mode 100644 index ce82450fd5..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/sonatype/logger/SonatypeLogger.scala +++ /dev/null @@ -1,11 +0,0 @@ -package coursier.publish.sonatype.logger - -trait SonatypeLogger { - def listingProfiles(attempt: Int, total: Int): Unit = () - def listedProfiles(errorOpt: Option[Throwable]): Unit = () -} - -object SonatypeLogger { - val nop: SonatypeLogger = - new SonatypeLogger {} -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/DummyUpload.scala b/modules/publish/src/main/scala/coursier/publish/upload/DummyUpload.scala deleted file mode 100644 index dbf7d40014..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/DummyUpload.scala +++ /dev/null @@ -1,16 +0,0 @@ -package coursier.publish.upload - -import coursier.core.Authentication -import coursier.publish.upload.logger.UploadLogger -import coursier.util.Task - -final case class DummyUpload(underlying: Upload) extends Upload { - def upload( - url: String, - authentication: Option[Authentication], - content: Array[Byte], - logger: UploadLogger, - loggingId: Option[Object] - ): Task[Option[Upload.Error]] = - Task.point(None) -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/FileUpload.scala b/modules/publish/src/main/scala/coursier/publish/upload/FileUpload.scala deleted file mode 100644 index 8da1aeeb1b..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/FileUpload.scala +++ /dev/null @@ -1,47 +0,0 @@ -package coursier.publish.upload - -import coursier.core.Authentication -import coursier.paths.Util -import coursier.publish.upload.logger.UploadLogger -import coursier.util.Task - -import java.net.URI -import java.nio.file.{Files, Path, Paths} - -import scala.util.control.NonFatal - -/** Copies - * @param base - */ -final case class FileUpload(base: Path) extends Upload { - private val base0 = base.normalize() - def upload( - url: String, - authentication: Option[Authentication], - content: Array[Byte], - logger: UploadLogger, - loggingId: Option[Object] - ): Task[Option[Upload.Error]] = { - - val p = base0.resolve(Paths.get(new URI(url))).normalize() - if (p.startsWith(base0)) - Task.delay { - logger.uploading(p.toString, loggingId, Some(content.length)) - val errorOpt = - try { - Util.createDirectories(p.getParent) - Files.write(p, content) - None - } - catch { - case NonFatal(e) => - Some(e) - } - logger.uploaded(p.toString, loggingId, errorOpt.map(e => new Upload.Error.FileException(e))) - - None - } - else - Task.fail(new Exception(s"Invalid path: $url (base: $base0, p: $p)")) - } -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/HttpURLConnectionUpload.scala b/modules/publish/src/main/scala/coursier/publish/upload/HttpURLConnectionUpload.scala deleted file mode 100644 index 7e4bc5ebb2..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/HttpURLConnectionUpload.scala +++ /dev/null @@ -1,143 +0,0 @@ -package coursier.publish.upload - -import java.io.{ByteArrayOutputStream, InputStream, OutputStream} -import java.net.{HttpURLConnection, URL} -import java.nio.charset.StandardCharsets -import java.util.concurrent.ExecutorService - -import coursier.cache.CacheUrl -import coursier.core.Authentication -import coursier.publish.upload.logger.UploadLogger -import coursier.util.Task - -import scala.jdk.CollectionConverters._ -import scala.util.Try -import scala.util.control.NonFatal - -final case class HttpURLConnectionUpload( - pool: ExecutorService, - urlSuffix: String -) extends Upload { - - import coursier.publish.download.OkhttpDownload.TryOps - - def upload( - url: String, - authentication: Option[Authentication], - content: Array[Byte], - logger: UploadLogger, - loggingIdOpt: Option[Object] - ): Task[Option[Upload.Error]] = - Task.schedule(pool) { - logger.uploading(url, loggingIdOpt, Some(content.length)) - - val res = Try { - val url0 = new URL(url + urlSuffix) - - val conn = url0.openConnection().asInstanceOf[HttpURLConnection] - - conn.setRequestMethod("PUT") - - for (auth <- authentication; (k, v) <- auth.allHttpHeaders) - conn.setRequestProperty(k, v) - - conn.setDoOutput(true) - - conn.setRequestProperty("Content-Type", "application/octet-stream") - conn.setRequestProperty("Content-Length", content.length.toString) - - var is: InputStream = null - var es: InputStream = null - var os: OutputStream = null - - try { - os = conn.getOutputStream - os.write(content) - os.close() - - val code = conn.getResponseCode - if (code == 401) { - val realmOpt = Option(conn.getRequestProperty("WWW-Authenticate")).collect { - case CacheUrl.BasicRealm(r) => r - } - Some(new Upload.Error.Unauthorized(url, realmOpt)) - } - else if (code / 100 == 2) - None - else { - es = conn.getErrorStream - val buf = Array.ofDim[Byte](16384) - val baos = new ByteArrayOutputStream - var read = -1 - while ({ - read = es.read(buf); read >= 0 - }) - baos.write(buf, 0, read) - es.close() - // FIXME Adjust charset with headers? - val content = - Try(new String(baos.toByteArray, StandardCharsets.UTF_8)).toOption.getOrElse("") - Some( - new Upload.Error.HttpError( - code, - conn.getHeaderFields.asScala.mapValues(_.asScala.toList).iterator.toMap, - content - ) - ) - } - } - finally { - // Trying to ensure the same connection is being re-used across requests - // see https://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html - try { - if (os == null) - os = conn.getOutputStream - if (os != null) - os.close() - } - catch { - case NonFatal(_) => - } - try { - if (is == null) - is = conn.getInputStream - if (is != null) { - val buf = Array.ofDim[Byte](16384) - while (is.read(buf) > 0) {} - is.close() - } - } - catch { - case NonFatal(_) => - } - try { - if (es == null) - es = conn.getErrorStream - if (es != null) { - val buf = Array.ofDim[Byte](16384) - while (es.read(buf) > 0) {} - es.close() - } - } - catch { - case NonFatal(_) => - } - } - } - - logger.uploaded( - url, - loggingIdOpt, - res.toEither.fold(e => Some(new Upload.Error.UploadError(url, e)), x => x) - ) - - res.get - } -} - -object HttpURLConnectionUpload { - def create(pool: ExecutorService): Upload = - HttpURLConnectionUpload(pool, "") - def create(pool: ExecutorService, urlSuffix: String): Upload = - HttpURLConnectionUpload(pool, urlSuffix) -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/OkhttpUpload.scala b/modules/publish/src/main/scala/coursier/publish/upload/OkhttpUpload.scala deleted file mode 100644 index 0ae91f2f29..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/OkhttpUpload.scala +++ /dev/null @@ -1,122 +0,0 @@ -package coursier.publish.upload - -import java.util.concurrent.{ExecutorService, TimeUnit} - -import coursier.cache.CacheUrl -import coursier.core.Authentication -import coursier.publish.upload.logger.UploadLogger -import coursier.util.Task -import okhttp3.{MediaType, OkHttpClient, Request, RequestBody} -import okio.BufferedSink - -import scala.jdk.CollectionConverters._ -import scala.util.Try - -final case class OkhttpUpload( - client: OkHttpClient, - pool: ExecutorService, - expect100Continue: Boolean, - urlSuffix: String -) extends Upload { - - import OkhttpUpload.mediaType - import coursier.publish.download.OkhttpDownload.TryOps - - def upload( - url: String, - authentication: Option[Authentication], - content: Array[Byte], - logger: UploadLogger, - loggingIdOpt: Option[Object] - ): Task[Option[Upload.Error]] = { - - val body: RequestBody = - new RequestBody { - def contentType(): MediaType = - mediaType - def writeTo(sink: BufferedSink): Unit = { - var n = 0 - logger.progress(url, loggingIdOpt, n, content.length) - while (n < content.length) { - val len = Math.min(16384, content.length - n) - sink.write(content, n, len) - n += len - logger.progress(url, loggingIdOpt, n, content.length) - } - } - } - - val request = { - val b = new Request.Builder() - .url(url + urlSuffix) - - if (expect100Continue) - b.addHeader("Expect", "100-continue") - - b.put(body) - - // Handling this ourselves rather than via client.setAuthenticator / com.squareup.okhttp.Authenticator - for (auth <- authentication; (k, v) <- auth.allHttpHeaders) - b.addHeader(k, v) - - b.build() - } - - Task.schedule(pool) { - logger.uploading(url, loggingIdOpt, Some(content.length)) - - val res = Try { - val response = client.newCall(request).execute() - - if (response.isSuccessful) - None - else { - val code = response.code() - if (code == 401) { - val realmOpt = Option(response.header("WWW-Authenticate")).collect { - case CacheUrl.BasicRealm(r) => r - } - Some(new Upload.Error.Unauthorized(url, realmOpt)) - } - else { - val content = Try(response.body().string()).getOrElse("") - Some( - new Upload.Error.HttpError( - code, - response.headers().toMultimap.asScala.mapValues(_.asScala.toList).iterator.toMap, - content - ) - ) - } - } - } - - logger.uploaded( - url, - loggingIdOpt, - res.toEither.fold(e => Some(new Upload.Error.UploadError(url, e)), x => x) - ) - - res.get - } - } -} - -object OkhttpUpload { - private val mediaType = MediaType.parse("application/octet-stream") - - private def client(): OkHttpClient = - new OkHttpClient.Builder() - .readTimeout(60L, TimeUnit.SECONDS) - .build() - - def create(pool: ExecutorService): Upload = - // Seems we can't even create / shutdown the client thread pool (via its Dispatcher)… - OkhttpUpload(client(), pool, expect100Continue = false, "") - def create(pool: ExecutorService, expect100Continue: Boolean): Upload = - // Seems we can't even create / shutdown the client thread pool (via its Dispatcher)… - OkhttpUpload(client(), pool, expect100Continue, "") - def create(pool: ExecutorService, expect100Continue: Boolean, urlSuffix: String): Upload = - // Seems we can't even create / shutdown the client thread pool (via its Dispatcher)… - OkhttpUpload(client(), pool, expect100Continue, urlSuffix) -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/Upload.scala b/modules/publish/src/main/scala/coursier/publish/upload/Upload.scala deleted file mode 100644 index 844747608b..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/Upload.scala +++ /dev/null @@ -1,133 +0,0 @@ -package coursier.publish.upload - -import coursier.publish.Content -import coursier.publish.fileset.{FileSet, Path} -import coursier.core.Authentication -import coursier.maven.MavenRepository -import coursier.publish.upload.logger.UploadLogger -import coursier.util.Task - -/** Uploads / sends content to a repository. - */ -trait Upload { - - // TODO Support chunked content? - - /** Uploads content at the passed `url`. - * - * @param url: - * URL to upload content at - * @param authentication: - * optional authentication parameters - * @param content: - * content to upload - * @param logger - * @return - * an optional [[Upload.Error]], non-empty in case of error - */ - def upload( - url: String, - authentication: Option[Authentication], - content: Array[Byte], - logger: UploadLogger, - loggingIdOpt: Option[Object] - ): Task[Option[Upload.Error]] - - final def upload( - url: String, - authentication: Option[Authentication], - content: Array[Byte], - logger: UploadLogger - ): Task[Option[Upload.Error]] = - upload(url, authentication, content, logger, None) - - /** Uploads a whole [[FileSet]]. - * - * @param repository - * @param fileSet - * @param logger - * @return - */ - final def uploadFileSet( - repository: MavenRepository, - fileSet: FileSet, - logger: UploadLogger, - parallel: Boolean - ): Task[Seq[(Path, Content, Upload.Error)]] = { - - val baseUrl0 = repository.root - - // TODO Add exponential back off for transient errors - - // uploading stuff sequentially for now - // stops at first error - def doUpload(id: Object): Task[Seq[(Path, Content, Upload.Error)]] = { - - val tasks = fileSet - .elements - .map { - case (path, content) => - val url = s"$baseUrl0/${path.elements.mkString("/")}" - content.contentTask.flatMap(b => - upload(url, repository.authentication, b, logger, Some(id)) - .map(_.map((path, content, _))) - ) - } - - if (parallel) - Task.gather.gather(tasks).map { l => - l.flatten - } - else - tasks - .foldLeft(Task.point(Option.empty[(Path, Content, Upload.Error)])) { - case (acc, task) => - for { - previousErrorOpt <- acc - errorOpt <- previousErrorOpt.fold(task)(e => Task.point(Some(e))) - } yield errorOpt - } - .map(_.toSeq) - } - - val before = Task.delay { - val id = new Object - logger.start() - logger.uploadingSet(id, fileSet) - id - } - - def after(id: Object) = Task.delay { - logger.uploadedSet(id, fileSet) - logger.stop() - } - - for { - id <- before - a <- doUpload(id).attempt - _ <- after(id) - res <- Task.fromEither(a) - } yield res - } -} - -object Upload { - - sealed abstract class Error(val transient: Boolean, message: String, cause: Throwable = null) - extends Exception(message, cause) - - object Error { - final class HttpError(code: Int, headers: Map[String, Seq[String]], response: String) - extends Error(transient = code / 100 == 5, s"HTTP $code\n$response") - final class Unauthorized(url: String, realm: Option[String]) - extends Error(transient = false, s"Unauthorized ($url, ${realm.getOrElse("[no realm]")})") - final class UploadError(url: String, exception: Throwable) - extends Error(transient = false, s"Error uploading $url", exception) - final class FileException(exception: Throwable) extends Error( - transient = false, - "I/O error", - exception - ) // can some exceptions be transient? - } - -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/logger/BatchUploadLogger.scala b/modules/publish/src/main/scala/coursier/publish/upload/logger/BatchUploadLogger.scala deleted file mode 100644 index 2b00986fd9..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/logger/BatchUploadLogger.scala +++ /dev/null @@ -1,34 +0,0 @@ -package coursier.publish.upload.logger - -import java.io.PrintStream - -import coursier.publish.fileset.FileSet -import coursier.publish.upload.Upload - -final class BatchUploadLogger(out: PrintStream, dummy: Boolean, isLocal: Boolean) - extends UploadLogger { - - private val processing = - if (isLocal) - if (dummy) - "Would have tried to write" - else - "Writing" - else if (dummy) - "Would have tried to upload" - else - "Uploading" - - override def uploadingSet(id: Object, fileSet: FileSet): Unit = - out.println(s"$processing ${fileSet.elements.length} files") - - override def uploading(url: String, idOpt: Option[Object], totalOpt: Option[Long]): Unit = - out.println(s"Uploading $url") - override def uploaded(url: String, idOpt: Option[Object], errorOpt: Option[Upload.Error]): Unit = - errorOpt match { - case None => - out.println(s"Uploaded $url") - case Some(err) => - out.println(s"Failed to upload $url: $err") - } -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/logger/InteractiveUploadLogger.scala b/modules/publish/src/main/scala/coursier/publish/upload/logger/InteractiveUploadLogger.scala deleted file mode 100644 index 6dc4b59730..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/logger/InteractiveUploadLogger.scala +++ /dev/null @@ -1,53 +0,0 @@ -package coursier.publish.upload.logger - -import java.io.{OutputStream, OutputStreamWriter, Writer} - -import coursier.publish.fileset.FileSet -import coursier.publish.logging.ProgressLogger -import coursier.publish.upload.Upload - -// FIXME Would have been better if dummy was passed by the Upload instance when calling the methods of UploadLogger -final class InteractiveUploadLogger(out: Writer, dummy: Boolean, isLocal: Boolean) - extends UploadLogger { - - private val underlying = new ProgressLogger[Object]( - if (isLocal) - if (dummy) - "Would have written" - else - "Wrote" - else if (dummy) - "Would have uploaded" - else - "Uploaded", - "files", - out, - doneEmoji = Some("\ud83d\ude9a") - ) - - override def uploadingSet(id: Object, fileSet: FileSet): Unit = - underlying.processingSet(id, Some(fileSet.elements.length)) - override def uploadedSet(id: Object, fileSet: FileSet): Unit = - underlying.processedSet(id) - - override def uploading(url: String, idOpt: Option[Object], totalOpt: Option[Long]): Unit = - for (id <- idOpt) - underlying.processing(url, id) - - override def progress(url: String, idOpt: Option[Object], uploaded: Long, total: Long): Unit = - for (id <- idOpt) - underlying.progress(url, id, uploaded, total) - override def uploaded(url: String, idOpt: Option[Object], errorOpt: Option[Upload.Error]): Unit = - for (id <- idOpt) - underlying.processed(url, id, errorOpt.nonEmpty) - - override def start(): Unit = - underlying.start() - override def stop(keep: Boolean): Unit = - underlying.stop(keep) -} - -object InteractiveUploadLogger { - def create(out: OutputStream, dummy: Boolean, isLocal: Boolean): UploadLogger = - new InteractiveUploadLogger(new OutputStreamWriter(out), dummy, isLocal) -} diff --git a/modules/publish/src/main/scala/coursier/publish/upload/logger/UploadLogger.scala b/modules/publish/src/main/scala/coursier/publish/upload/logger/UploadLogger.scala deleted file mode 100644 index 49e3a5daa9..0000000000 --- a/modules/publish/src/main/scala/coursier/publish/upload/logger/UploadLogger.scala +++ /dev/null @@ -1,15 +0,0 @@ -package coursier.publish.upload.logger - -import coursier.publish.fileset.FileSet -import coursier.publish.upload.Upload.Error - -trait UploadLogger { - def uploadingSet(id: Object, fileSet: FileSet): Unit = () - def uploadedSet(id: Object, fileSet: FileSet): Unit = () - def uploading(url: String, idOpt: Option[Object], totalOpt: Option[Long]): Unit = () - def progress(url: String, idOpt: Option[Object], uploaded: Long, total: Long): Unit = () - def uploaded(url: String, idOpt: Option[Object], errorOpt: Option[Error]): Unit = () - - def start(): Unit = () - def stop(keep: Boolean = true): Unit = () -} diff --git a/project/deps.sc b/project/deps.sc index 391520d8e6..d27073d9fd 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -30,7 +30,6 @@ object Deps { def mdoc = ivy"org.scalameta::mdoc:2.3.2" def monadlessCats = ivy"io.monadless::monadless-cats:${Versions.monadless}" def monadlessStdlib = ivy"io.monadless::monadless-stdlib:${Versions.monadless}" - def okhttp = ivy"com.squareup.okhttp3:okhttp:3.14.9" def osLib = ivy"com.lihaoyi::os-lib:0.8.1" def plexusArchiver = ivy"org.codehaus.plexus:plexus-archiver:4.2.7" // plexus-archiver needs its loggers