From ee2e274bc5d576a03532b54c72bd9b3a3954a574 Mon Sep 17 00:00:00 2001 From: Tim Nieradzik Date: Sun, 23 Feb 2020 19:00:10 +0000 Subject: [PATCH] Implement publishing to Bintray (#77) This introduces two user-facing features: the ability to publish and document modules. The changes resulted in the stabilisation of the artefact resolution and progress bar logic. Furthermore, the `import` keyword is now available in the global Seed configuration file. With the new CLI command `publish`, modules can be published to a repository. It builds, packages and uploads the specified modules. At the moment, only Maven-style Bintray repositories are supported. For each supplied module, JAR artefacts along with a POM file are created. These artefacts contain the compiled classes, source files and documentation. Publishing of sources and documentation is optional and can be disabled. Progress bars are shown when publishing modules. Unless specified, Seed will attempt to read the version from the project's Git repository. Internally, the feature uses the Apache HTTP client, wrapped as a ZIO task, and sends requests to the [Bintray REST API](https://bintray.com/docs/api/). The newly-introduced CLI command `doc` runs Scaladoc on the supplied modules and generates an HTML documentation for them. Seed runs the Scala compiler directly, bypassing Bloop. This results in an additional compilation pass. Compiler bridges are provided for Scala 2.11, 2.12 and 2.13. However, the actual resolution of the `scala-compiler` artefact occurs during runtime. Therefore, documentation generation is compatible with alternative compilers such as Typelevel Scala. The aforementioned features required changes to the artefact resolution. Previously, all dependencies were resolved at once. However, modular projects are likely to contain libraries with diverging versions. A resolution pass in Coursier merges these duplicate libraries, only retaining the latest version. This may lead to unexpected compile- and runtime behaviour. The new resolution logic solves this limitation by performing a separate resolution pass for each module. Test modules are resolved separately too. As a consequence of these changes, the resolution is slightly slower, but still acceptable on larger projects. A further change is that the standard library's organisation and version are forcibly set during dependency resolution by using Coursier's `ResolutionParams` feature. This avoids having to patch artefacts later on, e.g. during generation. The progress bar logic was refactored and several bugs were fixed. Notably, as build targets may run asynchronously, they can produce output while the project is still compiling. In this case, the progress bar output would get corrupted. The graphical glitch was resolved by using the console output's logger in `seed.cli.BuildTarget.buildTargets()`. Finally, the `import` keyword, previously only available in build files, can now be used within the global Seed configuration. Since Bintray repository credentials are part of this file, users may want to move the credentials to a custom file and import it instead. --- .drone.yml | 2 +- README.md | 122 +++- build.sbt | 75 +- release.sh | 3 + .../scala/seed/publish/util/Scaladoc.scala | 188 +++++ src/main/scala/seed/Cli.scala | 126 +++- .../seed/artefact/ArtefactResolution.scala | 335 ++++++--- src/main/scala/seed/artefact/Coursier.scala | 113 ++- src/main/scala/seed/build/Bloop.scala | 210 ++---- src/main/scala/seed/cli/Build.scala | 6 +- src/main/scala/seed/cli/Doc.scala | 293 ++++++++ src/main/scala/seed/cli/Generate.scala | 37 +- src/main/scala/seed/cli/Link.scala | 7 +- src/main/scala/seed/cli/Package.scala | 121 ++-- src/main/scala/seed/cli/Publish.scala | 663 ++++++++++++++++++ src/main/scala/seed/cli/Run.scala | 5 +- .../scala/seed/cli/util/ConsoleOutput.scala | 74 +- src/main/scala/seed/cli/util/Module.scala | 8 + .../scala/seed/cli/util/ProgressBars.scala | 92 +++ src/main/scala/seed/cli/util/Target.scala | 3 +- src/main/scala/seed/config/BuildConfig.scala | 45 +- src/main/scala/seed/config/SeedConfig.scala | 38 +- .../scala/seed/config/util/TomlUtils.scala | 19 +- src/main/scala/seed/generation/Bloop.scala | 71 +- src/main/scala/seed/generation/Idea.scala | 80 ++- src/main/scala/seed/generation/Package.scala | 10 +- .../seed/generation/util/ScalaCompiler.scala | 54 +- src/main/scala/seed/model/Build.scala | 20 + src/main/scala/seed/model/Config.scala | 5 +- src/main/scala/seed/model/Licence.scala | 83 +++ src/main/scala/seed/publish/Bintray.scala | 89 +++ .../util/CompletableHttpAsyncClient.java | 56 ++ src/main/scala/seed/publish/util/Http.scala | 77 ++ src/main/scala/seed/util/ZioHelpers.scala | 38 + .../artefact/ArtefactResolutionSpec.scala | 297 ++++++-- .../scala/seed/artefact/CoursierSpec.scala | 33 + src/test/scala/seed/cli/DocSpec.scala | 69 ++ .../seed/cli/util/ConsoleOutputSpec.scala | 57 ++ src/test/scala/seed/cli/util/TargetSpec.scala | 10 +- .../scala/seed/config/BuildConfigSpec.scala | 2 +- .../generation/BloopIntegrationSpec.scala | 3 +- src/test/scala/seed/generation/IdeaSpec.scala | 96 +-- .../generation/util/ProjectGeneration.scala | 59 +- test/compiler-options/jvm/src/Main.scala | 3 + test/resolve-dep-versions/build.toml | 26 + 45 files changed, 3139 insertions(+), 684 deletions(-) create mode 100644 scaladoc/src/main/scala/seed/publish/util/Scaladoc.scala create mode 100644 src/main/scala/seed/cli/Doc.scala create mode 100644 src/main/scala/seed/cli/Publish.scala create mode 100644 src/main/scala/seed/cli/util/Module.scala create mode 100644 src/main/scala/seed/cli/util/ProgressBars.scala create mode 100644 src/main/scala/seed/model/Licence.scala create mode 100644 src/main/scala/seed/publish/Bintray.scala create mode 100644 src/main/scala/seed/publish/util/CompletableHttpAsyncClient.java create mode 100644 src/main/scala/seed/publish/util/Http.scala create mode 100644 src/main/scala/seed/util/ZioHelpers.scala create mode 100644 src/test/scala/seed/cli/DocSpec.scala create mode 100644 src/test/scala/seed/cli/util/ConsoleOutputSpec.scala create mode 100644 test/compiler-options/jvm/src/Main.scala create mode 100644 test/resolve-dep-versions/build.toml diff --git a/.drone.yml b/.drone.yml index 638a45c..ea73a90 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,7 @@ steps: - export PATH=$(pwd)/bloop:$PATH - blp-server & - ./build.sh - - COURSIER_SBT_LAUNCHER_ADD_PLUGIN=true ./csbt test + - COURSIER_SBT_LAUNCHER_ADD_PLUGIN=true ./csbt "; scaladoc211/publishLocal; scaladoc212/publishLocal; scaladoc213/publishLocal; test" - name: publish_prerelease image: plugins/docker environment: diff --git a/README.md b/README.md index 030bb9a..b8ec2dd 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,10 @@ This compiles the module to `build/` and runs it. * Unicode characters * Progress bars * Project creation wizard -* Packaging support +* Generate module documentation +* Publish artefacts to Bintray + * Read version from Git repository +* Package modules * Copy over dependencies * Server mode * Expose a WebSocket server @@ -528,7 +531,7 @@ For two equivalent examples of using code generation, please refer to these link * [Command target](test/custom-command-target/) ### Compiler plug-ins -Project and modules can add Scala plug-ins with `compilerDeps`. The setting behaves like `scalaDeps`, but also adds the `-Xplugin` parameter to the Scala compiler when modules are compiled. For example: +Scala plug-ins can be included with the `compilerDeps` setting. It behaves similar to `scalaDeps`, but adds the `-Xplugin` parameter to the Scala compiler. `compilerDeps` is available on the project as well as module level: ```toml [project] @@ -542,8 +545,9 @@ compilerDeps = [ ] ``` -Note that project-level plug-ins are inherited by all modules defined under the project, as well as any dependent modules such that `compilerDeps` only needs to be defined on the base project or module. -In the example above, `module.macros.js` inherits the semanticdb plug-in from the *project* and adds a separate dependency on the macro paradise plug-in. +Note that project-level plug-ins are inherited by all modules defined in the project file. When module `a` depends on module `b` which defines compiler plug-ins, these are inherited by `a`. Thus, in order to avoid duplication, shared compiler dependencies can be defined on base projects and modules. + +In the example above, `module.macros.js` inherits the SemanticDB plug-in from the project and adds a separate dependency for the Macro Paradise plug-in. For a complete cross-compiled Macro Paradise example, please refer to [this project](test/example-paradise/). @@ -613,6 +617,52 @@ ivy = [ ] ``` +### Package +When publishing artefacts, a [Maven POM](http://maven.apache.org/pom.html) file is created. To configure its contents, define a `package` section: + +```toml +[package] +# Project name (`name` field) +# name = "example" +name = "" + +# Package organisation (`groupId` field) +# organisation = "com.smith" +organisation = "" + +# List of developers (ID, name, e-mail) +# developers = [{ id = "joesmith", name = "Joe Smith", email = "joe@smith.com" }] +# developers = [["joesmith", "Joe Smith", "joe@smith.com"]] +developers = [] + +# Project URL +url = "" + +# List of project licences +# Available values: +# gpl:2.0, gpl:3.0, lgpl:2.1, lgpl:3.0, cddl:1.0, cddl+gpl, apache:2.0, bsd:2, +# bsd:3, mit, epl:1.0, ecl:1.0, mpl:2.0 +# licences = ["apache:2.0"] +licences = [] + +# Source code information +# `developerConnection` is optional and if omitted, has the same value as `connection` +#scm = { +# url = "https://github.com/joesmith/example", +# connection = "scm:git:git@github.com:joesmith/joesmith.git", +# developerConnection = "scm:git:git@github.com:joesmith/joesmith.git" +#} +scm = { url = "", connection = "", developerConnection = "" } + +# Publish sources +# Alternatively, use --skip-sources to override this setting +sources = true + +# Generate and publish documentation +# Alternatively, use --skip-docs to override this setting +docs = true +``` + ### Sample configurations You can take some inspiration from the following projects: @@ -683,6 +733,20 @@ progress = true The default values are indicated. +### Publishing settings +```toml +# Bintray repository credentials +[repository.bintray] +user = "" +apiKey = "" +``` + +If you maintain your configuration files in a public Git repository, it is advisable to move the Bintray section to a separate untracked file (e.g. `seed-credentials.toml`) and import it from the main configuration: + +```toml +import = ["seed-credentials.toml"] +``` + ## Git ### .gitignore For a Seed project, `.gitignore` only needs to contain these four directories: @@ -825,6 +889,15 @@ There are two related issues tracking the testing problems: [issue SCL-8972](htt As a workaround, you can open a terminal within IntelliJ and use Bloop, for example: `bloop test -js` +## Generate documentation +Seed can generate an HTML documentation for your modules with the `doc` command: + +```shell +seed doc example:jvm example:js +``` + +The functionality is based on Scaladoc. For each specified module, it uses the corresponding Scala compiler. Scaladoc bypasses Bloop and performs a separate non-incremental compilation pass. + ## Packaging In order to distribute your project, you may want to package the compiled sources. The approach chosen in Seed is to bundle them as a JAR file. @@ -872,6 +945,34 @@ If you would like to use pre-release versions, you can also pass in this paramet seed update --pre-releases ``` +## Publishing +Seed can publish modules to Maven-style Bintray repositories. First, populate the build file's `package` section. Then run the following command: + +```shell +seed publish --version=1.0 bintray:joesmith/maven/releases demo:js demo:jvm +``` + +The credentials must be configured in the global Seed configuration: + +```toml +[repository.bintray] +user = "joesmith" +apiKey = "" +``` + +Alternatively, Seed reads the environment variables `BINTRAY_USER` and `BINTRAY_API_KEY`. + +Seed publishes sources and generates the documentation, which slows down the publishing process. If any of these artefacts are unneeded, you can permanently change the behaviour via `package.sources` and `package.docs` in the build file, or temporarily by passing `--skip-sources` and `--skip-docs` to the CLI. + +Finally, to depend on the published library in another project, add the Bintray resolver there: + +```toml +[resolvers] +maven = ["https://repo1.maven.org/maven2", "https://dl.bintray.com/joesmith/maven"] +``` + +The `--version` parameter is optional. By default, Seed reads the version number from the Git repository via `git describe --tags`. This allows publishing artefacts for every single commit as part of CI executions. + ## Performance On average, Bloop project generation and compilation are roughly 3x faster in Seed compared to sbt in non-interactive mode. Seed's startup is 10x faster than sbt's. @@ -911,7 +1012,7 @@ Seed does not offer any capability for writing plug-ins. If you would like to re If some settings of the build are dynamic, you could write a script to generate TOML files from a template. A use case would be to cross-compile your modules for different Scala versions. Cross-compilation between Scala versions may require code changes. It is thinkable to have the `build.toml` point to the latest supported Scala version and have scripts that downgrade the sources, e.g. using a tool like [scalafix](https://scalacenter.github.io/scalafix/). ### Publishing -Publishing libraries is not possible yet, but Bintray/Sonatype support is planned for future versions. +At the moment, artefacts can only be published to Bintray. Sonatype support is planned for future versions. ## Design Goals The three overarching design goals were usability, simplicity and speed. The objective for Seed is to offer a narrow, but well-designed feature set that covers most common use cases for library and application development. This means leaving out features such as plug-ins, shell, tasks and code execution that would have a high footprint on the design. @@ -932,6 +1033,17 @@ The output could use colours and bold/italic/underlined text. Also, components s [error] Trace: module → targets ``` +## Development +To run all test cases, the Scaladoc bridges must be published locally after each commit: + +```shell +$ sbt +scaladoc211/publishLocal +scaladoc212/publishLocal +scaladoc213/publishLocal +test +``` + ## Credits Seed achieves its simplicity by delegating much of the heavy lifting to external tools and libraries, notably Bloop and Coursier. The decision of using TOML for configuration and the build schema are influenced by [Cargo](https://github.com/rust-lang/cargo). diff --git a/build.sbt b/build.sbt index 8c03c39..0002ba5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,13 +12,17 @@ def parseVersion(file: Path): Option[String] = .find(_.nonEmpty) .map(_.trim) -def seedVersion = +val seedOrganisation = "tindzk" + +// Should be `val` instead of `def`, otherwise it is necessary to run +// scaladoc*/publishLocal after every commit +val seedVersion = parseVersion(Paths.get("SEED")) // CI .getOrElse(Seq("git", "describe", "--tags").!!.trim) // Local development -def bloopVersion = parseVersion(Paths.get("BLOOP")).get -def bloopCoursierVersion = parseVersion(Paths.get("COURSIER")).get +val bloopVersion = parseVersion(Paths.get("BLOOP")).get +val bloopCoursierVersion = parseVersion(Paths.get("COURSIER")).get -organization := "tindzk" +organization := seedOrganisation version := seedVersion scalaVersion := "2.12.4-bin-typelevel-4" scalaOrganization := "org.typelevel" @@ -31,9 +35,10 @@ Compile / sourceGenerators += Def.task { s"""package seed | |object BuildInfo { - | val Version = "$seedVersion" - | val Bloop = "$bloopVersion" - | val Coursier = "$bloopCoursierVersion" + | val Organisation = "$seedOrganisation" + | val Version = "$seedVersion" + | val Bloop = "$bloopVersion" + | val Coursier = "$bloopCoursierVersion" |}""".stripMargin ) @@ -51,6 +56,7 @@ libraryDependencies ++= Seq( "ch.epfl.scala" % "directory-watcher" % "0.8.0+6-f651bd93", "com.joefkelley" %% "argyle" % "1.0.0", "org.scalaj" %% "scalaj-http" % "2.4.2", + "org.apache.httpcomponents" % "httpasyncclient" % "4.1.4", "dev.zio" %% "zio" % "1.0.0-RC14", "dev.zio" %% "zio-streams" % "1.0.0-RC14", "io.circe" %% "circe-core" % "0.11.1", @@ -71,7 +77,60 @@ Global / cancelable := true testFrameworks += new TestFramework("minitest.runner.Framework") licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")) -bintrayVcsUrl := Some("git@github.com:tindzk/seed.git") +bintrayVcsUrl := Some(s"git@github.com:$seedOrganisation/seed.git") publishArtifact in (Compile, packageDoc) := false publishArtifact in (Compile, packageSrc) := false + +lazy val scaladoc211 = project + .in(file("scaladoc211")) + .settings( + organization := seedOrganisation, + version := seedVersion, + name := "seed-scaladoc", + scalaVersion := "2.11.12", + libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.11.12" % Provided, + // Publish artefact without the standard library. The correct version of + // scala-library will be resolved during runtime + pomPostProcess := dropScalaLibraries, + Compile / unmanagedSourceDirectories += baseDirectory.value / ".." / "scaladoc" / "src" / "main" / "scala" + ) + +lazy val scaladoc212 = project + .in(file("scaladoc212")) + .settings( + organization := seedOrganisation, + version := seedVersion, + name := "seed-scaladoc", + scalaVersion := "2.12.10", + libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.12.10" % Provided, + pomPostProcess := dropScalaLibraries, + Compile / unmanagedSourceDirectories += baseDirectory.value / ".." / "scaladoc" / "src" / "main" / "scala" + ) + +lazy val scaladoc213 = project + .in(file("scaladoc213")) + .settings( + organization := seedOrganisation, + version := seedVersion, + name := "seed-scaladoc", + scalaVersion := "2.13.1", + libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.13.1" % Provided, + pomPostProcess := dropScalaLibraries, + Compile / unmanagedSourceDirectories += baseDirectory.value / ".." / "scaladoc" / "src" / "main" / "scala" + ) + +// From https://stackoverflow.com/questions/27835740/sbt-exclude-certain-dependency-only-during-publish +import scala.xml.{Node => XmlNode, NodeSeq => XmlNodeSeq, _} +import scala.xml.transform.{RewriteRule, RuleTransformer} +def dropScalaLibraries(node: XmlNode): XmlNode = + new RuleTransformer(new RewriteRule { + override def transform(node: XmlNode): XmlNodeSeq = node match { + case e: Elem + if e.label == "dependency" && e.child.exists( + child => child.label == "groupId" && child.text == "org.scala-lang" + ) => + XmlNodeSeq.Empty + case _ => node + } + }).transform(node).head diff --git a/release.sh b/release.sh index 5fc518b..c175b34 100755 --- a/release.sh +++ b/release.sh @@ -1,4 +1,7 @@ #!/bin/sh +set -e +set -x + version=$1 /usr/bin/git tag -f $version diff --git a/scaladoc/src/main/scala/seed/publish/util/Scaladoc.scala b/scaladoc/src/main/scala/seed/publish/util/Scaladoc.scala new file mode 100644 index 0000000..a750183 --- /dev/null +++ b/scaladoc/src/main/scala/seed/publish/util/Scaladoc.scala @@ -0,0 +1,188 @@ +package seed.publish.util + +import java.io.File +import java.util.function.Consumer + +import scala.reflect.internal.util.Position +import scala.tools.nsc.doc.{DocFactory, Settings} +import scala.tools.nsc.reporters.Reporter + +/** @author Gilles Dubochet, Stephane Micheloud */ +class Scaladoc { + + /** Transforms a file into a scalac-readable string + * + * @param file file to convert + * @return string representation of the file like `/x/k/a.scala` + */ + private def asString(file: File): String = file.getAbsolutePath + + /** Transforms a path into a scalac-readable string + * + * @param path path to convert + * @return string representation of the path like `a.jar:b.jar` + */ + private def asString(path: Array[File]): String = + path.toList.map(asString).mkString("", File.pathSeparator, "") + + private object Defaults { + + /** character encoding of the files to compile */ + val encoding: Option[String] = None + + /** fully qualified name of a doclet class, which will be used to generate the documentation */ + val docGenerator: Option[String] = None + + /** file from which the documentation content of the root package will be taken */ + val docRootContent: Option[File] = None + + /** document title of the generated HTML documentation */ + val docTitle: Option[String] = None + + /** document footer of the generated HTML documentation */ + val docFooter: Option[String] = None + + /** document version, added to the title */ + val docVersion: Option[String] = None + + /** generate links to sources */ + val docSourceUrl: Option[String] = None + + /** point out uncompilable sources */ + val docUncompilable: Option[String] = None + + /** ciompiler: generate deprecation information */ + val deprecation: Boolean = false + + /** compiler: generate unchecked information */ + val unchecked: Boolean = false + + /** document implicit conversions */ + val docImplicits: Boolean = false + + /** document all (including impossible) implicit conversions */ + val docImplicitsShowAll: Boolean = false + + /** output implicits debugging information */ + val docImplicitsDebug: Boolean = false + + /** create diagrams */ + val docDiagrams: Boolean = false + + /** output diagram creation debugging information */ + val docDiagramsDebug: Boolean = false + + /** use the binary given to create diagrams */ + val docDiagramsDotPath: Option[String] = None + + /** produce textual output from html pages, for easy diff-ing */ + val docRawOutput: Boolean = false + + /** do not generate prefixes */ + val docNoPrefixes: Boolean = false + + /** group similar functions together */ + val docGroups: Boolean = false + + /** skip certain packages */ + val docSkipPackages: String = "" + } + + def settings( + sourcePath: Array[File], + classpath: Array[File], + bootClasspath: Array[File], + externalFolders: Array[File], + destination: File, + addParams: String + ): Settings = { + def error(msg: String): Unit = require(false) + val docSettings = new Settings(error) + docSettings.outdir.value = asString(destination) + if (classpath.nonEmpty) + docSettings.classpath.value = asString(classpath) + if (sourcePath.nonEmpty) + docSettings.sourcepath.value = asString(sourcePath) + if (bootClasspath.nonEmpty) + docSettings.bootclasspath.value = asString(bootClasspath) + if (externalFolders.nonEmpty) + docSettings.extdirs.value = asString(externalFolders) + + import Defaults._ + encoding.foreach(docSettings.encoding.value = _) + docTitle.foreach(docSettings.doctitle.value = _) + docFooter.foreach(docSettings.docfooter.value = _) + docVersion.foreach(docSettings.docversion.value = _) + docSourceUrl.foreach(docSettings.docsourceurl.value = _) + docUncompilable.foreach(docSettings.docUncompilable.value = _) + + docSettings.deprecation.value = deprecation + docSettings.unchecked.value = unchecked + docSettings.docImplicits.value = docImplicits + docSettings.docImplicitsDebug.value = docImplicitsDebug + docSettings.docImplicitsShowAll.value = docImplicitsShowAll + docSettings.docDiagrams.value = docDiagrams + docSettings.docDiagramsDebug.value = docDiagramsDebug + docSettings.docRawOutput.value = docRawOutput + docSettings.docNoPrefixes.value = docNoPrefixes + docSettings.docGroups.value = docGroups + docSettings.docSkipPackages.value = docSkipPackages + docDiagramsDotPath.foreach(docSettings.docDiagramsDotPath.value = _) + + docGenerator.foreach(docSettings.docgenerator.value = _) + docRootContent.foreach( + value => docSettings.docRootContent.value = value.getAbsolutePath + ) + + // Enabled because DocFactory uses println() before 2.13.2 which clashes + // with Seed's progress bars + // See https://github.com/scala/scala/pull/8442 + docSettings.scaladocQuietRun = true + + docSettings.processArgumentString(addParams) + docSettings + } + + def execute( + docSettings: Settings, + sourceFiles: Array[File], + error: Consumer[String], + warn: Consumer[String] + ): java.lang.Boolean = { + val e = error + val w = warn + + var errors = false + + val reporter = new Reporter { + override protected def info0( + pos: Position, + msg: String, + severity: Severity, + force: Boolean + ): Unit = + if (!force) { + if (severity == WARNING) w.accept(msg) + else if (severity == ERROR) { + errors = true + e.accept(msg) + } + } + } + + try { + val docProcessor = new DocFactory(reporter, docSettings) + docProcessor.document(sourceFiles.toList.map(_.toString): List[String]) + !errors + } catch { + case t: Throwable => + Option(t.getMessage) match { + case None => + error.accept("Scaladoc failed due to an internal error") + case Some(msg) => + error.accept(s"Scaladoc failed due to an internal error ($msg)") + } + false + } + } +} diff --git a/src/main/scala/seed/Cli.scala b/src/main/scala/seed/Cli.scala index 8fffef6..0d44fe9 100644 --- a/src/main/scala/seed/Cli.scala +++ b/src/main/scala/seed/Cli.scala @@ -6,9 +6,10 @@ import scala.util.{Failure, Success, Try} import com.joefkelley.argyle._ import com.joefkelley.argyle.reader.Reader import seed.artefact.Coursier -import seed.cli.util.{Ansi, ColourScheme} +import seed.cli.util.{Ansi, ColourScheme, RTS} import seed.config.{BuildConfig, SeedConfig} import seed.cli.util.ArgyleHelpers._ +import seed.model.Build.Resolvers object Cli { case class PackageConfig( @@ -55,6 +56,19 @@ object Cli { output: Option[Path], module: String ) extends Command + case class Publish( + packageConfig: PackageConfig, + version: Option[String], + skipSources: Boolean, + skipDocs: Boolean, + modules: List[String], + target: String + ) extends Command + case class Doc( + packageConfig: PackageConfig, + output: Option[Path], + modules: List[String] + ) extends Command sealed trait Generate extends Command { def packageConfig: PackageConfig @@ -139,6 +153,21 @@ object Cli { .and(requiredFree[String]) .to[Command.Package] + val publishCommand = + packageConfigArg + .and(optional[String]("--version")) + .and(flag("--skip-sources")) + .and(flag("--skip-docs")) + .and(repeatedAtLeastOnceFree[String]) + .and(requiredFree[String]) + .to[Command.Publish] + + val docCommand = + packageConfigArg + .and(optional[Path]("--output")) + .and(repeatedAtLeastOnceFree[String]) + .to[Command.Doc] + val cliArgs = optional[Path]("--config") .and(optional[Path]("--build").default(Paths.get(""))) @@ -156,7 +185,9 @@ object Cli { "link" -> linkCommand, "buildEvents" -> buildEventsCommand, "update" -> flag("--pre-releases").to[Command.Update], - "package" -> packageCommand + "package" -> packageCommand, + "publish" -> publishCommand, + "doc" -> docCommand ) ) .to[Config] @@ -183,6 +214,8 @@ ${underlined("Usage:")} seed [--build=] [--config=] ${italic("update")} Check library dependencies for updates ${italic("package")} Create JAR package for given module and its dependencies Also sets the main class from the build file + ${italic("publish")} Publish supplied module(s) to package repository + ${italic("doc")} Generate HTML documentation for supplied module(s) ${bold("Parameters:")} ${italic("--build")} Path to the build file (default: ${Ansi.italic("build.toml")}) @@ -241,7 +274,22 @@ ${underlined("Usage:")} seed [--build=] [--config=] ${italic("--tmpfs")} Read build directory in tmpfs ${italic("--libs")} Copy all library dependencies, reference them in the generated JAR file's class path ${italic("--output")} Output path (default: ${Ansi.italic("build/dist/")}) - ${italic("")} Module to build and package""") + ${italic("")} Module to build and package + + ${bold("Command:")} ${underlined("publish")} [--version=] [--skip-sources] [--skip-docs] + ${italic("--version")} Set artefact version. Must be a semantic version. + If not specified, the version is read from the current Git tag. + ${italic("--skip-sources")} Do not publish sources + ${italic("--skip-docs")} Do not publish documentation + ${italic("")} Target repository + ${italic("Syntax:")} bintray:// + ${italic("")} One or multiple space-separated modules. The syntax of a module is: ${italic("")} or ${italic(":")} + ${italic("Examples:")} + - app Publish all available platform modules of ${Ansi.italic("app")} + - app:js Only publish JavaScript platform module of ${Ansi.italic("app")} + + ${bold("Command:")} ${underlined("doc")} [--output=] + ${italic("--output")} Output path (default: ${Ansi.italic("build/docs/")})""") } // format: on @@ -296,7 +344,7 @@ ${underlined("Usage:")} seed [--build=] [--config=] val result = BuildConfig.load(buildPath, log).getOrElse(sys.exit(1)) val progress = config.cli.progress - cli.Package.ui( + val succeeded = cli.Package.ui( config, result.projectPath, result.resolvers, @@ -308,13 +356,34 @@ ${underlined("Usage:")} seed [--build=] [--config=] packageConfig, log ) + if (!succeeded) log.error("Packaging failed") + sys.exit(if (succeeded) 0 else 1) + case Success(Config(configPath, buildPath, command: Command.Publish)) => + val config = SeedConfig.load(configPath) + val log = Log(config) + val result = + BuildConfig.load(buildPath, log).getOrElse(sys.exit(1)) + cli.Publish.ui( + config, + result.projectPath, + result.resolvers, + result.`package`, + result.build, + command.version, + command.target, + command.modules, + config.cli.progress, + command.skipSources, + command.skipDocs, + command.packageConfig, + log + ) case Success( Config(configPath, buildPath, command: Command.Generate) ) => val config = SeedConfig.load(configPath) val log = Log(config) - val result = - BuildConfig.load(buildPath, log).getOrElse(sys.exit(1)) + val result = BuildConfig.load(buildPath, log).getOrElse(sys.exit(1)) cli.Generate.ui( config, result.projectPath, @@ -340,6 +409,21 @@ ${underlined("Usage:")} seed [--build=] [--config=] val config = SeedConfig.load(configPath) val log = Log(config) cli.Link.ui(buildPath, config, command, log) + case Success(Config(configPath, buildPath, command: Command.Doc)) => + val config = SeedConfig.load(configPath) + val log = Log(config) + val build = BuildConfig.load(buildPath, log).getOrElse(sys.exit(1)) + val uio = cli.Doc.ui( + build, + build.projectPath, + config, + command, + command.packageConfig, + config.cli.progress, + log + ) + val result = RTS.unsafeRunSync(uio) + sys.exit(if (result.succeeded) 0 else 1) case Success(Config(configPath, _, command: Command.BuildEvents)) => val config = SeedConfig.load(configPath) val log = Log(config) @@ -352,4 +436,34 @@ ${underlined("Usage:")} seed [--build=] [--config=] sys.exit(1) } } + + def showResolvers( + seedConfig: seed.model.Config, + resolvers: Resolvers, + packageConfig: PackageConfig, + log: Log + ): Unit = { + import packageConfig._ + val resolvedIvyPath = ivyPath.getOrElse(seedConfig.resolution.ivyPath) + val resolvedCachePath = cachePath.getOrElse(seedConfig.resolution.cachePath) + + log.info("Configured resolvers:") + log.info( + "- " + Ansi.italic(resolvedIvyPath.toString) + " (Ivy)", + detail = true + ) + log.info( + "- " + Ansi.italic(resolvedCachePath.toString) + " (Coursier)", + detail = true + ) + + resolvers.ivy + .foreach( + ivy => log.info("- " + Ansi.italic(ivy.url) + " (Ivy)", detail = true) + ) + resolvers.maven + .foreach( + maven => log.info("- " + Ansi.italic(maven) + " (Maven)", detail = true) + ) + } } diff --git a/src/main/scala/seed/artefact/ArtefactResolution.scala b/src/main/scala/seed/artefact/ArtefactResolution.scala index dce8a1b..4cb5f67 100644 --- a/src/main/scala/seed/artefact/ArtefactResolution.scala +++ b/src/main/scala/seed/artefact/ArtefactResolution.scala @@ -4,11 +4,11 @@ import java.nio.file.Path import seed.Cli.PackageConfig import MavenCentral.{CompilerVersion, PlatformVersion} -import seed.cli.util.Ansi import seed.model.Build.{Dep, JavaDep, Module, Resolvers, ScalaDep} import seed.model.Platform.{JVM, JavaScript, Native} -import seed.model.{Artefact, Platform, Resolution} -import seed.Log +import seed.model.{Artefact, Config, Platform, Resolution} +import seed.{Cli, Log} +import seed.artefact.Coursier.ResolutionResult import seed.config.BuildConfig import seed.config.BuildConfig.Build @@ -53,6 +53,18 @@ object ArtefactResolution { version ) + def scalaLibraryDeps( + scalaOrganisation: String, + scalaVersion: String + ): Set[JavaDep] = + Set( + JavaDep(scalaOrganisation, "scala-library", scalaVersion), + JavaDep(scalaOrganisation, "scala-reflect", scalaVersion) + ) + + def jvmPlatformDeps(module: Module): Set[JavaDep] = + scalaLibraryDeps(module.scalaOrganisation.get, module.scalaVersion.get) + def jsPlatformDeps(module: Module): Set[JavaDep] = { val scalaVersion = module.scalaVersion.get val scalaJsVersion = module.scalaJsVersion.get @@ -68,7 +80,7 @@ object ArtefactResolution { scalaJsVersion, scalaVersion ) - ) + ) ++ scalaLibraryDeps(module.scalaOrganisation.get, module.scalaVersion.get) } def nativePlatformDeps(module: Module): Set[JavaDep] = { @@ -89,7 +101,7 @@ object ArtefactResolution { scalaNativeVersion, scalaVersion ) - ) + ) ++ scalaLibraryDeps(module.scalaOrganisation.get, module.scalaVersion.get) } def nativeLibraryDep(module: Module): JavaDep = { @@ -139,7 +151,7 @@ object ArtefactResolution { .toSet def nativeArtefacts(module: Module): Set[(Platform, Dep)] = - module.scalaDeps.map(dep => Native -> dep).toSet + module.scalaDeps.map(Native -> _).toSet def nativeDeps(module: Module): Set[JavaDep] = module.scalaDeps @@ -154,10 +166,8 @@ object ArtefactResolution { ) .toSet - def compilerDeps(module: Module): List[Set[JavaDep]] = { - def f(module: Module, platform: Platform): Set[JavaDep] = { - val platformModule = BuildConfig.platformModule(module, platform).get - + def compilerDeps(module: Module, platform: Platform): Set[JavaDep] = { + def f(platformModule: Module, platform: Platform): Set[JavaDep] = { val platformVer = BuildConfig.platformVersion(platformModule, platform) val compilerVer = platformModule.scalaVersion.get val organisation = platformModule.scalaOrganisation.get @@ -188,27 +198,95 @@ object ArtefactResolution { ) } - module.targets.map(target => f(module, target)).filter(_.nonEmpty) + BuildConfig.platformModule(module, platform) match { + case None => Set() + case Some(platformModule) => f(platformModule, platform) + } } - def allCompilerDeps(build: Build): List[Set[JavaDep]] = - build.values.toList.flatMap(m => compilerDeps(m.module)).distinct + sealed trait Type + case object Regular extends Type + case object Test extends Type - def platformDeps(build: Build, module: Module): Set[JavaDep] = - module.targets.toSet[Platform].flatMap { target => - if (target == JavaScript) jsPlatformDeps(module.js.get) - else if (target == Native) nativePlatformDeps(module.native.get) - else Set[JavaDep]() - } + /** @return runtime libraries from module and its transitive module dependencies */ + def allRuntimeLibs( + build: Build, + name: String, + platform: Platform, + tpe: Type + ): Set[JavaDep] = { + val m = build(name).module + val pm = BuildConfig.platformModule(m, platform).get + val modules = BuildConfig + .collectModuleDeps( + build, + // TODO should be pm, but module deps are not inherited + m, + Set(platform) + ) + .map(m => build(m).module) + .toSet ++ Set(m) + + if (tpe == Test) + modules.flatMap( + _.test.fold(Set[JavaDep]())(t => libraryDeps(t, Set(platform))) + ) + else + modules.flatMap(libraryDeps(_, Set(platform))) ++ + platformDeps(pm, platform) + } + + /** Perform resolution for all modules separately */ + type ModuleRef = (String, Platform, Type) + def allRuntimeLibs(build: Build): Map[ModuleRef, Set[JavaDep]] = { + val all = build.toList + .flatMap { case (n, m) => m.module.targets.map(n -> _) } + .flatMap { + case (m, p) => + List( + (m, p, Regular: Type) -> allRuntimeLibs(build, m, p, Regular), + (m, p, Test: Type) -> allRuntimeLibs(build, m, p, Test) + ) + } + + val r = all.toMap + require(all.length == r.size) + r + } - def libraryDeps(module: Module, platforms: Set[Platform]): Set[JavaDep] = + type ScalaOrganisation = String + type ScalaVersion = String + + def allCompilerLibs( + build: Build + ): Set[(ScalaOrganisation, ScalaVersion, JavaDep)] = + build.values + .map(_.module) + .flatMap( + m => + m.targets.toSet.flatMap { p => + val pm = BuildConfig.platformModule(m, p).get + compilerDeps(m, p) + .map(d => (pm.scalaOrganisation.get, pm.scalaVersion.get, d)) + } + ) + .toSet + + def platformDeps(module: Module, platform: Platform): Set[JavaDep] = + if (platform == JavaScript) jsPlatformDeps(module) + else if (platform == Native) nativePlatformDeps(module) + else jvmPlatformDeps(module) + + def libraryDeps( + module: Module, + platforms: Set[Platform] = Set(JVM, JavaScript, Native) + ): Set[JavaDep] = (if (!platforms.contains(JVM)) Set() else module.jvm.toSet.flatMap(jvmDeps)) ++ (if (!platforms.contains(JavaScript)) Set() else module.js.toSet.flatMap(jsDeps)) ++ (if (!platforms.contains(Native)) Set() - else module.native.toSet.flatMap(nativeDeps)) ++ - module.test.toSet.flatMap(libraryDeps(_, platforms)) + else module.native.toSet.flatMap(nativeDeps)) def libraryArtefacts(module: Module): Set[(Platform, Dep)] = module.jvm.toSet.flatMap(jvmArtefacts) ++ @@ -216,15 +294,6 @@ object ArtefactResolution { module.native.toSet.flatMap(nativeArtefacts) ++ module.test.toSet.flatMap(libraryArtefacts) - def allPlatformDeps(build: Build): Set[JavaDep] = - build.values.toSet.flatMap(m => platformDeps(build, m.module)) - - def allLibraryDeps( - build: Build, - platforms: Set[Platform] = Set(JVM, JavaScript, Native) - ): Set[JavaDep] = - build.values.toSet.flatMap(m => libraryDeps(m.module, platforms)) - def allLibraryArtefacts(build: Build): Map[Platform, Set[Dep]] = build.values.toSet .flatMap(m => libraryArtefacts(m.module)) @@ -236,109 +305,126 @@ object ArtefactResolution { javaDep.artefact == "scala-reflect" def resolveScalaCompiler( - resolutionResult: List[Coursier.ResolutionResult], + resolution: CompilerResolution, scalaOrganisation: String, scalaVersion: String, userLibraries: List[Resolution.Artefact], - classPath: List[Path], - optionalArtefacts: Boolean + classPath: List[Path] ): Resolution.ScalaCompiler = { require(classPath.length == classPath.distinct.length) val compilerDep = JavaDep(scalaOrganisation, "scala-compiler", scalaVersion) - val libraryDep = JavaDep(scalaOrganisation, "scala-library", scalaVersion) - val reflectDep = JavaDep(scalaOrganisation, "scala-reflect", scalaVersion) - - val resolution = - resolutionResult - .find(r => Coursier.hasDep(r, compilerDep)) - .getOrElse( - throw new Exception(s"Could not find dependency $compilerDep") - ) - val compiler = - Coursier.localArtefacts(resolution, Set(compilerDep), false) - val scalaLibraries = - Coursier.localArtefacts( - resolution, - Set(libraryDep, reflectDep), - optionalArtefacts - ) + val r = resolution.find(Coursier.hasDep(_, compilerDep)) + require(r.isDefined, s"Could not find dependency $compilerDep") - // Replace official scala-library and scala-reflect artefacts by - // organisation-specific ones. This is needed for Typelevel Scala. - val libraries = - scalaLibraries ++ userLibraries.filter(a => !isScalaLibrary(a.javaDep)) - - val compilerArtefacts = - compiler.filter(a => !isScalaLibrary(a.javaDep)) ++ scalaLibraries + val compiler = Coursier.localArtefacts(r.get, Set(compilerDep), false) Resolution.ScalaCompiler( scalaOrganisation, scalaVersion, - libraries, + userLibraries, classPath, - compilerArtefacts.map(_.libraryJar) + compiler.map(_.libraryJar) ) } + def ivyPath( + seedConfig: seed.model.Config, + packageConfig: PackageConfig + ): Path = + packageConfig.ivyPath.getOrElse(seedConfig.resolution.ivyPath) + + def cachePath( + seedConfig: seed.model.Config, + packageConfig: PackageConfig + ): Path = + packageConfig.cachePath.getOrElse(seedConfig.resolution.cachePath) + def resolution( seedConfig: seed.model.Config, resolvers: Resolvers, - build: Build, packageConfig: PackageConfig, + dependencies: Set[JavaDep], + forceScala: (ScalaOrganisation, ScalaVersion), optionalArtefacts: Boolean, - platformDeps: Set[JavaDep], - compilerDeps: List[Set[JavaDep]], log: Log - ) = { - val silent = packageConfig.silent || seedConfig.resolution.silent - - import packageConfig._ - val resolvedIvyPath = ivyPath.getOrElse(seedConfig.resolution.ivyPath) - val resolvedCachePath = cachePath.getOrElse(seedConfig.resolution.cachePath) - - log.info("Configured resolvers:") - log.info( - "- " + Ansi.italic(resolvedIvyPath.toString) + " (Ivy)", - detail = true - ) - log.info( - "- " + Ansi.italic(resolvedCachePath.toString) + " (Coursier)", - detail = true + ): Coursier.ResolutionResult = + Coursier.resolveAndDownload( + dependencies, + forceScala, + resolvers, + ivyPath(seedConfig, packageConfig), + cachePath(seedConfig, packageConfig), + optionalArtefacts, + packageConfig.silent || seedConfig.resolution.silent, + log ) - resolvers.ivy - .foreach( - ivy => log.info("- " + Ansi.italic(ivy.url) + " (Ivy)", detail = true) - ) - resolvers.maven - .foreach( - maven => log.info("- " + Ansi.italic(maven) + " (Maven)", detail = true) - ) - def resolve(deps: Set[JavaDep]) = - Coursier.resolveAndDownload( - deps, - resolvers, - resolvedIvyPath, - resolvedCachePath, - optionalArtefacts, - silent, - log - ) - - log.info("Resolving platform artefacts...") - - val platformResolution = resolve(platformDeps) + type RuntimeResolution = Map[ModuleRef, Coursier.ResolutionResult] - log.info("Resolving compiler artefacts...") + /** + * Coursier merges all library artefacts that occur multiple times in the + * dependency tree, choosing their latest version. Therefore, resolve + * libraries from each module separately. + */ + def runtimeResolution( + build: Build, + seedConfig: seed.model.Config, + resolvers: Resolvers, + packageConfig: PackageConfig, + optionalArtefacts: Boolean, + log: Log + ): RuntimeResolution = { + val all = allRuntimeLibs(build) + all.map { + case (path, libs) => + val (n, p, t) = path + val m = build(n).module + val pm = (if (t == Regular) BuildConfig.platformModule(m, p) + else + m.test match { + case None => BuildConfig.platformModule(m, p) + case Some(t) => BuildConfig.platformModule(t, p) + }).get + val (scalaOrg, scalaVer) = + (pm.scalaOrganisation.get, pm.scalaVersion.get) + + path -> resolution( + seedConfig, + resolvers, + packageConfig, + libs ++ (if (t == Regular) Set() else all((n, p, Regular))), + (scalaOrg, scalaVer), + optionalArtefacts, + log + ) + } + } - // Resolve Scala compilers separately because Coursier merges dependencies - // with different versions - val compilerResolution = compilerDeps.map(resolve) + type CompilerResolution = List[Coursier.ResolutionResult] - (resolvedCachePath, platformResolution, compilerResolution) - } + def compilerResolution( + build: Build, + seedConfig: seed.model.Config, + resolvers: Resolvers, + packageConfig: PackageConfig, + optionalArtefacts: Boolean, + log: Log + ): CompilerResolution = + allCompilerLibs(build).toList + .map { + case (scalaOrganisation, scalaVersion, dep) => + resolution( + seedConfig, + resolvers, + packageConfig, + Set(dep), + (scalaOrganisation, scalaVersion), + optionalArtefacts, + log + ) + } /** @return If there are two dependencies with the same organisation and * artefact name, only retain the last one, regardless of its version. @@ -353,4 +439,41 @@ object ArtefactResolution { case Some(previous) => acc.diff(List(previous)) :+ cur } } + + case class Resolved( + resolution: ResolutionResult, + deps: Map[JavaDep, List[(coursier.Classifier, Coursier.Artefact)]] + ) + + def resolvePackageArtefacts( + seedConfig: Config, + packageConfig: Cli.PackageConfig, + resolvers: Resolvers, + build: Build, + moduleName: String, + platform: Platform, + log: Log + ): Resolved = { + val m = build(moduleName).module + val pm = BuildConfig.platformModule(m, platform).get + + val (scalaOrg, scalaVer) = (pm.scalaOrganisation.get, pm.scalaVersion.get) + + val libs = allRuntimeLibs(build, moduleName, platform, Regular) + + val r = + resolution( + seedConfig, + resolvers, + packageConfig, + libs, + (scalaOrg, scalaVer), + optionalArtefacts = false, + log + ) + + val resolvedLibs = + Coursier.resolveSubset(r.resolution, libs, optionalArtefacts = false) + Resolved(r, resolvedLibs) + } } diff --git a/src/main/scala/seed/artefact/Coursier.scala b/src/main/scala/seed/artefact/Coursier.scala index 718c7d1..efb3f64 100644 --- a/src/main/scala/seed/artefact/Coursier.scala +++ b/src/main/scala/seed/artefact/Coursier.scala @@ -8,7 +8,10 @@ import coursier.ivy.IvyRepository import coursier.paths.CachePath import coursier.cache._ import coursier.cache.loggers._ +import coursier.error.ResolutionError.CantDownloadModule +import coursier.params.ResolutionParams import coursier.util.{Gather, Task} +import seed.artefact.ArtefactResolution.{ScalaOrganisation, ScalaVersion} import seed.cli.util.Ansi import seed.model.Build.{JavaDep, Resolvers} import seed.model.Platform @@ -33,6 +36,8 @@ object Coursier { def withLogger[T](silent: Boolean)(f: CacheLogger => T): T = if (silent) f(CacheLogger.nop) else { + + /** TODO Use [[seed.cli.util.ProgressBars]] **/ val logger = RefreshLogger.create(System.err, ProgressBarRefreshDisplay.create()) logger.init() @@ -65,6 +70,7 @@ object Coursier { def resolve( all: Set[JavaDep], + forceScala: (ScalaOrganisation, ScalaVersion), resolvers: Resolvers, ivyPath: Path, cachePath: Path, @@ -108,18 +114,51 @@ object Coursier { val repositories = ivyRepository +: (resolvers.maven.map(MavenRepository(_)) ++ ivy) - val resolution = - Resolve() + val (scalaOrganisation, scalaVersion) = forceScala + val scalaOrg = Organization(scalaOrganisation) + val forceVersions = + Seq( + Module(scalaOrg, ModuleName("scala-library")) -> scalaVersion, + Module(scalaOrg, ModuleName("scala-reflect")) -> scalaVersion, + Module(scalaOrg, ModuleName("scala-compiler")) -> scalaVersion, + Module(scalaOrg, ModuleName("scalap")) -> scalaVersion + ) + + val resolutionParams = + ResolutionParams() + .withForceVersion(forceVersions.toMap) + .withTypelevel { + // TODO Support other organisations too + val org = model.Organisation.resolve(scalaOrganisation) + if (org.isEmpty) + log.error( + s"Non-standard organisation $scalaOrganisation is not supported by Coursier" + ) + org.contains(model.Organisation.Typelevel) + } + + val resolution: Resolution = + try Resolve() .withDependencies(mapped) .withRepositories(repositories) + .withResolutionParams(resolutionParams) .run() + catch { + case e: CantDownloadModule => + log.error( + s"Could not resolve dependency ${e.module.name.value} in ${e.module.organization.value}" + ) + sys.exit(1) + } val errors = resolution.errors if (errors.nonEmpty) { log.error("Some dependencies could not be resolved:") errors.foreach { case ((module, _), _) => - log.error(s" - ${module.name} in ${module.organization}") + log.error( + s" - ${module.name.value} in ${module.organization.value}" + ) } sys.exit(1) } @@ -159,8 +198,14 @@ object Coursier { .unsafeRun() } - if (localArtefacts.exists(_._2.isLeft)) - log.error("Failed to download: " + localArtefacts.filter(_._2.isLeft)) + val failures = localArtefacts.filter(_._2.isLeft) + if (failures.nonEmpty) { + log.error("Some artefacts could not be downloaded:") + failures.foreach { + case (_, Left(b)) => log.error(" - " + b.describe) + case _ => + } + } localArtefacts.toMap .collect { @@ -172,6 +217,7 @@ object Coursier { def resolveAndDownload( deps: Set[JavaDep], + forceScala: (ScalaOrganisation, ScalaVersion), resolvers: Resolvers, ivyPath: Path, cachePath: Path, @@ -179,7 +225,9 @@ object Coursier { silent: Boolean, log: Log ): ResolutionResult = { - val resolution = resolve(deps, resolvers, ivyPath, cachePath, silent, log) + val resolution = + resolve(deps, forceScala, resolvers, ivyPath, cachePath, silent, log) + val artefacts = resolution .dependencyArtifacts( Some( @@ -249,33 +297,44 @@ object Coursier { (if (sources) Seq(Classifier.sources) else Seq()) ++ (if (javaDoc) Seq(Classifier.javadoc) else Seq()) + def localArtefacts( + result: ResolutionResult, + artefacts: List[(JavaDep, List[(coursier.Classifier, Coursier.Artefact)])] + ): List[model.Resolution.Artefact] = + artefacts.map { + case (dep, a) => + // `a` also contains URLs to POM files + val jars = a.filter(_._2.url.endsWith(".jar")) + + val jar = + result.artefacts(jars.find(_._1 == Classifier.empty).get._2.url) + val doc = jars + .find(_._1 == Classifier.javadoc) + .map(_._2.url) + .flatMap(result.artefacts.get) + val src = jars + .find(_._1 == Classifier.sources) + .map(_._2.url) + .flatMap(result.artefacts.get) + + model.Resolution.Artefact( + javaDep = dep, + libraryJar = jar.toPath, + javaDocJar = doc.map(_.toPath), + sourcesJar = src.map(_.toPath) + ) + } + /** Resolves requested libraries and their dependencies */ def localArtefacts( result: ResolutionResult, all: Set[JavaDep], optionalArtefacts: Boolean ): List[model.Resolution.Artefact] = - resolveSubset(result.resolution, all, optionalArtefacts).toList - .map { - case (dep, a) => - val jar = - result.artefacts(a.find(_._1 == Classifier.empty).get._2.url) - val doc = a - .find(_._1 == Classifier.javadoc) - .map(_._2.url) - .flatMap(result.artefacts.get) - val src = a - .find(_._1 == Classifier.sources) - .map(_._2.url) - .flatMap(result.artefacts.get) - - model.Resolution.Artefact( - javaDep = dep, - libraryJar = jar.toPath, - javaDocJar = doc.map(_.toPath), - sourcesJar = src.map(_.toPath) - ) - } + localArtefacts( + result, + resolveSubset(result.resolution, all, optionalArtefacts).toList + ) /** Resolves path to JAR file of requested artefact */ def artefactPath( diff --git a/src/main/scala/seed/build/Bloop.scala b/src/main/scala/seed/build/Bloop.scala index 7f64047..d3ffb2c 100644 --- a/src/main/scala/seed/build/Bloop.scala +++ b/src/main/scala/seed/build/Bloop.scala @@ -1,13 +1,7 @@ package seed.build import java.net.Socket -import java.util.concurrent.{ - CompletableFuture, - Executor, - Executors, - RejectedExecutionException, - TimeUnit -} +import java.util.concurrent.{Executors, TimeUnit} import com.google.gson.JsonObject import org.eclipse.lsp4j.jsonrpc.Launcher @@ -16,13 +10,16 @@ import seed.cli.util.{ BloopCli, ColourScheme, ConsoleOutput, + Module, ProgressBar, + ProgressBarItem, + ProgressBars, Watcher } import ch.epfl.scala.bsp4j._ import com.google.gson.{Gson, JsonElement} -import scala.collection.{JavaConverters, mutable} +import scala.collection.JavaConverters import java.nio.file.{Files, Path} import org.newsclub.net.unix.{AFUNIXSocket, AFUNIXSocketAddress} @@ -38,6 +35,7 @@ import zio.stream._ import zio.duration._ import scala.concurrent.CancellationException +import seed.util.ZioHelpers._ class BloopClient( consoleOutput: ConsoleOutput, @@ -47,59 +45,38 @@ class BloopClient( allModules: List[(String, Platform)], onBuildEvent: BuildEvent => Unit ) extends BuildClient { + val pb = new ProgressBars( + if (progress) consoleOutput else new ConsoleOutput(Log.silent, _ => ()), + allModules.map { + case (m, p) => + ProgressBarItem( + BuildConfig.targetName(build, m, p), + Module.format(m, p) + ) + } + ) + import consoleOutput.log private val gson: Gson = new Gson() - // Cannot use ListMap here since updating it changes the order of the elements - private val modules = mutable.ListBuffer[(String, ProgressBar.Line)]() - reset() - def reset(): Unit = { - modules.clear() - modules ++= allModules.map { - case (m, p) => - BuildConfig.targetName(build, m, p) -> ProgressBar - .Line( - 0, - ProgressBar.Result.Waiting, - m + " (" + p.caption + ")", - 0, - 0 - ) - } - + pb.reset() lastDiagnosticFilePath = "" } - /** Modules compiled, the upcoming notifications are related to running */ - def compiled: Boolean = consoleOutput.isFlushed - - def printPb(): Unit = - if (!compiled && progress) - consoleOutput.write(ProgressBar.printAll(modules), sticky = true) - - def updatePb(): Unit = { - modules.zipWithIndex.foreach { - case ((id, line), i) => - modules.update(i, id -> line.copy(tick = line.tick + 1)) - } - - printPb() - } - override def onBuildShowMessage(params: ShowMessageParams): Unit = { require(!params.getMessage.endsWith("\n")) log.infoRetainColour("[build] " + params.getMessage) } override def onBuildLogMessage(params: LogMessageParams): Unit = - // Compilation failures of modules is already indicated in the progress bar + // Compilation failures of modules are already indicated in the progress bar if (!params.getMessage.startsWith("Failed to compile ") && !params.getMessage.startsWith("Deduplicating compilation of ")) { require(!params.getMessage.endsWith("\n")) log.infoRetainColour(params.getMessage) - printPb() + pb.printPb() } import scala.collection.JavaConverters._ @@ -153,13 +130,13 @@ class BloopClient( } lastDiagnosticFilePath = filePath - printPb() + pb.printPb() } override def onBuildTargetDidChange(params: DidChangeBuildTarget): Unit = () override def onBuildTaskStart(params: TaskStartParams): Unit = - if (!compiled) { + if (!pb.compiled) { val uri = params.getData .asInstanceOf[JsonObject] .get("target") @@ -180,19 +157,14 @@ class BloopClient( .getAsString val bloopId = uri.split('\u003d').last - val index = modules.indexWhere(_._1 == bloopId) - if (index != -1) { - modules.update( - index, - bloopId -> modules(index)._2.copy( - result = ProgressBar.Result.InProgress, - step = params.getProgress.toInt, - total = params.getTotal.toInt - ) + pb.update( + bloopId, + _.copy( + result = ProgressBar.Result.InProgress, + step = params.getProgress.toInt, + total = params.getTotal.toInt ) - - printPb() - } + ) } override def onBuildTaskFinish(params: TaskFinishParams): Unit = @@ -213,30 +185,25 @@ class BloopClient( ProgressBar.Result.Success } else ProgressBar.Result.Failure - val index = modules.indexWhere(_._1 == bloopId) - if (index != -1) { - if (report.getErrors == 0) { - if (!compiled) - onBuildEvent(BuildEvent.Compiled(parsed._1, parsed._2)) - modules.update( - index, - bloopId -> modules(index)._2 - .copy(result = r, step = 100, total = 100) - ) - - if (progress) printPb() - else log.info("Module " + Ansi.italic(parsed._1) + " compiled") - } else { - if (!compiled) onBuildEvent(BuildEvent.Failed(parsed._1, parsed._2)) - modules.update(index, bloopId -> modules(index)._2.copy(result = r)) - - if (progress) printPb() - else - log.error( - "Module " + Ansi.italic(parsed._1) + " could not be compiled" - ) - } - } + pb.update( + bloopId, + current => + if (report.getErrors == 0) { + if (!pb.compiled) + onBuildEvent(BuildEvent.Compiled(parsed._1, parsed._2)) + if (!progress) + log.info("Module " + Ansi.italic(parsed._1) + " compiled") + current.copy(result = r, step = 100, total = 100) + } else { + if (!pb.compiled) + onBuildEvent(BuildEvent.Failed(parsed._1, parsed._2)) + if (!progress) + log.error( + "Module " + Ansi.italic(parsed._1) + " could not be compiled" + ) + current.copy(result = r) + } + ) case TaskDataKind.TEST_REPORT => val json = params.getData.asInstanceOf[JsonElement] @@ -402,40 +369,6 @@ object Bsp { } } yield () - def fromCompletableFuture[T](future: => CompletableFuture[T]): Task[T] = - Task.descriptorWith( - d => - ZIO - .effect(future) - .flatMap( - f => - Task - .effectAsync { (cb: Task[T] => Unit) => - f.whenCompleteAsync( - (v: T, e: Throwable) => - if (e == null) cb(Task.succeed(v)) - else { - if (!e.isInstanceOf[CancellationException]) { - e.printStackTrace() - cb(Task.fail(e)) - } - }, - new Executor { - override def execute(r: Runnable): Unit = - if (!d.executor.submit(r)) - throw new RejectedExecutionException( - "Rejected: " + r.toString - ) - } - ) - } - .onTermination { _ => - if (!f.isDone) f.cancel(true) - UIO(()) - } - ) - ) - def runBspServer( projectPath: Path, log: Log, @@ -519,43 +452,6 @@ object Bsp { r <- f.join } yield r - private def progressBarUpdater( - client: BloopClient, - consoleOutput: ConsoleOutput - ) = { - val effect = RIO.effect(client.updatePb()) - val scheduler = Schedule.spaced(150.millis) - val runtime = new DefaultRuntime {} - - Stream - .fromEffect(effect) - .repeat(scheduler) - .provide(runtime.Environment) - .runDrain - .fork - } - - def withProgressBar( - client: BloopClient, - consoleOutput: ConsoleOutput, - zio: ZIO[Any, Nothing, Option[StatusCode]] - ): ZIO[Any, Nothing, Unit] = - UIO(client.printPb()).flatMap( - _ => - progressBarUpdater(client, consoleOutput).flatMap( - pb => - zio.flatMap( - result => - for { - _ <- pb.interrupt.map(_ => ()) - _ <- UIO(consoleOutput.flushSticky()) - _ <- if (result.exists(_ != StatusCode.OK)) IO.interrupt - else IO.unit - } yield () - ) - ) - ) - def compile( client: BloopClient, server: BloopServer, @@ -572,14 +468,10 @@ object Bsp { val b = Bsp .buildModules(server, build, projectPath, bloopModules) .option - .map(_.map(_.getStatusCode)) + .map(_.map(_.getStatusCode).contains(StatusCode.OK)) - if (progress) withProgressBar(client, consoleOutput, b) - else - b.flatMap( - result => - if (result.exists(_ != StatusCode.OK)) IO.interrupt else IO.unit - ) + if (progress) ProgressBars.withProgressBar(client.pb, consoleOutput, b) + else b.flatMap(if (_) IO.unit else IO.interrupt) } def watchAction( diff --git a/src/main/scala/seed/cli/Build.scala b/src/main/scala/seed/cli/Build.scala index 9ae9049..41bca68 100644 --- a/src/main/scala/seed/cli/Build.scala +++ b/src/main/scala/seed/cli/Build.scala @@ -78,13 +78,15 @@ object Build { util.Validation.unpack(parsedModules).right.map { allModules => val path = projectPath.getOrElse(buildProjectPath) + val consoleOutput = new ConsoleOutput(log, onStdOut) + val processes = BuildTarget.buildTargets( build, allModules, path, watch, tmpfs, - log + consoleOutput.log ) val buildModules = allModules.flatMap { @@ -98,8 +100,6 @@ object Build { val expandedModules = BuildConfig.expandModules(build, buildModules) - val consoleOutput = new ConsoleOutput(log, onStdOut) - val program = if (expandedModules.isEmpty) UIO(()) else { diff --git a/src/main/scala/seed/cli/Doc.scala b/src/main/scala/seed/cli/Doc.scala new file mode 100644 index 0000000..0c4da90 --- /dev/null +++ b/src/main/scala/seed/cli/Doc.scala @@ -0,0 +1,293 @@ +package seed.cli + +import java.io.File +import java.nio.file.{Files, Path} +import java.util.function.Consumer + +import org.apache.commons.io.FileUtils +import seed.{BuildInfo, Cli, Log} +import seed.model.{Config, Platform} +import seed.Cli.{Command, PackageConfig} +import seed.artefact.ArtefactResolution.{ + CompilerResolution, + ModuleRef, + RuntimeResolution +} +import seed.artefact.{ArtefactResolution, Coursier, SemanticVersioning} +import seed.cli.util.{Ansi, ConsoleOutput} +import seed.config.BuildConfig +import seed.config.BuildConfig.Build +import seed.generation.util.{PathUtil, ScalaCompiler} +import seed.model.Build.{JavaDep, Resolvers} +import zio.UIO + +import scala.util.Try + +object Doc { + def ui( + build: BuildConfig.Result, + bloopPath: Path, + seedConfig: Config, + command: Command.Doc, + packageConfig: Cli.PackageConfig, + progress: Boolean, + log: Log + ): UIO[Unit] = { + Cli.showResolvers(seedConfig, build.resolvers, packageConfig, log) + + val tmpfs = packageConfig.tmpfs || seedConfig.build.tmpfs + + val buildPath = PathUtil.buildPath(build.projectPath, tmpfs, log) + val destination = command.output.getOrElse(buildPath.resolve("docs")) + + val parsedModules = + command.modules.map(util.Target.parseModuleString(build.build)) + util.Validation.unpack(parsedModules) match { + case Left(errors) => + errors.foreach(log.error(_)) + UIO.interrupt + + case Right(allModules) => + val docModules = allModules.flatMap { + case util.Target.Parsed(module, None) => + BuildConfig.allTargets(build.build, module.name) + case util.Target.Parsed(module, Some(Left(platform))) => + List((module.name, platform)) + case util.Target.Parsed(_, Some(Right(_))) => List() + } + + val runtimeLibs = ArtefactResolution.allRuntimeLibs(build.build) + val runtimeResolution = ArtefactResolution.runtimeResolution( + build.build, + seedConfig, + build.resolvers, + packageConfig, + false, + log + ) + val compilerResolution = ArtefactResolution.compilerResolution( + build.build, + seedConfig, + build.resolvers, + packageConfig, + false, + log + ) + + def onBuilt( + co: ConsoleOutput, + paths: Map[(String, Platform), String] + ): UIO[Unit] = { + val r = docModules.forall { + case (module, platform) => + if (!build.build.contains(module)) { + log.error(s"Module ${Ansi.italic(module)} does not exist") + false + } else { + log.info(s"Documenting module ${Ansi + .italic(seed.cli.util.Module.format(module, platform))}...") + + val moduleDest = + destination.resolve(module + "-" + platform.id) + FileUtils.deleteDirectory(moduleDest.toFile) + Files.createDirectories(moduleDest) + + log.info( + s"Output path: ${Ansi.italic(moduleDest.toString)}" + ) + Try { + documentModule( + build.build, + build.resolvers, + runtimeResolution, + compilerResolution, + seedConfig, + command.packageConfig, + module, + platform, + runtimeLibs, + moduleDest.toFile, + paths, + log + ) + }.toEither match { + case Left(e) => + log.error("Internal error occurred") + e.printStackTrace() + false + case Right(v) => v + } + } + } + + if (r) UIO.succeed(()) else UIO.interrupt + } + + Package.buildModule( + log, + bloopPath, + build.build, + progress, + docModules, + onBuilt + ) + } + } + + def documentModule( + build: Build, + resolvers: Resolvers, + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, + seedConfig: Config, + packageConfig: PackageConfig, + module: String, + platform: Platform, + runtimeLibs: Map[ModuleRef, Set[JavaDep]], + dest: File, + modulePaths: Map[(String, Platform), String], + log: Log + ): Boolean = { + val deps = + BuildConfig.collectModuleDeps(build, build(module).module, platform) + val moduleClasspaths = deps.flatMap(d => modulePaths.get((d, platform))) + + val scaladocLibs = resolveScaladocBridge( + build, + module, + platform, + resolvers, + packageConfig, + seedConfig, + log + ) + + val r = runtimeResolution((module, platform, ArtefactResolution.Regular)) + val resolvedLibraryDeps = Coursier + .resolveSubset( + r.resolution, + runtimeLibs((module, platform, ArtefactResolution.Regular)), + optionalArtefacts = false + ) + .toList + + val moduleLibs = + Coursier.localArtefacts(r, resolvedLibraryDeps).map(_.libraryJar) + + val classLoader = new java.net.URLClassLoader( + scaladocLibs.map(_.toUri.toURL).toArray, + null + ) + + val scaladocClass = + classLoader.loadClass("seed.publish.util.Scaladoc") + val scaladoc = scaladocClass.newInstance() + val scaladocMethods = scaladocClass.getDeclaredMethods.toList + val scaladocExecuteMethod = + scaladocMethods.find(_.getName == "execute").get + val scaladocSettingsMethod = + scaladocMethods.find(_.getName == "settings").get + + val sourceFiles = BuildConfig + .sourcePaths(build, List(module -> platform)) + .flatMap(BuildConfig.allSourceFiles) + .map(_.toFile) + + log.info( + s"Documenting ${Ansi.bold(sourceFiles.length.toString)} files..." + ) + + val platformModule = + BuildConfig.platformModule(build(module).module, platform).get + val scalacParams = platformModule.scalaOptions ++ ScalaCompiler + .compilerPlugIns( + build, + platformModule, + compilerResolution, + platform, + platformModule.scalaVersion.get + ) + + val classpath = moduleClasspaths.map(new File(_)) ++ + moduleLibs.map(_.toFile) + + val settings = scaladocSettingsMethod.invoke( + scaladoc, + Array[File](), + classpath.toArray, + Array[File](), + Array[File](), + dest, + scalacParams.mkString(" ") + ) + + val result = scaladocExecuteMethod + .invoke( + scaladoc, + settings, + sourceFiles.toArray, + (message => log.error(Ansi.bold("[scaladoc] ") + message)): Consumer[ + String + ], + (message => log.warn(Ansi.bold("[scaladoc] ") + message)): Consumer[ + String + ] + ) + .asInstanceOf[java.lang.Boolean] + + if (result) + log.info( + s"Module ${Ansi.italic(seed.cli.util.Module.format(module, platform))} documented" + ) + else + log.error( + s"Module ${Ansi.italic(seed.cli.util.Module.format(module, platform))} could not be documented" + ) + + result + } + + def resolveScaladocBridge( + build: Build, + moduleName: String, + platform: Platform, + resolvers: Resolvers, + packageConfig: PackageConfig, + seedConfig: Config, + log: Log + ): List[Path] = { + val module = build(moduleName).module + val pm = BuildConfig.platformModule(module, platform).get + + val scalaOrg = pm.scalaOrganisation.get + val scalaVer = pm.scalaVersion.get + + val scalaCompilerDep = JavaDep(scalaOrg, "scala-compiler", scalaVer) + val scalaLibraryDep = JavaDep(scalaOrg, "scala-library", scalaVer) + + val scaladoc = JavaDep( + BuildInfo.Organisation, + "seed-scaladoc_" + SemanticVersioning.majorMinorVersion(scalaVer), + BuildInfo.Version + ) + + val scaladocDeps = Set[JavaDep](scaladoc, scalaLibraryDep, scalaCompilerDep) + + val r = + ArtefactResolution.resolution( + seedConfig, + resolvers, + packageConfig, + scaladocDeps, + (scalaOrg, scalaVer), + optionalArtefacts = false, + log + ) + + val resolvedDeps = Coursier + .resolveSubset(r.resolution, scaladocDeps, optionalArtefacts = false) + .toList + + Coursier.localArtefacts(r, resolvedDeps).map(_.libraryJar) + } +} diff --git a/src/main/scala/seed/cli/Generate.scala b/src/main/scala/seed/cli/Generate.scala index 0d82437..4d9852b 100644 --- a/src/main/scala/seed/cli/Generate.scala +++ b/src/main/scala/seed/cli/Generate.scala @@ -2,7 +2,7 @@ package seed.cli import java.nio.file.Path -import seed.{Log, model} +import seed.{Cli, Log, model} import seed.Cli.Command import seed.generation.{Bloop, Idea} import seed.artefact.ArtefactResolution @@ -19,9 +19,7 @@ object Generate { command: Command.Generate, log: Log ): Unit = { - val compilerDeps = ArtefactResolution.allCompilerDeps(build) - val platformDeps = ArtefactResolution.allPlatformDeps(build) - val libraryDeps = ArtefactResolution.allLibraryDeps(build) + Cli.showResolvers(seedConfig, resolvers, command.packageConfig, log) val (isBloop, isIdea) = command match { case _: Command.Bloop => (true, false) @@ -31,17 +29,22 @@ object Generate { val optionalArtefacts = isIdea || seedConfig.resolution.optionalArtefacts - val (_, platformResolution, compilerResolution) = - ArtefactResolution.resolution( - seedConfig, - resolvers, - build, - command.packageConfig, - optionalArtefacts, - platformDeps ++ libraryDeps, - compilerDeps, - log - ) + val runtimeResolution = ArtefactResolution.runtimeResolution( + build, + seedConfig, + resolvers, + command.packageConfig, + optionalArtefacts, + log + ) + val compilerResolution = ArtefactResolution.compilerResolution( + build, + seedConfig, + resolvers, + command.packageConfig, + optionalArtefacts, + log + ) val tmpfs = command.packageConfig.tmpfs || seedConfig.build.tmpfs if (isBloop) @@ -49,7 +52,7 @@ object Generate { projectPath, outputPath, build, - platformResolution, + runtimeResolution, compilerResolution, tmpfs, optionalArtefacts, @@ -60,7 +63,7 @@ object Generate { projectPath, outputPath, build, - platformResolution, + runtimeResolution, compilerResolution, tmpfs, log diff --git a/src/main/scala/seed/cli/Link.scala b/src/main/scala/seed/cli/Link.scala index 7c3f256..125331d 100644 --- a/src/main/scala/seed/cli/Link.scala +++ b/src/main/scala/seed/cli/Link.scala @@ -78,13 +78,15 @@ object Link { val parsedModules = modules.map(util.Target.parseModuleString(result.build)) util.Validation.unpack(parsedModules).right.map { allModules => + val consoleOutput = new ConsoleOutput(log, onStdOut) + val processes = seed.cli.BuildTarget.buildTargets( build, allModules, projectPath, watch, tmpfs, - log + consoleOutput.log ) val linkModules = allModules.flatMap { @@ -97,7 +99,6 @@ object Link { val expandedModules = BuildConfig.expandModules(build, linkModules) - val consoleOutput = new ConsoleOutput(log, onStdOut) val client = new BloopClient( consoleOutput, progress, @@ -190,7 +191,7 @@ object Link { }, optimise, log, - line => consoleOutput.write(line + "\n", sticky = false), + line => consoleOutput.writeRegular(line + "\n"), onBuildEvent ) } yield () diff --git a/src/main/scala/seed/cli/Package.scala b/src/main/scala/seed/cli/Package.scala index 24ea72c..c9faecf 100644 --- a/src/main/scala/seed/cli/Package.scala +++ b/src/main/scala/seed/cli/Package.scala @@ -1,5 +1,6 @@ package seed.cli +import java.io.FileOutputStream import java.nio.file.{Files, Path, Paths} import scala.collection.JavaConverters._ @@ -9,7 +10,7 @@ import seed.cli.util.{Ansi, ConsoleOutput, RTS} import seed.config.BuildConfig import seed.config.BuildConfig.Build import seed.generation.util.PathUtil -import seed.model.Build.{JavaDep, Module, Resolvers} +import seed.model.Build.{JavaDep, Resolvers} import seed.model.{Config, Platform} import seed.model.Platform.JVM import seed.{Cli, Log} @@ -27,20 +28,22 @@ object Package { progress: Boolean, packageConfig: Cli.PackageConfig, log: Log - ): Unit = { + ): Boolean = { + Cli.showResolvers(seedConfig, resolvers, packageConfig, log) val tmpfs = packageConfig.tmpfs || seedConfig.build.tmpfs val buildPath = PathUtil.buildPath(projectPath, tmpfs, log) val outputPath = output.getOrElse(buildPath.resolve("dist")) - if (!build.contains(module)) + if (!build.contains(module)) { log.error(s"Module ${Ansi.italic(module)} does not exist") - else { - val expandedModules = - BuildConfig.expandModules(build, List(module -> JVM)) - - def onBuilt(stringPaths: List[String]) = UIO { - val paths = stringPaths.map(Paths.get(_)) + false + } else { + def onBuilt( + consoleOutput: ConsoleOutput, + modulePaths: Map[(String, Platform), String] + ) = UIO { + val paths = modulePaths.toList.map(_._2).map(Paths.get(_)) require(paths.forall(Files.exists(_))) if (paths.isEmpty) log.error("No build paths were found") @@ -54,12 +57,12 @@ object Package { val classPath = if (!libs) List() else - getLibraryClassPath( + libraryClassPath( seedConfig, packageConfig, resolvers, build, - jvmModule, + module, outputPath, log ) @@ -77,17 +80,25 @@ object Package { Files.deleteIfExists(outputJar) seed.generation.Package.create( files, - outputJar, + new FileOutputStream(outputJar.toFile), mainClass, classPath, log ) + log.info(s"Created JAR file ${Ansi.italic(outputJar.toString)}") } } - val program = - buildModule(log, projectPath, build, progress, expandedModules, onBuilt) - RTS.unsafeRunSync(program) + val program = buildModule( + log, + projectPath, + build, + progress, + List(module -> JVM), + onBuilt + ) + + RTS.unsafeRunSync(program).succeeded } } @@ -96,9 +107,11 @@ object Package { projectPath: Path, build: Build, progress: Boolean, - expandedModules: List[(String, Platform)], - onBuilt: List[String] => UIO[Unit] + modules: List[(String, Platform)], + onBuilt: (ConsoleOutput, Map[(String, Platform), String]) => UIO[Unit] ): UIO[Unit] = { + val expandedModules = BuildConfig.expandModules(build, modules) + val consoleOutput = new ConsoleOutput(log, print) val client = new BloopClient( @@ -121,21 +134,9 @@ object Package { expandedModules ) - consoleOutput.log.info( - s"Compiling ${Ansi.bold(expandedModules.length.toString)} modules..." - ) - val program = for { moduleDirs <- classDirectories.option.map(_.get) - _ = { - moduleDirs.foreach { - case (module, dir) => - consoleOutput.log.debug( - s"Module ${Ansi.italic(module._1)}: ${Ansi.italic(dir)}" - ) - } - } _ <- (for { _ <- Bsp.compile( client, @@ -146,7 +147,17 @@ object Package { projectPath, expandedModules ) - _ <- onBuilt(moduleDirs.toList.map(_._2)) + _ <- { + consoleOutput.log.info("All modules compiled") + moduleDirs.foreach { + case (module, dir) => + consoleOutput.log.debug( + s"Module path for ${seed.cli.util.Module + .format(module._1, module._2)}: ${Ansi.italic(dir)}" + ) + } + onBuilt(consoleOutput, moduleDirs) + } } yield ()).ensuring(Bsp.shutdown(bspProcess, socket, server)) } yield () @@ -166,48 +177,30 @@ object Package { .map(p => p -> path.relativize(p).toString) ) - def getLibraryClassPath( + def libraryClassPath( seedConfig: Config, packageConfig: Cli.PackageConfig, resolvers: Resolvers, build: Build, - jvmModule: Module, + moduleName: String, outputPath: Path, log: Log ): List[String] = { - val scalaVersion = jvmModule.scalaVersion.get - val scalaLibraryDep = - JavaDep(jvmModule.scalaOrganisation.get, "scala-library", scalaVersion) - val scalaReflectDep = - JavaDep(jvmModule.scalaOrganisation.get, "scala-reflect", scalaVersion) - val platformDeps = Set(scalaLibraryDep, scalaReflectDep) - - val libraryDeps = ArtefactResolution.allLibraryDeps(build, Set(JVM)) - val (resolvedDepPath, libraryResolution, platformResolution) = - ArtefactResolution.resolution( - seedConfig, - resolvers, - build, - packageConfig, - optionalArtefacts = false, - libraryDeps, - List(platformDeps), - log - ) - val resolvedLibraryDeps = Coursier.localArtefacts( - libraryResolution, - libraryDeps, - optionalArtefacts = false + val resolved = ArtefactResolution.resolvePackageArtefacts( + seedConfig, + packageConfig, + resolvers, + build, + moduleName, + JVM, + log ) - val resolvedPlatformDeps = Coursier.localArtefacts( - platformResolution.head, - platformDeps, - optionalArtefacts = false - ) + val cachePath = ArtefactResolution.cachePath(seedConfig, packageConfig) - val resolvedDeps = (resolvedLibraryDeps ++ resolvedPlatformDeps).distinct - .sortBy(_.libraryJar) + import resolved._ + val resolvedDeps = + Coursier.localArtefacts(resolution, deps.toList).sortBy(_.libraryJar) log.info( s"Copying ${Ansi.bold(resolvedDeps.length.toString)} libraries to ${Ansi @@ -215,7 +208,7 @@ object Package { ) resolvedDeps.foreach { dep => val target = - outputPath.resolve(resolvedDepPath.relativize(dep.libraryJar)) + outputPath.resolve(cachePath.relativize(dep.libraryJar)) if (Files.exists(target)) log.debug(s"Skipping ${dep.libraryJar.toString} as it exists already") else { @@ -228,6 +221,6 @@ object Package { } log.info(s"Adding libraries to class path...") - resolvedDeps.map(p => resolvedDepPath.relativize(p.libraryJar).toString) + resolvedDeps.map(p => cachePath.relativize(p.libraryJar).toString) } } diff --git a/src/main/scala/seed/cli/Publish.scala b/src/main/scala/seed/cli/Publish.scala new file mode 100644 index 0000000..17d7bbd --- /dev/null +++ b/src/main/scala/seed/cli/Publish.scala @@ -0,0 +1,663 @@ +package seed.cli + +import java.io.ByteArrayOutputStream +import java.nio.file.{Files, Path, Paths} + +import org.apache.commons.io.FileUtils +import seed.artefact.ArtefactResolution.{ + CompilerResolution, + ModuleRef, + RuntimeResolution +} +import seed.artefact.{ArtefactResolution, Coursier, SemanticVersioning} +import seed.cli.util.{ + Ansi, + ConsoleOutput, + Module, + ProgressBar, + ProgressBarItem, + ProgressBars, + RTS +} +import seed.config.BuildConfig +import seed.config.BuildConfig.Build +import seed.generation.util.PathUtil +import seed.model.Build.{JavaDep, Resolvers} +import seed.model.Platform.{JavaScript, Native} +import seed.model.{Config, Platform} +import seed.publish.Bintray +import seed.publish.util.Http +import seed.{Cli, Log} +import zio._ + +import scala.util.Try + +object Publish { + def ui( + seedConfig: Config, + projectPath: Path, + resolvers: Resolvers, + `package`: seed.model.Build.Package, + build: Build, + version: Option[String], + target: String, + modules: List[String], + progress: Boolean, + skipSources: Boolean, + skipDocs: Boolean, + packageConfig: Cli.PackageConfig, + log: Log + ): Unit = { + Cli.showResolvers(seedConfig, resolvers, packageConfig, log) + + val bintrayUser = Some(seedConfig.repository.bintray.user) + .filter(_.nonEmpty) + .orElse(sys.env.get("BINTRAY_USER").filter(_.nonEmpty)) + val bintrayApiKey = Some(seedConfig.repository.bintray.apiKey) + .filter(_.nonEmpty) + .orElse(sys.env.get("BINTRAY_API_KEY").filter(_.nonEmpty)) + + if (bintrayUser.isEmpty || bintrayApiKey.isEmpty) { + log.error("Bintray username and API key must be set") + sys.exit(1) + } else if (!`package`.name.exists(_.nonEmpty) || + !`package`.organisation.exists(_.nonEmpty)) { + log.error("Package name and organisation must be set in build file") + sys.exit(1) + } + + if (!target.startsWith("bintray:") || target.count(_ == '/') != 2) { + log.error("Seed can only publish to Bintray") + log.error( + s"${Ansi.bold("Syntax:")} ${Ansi.italic("seed publish bintray:// ")}" + ) + sys.exit(1) + } + + val bintrayPath = target.drop("bintray:".length).split('/') + val (bintrayOrganisation, bintrayRepository, bintrayPackage) = + (bintrayPath(0), bintrayPath(1), bintrayPath(2)) + + val tmpfs = packageConfig.tmpfs || seedConfig.build.tmpfs + + val buildPath = PathUtil.buildPath(projectPath, tmpfs, log) + + val parsedModules = modules.map(util.Target.parseModuleString(build)) + util.Validation.unpack(parsedModules) match { + case Left(errors) => + errors.foreach(log.error(_)) + sys.exit(1) + + case Right(allModules) => + val publishModules = allModules.flatMap { + case util.Target.Parsed(module, None) => + BuildConfig.allTargets(build, module.name) + case util.Target.Parsed(module, Some(Left(platform))) => + List((module.name, platform)) + case util.Target.Parsed(_, Some(Right(_))) => List() + } + + val v = getVersion(projectPath, version, log).getOrElse(sys.exit(1)) + log.info(s"Publishing version $v...") + + def onBuilt( + consoleOutput: ConsoleOutput, + modulePaths: Map[(String, Platform), String] + ): UIO[Unit] = { + val log = consoleOutput.log + + val paths = modulePaths.mapValues(Paths.get(_)) + require(paths.values.forall(Files.exists(_))) + + if (paths.isEmpty) { + log.error("No build paths were found") + UIO.interrupt + } else { + consoleOutput.log.info( + s"Publishing ${Ansi.bold(publishModules.length.toString)} modules..." + ) + + consoleOutput.reset() + val pb = new ProgressBars(consoleOutput, publishModules.map { + case (m, p) => + ProgressBarItem( + BuildConfig.targetName(build, m, p), + Module.format(m, p) + ) + }) + + val runtimeLibs = ArtefactResolution.allRuntimeLibs(build) + val runtimeResolution = ArtefactResolution.runtimeResolution( + build, + seedConfig, + resolvers, + packageConfig, + false, + log + ) + val compilerResolution = ArtefactResolution.compilerResolution( + build, + seedConfig, + resolvers, + packageConfig, + false, + log + ) + + val p = UIO( + Http.create( + log, + "bintray.com", + (bintrayUser.get, bintrayApiKey.get) + ) + ).bracket(http => UIO(http.destroy())) { http => + UIO + .collectAll(publishModules.map { + case (module, platform) => + publishModule( + runtimeLibs, + http, + bintrayOrganisation, + bintrayRepository, + bintrayPackage, + v, + seedConfig, + runtimeResolution, + compilerResolution, + resolvers, + `package`, + packageConfig, + pb, + paths, + modulePaths, + buildPath, + build, + module, + platform, + skipSources, + skipDocs, + log + ) + }) + .flatMap { _ => + log.info("Publishing content...") + Bintray.publishContent( + http, + log, + organisation = bintrayOrganisation, + repository = bintrayRepository, + `package` = bintrayPackage, + packageVersion = v + ) + } + } + + if (progress) + ProgressBars.withProgressBar( + pb, + consoleOutput, + p.option.map(_.isDefined) + ) + else + p.option.flatMap(r => if (r.isDefined) IO.unit else IO.interrupt) + } + } + + val program = Package.buildModule( + log, + projectPath, + build, + progress, + publishModules, + onBuilt + ) + + val result = RTS.unsafeRunSync(program) + if (!result.succeeded) log.error("Publishing failed") + sys.exit(if (result.succeeded) 0 else 1) + } + } + + def publish( + bintrayOrganisation: String, + bintrayRepository: String, + bintrayPackage: String, + http: Http, + log: Log, + `package`: seed.model.Build.Package, + classesJar: UIO[ByteArrayOutputStream], + docsJar: Option[UIO[ByteArrayOutputStream]], + sourcesJar: Option[UIO[ByteArrayOutputStream]], + pom: UIO[String], + version: String, + packageArtefact: String, + pbUpdate: (Int, Int) => Unit + ): UIO[Unit] = { + val uploadJar = classesJar.flatMap( + classesJar => + Bintray.uploadMavenFile( + http, + log, + organisation = bintrayOrganisation, + repository = bintrayRepository, + `package` = bintrayPackage, + packageGroup = `package`.organisation.get, + packageArtefact = packageArtefact, + packageClassifier = "", + packageVersion = version, + bytes = classesJar.toByteArray, + extension = "jar" + ) + ) + + val uploadDocs = docsJar.toList.map( + _.flatMap( + jar => + Bintray.uploadMavenFile( + http, + log, + organisation = bintrayOrganisation, + repository = bintrayRepository, + `package` = bintrayPackage, + packageGroup = `package`.organisation.get, + packageArtefact = packageArtefact, + packageClassifier = "-javadoc", + packageVersion = version, + bytes = jar.toByteArray, + extension = "jar" + ) + ) + ) + + val uploadSources = sourcesJar.toList.map( + _.flatMap( + jar => + Bintray.uploadMavenFile( + http, + log, + organisation = bintrayOrganisation, + repository = bintrayRepository, + `package` = bintrayPackage, + packageGroup = `package`.organisation.get, + packageArtefact = packageArtefact, + packageClassifier = "-sources", + packageVersion = version, + bytes = jar.toByteArray, + extension = "jar" + ) + ) + ) + + val uploadPom = pom.flatMap( + pom => + Bintray.uploadMavenFile( + http, + log, + organisation = bintrayOrganisation, + repository = bintrayRepository, + `package` = bintrayPackage, + packageGroup = `package`.organisation.get, + packageArtefact = packageArtefact, + packageClassifier = "", + packageVersion = version, + bytes = pom.getBytes, + extension = "pom" + ) + ) + + var completed = 0 + val all = List(uploadJar, uploadPom) ++ uploadDocs ++ uploadSources + + def onComplete(uio: UIO[Unit]): UIO[Unit] = uio.map { _ => + completed += 1 + pbUpdate(completed, all.length) + } + + for { + // Change progress bar to 'in progress' + _ <- UIO(pbUpdate(0, all.length)) + _ <- UIO.collectAll(all.map(onComplete)) + } yield () + } + + def createPom( + pkg: seed.model.Build.Package, + artefactId: String, + version: String, + dependencies: Set[JavaDep] + ): String = { + import pine._ + + val url = pkg.url.map(url => xml"""$url""").toList + + val licences = + pkg.licences.map { licence => + xml""" + + ${licence.name} + ${licence.url} + repo + + """ + } + + val developers = pkg.developers.map { developer => + xml""" + + ${developer.id} + ${developer.name} + ${developer.email} + + """ + } + + val scm = pkg.scm + .map( + scm => + xml""" + + ${scm.url} + ${scm.connection} + ${scm.developerConnection + .getOrElse(scm.connection)} + + """ + ) + .toList + + val dependenciesXml = dependencies.toList + .sortBy(dep => (dep.organisation, dep.artefact)) + .map(dep => xml""" + + ${dep.organisation} + ${dep.artefact} + ${dep.version} + + """) + + xml""" + + + 4.0.0 + ${pkg.organisation.get} + $artefactId + bundle + ${pkg.name.get} + $version + ${pkg.name.get} + $url + + ${pkg.organisation.get} + + $licences + $developers + $dependenciesXml + $scm + + """.toXml + } + + def encodeVersion(version: String): String = + if (SemanticVersioning.isPreRelease(version)) version + else SemanticVersioning.majorMinorVersion(version) + + def getVersion( + projectPath: Path, + version: Option[String], + log: Log + ): Option[String] = + version + .orElse { + log.info("Determining version from Git tag...") + import sys.process._ + val result = + Try(Process("git describe --tags", projectPath.toFile).!!).toOption + .map { t => + val tag = t.trim + if (!tag.startsWith("v")) tag else tag.tail + } + if (result.isEmpty) log.error("Could not retrieve Git tag") + result + } + .flatMap { v => + val parsed = SemanticVersioning.parseVersion(v) + if (parsed.isDefined) Some(v) + else { + log.error(s"'$v' is not a valid semantic version") + None + } + } + + def publishModule( + deps: Map[ModuleRef, Set[JavaDep]], + http: Http, + bintrayOrganisation: String, + bintrayRepository: String, + bintrayPackage: String, + v: String, + seedConfig: Config, + resolution: Map[ModuleRef, Coursier.ResolutionResult], + compilerResolution: CompilerResolution, + resolvers: Resolvers, + `package`: seed.model.Build.Package, + packageConfig: Cli.PackageConfig, + pb: ProgressBars, + paths: Map[(String, Platform), Path], + modulePaths: Map[(String, Platform), String], + buildPath: Path, + build: Build, + module: String, + platform: Platform, + skipSources: Boolean, + skipDocs: Boolean, + log: Log + ): ZIO[Any, Nothing, Unit] = { + val platformModule = + BuildConfig.platformModule(build(module).module, platform).get + + val classesJar = + packageClasses(build, module, platform, platformModule, paths, log) + + val docsJar = + if (!`package`.docs || skipDocs) None + else + Some( + packageDocs( + deps, + seedConfig, + resolution, + compilerResolution, + resolvers, + packageConfig, + modulePaths, + buildPath, + build, + module, + platform, + log + ) + ) + + val sourcesJar = + if (!`package`.sources || skipSources) None + else Some(packageSources(build, module, platform, log)) + + val packageArtefact = packageArtefactName(module, platform, platformModule) + + val pom = + resolveArtefactsAndCreatePom( + seedConfig, + resolvers, + `package`, + build, + module, + platform, + packageArtefact, + v, + packageConfig, + log + ) + + publish( + bintrayOrganisation, + bintrayRepository, + bintrayPackage, + http, + log, + `package`, + classesJar, + docsJar, + sourcesJar, + pom, + v, + packageArtefact, + (current, total) => + pb.update( + BuildConfig.targetName(build, module, platform), + _.copy( + step = 100 * current / total, + total = 100, + result = ProgressBar.Result.InProgress + ) + ) + ).map( + _ => + pb.update( + BuildConfig.targetName(build, module, platform), + _.copy(result = ProgressBar.Result.Success) + ) + ) + .onTermination( + _ => + UIO( + pb.update( + BuildConfig.targetName(build, module, platform), + _.copy(result = ProgressBar.Result.Failure) + ) + ) + ) + } + + def packageClasses( + build: Build, + module: String, + platform: Platform, + platformModule: seed.model.Build.Module, + paths: Map[(String, Platform), Path], + log: Log + ): UIO[ByteArrayOutputStream] = UIO { + val files = Package.collectFiles(List(paths((module, platform)))) + val mainClass = platformModule.mainClass + if (mainClass.isDefined) + log.info(s"Main class is ${Ansi.italic(mainClass.get)}") + + val classesJar = new ByteArrayOutputStream(1024) + seed.generation.Package.create(files, classesJar, mainClass, List(), log) + classesJar + } + + def packageDocs( + deps: Map[ModuleRef, Set[JavaDep]], + seedConfig: Config, + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, + resolvers: Resolvers, + packageConfig: Cli.PackageConfig, + modulePaths: Map[(String, Platform), String], + buildPath: Path, + build: Build, + module: String, + platform: Platform, + log: Log + ): UIO[ByteArrayOutputStream] = + UIO { + + /** Same as in [[Doc.ui]] */ + val outputDocs = + buildPath.resolve("docs").resolve(module + "-" + platform.id) + FileUtils.deleteDirectory(outputDocs.toFile) + Files.createDirectories(outputDocs) + log.info( + s"Creating documentation for module ${Ansi.italic(module)} in $outputDocs..." + ) + + if (!seed.cli.Doc.documentModule( + build, + resolvers, + runtimeResolution, + compilerResolution, + seedConfig, + packageConfig, + module, + platform, + deps, + outputDocs.toFile, + modulePaths, + log + )) sys.exit(1) + + val stream = new ByteArrayOutputStream(1024) + seed.generation.Package.create( + Package.collectFiles(List(outputDocs)), + stream, + None, + List(), + log + ) + stream + } + + def packageSources( + build: Build, + module: String, + platform: Platform, + log: Log + ): UIO[ByteArrayOutputStream] = + UIO { + val stream = new ByteArrayOutputStream(1024) + val sourcePaths = Package.collectFiles( + BuildConfig.sourcePaths(build, List((module, platform))) + ) + seed.generation.Package.create(sourcePaths, stream, None, List(), log) + stream + } + + def packageArtefactName( + module: String, + platform: Platform, + platformModule: seed.model.Build.Module + ): String = { + val packageArtefactPlatform = + if (platform == JavaScript) + "_sjs" + encodeVersion(platformModule.scalaJsVersion.get) + else if (platform == Native) + "_native" + encodeVersion(platformModule.scalaNativeVersion.get) + else "" + val scalaVersion = "_" + encodeVersion(platformModule.scalaVersion.get) + s"$module$packageArtefactPlatform$scalaVersion" + } + + def resolveArtefactsAndCreatePom( + seedConfig: Config, + resolvers: Resolvers, + `package`: seed.model.Build.Package, + build: Build, + module: String, + platform: Platform, + packageArtefact: String, + v: String, + packageConfig: Cli.PackageConfig, + log: Log + ): UIO[String] = UIO { + val resolution = ArtefactResolution.resolvePackageArtefacts( + seedConfig, + packageConfig, + resolvers, + build, + module, + platform, + log + ) + createPom(`package`, packageArtefact, v, resolution.deps.keySet) + } +} diff --git a/src/main/scala/seed/cli/Run.scala b/src/main/scala/seed/cli/Run.scala index b370bc8..094e0f7 100644 --- a/src/main/scala/seed/cli/Run.scala +++ b/src/main/scala/seed/cli/Run.scala @@ -83,13 +83,15 @@ object Run { case util.Target.Parsed(_, Some(Right(_))) => Left(List("Invalid platform target specified")) case _ => + val consoleOutput = new ConsoleOutput(log, onStdOut) + val processes = seed.cli.BuildTarget.buildTargets( build, List(module), projectPath, watch, tmpfs, - log + consoleOutput.log ) val runModule = module match { @@ -106,7 +108,6 @@ object Run { case Some((module, platform)) => val expandedModules = BuildConfig.expandModules(build, List(module -> platform)) - val consoleOutput = new ConsoleOutput(log, onStdOut) val client = new BloopClient( consoleOutput, diff --git a/src/main/scala/seed/cli/util/ConsoleOutput.scala b/src/main/scala/seed/cli/util/ConsoleOutput.scala index e5232be..e3e844f 100644 --- a/src/main/scala/seed/cli/util/ConsoleOutput.scala +++ b/src/main/scala/seed/cli/util/ConsoleOutput.scala @@ -4,64 +4,68 @@ import seed.Log class ConsoleOutput(parentLog: Log, print: String => Unit) { val log = new Log( - l => write(l + "\n", sticky = false), + l => writeRegular(l + "\n"), identity, parentLog.level, parentLog.unicode ) - private var stickyLines = 0 - private var clearLines = 0 + private var stickyLines = List[String]() private var flushed = false def isFlushed: Boolean = flushed - def processLines(output: String): String = - output.flatMap { - case '\n' if clearLines != 0 => - clearLines -= 1 + def processLines(output: List[String], clearLines: Int): String = { + var clear = clearLines + output.map { line => + val clearEsc = + if (clear == 0) "" + else { + clear -= 1 - // Clear until end of line - Ansi.Escape + "0K" + "\n" + // Clear until end of line + Ansi.Escape + "0K" + } - case c => c.toString + line + clearEsc + "\n" + }.mkString + } + + def writeRegular(output: String): Unit = { + require(output.endsWith("\n")) + + if (stickyLines.isEmpty) print(output) + else { + val lines = output.split('\n').toList + print( + Ansi.Escape + s"${stickyLines.length}A" + + processLines(lines ++ stickyLines, stickyLines.length) + ) } + } - def write(output: String, sticky: Boolean = false): Unit = { + def writeSticky(output: String): Unit = { require(output.endsWith("\n")) + require(!flushed) + val lines = output.count(_ == '\n') + require(stickyLines.isEmpty || stickyLines.length == lines) - if (sticky) { - require(!flushed) - val lines = output.count(_ == '\n') - require(stickyLines == 0 || stickyLines == lines) + val initialStickyLines = stickyLines.isEmpty + stickyLines = output.split('\n').toList - if (stickyLines == 0) { - stickyLines = lines - print(output) - } else { - // Move up - print(Ansi.Escape + s"${stickyLines}A" + processLines(output)) - } - } else { - if (stickyLines > 0) { - clearLines = stickyLines - print(Ansi.Escape + s"${stickyLines}A" + processLines(output)) - stickyLines = 0 - } else { - print(processLines(output)) - } - } + if (initialStickyLines) print(output) + else + // Move up first, then replace lines + print(Ansi.Escape + s"${lines}A" + processLines(stickyLines, lines)) } def flushSticky(): Unit = { - stickyLines = 0 - clearLines = 0 + stickyLines = List() flushed = true } def reset(): Unit = { - stickyLines = 0 - clearLines = 0 + stickyLines = List() flushed = false } } diff --git a/src/main/scala/seed/cli/util/Module.scala b/src/main/scala/seed/cli/util/Module.scala new file mode 100644 index 0000000..ef0c139 --- /dev/null +++ b/src/main/scala/seed/cli/util/Module.scala @@ -0,0 +1,8 @@ +package seed.cli.util + +import seed.model.Platform + +object Module { + def format(module: String, platform: Platform): String = + module + " (" + platform.caption + ")" +} diff --git a/src/main/scala/seed/cli/util/ProgressBars.scala b/src/main/scala/seed/cli/util/ProgressBars.scala new file mode 100644 index 0000000..185a358 --- /dev/null +++ b/src/main/scala/seed/cli/util/ProgressBars.scala @@ -0,0 +1,92 @@ +package seed.cli.util + +import scala.collection.mutable +import seed.config.BuildConfig +import seed.config.BuildConfig.Build +import seed.model.Platform +import zio._ +import zio.stream._ +import zio.duration._ + +case class ProgressBarItem(id: String, caption: String) + +class ProgressBars( + consoleOutput: ConsoleOutput, + items: List[ProgressBarItem] +) { + // Cannot use ListMap here since updating it changes the order of the elements + private val itemsProgress = mutable.ListBuffer[(String, ProgressBar.Line)]() + reset() + + def reset(): Unit = { + itemsProgress.clear() + itemsProgress ++= items.map { + case ProgressBarItem(id, caption) => + id -> ProgressBar.Line(0, ProgressBar.Result.Waiting, caption, 0, 0) + } + } + + /** If modules were compiled, the upcoming notifications are related to the + * next build stage (e.g. running the program) + */ + def compiled: Boolean = consoleOutput.isFlushed + + def printPb(): Unit = + if (!compiled) + consoleOutput.writeSticky(ProgressBar.printAll(itemsProgress)) + + def updatePb(): Unit = { + itemsProgress.zipWithIndex.foreach { + case ((id, line), i) => + itemsProgress.update(i, id -> line.copy(tick = line.tick + 1)) + } + + printPb() + } + + def update(id: String, f: ProgressBar.Line => ProgressBar.Line): Unit = { + val index = itemsProgress.indexWhere(_._1 == id) + if (index != -1) { + itemsProgress.update(index, id -> f(itemsProgress(index)._2)) + printPb() + } + } +} + +object ProgressBars { + private def progressBarUpdater( + progressBars: ProgressBars, + consoleOutput: ConsoleOutput + ) = { + val effect = RIO.effect(progressBars.updatePb()) + val scheduler = Schedule.spaced(150.millis) + val runtime = new DefaultRuntime {} + + Stream + .fromEffect(effect) + .repeat(scheduler) + .provide(runtime.Environment) + .runDrain + .fork + } + + def withProgressBar( + progressBars: ProgressBars, + consoleOutput: ConsoleOutput, + zio: ZIO[Any, Nothing, Boolean] + ): ZIO[Any, Nothing, Unit] = + UIO(progressBars.printPb()).flatMap( + _ => + progressBarUpdater(progressBars, consoleOutput).flatMap( + pb => + zio.flatMap( + result => + for { + _ <- pb.interrupt.map(_ => ()) + _ <- UIO(consoleOutput.flushSticky()) + _ <- if (result) IO.unit else IO.interrupt + } yield () + ) + ) + ) +} diff --git a/src/main/scala/seed/cli/util/Target.scala b/src/main/scala/seed/cli/util/Target.scala index 9f6be4d..d578346 100644 --- a/src/main/scala/seed/cli/util/Target.scala +++ b/src/main/scala/seed/cli/util/Target.scala @@ -41,7 +41,8 @@ object Target { .map(tgt => Right(TargetRef(target, tgt))) ) match { case None => - Left(s"Invalid build target ${Ansi.italic(target)} provided") + Left(s"Invalid build target ${Ansi + .italic(target)} provided on module ${Ansi.italic(name)}") case Some(tgt) => Right(Parsed(ModuleRef(name, build(name).module), Some(tgt))) } diff --git a/src/main/scala/seed/config/BuildConfig.scala b/src/main/scala/seed/config/BuildConfig.scala index 833a256..a5e8379 100644 --- a/src/main/scala/seed/config/BuildConfig.scala +++ b/src/main/scala/seed/config/BuildConfig.scala @@ -2,6 +2,8 @@ package seed.config import java.nio.file.{Files, Path} +import scala.collection.JavaConverters._ + import org.apache.commons.io.FileUtils import seed.cli.util.{Ansi, ColourScheme, Watcher} import seed.model.Build.{JavaDep, Module, ScalaDep} @@ -17,7 +19,12 @@ object BuildConfig { case class ModuleConfig(module: Module, path: Path) type Build = Map[String, ModuleConfig] - case class Result(projectPath: Path, resolvers: Build.Resolvers, build: Build) + case class Result( + projectPath: Path, + `package`: Build.Package, + resolvers: Build.Resolvers, + build: Build + ) def load(path: Path, log: Log): Option[Result] = loadInternal(path, log).filter( @@ -68,7 +75,12 @@ object BuildConfig { path => loadInternal(path, log), log ) - Result(projectPath.normalize(), parsed.resolvers, modules) + Result( + projectPath.normalize(), + parsed.`package`, + parsed.resolvers, + modules + ) } } } @@ -427,6 +439,12 @@ object BuildConfig { def targetName(build: Build, name: String, platform: Platform): String = if (!isCrossBuild(build(name).module)) name else name + "-" + platform.id + def allTargets(build: Build, module: String): List[(String, Platform)] = { + val m = build(module).module + val p = m.targets + p.map((module, _)) + } + def linkTargets(build: Build, module: String): List[(String, Platform)] = { val m = build(module).module val p = m.targets.diff(List(JVM)) @@ -443,6 +461,21 @@ object BuildConfig { m.sources ++ pmSources }.distinct + /** Returns list of all Scala and Java files */ + def allSourceFiles(path: Path): List[Path] = + if (!Files.exists(path)) List() + else + Files + .walk(path) + .iterator() + .asScala + .toList + .filter( + p => + Files.isRegularFile(p) && (p.toString + .endsWith(".scala") || p.toString.endsWith(".java")) + ) + def targetsFromPlatformModules(module: Build.Module): List[Platform] = (if (module.jvm.nonEmpty) List(JVM) else List()) ++ (if (module.js.nonEmpty) List(JavaScript) else List()) ++ @@ -556,8 +589,12 @@ object BuildConfig { } } - def collectModuleDeps(build: Build, module: Module): List[String] = - Platform.All.keys.toList + def collectModuleDeps( + build: Build, + module: Module, + platforms: Set[Platform] = Platform.All.keySet + ): List[String] = + platforms.toList .filter(module.targets.contains) .flatMap(p => collectModuleDepsBase(build, module, p)) .distinct diff --git a/src/main/scala/seed/config/SeedConfig.scala b/src/main/scala/seed/config/SeedConfig.scala index 5440288..c0d520a 100644 --- a/src/main/scala/seed/config/SeedConfig.scala +++ b/src/main/scala/seed/config/SeedConfig.scala @@ -21,10 +21,44 @@ object SeedConfig { val log = Log(Config()) - def parse(path: Path) = + def parse(tomlPath: Path) = { + def parseRaw( + toml: String + ): Either[_root_.toml.Codec.Error, List[Value.Tbl]] = + Toml.parse(toml) match { + case Left(l) => Left(List() -> l) + case Right(r) => + r.values.get("import") match { + case Some(Value.Arr(values)) + if values.forall(_.isInstanceOf[Value.Str]) => + val included = values.flatMap { + case Value.Str(path) => + val resolved = + TomlUtils.fixPath(tomlPath.getParent, Paths.get(path)) + TomlUtils + .parseFile(resolved, parseRaw, "Seed configuration", log) + .getOrElse(sys.exit(1)) + } + Right(included :+ Value.Tbl(r.values - "import")) + case None => Right(List(r)) + case _ => Left(List("import") -> "Only paths may be specified") + } + } + + def f(toml: String): Either[Codec.Error, Config] = + parseRaw(toml).right.flatMap { configurations => + val parsedAll = configurations.foldLeft(Map[String, Value]()) { + case (acc, cur) => + acc ++ cur.values + } + + Toml.parseAs[Config](Value.Tbl(parsedAll)) + } + TomlUtils - .parseFile(path, Toml.parseAs[Config](_), "Seed configuration", log) + .parseFile(tomlPath, f, "Seed configuration", log) .getOrElse(sys.exit(1)) + } userConfigPath match { case None => diff --git a/src/main/scala/seed/config/util/TomlUtils.scala b/src/main/scala/seed/config/util/TomlUtils.scala index 339ac74..f9188d7 100644 --- a/src/main/scala/seed/config/util/TomlUtils.scala +++ b/src/main/scala/seed/config/util/TomlUtils.scala @@ -6,7 +6,7 @@ import org.apache.commons.io.FileUtils import seed.{Log, LogLevel} import seed.cli.util.Ansi import seed.model.Build.{PlatformModule, VersionTag} -import seed.model.{Platform, TomlBuild} +import seed.model.{Licence, Platform, TomlBuild} import toml.{Codec, Value} import scala.util.Try @@ -70,6 +70,23 @@ object TomlUtils { Left((List(), s"Version tag expected, $value provided")) } + implicit val licenceCodec: Codec[Licence] = Codec { + case (Value.Str(id), _, _) => + Licence.All.find(_.id == id) match { + case Some(licence) => Right(licence) + case _ => + Left( + ( + List(), + s"Invalid licence ID provided. Choose one of: ${Licence.All.map(_.id).mkString(", ")}" + ) + ) + } + + case (value, _, _) => + Left((List(), s"Licence ID expected, $value provided")) + } + def pathCodec(f: Path => Path): Codec[Path] = Codec { case (Value.Str(path), _, _) => Right(f(Paths.get(path))) case (value, _, _) => Left((List(), s"Path expected, $value provided")) diff --git a/src/main/scala/seed/generation/Bloop.scala b/src/main/scala/seed/generation/Bloop.scala index b7827ee..5904f39 100644 --- a/src/main/scala/seed/generation/Bloop.scala +++ b/src/main/scala/seed/generation/Bloop.scala @@ -12,12 +12,19 @@ import seed.config.BuildConfig.{ collectNativeClassPath, collectNativeDeps } -import seed.artefact.{ArtefactResolution, Coursier, SemanticVersioning} +import seed.artefact.{ArtefactResolution, Coursier} import seed.cli.util.Ansi import seed.model.Build.Module import seed.model.Platform.{JVM, JavaScript, Native} import seed.model.Resolution import seed.Log +import seed.artefact.ArtefactResolution.{ + CompilerResolution, + ModuleRef, + Regular, + RuntimeResolution, + Test +} import seed.config.BuildConfig import seed.generation.util.PathUtil @@ -133,14 +140,15 @@ object Bloop { def writeJsModule( build: Build, + moduleName: String, name: String, projectPath: Path, bloopPath: Path, buildPath: Path, jsOutputPath: Option[Path], module: Module, - resolution: Coursier.ResolutionResult, - compilerResolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, test: Boolean, optionalArtefacts: Boolean, log: Log @@ -162,7 +170,9 @@ object Bloop { ) val resolvedDeps = Coursier.localArtefacts( - resolution, + runtimeResolution( + (moduleName, JavaScript, if (test) Test else Regular) + ), collectJsDeps(build, test, js) .map( dep => @@ -193,8 +203,7 @@ object Bloop { js.scalaOrganisation.get, js.scalaVersion.get, resolvedDeps, - classPath, - optionalArtefacts + classPath ) writeBloop( @@ -227,14 +236,15 @@ object Bloop { def writeNativeModule( build: Build, + moduleName: String, name: String, projectPath: Path, bloopPath: Path, buildPath: Path, outputPathBinary: Option[Path], module: Module, - resolution: Coursier.ResolutionResult, - compilerResolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, test: Boolean, optionalArtefacts: Boolean, log: Log @@ -262,7 +272,7 @@ object Bloop { val resolvedDeps = Coursier.localArtefacts( - resolution, + runtimeResolution((moduleName, Native, if (test) Test else Regular)), collectNativeDeps(build, test, native) .map( dep => @@ -304,8 +314,7 @@ object Bloop { native.scalaOrganisation.get, native.scalaVersion.get, resolvedDeps, - classPath, - optionalArtefacts + classPath ) writeBloop( @@ -344,13 +353,14 @@ object Bloop { def writeJvmModule( build: Build, + moduleName: String, name: String, projectPath: Path, bloopPath: Path, buildPath: Path, module: Module, - resolution: Coursier.ResolutionResult, - compilerResolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, test: Boolean, optionalArtefacts: Boolean, log: Log @@ -365,10 +375,10 @@ object Bloop { dep => ArtefactResolution .javaDepFromScalaDep(dep, JVM, scalaVersion, scalaVersion) - ) + ) ++ ArtefactResolution.jvmPlatformDeps(jvm) val resolvedDeps = Coursier.localArtefacts( - resolution, + runtimeResolution((moduleName, JVM, if (test) Test else Regular)), (javaDeps ++ scalaDeps).toSet, optionalArtefacts ) @@ -398,8 +408,7 @@ object Bloop { jvm.scalaOrganisation.get, scalaVersion, resolvedDeps, - classPath, - optionalArtefacts + classPath ) writeBloop( @@ -439,8 +448,8 @@ object Bloop { buildPath: Path, bloopBuildPath: Path, build: Build, - resolution: Coursier.ResolutionResult, - compilerResolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, name: String, module: Module, optionalArtefacts: Boolean, @@ -464,13 +473,14 @@ object Bloop { writeJsModule( build, + name, if (!isCrossBuild) name else name + "-js", projectPath, bloopPath, bloopBuildPath, jsOutputPath, module, - resolution, + runtimeResolution, compilerResolution, test = false, optionalArtefacts, @@ -479,12 +489,13 @@ object Bloop { writeJvmModule( build, + name, if (!isCrossBuild) name else name + "-jvm", projectPath, bloopPath, bloopBuildPath, module, - resolution, + runtimeResolution, compilerResolution, test = false, optionalArtefacts, @@ -493,13 +504,14 @@ object Bloop { writeNativeModule( build, + name, if (!isCrossBuild) name else name + "-native", projectPath, bloopPath, bloopBuildPath, nativeOutputPath, module, - resolution, + runtimeResolution, compilerResolution, test = false, optionalArtefacts, @@ -522,13 +534,14 @@ object Bloop { writeJsModule( build, + name, if (!isCrossBuild) name else name + "-js", projectPath, bloopPath, bloopBuildPath, None, BuildConfig.mergeTestModule(build, module, JavaScript), - resolution, + runtimeResolution, compilerResolution, test = true, optionalArtefacts, @@ -537,13 +550,14 @@ object Bloop { writeNativeModule( build, + name, if (!isCrossBuild) name else name + "-native", projectPath, bloopPath, bloopBuildPath, None, BuildConfig.mergeTestModule(build, module, Native), - resolution, + runtimeResolution, compilerResolution, test = true, optionalArtefacts, @@ -552,12 +566,13 @@ object Bloop { writeJvmModule( build, + name, if (!isCrossBuild) name else name + "-jvm", projectPath, bloopPath, bloopBuildPath, BuildConfig.mergeTestModule(build, module, JVM), - resolution, + runtimeResolution, compilerResolution, test = true, optionalArtefacts, @@ -586,8 +601,8 @@ object Bloop { projectPath: Path, outputPath: Path, build: Build, - resolution: Coursier.ResolutionResult, - compilerResolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, tmpfs: Boolean, optionalArtefacts: Boolean, log: Log @@ -616,7 +631,7 @@ object Bloop { buildPath, bloopBuildPath, build, - resolution, + runtimeResolution, compilerResolution, name, module.module, diff --git a/src/main/scala/seed/generation/Idea.scala b/src/main/scala/seed/generation/Idea.scala index 01d3934..c23faee 100644 --- a/src/main/scala/seed/generation/Idea.scala +++ b/src/main/scala/seed/generation/Idea.scala @@ -22,6 +22,13 @@ import seed.model.{Platform, Resolution} import seed.model.Build.Module import seed.model.Platform.{JVM, JavaScript, Native} import seed.Log +import seed.artefact.ArtefactResolution.{ + CompilerResolution, + ModuleRef, + Regular, + RuntimeResolution, + Test +} import seed.config.BuildConfig import seed.generation.util.PathUtil.normalisePath @@ -126,27 +133,35 @@ object Idea { def createCompilerLibraries( modules: Build, - resolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, librariesPath: Path ): Unit = { - val scalaVersions = modules.values.toList - .map(_.module) - .flatMap( - module => - (module.jvm.toList ++ module.js.toList ++ module.native.toList) - .map(s => s.scalaOrganisation.get -> s.scalaVersion.get) - ) - .distinct + val scalaVersions = modules.toList + .flatMap { + case (name, module) => + val targets = BuildConfig.targetsFromPlatformModules(module.module) + targets.map { target => + val m = BuildConfig.platformModule(module.module, target).get + (m.scalaOrganisation.get, m.scalaVersion.get) -> (name, target, ArtefactResolution.Regular) + } + } + .groupBy(_._1) + .mapValues(_.map(_._2)) scalaVersions.foreach { - case (scalaOrganisation, scalaVersion) => + case ((scalaOrganisation, scalaVersion), moduleRef) => + val r = runtimeResolution(moduleRef.head) + val libraryDeps = + ArtefactResolution.scalaLibraryDeps(scalaOrganisation, scalaVersion) + val libraryArtefacts = Coursier.localArtefacts(r, libraryDeps, true) + val scalaCompiler = ArtefactResolution.resolveScalaCompiler( - resolution, + compilerResolution, scalaOrganisation, scalaVersion, - List(), - List(), - optionalArtefacts = true + libraryArtefacts, + List() ) val xml = IdeaFile.createLibrary( @@ -195,8 +210,8 @@ object Idea { else BuildConfig.ideaPlatformTargetName(build, m, target).toList val platformModule = BuildConfig.platformModule(module, target).get - ideaModules -> (platformModule.scalaOptions ++ util.ScalaCompiler - .compilerPlugIns( + ideaModules -> (platformModule.scalaOptions ++ + util.ScalaCompiler.compilerPlugIns( build, platformModule, compilerResolution, @@ -235,8 +250,7 @@ object Idea { ideaPath: Path, modulesPath: Path, librariesPath: Path, - compilerResolution: List[Coursier.ResolutionResult], - resolution: Coursier.ResolutionResult, + resolution: RuntimeResolution, name: String, module: Module, log: Log @@ -266,7 +280,7 @@ object Idea { sources = jsSources, tests = jsTests, resolvedDeps = Coursier.localArtefacts( - resolution, + resolution((name, JavaScript, Regular)), collectJsDeps(build, false, jsModule) .map( dep => @@ -286,7 +300,7 @@ object Idea { .flatMap( test => Coursier.localArtefacts( - resolution, + resolution((name, JavaScript, Test)), collectJsDeps(build, true, test) .map( dep => @@ -341,7 +355,7 @@ object Idea { sources = jvmSources, tests = jvmTests, resolvedDeps = Coursier.localArtefacts( - resolution, + resolution((name, JVM, Regular)), collectJvmJavaDeps(build, false, jvmModule).toSet ++ collectJvmScalaDeps(build, false, jvmModule) .map( @@ -362,7 +376,7 @@ object Idea { .flatMap( test => Coursier.localArtefacts( - resolution, + resolution((name, JVM, Test)), collectJvmJavaDeps(build, true, test).toSet ++ collectJvmScalaDeps(build, true, test) .map( @@ -420,7 +434,7 @@ object Idea { sources = nativeSources, tests = nativeTests, resolvedDeps = Coursier.localArtefacts( - resolution, + resolution((name, Native, Regular)), collectNativeDeps(build, false, nativeModule) .map( dep => @@ -440,7 +454,7 @@ object Idea { .flatMap( test => Coursier.localArtefacts( - resolution, + resolution((name, Native, Test)), collectNativeDeps(build, true, test) .map( dep => @@ -498,7 +512,7 @@ object Idea { sources = sharedSources, tests = sharedTests, resolvedDeps = Coursier.localArtefacts( - resolution, + resolution((name, platform, Regular)), collectJvmJavaDeps(build, false, module).toSet ++ collectJvmScalaDeps(build, false, module) .map( @@ -517,7 +531,7 @@ object Idea { .flatMap( test => Coursier.localArtefacts( - resolution, + resolution((name, platform, Test)), collectJvmJavaDeps(build, true, test).toSet ++ collectJvmScalaDeps(build, true, test) .map( @@ -609,8 +623,8 @@ object Idea { projectPath: Path, outputPath: Path, modules: Build, - resolution: Coursier.ResolutionResult, - compilerResolution: List[Coursier.ResolutionResult], + runtimeResolution: RuntimeResolution, + compilerResolution: CompilerResolution, tmpfs: Boolean, log: Log ): Unit = { @@ -641,7 +655,12 @@ object Idea { .asScala .foreach(Files.delete) - createCompilerLibraries(modules, compilerResolution, librariesPath) + createCompilerLibraries( + modules, + runtimeResolution, + compilerResolution, + librariesPath + ) FileUtils.write( ideaPath.resolve("misc.xml").toFile, IdeaFile.createJdk(jdkVersion = "1.8"), @@ -657,8 +676,7 @@ object Idea { ideaPath, modulesPath, librariesPath, - compilerResolution, - resolution, + runtimeResolution, name, module.module, log diff --git a/src/main/scala/seed/generation/Package.scala b/src/main/scala/seed/generation/Package.scala index f7e7284..ae09ec6 100644 --- a/src/main/scala/seed/generation/Package.scala +++ b/src/main/scala/seed/generation/Package.scala @@ -1,6 +1,6 @@ package seed.generation -import java.io.{File, FileInputStream, FileOutputStream} +import java.io.{File, FileInputStream, OutputStream} import java.util.jar.{Attributes, JarEntry, JarOutputStream, Manifest} import org.apache.commons.io.IOUtils @@ -15,7 +15,7 @@ import scala.collection.mutable object Package { def create( source: List[(Path, String)], - target: Path, + target: OutputStream, mainClass: Option[String], classPath: List[String], log: Log @@ -23,20 +23,20 @@ object Package { val manifest = new Manifest() val mainAttributes = manifest.getMainAttributes mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0") + // TODO Set additional package fields: https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html mainClass.foreach( cls => mainAttributes.put(Attributes.Name.MAIN_CLASS, cls) ) if (classPath.nonEmpty) mainAttributes.put(Attributes.Name.CLASS_PATH, classPath.mkString(" ")) - val targetFile = - new JarOutputStream(new FileOutputStream(target.toFile), manifest) + + val targetFile = new JarOutputStream(target, manifest) val entryCache = mutable.Set[String]() source.foreach { case (path, jarPath) => log.debug(s"Packaging ${Ansi.italic(path.toString)}...") add(path.toFile, jarPath, targetFile, entryCache, log) } - log.info(s"Written $target") targetFile.close() } diff --git a/src/main/scala/seed/generation/util/ScalaCompiler.scala b/src/main/scala/seed/generation/util/ScalaCompiler.scala index 4d8f389..2b82ade 100644 --- a/src/main/scala/seed/generation/util/ScalaCompiler.scala +++ b/src/main/scala/seed/generation/util/ScalaCompiler.scala @@ -5,40 +5,35 @@ import java.nio.file.Path import seed.artefact.Coursier import seed.config.BuildConfig import seed.artefact.ArtefactResolution +import seed.artefact.ArtefactResolution.CompilerResolution import seed.config.BuildConfig.Build import seed.model.Build.Module import seed.model.Platform.{JavaScript, Native} import seed.model.{Artefact, Build, Platform} object ScalaCompiler { - def resolveCompiler( - compilerResolution: List[Coursier.ResolutionResult], + private def resolveCompiler( + resolution: Coursier.ResolutionResult, artefact: Artefact, artefactVersion: String, platform: Platform, platformVer: String, compilerVer: String - ): Path = - compilerResolution.iterator - .flatMap( - resolution => - Coursier.artefactPath( - resolution, - artefact, - platform, - platformVer, - compilerVer, - artefactVersion - ) + ): Option[Path] = + Coursier + .artefactPath( + resolution, + artefact, + platform, + platformVer, + compilerVer, + artefactVersion ) - .toList - .headOption - .getOrElse(throw new Exception(s"Artefact '$artefact' missing")) def compilerPlugIns( build: Build, module: Module, - compilerResolution: List[Coursier.ResolutionResult], + compilerResolution: CompilerResolution, platform: Platform, compilerVer: String ): List[String] = { @@ -58,17 +53,24 @@ object ScalaCompiler { mergeDeps(dependencies) }).map(d => Artefact.fromDep(d) -> d.version) + // TODO Implement -Xplugin with dependencies: https://github.com/sbt/sbt/issues/2255 artefacts .map { case (artefact, version) => - resolveCompiler( - compilerResolution, - artefact, - version, - platform, - platformVer, - compilerVer - ) + compilerResolution + .flatMap( + r => + resolveCompiler( + r, + artefact, + version, + platform, + platformVer, + compilerVer + ) + ) + .headOption + .getOrElse(throw new Exception(s"Artefact '$artefact' missing")) } .map(p => "-Xplugin:" + p) } diff --git a/src/main/scala/seed/model/Build.scala b/src/main/scala/seed/model/Build.scala index 614ddc6..e3c60e6 100644 --- a/src/main/scala/seed/model/Build.scala +++ b/src/main/scala/seed/model/Build.scala @@ -6,6 +6,7 @@ import seed.artefact.MavenCentral case class TomlBuild( `import`: List[Path] = List(), + `package`: Build.Package = Build.Package(), project: Build.Project = Build.Project(), resolvers: Build.Resolvers = Build.Resolvers(), module: Map[String, Build.Module] @@ -57,6 +58,25 @@ object Build { await: Boolean = false ) + case class Developer(id: String, name: String, email: String) + + case class SourceManagement( + url: String, + connection: String, + developerConnection: Option[String] + ) + + case class Package( + name: Option[String] = None, + organisation: Option[String] = None, + developers: List[Developer] = List(), + url: Option[String] = None, + licences: List[Licence] = List(), + scm: Option[SourceManagement] = None, + sources: Boolean = true, + docs: Boolean = true + ) + case class Project( scalaVersion: Option[String] = None, scalaJsVersion: Option[String] = None, diff --git a/src/main/scala/seed/model/Config.scala b/src/main/scala/seed/model/Config.scala index 992e631..a7a61f0 100644 --- a/src/main/scala/seed/model/Config.scala +++ b/src/main/scala/seed/model/Config.scala @@ -8,7 +8,8 @@ import seed.artefact.Coursier case class Config( cli: Config.Cli = Config.Cli(), build: Config.Build = Config.Build(), - resolution: Config.Resolution = Config.Resolution() + resolution: Config.Resolution = Config.Resolution(), + repository: Config.Repository = Config.Repository() ) object Config { @@ -24,4 +25,6 @@ object Config { cachePath: Path = Coursier.DefaultCachePath, optionalArtefacts: Boolean = false ) + case class Repository(bintray: BintrayRepository = BintrayRepository()) + case class BintrayRepository(user: String = "", apiKey: String = "") } diff --git a/src/main/scala/seed/model/Licence.scala b/src/main/scala/seed/model/Licence.scala new file mode 100644 index 0000000..59ae7d2 --- /dev/null +++ b/src/main/scala/seed/model/Licence.scala @@ -0,0 +1,83 @@ +package seed.model + +// Adapted from https://github.com/sbt/sbt-license-report/blob/ffff4e456e153f7149df5a58302e9eb96b5a4744/src/main/scala/com/typesafe/sbt/license/Licence.scala#L1 +case class Licence(id: String, name: String, url: String) +object Licence { + val GPL2 = Licence( + "gpl:2.0", + "GNU General Public License (GPL), Version 2.0", + "http://opensource.org/licenses/GPL-2.0" + ) + val GPL3 = Licence( + "gpl:3.0", + "GNU General Public License (GPL), Version 3.0", + "http://opensource.org/licenses/GPL-3.0" + ) + val LGPL2 = Licence( + "lgpl:2.1", + "GNU Library or \"Lesser\" General Public License (LGPL), Version 2.1", + "http://opensource.org/licenses/LGPL-2.1" + ) + val LGPL3 = Licence( + "lgpl:3.0", + "GNU Library or \"Lesser\" General Public License (LGPL), Version 3.0", + "http://opensource.org/licenses/LGPL-3.0" + ) + val CDDL = Licence( + "cddl:1.0", + "Common Development and Distribution License (CDDL), Version 1.0", + "http://opensource.org/licenses/CDDL-1.0" + ) + val CDDL_GPL = Licence( + "cddl+gpl", + "CDDL + GPLv2 License", + "https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html" + ) + val Apache2 = Licence( + "apache:2.0", + "Apache License, Version 2.0", + "http://www.apache.org/licenses/LICENSE-2.0" + ) + val BSD2 = Licence( + "bsd:2", + "BSD 2-Clause", + "http://opensource.org/licenses/BSD-2-Clause" + ) + val BSD3 = Licence( + "bsd:3", + "BSD 3-Clause", + "http://opensource.org/licenses/BSD-3-Clause" + ) + val MIT = Licence("mit", "MIT License", "http://opensource.org/licenses/MIT") + val EPL = Licence( + "epl:1.0", + "Eclipse Public License, Version 1.0", + "https://www.eclipse.org/legal/epl-v10.html" + ) + val EDL = Licence( + "ecl:1.0", + "Eclipse Distribution License, Version 1.0", + "http://www.eclipse.org/org/documents/edl-v10.php" + ) + val MPL = Licence( + "mpl:2.0", + "Mozilla Public License, Version 2.0", + "https://www.mozilla.org/MPL/2.0/" + ) + + val All = List( + GPL2, + GPL3, + LGPL2, + LGPL3, + CDDL, + CDDL_GPL, + Apache2, + BSD2, + BSD3, + MIT, + EPL, + EDL, + MPL + ) +} diff --git a/src/main/scala/seed/publish/Bintray.scala b/src/main/scala/seed/publish/Bintray.scala new file mode 100644 index 0000000..da163ac --- /dev/null +++ b/src/main/scala/seed/publish/Bintray.scala @@ -0,0 +1,89 @@ +package seed.publish + +import io.circe.parser._ +import io.circe.generic.auto._ +import seed.Log +import seed.publish.util.Http +import zio._ + +object Bintray { + val Host = "https://bintray.com" + + // TODO Publish upload progress + def uploadMavenFile( + http: Http, + log: Log, + organisation: String, + repository: String, + `package`: String, + packageGroup: String, + packageArtefact: String, + packageClassifier: String, + packageVersion: String, + bytes: Array[Byte], + extension: String + ): UIO[Unit] = { + val packageParts = packageGroup.split('.').mkString("/") + + val url = + s"$Host/api/v1/maven/$organisation/$repository/${`package`}/$packageParts/$packageArtefact/$packageVersion/$packageArtefact-$packageVersion$packageClassifier.$extension" + + http + .put(url, bytes) + .option + .flatMap { + case None => Task.interrupt + case Some(response) => + decode[FileUploadResponse](response) match { + case Left(_) => + log.error(s"Server response could not be parsed: $response") + Task.interrupt + case Right(m) => + if (m.message == "success") { + log.info("Request successful") + Task.succeed(()) + } else { + log.error(s"Server reported error:") + log.error(m.message) + Task.interrupt + } + } + } + } + + def publishContent( + http: Http, + log: Log, + organisation: String, + repository: String, + `package`: String, + packageVersion: String + ): UIO[Unit] = { + val url = + s"$Host/api/v1/content/$organisation/$repository/${`package`}/$packageVersion/publish" + + http + .post(url, Array()) + .option + .flatMap { + case None => Task.interrupt + case Some(response) => + decode[ContentPublishResponse](response) match { + case Left(_) => + log.error(s"Server response could not be parsed: $response") + Task.interrupt + case Right(m) => + if (m.files > 0) { + log.info(s"${m.files} files were published") + Task.succeed(()) + } else { + log.error("No files were published") + Task.interrupt + } + } + } + } + + case class FileUploadResponse(message: String) + case class ContentPublishResponse(files: Int) +} diff --git a/src/main/scala/seed/publish/util/CompletableHttpAsyncClient.java b/src/main/scala/seed/publish/util/CompletableHttpAsyncClient.java new file mode 100644 index 0000000..0775931 --- /dev/null +++ b/src/main/scala/seed/publish/util/CompletableHttpAsyncClient.java @@ -0,0 +1,56 @@ +package seed.publish.util; + +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.nio.client.HttpAsyncClient; +import org.apache.http.nio.protocol.HttpAsyncRequestProducer; +import org.apache.http.nio.protocol.HttpAsyncResponseConsumer; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +// From https://gist.github.com/tomfitzhenry/4bb032f6a9f56d95f6fb54454e617fc1 +public class CompletableHttpAsyncClient { + private final HttpAsyncClient httpAsyncClient; + + public CompletableHttpAsyncClient(HttpAsyncClient httpAsyncClient) { + this.httpAsyncClient = httpAsyncClient; + } + + public CompletableFuture execute( + HttpAsyncRequestProducer httpAsyncRequestProducer, + HttpAsyncResponseConsumer httpAsyncResponseConsumer, + HttpClientContext httpClientContext + ) { + return toCompletableFuture(fc -> + httpAsyncClient.execute( + httpAsyncRequestProducer, + httpAsyncResponseConsumer, + httpClientContext, + fc)); + } + + private static CompletableFuture toCompletableFuture( + Consumer> c + ) { + CompletableFuture promise = new CompletableFuture<>(); + c.accept(new FutureCallback() { + @Override + public void completed(T t) { + promise.complete(t); + } + + @Override + public void failed(Exception e) { + promise.completeExceptionally(e); + } + + @Override + public void cancelled() { + promise.cancel(true); + } + }); + + return promise; + } +} diff --git a/src/main/scala/seed/publish/util/Http.scala b/src/main/scala/seed/publish/util/Http.scala new file mode 100644 index 0000000..32e2519 --- /dev/null +++ b/src/main/scala/seed/publish/util/Http.scala @@ -0,0 +1,77 @@ +package seed.publish.util + +import java.net.URI + +import org.apache.commons.io.IOUtils +import org.apache.http.{HttpHost, HttpRequest, HttpRequestInterceptor} +import org.apache.http.entity.ContentType +import seed.util.ZioHelpers._ +import zio.Task +import org.apache.http.auth.AuthScope +import org.apache.http.auth.UsernamePasswordCredentials +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.impl.auth.BasicScheme +import org.apache.http.impl.client.{BasicAuthCache, BasicCredentialsProvider} +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient +import org.apache.http.impl.nio.client.HttpAsyncClients +import org.apache.http.nio.client.methods.HttpAsyncMethods +import org.apache.http.nio.protocol.HttpAsyncRequestProducer +import org.apache.http.protocol.HttpContext + +class Http(httpClient: CloseableHttpAsyncClient) { + def put(url: String, bytes: Array[Byte]): Task[String] = { + val producer = + HttpAsyncMethods.createPut(url, bytes, ContentType.DEFAULT_BINARY) + send(url, producer) + } + + def post(url: String, bytes: Array[Byte]): Task[String] = { + val producer = + HttpAsyncMethods.createPost(url, bytes, ContentType.DEFAULT_BINARY) + send(url, producer) + } + + def destroy(): Unit = httpClient.close() + + private def send(url: String, producer: HttpAsyncRequestProducer) = { + val client = new CompletableHttpAsyncClient(httpClient) + + val uri = URI.create(url) + val targetHost = new HttpHost(uri.getHost, uri.getPort, uri.getScheme) + val authCache = new BasicAuthCache() + authCache.put(targetHost, new BasicScheme()) + + val clientContext = HttpClientContext.create() + clientContext.setAuthCache(authCache) + + val future = + client.execute(producer, HttpAsyncMethods.createConsumer(), clientContext) + + fromCompletableFuture(future) + .map(r => IOUtils.toString(r.getEntity.getContent, "UTF-8")) + } +} + +class CustomRequestInterceptor(log: seed.Log) extends HttpRequestInterceptor { + override def process(request: HttpRequest, context: HttpContext): Unit = + log.debug("Sending HTTP request " + request + "...") +} + +object Http { + def create(log: seed.Log, authHost: String, auth: (String, String)): Http = { + val credsProvider = new BasicCredentialsProvider() + credsProvider.setCredentials( + new AuthScope(authHost, 443), + new UsernamePasswordCredentials(auth._1, auth._2) + ) + + val c = HttpAsyncClients + .custom() + .setDefaultCredentialsProvider(credsProvider) + .addInterceptorFirst(new CustomRequestInterceptor(log)) + .build() + c.start() + + new Http(c) + } +} diff --git a/src/main/scala/seed/util/ZioHelpers.scala b/src/main/scala/seed/util/ZioHelpers.scala new file mode 100644 index 0000000..3a46711 --- /dev/null +++ b/src/main/scala/seed/util/ZioHelpers.scala @@ -0,0 +1,38 @@ +package seed.util + +import java.util.concurrent.{ + CompletableFuture, + Executor, + RejectedExecutionException +} +import scala.concurrent.CancellationException +import zio._ + +object ZioHelpers { + def fromCompletableFuture[T](future: => CompletableFuture[T]): Task[T] = + Task.descriptorWith( + d => + ZIO + .effect(future) + .flatMap( + f => + Task + .effectAsync { (cb: Task[T] => Unit) => + f.whenCompleteAsync( + (v: T, e: Throwable) => + if (e == null) cb(Task.succeed(v)) + else if (!e.isInstanceOf[CancellationException]) + cb(Task.fail(e)), + ( + r => + if (!d.executor.submit(r)) + throw new RejectedExecutionException( + "Rejected: " + r.toString + ) + ): Executor + ) + } + .onTermination(_ => UIO(if (!f.isDone) f.cancel(true))) + ) + ) +} diff --git a/src/test/scala/seed/artefact/ArtefactResolutionSpec.scala b/src/test/scala/seed/artefact/ArtefactResolutionSpec.scala index a796368..192e816 100644 --- a/src/test/scala/seed/artefact/ArtefactResolutionSpec.scala +++ b/src/test/scala/seed/artefact/ArtefactResolutionSpec.scala @@ -1,18 +1,24 @@ package seed.artefact import java.io.File -import java.nio.charset.Charset -import java.nio.file.Paths +import java.nio.file.{Files, Path, Paths} import minitest.SimpleTestSuite import org.apache.commons.io.FileUtils +import seed.cli +import seed.Cli.Command +import seed.Log +import seed.artefact.ArtefactResolution.{Regular, Test} +import seed.config.BuildConfig.ModuleConfig import seed.config.{BuildConfig, BuildConfigSpec} import seed.generation.util.ProjectGeneration -import seed.model.Build.{JavaDep, Module, ScalaDep, VersionTag} +import seed.model.Build.{JavaDep, Module, Resolvers, ScalaDep, VersionTag} import seed.model.Platform.{JVM, JavaScript, Native} +import seed.generation.util.BuildUtil.tempPath +import seed.model.Config object ArtefactResolutionSpec extends SimpleTestSuite { - test("dependencyFromScalaDep() with Scala.js dependency") { + test("javaDepFromScalaDep() with Scala.js dependency") { val scalaDep = ScalaDep("org.scala-js", "scalajs-dom", "0.9.6") val javaDep = ArtefactResolution.javaDepFromScalaDep( scalaDep, @@ -39,7 +45,7 @@ object ArtefactResolutionSpec extends SimpleTestSuite { ) } - test("Extract platform dependencies of test module in libraryDeps()") { + test("Derive runtime libraries from test module") { val modules = Map( "a" -> Module( scalaVersion = Some("2.12.8"), @@ -47,7 +53,7 @@ object ArtefactResolutionSpec extends SimpleTestSuite { targets = List(JVM, JavaScript), test = Some( Module( - sources = List(Paths.get("a/test")), + sources = List(Paths.get("a", "test")), scalaDeps = List(ScalaDep("io.monix", "minitest", "2.3.2")) ) ) @@ -69,12 +75,57 @@ object ArtefactResolutionSpec extends SimpleTestSuite { ) ) - val libraryDeps = ArtefactResolution.allLibraryDeps(build) + val libs = ArtefactResolution.allRuntimeLibs(build) assertEquals( - libraryDeps, - Set( - JavaDep("io.monix", "minitest_2.12", "2.3.2"), - JavaDep("io.monix", "minitest_sjs0.6_2.12", "2.3.2") + libs, + Map( + ("a", JVM, Regular) -> Set( + JavaDep("org.scala-lang", "scala-library", "2.12.8"), + JavaDep("org.scala-lang", "scala-reflect", "2.12.8") + ), + ("a", JVM, Test) -> Set( + JavaDep("io.monix", "minitest_2.12", "2.3.2") + ), + ("a", JavaScript, Regular) -> Set( + JavaDep("org.scala-js", "scalajs-library_2.12", "0.6.26"), + JavaDep("org.scala-lang", "scala-library", "2.12.8"), + JavaDep("org.scala-lang", "scala-reflect", "2.12.8") + ), + ("a", JavaScript, Test) -> Set( + JavaDep("io.monix", "minitest_sjs0.6_2.12", "2.3.2") + ) + ) + ) + } + + test("Derive runtime libraries from test module (2)") { + // The test module overrides the target platforms. Therefore, no Scala + // Native dependencies should be fetched. + val path = new File("test", "test-module-dep") + val tomlBuild = + FileUtils.readFileToString(new File(path, "build.toml"), "UTF-8") + val build = BuildConfigSpec.parseBuild(tomlBuild)(_ => "") + + val libs = ArtefactResolution.allRuntimeLibs(build) + assertEquals( + libs, + Map( + ("example", JVM, Regular) -> Set( + JavaDep("org.scala-lang", "scala-library", "2.11.11"), + JavaDep("org.scala-lang", "scala-reflect", "2.11.11") + ), + ("example", JVM, Test) -> Set( + JavaDep("org.scalatest", "scalatest_2.11", "3.0.8") + ), + ("example", Native, Regular) -> Set( + JavaDep("org.scala-lang", "scala-reflect", "2.11.11"), + JavaDep("org.scala-lang", "scala-library", "2.11.11"), + JavaDep("org.scala-native", "javalib_native0.3_2.11", "0.3.7"), + JavaDep("org.scala-native", "scalalib_native0.3_2.11", "0.3.7"), + JavaDep("org.scala-native", "nativelib_native0.3_2.11", "0.3.7"), + JavaDep("org.scala-native", "auxlib_native0.3_2.11", "0.3.7") + ), + ("example", Native, Test) -> Set() ) ) } @@ -117,27 +168,34 @@ object ArtefactResolutionSpec extends SimpleTestSuite { ) ) - val deps = ArtefactResolution.compilerDeps( - BuildConfig.inheritSettings(Module())(module) + val jsDeps = ArtefactResolution.compilerDeps( + BuildConfig.inheritSettings(Module())(module), + JavaScript + ) + val jvmDeps = ArtefactResolution.compilerDeps( + BuildConfig.inheritSettings(Module())(module), + JVM ) assertEquals( - deps, - List( - Set( - JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), - JavaDep("org.scala-lang", "scala-library", "2.12.8"), - JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), - JavaDep("org.scala-js", "scalajs-compiler_2.12.8", "0.6.26"), - JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.1"), - JavaDep("com.softwaremill.clippy", "plugin_2.12", "0.6.0") - ), - Set( - JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), - JavaDep("org.scala-lang", "scala-library", "2.12.8"), - JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), - JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.1") - ) + jsDeps, + Set( + JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), + JavaDep("org.scala-lang", "scala-library", "2.12.8"), + JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), + JavaDep("org.scala-js", "scalajs-compiler_2.12.8", "0.6.26"), + JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.1"), + JavaDep("com.softwaremill.clippy", "plugin_2.12", "0.6.0") + ) + ) + + assertEquals( + jvmDeps, + Set( + JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), + JavaDep("org.scala-lang", "scala-library", "2.12.8"), + JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), + JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.1") ) ) } @@ -158,26 +216,33 @@ object ArtefactResolutionSpec extends SimpleTestSuite { ) ) - val deps = ArtefactResolution.compilerDeps( - BuildConfig.inheritSettings(Module())(module) + val jsDeps = ArtefactResolution.compilerDeps( + BuildConfig.inheritSettings(Module())(module), + JavaScript + ) + val jvmDeps = ArtefactResolution.compilerDeps( + BuildConfig.inheritSettings(Module())(module), + JVM ) assertEquals( - deps, - List( - Set( - JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), - JavaDep("org.scala-lang", "scala-library", "2.12.8"), - JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), - JavaDep("org.scala-js", "scalajs-compiler_2.12.8", "0.6.26"), - JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.1") - ), - Set( - JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), - JavaDep("org.scala-lang", "scala-library", "2.12.8"), - JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), - JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.0") - ) + jsDeps, + Set( + JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), + JavaDep("org.scala-lang", "scala-library", "2.12.8"), + JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), + JavaDep("org.scala-js", "scalajs-compiler_2.12.8", "0.6.26"), + JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.1") + ) + ) + + assertEquals( + jvmDeps, + Set( + JavaDep("org.scala-lang", "scala-compiler", "2.12.8"), + JavaDep("org.scala-lang", "scala-library", "2.12.8"), + JavaDep("org.scala-lang", "scala-reflect", "2.12.8"), + JavaDep("org.scalamacros", "paradise_2.12.8", "2.1.0") ) ) } @@ -236,32 +301,136 @@ object ArtefactResolutionSpec extends SimpleTestSuite { ) ) - val deps = ArtefactResolution.compilerDeps( - BuildConfig.inheritSettings(Module())(module) + val nativeDeps = ArtefactResolution.compilerDeps( + BuildConfig.inheritSettings(Module())(module), + Native ) assertEquals( - deps, + nativeDeps, + Set( + JavaDep("org.scala-lang", "scala-compiler", "2.11.11"), + JavaDep("org.scala-lang", "scala-library", "2.11.11"), + JavaDep("org.scala-lang", "scala-reflect", "2.11.11"), + JavaDep("org.scala-native", "nscplugin_2.11.11", "0.3.7") + ) + ) + } + + import seed.generation.BloopIntegrationSpec.packageConfig + import seed.generation.BloopIntegrationSpec.readBloopJson + + test("Resolve library versions requested by modules") { + val projectPath = Paths.get("test", "resolve-dep-versions") + val result = BuildConfig.load(projectPath, Log.urgent).get + import result._ + val buildPath = tempPath.resolve("resolve-dep-versions") + Files.createDirectory(buildPath) + cli.Generate.ui( + Config(), + projectPath, + buildPath, + resolvers, + build, + Command.Bloop(packageConfig), + Log.urgent + ) + + val bloopPath = buildPath.resolve(".bloop") + + val a = readBloopJson(bloopPath.resolve("a.json")) + val b = readBloopJson(bloopPath.resolve("b.json")) + val example1 = readBloopJson(bloopPath.resolve("example1.json")) + val example2 = readBloopJson(bloopPath.resolve("example2.json")) + + def artefact(path: Path): String = path.getFileName.toString + + assertEquals( + a.project.classpath.map(artefact).sorted, List( - Set( - JavaDep("org.scala-lang", "scala-compiler", "2.11.11"), - JavaDep("org.scala-lang", "scala-library", "2.11.11"), - JavaDep("org.scala-lang", "scala-reflect", "2.11.11"), - JavaDep("org.scala-native", "nscplugin_2.11.11", "0.3.7") - ) + "pine_sjs0.6_2.11-0.1.6.jar", + "scala-library-2.11.11-bin-typelevel-4.jar", + "scala-reflect-2.11.11-bin-typelevel-4.jar", + "scalajs-dom_sjs0.6_2.11-0.9.7.jar", + "scalajs-library_2.11-0.6.28.jar" + ) + ) + assertEquals( + b.project.classpath.map(artefact).sorted, + List( + "scala-library-2.11.11-bin-typelevel-4.jar", + "scala-reflect-2.11.11-bin-typelevel-4.jar", + "scalajs-dom_sjs0.6_2.11-0.9.8.jar", + "scalajs-library_2.11-0.6.28.jar" + ) + ) + assertEquals( + example1.project.classpath.map(artefact).sorted, + List( + "a", + "pine_sjs0.6_2.11-0.1.6.jar", + "scala-library-2.11.11-bin-typelevel-4.jar", + "scala-reflect-2.11.11-bin-typelevel-4.jar", + "scalajs-dom_sjs0.6_2.11-0.9.7.jar", + "scalajs-library_2.11-0.6.28.jar" + ) + ) + assertEquals( + example2.project.classpath.map(artefact).sorted, + List( + "a", + "b", + "pine_sjs0.6_2.11-0.1.6.jar", + "scala-library-2.11.11-bin-typelevel-4.jar", + "scala-reflect-2.11.11-bin-typelevel-4.jar", + "scalajs-dom_sjs0.6_2.11-0.9.8.jar", + "scalajs-library_2.11-0.6.28.jar" ) ) } - test("Resolve correct platform libraries on test module") { - // The test module overrides the target platforms. Therefore, no Scala - // Native dependencies should be fetched. - val path = new File("test", "test-module-dep") - val tomlBuild = - FileUtils.readFileToString(new File(path, "build.toml"), "UTF-8") - val build = BuildConfigSpec.parseBuild(tomlBuild)(_ => "") + test("Resolve Typelevel Scala compiler JARs") { + val scalaOrganisation = "org.typelevel" + val scalaVersion = "2.12.4-bin-typelevel-4" - val deps = ArtefactResolution.allLibraryDeps(build) - assertEquals(deps, Set(JavaDep("org.scalatest", "scalatest_2.11", "3.0.8"))) + val build: Map[String, ModuleConfig] = Map( + "example" -> ModuleConfig( + BuildConfig.inheritSettings(Module())( + Module( + scalaOrganisation = Some(scalaOrganisation), + scalaVersion = Some(scalaVersion), + targets = List(JVM) + ) + ), + Paths.get(".") + ) + ) + + val compilerResolutions = ArtefactResolution.compilerResolution( + build, + seed.model.Config(), + Resolvers(), + packageConfig, + optionalArtefacts = false, + Log.urgent + ) + + val scalaCompiler = ArtefactResolution.resolveScalaCompiler( + compilerResolutions, + scalaOrganisation, + scalaVersion, + List(), + List() + ) + + assertEquals( + scalaCompiler.compilerJars.map(_.getFileName.toString), + List( + "scala-xml_2.12-1.0.6.jar", + "scala-compiler-2.12.4-bin-typelevel-4.jar", + "scala-library-2.12.4-bin-typelevel-4.jar", + "scala-reflect-2.12.4-bin-typelevel-4.jar" + ) + ) } } diff --git a/src/test/scala/seed/artefact/CoursierSpec.scala b/src/test/scala/seed/artefact/CoursierSpec.scala index 7ba38b7..1df45f1 100644 --- a/src/test/scala/seed/artefact/CoursierSpec.scala +++ b/src/test/scala/seed/artefact/CoursierSpec.scala @@ -1,5 +1,7 @@ package seed.artefact +import java.nio.file.Path + import minitest.SimpleTestSuite import seed.Log import seed.model.{Artefact, Build, Platform} @@ -8,10 +10,14 @@ import seed.model.Build.JavaDep object CoursierSpec extends SimpleTestSuite { var resolution: Coursier.ResolutionResult = _ + val scalaOrganisation = "org.scala-lang" + val scalaVersion = "2.12.8" + test("Resolve dependency") { val dep = JavaDep("org.scala-js", "scalajs-dom_sjs0.6_2.12", "0.9.6") resolution = Coursier.resolveAndDownload( Set(dep), + (scalaOrganisation, scalaVersion), Build.Resolvers(), Coursier.DefaultIvyPath, Coursier.DefaultCachePath, @@ -55,4 +61,31 @@ object CoursierSpec extends SimpleTestSuite { ) assert(path2.isEmpty) } + + test("Resolve dependency for Typelevel Scala") { + val dep = JavaDep("org.scala-js", "scalajs-dom_sjs0.6_2.12", "0.9.6") + val resolution = Coursier.resolveAndDownload( + Set(dep), + ("org.typelevel", "2.12.4-bin-typelevel-4"), + Build.Resolvers(), + Coursier.DefaultIvyPath, + Coursier.DefaultCachePath, + optionalArtefacts = true, + silent = true, + Log.urgent + ) + + def artefact(path: Path): String = path.getFileName.toString + + val result = + Coursier.localArtefacts(resolution, Set(dep), optionalArtefacts = true) + assertEquals( + result.map(_.libraryJar).map(artefact), + List( + "scalajs-library_2.12-0.6.23.jar", + "scala-library-2.12.4-bin-typelevel-4.jar", + "scalajs-dom_sjs0.6_2.12-0.9.6.jar" + ) + ) + } } diff --git a/src/test/scala/seed/cli/DocSpec.scala b/src/test/scala/seed/cli/DocSpec.scala new file mode 100644 index 0000000..c4e88f8 --- /dev/null +++ b/src/test/scala/seed/cli/DocSpec.scala @@ -0,0 +1,69 @@ +package seed.cli + +import java.nio.file.{Files, Paths} + +import minitest.TestSuite +import seed.Cli.Command +import seed.cli.util.RTS +import seed.config.BuildConfig +import seed.{Log, cli} +import seed.generation.BloopIntegrationSpec +import seed.generation.BloopIntegrationSpec.packageConfig +import seed.generation.util.BuildUtil.tempPath +import seed.model.Config +import seed.generation.util.TestProcessHelper + +object DocSpec extends TestSuite[Unit] { + override def setupSuite(): Unit = TestProcessHelper.semaphore.acquire() + override def tearDownSuite(): Unit = TestProcessHelper.semaphore.release() + + override def setup(): Unit = () + override def tearDown(env: Unit): Unit = () + + private val log = Log.urgent + + private def testProject(name: String, modules: List[String]) = { + val projectPath = Paths.get("test").resolve(name) + + val config = BuildConfig.load(projectPath, log).get + import config._ + + val buildPath = tempPath.resolve(name + "-doc") + if (!Files.exists(buildPath)) Files.createDirectory(buildPath) + + cli.Generate.ui( + Config(), + projectPath, + buildPath, + resolvers, + build, + Command.Bloop(packageConfig), + Log.urgent + ) + + val command = + Command.Doc(BloopIntegrationSpec.packageConfig, Some(buildPath), modules) + val uio = Doc.ui( + config, + buildPath, + Config(), + command, + command.packageConfig, + progress = false, + log + ) + RTS.unsafeRunToFuture(uio) + } + + testAsync("Document Scala 2.11 project with Macro Paradise") { _ => + testProject("example-paradise", List("macros", "example")) + } + + testAsync("Document Scala 2.12 project using Typelevel compiler") { _ => + testProject("compiler-options", List("demo:jvm")) + } + + testAsync("Document Scala.js 2.13 project") { _ => + testProject("submodule-output-path", List("app:js")) + } +} diff --git a/src/test/scala/seed/cli/util/ConsoleOutputSpec.scala b/src/test/scala/seed/cli/util/ConsoleOutputSpec.scala new file mode 100644 index 0000000..5b78fa1 --- /dev/null +++ b/src/test/scala/seed/cli/util/ConsoleOutputSpec.scala @@ -0,0 +1,57 @@ +package seed.cli.util + +import minitest.SimpleTestSuite +import seed.Log + +import scala.util.Try + +object ConsoleOutputSpec extends SimpleTestSuite { + test("Write regular") { + var output = "" + val co = new ConsoleOutput(Log.silent, output += _) + co.writeRegular("a\n") + assertEquals(output, "a\n") + co.writeRegular("b\n") + assertEquals(output, "a\nb\n") + } + + test("Write sticky and update") { + var output = "" + val co = new ConsoleOutput(Log.silent, output += _) + co.writeSticky("abc\n") + assertEquals(output, "abc\n") + + co.writeSticky("d\n") + + // Move up and clear until the end of the line + val moveUp = s"${Ansi.Escape}1A" + val clear = s"${Ansi.Escape}0K" + assertEquals(output, s"abc\n${moveUp}d$clear\n") + + // Number of lines must match (1 != 2) + assert(Try(co.writeSticky("b\nc\n")).isFailure) + + output = "" + co.writeSticky("ef\n") + assertEquals(output, s"${moveUp}ef$clear\n") + } + + test("Write regular after sticky") { + var output = "" + val co = new ConsoleOutput(Log.silent, output += _) + co.writeSticky("abc\ndef\n") + co.writeRegular("g\n") + + val moveUp = s"${Ansi.Escape}2A" + val clear = s"${Ansi.Escape}0K" + assertEquals(output, s"abc\ndef\n${moveUp}g$clear\nabc$clear\ndef\n") + + output = "" + co.writeSticky("hi\njk\n") + assertEquals(output, s"${moveUp}hi${clear}\njk${clear}\n") + + output = "" + co.writeRegular("m\n") + assertEquals(output, s"${moveUp}m${clear}\nhi$clear\njk\n") + } +} diff --git a/src/test/scala/seed/cli/util/TargetSpec.scala b/src/test/scala/seed/cli/util/TargetSpec.scala index c5fce03..12cce0c 100644 --- a/src/test/scala/seed/cli/util/TargetSpec.scala +++ b/src/test/scala/seed/cli/util/TargetSpec.scala @@ -5,9 +5,10 @@ import java.nio.file.Paths import minitest.SimpleTestSuite import seed.config.BuildConfig.ModuleConfig import seed.model.{Build, Platform} -import seed.model.Build.Module object TargetSpec extends SimpleTestSuite { + import seed.model.Build.Module + test("Parse module string") { assertEquals( Target.parseModuleString(Map())(""), @@ -23,7 +24,9 @@ object TargetSpec extends SimpleTestSuite { Target.parseModuleString( Map("test" -> ModuleConfig(Module(), Paths.get("."))) )("test:jvm"), - Left(s"Invalid build target ${Ansi.italic("jvm")} provided") + Left( + s"Invalid build target ${Ansi.italic("jvm")} provided on module ${Ansi.italic("test")}" + ) ) assertEquals( @@ -44,7 +47,8 @@ object TargetSpec extends SimpleTestSuite { Target.parseModuleString( Map("test" -> ModuleConfig(Module(), Paths.get("."))) )("test:custom"), - Left(s"Invalid build target ${Ansi.italic("custom")} provided") + Left(s"Invalid build target ${Ansi + .italic("custom")} provided on module ${Ansi.italic("test")}") ) assertEquals( diff --git a/src/test/scala/seed/config/BuildConfigSpec.scala b/src/test/scala/seed/config/BuildConfigSpec.scala index fe565a7..5c8986f 100644 --- a/src/test/scala/seed/config/BuildConfigSpec.scala +++ b/src/test/scala/seed/config/BuildConfigSpec.scala @@ -121,7 +121,7 @@ object BuildConfigSpec extends SimpleTestSuite { parsed.right.get, Paths.get("."), { path => val build = parseBuild(f(path))(f) - Some(Result(Paths.get("."), Resolvers(), build)) + Some(Result(Paths.get("."), Build.Package(), Resolvers(), build)) }, Log.urgent ) diff --git a/src/test/scala/seed/generation/BloopIntegrationSpec.scala b/src/test/scala/seed/generation/BloopIntegrationSpec.scala index abd0a78..fcbc702 100644 --- a/src/test/scala/seed/generation/BloopIntegrationSpec.scala +++ b/src/test/scala/seed/generation/BloopIntegrationSpec.scala @@ -52,7 +52,7 @@ object BloopIntegrationSpec extends TestSuite[Unit] { for { _ <- compile; _ <- run } yield () } - private val packageConfig = PackageConfig( + private[seed] val packageConfig = PackageConfig( tmpfs = false, silent = false, ivyPath = None, @@ -258,6 +258,7 @@ object BloopIntegrationSpec extends TestSuite[Unit] { .get import config._ val buildPath = tempPath.resolve("multiple-scala-versions-bloop") + Files.createDirectory(buildPath) cli.Generate.ui( Config(), diff --git a/src/test/scala/seed/generation/IdeaSpec.scala b/src/test/scala/seed/generation/IdeaSpec.scala index efb0add..7475033 100644 --- a/src/test/scala/seed/generation/IdeaSpec.scala +++ b/src/test/scala/seed/generation/IdeaSpec.scala @@ -1,7 +1,7 @@ package seed.generation import minitest.SimpleTestSuite -import java.nio.file.{Files, Paths} +import java.nio.file.{Files, Path, Paths} import org.apache.commons.io.FileUtils import seed.Cli.{Command, PackageConfig} @@ -15,6 +15,10 @@ import seed.model.{Build, Config} import seed.generation.util.BuildUtil.tempPath object IdeaSpec extends SimpleTestSuite { + private val seedConfig = seed.model.Config() + + private val resolvers = Resolvers() + private val packageConfig = PackageConfig( tmpfs = false, silent = false, @@ -22,12 +26,21 @@ object IdeaSpec extends SimpleTestSuite { cachePath = None ) + private val log = Log.urgent + private def dropPath(path: String): String = path.lastIndexOf('/') match { case -1 => path case n => path.drop(n + 1) } + private def createProject(name: String): (BuildConfig.Result, Path) = { + val result = BuildConfig.load(Paths.get("test", name), Log.urgent).get + val outputPath = tempPath.resolve(name + "-idea") + Files.createDirectory(outputPath) + (result, outputPath) + } + test("Normalise paths") { assertEquals( PathUtil.normalisePath(Idea.ModuleDir, Paths.get("/tmp"))( @@ -104,26 +117,31 @@ object IdeaSpec extends SimpleTestSuite { List(Paths.get("c/test")) ) - val projectPath = Paths.get(".") - val outputPath = Paths.get("/tmp") - val compilerDeps0 = ArtefactResolution.allCompilerDeps(build) - val (_, platformResolution, compilerResolution) = - ArtefactResolution.resolution( - seed.model.Config(), - Resolvers(), - build, - packageConfig, - optionalArtefacts = false, - Set(), - compilerDeps0, - Log.urgent - ) + val projectPath = Paths.get(".") + val outputPath = Paths.get("/tmp") + + val runtimeResolution = ArtefactResolution.runtimeResolution( + build, + seedConfig, + resolvers, + packageConfig, + false, + log + ) + val compilerResolution = ArtefactResolution.compilerResolution( + build, + seedConfig, + resolvers, + packageConfig, + false, + log + ) Idea.build( projectPath, outputPath, build, - platformResolution, + runtimeResolution, compilerResolution, false, Log.silent @@ -139,11 +157,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate project with correct module dependencies") { - val result = BuildConfig - .load(Paths.get("test").resolve("platform-module-deps"), Log.urgent) - .get - val outputPath = tempPath.resolve("platform-module-deps") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("platform-module-deps") + cli.Generate.ui( Config(), result.projectPath, @@ -173,11 +188,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate non-JVM cross-platform module") { - val result = BuildConfig - .load(Paths.get("test").resolve("shared-module"), Log.urgent) - .get - val outputPath = tempPath.resolve("shared-module") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("shared-module") + cli.Generate.ui( Config(), result.projectPath, @@ -273,10 +285,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate project with custom compiler options") { - val result = - BuildConfig.load(Paths.get("test/compiler-options"), Log.urgent).get - val outputPath = tempPath.resolve("compiler-options") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("compiler-options") + cli.Generate.ui( Config(), result.projectPath, @@ -338,11 +348,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate project with modules that have different Scala options") { - val result = BuildConfig - .load(Paths.get("test/module-scala-options"), Log.urgent) - .get - val outputPath = tempPath.resolve("module-scala-options-idea") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("module-scala-options") + cli.Generate.ui( Config(), result.projectPath, @@ -382,11 +389,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate project with different Scala versions") { - val result = BuildConfig - .load(Paths.get("test/multiple-scala-versions"), Log.urgent) - .get - val outputPath = tempPath.resolve("multiple-scala-versions-idea") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("multiple-scala-versions") + cli.Generate.ui( Config(), result.projectPath, @@ -478,10 +482,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate project with test project") { - val result = - BuildConfig.load(Paths.get("test/test-module"), Log.urgent).get - val outputPath = tempPath.resolve("test-module") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("test-module") + cli.Generate.ui( Config(), result.projectPath, @@ -511,10 +513,8 @@ object IdeaSpec extends SimpleTestSuite { } test("Generate Scala Native project") { - val result = - BuildConfig.load(Paths.get("test/scala-native-module"), Log.urgent).get - val outputPath = tempPath.resolve("scala-native-module") - Files.createDirectory(outputPath) + val (result, outputPath) = createProject("scala-native-module") + cli.Generate.ui( Config(), result.projectPath, diff --git a/src/test/scala/seed/generation/util/ProjectGeneration.scala b/src/test/scala/seed/generation/util/ProjectGeneration.scala index 6a30ae0..038c830 100644 --- a/src/test/scala/seed/generation/util/ProjectGeneration.scala +++ b/src/test/scala/seed/generation/util/ProjectGeneration.scala @@ -3,8 +3,9 @@ package seed.generation.util import java.nio.file.{Files, Path, Paths} import org.apache.commons.io.FileUtils +import seed.Cli.PackageConfig import seed.Log -import seed.artefact.{ArtefactResolution, Coursier} +import seed.artefact.ArtefactResolution import seed.config.BuildConfig import seed.config.BuildConfig.{Build, ModuleConfig} import seed.generation.Bloop @@ -12,6 +13,13 @@ import seed.model.Build.{JavaDep, Resolvers} import seed.model.{Build, Platform} object ProjectGeneration { + private val packageConfig = PackageConfig( + tmpfs = false, + silent = false, + ivyPath = None, + cachePath = None + ) + def generate(projectPath: Path, build: Build): Unit = { val bloopPath = projectPath.resolve(".bloop") val buildPath = projectPath.resolve("build") @@ -20,36 +28,23 @@ object ProjectGeneration { Set(bloopPath, buildPath, bloopBuildPath) .foreach(Files.createDirectories(_)) - val resolvedIvyPath = Coursier.DefaultIvyPath - val resolvedCachePath = Coursier.DefaultCachePath - - val compilerDeps = ArtefactResolution.allCompilerDeps(build) - val platformDeps = ArtefactResolution.allPlatformDeps(build) - val libraryDeps = ArtefactResolution.allLibraryDeps(build) - - val resolution = - Coursier.resolveAndDownload( - platformDeps ++ libraryDeps, - Resolvers(), - resolvedIvyPath, - resolvedCachePath, - optionalArtefacts = false, - silent = true, - Log.urgent - ) - val compilerResolution = - compilerDeps.map( - d => - Coursier.resolveAndDownload( - d, - Resolvers(), - resolvedIvyPath, - resolvedCachePath, - optionalArtefacts = false, - silent = true, - Log.urgent - ) - ) + val runtimeResolution = ArtefactResolution.runtimeResolution( + build, + seed.model.Config(), + Resolvers(), + packageConfig, + optionalArtefacts = false, + Log.urgent + ) + + val compilerResolution = ArtefactResolution.compilerResolution( + build, + seed.model.Config(), + Resolvers(), + packageConfig, + optionalArtefacts = false, + Log.urgent + ) build.foreach { case (id, module) => @@ -59,7 +54,7 @@ object ProjectGeneration { buildPath, bloopBuildPath, build, - resolution, + runtimeResolution, compilerResolution, id, module.module, diff --git a/test/compiler-options/jvm/src/Main.scala b/test/compiler-options/jvm/src/Main.scala new file mode 100644 index 0000000..84597b7 --- /dev/null +++ b/test/compiler-options/jvm/src/Main.scala @@ -0,0 +1,3 @@ +object Main { + val value: 42 = 42 +} diff --git a/test/resolve-dep-versions/build.toml b/test/resolve-dep-versions/build.toml new file mode 100644 index 0000000..2667ec6 --- /dev/null +++ b/test/resolve-dep-versions/build.toml @@ -0,0 +1,26 @@ +[project] +scalaVersion = "2.11.11-bin-typelevel-4" +scalaJsVersion = "0.6.28" +scalaOrganisation = "org.typelevel" + +[module.a.js] +sources = ["a"] +scalaDeps = [ + # Depends on scalajs-dom 0.9.7 + ["tech.sparse", "pine", "0.1.6"] +] + +[module.b.js] +sources = ["b"] +scalaDeps = [ + ["org.scala-js", "scalajs-dom", "0.9.8"] +] + +[module.example1.js] +sources = ["src"] +moduleDeps = ["a"] + +# Depends on scalajs-dom 0.9.8 +[module.example2.js] +sources = ["src"] +moduleDeps = ["a", "b"]