Skip to content

Commit 162d07d

Browse files
Merge pull request #118 from alexarchambault/merge-upstream
Merge upstream changes
2 parents e34dca3 + 87f14df commit 162d07d

38 files changed

+1295
-133
lines changed

backend/src/main/scala/bloop/task/Task.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,14 @@ sealed trait Task[+A] { self =>
164164
Task
165165
.chooseFirstOf(
166166
self,
167-
Task.sleep(duration).flatMap(_ => backup)
167+
Task.sleep(duration)
168168
)
169-
.map {
170-
case Left((a, _)) => a
171-
case Right((_, b)) => b
169+
.flatMap {
170+
case Left((a, _)) =>
171+
Task.now(a)
172+
case Right((a, _)) =>
173+
a.cancel()
174+
backup
172175
}
173176
}
174177

backend/src/test/scala/bloop/task/TaskSpec.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,15 @@ class TaskSpec {
183183

184184
}
185185

186+
@Test
187+
def timeoutTo2: Unit = {
188+
val ref = new AtomicBoolean(true)
189+
val t1 = Task.unit.delayExecution(500.milli)
190+
val t2 = Task(ref.set(false))
191+
val withTimeout = t1.timeoutTo(1.second, t2)
192+
193+
Await.result((withTimeout *> Task.sleep(2.seconds)).runAsync, 3.second)
194+
assertEquals(true, ref.get())
195+
}
196+
186197
}

config/src/main/scala/bloop/config/Config.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ object Config {
236236
excludes: List[String]
237237
)
238238

