Skip to content

Commit 2e337cb

Browse files
authored
Merge pull request scala#10472 from lrytz/sbt-bridge-2023
2 parents a7c3bc7 + 33c45fc commit 2e337cb

37 files changed

+5949
-18
lines changed

build.sbt

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ val jnaDep = "net.java.dev.jna" % "jna"
4545
val jlineDeps = Seq(jlineDep, jnaDep)
4646
val testInterfaceDep = "org.scala-sbt" % "test-interface" % "1.0"
4747
val diffUtilsDep = "io.github.java-diff-utils" % "java-diff-utils" % "4.12"
48+
val compilerInterfaceDep = "org.scala-sbt" % "compiler-interface" % "1.9.3"
4849

4950
val projectFolder = settingKey[String]("subfolder in src when using configureAsSubproject, else the project name")
5051

@@ -275,8 +276,26 @@ def fixPom(extra: (String, scala.xml.Node)*): Setting[_] = {
275276
) ++ extra) }
276277
}
277278

279+
def ivyDependencyFilter(deps: Seq[(String, String)], scalaBinaryVersion: String) = {
280+
import scala.xml._
281+
import scala.xml.transform._
282+
new RuleTransformer(new RewriteRule {
283+
override def transform(node: Node) = node match {
284+
case e: Elem if e.label == "dependency" && {
285+
val org = e.attribute("org").getOrElse("").toString
286+
val name = e.attribute("name").getOrElse("").toString
287+
deps.exists { case (g, a) =>
288+
org == g && (name == a || name == (a + "_" + scalaBinaryVersion))
289+
}
290+
} => Seq.empty
291+
case n => n
292+
}
293+
})
294+
}
295+
278296
val pomDependencyExclusions =
279297
settingKey[Seq[(String, String)]]("List of (groupId, artifactId) pairs to exclude from the POM and ivy.xml")
298+
lazy val fixCsrIvy = taskKey[Unit]("Apply pomDependencyExclusions to coursier ivy")
280299

281300
Global / pomDependencyExclusions := Nil
282301

@@ -294,27 +313,47 @@ lazy val removePomDependencies: Seq[Setting[_]] = Seq(
294313
e.child.contains(<groupId>{g}</groupId>) &&
295314
(e.child.contains(<artifactId>{a}</artifactId>) || e.child.contains(<artifactId>{a + "_" + scalaBinaryVersion.value}</artifactId>))
296315
} => Seq.empty
297-
case n => Seq(n)
316+
case n => n
298317
}
299318
}).transform(Seq(n2)).head
300319
},
320+
fixCsrIvy := {
321+
// - coursier makes target/sbt-bridge/resolution-cache/org.scala-lang/scala2-sbt-bridge/2.13.12-bin-SNAPSHOT/resolved.xml.xml
322+
// - copied to target/sbt-bridge//ivy-2.13.12-bin-SNAPSHOT.xml
323+
// - copied to ~/.ivy2/local/org.scala-lang/scala2-sbt-bridge/2.13.12-bin-SNAPSHOT/ivys/ivy.xml
324+
import scala.jdk.CollectionConverters._
325+
import scala.xml._
326+
val currentProject = csrProject.value
327+
val ivyModule = org.apache.ivy.core.module.id.ModuleRevisionId.newInstance(
328+
currentProject.module.organization.value,
329+
currentProject.module.name.value,
330+
currentProject.version,
331+
currentProject.module.attributes.asJava)
332+
val ivyFile = ivySbt.value.withIvy(streams.value.log)(_.getResolutionCacheManager).getResolvedIvyFileInCache(ivyModule)
333+
val e = ivyDependencyFilter(pomDependencyExclusions.value, scalaBinaryVersion.value)
334+
.transform(Seq(XML.loadFile(ivyFile))).head
335+
XML.save(ivyFile.getAbsolutePath, e, xmlDecl = true)
336+
},
337+
publishConfiguration := Def.taskDyn {
338+
val pc = publishConfiguration.value
339+
Def.task {
340+
fixCsrIvy.value
341+
pc
342+
}
343+
}.value,
344+
publishLocalConfiguration := Def.taskDyn {
345+
val pc = publishLocalConfiguration.value
346+
Def.task {
347+
fixCsrIvy.value
348+
pc
349+
}
350+
}.value,
301351
deliverLocal := {
352+
// this doesn't seem to do anything currently, it probably worked before sbt used coursier
302353
import scala.xml._
303-
import scala.xml.transform._
304354
val f = deliverLocal.value
305-
val deps = pomDependencyExclusions.value
306-
val e = new RuleTransformer(new RewriteRule {
307-
override def transform(node: Node) = node match {
308-
case e: Elem if e.label == "dependency" && {
309-
val org = e.attribute("org").getOrElse("").toString
310-
val name = e.attribute("name").getOrElse("").toString
311-
deps.exists { case (g, a) =>
312-
org == g && (name == a || name == (a + "_" + scalaBinaryVersion.value))
313-
}
314-
} => Seq.empty
315-
case n => Seq(n)
316-
}
317-
}).transform(Seq(XML.loadFile(f))).head
355+
val e = ivyDependencyFilter(pomDependencyExclusions.value, scalaBinaryVersion.value)
356+
.transform(Seq(XML.loadFile(f))).head
318357
XML.save(f.getAbsolutePath, e, xmlDecl = true)
319358
f
320359
}
@@ -579,6 +618,42 @@ lazy val scaladoc = configureAsSubproject(project)
579618
)
580619
.dependsOn(compiler)
581620

