From 034834bd712066db31861518edae6e8894b59d8e Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:41:04 -0500 Subject: [PATCH] [2.x] feat: Support doc / skip := true (#8824) --- main/src/main/scala/sbt/Defaults.scala | 73 +----------------- main/src/main/scala/sbt/Keys.scala | 2 +- .../main/scala/sbt/internal/Compiler.scala | 76 +++++++++++++++++++ .../src/sbt-test/project/skip-doc/build.sbt | 5 ++ .../project/skip-doc/src/main/scala/A.scala | 2 + sbt-app/src/sbt-test/project/skip-doc/test | 4 + 6 files changed, 89 insertions(+), 73 deletions(-) create mode 100644 sbt-app/src/sbt-test/project/skip-doc/build.sbt create mode 100644 sbt-app/src/sbt-test/project/skip-doc/src/main/scala/A.scala create mode 100644 sbt-app/src/sbt-test/project/skip-doc/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index eaf771149..68444d261 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -117,7 +117,6 @@ import xsbti.compile.{ Compilers, DefinesClass, IncOptions, - IncToolOptionsUtil, Inputs, MiniSetup, PerClasspathEntryLookup, @@ -2044,77 +2043,7 @@ object Defaults extends BuildCommon { Seq("-project", project) } else Seq.empty }, - (TaskZero / key) := Def.uncached { - val s = streams.value - val cs: Compilers = compilers.value - val srcs = sources.value - val out = target.value - val sOpts = scalacOptions.value - val xapis = apiMappings.value - val hasScala = srcs.exists(_.name.endsWith(".scala")) - val hasJava = srcs.exists(_.name.endsWith(".java")) - val cp = data(dependencyClasspath.value).toList - val label = nameForSrc(configuration.value.name) - val reporter = (compile / bspReporter).value - val converter = fileConverter.value - val tFiles = tastyFiles.value - val sv = scalaVersion.value - val allDeps = allDependencies.value - (hasScala, hasJava) match { - case (true, _) => - val xapisFiles = xapis.map { (k, v) => - converter.toPath(k).toFile() -> v - } - val externalApiOpts = - if (ScalaArtifacts.isScala3(sv)) Opts.doc.externalAPIScala3(xapisFiles) - else Opts.doc.externalAPI(xapisFiles) - val options = sOpts ++ externalApiOpts - def convertVfRef(value: String): String = - if !value.contains("$") then value - else converter.toPath(VirtualFileRef.of(value)).toString - val resolvedOptions = options.map { x => - if !x.contains("$") then x - else x.split(":").map(_.split(",").map(convertVfRef).mkString(",")).mkString(":") - } - val scalac = cs.scalac match - case ac: AnalyzingCompiler => ac.onArgs(Compiler.exported(s, "scaladoc")) - val docSrcFiles = if ScalaArtifacts.isScala3(sv) then tFiles else srcs - // todo: cache this - if docSrcFiles.nonEmpty then - IO.delete(out) - IO.createDirectory(out) - // use PlainVirtualFile since Scaladoc currently doesn't handle actual VirtualFiles - scalac.doc( - docSrcFiles.map(_.toPath()).map(new sbt.internal.inc.PlainVirtualFile(_)), - cp.map(converter.toPath).map(new sbt.internal.inc.PlainVirtualFile(_)), - converter, - out.toPath(), - resolvedOptions, - maxErrors.value, - s.log, - ) - else () - case (_, true) => - import sbt.internal.inc.javac.JavaCompilerArguments - val javaSourcesOnly: VirtualFile => Boolean = _.id.endsWith(".java") - val classpath = cp.map(converter.toPath).map(converter.toVirtualFile) - val options = javacOptions.value.toList - cs.javaTools.javadoc.run( - srcs.toArray - .map { x => - converter.toVirtualFile(x.toPath) - } - .filter(javaSourcesOnly), - JavaCompilerArguments(Nil, classpath, options).toArray, - CompileOutput(out.toPath), - IncToolOptionsUtil.defaultIncToolOptions(), - reporter, - s.log, - ) - case _ => () // do nothing - } - out - } + (TaskZero / key) := Def.uncached(Compiler.docTask(key).value) ) ++ compilersSetting ) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 74f169a24..2f2046339 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -672,7 +672,7 @@ object Keys { val sbtDependency = settingKey[ModuleID]("Provides a definition for declaring the current version of sbt.").withRank(BMinusSetting) val sbtVersion = settingKey[String]("Provides the version of sbt. This setting should not be modified.").withRank(AMinusSetting) val sbtBinaryVersion = settingKey[String]("Defines the binary compatibility version substring.").withRank(BPlusSetting) - val skip = taskKey[Boolean]("For tasks that support it (currently only 'compile', 'update', 'publish' and 'publishLocal'), setting skip to true will force the task to not to do its work. The exact semantics may vary depending on the task.").withRank(BSetting) + val skip = taskKey[Boolean]("For tasks that support it (currently 'compile', 'update', 'publish', 'publishLocal', and 'doc'), setting skip to true will force the task to not to do its work. The exact semantics may vary depending on the task.").withRank(BSetting) val templateResolverInfos = settingKey[Seq[TemplateResolverInfo]]("Template resolvers used for 'new'.").withRank(BSetting) val templateDescriptions = settingKey[Seq[(String, String)]]("List of templates with description used for 'new' / 'init'.") val templateRunLocal = inputKey[Unit]("Runs a local template.").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/Compiler.scala b/main/src/main/scala/sbt/internal/Compiler.scala index 54d803432..fe9b61e55 100644 --- a/main/src/main/scala/sbt/internal/Compiler.scala +++ b/main/src/main/scala/sbt/internal/Compiler.scala @@ -417,6 +417,82 @@ object Compiler: private def jsonRpcRequest(id: Long, method: String, params: String): String = s"""{ "jsonrpc": "2.0", "method": "$method", "params": $params, "id": $id }""" + def docTask(key: TaskKey[File]): Def.Initialize[Task[File]] = + Def.taskIf { + if (key / Keys.skip).value then + val out = (key / Keys.target).value + Keys.streams.value.log.debug(s"Skipping doc for ${Keys.thisProjectRef.value.project}") + out + else + val s = Keys.streams.value + val cs: xsbti.compile.Compilers = Keys.compilers.value + val srcs = Keys.sources.value + val out = (key / Keys.target).value + val sOpts = Keys.scalacOptions.value + val xapis = Keys.apiMappings.value + val hasScala = srcs.exists(_.getName.endsWith(".scala")) + val hasJava = srcs.exists(_.getName.endsWith(".java")) + val cp = Attributed.data(Keys.dependencyClasspath.value).toList + val reporter = (Keys.compile / Keys.bspReporter).value + val converter = Keys.fileConverter.value + val tFiles = Keys.tastyFiles.value + val sv = Keys.scalaVersion.value + (hasScala, hasJava) match { + case (true, _) => + val xapisFiles = xapis.map { (k, v) => + converter.toPath(k).toFile() -> v + } + val externalApiOpts = + if (ScalaArtifacts.isScala3(sv)) Opts.doc.externalAPIScala3(xapisFiles) + else Opts.doc.externalAPI(xapisFiles) + val options = sOpts ++ externalApiOpts + def convertVfRef(value: String): String = + if !value.contains("$") then value + else converter.toPath(xsbti.VirtualFileRef.of(value)).toString + val resolvedOptions = options.map { x => + if !x.contains("$") then x + else x.split(":").map(_.split(",").map(convertVfRef).mkString(",")).mkString(":") + } + val scalac = cs.scalac match + case ac: AnalyzingCompiler => ac.onArgs(exported(s, "scaladoc")) + val docSrcFiles = if ScalaArtifacts.isScala3(sv) then tFiles else srcs + // todo: cache this + if docSrcFiles.nonEmpty then + IO.delete(out) + IO.createDirectory(out) + // use PlainVirtualFile since Scaladoc currently doesn't handle actual VirtualFiles + scalac.doc( + docSrcFiles.map(_.toPath()).map(new sbt.internal.inc.PlainVirtualFile(_)), + cp.map(converter.toPath).map(new sbt.internal.inc.PlainVirtualFile(_)), + converter, + out.toPath(), + resolvedOptions, + Keys.maxErrors.value, + s.log, + ) + else () + case (_, true) => + import sbt.internal.inc.javac.JavaCompilerArguments + val javaSourcesOnly: xsbti.VirtualFile => Boolean = _.id.endsWith(".java") + val classpath = cp.map(converter.toPath).map(converter.toVirtualFile) + val options = Keys.javacOptions.value.toList + cs.javaTools.javadoc.run( + srcs.toArray + .map { x => + converter.toVirtualFile(x.toPath) + } + .filter(javaSourcesOnly), + JavaCompilerArguments(Nil, classpath, options).toArray, + sbt.internal.inc.CompileOutput(out.toPath), + xsbti.compile.IncToolOptionsUtil.defaultIncToolOptions(), + reporter, + s.log, + ) + case _ => () // do nothing + } + out + } + def consoleForkOptions: Def.Initialize[Task[ForkOptions]] = Def.task { // Build environment variables for proper terminal handling val termEnv = sys.env.get("TERM").getOrElse("xterm-256color") diff --git a/sbt-app/src/sbt-test/project/skip-doc/build.sbt b/sbt-app/src/sbt-test/project/skip-doc/build.sbt new file mode 100644 index 000000000..629c66acf --- /dev/null +++ b/sbt-app/src/sbt-test/project/skip-doc/build.sbt @@ -0,0 +1,5 @@ +lazy val root = (project in file(".")) + .settings( + scalaVersion := "3.7.4", + doc / skip := true, + ) diff --git a/sbt-app/src/sbt-test/project/skip-doc/src/main/scala/A.scala b/sbt-app/src/sbt-test/project/skip-doc/src/main/scala/A.scala new file mode 100644 index 000000000..f484dffe0 --- /dev/null +++ b/sbt-app/src/sbt-test/project/skip-doc/src/main/scala/A.scala @@ -0,0 +1,2 @@ +object A: + def hello: String = "hello" diff --git a/sbt-app/src/sbt-test/project/skip-doc/test b/sbt-app/src/sbt-test/project/skip-doc/test new file mode 100644 index 000000000..a4d2073e9 --- /dev/null +++ b/sbt-app/src/sbt-test/project/skip-doc/test @@ -0,0 +1,4 @@ +# doc should succeed even with skip := true +> doc +# The target/scala-3.7.4/api directory should not contain generated docs +$ absent target/scala-3.7.4/api/index.html