@@ -8,58 +8,203 @@ Look at the `usageMessage` below for more details.
8
8
9
9
import sys .process ._
10
10
import scala .io .Source
11
- import Releases .Release
12
11
import java .io .File
12
+ import java .nio .file .attribute .PosixFilePermissions
13
+ import java .nio .charset .StandardCharsets
14
+ import java .nio .file .Files
13
15
14
16
val usageMessage = """
15
17
|Usage:
16
- | > scala-cli project/scripts/bisect.scala -- < validation-script >
18
+ | > scala-cli project/scripts/bisect.scala -- [<bisect-options>] < validation-command >
17
19
|
18
- |The validation script should be executable and accept a single parameter, which will be the scala version to validate.
20
+ |The <validation-command> should be one of:
21
+ |* compile <arg1> <arg2> ...
22
+ |* run <arg1> <arg2> ...
23
+ |* <custom-validation-script-path>
24
+ |
25
+ |The arguments for 'compile' and 'run' should be paths to the source file(s) and optionally additional options passed directly to scala-cli.
26
+ |
27
+ |A custom validation script should be executable and accept a single parameter, which will be the scala version to validate.
19
28
|Look at bisect-cli-example.sh and bisect-expect-example.exp for reference.
20
- |Don't use the example scripts modified in place as they might disappear from the repo during a checkout.
21
- |Instead copy them to a different location first.
29
+ |If you want to use one of the example scripts - use a copy of the file instead of modifying it in place because that might mess up the checkout.
22
30
|
23
- |Warning: The bisect script should not be run multiple times in parallel because of a potential race condition while publishing artifacts locally.
31
+ |The optional <bisect-options> may be any combination of:
32
+ |* --dry-run
33
+ | Don't try to bisect - just make sure the validation command works correctly
34
+ |* --releases <releases-range>
35
+ | Bisect only releases from the given range (defaults to all releases).
36
+ | The range format is <first>...<last>, where both <first> and <last> are optional, e.g.
37
+ | * 3.1.0-RC1-bin-20210827-427d313-NIGHTLY..3.2.1-RC1-bin-20220716-bb9c8ff-NIGHTLY
38
+ | * 3.2.1-RC1-bin-20220620-de3a82c-NIGHTLY..
39
+ | * ..3.3.0-RC1-bin-20221124-e25362d-NIGHTLY
40
+ | The ranges are treated as inclusive.
41
+ |* --bootstrapped
42
+ | Publish locally and test a bootstrapped compiler rather than a nonboostrapped one
24
43
|
25
- |Tip: Before running the bisect script run the validation script manually with some published versions of the compiler to make sure it succeeds and fails as expected.
44
+ |Warning: The bisect script should not be run multiple times in parallel because of a potential race condition while publishing artifacts locally.
45
+
26
46
""" .stripMargin
27
47
28
- @ main def dottyCompileBisect (args : String * ): Unit =
29
- val validationScriptPath = args match
30
- case Seq (path) =>
31
- (new File (path)).getAbsolutePath.toString
32
- case _ =>
33
- println(" Wrong script parameters." )
34
- println()
35
- println(usageMessage)
36
- System .exit(1 )
37
- null
38
-
39
- val releaseBisect = ReleaseBisect (validationScriptPath)
40
- val bisectedBadRelease = releaseBisect.bisectedBadRelease(Releases .allReleases)
41
- println(" \n Finished bisecting releases\n " )
42
-
43
- bisectedBadRelease match
44
- case Some (firstBadRelease) =>
45
- firstBadRelease.previous match
46
- case Some (lastGoodRelease) =>
47
- println(s " Last good release: $lastGoodRelease" )
48
- println(s " First bad release: $firstBadRelease" )
49
- val commitBisect = CommitBisect (validationScriptPath)
50
- commitBisect.bisect(lastGoodRelease.hash, firstBadRelease.hash)
51
- case None =>
52
- println(s " No good release found " )
53
- case None =>
54
- println(s " No bad release found " )
55
-
56
- class ReleaseBisect (validationScriptPath : String ):
57
- def bisectedBadRelease (releases : Vector [Release ]): Option [Release ] =
58
- Some (bisect(releases : Vector [Release ]))
59
- .filter(! isGoodRelease(_))
60
-
61
- def bisect (releases : Vector [Release ]): Release =
62
- assert(releases.length > 1 , " Need at least 2 releases to bisect" )
48
+ @ main def run (args : String * ): Unit =
49
+ val scriptOptions =
50
+ try ScriptOptions .fromArgs(args)
51
+ catch
52
+ case _ =>
53
+ sys.error(s " Wrong script parameters. \n ${usageMessage}" )
54
+
55
+ val validationScript = scriptOptions.validationCommand.validationScript
56
+ val releases = Releases .fromRange(scriptOptions.releasesRange)
57
+ val releaseBisect = ReleaseBisect (validationScript, releases)
58
+
59
+ releaseBisect.verifyEdgeReleases()
60
+
61
+ if (! scriptOptions.dryRun) then
62
+ val (lastGoodRelease, firstBadRelease) = releaseBisect.bisectedGoodAndBadReleases()
63
+ println(s " Last good release: ${lastGoodRelease.version}" )
64
+ println(s " First bad release: ${firstBadRelease.version}" )
65
+ println(" \n Finished bisecting releases\n " )
66
+
67
+ val commitBisect = CommitBisect (validationScript, bootstrapped = scriptOptions.bootstrapped, lastGoodRelease.hash, firstBadRelease.hash)
68
+ commitBisect.bisect()
69
+
70
+
71
+ case class ScriptOptions (validationCommand : ValidationCommand , dryRun : Boolean , bootstrapped : Boolean , releasesRange : ReleasesRange )
72
+ object ScriptOptions :
73
+ def fromArgs (args : Seq [String ]) =
74
+ val defaultOptions = ScriptOptions (
75
+ validationCommand = null ,
76
+ dryRun = false ,
77
+ bootstrapped = false ,
78
+ ReleasesRange (first = None , last = None )
79
+ )
80
+ parseArgs(args, defaultOptions)
81
+
82
+ private def parseArgs (args : Seq [String ], options : ScriptOptions ): ScriptOptions =
83
+ args match
84
+ case " --dry-run" :: argsRest => parseArgs(argsRest, options.copy(dryRun = true ))
85
+ case " --bootstrapped" :: argsRest => parseArgs(argsRest, options.copy(bootstrapped = true ))
86
+ case " --releases" :: argsRest =>
87
+ val range = ReleasesRange .tryParse(argsRest.head).get
88
+ parseArgs(argsRest.tail, options.copy(releasesRange = range))
89
+ case _ =>
90
+ val command = ValidationCommand .fromArgs(args)
91
+ options.copy(validationCommand = command)
92
+
93
+ enum ValidationCommand :
94
+ case Compile (args : Seq [String ])
95
+ case Run (args : Seq [String ])
96
+ case CustomValidationScript (scriptFile : File )
97
+
98
+ def validationScript : File = this match
99
+ case Compile (args) =>
100
+ ValidationScript .tmpScalaCliScript(command = " compile" , args)
101
+ case Run (args) =>
102
+ ValidationScript .tmpScalaCliScript(command = " run" , args)
103
+ case CustomValidationScript (scriptFile) =>
104
+ ValidationScript .copiedFrom(scriptFile)
105
+
106
+ object ValidationCommand :
107
+ def fromArgs (args : Seq [String ]) = args match
108
+ case Seq (" compile" , commandArgs* ) => Compile (commandArgs)
109
+ case Seq (" run" , commandArgs* ) => Run (commandArgs)
110
+ case Seq (path) => CustomValidationScript (new File (path))
111
+
112
+
113
+ object ValidationScript :
114
+ def copiedFrom (file : File ): File =
115
+ val fileContent = scala.io.Source .fromFile(file).mkString
116
+ tmpScript(fileContent)
117
+
118
+ def tmpScalaCliScript (command : String , args : Seq [String ]): File = tmpScript(s """
119
+ |#!/usr/bin/env bash
120
+ |scala-cli ${command} -S " $$ 1" --server=false ${args.mkString(" " )}
121
+ | """ .stripMargin
122
+ )
123
+
124
+ private def tmpScript (content : String ): File =
125
+ val executableAttr = PosixFilePermissions .asFileAttribute(PosixFilePermissions .fromString(" rwxr-xr-x" ))
126
+ val tmpPath = Files .createTempFile(" scala-bisect-validator" , " " , executableAttr)
127
+ val tmpFile = tmpPath.toFile
128
+
129
+ print(s " Bisecting with validation script: ${tmpPath.toAbsolutePath}\n " )
130
+ print(" #####################################\n " )
131
+ print(s " ${content}\n\n " )
132
+ print(" #####################################\n\n " )
133
+
134
+ tmpFile.deleteOnExit()
135
+ Files .write(tmpPath, content.getBytes(StandardCharsets .UTF_8 ))
136
+ tmpFile
137
+
138
+
139
+ case class ReleasesRange (first : Option [String ], last : Option [String ]):
140
+ def filter (releases : Seq [Release ]) =
141
+ def releaseIndex (version : String ): Int =
142
+ val index = releases.indexWhere(_.version == version)
143
+ assert(index > 0 , s " ${version} matches no nightly compiler release " )
144
+ index
145
+
146
+ val startIdx = first.map(releaseIndex(_)).getOrElse(0 )
147
+ val endIdx = last.map(releaseIndex(_) + 1 ).getOrElse(releases.length)
148
+ val filtered = releases.slice(startIdx, endIdx).toVector
149
+ assert(filtered.nonEmpty, " No matching releases" )
150
+ filtered
151
+
152
+ object ReleasesRange :
153
+ def all = ReleasesRange (None , None )
154
+ def tryParse (range : String ): Option [ReleasesRange ] = range match
155
+ case s " ${first}... ${last}" => Some (ReleasesRange (
156
+ Some (first).filter(_.nonEmpty),
157
+ Some (last).filter(_.nonEmpty)
158
+ ))
159
+ case _ => None
160
+
161
+ class Releases (val releases : Vector [Release ])
162
+
163
+ object Releases :
164
+ lazy val allReleases : Vector [Release ] =
165
+ val re = raw """ (?<=title=")(.+-bin-\d{8}-\w{7}-NIGHTLY)(?=/") """ .r
166
+ val html = Source .fromURL(" https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/" )
167
+ re.findAllIn(html.mkString).map(Release .apply).toVector
168
+
169
+ def fromRange (range : ReleasesRange ): Vector [Release ] = range.filter(allReleases)
170
+
171
+ case class Release (version : String ):
172
+ private val re = raw " .+-bin-(\d{8})-(\w{7})-NIGHTLY " .r
173
+ def date : String =
174
+ version match
175
+ case re(date, _) => date
176
+ case _ => sys.error(s " Could not extract date from release name: $version" )
177
+ def hash : String =
178
+ version match
179
+ case re(_, hash) => hash
180
+ case _ => sys.error(s " Could not extract hash from release name: $version" )
181
+
182
+ override def toString : String = version
183
+
184
+
185
+ class ReleaseBisect (validationScript : File , allReleases : Vector [Release ]):
186
+ assert(allReleases.length > 1 , " Need at least 2 releases to bisect" )
187
+
188
+ private val isGoodReleaseCache = collection.mutable.Map .empty[Release , Boolean ]
189
+
190
+ def verifyEdgeReleases (): Unit =
191
+ println(s " Verifying the first release: ${allReleases.head.version}" )
192
+ assert(isGoodRelease(allReleases.head), s " The evaluation script unexpectedly failed for the first checked release " )
193
+ println(s " Verifying the last release: ${allReleases.last.version}" )
194
+ assert(! isGoodRelease(allReleases.last), s " The evaluation script unexpectedly succeeded for the last checked release " )
195
+
196
+ def bisectedGoodAndBadReleases (): (Release , Release ) =
197
+ val firstBadRelease = bisect(allReleases)
198
+ assert(! isGoodRelease(firstBadRelease), s " Bisection error: the 'first bad release' ${firstBadRelease.version} is not a bad release " )
199
+ val lastGoodRelease = firstBadRelease.previous
200
+ assert(isGoodRelease(lastGoodRelease), s " Bisection error: the 'last good release' ${lastGoodRelease.version} is not a good release " )
201
+ (lastGoodRelease, firstBadRelease)
202
+
203
+ extension (release : Release ) private def previous : Release =
204
+ val idx = allReleases.indexOf(release)
205
+ allReleases(idx - 1 )
206
+
207
+ private def bisect (releases : Vector [Release ]): Release =
63
208
if releases.length == 2 then
64
209
if isGoodRelease(releases.head) then releases.last
65
210
else releases.head
@@ -69,44 +214,24 @@ class ReleaseBisect(validationScriptPath: String):
69
214
else bisect(releases.take(releases.length / 2 + 1 ))
70
215
71
216
private def isGoodRelease (release : Release ): Boolean =
72
- println(s " Testing ${release.version}" )
73
- val result = Seq (validationScriptPath, release.version).!
74
- val isGood = result == 0
75
- println(s " Test result: ${release.version} is a ${if isGood then " good" else " bad" } release \n " )
76
- isGood
77
-
78
- object Releases :
79
- lazy val allReleases : Vector [Release ] =
80
- val re = raw " (?<=title= $" )(.+-bin-\d{8}-\w{7}-NIGHTLY)(?=/ $" ) " .r
81
- val html = Source .fromURL(" https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/" )
82
- re.findAllIn(html.mkString).map(Release .apply).toVector
217
+ isGoodReleaseCache.getOrElseUpdate(release, {
218
+ println(s " Testing ${release.version}" )
219
+ val result = Seq (validationScript.getAbsolutePath, release.version).!
220
+ val isGood = result == 0
221
+ println(s " Test result: ${release.version} is a ${if isGood then " good" else " bad" } release \n " )
222
+ isGood
223
+ })
83
224
84
- case class Release (version : String ):
85
- private val re = raw " .+-bin-(\d{8})-(\w{7})-NIGHTLY " .r
86
- def date : String =
87
- version match
88
- case re(date, _) => date
89
- case _ => sys.error(s " Could not extract date from version $version" )
90
- def hash : String =
91
- version match
92
- case re(_, hash) => hash
93
- case _ => sys.error(s " Could not extract hash from version $version" )
94
-
95
- def previous : Option [Release ] =
96
- val idx = allReleases.indexOf(this )
97
- if idx == 0 then None
98
- else Some (allReleases(idx - 1 ))
99
-
100
- override def toString : String = version
101
-
102
- class CommitBisect (validationScriptPath : String ):
103
- def bisect (lastGoodHash : String , fistBadHash : String ): Unit =
225
+ class CommitBisect (validationScript : File , bootstrapped : Boolean , lastGoodHash : String , fistBadHash : String ):
226
+ def bisect (): Unit =
104
227
println(s " Starting bisecting commits $lastGoodHash.. $fistBadHash\n " )
228
+ val scala3CompilerProject = if bootstrapped then " scala3-compiler-bootstrapped" else " scala3-compiler"
229
+ val scala3Project = if bootstrapped then " scala3-bootstrapped" else " scala3"
105
230
val bisectRunScript = s """
106
- |scalaVersion= $$ (sbt "print scala3-compiler-bootstrapped /version" | tail -n1)
231
+ |scalaVersion= $$ (sbt "print ${scala3CompilerProject} /version" | tail -n1)
107
232
|rm -r out
108
- |sbt "clean; scala3-bootstrapped /publishLocal"
109
- | $validationScriptPath " $$ scalaVersion"
233
+ |sbt "clean; ${scala3Project} /publishLocal"
234
+ | ${validationScript.getAbsolutePath} " $$ scalaVersion"
110
235
""" .stripMargin
111
236
" git bisect start" .!
112
237
s " git bisect bad $fistBadHash" .!
0 commit comments