621+
// dependencies on compiler and compiler-interface are "provided" to align with scala3-sbt-bridge
622+
lazy val sbtBridge = configureAsSubproject(project, srcdir = Some("sbt-bridge"))
623+
.settings(Osgi.settings)
624+
.settings(AutomaticModuleName.settings("scala.sbtbridge"))
625+
//.settings(fatalWarningsSettings)
626+
.settings(
627+
name := "scala2-sbt-bridge",
628+
description := "sbt compiler bridge for Scala 2",
629+
libraryDependencies += compilerInterfaceDep % Provided,
630+
generateServiceProviderResources("xsbti.compile.CompilerInterface2" -> "scala.tools.xsbt.CompilerBridge"),
631+
generateServiceProviderResources("xsbti.compile.ConsoleInterface1" -> "scala.tools.xsbt.ConsoleBridge"),
632+
generateServiceProviderResources("xsbti.compile.ScaladocInterface2" -> "scala.tools.xsbt.ScaladocBridge"),
633+
generateServiceProviderResources("xsbti.InteractiveConsoleFactory" -> "scala.tools.xsbt.InteractiveConsoleBridgeFactory"),
634+
Compile / managedResourceDirectories := Seq((Compile / resourceManaged).value),
635+
pomDependencyExclusions ++= List((organization.value, "scala-repl-frontend"), (organization.value, "scala-compiler-doc")),
636+
fixPom(
637+
"/project/name" -> <name>Scala 2 sbt Bridge</name>,
638+
"/project/description" -> <description>sbt compiler bridge for Scala 2</description>,
639+
"/project/packaging" -> <packaging>jar</packaging>
640+
),
641+
headerLicense := Some(HeaderLicense.Custom(
642+
s"""Zinc - The incremental compiler for Scala.
643+
|Copyright Scala Center, Lightbend, and Mark Harrah
644+
|
645+
|Scala (${(ThisBuild/homepage).value.get})
646+
|Copyright EPFL and Lightbend, Inc.
647+
|
648+
|Licensed under Apache License 2.0
649+
|(http://www.apache.org/licenses/LICENSE-2.0).
650+
|
651+
|See the NOTICE file distributed with this work for
652+
|additional information regarding copyright ownership.
653+
|""".stripMargin)),
654+
)
655+
.dependsOn(compiler % Provided, replFrontend, scaladoc)
656+
582657
lazy val scalap = configureAsSubproject(project)
583658
.settings(fatalWarningsSettings)
584659
.settings(
@@ -727,7 +802,7 @@ val addOpensForTesting = "-XX:+IgnoreUnrecognizedVMOptions" +: "--add-exports=jd
727802
Seq("java.util.concurrent.atomic", "java.lang", "java.lang.reflect", "java.net").map(p => s"--add-opens=java.base/$p=ALL-UNNAMED")
728803

729804
lazy val junit = project.in(file("test") / "junit")
730-
.dependsOn(testkit, compiler, replFrontend, scaladoc)
805+
.dependsOn(testkit, compiler, replFrontend, scaladoc, sbtBridge)
731806
.settings(commonSettings)
732807
.settings(disableDocs)
733808
.settings(fatalWarningsSettings)
@@ -745,7 +820,7 @@ lazy val junit = project.in(file("test") / "junit")
745820
"-Ypatmat-exhaust-depth", "40", // despite not caring about patmat exhaustiveness, we still get warnings for this
746821
),
747822
Compile / javacOptions ++= Seq("-Xlint"),
748-
libraryDependencies ++= Seq(junitInterfaceDep, jolDep, diffUtilsDep),
823+
libraryDependencies ++= Seq(junitInterfaceDep, jolDep, diffUtilsDep, compilerInterfaceDep),
749824
testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s"),
750825
Compile / unmanagedSourceDirectories := Nil,
751826
Test / unmanagedSourceDirectories := List(baseDirectory.value),
@@ -1048,7 +1123,7 @@ lazy val root: Project = (project in file("."))
10481123

