Skip to content

Commit f2d0f1e

Browse files
committed
Use sbt for PR validation
- Support directories in `-doc-external-doc`: It is documented as accepting a “classpath_entry_path” for the keys but this only worked for JARs and not for individual class files. When checking for external-doc mappings for a Symbol, we now find the root directory relative to a class file instead of using the full class file path. The corresponding tests for SI-191 and SI8557 are also fixed to support individual class files instead of JARs in partest. This is required for the sbt build which runs partest on “quick” instead of “pack”. - Fix version and repository handling for bootstrapping. The bootstrap `scalaInstance` can now be resolved from any repository added to the project (not just the bootstrap repositories) by using a different workaround for sbt/sbt#1872. - Workaround for sbt/sbt#2640 (putting the wrong `scalaInstance` on partest’s classpath). The required `ScalaInstance` constructor is deprecated, so we have to disable deprecation warnings and fatal warnings until there is a better fix. - Add MiMa to the sbt build (port of the old `test.bc` ant task). The sbt-mima plugin doesn’t have all the features we need, so we do it manually in a similar way to what the plugin does. Checks are done in both directions for the `library` and `compiler` projects. The base version has to be set in `build.sbt`. When set to `None`, MiMa checks are skipped. MiMa checks are run sequentially to avoid spurious errors (see lightbend-labs/mima#115). - Port the OSGi tests to the sbt build. The set of JARs that gets copied into build/osgi as bundles is a bit different from the ant build. We omit the source JARs but add additional modules that are part of the Scala distribution, which seems more correct. - Get rid up `pull-binary-libs.sh` for the sbt build. Add artifacts are resolved from the special bootstrap repository through Ivy. The special `code.jar` and `instrumented.jar` artifacts are copied to the location where partest expects them (because these paths are hardcoded in partest). Other extra JARs for partest in `test/files/lib` are referenced directly from the Ivy cache. - Move common settings that should be available with unqualified names in local `.sbt` files and on the command line into an auto-plugin. - Add an `antStyle` setting to sbt to allow users to easily enable ant-style incremental compilation instead of sbt’s standard name hashing with `set antStyle := true`. - Disable verbose `info`-level logging during sbt startup for both, `validate/test` and `validate/publish-core` jobs. Update logging is no longer disabled when running locally (where it is useful and doesn’t generate excessive output). - Pass optimization flags for scalac down to partest, using the new partest version 1.0.15\6. - Call the new sbt-based PR validation from `scripts/jobs/validate/test`. - Disable the tests `run/t7843-jsr223-service` and `run/t7933` from scala#4959 for now. We need to set up a new test project (either partest or junit) that can run them on a packaged version of Scala, or possibly move them into a separate project that would naturally run from a packaged Scala as part of the community build.
1 parent 91b6944 commit f2d0f1e

21 files changed

+382
-92
lines changed

build.sbt

Lines changed: 132 additions & 55 deletions
Large diffs are not rendered by default.

project/BuildSettings.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
3+
/** This object defines keys that should be visible with an unqualified name in all .sbt files and the command line */
4+
object BuildSettings extends AutoPlugin {
5+
object autoImport {
6+
lazy val antStyle = settingKey[Boolean]("Use ant-style incremental builds instead of name-hashing")
7+
lazy val baseVersion = settingKey[String]("The base version number from which all others are derived")
8+
lazy val baseVersionSuffix = settingKey[String]("Identifies the kind of version to build")
9+
lazy val mimaReferenceVersion = settingKey[Option[String]]("Scala version number to run MiMa against")
10+
}
11+
}

