Skip to content

Commit 6addf9a

Browse files
Merge branch 'evaluator-free-of-classpath-issues' into sbt-org-policies-integration
2 parents c54044e + ebeac2e commit 6addf9a

File tree

17 files changed

+880
-647
lines changed

17 files changed

+880
-647
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ lib_managed/
1111
src_managed/
1212
project/boot/
1313
project/plugins/project/
14+
temp/
1415

1516
# ensime
1617
.ensime

.scalafmt.conf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,3 @@ rewrite {
2020
rules = [SortImports, RedundantBraces]
2121
redundantBraces.maxLines = 1
2222
}
23-

.travis.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ services:
44
- docker
55
scala:
66
- 2.11.8
7+
- 2.12.1
78
jdk:
89
- oraclejdk8
910
cache:
@@ -14,21 +15,28 @@ cache:
1415
env:
1516
global: JAVA_OPTS=-Xmx2g SBT_OPTS="-XX:+UseConcMarkSweepGC -XX:MaxPermSize=512m"
1617
script:
17-
- sbt test
18+
- sbt ++$TRAVIS_SCALA_VERSION test
1819

1920
before_install:
2021
- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then
2122
openssl aes-256-cbc -K $encrypted_98b97a2f355e_key -iv $encrypted_98b97a2f355e_iv -in secring.gpg.enc -out secring.gpg -d;
2223
fi
2324

