@@ -15,7 +15,7 @@ val communityBuildVersion = "v0.2.4"
1515 val config = scopt.OParser
1616 .parse(Config .parser, args, Config ())
1717 .getOrElse(sys.error(" Failed to parse config" ))
18-
18+
1919 val validationScript = config.validationScript
2020 val releases = Releases .fromRange(config.releasesRange)
2121 val releaseBisect = ReleaseBisect (validationScript, shouldFail = config.shouldFail, releases)
@@ -28,37 +28,55 @@ val communityBuildVersion = "v0.2.4"
2828 println(s " First bad release: ${firstBadRelease.version}" )
2929 println(" \n Finished bisecting releases\n " )
3030
31- val commitBisect = CommitBisect (validationScript, shouldFail = config.shouldFail, lastGoodRelease.hash, firstBadRelease.hash)
31+ val commitBisect = CommitBisect (
32+ validationScript,
33+ compilerDir = config.compilerDir,
34+ openCommunityBuildDir = config.openCommunityBuildDir,
35+ shouldFail = config.shouldFail,
36+ lastGoodRelease.hash,
37+ firstBadRelease.hash
38+ )
3239 commitBisect.bisect()
40+ end run
3341
34- case class ValidationCommand (projectName : String = " " , targets : String = " " , extraScalacOptions : String = " " , disabledScalacOption : String = " " )
42+ case class ValidationCommand (
43+ projectName : String = " " ,
44+ targets : String = " " ,
45+ extraScalacOptions : String = " " ,
46+ disabledScalacOption : String = " "
47+ )
3548case class Config (
36- dryRun : Boolean = false ,
37- releasesRange : ReleasesRange = ReleasesRange .all,
38- shouldFail : Boolean = false ,
39- openCommunityBuildDir : Path = Path .of(" " ),
40- compilerDir : Path = Path .of(" " ),
41- command : ValidationCommand = ValidationCommand ()
42- ){
43- inline def withCommand (mapping : ValidationCommand => ValidationCommand ) = copy(command = mapping(command))
49+ dryRun : Boolean = false ,
50+ releasesRange : ReleasesRange = ReleasesRange .all,
51+ shouldFail : Boolean = false ,
52+ openCommunityBuildDir : Path = Path .of(" " ),
53+ compilerDir : Path = Path .of(" " ),
54+ command : ValidationCommand = ValidationCommand ()
55+ ) {
56+ inline def withCommand (mapping : ValidationCommand => ValidationCommand ) =
57+ copy(command = mapping(command))
4458
4559 lazy val remoteValidationScript : File = ValidationScript .buildProject(
4660 projectName = command.projectName,
4761 targets = Option (command.targets).filter(_.nonEmpty),
4862 extraScalacOptions = command.extraScalacOptions,
49- disabledScalacOption= command.disabledScalacOption,
63+ disabledScalacOption = command.disabledScalacOption,
5064 runId = s " bisect- ${command.projectName}" ,
51- buildURL= " " ,
65+ buildURL = " " ,
5266 executeTests = false ,
5367 openCBDir = openCommunityBuildDir
5468 )
55- lazy val validationScript : File =
69+ lazy val validationScript : File =
5670 require(Files .exists(openCommunityBuildDir), " Open CB dir does not exist" )
5771 require(Files .exists(compilerDir), " Compiler dir does not exist" )
58- ValidationScript .dockerRunBuildProject(command.projectName, remoteValidationScript, openCommunityBuildDir.toFile())
72+ ValidationScript .dockerRunBuildProject(
73+ command.projectName,
74+ remoteValidationScript,
75+ openCommunityBuildDir.toFile()
76+ )
5977}
6078
61- object Config {
79+ object Config {
6280 val parser = {
6381 import scopt .OParser
6482 val builder = OParser .builder[Config ]
@@ -103,19 +121,28 @@ object Config{
103121 .action:
104122 (v, c) => c.copy(compilerDir = Path .of(v))
105123 .text(" Directory containing Scala compiler repository, required for commit-based bissect" )
106- .required()
107- ,
124+ .required() ,
108125 checkConfig { c =>
109126 if ! Files .exists(c.compilerDir) then failure(" Compiler directory does not exist" )
110- else if ! Files .exists(c.openCommunityBuildDir) then failure(" Open Community Build directory does not exist" )
127+ else if ! Files .exists(c.openCommunityBuildDir) then
128+ failure(" Open Community Build directory does not exist" )
111129 else success
112130 }
113131 )
114132 }
115133}
116134
117135object ValidationScript :
118- def buildProject (projectName : String , targets : Option [String ], extraScalacOptions : String , disabledScalacOption : String , runId : String , buildURL : String , executeTests : Boolean , openCBDir : Path ): File = tmpScript(openCBDir){
136+ def buildProject (
137+ projectName : String ,
138+ targets : Option [String ],
139+ extraScalacOptions : String ,
140+ disabledScalacOption : String ,
141+ runId : String ,
142+ buildURL : String ,
143+ executeTests : Boolean ,
144+ openCBDir : Path
145+ ): File = tmpScript(openCBDir) {
119146 val configPatch =
120147 if executeTests
121148 then " "
@@ -158,9 +185,9 @@ object ValidationScript:
158185 """ .stripMargin
159186 }
160187
161- def dockerRunBuildProject (projectName : String , validationScript : File , openCBDir : File ): File =
188+ def dockerRunBuildProject (projectName : String , validationScript : File , openCBDir : File ): File = {
162189 val scriptsPath = " /scripts/"
163- val validationScriptPath = " /scripts/validationScript.sh"
190+ val validationScriptPath = " /scripts/validationScript.sh"
164191 assert(Files .exists(validationScript.toPath()))
165192 tmpScript(openCBDir.toPath)(raw """
166193 |#!/usr/bin/env bash
@@ -175,9 +202,36 @@ object ValidationScript:
175202 | virtuslab/scala-community-build-project-builder:jdk $$ {javaVersion}- $communityBuildVersion \
176203 | /bin/bash -c " $validationScriptPath $$ scalaVersion"
177204 """ .stripMargin)
178-
179- private def tmpScript (openCBDir : Path )(content : String ): File =
180- val executableAttr = PosixFilePermissions .asFileAttribute(PosixFilePermissions .fromString(" rwxr-xr-x" ))
205+ }
206+
207+ def buildCompilerAndValidate (
208+ shouldFail : Boolean ,
209+ validationScript : File ,
210+ openCBDir : Path
211+ ): File = {
212+ // Always bootstrapped
213+ val scala3CompilerProject = " scala3-compiler-bootstrapped"
214+ val scala3Project = " scala3-bootstrapped"
215+ val mavenRepo = " https://scala3.westeurope.cloudapp.azure.com/maven2/bisect/"
216+ // invert the process status if failure was expected
217+ val validationCommandStatusModifier = if shouldFail then " ! " else " "
218+
219+ tmpScript(openCBDir)(raw """
220+ |#!/usr/bin/env bash
221+ |export NIGHTLYBUILD=yes
222+ |#Why we need these strange pipe operations instead of `tail -n1`?
223+ |#I don't know - it would work locally, but keeps failing in GithubAction CI
224+ |scalaVersion= $$ (sbt "print ${scala3CompilerProject}/version" | grep . | tail -n 2 | head -n 1)
225+ |echo "ScalaVersion= $$ {scalaVersion}"
226+ |rm -r out
227+ |sbt "clean; set every sonatypePublishToBundle := Some(\"CommunityBuildRepo\" at \" $mavenRepo\"); ${scala3Project}/publish"
228+ | ${validationCommandStatusModifier}${validationScript.getAbsolutePath} " $$ scalaVersion"
229+ """ .stripMargin)
230+ }
231+
232+ private def tmpScript (openCBDir : Path )(content : String ): File = {
233+ val executableAttr =
234+ PosixFilePermissions .asFileAttribute(PosixFilePermissions .fromString(" rwxr-xr-x" ))
181235 val tmpPath = Files .createTempFile(openCBDir, " scala-bisect-validator" , " .sh" , executableAttr)
182236 val tmpFile = tmpPath.toFile
183237
@@ -189,7 +243,7 @@ object ValidationScript:
189243 tmpFile.deleteOnExit()
190244 Files .write(tmpPath, content.getBytes(StandardCharsets .UTF_8 ))
191245 tmpFile
192-
246+ }
193247
194248case class ReleasesRange (first : Option [String ], last : Option [String ]):
195249 def filter (releases : Seq [Release ]) =
@@ -207,10 +261,13 @@ case class ReleasesRange(first: Option[String], last: Option[String]):
207261object ReleasesRange :
208262 def all = ReleasesRange (None , None )
209263 def tryParse (range : String ): Option [ReleasesRange ] = range match
210- case s " ${first}.. ${last}" => Some (ReleasesRange (
211- Some (first).filter(_.nonEmpty),
212- Some (last).filter(_.nonEmpty)
213- ))
264+ case s " ${first}.. ${last}" =>
265+ Some (
266+ ReleasesRange (
267+ Some (first).filter(_.nonEmpty),
268+ Some (last).filter(_.nonEmpty)
269+ )
270+ )
214271 case _ => None
215272
216273class Releases (val releases : Vector [Release ])
@@ -228,36 +285,48 @@ case class Release(version: String):
228285 def date : String =
229286 version match
230287 case re(date, _) => date
231- case _ => sys.error(s " Could not extract date from release name: $version" )
288+ case _ => sys.error(s " Could not extract date from release name: $version" )
232289 def hash : String =
233290 version match
234291 case re(_, hash) => hash
235- case _ => sys.error(s " Could not extract hash from release name: $version" )
292+ case _ => sys.error(s " Could not extract hash from release name: $version" )
236293
237294 override def toString : String = version
238295
239-
240296class ReleaseBisect (validationScript : File , shouldFail : Boolean , allReleases : Vector [Release ]):
241297 assert(allReleases.length > 1 , " Need at least 2 releases to bisect" )
242298
243299 private val isGoodReleaseCache = collection.mutable.Map .empty[Release , Boolean ]
244300
245301 def verifyEdgeReleases (): Unit =
246302 println(s " Verifying the first release: ${allReleases.head.version}" )
247- assert(isGoodRelease(allReleases.head), s " The evaluation script unexpectedly failed for the first checked release " )
303+ assert(
304+ isGoodRelease(allReleases.head),
305+ s " The evaluation script unexpectedly failed for the first checked release "
306+ )
248307 println(s " Verifying the last release: ${allReleases.last.version}" )
249- assert(! isGoodRelease(allReleases.last), s " The evaluation script unexpectedly succeeded for the last checked release " )
308+ assert(
309+ ! isGoodRelease(allReleases.last),
310+ s " The evaluation script unexpectedly succeeded for the last checked release "
311+ )
250312
251313 def bisectedGoodAndBadReleases (): (Release , Release ) =
252314 val firstBadRelease = bisect(allReleases)
253- assert(! isGoodRelease(firstBadRelease), s " Bisection error: the 'first bad release' ${firstBadRelease.version} is not a bad release " )
315+ assert(
316+ ! isGoodRelease(firstBadRelease),
317+ s " Bisection error: the 'first bad release' ${firstBadRelease.version} is not a bad release "
318+ )
254319 val lastGoodRelease = firstBadRelease.previous
255- assert(isGoodRelease(lastGoodRelease), s " Bisection error: the 'last good release' ${lastGoodRelease.version} is not a good release " )
320+ assert(
321+ isGoodRelease(lastGoodRelease),
322+ s " Bisection error: the 'last good release' ${lastGoodRelease.version} is not a good release "
323+ )
256324 (lastGoodRelease, firstBadRelease)
257325
258- extension (release : Release ) private def previous : Release =
259- val idx = allReleases.indexOf(release)
260- allReleases(idx - 1 )
326+ extension (release : Release )
327+ private def previous : Release =
328+ val idx = allReleases.indexOf(release)
329+ allReleases(idx - 1 )
261330
262331 private def bisect (releases : Vector [Release ]): Release =
263332 if releases.length == 2 then
@@ -269,31 +338,38 @@ class ReleaseBisect(validationScript: File, shouldFail: Boolean, allReleases: Ve
269338 else bisect(releases.take(releases.length / 2 + 1 ))
270339
271340 private def isGoodRelease (release : Release ): Boolean =
272- isGoodReleaseCache.getOrElseUpdate(release, {
273- println(s " Testing ${release.version}" )
274- val result = Seq (validationScript.getAbsolutePath, release.version).!
275- val isGood = if (shouldFail) result != 0 else result == 0 // invert the process status if failure was expected
276- println(s " Test result: ${release.version} is a ${if isGood then " good" else " bad" } release \n " )
277- isGood
278- })
279-
280- class CommitBisect (validationScript : File , shouldFail : Boolean , lastGoodHash : String , fistBadHash : String ):
281- def bisect (): Unit =
341+ isGoodReleaseCache.getOrElseUpdate(
342+ release, {
343+ println(s " Testing ${release.version}" )
344+ val result = Seq (validationScript.getAbsolutePath, release.version).!
345+ val isGood =
346+ if (shouldFail) result != 0
347+ else result == 0 // invert the process status if failure was expected
348+ println(
349+ s " Test result: ${release.version} is a ${if isGood then " good" else " bad" } release \n "
350+ )
351+ isGood
352+ }
353+ )
354+
355+ class CommitBisect (
356+ validationScript : File ,
357+ compilerDir : Path ,
358+ openCommunityBuildDir : Path ,
359+ shouldFail : Boolean ,
360+ lastGoodHash : String ,
361+ fistBadHash : String
362+ ):
363+ def bisect (): Unit = {
282364 println(s " Starting bisecting commits $lastGoodHash.. $fistBadHash\n " )
283- // Always bootstrapped
284- val scala3CompilerProject = " scala3-compiler-bootstrapped"
285- val scala3Project = " scala3-bootstrapped"
286- val mavenRepo = " https://scala3.westeurope.cloudapp.azure.com/maven2/bisect/"
287- val validationCommandStatusModifier = if shouldFail then " ! " else " " // invert the process status if failure was expected
288- val bisectRunScript = raw """
289- |export NIGHTLYBUILD=yes
290- |scalaVersion= $$ (sbt "print ${scala3CompilerProject}/version" | tail -n1)
291- |rm -r out
292- |sbt "clean; set every sonatypePublishToBundle := Some(\"CommunityBuildRepo\" at \" $mavenRepo\"); ${scala3Project}/publish"
293- | ${validationCommandStatusModifier}${validationScript.getAbsolutePath} " $$ scalaVersion"
294- """ .stripMargin
365+ val scriptFile = ValidationScript .buildCompilerAndValidate(
366+ shouldFail,
367+ validationScript,
368+ openCBDir = openCommunityBuildDir
369+ )
295370 " git bisect start" .!
296371 s " git bisect bad $fistBadHash" .!
297372 s " git bisect good $lastGoodHash" .!
298- Seq (" git" , " bisect" , " run" , " sh" , " -c" , bisectRunScript).!
299- s " git bisect reset " .!
373+ Seq (" git" , " bisect" , " run" , scriptFile.getAbsolutePath()).!
374+ " git bisect reset" .!
375+ }
0 commit comments