project/MiMa.scala

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// It would be nice to use sbt-mima-plugin here, but the plugin is missing
2+
// at least two features we need:
3+
// * ability to run MiMa twice, swapping `curr` and `prev`, to detect
4+
// both forwards and backwards incompatibilities (possibly fixed as of
5+
// https://github.com/typesafehub/migration-manager/commit/2844ffa48b6d2255aa64bd687703aec21dadd55e)
6+
// * ability to pass a filter file (https://github.com/typesafehub/migration-manager/issues/102)
7+
// So we invoke the MiMa CLI directly; it's also what the Ant build did.
8+
9+
import sbt._
10+
import sbt.Keys._
11+
import BuildSettings.autoImport._
12+
13+
object MiMa {
14+
lazy val mima =
15+
taskKey[Unit]("run Migration Manager to detect binary incompatibilities")
16+
17+
lazy val settings =
18+
Seq(
19+
mima := {
20+
val log = streams.value.log
21+
mimaReferenceVersion.value.fold {
22+
log.info(s"No reference version defined - skipping binary compatibility checks")
23+
} { refVersion =>
24+
def runOnce(prev: java.io.File, curr: java.io.File, isForward: Boolean): Unit = {
25+
val direction = if (isForward) "forward" else "backward"
26+
log.info(s"Checking $direction binary compatibility")
27+
log.debug(s"prev = $prev, curr = $curr")
28+
runMima(
29+
prev = if (isForward) curr else prev,
30+
curr = if (isForward) prev else curr,
31+
// TODO: it would be nicer if each subproject had its own whitelist, but for now
32+
// for compatibility with how Ant did things, there's just one at the root.
33+
// once Ant is gone we'd be free to split it up.
34+
filter = (baseDirectory in ThisBuild).value / s"bincompat-$direction.whitelist.conf",
35+
log)
36+
}
37+
val artifact =
38+
getPreviousArtifact(
39+
"org.scala-lang" % s"${name.value}" % refVersion,
40+
ivySbt.value, streams.value)
41+
for (isForward <- Seq(false, true))
42+
runOnce(artifact, (packageBin in Compile).value, isForward)
43+
}
44+
}
45+
)
46+
47+
def runMima(prev: java.io.File, curr: java.io.File, filter: java.io.File, log: Logger): Unit = {
48+
val args = Array(
49+
"--prev", prev.getAbsolutePath,
50+
"--curr", curr.getAbsolutePath,
51+
"--filters", filter.getAbsolutePath,
52+
"--generate-filters"
53+
)
54+
val exitCode = TrapExit(com.typesafe.tools.mima.cli.Main.main(args), log)
55+
if (exitCode != 0)
56+
throw new RuntimeException(s"MiMa failed with exit code $exitCode")
57+
}
58+
59+
// cribbed from https://github.com/typesafehub/migration-manager/blob/master/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala
60+
def getPreviousArtifact(m: ModuleID, ivy: IvySbt, s: TaskStreams): File = {
61+
val moduleSettings = InlineConfiguration(
62+
"dummy" % "test" % "version",
63+
ModuleInfo("dummy-test-project-for-resolving"),
64+
dependencies = Seq(m))
65+
val module = new ivy.Module(moduleSettings)
66+
val report = Deprecated.Inner.ivyUpdate(ivy)(module, s)
67+
val optFile = (for {
68+
config <- report.configurations
69+
module <- config.modules
70+
(artifact, file) <- module.artifacts
71+
// TODO - Hardcode this?
72+
if artifact.name == m.name
73+
} yield file).headOption
74+
optFile getOrElse sys.error("Could not resolve previous artifact: " + m)
75+
}
76+
77+
}
78+
79+
// use the SI-7934 workaround to silence a deprecation warning on an sbt API
80+
// we have no choice but to call. on the lack of any suitable alternative,
81+
// see https://gitter.im/sbt/sbt-dev?at=5616e2681b0e279854bd74a4 :
82+
// "it's my intention to eventually come up with a public API" says Eugene Y
83+
object Deprecated {
84+
@deprecated("", "") class Inner {
85+
def ivyUpdate(ivy: IvySbt)(module: ivy.Module, s: TaskStreams) =
86+
IvyActions.update(
87+
module,
88+
new UpdateConfiguration(
89+
retrieve = None,
90+
missingOk = false,
91+
logging = UpdateLogging.DownloadOnly),
92+
s.log)
93+
}
94+
object Inner extends Inner
95+
}

project/Osgi.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ object Osgi {
2828
"Bundle-Name" -> bundleName.value,
2929
"Bundle-SymbolicName" -> bundleSymbolicName.value,
3030
"ver" -> v,
31-
"Export-Package" -> ("*;version=${ver}"),
32-
"Import-Package" -> ("scala.*;version=\"${range;[==,=+);${ver}}\",*"),
31+
"Export-Package" -> "*;version=${ver}",
32+
"Import-Package" -> "scala.*;version=\"${range;[==,=+);${ver}}\",*",
3333
"Bundle-Version" -> v,
3434
"Bundle-RequiredExecutionEnvironment" -> "JavaSE-1.6, JavaSE-1.7",
3535
"-eclipse" -> "false"

project/Quiet.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,4 @@ object Quiet {
2828
case x => x
2929
}
3030
}
31-
32-
def silenceIvyUpdateInfoLogging = logLevel in update := Level.Warn
3331
}

project/ScalaTool.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ case class ScalaTool(mainClass: String,
2727
} else classpath.mkString(":").replace('\\', '/').replaceAll(varRegex, """\${$1}""")
2828

