11// Based on https://github.com/lampepfl/dotty/blob/main/project/scripts/bisect.scala
2-
3- /*
4- This script will bisect a problem with the compiler based on success/failure of the validation script passed as an argument.
5- It starts with a fast bisection on released nightly builds.
6- Then it will bisect the commits between the last nightly that worked and the first nightly that failed.
7- Look at the `usageMessage` below for more details.
8- */
9-
10-
11-
2+ //> using lib " com.github.scopt::scopt:4.1.0"
3+ //> using scala 3.3
124
135import sys .process ._
146import scala .io .Source
157import java .io .File
168import java .nio .file .attribute .PosixFilePermissions
179import java .nio .charset .StandardCharsets
18- import java .nio .file .Files
19-
20- // --extra-scalac-options ${{ inputs.extra-scalac-options }} \
21- // --disabled-scalac-options ${{ inputs.disabled-scalac-options }} \
22- // --community-build-dir ${{ github.workspace }}/opencb
10+ import java .nio .file ._
2311
24- val usageMessage = """
25- |Usage:
26- | > scala-cli project/scripts/bisect.scala -- [<bisect-options>] <projectName> <targets>*
27- |
28- |The optional <bisect-options> may be any combination of:
29- |* --dry-run
30- | Don't try to bisect - just make sure the validation command works correctly
31- |
32- |* --extra-scalac-options <options>
33- | Comma delimited of additional scalacOptions passed to project build
34- |
35- |* --disabled-scalac-options <options>
36- | Comma delimited of disabled scalacOptions passed to project build
37- |
38- |* --community-build-dir <path>
39- | Directory with community build project from which the project config would be resolved
40- |
41- |* --compiler-dir <path>
42- | Directory containing Scala compiler repository, required for commit-based bissect
43- |
44- |* --releases <releases-range>
45- | Bisect only releases from the given range (defaults to all releases).
46- | The range format is <first>..<last>, where both <first> and <last> are optional, e.g.
47- | * 3.1.0-RC1-bin-20210827-427d313-NIGHTLY..3.2.1-RC1-bin-20220716-bb9c8ff-NIGHTLY
48- | * 3.2.1-RC1-bin-20220620-de3a82c-NIGHTLY..
49- | * ..3.3.0-RC1-bin-20221124-e25362d-NIGHTLY
50- | The ranges are treated as inclusive.
51- |
52- |* --should-fail
53- | Expect the validation command to fail rather that succeed. This can be used e.g. to find out when some illegal code started to compile.
54- |
55- |Warning: The bisect script should not be run multiple times in parallel because of a potential race condition while publishing artifacts locally.
56-
57- """ .stripMargin
12+ val communityBuildVersion = " v0.2.4"
5813
5914@ main def run (args : String * ): Unit =
60- val scriptOptions =
61- try ScriptOptions .fromArgs(args)
62- catch
63- case _ =>
64- sys.error(s " Wrong script parameters. \n ${usageMessage}" )
65-
66- val validationScript = scriptOptions.validationCommand.validationScript
67- val releases = Releases .fromRange(scriptOptions.releasesRange)
68- val releaseBisect = ReleaseBisect (validationScript, shouldFail = scriptOptions.shouldFail, releases)
15+ val config = scopt.OParser
16+ .parse(Config .parser, args, Config ())
17+ .getOrElse(sys.error(" Failed to parse config" ))
18+
19+ val validationScript = config.validationScript
20+ val releases = Releases .fromRange(config.releasesRange)
21+ val releaseBisect = ReleaseBisect (validationScript, shouldFail = config.shouldFail, releases)
6922
7023 releaseBisect.verifyEdgeReleases()
7124
72- if (! scriptOptions .dryRun) then
25+ if (! config .dryRun) then
7326 val (lastGoodRelease, firstBadRelease) = releaseBisect.bisectedGoodAndBadReleases()
7427 println(s " Last good release: ${lastGoodRelease.version}" )
7528 println(s " First bad release: ${firstBadRelease.version}" )
7629 println(" \n Finished bisecting releases\n " )
7730
78- val commitBisect = CommitBisect (validationScript, shouldFail = scriptOptions .shouldFail, lastGoodRelease.hash, firstBadRelease.hash)
31+ val commitBisect = CommitBisect (validationScript, shouldFail = config .shouldFail, lastGoodRelease.hash, firstBadRelease.hash)
7932 commitBisect.bisect()
8033
81-
82- case class ScriptOptions (validationCommand : ValidationCommand , dryRun : Boolean , releasesRange : ReleasesRange , shouldFail : Boolean )
83- object ScriptOptions :
84- def fromArgs (args : Seq [String ]) =
85- val defaultOptions = ScriptOptions (
86- validationCommand = null ,
87- dryRun = false ,
88- ReleasesRange (first = None , last = None ),
89- shouldFail = false
90- )
91- parseArgs(args, defaultOptions)
92-
93- private def parseArgs (args : Seq [String ], options : ScriptOptions ): ScriptOptions =
94- println(s " parse: $args" )
95- args match
96- case " --dry-run" :: argsRest => parseArgs(argsRest, options.copy(dryRun = true ))
97- case " --releases" :: argsRest =>
98- val range = ReleasesRange .tryParse(argsRest.head).get
99- parseArgs(argsRest.tail, options.copy(releasesRange = range))
100- case " --should-fail" :: argsRest => parseArgs(argsRest, options.copy(shouldFail = true ))
101- case args =>
102- val command = ValidationCommand .fromArgs(args)
103- options.copy(validationCommand = command)
104-
105- case class ValidationCommand (projectName : String , openCommunityBuildDir : File , targets : Seq [String ]):
106- val remoteValidationScript : File = ValidationScript .buildProject(
107- projectName = projectName,
108- targets = Option .when(targets.nonEmpty)(targets.mkString(" " )),
109- extraScalacOptions = " " ,
110- disabledScalacOption= " " ,
111- runId = " test" ,
112- buildURL= " " ,
113- executeTests = false
34+ case class ValidationCommand (projectName : String = " " , targets : String = " " , extraScalacOptions : String = " " , disabledScalacOption : String = " " )
35+ case 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))
44+
45+ lazy val remoteValidationScript : File = ValidationScript .buildProject(
46+ projectName = command.projectName,
47+ targets = Option (command.targets).filter(_.nonEmpty),
48+ extraScalacOptions = command.extraScalacOptions,
49+ disabledScalacOption= command.disabledScalacOption,
50+ runId = s " bisect- ${command.projectName}" ,
51+ buildURL= " " ,
52+ executeTests = false ,
53+ openCBDir = openCommunityBuildDir
11454 )
115- val validationScript : File = ValidationScript .dockerRunBuildProject(projectName, remoteValidationScript, openCommunityBuildDir)
116-
117- object ValidationCommand :
118- def fromArgs (args : Seq [String ]) =
119- args match
120- case Seq (projectName, openCBDir, targets* ) => ValidationCommand (projectName, new File (openCBDir), targets)
121-
55+ lazy val validationScript : File =
56+ require(Files .exists(openCommunityBuildDir), " Open CB dir does not exist" )
57+ require(Files .exists(compilerDir), " Compiler dir does not exist" )
58+ ValidationScript .dockerRunBuildProject(command.projectName, remoteValidationScript, openCommunityBuildDir.toFile())
59+ }
60+
61+ object Config {
62+ val parser = {
63+ import scopt .OParser
64+ val builder = OParser .builder[Config ]
65+ import builder .*
66+ OParser .sequence(
67+ head(" Scala 3 Open Community Build bisect" , communityBuildVersion),
68+ opt[Unit ](" dry-run" )
69+ .action:
70+ (_, c) => c.copy(dryRun = true )
71+ .text(" Don't try to bisect - just make sure the validation command works correctly" ),
72+ opt[String ](" releases" )
73+ .action:
74+ (v, c) => c.copy(releasesRange = ReleasesRange .tryParse(v).getOrElse(c.releasesRange))
75+ .text(" Bisect only releases from the given range 'first..last' (defaults to all releases)" ),
76+ opt[Unit ](" should-fail" )
77+ .action:
78+ (_, c) => c.copy(shouldFail = true )
79+ .text(" Expect the validation command to fail rather that succeed. This can be used e.g. to find out when some illegal code started to compile" ),
80+ opt[String ](" project-name" )
81+ .action:
82+ (v, c) => c.withCommand(_.copy(projectName = v ))
83+ .text(" Name of the project to run using GitHub coordinates" )
84+ .required(),
85+ opt[String ](" targets" )
86+ .action:
87+ (v, c) => c.withCommand(_.copy(targets = v))
88+ .text(" Comma delimited list of targets to limit scope of project building" ),
89+ opt[String ](" extra-scalac-options" )
90+ .action:
91+ (v, c) => c.withCommand(_.copy(extraScalacOptions = v))
92+ .text(" Extra scalac options passed to project build" ),
93+ opt[String ](" disabled-scalac-options" )
94+ .action:
95+ (v, c) => c.withCommand(_.copy(disabledScalacOption = v))
96+ .text(" Filtered out scalac options passed to project build" ),
97+ opt[String ](" community-build-dir" )
98+ .action:
99+ (v, c) => c.copy(openCommunityBuildDir = Path .of(v))
100+ .text(" Directory with community build project from which the project config would be resolved" )
101+ .required(),
102+ opt[String ](" compiler-dir" )
103+ .action:
104+ (v, c) => c.copy(compilerDir = Path .of(v))
105+ .text(" Directory containing Scala compiler repository, required for commit-based bissect" )
106+ .required()
107+ ,
108+ checkConfig { c =>
109+ 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" )
111+ else success
112+ }
113+ )
114+ }
115+ }
122116
123117object ValidationScript :
124- def buildProject (projectName : String , targets : Option [String ], extraScalacOptions : String , disabledScalacOption : String , runId : String , buildURL : String , executeTests : Boolean ): File = tmpScript{
118+ def buildProject (projectName : String , targets : Option [String ], extraScalacOptions : String , disabledScalacOption : String , runId : String , buildURL : String , executeTests : Boolean , openCBDir : Path ): File = tmpScript(openCBDir) {
125119 val configPatch =
126120 if executeTests
127121 then " "
@@ -159,31 +153,16 @@ object ValidationScript:
159153 | " $extraScalacOptions" \
160154 | " $disabledScalacOption"
161155 |
162- |#/build/feed-elastic.sh \
163- |# 'https://scala3.westeurope.cloudapp.azure.com/data' \
164- |# " ${projectName}" \
165- |# " $$ (cat build-status.txt)" \
166- |# " $$ (date --iso-8601=seconds)" \
167- |# build-summary.txt \
168- |# build-logs.txt \
169- |# " $$ (config .version)" \
170- |# " $$ {scalaVersion}" \
171- |# " ${runId}" \
172- |# " ${buildURL}"
173- |#
174- |#if [ $$ ? != 0 ]; then
175- |# echo "::warning title=Indexing failure::Indexing results of ${projectName} failed"
176- |#fi
177- |
178156 |grep -q "success" build-status.txt;
179157 |exit $$ ?
180158 """ .stripMargin
181159 }
182160
183161 def dockerRunBuildProject (projectName : String , validationScript : File , openCBDir : File ): File =
162+ val scriptsPath = " /scripts/"
184163 val validationScriptPath = " /scripts/validationScript.sh"
185- val imageVersion = " v0.2.4 "
186- tmpScript(raw """
164+ assert( Files .exists(validationScript.toPath()))
165+ tmpScript(openCBDir.toPath)( raw """
187166 |#!/usr/bin/env bash
188167 |set -e
189168 |scalaVersion= $$ 1
@@ -193,13 +172,13 @@ object ValidationScript:
193172 |docker run --rm \
194173 | -v ${validationScript.getAbsolutePath()}: $validationScriptPath \
195174 | -v ${openCBDir.getAbsolutePath()}:/opencb/ \
196- | virtuslab/scala-community-build-project-builder:jdk $$ {javaVersion}- $imageVersion \
197- | /bin/bash $validationScriptPath $$ scalaVersion
175+ | virtuslab/scala-community-build-project-builder:jdk $$ {javaVersion}- $communityBuildVersion \
176+ | /bin/bash -c " $validationScriptPath $$ scalaVersion"
198177 """ .stripMargin)
199-
200- private def tmpScript (content : String ): File =
178+
179+ private def tmpScript (openCBDir : Path )( content : String ): File =
201180 val executableAttr = PosixFilePermissions .asFileAttribute(PosixFilePermissions .fromString(" rwxr-xr-x" ))
202- val tmpPath = Files .createTempFile(" scala-bisect-validator" , " " , executableAttr)
181+ val tmpPath = Files .createTempFile(openCBDir, " scala-bisect-validator" , " .sh " , executableAttr)
203182 val tmpFile = tmpPath.toFile
204183
205184 print(s " Bisecting with validation script: ${tmpPath.toAbsolutePath}\n " )
0 commit comments