2425
after_success:
25-
- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then
26+
- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" -a "$TRAVIS_SCALA_VERSION" = "2.11.8" ]; then
2627
sbt publishSignedAll;
2728
echo "Deploying to Heroku";
2829
docker login [email protected] --password=$heroku_token registry.heroku.com;
2930
sbt dockerBuildAndPush;
3031
sbt smoketests/test;
3132
fi
33+
- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" -a "$TRAVIS_SCALA_VERSION" = "2.12.1" ]; then
34+
sbt -Devaluator.heroku.name=scala-evaluator-212 publishSignedAll;
35+
echo "Deploying to Heroku";
36+
docker login [email protected] --password=$heroku_token registry.heroku.com;
37+
sbt -Devaluator.heroku.name=scala-evaluator-212 dockerBuildAndPush;
38+
sbt smoketests/test;
39+
fi
3240
- if [ "$TRAVIS_PULL_REQUEST" = "true" ]; then
3341
echo "Not in master branch, skipping deploy and release";
3442
fi

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,23 @@ Evaluating code that may result in a thrown exception
152152
}
153153
```
154154

155+
# License
155156

157+
Copyright (C) 2015-2016 47 Degrees, LLC.
158+
Reactive, scalable software solutions.
159+
http://47deg.com
160+
161+
162+
Some parts of the code have been taken from [twitter-eval](https://github.com/twitter/util/tree/302235a473d20735e5327d785e19b0f489b4a59f/util-eval), and slightly adapted to the evaluator needs. Copyright 2010 Twitter, Inc.
163+
164+
Licensed under the Apache License, Version 2.0 (the "License");
165+
you may not use this file except in compliance with the License.
166+
You may obtain a copy of the License at
167+
168+
http://www.apache.org/licenses/LICENSE-2.0
169+
170+
Unless required by applicable law or agreed to in writing, software
171+
distributed under the License is distributed on an "AS IS" BASIS,
172+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
173+
See the License for the specific language governing permissions and
174+
limitations under the License.

build.sbt

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ pgpPassphrase := Some(getEnvVar("PGP_PASSPHRASE").getOrElse("").toCharArray)
22
pgpPublicRing := file(s"$gpgFolder/pubring.gpg")
33
pgpSecretRing := file(s"$gpgFolder/secring.gpg")
44

5+
addCommandAlias(
6+
"publishSignedAll",
7+
";evaluator-sharedJS/publishSigned;evaluator-sharedJVM/publishSigned;evaluator-clientJS/publishSigned;evaluator-clientJVM/publishSigned;evaluator-compiler/+publishSigned"
8+
)
9+
510
lazy val root = (project in file("."))
6-
.settings(mainClass in Universal := Some("org.scalaexercises.evaluator.EvaluatorServer"))
7-
.settings(stage <<= (stage in Universal in `evaluator-server`))
811
.settings(noPublishSettings: _*)
912
.aggregate(
1013
`evaluator-server`,
14+
`evaluator-compiler`,
1115
`evaluator-shared-jvm`,
1216
`evaluator-shared-js`,
1317
`evaluator-client-jvm`,
@@ -42,7 +46,7 @@ lazy val `evaluator-client-jvm` = `evaluator-client`.jvm
4246
lazy val `evaluator-client-js` = `evaluator-client`.js
4347

4448
lazy val `evaluator-server` = (project in file("server"))
45-
.dependsOn(`evaluator-shared-jvm`)
49+
.dependsOn(`evaluator-shared-jvm`, `evaluator-compiler` % "test->test;compile->compile")
4650
.enablePlugins(JavaAppPackaging)
4751
.enablePlugins(AutomateHeaderPlugin)
4852
.enablePlugins(sbtdocker.DockerPlugin)
@@ -69,7 +73,15 @@ lazy val `evaluator-server` = (project in file("server"))
6973
assemblyJarName in assembly := "evaluator-server.jar"
7074
)
7175
.settings(dockerSettings)
72-
.settings(scalaMacroDependencies: _*)
76+
77+
lazy val `evaluator-compiler` = (project in file("compiler"))
78+
.dependsOn(`evaluator-shared-jvm`)
79+
.enablePlugins(AutomateHeaderPlugin)
80+
.enablePlugins(BuildInfoPlugin)
81+
.settings(
82+
name := "evaluator-compiler"
83+
)
84+
.settings(compilerDependencySettings: _*)
7385

7486
lazy val `smoketests` = (project in file("smoketests"))
7587
.dependsOn(`evaluator-server`)
@@ -85,30 +97,3 @@ lazy val `smoketests` = (project in file("smoketests"))
8597
%%("scalatest") % "test"
8698
)
8799
)
88-
89-
onLoad in Global := (Command
90-
.process("project evaluator-server", _: State)) compose (onLoad in Global).value
91-
addCommandAlias(
92-
"publishSignedAll",
93-
";evaluator-sharedJS/publishSigned;evaluator-sharedJVM/publishSigned;evaluator-clientJS/publishSigned;evaluator-clientJVM/publishSigned"
94-
)
95-
96-
lazy val dockerSettings = Seq(
97-
docker <<= docker dependsOn assembly,
98-
dockerfile in docker := {
99-
100-
val artifact: File = assembly.value
101-
val artifactTargetPath = artifact.name
102-
103-
sbtdocker.immutable.Dockerfile.empty
104-
.from("ubuntu:latest")
105-
.run("apt-get", "update")
106-
.run("apt-get", "install", "-y", "openjdk-8-jdk")
107-
.run("useradd", "-m", "evaluator")
108-
.user("evaluator")
109-
.add(artifact, artifactTargetPath)
110-
.cmdRaw(
111-
s"java -Dhttp.port=$$PORT -Deval.auth.secretKey=$$EVAL_SECRET_KEY -jar $artifactTargetPath")
112-
},
113-
imageNames in docker := Seq(ImageName(repository = "registry.heroku.com/scala-evaluator/web"))
114-
)
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* scala-exercises - evaluator-compiler
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
6+
package org.scalaexercises.evaluator
7+
8+
import java.io.{File, PrintWriter}
9+
import java.math.BigInteger
10+
import java.security.MessageDigest
11+
12+
import org.scalaexercises.evaluator.Eval.CompilerException
13+
import org.xeustechnologies.jcl.{JarClassLoader, JclObjectFactory}
14+
15+
import java.net.URLClassLoader
16+
import scala.reflect.internal.util.{AbstractFileClassLoader, Position}
17+
import scala.tools.nsc.Settings
18+
import scala.tools.nsc.io.{AbstractFile, VirtualDirectory}
19+
import scala.tools.nsc.reporters._
20+
21+
/**
22+
* The code in this file was taken and only slightly modified from
23+
*
24+
* https://github.com/twitter/util/blob/302235a473d20735e5327d785e19b0f489b4a59f/util-eval/src/main/scala/com/twitter/util/Eval.scala
25+
*
26+
* Twitter, Inc.
27+
*
28+
* Evaluates files, strings, or input streams as Scala code, and returns the result.
29+
*
30+
* If `target` is `None`, the results are compiled to memory (and are therefore ephemeral). If
31+
* `target` is `Some(path)`, the path must point to a directory, and classes will be saved into
32+
* that directory. You can optionally pass a list of JARs to include to the classpath during
33+
* compilation and evaluation.
34+
*
35+
* The flow of evaluation is:
36+
* - wrap code in an `apply` method in a generated class
37+
* - compile the class adding the jars to the classpath
38+
* - construct an instance of that class
39+
* - return the result of `apply()`
40+
*/
41+
case class Eval(target: Option[File] = None, jars: List[File] = Nil) {
42+
@volatile var errors: Map[String, List[CompilationInfo]] = Map.empty
43+
44+
val compilerOutputDir: AbstractFile = target match {
45+
case Some(dir) => AbstractFile.getDirectory(dir)
46+
case None => new VirtualDirectory("(memory)", None)
47+
}
48+
49+
protected lazy val compilerSettings: Settings = new EvalSettings(compilerOutputDir)
50+
51+
protected lazy val compilerMessageHandler: Option[Reporter] = Some(new AbstractReporter {
52+
override val settings: Settings = compilerSettings
53+
override def displayPrompt(): Unit = ()
54+
override def display(pos: Position, msg: String, severity: this.type#Severity): Unit =
55+
errors += convert((pos, msg, severity.toString))
56+
override def reset(): Unit = {
57+
super.reset()
58+
errors = Map.empty
59+
}
60+
private[this] def convert(
61+
errors: (Position, String, String)): (String, List[CompilationInfo]) = {
62+
val (pos, msg, severity) = errors
63+
(severity, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil)
64+
}
65+
})
66+
67+
// Primary encapsulation around native Scala compiler
68+
private[this] lazy val compiler = new StringCompiler(
69+
codeWrapperLineOffset,
70+
target,
71+
compilerOutputDir,
72+
compilerSettings,
73+
compilerMessageHandler
74+
)
75+
76+
/**
77+
* Check if code is Eval-able.
78+
* @throws CompilerException if not Eval-able.
79+
*/
80+
def check(code: String) {
81+
val id = uniqueId(code)
82+
val className = "Evaluator__" + id
83+
val wrappedCode = wrapCodeInClass(className, code)
84+
85+
val scalaSource = createScalaSource(className, wrappedCode)
86+
87+
compiler.compile(scalaSource)
88+
}
89+
90+
/**
91+
* Will generate a className of the form `Evaluator__<unique>`,
92+
* where unique is computed from the jvmID (a random number)
93+
* and a digest of code
94+
*/
95+
def execute[T](code: String, resetState: Boolean, jars: Seq[File]): T = {
96+
val id = uniqueId(code)
97+
val className = "Evaluator__" + id
98+
execute(className, code, resetState, jars)
99+
}
100+
101+
def execute[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = {
102+
103+
import collection.JavaConverters._
104+
val urlClassLoader =
105+
new URLClassLoader((jars map (_.toURI.toURL)).toArray, NullLoader)
106+
val classLoader =
107+
new AbstractFileClassLoader(compilerOutputDir, urlClassLoader)
108+
val jcl = new JarClassLoader(classLoader)
109+
110+
val jarUrls = (jars map (_.getAbsolutePath)).toList
111+
jcl.addAll(jarUrls.asJava)
112+
jcl.add(compilerOutputDir.file.toURI.toURL)
113+
114+
val wrappedCode = wrapCodeInClass(className, code)
115+
116+
compiler.compile(
117+
createScalaSource(className, wrappedCode),
118+
className,
119+
resetState
120+
)
121+
122+
val factory = JclObjectFactory.getInstance()
123+
val instantiated = factory.create(jcl, className)
124+
val method = instantiated.getClass.getMethod("run")
125+
val result: Any = Option(method.invoke(instantiated)).getOrElse((): Unit)
126+
127+
result.asInstanceOf[T]
128+
}
129+
130+
private[this] def createScalaSource(fileName: String, code: String) = {
131+
val path = s"temp/src/main/scala/"
132+
val scalaSourceDir = new File(path)
133+
val scalaSource = new File(s"$path/$fileName.scala")
134+
135+
scalaSourceDir.mkdirs()
136+
137+
val writer = new PrintWriter(scalaSource)
138+
139+
writer.write(code)
140+
writer.close()
141+
scalaSource
142+
}
143+
144+
private[this] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = {
145+
val digest = MessageDigest.getInstance("SHA-1").digest(code.getBytes())
146+
val sha = new BigInteger(1, digest).toString(16)
147+
idOpt match {
148+
case Some(i) => sha + "_" + i
149+
case _ => sha
150+
}
151+
}
152+
153+
/*
154+
* Wraps source code in a new class with an apply method.
155+
* NB: If this method is changed, make sure `codeWrapperLineOffset` is correct.
156+
*/
157+
private[this] def wrapCodeInClass(className: String, code: String) = {
158+
s"""
159+
class $className extends java.io.Serializable {
160+
def run() = {
161+
$code
162+
}
163+
}
164+
"""
165+
}
166+
167+
/*
168+
* Defines the number of code lines that proceed evaluated code.
169+
* Used to ensure compile error messages report line numbers aligned with user's code.
170+
* NB: If `wrapCodeInClass(String,String)` is changed, make sure this remains correct.
171+
*/
172+
private[this] val codeWrapperLineOffset = 2
173+
174+
class EvalSettings(output: AbstractFile) extends Settings {
175+
nowarnings.value = true // warnings are exceptions, so disable
176+
outputDirs.setSingleOutput(output)
177+
if (jars.nonEmpty) {
178+
val newJars = (jars :+ output.file).mkString(File.pathSeparator)
179+
classpath.value = newJars
180+
bootclasspath.value = newJars
181+
}
182+
}
183+
}
184+
185+
object Eval {
186+
private val jvmId = java.lang.Math.abs(new java.util.Random().nextInt())
187+
188+
class CompilerException(val messages: List[List[String]])
189+
extends Exception("Compiler exception " + messages.map(_.mkString("\n")).mkString("\n"))
190+
}

0 commit comments

Comments
 (0)