2929
val variables = Map(
30-
("@@" -> "@"), // for backwards compatibility
31-
("@class@" -> mainClass),
32-
("@properties@" -> (properties map { case (k, v) => s"""-D$k="$v""""} mkString " ")),
33-
("@javaflags@" -> javaOpts),
34-
("@toolflags@" -> toolFlags),
35-
("@classpath@" -> platformClasspath)
30+
"@@" -> "@", // for backwards compatibility
31+
"@class@" -> mainClass,
32+
"@properties@" -> (properties map { case (k, v) => s"""-D$k="$v""""} mkString " "),
33+
"@javaflags@" -> javaOpts,
34+
"@toolflags@" -> toolFlags,
35+
"@classpath@" -> platformClasspath
3636
)
3737

3838
val (from, to) = variables.unzip

project/ScriptCommands.scala

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
import sbt._
22
import Keys._
3-
import complete.DefaultParsers._
3+
import BuildSettings.autoImport._
44

55
/** Custom commands for use by the Jenkins scripts. This keeps the surface area and call syntax small. */
66
object ScriptCommands {
7-
def all = Seq(setupPublishCore)
7+
def all = Seq(setupPublishCore, setupValidateTest)
88

99
/** Set up the environment for `validate/publish-core`. The argument is the Artifactory snapshot repository URL. */
1010
def setupPublishCore = Command.single("setupPublishCore") { case (state, url) =>
1111
Project.extract(state).append(Seq(
12-
VersionUtil.baseVersionSuffix in Global := "SHA-SNAPSHOT",
12+
baseVersionSuffix in Global := "SHA-SNAPSHOT",
1313
// Append build.timestamp to Artifactory URL to get consistent build numbers (see https://github.com/sbt/sbt/issues/2088):
1414
publishTo in Global := Some("scala-pr" at url.replaceAll("/$", "") + ";build.timestamp=" + System.currentTimeMillis),
1515
publishArtifact in (Compile, packageDoc) in ThisBuild := false,
16-
scalacOptions in Compile in ThisBuild += "-optimise"
16+
scalacOptions in Compile in ThisBuild += "-optimise",
17+
logLevel in ThisBuild := Level.Info,
18+
logLevel in update in ThisBuild := Level.Warn
1719
), state)
1820
}
21+
22+
/** Set up the environment for `validate/test`. The argument is the Artifactory snapshot repository URL. */
23+
def setupValidateTest = Command.single("setupValidateTest") { case (state, url) =>
24+
//TODO When ant is gone, pass starr version as an argument to this command instead of using version.properties
25+
Project.extract(state).append(Seq(
26+
resolvers in Global += "scala-pr" at url,
27+
scalacOptions in Compile in ThisBuild += "-optimise",
28+
logLevel in ThisBuild := Level.Info,
29+
logLevel in update in ThisBuild := Level.Warn
30+
), state)
31+
}
1932
}

project/VersionUtil.scala

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import sbt._
22
import Keys._
33
import java.util.Properties
4-
import java.io.FileInputStream
4+
import java.io.{File, FileInputStream}
55
import scala.collection.JavaConverters._
6+
import BuildSettings.autoImport._
67

78
object VersionUtil {
8-
lazy val baseVersion = settingKey[String]("The base version number from which all others are derived")
9-
lazy val baseVersionSuffix = settingKey[String]("Identifies the kind of version to build")
109
lazy val copyrightString = settingKey[String]("Copyright string.")
1110
lazy val versionProperties = settingKey[Versions]("Version properties.")
1211
lazy val generateVersionPropertiesFile = taskKey[File]("Generating version properties file.")
@@ -123,4 +122,33 @@ object VersionUtil {
123122
/** Get a subproject version number from `versionProps` */
124123
def versionNumber(name: String): String =
125124
versionProps(s"$name.version.number")
125+
126+
/** Build a dependency to a Scala module with the given group and artifact ID */
127+
def scalaDep(group: String, artifact: String, versionProp: String = null, scope: String = null, compatibility: String = "binary") = {
128+
val vp = if(versionProp eq null) artifact else versionProp
129+
val m = group % (artifact + "_" + versionProps(s"scala.$compatibility.version")) % versionNumber(vp)
130+
val m2 = if(scope eq null) m else m % scope
131+
// exclusion of the scala-library transitive dependency avoids eviction warnings during `update`:
132+
m2.exclude("org.scala-lang", "*")
133+
}
134+
135+
private def bootstrapOrganization(path: String) =
136+
"org.scala-lang.scala-sha-bootstrap." + path.replace('/', '.')
137+
138+
/** Build a dependency to a JAR file in the bootstrap repository */
139+
def bootstrapDep(baseDir: File, path: String, libName: String): ModuleID = {
140+
val sha = IO.read(baseDir / path / s"$libName.jar.desired.sha1").split(' ')(0)
141+
bootstrapOrganization(path) % libName % sha from
142+
s"https://dl.bintray.com/typesafe/scala-sha-bootstrap/org/scala-lang/bootstrap/$sha/$path/$libName.jar"
143+
}
144+
145+
/** Copy a boostrap dependency JAR that is on the classpath to a file */
146+
def copyBootstrapJar(cp: Seq[Attributed[File]], baseDir: File, path: String, libName: String): Unit = {
147+
val org = bootstrapOrganization(path)
148+
val resolved = cp.find { a =>
149+
val mod = a.get(moduleID.key)
150+
mod.map(_.organization) == Some(org) && mod.map(_.name) == Some(libName)
151+
}.map(_.data).get
152+
IO.copyFile(resolved, baseDir / path / s"$libName.jar")
153+
}
126154
}

project/plugins.sbt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
scalacOptions ++= Seq("-unchecked", "-feature", /*"-deprecation",*/
2+
"-Xlint" /*, "-Xfatal-warnings"*/)
3+
14
libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.3.2"
25

36
libraryDependencies += "org.pantsbuild" % "jarjar" % "1.6.3"
@@ -15,3 +18,5 @@ buildClasspath := (externalDependencyClasspath in Compile).value.map(_.data).mkS
1518
buildInfoKeys := Seq[BuildInfoKey](buildClasspath)
1619

1720
buildInfoPackage := "scalabuild"
21+
22+
libraryDependencies += "com.typesafe" %% "mima-reporter" % "0.1.8"

scripts/jobs/validate/publish-core

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ case $prDryRun in
1616
;;
1717
*)
1818
echo ">>> Getting Scala version number."
19-
$SBT_CMD "setupPublishCore $prRepoUrl" generateBuildCharacterPropertiesFile
19+
$SBT_CMD --warn "setupPublishCore $prRepoUrl" generateBuildCharacterPropertiesFile
2020
parseScalaProperties buildcharacter.properties # produce maven_version_number
2121

2222
echo ">>> Checking availability of Scala ${maven_version_number} in $prRepoUrl."
@@ -27,7 +27,7 @@ case $prDryRun in
2727
if $libraryAvailable && $reflectAvailable && $compilerAvailable; then
2828
echo "Scala core already built!"
2929
else
30-
$SBT_CMD "setupPublishCore $prRepoUrl" $antBuildArgs publish
30+
$SBT_CMD --warn "setupPublishCore $prRepoUrl" publish
3131
fi
3232

3333
mv buildcharacter.properties jenkins.properties # parsed by the jenkins job

scripts/jobs/validate/test

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1-
#!/bin/bash -e
1+
#!/bin/bash -e -v -x
2+
3+
baseDir=${WORKSPACE-`pwd`}
4+
scriptsDir="$baseDir/scripts"
5+
. $scriptsDir/common
26

37
case $prDryRun in
8+
49
yep)
510
echo "DRY RUN"
611
;;
12+
713
*)
8-
./pull-binary-libs.sh
914

1015
# build quick using STARR built upstream, as specified by scalaVersion
11-
# (in that sense it's locker, since it was built with starr by that upstream job)
12-
ant -Dstarr.version=$scalaVersion \
13-
-Dscalac.args.optimise=-optimise \
14-
-Dlocker.skip=1 -Dextra.repo.url=$prRepoUrl \
15-
$testExtraArgs ${testTarget-test.core docs.done}
16+
# (in that sense it's locker, since it was built with starr by that upstream job);
17+
# and run JUnit tests, partest, OSGi tests, MiMa and scaladoc
18+
$SBT_CMD \
19+
-Dstarr.version=$scalaVersion \
20+
--warn \
21+
"setupValidateTest $prRepoUrl" \
22+
$testExtraArgs \
23+
"test" \
24+
"partest run pos neg jvm" \
25+
"partest res scalap specialized scalacheck" \
26+
"partest instrumented presentation" \
27+
"partest --srcpath scaladoc" \
28+
osgiTestFelix/test \
29+
osgiTestEclipse/test \
30+
library/mima \
31+
reflect/mima \
32+
doc
33+
1634
;;
17-
esac
35+
36+
esac

src/scaladoc/scala/tools/nsc/doc/model/MemberLookup.scala

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,22 @@ trait MemberLookup extends base.MemberLookupBase {
4444
/* Get package object which has associatedFile ne null */
4545
sym.info.member(newTermName("package"))
4646
else sym
47-
Option(sym1.associatedFile) flatMap (_.underlyingSource) flatMap { src =>
48-
val path = src.canonicalPath
47+
def classpathEntryFor(s: Symbol): Option[String] = {
48+
Option(s.associatedFile).flatMap(_.underlyingSource).map { src =>
49+
val path = src.canonicalPath
50+
if(path.endsWith(".class")) { // Individual class file -> Classpath entry is root dir
51+
var nesting = s.ownerChain.count(_.hasPackageFlag)
52+
if(nesting > 0) {
53+
val p = 0.until(nesting).foldLeft(src) {
54+
case (null, _) => null
55+
case (f, _) => f.container
56+
}
57+
if(p eq null) path else p.canonicalPath
58+
} else path
59+
} else path // JAR file (and fallback option)
60+
}
61+
}
62+
classpathEntryFor(sym1) flatMap { path =>
4963
settings.extUrlMapping get path map { url =>
5064
LinkToExternal(name, url + "#" + name)
5165
}
File renamed without changes.
File renamed without changes.

test/osgi/src/logback.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>*** \(%logger{30}\)%green(%X{debugId}) %msg%n</pattern>
5+
</encoder>
6+
</appender>
7+
<root level="${log.root:-warn}">
8+
<appender-ref ref="STDOUT" />
9+
</root>
10+
</configuration>

test/scaladoc/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.log
2+
*.obj/

test/scaladoc/run/SI-191.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ object Test extends ScaladocModelTest {
3333
def scalaURL = "http://bog.us"
3434

3535
override def scaladocSettings = {
36-
val scalaLibUri = getClass.getClassLoader.getResource("scala/Function1.class").getPath.split("!")(0)
37-
val scalaLibPath = new URI(scalaLibUri).getPath
38-
val externalArg = s"$scalaLibPath#$scalaURL"
39-
"-no-link-warnings -doc-external-doc " + externalArg
36+
val samplePath = getClass.getClassLoader.getResource("scala/Function1.class").getPath
37+
val scalaLibPath = if(samplePath.contains("!")) { // in scala-library.jar
38+
val scalaLibUri = samplePath.split("!")(0)
39+
new URI(scalaLibUri).getPath
40+
} else { // individual class files on disk
41+
samplePath.replace('\\', '/').dropRight("scala/Function1.class".length)
42+
}
43+
s"-no-link-warnings -doc-external-doc $scalaLibPath#$scalaURL"
4044
}
4145

4246
def testModel(rootPackage: Package) {

test/scaladoc/run/t8557.scala

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import java.net.URI
2+
13
import scala.tools.nsc.doc.base._
24
import scala.tools.nsc.doc.model._
35
import scala.tools.partest.ScaladocModelTest
@@ -15,10 +17,22 @@ object Test extends ScaladocModelTest {
1517
class A
1618
"""
1719

20+
def scalaURL = "http://www.scala-lang.org/api/current/"
21+
1822
// a non-canonical path to scala-library.jar should still work
19-
// this is a bit fragile (depends on the current directory being the root of the repo ;
20-
// ant & partest seem to do that properly)
21-
def scaladocSettings = "-doc-external-doc build/pack/bin/../lib/scala-library.jar#http://www.scala-lang.org/api/current/"
23+
override def scaladocSettings = {
24+
val samplePath = getClass.getClassLoader.getResource("scala/Function1.class").getPath.replace('\\', '/')
25+
val scalaLibPath = if(samplePath.contains("!")) { // in scala-library.jar
26+
val scalaLibUri = samplePath.split("!")(0)
27+
val p = new URI(scalaLibUri).getPath
28+
// this is a bit fragile (depends on the scala library being in build/pack as produced by ant)
29+
p.replace("/pack/lib/scala-library.jar", "/pack/bin/../lib/scala-library.jar")
30+
} else { // individual class files on disk
31+
val p = samplePath.dropRight("scala/Function1.class".length + 1)
32+
p + "/.." + p.takeRight(p.length - p.lastIndexOf('/'))
33+
}
34+
s"-doc-external-doc $scalaLibPath#$scalaURL"
35+
}
2236

2337
def testModel(rootPackage: Package) = {
2438
// get the quick access implicit defs in scope (_package(s), _class(es), _trait(s), object(s) _method(s), _value(s))

0 commit comments

Comments
 (0)