mirror of https://github.com/sbt/sbt.git
Option to automatically manage API documentation mappings
Set autoAPIMappings := true to enable. Then, set apiURL to the base URL of the API documentation for a project. This will get stored in an extra attribute in the ivy.xml or as a property a pom.xml. When using managed dependencies that have set their apiURL, the -doc-external-doc setting for scaladoc will be automatically configured. Note that this option will only be available in Scala 2.10.1 and so enabling autoAPIMappings for earlier versions will result in an error from scaladoc. For unmanaged dependencies or dependencies without an automatic apiURL, add the (File,URL) mapping to apiMappings. The File is the classpath entry and the URL is the location of the API documentation.
This commit is contained in:
parent
e5a9d31d6f
commit
a152965933
|
|
@ -30,9 +30,14 @@ final class CustomPomParser(delegate: ModuleDescriptorParser, transform: (Module
|
|||
}
|
||||
object CustomPomParser
|
||||
{
|
||||
/** The key prefix that indicates that this is used only to store extra information and is not intended for dependency resolution.*/
|
||||
val InfoKeyPrefix = "info."
|
||||
val ApiURLKey = "info.apiURL"
|
||||
|
||||
val SbtVersionKey = "sbtVersion"
|
||||
val ScalaVersionKey = "scalaVersion"
|
||||
val ExtraAttributesKey = "extraDependencyAttributes"
|
||||
private[this] val unqualifiedKeys = Set(SbtVersionKey, ScalaVersionKey, ExtraAttributesKey, ApiURLKey)
|
||||
|
||||
// packagings that should be jars, but that Ivy doesn't handle as jars
|
||||
val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit")
|
||||
|
|
@ -75,8 +80,7 @@ object CustomPomParser
|
|||
}
|
||||
private[this] def artifactExtIncorrect(md: ModuleDescriptor): Boolean =
|
||||
md.getConfigurations.exists(conf => md.getArtifacts(conf.getName).exists(art => JarPackagings(art.getExt)))
|
||||
private[this] def shouldBeUnqualified(m: Map[String, String]): Map[String, String] =
|
||||
m.filter { case (SbtVersionKey | ScalaVersionKey | ExtraAttributesKey,_) => true; case _ => false }
|
||||
private[this] def shouldBeUnqualified(m: Map[String, String]): Map[String, String] = m.filterKeys(unqualifiedKeys)
|
||||
|
||||
private[this] def condAddExtra(properties: Map[String, String], id: ModuleRevisionId): ModuleRevisionId =
|
||||
if(properties.isEmpty) id else addExtra(properties, id)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ object IvyActions
|
|||
|
||||
def processUnresolved(err: ResolveException, log: Logger)
|
||||
{
|
||||
val withExtra = err.failed.filter(!_.extraAttributes.isEmpty)
|
||||
val withExtra = err.failed.filter(!_.extraDependencyAttributes.isEmpty)
|
||||
if(!withExtra.isEmpty)
|
||||
{
|
||||
log.warn("\n\tNote: Some unresolved dependencies have extra attributes. Check that these dependencies exist with the requested attributes.")
|
||||
|
|
|
|||
|
|
@ -11,11 +11,16 @@ import org.apache.ivy.util.url.CredentialsStore
|
|||
|
||||
final case class ModuleID(organization: String, name: String, revision: String, configurations: Option[String] = None, isChanging: Boolean = false, isTransitive: Boolean = true, isForce: Boolean = false, explicitArtifacts: Seq[Artifact] = Nil, exclusions: Seq[ExclusionRule] = Nil, extraAttributes: Map[String,String] = Map.empty, crossVersion: CrossVersion = CrossVersion.Disabled)
|
||||
{
|
||||
override def toString =
|
||||
override def toString: String =
|
||||
organization + ":" + name + ":" + revision +
|
||||
(configurations match { case Some(s) => ":" + s; case None => "" }) +
|
||||
(if(extraAttributes.isEmpty) "" else " " + extraString)
|
||||
def extraString = extraAttributes.map { case (k,v) => k + "=" + v } mkString("(",", ",")")
|
||||
|
||||
/** String representation of the extra attributes, excluding any information only attributes. */
|
||||
def extraString: String = extraDependencyAttributes.map { case (k,v) => k + "=" + v } mkString("(",", ",")")
|
||||
|
||||
/** Returns the extra attributes except for ones marked as information only (ones that typically would not be used for dependency resolution). */
|
||||
def extraDependencyAttributes: Map[String,String] = extraAttributes.filterKeys(!_.startsWith(CustomPomParser.InfoKeyPrefix))
|
||||
|
||||
@deprecated("Use `cross(CrossVersion)`, the variant accepting a CrossVersion value constructed by a member of the CrossVersion object instead.", "0.12.0")
|
||||
def cross(v: Boolean): ModuleID = cross(if(v) CrossVersion.binary else CrossVersion.Disabled)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.{MalformedURLException,URL}
|
||||
|
||||
private[sbt] object APIMappings
|
||||
{
|
||||
def extract(cp: Seq[Attributed[File]], log: Logger): Seq[(File,URL)] =
|
||||
cp.flatMap(entry => extractFromEntry(entry, log))
|
||||
|
||||
def extractFromEntry(entry: Attributed[File], log: Logger): Option[(File,URL)] =
|
||||
entry.get(Keys.entryApiURL) match {
|
||||
case Some(u) => Some( (entry.data, u) )
|
||||
case None => entry.get(Keys.moduleID.key).flatMap { mid => extractFromID(entry.data, mid, log) }
|
||||
}
|
||||
|
||||
private[this] def extractFromID(entry: File, mid: ModuleID, log: Logger): Option[(File,URL)] =
|
||||
for {
|
||||
urlString <- mid.extraAttributes.get(CustomPomParser.ApiURLKey)
|
||||
u <- parseURL(urlString, entry, log)
|
||||
} yield (entry, u)
|
||||
|
||||
private[this] def parseURL(s: String, forEntry: File, log: Logger): Option[URL] =
|
||||
try Some(new URL(s)) catch { case e: MalformedURLException =>
|
||||
log.warn("Invalid API base URL '$s' for classpath entry '$forEntry': ${e.toString}")
|
||||
None
|
||||
}
|
||||
|
||||
def store[T](attr: Attributed[T], entryAPI: Option[URL]): Attributed[T] = entryAPI match {
|
||||
case None => attr
|
||||
case Some(u) => attr.put(Keys.entryApiURL, u)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ package sbt
|
|||
import org.apache.ivy.core.module.{descriptor, id}
|
||||
import descriptor.ModuleDescriptor, id.ModuleRevisionId
|
||||
import java.io.File
|
||||
import java.net.{URI,URL}
|
||||
import java.net.{URI,URL,MalformedURLException}
|
||||
import java.util.concurrent.Callable
|
||||
import sbinary.DefaultProtocol.StringFormat
|
||||
import Cache.seqFormat
|
||||
|
|
@ -71,6 +71,8 @@ object Defaults extends BuildCommon
|
|||
connectInput :== false,
|
||||
cancelable :== false,
|
||||
sourcesInBase :== true,
|
||||
autoAPIMappings := false,
|
||||
apiMappings := Map.empty,
|
||||
autoScalaLibrary :== true,
|
||||
managedScalaInstance :== true,
|
||||
onLoad <<= onLoad ?? idFun[State],
|
||||
|
|
@ -90,6 +92,7 @@ object Defaults extends BuildCommon
|
|||
initialize :== {},
|
||||
credentials :== Nil,
|
||||
scalaHome :== None,
|
||||
apiURL := None,
|
||||
javaHome :== None,
|
||||
extraLoggers :== { _ => Nil },
|
||||
skip :== false,
|
||||
|
|
@ -599,7 +602,8 @@ object Defaults extends BuildCommon
|
|||
def docTaskSettings(key: TaskKey[File] = doc): Seq[Setting[_]] = inTask(key)(compileInputsSettings ++ Seq(
|
||||
target := docDirectory.value, // deprecate docDirectory in favor of 'target in doc'; remove when docDirectory is removed
|
||||
scalacOptions <<= scaladocOptions or scalacOptions, // deprecate scaladocOptions in favor of 'scalacOptions in doc'; remove when scaladocOptions is removed
|
||||
key in TaskGlobal <<= (compileInputs, target, configuration, streams) map { (in, out, config, s) =>
|
||||
apiMappings ++= { if(autoAPIMappings.value) APIMappings.extract(dependencyClasspath.value, streams.value.log).toMap else Map.empty[File,URL] },
|
||||
key in TaskGlobal <<= (compileInputs, target, configuration, apiMappings, streams) map { (in, out, config, xapis, s) =>
|
||||
val srcs = in.config.sources
|
||||
val hasScala = srcs.exists(_.name.endsWith(".scala"))
|
||||
val hasJava = srcs.exists(_.name.endsWith(".java"))
|
||||
|
|
@ -607,7 +611,7 @@ object Defaults extends BuildCommon
|
|||
val label = nameForSrc(config.name)
|
||||
val (options, runDoc) =
|
||||
if(hasScala)
|
||||
(in.config.options, Doc.scaladoc(label, s.cacheDirectory / "scala", in.compilers.scalac))
|
||||
(in.config.options ++ Opts.doc.externalAPI(xapis), Doc.scaladoc(label, s.cacheDirectory / "scala", in.compilers.scalac))
|
||||
else if(hasJava)
|
||||
(in.config.javacOptions, Doc.javadoc(label, s.cacheDirectory / "java", in.compilers.javac))
|
||||
else
|
||||
|
|
@ -889,7 +893,7 @@ object Classpaths
|
|||
artifactPath in makePom <<= artifactPathSetting(artifact in makePom),
|
||||
publishArtifact in makePom := publishMavenStyle.value && publishArtifact.value,
|
||||
artifact in makePom := Artifact.pom(moduleName.value),
|
||||
projectID := ModuleID(organization.value, moduleName.value, version.value).cross(crossVersion in projectID value).artifacts(artifacts.value : _*),
|
||||
projectID <<= defaultProjectID,
|
||||
projectID <<= pluginProjectID,
|
||||
resolvers in GlobalScope :== Nil,
|
||||
projectDescriptors <<= depMap,
|
||||
|
|
@ -938,6 +942,15 @@ object Classpaths
|
|||
log.warn("Multiple resolvers having different access mechanism configured with same name '" + name + "'. To avoid conflict, Remove duplicate project resolvers (`resolvers`) or rename publishing resolver (`publishTo`).")
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] def defaultProjectID: Initialize[ModuleID] = Def.setting {
|
||||
val base = ModuleID(organization.value, moduleName.value, version.value).cross(crossVersion in projectID value).artifacts(artifacts.value : _*)
|
||||
apiURL.value match {
|
||||
case Some(u) if autoAPIMappings.value => base.extra(CustomPomParser.ApiURLKey -> u.toExternalForm)
|
||||
case _ => base
|
||||
}
|
||||
}
|
||||
|
||||
def pluginProjectID: Initialize[ModuleID] = (sbtBinaryVersion in update, scalaBinaryVersion in update, projectID, sbtPlugin) {
|
||||
(sbtBV, scalaBV, pid, isPlugin) =>
|
||||
if(isPlugin) sbtPluginExtra(pid, sbtBV, scalaBV) else pid
|
||||
|
|
@ -1088,8 +1101,8 @@ object Classpaths
|
|||
def makeProducts: Initialize[Task[Seq[File]]] =
|
||||
(compile, compileInputs, copyResources) map { (_, i, _) => i.config.classesDirectory :: Nil }
|
||||
def exportProductsTask: Initialize[Task[Classpath]] =
|
||||
(products.task, packageBin.task, exportJars, compile) flatMap { (psTask, pkgTask, useJars, analysis) =>
|
||||
(if(useJars) Seq(pkgTask).join else psTask) map { _ map { f => analyzed(f, analysis) } }
|
||||
(products.task, packageBin.task, exportJars, compile, apiURL) flatMap { (psTask, pkgTask, useJars, analysis, u) =>
|
||||
(if(useJars) Seq(pkgTask).join else psTask) map { _ map { f => APIMappings.store(analyzed(f, analysis), u) } }
|
||||
}
|
||||
|
||||
def constructBuildDependencies: Initialize[BuildDependencies] =
|
||||
|
|
|
|||
|
|
@ -218,6 +218,10 @@ object Keys
|
|||
val organization = SettingKey[String]("organization", "Organization/group ID.", APlusSetting)
|
||||
val organizationName = SettingKey[String]("organization-name", "Organization full/formal name.", BMinusSetting)
|
||||
val organizationHomepage = SettingKey[Option[URL]]("organization-homepage", "Organization homepage.", BMinusSetting)
|
||||
val apiURL = SettingKey[Option[URL]]("api-url", "Base URL for API documentation.", BMinusSetting)
|
||||
val entryApiURL = AttributeKey[URL]("entry-api-url", "Base URL for the API documentation for a classpath entry.")
|
||||
val apiMappings = TaskKey[Map[File,URL]]("api-mappings", "Mappings from classpath entry to API documentation base URL.", BMinusSetting)
|
||||
val autoAPIMappings = SettingKey[Boolean]("auto-api-mappings", "If true, automatically manages mappings to the API doc URL.", BMinusSetting)
|
||||
val scmInfo = SettingKey[Option[ScmInfo]]("scm-info", "Basic SCM information for the project.", BMinusSetting)
|
||||
val projectInfo = SettingKey[ModuleInfo]("project-info", "Addition project information like formal name, homepage, licenses etc.", CSetting)
|
||||
val defaultConfiguration = SettingKey[Option[Configuration]]("default-configuration", "Defines the configuration used when none is specified for a dependency.", CSetting)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
*/
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
/** Options for well-known tasks. */
|
||||
object Opts {
|
||||
object compile {
|
||||
|
|
@ -15,10 +18,12 @@ object Opts {
|
|||
val verbose = "-verbose"
|
||||
}
|
||||
object doc {
|
||||
def generator(g: String) = Seq("-doc-generator", g)
|
||||
def sourceUrl(u: String) = Seq("-doc-source-url", u)
|
||||
def title(t: String) = Seq("-doc-title", t)
|
||||
def version(v: String) = Seq("-doc-version", v)
|
||||
def generator(g: String): Seq[String] = Seq("-doc-generator", g)
|
||||
def sourceUrl(u: String): Seq[String] = Seq("-doc-source-url", u)
|
||||
def title(t: String): Seq[String] = Seq("-doc-title", t)
|
||||
def version(v: String): Seq[String] = Seq("-doc-version", v)
|
||||
def externalAPI(mappings: Iterable[(File,URL)]): Seq[String] = if(mappings.isEmpty) Nil else
|
||||
mappings.map{ case (f,u) => s"${f.getAbsolutePath}#${u.toExternalForm}"}.mkString("-doc-external-doc:", ",", "") :: Nil
|
||||
}
|
||||
object resolver {
|
||||
import Path._
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
Seq(
|
||||
autoAPIMappings in ThisBuild := true,
|
||||
publishArtifact in (ThisBuild, packageDoc) := false,
|
||||
publishArtifact in packageSrc := false,
|
||||
organization in ThisBuild := "org.example",
|
||||
version := "1.0"
|
||||
)
|
||||
|
||||
val aPublishResolver = Def.setting {
|
||||
Resolver.file("a-resolver", baseDirectory.in(ThisBuild).value / "a-repo")
|
||||
}
|
||||
val aResolver = Def.setting {
|
||||
val dir = baseDirectory.in(ThisBuild).value
|
||||
"a-resolver" at s"file://${dir.getAbsolutePath}/a-repo"
|
||||
}
|
||||
|
||||
val bResolver = Def.setting {
|
||||
val dir = baseDirectory.in(ThisBuild).value / "b-repo"
|
||||
Resolver.file("b-resolver", dir)(Resolver.defaultIvyPatterns)
|
||||
}
|
||||
|
||||
val apiBaseSetting = apiURL := Some(apiBase(name.value))
|
||||
def apiBase(projectName: String) = url(s"http://example.org/${projectName}")
|
||||
def addDep(projectName: String) =
|
||||
libraryDependencies += organization.value %% projectName % version.value
|
||||
|
||||
|
||||
val checkApiMappings = taskKey[Unit]("Verifies that the API mappings are collected as expected.")
|
||||
|
||||
def expectedMappings = Def.task {
|
||||
val ms = update.value.configuration(Compile.name).get.modules.flatMap { mod =>
|
||||
mod.artifacts.flatMap { case (a,f) =>
|
||||
val n = a.name.stripSuffix("_" + scalaBinaryVersion.value)
|
||||
n match {
|
||||
case "a" | "b" | "c" => (f, apiBase(n)) :: Nil
|
||||
case _ => Nil
|
||||
}
|
||||
}
|
||||
}
|
||||
val mc = (classDirectory in (c,Compile)).value -> apiBase("c")
|
||||
(mc +: ms).toMap
|
||||
}
|
||||
|
||||
|
||||
val a = project.settings(
|
||||
apiBaseSetting,
|
||||
publishMavenStyle := true,
|
||||
publishTo := Some(aPublishResolver.value)
|
||||
)
|
||||
|
||||
val b = project.settings(
|
||||
apiBaseSetting,
|
||||
publishMavenStyle := false,
|
||||
publishTo := Some(bResolver.value)
|
||||
)
|
||||
|
||||
val c = project.settings(apiBaseSetting)
|
||||
|
||||
val d = project.dependsOn( c ).settings(
|
||||
externalResolvers := Seq(aResolver.value, bResolver.value),
|
||||
addDep("a"),
|
||||
addDep("b"),
|
||||
checkApiMappings := {
|
||||
val actual = apiMappings.in(Compile,doc).value
|
||||
println("Actual API Mappings: " + actual.mkString("\n\t", "\n\t", ""))
|
||||
val expected = expectedMappings.value
|
||||
println("Expected API Mappings: " + expected.mkString("\n\t", "\n\t", ""))
|
||||
assert(actual == expected)
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Project A publishes as a pom to test that the API URL is properly handled from poms
|
||||
> a/publish
|
||||
|
||||
# Project B publishes as ivy.xml to test that format
|
||||
> b/publish
|
||||
|
||||
# Project C is an internal dependency to test that unpublished internal dependencies
|
||||
# have their URL properly set
|
||||
# Project D uses all three projects as well as an unmanaged dependency
|
||||
# Here, verify that the API mappings have been properly gathered
|
||||
> d/checkApiMappings
|
||||
Loading…
Reference in New Issue