239+
case class SourceGenerator(
240+
sourcesGlobs: List[SourcesGlobs],
241+
outputDirectory: Path,
242+
command: List[String]
243+
)
244+
239245
case class Project(
240246
name: String,
241247
directory: Path,
@@ -254,12 +260,13 @@ object Config {
254260
test: Option[Test],
255261
platform: Option[Platform],
256262
resolution: Option[Resolution],
257-
tags: Option[List[String]]
263+
tags: Option[List[String]],
264+
sourceGenerators: Option[List[SourceGenerator]]
258265
)
259266

260267
object Project {
261268
// FORMAT: OFF
262-
private[bloop] val empty: Project = Project("", emptyPath, None, List(), None, None, List(), List(), emptyPath, emptyPath, None, None, None, None, None, None, None, None)
269+
private[bloop] val empty: Project = Project("", emptyPath, None, List(), None, None, List(), List(), emptyPath, emptyPath, None, None, None, None, None, None, None, None, None)
263270
// FORMAT: ON
264271

265272
def analysisFileName(projectName: String): String = s"$projectName-analysis.bin"
@@ -328,6 +335,7 @@ object Config {
328335
Some(Test(List(), TestOptions(Nil, Nil))),
329336
Some(platform),
330337
Some(Resolution(Nil)),
338+
None,
331339
None
332340
)
333341

frontend/src/it/scala/bloop/CommunityBuild.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import bloop.engine.{
2121
Run,
2222
State
2323
}
24-
import bloop.engine.caches.ResultsCache
24+
import bloop.engine.caches.{ResultsCache, SourceGeneratorCache}
2525
import bloop.io.AbsolutePath
2626
import bloop.logging.{BloopLogger, Logger, NoopLogger}
2727
import bloop.task.Task
@@ -95,7 +95,7 @@ abstract class CommunityBuild(val buildpressHomeDir: AbsolutePath) {
9595
val loadedProjects = BuildLoader.loadSynchronously(configDirectory, logger)
9696
val workspaceSettings = WorkspaceSettings.readFromFile(configDirectory, logger)
9797
val build = Build(configDirectory, loadedProjects, workspaceSettings)
98-
val state = State.forTests(build, compilerCache, logger)
98+
val state = State.forTests(build, compilerCache, SourceGeneratorCache.empty, logger)
9999
state.copy(results = ResultsCache.emptyForTests)
100100
}
101101

@@ -139,6 +139,7 @@ abstract class CommunityBuild(val buildpressHomeDir: AbsolutePath) {
139139
sources = Nil,
140140
sourcesGlobs = Nil,
141141
sourceRoots = None,
142+
sourceGenerators = Nil,
142143
testFrameworks = Nil,
143144
testOptions = Config.TestOptions.empty,
144145
out = dummyClassesDir,

frontend/src/main/scala/bloop/Cli.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,16 @@ object Cli {
8888
else parse(args, nailgunOptions)
8989
}
9090

91-
val exitStatus = run(cmd, NailgunPool(ngContext))
92-
ngContext.exit(exitStatus.code)
91+
try {
92+
val exitStatus = run(cmd, NailgunPool(ngContext))
93+
ngContext.exit(exitStatus.code)
94+
} catch {
95+
case x: java.util.concurrent.ExecutionException =>
96+
// print stack trace of fatal errors thrown in asynchronous code, see https://stackoverflow.com/questions/17265022/what-is-a-boxed-error-in-scala
97+
// the stack trace is somehow propagated all the way to the client when printing this
98+
x.getCause.printStackTrace(ngContext.out)
99+
ngContext.exit(ExitStatus.UnexpectedError.code)
100+
}
93101
}
94102

95103
val commands: Seq[String] = Commands.RawCommand.help.messages.flatMap(_._1.headOption.toSeq)

frontend/src/main/scala/bloop/bsp/BloopBspServices.scala

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import ch.epfl.scala.bsp
2020
import ch.epfl.scala.bsp.BuildTargetIdentifier
2121
import ch.epfl.scala.bsp.MessageType
2222
import ch.epfl.scala.bsp.ShowMessageParams
23+
import ch.epfl.scala.bsp.CompileResult
24+
import ch.epfl.scala.bsp.StatusCode
25+
import ch.epfl.scala.bsp.Uri
26+
import ch.epfl.scala.bsp.endpoints
27+
import ch.epfl.scala.bsp.CompileResult
28+
import ch.epfl.scala.bsp.MessageType
29+
import ch.epfl.scala.bsp.ShowMessageParams
30+
import ch.epfl.scala.bsp.StatusCode
2331
import ch.epfl.scala.bsp.Uri
2432
import ch.epfl.scala.bsp.endpoints
2533
import ch.epfl.scala.debugadapter.DebugServer
@@ -302,7 +310,7 @@ final class BloopBspServices(
302310
compileProvider = Some(BloopBspServices.DefaultCompileProvider),
303311
testProvider = Some(BloopBspServices.DefaultTestProvider),
304312
runProvider = Some(BloopBspServices.DefaultRunProvider),
305-
debugProvider = None, // todo kpodsiad
313+
debugProvider = Some(BloopBspServices.DefaultDebugProvider),
306314
inverseSourcesProvider = Some(true),
307315
dependencySourcesProvider = Some(true),
308316
dependencyModulesProvider = None,
@@ -689,10 +697,7 @@ final class BloopBspServices(
689697
}
690698

691699
def test(params: bsp.TestParams): BspEndpointResponse[bsp.TestResult] = {
692-
def test(
693-
project: Project,
694-
state: State
695-
): Task[State] = {
700+
def test(project: Project, state: State): Task[Tasks.TestRuns] = {
696701
val testFilter = TestInternals.parseFilters(Nil) // Don't support test only for now
697702
val handler = new LoggingEventHandler(state.logger)
698703
Tasks.test(
@@ -717,25 +722,35 @@ final class BloopBspServices(
717722
val args = params.arguments.getOrElse(Nil)
718723
val logger = logger0.asBspServerVerbose
719724
compileProjects(mappings, state, args, originId, logger).flatMap {
720-
case (newState, compileResult) =>
721-
compileResult match {
722-
case Right(_) =>
723-
val sequentialTestExecution = mappings.foldLeft(Task.now(newState)) {
724-
case (taskState, (_, p)) => taskState.flatMap(state => test(p, state))
725-
}
726-
727-
sequentialTestExecution.materialize.map(_.toEither).map {
728-
case Right(newState) =>
725+
case (newState, Right(CompileResult(_, StatusCode.Ok, _, _))) =>
726+
val sequentialTestExecution: Task[Seq[Tasks.TestRuns]] =
727+
Task.sequence(mappings.map { case (_, p) => test(p, state) })
728+
729+
sequentialTestExecution.materialize.map {
730+
case Success(testRunsSeq) =>
731+
testRunsSeq.reduceOption(_ ++ _) match {
732+
case None =>
729733
(newState, Right(bsp.TestResult(originId, bsp.StatusCode.Ok, None, None)))
730-
case Left(e) =>
731-
//(newState, Right(bsp.TestResult(None, bsp.StatusCode.Error, None)))
732-
val errorMessage =
733-
Response.internalError(s"Failed test execution: ${e.getMessage}")
734-
(newState, Left(errorMessage))
734+
case Some(testRuns) =>
735+
val status = testRuns.status
736+
val bspStatus =
737+
if (status == ExitStatus.Ok) bsp.StatusCode.Ok else bsp.StatusCode.Error
738+
(
739+
newState.mergeStatus(status),
740+
Right(bsp.TestResult(originId, bspStatus, None, None))
741+
)
735742
}
736-
737-
case Left(error) => Task.now((newState, Left(error)))
743+
case Failure(e) =>
744+
val errorMessage =
745+
Response.internalError(s"Failed test execution: ${e.getMessage}")
746+
(newState, Left(errorMessage))
738747
}
748+
749+
case (newState, Right(CompileResult(_, errorCode, _, _))) =>
750+
Task.now((newState, Right(bsp.TestResult(originId, errorCode, None, None))))
751+
752+
case (newState, Left(error)) =>
753+
Task.now((newState, Left(error)))
739754
}
740755
}
741756
}
@@ -1026,7 +1041,8 @@ final class BloopBspServices(
10261041
val capabilities = bsp.BuildTargetCapabilities(
10271042
canCompile = true,
10281043
canTest = true,
1029-
canRun = true
1044+
canRun = true,
1045+
canDebug = true
10301046
)
10311047
val isJavaOnly = p.scalaInstance.isEmpty
10321048
val languageIds =
@@ -1064,29 +1080,49 @@ final class BloopBspServices(
10641080
projects: Seq[ProjectMapping],
10651081
state: State
10661082
): BspResult[bsp.SourcesResult] = {
1067-
val sourcesItems = projects.iterator.map {
1068-
case (target, project) =>
1069-
project.allSourceFilesAndDirectories.map { sources =>
1070-
val items = sources.map { s =>
1071-
import bsp.SourceItemKind._
1072-
val uri = s.underlying.toUri()
1073-
val (bspUri, kind) = if (s.exists) {
1074-
(uri, if (s.isFile) File else Directory)
1075-
} else {
1076-
val fileMatcher = FileSystems.getDefault.getPathMatcher("glob:*.{scala, java}")
1077-
if (fileMatcher.matches(s.underlying.getFileName)) (uri, File)
1078-
// If path doesn't exist and its name doesn't look like a file, assume it's a dir
1079-
else (new URI(uri.toString + "/"), Directory)
1080-
}
1081-
// TODO(jvican): Don't default on false for generated, add this info to JSON fields
1082-
bsp.SourceItem(bsp.Uri(bspUri), kind, false)
1083-
}
1083+
def sourceItem(s: AbsolutePath, isGenerated: Boolean): bsp.SourceItem = {
1084+
import bsp.SourceItemKind._
1085+
val uri = s.underlying.toUri()
1086+
val (bspUri, kind) = if (s.exists) {
1087+
(uri, if (s.isFile) File else Directory)
1088+
} else {
1089+
val fileMatcher = FileSystems.getDefault.getPathMatcher("glob:*.{scala, java}")
1090+
if (fileMatcher.matches(s.underlying.getFileName)) (uri, File)
1091+
// If path doesn't exist and its name doesn't look like a file, assume it's a dir
1092+
else (new URI(uri.toString + "/"), Directory)
1093+
}
1094+
bsp.SourceItem(bsp.Uri(bspUri), kind, isGenerated)
1095+
}
1096+
1097+
val dag = Aggregate(projects.map(p => state.build.getDagFor(p._2)).toList)
1098+
1099+
// Collect the projects' sources following the projects topological sorting, so that
1100+
// source generators that depend on other source generators' outputs can run correctly.
1101+
val collectSourcesTasks = Dag.topologicalSort(dag).map { project =>
1102+
val unmanagedSources = project.allUnmanagedSourceFilesAndDirectories.map { sources =>
1103+
sources.map(sourceItem(_, isGenerated = false))
1104+
}
1105+
1106+
val managedSources = {
1107+
val tasks = project.sourceGenerators
1108+
.map(state.sourceGeneratorCache.update(_, state.logger, state.commonOptions))
1109+
Task.gatherUnordered(tasks).map(_.flatten.map(sourceItem(_, isGenerated = true)))
1110+
}
1111+
1112+
for {
1113+
unmanaged <- unmanagedSources
1114+
managed <- managedSources
1115+
} yield (project, unmanaged ++ managed)
1116+
}
1117+
1118+
val projectToTarget = projects.map { case (target, project) => project -> target }.toMap
1119+
Task.sequence(collectSourcesTasks).map { results =>
1120+
val items = results.flatMap {
1121+
case (project, items) =>
10841122
val roots = project.sourceRoots.map(_.map(p => bsp.Uri(p.toBspUri)))
1085-
bsp.SourcesItem(target, items, roots)
1086-
}
1087-
}.toList
1123+
projectToTarget.get(project).map(bsp.SourcesItem(_, items, roots))
1124+
}
10881125

1089-
Task.sequence(sourcesItems).map { items =>
10901126
(state, Right(bsp.SourcesResult(items)))
10911127
}
10921128
}
@@ -1309,4 +1345,5 @@ object BloopBspServices {
13091345
private[bloop] val DefaultCompileProvider = bsp.CompileProvider(DefaultLanguages)
13101346
private[bloop] val DefaultTestProvider = bsp.TestProvider(DefaultLanguages)
13111347
private[bloop] val DefaultRunProvider = bsp.RunProvider(DefaultLanguages)
1348+
private[bloop] val DefaultDebugProvider = bsp.DebugProvider(DefaultLanguages)
13121349
}

frontend/src/main/scala/bloop/bsp/BloopRpcServices.scala

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ object BloopRpcServices {
4141

4242
object BloopEndpoint {
4343

44+
private def extractJsonParams(params: Option[RawJson]): RawJson = {
45+
// if params are empty, bsp4s endpoint's codec might decode it as just a 'null'
46+
// and then parsing it using `readFromArray` fails
47+
// to avoid this issue replace 'null' by empty object '{}'
48+
params
49+
.map(json =>
50+
if (java.util.Arrays.equals(json.value, RawJson.nullValue.value)) RawJson.emptyObj
51+
else json
52+
)
53+
.getOrElse(RawJson.emptyObj)
54+
}
55+
4456
def request[A, B](
4557
endpoint: Endpoint[A, B]
4658
)(f: A => Task[Either[Response.Error, B]]): BloopEndpoint =
@@ -51,7 +63,7 @@ object BloopRpcServices {
5163
val method = endpoint.method
5264
message match {
5365
case Request(`method`, params, id, jsonrpc, headers) =>
54-
val paramsJson = params.flatMap(v => Option(v)).getOrElse(RawJson.emptyObj)
66+
val paramsJson = extractJsonParams(params)
5567
Try(readFromArray[A](paramsJson.value)) match {
5668
case Success(value) =>
5769
f(value).materialize.map { v =>
@@ -87,10 +99,11 @@ object BloopRpcServices {
8799
val method = endpoint.method
88100
message match {
89101
case Notification(`method`, params, _, headers) =>
90-
val paramsJson = params.flatMap(v => Option(v)).getOrElse(RawJson.emptyObj)
102+
val paramsJson = extractJsonParams(params)
91103
Try(readFromArray[A](paramsJson.value)) match {
92104
case Success(value) => f(value).map(a => Response.None)
93-
case Failure(err) => fail(s"Failed to parse notification $message. Errors: $err")
105+
case Failure(err) =>
106+
fail(s"Failed to parse notification $message. Params: $paramsJson. Errors: $err")
94107
}
95108
case Notification(invalidMethod, _, _, headers) =>
96109
fail(s"Expected method '$method', obtained '$invalidMethod'")

frontend/src/main/scala/bloop/data/Project.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import bloop.bsp.ProjectUris
1414
import bloop.config.Config
1515
import bloop.config.ConfigCodecs
1616
import bloop.engine.Dag
17+
import bloop.engine.SourceGenerator
1718
import bloop.engine.tasks.toolchains.JvmToolchain
1819
import bloop.engine.tasks.toolchains.ScalaJsToolchain
1920
import bloop.engine.tasks.toolchains.ScalaNativeToolchain
@@ -45,6 +46,7 @@ final case class Project(
4546
sources: List[AbsolutePath],
4647
sourcesGlobs: List[SourcesGlobs],
4748
sourceRoots: Option[List[AbsolutePath]],
49+
sourceGenerators: List[SourceGenerator],
4850
testFrameworks: List[Config.TestFramework],
4951
testOptions: Config.TestOptions,
5052
out: AbsolutePath,
@@ -88,8 +90,11 @@ final case class Project(
8890
customWorkingDirectory.getOrElse(baseDirectory)
8991
}
9092

93+
def allGeneratorInputs: Task[List[AbsolutePath]] =
94+
Task.sequence(sourceGenerators.map(_.getSources)).map(_.flatten)
95+
9196
/** Returns concatenated list of "sources" and expanded "sourcesGlobs". */
92-
def allSourceFilesAndDirectories: Task[List[AbsolutePath]] = Task {
97+
def allUnmanagedSourceFilesAndDirectories: Task[List[AbsolutePath]] = Task {
9398
val buf = mutable.ListBuffer.empty[AbsolutePath]
9499
buf ++= sources
95100
for (glob <- sourcesGlobs) glob.walkThrough(buf += _)
@@ -354,10 +359,11 @@ object Project {
354359
val sourceRoots = project.sourceRoots.map(_.map(AbsolutePath.apply))
355360

356361
val tags = project.tags.getOrElse(Nil)
362+
val projectDirectory = AbsolutePath(project.directory)
357363

358364
Project(
359365
project.name,
360-
AbsolutePath(project.directory),
366+
projectDirectory,
361367
project.workspaceDir.map(AbsolutePath.apply),
362368
project.dependencies,
363369
instance,
@@ -370,6 +376,7 @@ object Project {
370376
project.sources.map(AbsolutePath.apply),
371377
SourcesGlobs.fromConfig(project, logger),
372378
sourceRoots,
379+
project.sourceGenerators.getOrElse(Nil).map(SourceGenerator.fromConfig(projectDirectory, _)),
373380
project.test.map(_.frameworks).getOrElse(Nil),
374381
project.test.map(_.options).getOrElse(Config.TestOptions.empty),
375382
AbsolutePath(project.out),

0 commit comments

Comments
 (0)