10491124
setIncOptions
10501125
)
1051-
.aggregate(library, reflect, compiler, interactive, repl, replFrontend,
1126+
.aggregate(library, reflect, compiler, interactive, repl, replFrontend, sbtBridge,
10521127
scaladoc, scalap, testkit, partest, junit, scalacheck, tasty, tastytest, scalaDist).settings(
10531128
Compile / sources := Seq.empty,
10541129
onLoadMessage := s"""|*** Welcome to the sbt build definition for Scala! ***
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* Zinc - The incremental compiler for Scala.
3+
* Copyright Scala Center, Lightbend, and Mark Harrah
4+
*
5+
* Scala (https://www.scala-lang.org)
6+
* Copyright EPFL and Lightbend, Inc.
7+
*
8+
* Licensed under Apache License 2.0
9+
* (http://www.apache.org/licenses/LICENSE-2.0).
10+
*
11+
* See the NOTICE file distributed with this work for
12+
* additional information regarding copyright ownership.
13+
*/
14+
15+
package scala.tools
16+
package xsbt
17+
18+
import scala.tools.nsc.Phase
19+
import scala.tools.nsc.symtab.Flags
20+
import xsbti.api._
21+
import xsbti.VirtualFile
22+
23+
object API {
24+
val name = "xsbt-api"
25+
}
26+
27+
final class API(val global: CallbackGlobal) extends Compat with GlobalHelpers with ClassName {
28+
import global._
29+
30+
import scala.collection.mutable
31+
private val nonLocalClassSymbolsInCurrentUnits = new mutable.HashSet[Symbol]()
32+
33+
def newPhase(prev: Phase) = new ApiPhase(prev)
34+
class ApiPhase(prev: Phase) extends GlobalPhase(prev) {
35+
override def description = "Extracts the public API from source files."
36+
def name = API.name
37+
override def run(): Unit = {
38+
val start = System.currentTimeMillis
39+
super.run()
40+
41+
// After processing all units, register generated classes
42+
registerGeneratedClasses(nonLocalClassSymbolsInCurrentUnits.iterator)
43+
nonLocalClassSymbolsInCurrentUnits.clear()
44+
45+
callback.apiPhaseCompleted()
46+
val stop = System.currentTimeMillis
47+
debuglog("API phase took : " + ((stop - start) / 1000.0) + " s")
48+
}
49+
50+
// TODO In 2.13, shouldSkipThisPhaseForJava should be overridden instead of cancelled
51+
// override def shouldSkipThisPhaseForJava = !global.callback.isPickleJava
52+
override def cancelled(unit: CompilationUnit) = {
53+
if (Thread.interrupted()) reporter.cancelled = true
54+
reporter.cancelled || unit.isJava && !global.callback.isPickleJava
55+
}
56+
57+
def apply(unit: global.CompilationUnit): Unit = processUnit(unit)
58+
59+
private def processUnit(unit: CompilationUnit): Unit = {
60+
if (!unit.isJava || global.callback.isPickleJava) {
61+
processScalaUnit(unit)
62+
}
63+
}
64+
65+
private def processScalaUnit(unit: CompilationUnit): Unit = {
66+
val sourceFile: VirtualFile = unit.source.file match { case AbstractZincFile(vf) => vf }
67+
debuglog("Traversing " + sourceFile)
68+
callback.startSource(sourceFile)
69+
val extractApi = new ExtractAPI[global.type](global, sourceFile)
70+
val traverser = new TopLevelHandler(extractApi)
71+
traverser.apply(unit.body)
72+
73+
val extractUsedNames = new ExtractUsedNames[global.type](global)
74+
extractUsedNames.extractAndReport(unit)
75+
76+
val classApis = traverser.allNonLocalClasses
77+
val mainClasses = traverser.mainClasses
78+
79+
// Use of iterators make this code easier to profile
80+
81+
val classApisIt = classApis.iterator
82+
while (classApisIt.hasNext) {
83+
callback.api(sourceFile, classApisIt.next())
84+
}
85+
86+
val mainClassesIt = mainClasses.iterator
87+
while (mainClassesIt.hasNext) {
88+
callback.mainClass(sourceFile, mainClassesIt.next())
89+
}
90+
91+
extractApi.allExtractedNonLocalSymbols.foreach { cs =>
92+
// Only add the class symbols defined in this compilation unit
93+
if (cs.sourceFile != null) nonLocalClassSymbolsInCurrentUnits.+=(cs)
94+
}
95+
}
96+
}
97+
98+
private case class FlattenedNames(binaryName: String, className: String)
99+
100+
/**
101+
* Registers only non-local generated classes in the callback by extracting
102+
* information about its names and using the names to generate class file paths.
103+
*
104+
* Mimics the previous logic that was present in `Analyzer`, despite the fact
105+
* that now we construct the names that the compiler will give to every non-local
106+
* class independently of genbcode.
107+
*
108+
* Why do we do this? The motivation is that we want to run the incremental algorithm
109+
* independently of the compiler pipeline. This independence enables us to:
110+
*
111+
* 1. Offload the incremental compiler logic out of the primary pipeline and
112+
* run the incremental phases concurrently.
113+
* 2. Know before the compilation is completed whether another compilation will or
114+
* will not be required. This is important to make incremental compilation work
115+
* with pipelining and enables further optimizations; for example, we can start
116+
* subsequent incremental compilations before (!) the initial compilation is done.
117+
* This can buy us ~30-40% faster incremental compiler iterations.
118+
*
119+
* This method only takes care of non-local classes because local classes have no
120+
* relevance in the correctness of the algorithm and can be registered after genbcode.
121+
* Local classes are only used to construct the relations of products and to produce
122+
* the list of generated files + stamps, but names referring to local classes **never**
123+
* show up in the name hashes of classes' APIs, hence never considered for name hashing.
124+
*
125+
* As local class files are owned by other classes that change whenever they change,
126+
* we could most likely live without adding their class files to the products relation
127+
* and registering their stamps. However, to be on the safe side, we will continue to
128+
* register the local products in `Analyzer`.
129+
*
130+
* @param allClassSymbols The class symbols found in all the compilation units.
131+
*/
132+
def registerGeneratedClasses(classSymbols: Iterator[Symbol]): Unit = {
133+
classSymbols.foreach { symbol =>
134+
val sourceFile = symbol.sourceFile
135+
val sourceVF0 =
136+
if (sourceFile == null) symbol.enclosingTopLevelClass.sourceFile
137+
else sourceFile
138+
val sourceVF: Option[VirtualFile] = sourceVF0 match {
139+
case AbstractZincFile(vf) => Some(vf)
140+
// This could be scala.reflect.io.FileZipArchive$LeakyEntry
141+
case _ => None
142+
}
143+
144+
def registerProductNames(names: FlattenedNames): Unit = {
145+
// Guard against a local class in case it surreptitiously leaks here
146+
if (!symbol.isLocalClass) {
147+
val pathToClassFile = s"${names.binaryName}.class"
148+
val classFile = {
149+
JarUtils.outputJar match {
150+
case Some(outputJar) =>
151+
new java.io.File(JarUtils.classNameInJar(outputJar, pathToClassFile))
152+
case None =>
153+
val outputDir = global.settings.outputDirs.outputDirFor(sourceFile).file
154+
new java.io.File(outputDir, pathToClassFile)
155+
}
156+
}
157+
val zincClassName = names.className
158+
val srcClassName = classNameAsString(symbol)
159+
sourceVF foreach { source =>
160+
callback.generatedNonLocalClass(
161+
source,
162+
classFile.toPath,
163+
zincClassName,
164+
srcClassName
165+
)
166+
}
167+
} else ()
168+
}
169+
170+
val names = FlattenedNames(
171+
fullName(symbol, java.io.File.separatorChar, symbol.moduleSuffix, true),
172+
fullName(symbol, '.', symbol.moduleSuffix, false)
173+
)
174+
175+
registerProductNames(names)
176+
177+
// Register the names of top-level module symbols that emit two class files
178+
val isTopLevelUniqueModule =
179+
symbol.owner.isPackageClass && symbol.isModuleClass && symbol.companionClass == NoSymbol
180+
if (isTopLevelUniqueModule || symbol.isPackageObject) {
181+
val names = FlattenedNames(
182+
fullName(symbol, java.io.File.separatorChar, "", true),
183+
fullName(symbol, '.', "", false)
184+
)
185+
registerProductNames(names)
186+
}
187+
}
188+
}
189+
190+
private final class TopLevelHandler(extractApi: ExtractAPI[global.type])
191+
extends TopLevelTraverser {
192+
def allNonLocalClasses: Set[ClassLike] = {
193+
extractApi.allExtractedNonLocalClasses
194+
}
195+
196+
def mainClasses: Set[String] = extractApi.mainClasses
197+
198+
def `class`(c: Symbol): Unit = {
199+
extractApi.extractAllClassesOf(c.owner, c)
200+
}
201+
}
202+
203+
private abstract class TopLevelTraverser extends Traverser {
204+
def `class`(s: Symbol): Unit
205+
override def traverse(tree: Tree): Unit = {
206+
tree match {
207+
case (_: ClassDef | _: ModuleDef) if isTopLevel(tree.symbol) => `class`(tree.symbol)
208+
case _: PackageDef =>
209+
super.traverse(tree)
210+
case _ =>
211+
}
212+
}
213+
def isTopLevel(sym: Symbol): Boolean = {
214+
!ignoredSymbol(sym) &&
215+
sym.isStatic &&
216+
!sym.isImplClass &&
217+
(!sym.hasFlag(Flags.JAVA) || global.callback.isPickleJava) &&
218+
!sym.isNestedClass
219+
}
220+
}
221+
222+
}

0 commit comments

Comments
 (0)