Skip to content

Commit 61303b3

Browse files
committed
first feature complete code
1 parent 797c7ee commit 61303b3

File tree

7 files changed

+219
-72
lines changed

7 files changed

+219
-72
lines changed

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ object Contexts {
273273
/** Sourcefile corresponding to given abstract file, memoized */
274274
def getSource(file: AbstractFile, codec: => Codec = Codec(settings.encoding.value)) = {
275275
util.Stats.record("Context.getSource")
276-
base.sources.getOrElseUpdate(file, new SourceFile(file, codec))
276+
base.sources.getOrElseUpdate(file, SourceFile(file, codec))
277277
}
278278

279279
/** SourceFile with given path name, memoized */

compiler/src/dotty/tools/dotc/util/SourceFile.scala

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,36 @@ object ScriptSourceFile {
2222
@sharable private val headerPattern = Pattern.compile("""^(::)?!#.*(\r|\n|\r\n)""", Pattern.MULTILINE)
2323
private val headerStarts = List("#!", "::#!")
2424

25+
/** Return true if has a script header */
26+
def hasScriptHeader(content: Array[Char]): Boolean = {
27+
headerStarts exists (content startsWith _)
28+
}
29+
2530
def apply(file: AbstractFile, content: Array[Char]): SourceFile = {
2631
/** Length of the script header from the given content, if there is one.
27-
* The header begins with "#!" or "::#!" and ends with a line starting
28-
* with "!#" or "::!#".
32+
* The header begins with "#!" or "::#!" and is either a single line,
33+
* or it ends with a line starting with "!#" or "::!#", if present.
2934
*/
3035
val headerLength =
3136
if (headerStarts exists (content startsWith _)) {
37+
// convert initial hash-bang line to a comment
3238
val matcher = headerPattern matcher content.mkString
3339
if (matcher.find) matcher.end
34-
else throw new IOException("script file does not close its header with !# or ::!#")
40+
else content.indexOf('\n') // end of first line
3541
}
3642
else 0
37-
new SourceFile(file, content drop headerLength) {
43+
44+
// overwrite hash-bang lines with all spaces
45+
val hashBangLines = content.take(headerLength).mkString.split("\\r?\\n")
46+
if hashBangLines.nonEmpty then
47+
for i <- 0 until headerLength do
48+
content(i) match {
49+
case '\r' | '\n' =>
50+
case _ =>
51+
content(i) = ' '
52+
}
53+
54+
new SourceFile(file, content) {
3855
override val underlying = new SourceFile(this.file, this.content)
3956
}
4057
}
@@ -201,6 +218,7 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends
201218
override def toString: String = file.toString
202219
}
203220
object SourceFile {
221+
204222
implicit def eqSource: CanEqual[SourceFile, SourceFile] = CanEqual.derived
205223

206224
implicit def fromContext(using Context): SourceFile = ctx.source
@@ -245,6 +263,25 @@ object SourceFile {
245263
else
246264
sourcePath.toString
247265
}
266+
267+
/** Return true if file is a script:
268+
* if filename extension is not .scala and has a script header.
269+
*/
270+
def isScript(file: AbstractFile, content: Array[Char]): Boolean =
271+
if file.hasExtension(".scala") then
272+
false
273+
else
274+
ScriptSourceFile.hasScriptHeader(content)
275+
276+
def apply(file: AbstractFile, codec: Codec): SourceFile =
277+
// see note above re: Files.exists is remarkably slow
278+
val chars = try new String(file.toByteArray, codec.charSet).toCharArray
279+
catch case _: java.nio.file.NoSuchFileException => Array[Char]()
280+
if isScript(file, chars) then
281+
ScriptSourceFile(file, chars)
282+
else
283+
new SourceFile(file, chars)
284+
248285
}
249286

250287
@sharable object NoSource extends SourceFile(NoAbstractFile, Array[Char]()) {
Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,148 @@
11
package dotty.tools.scripting
22

33
import java.io.File
4-
import java.nio.file.Path
4+
import java.nio.file.{Files, Paths, Path}
5+
import dotty.tools.dotc.util.SourceFile
6+
import java.net.{ URL, URLClassLoader }
7+
import java.lang.reflect.{ Modifier, Method }
8+
59

610
/** Main entry point to the Scripting execution engine */
711
object Main:
812
/** All arguments before -script <target_script> are compiler arguments.
913
All arguments afterwards are script arguments.*/
10-
def distinguishArgs(args: Array[String]): (Array[String], File, Array[String]) =
11-
args.foreach { printf("arg[%s]\n",_) }
12-
val (compilerArgs, rest) = args.splitAt(args.indexOf("-script"))
13-
if( rest.isEmpty ){
14+
def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) =
15+
// NOTE: if -script is required but not present, quit with error.
16+
val (leftArgs, rest) = args.splitAt(args.indexOf("-script"))
17+
if( rest.size < 2 ) then
1418
sys.error(s"missing: -script <scriptName>")
15-
}
16-
val file = File(rest.take(1).mkString)
19+
20+
val file = File(rest(1))
1721
val scriptArgs = rest.drop(2)
18-
(compilerArgs, file, scriptArgs)
22+
var saveCompiled = false
23+
val compilerArgs = leftArgs.filter {
24+
case "-save" | "-savecompiled" =>
25+
saveCompiled = true
26+
false
27+
case _ =>
28+
true
29+
}
30+
(compilerArgs, file, scriptArgs, saveCompiled)
1931
end distinguishArgs
2032

33+
val pathsep = sys.props("path.separator")
34+
2135
def main(args: Array[String]): Unit =
22-
val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args)
23-
try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun{ (tmpDir:Path,classpath:String) =>
24-
printf("%s\n",tmpDir.toString)
25-
printf("%s\n",classpath)
36+
val (compilerArgs, scriptFile, scriptArgs, saveCompiled) = distinguishArgs(args)
37+
if verbose then showArgs(args, compilerArgs, scriptFile, scriptArgs)
38+
try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun { (outDir:Path, classpath:String) =>
39+
val classFiles = outDir.toFile.listFiles.toList match {
40+
case Nil => sys.error(s"no files below [$outDir]")
41+
case list => list
42+
}
43+
44+
val (mainClassName, mainMethod) = detectMainMethod(outDir, classpath, scriptFile)
45+
46+
if saveCompiled then
47+
// write a standalone jar to the script parent directory
48+
writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClassName)
49+
50+
try
51+
// invoke the compiled script main method
52+
mainMethod.invoke(null, scriptArgs)
53+
catch
54+
case e: java.lang.reflect.InvocationTargetException =>
55+
throw e.getCause
56+
2657
}
2758
catch
28-
case ScriptingException(msg) =>
29-
println(s"Error: $msg")
59+
case e:Exception =>
60+
e.printStackTrace
61+
println(s"Error: ${e.getMessage}")
3062
sys.exit(1)
63+
64+
def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], classpath:String, mainClassName: String): Unit =
65+
import java.net.{URI, URL}
66+
val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match {
67+
case None => sys.error(s"no parent directory for script file [$scriptFile]")
68+
case Some(parent) => parent
69+
}
70+
71+
val scriptBasename = scriptFile.getName.takeWhile(_!='.')
72+
val jarPath = s"$jarTargetDir/$scriptBasename.jar"
73+
74+
val cpPaths = classpath.split(pathsep).map {
75+
// protect relative paths from being converted to absolute
76+
case str if str.startsWith(".") && File(str).isDirectory => s"${str.withSlash}/"
77+
case str if str.startsWith(".") => str.withSlash
78+
case str => File(str).toURI.toURL.toString
79+
}
80+
81+
import java.util.jar.Attributes.Name
82+
val cpString:String = cpPaths.distinct.mkString(" ")
83+
val manifestAttributes:Seq[(Name, String)] = Seq(
84+
(Name.MANIFEST_VERSION, "1.0.0"),
85+
(Name.MAIN_CLASS, mainClassName),
86+
(Name.CLASS_PATH, cpString),
87+
)
88+
import dotty.tools.io.{Jar, Directory}
89+
val jar = new Jar(jarPath)
90+
val writer = jar.jarWriter(manifestAttributes:_*)
91+
writer.writeAllFrom(Directory(outDir))
92+
end writeJarfile
93+
94+
lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) != None
95+
96+
def showArgs(args:Array[String], compilerArgs:Array[String], scriptFile:File, scriptArgs:Array[String]): Unit =
97+
args.foreach { printf("args[%s]\n", _) }
98+
compilerArgs.foreach { printf("compilerArgs[%s]\n", _) }
99+
scriptArgs.foreach { printf("scriptArgs[%s]\n", _) }
100+
printf("scriptFile[%s]\n", scriptFile)
101+
102+
private def detectMainMethod(outDir: Path, classpath: String, scriptFile: File): (String, Method) =
103+
val outDirURL = outDir.toUri.toURL
104+
val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL)
105+
val cl = URLClassLoader(classpathUrls :+ outDirURL)
106+
107+
def collectMainMethods(target: File, path: String): List[(String, Method)] =
108+
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
109+
val targetPath =
110+
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
111+
else nameWithoutExtension
112+
113+
if verbose then printf("targetPath [%s]\n",targetPath)
114+
115+
if target.isDirectory then
116+
for
117+
packageMember <- target.listFiles.toList
118+
membersMainMethod <- collectMainMethods(packageMember, targetPath)
119+
yield membersMainMethod
120+
else if target.getName.endsWith(".class") then
121+
val cls = cl.loadClass(targetPath)
122+
try
123+
val method = cls.getMethod("main", classOf[Array[String]])
124+
if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil
125+
catch
126+
case _: java.lang.NoSuchMethodException => Nil
127+
else Nil
128+
end collectMainMethods
129+
130+
val candidates = for
131+
file <- outDir.toFile.listFiles.toList
132+
method <- collectMainMethods(file, "")
133+
yield method
134+
135+
candidates match
136+
case Nil =>
137+
if verbose then outDir.toFile.listFiles.toList.foreach { f => System.err.printf("%s\n",f.toString) }
138+
throw ScriptingException(s"No main methods detected in script ${scriptFile}")
139+
case _ :: _ :: _ =>
140+
throw ScriptingException("A script must contain only one main method. " +
141+
s"Detected the following main methods:\n${candidates.mkString("\n")}")
142+
case m :: Nil => m
143+
end match
144+
end detectMainMethod
145+
146+
extension(pathstr:String) {
147+
def withSlash:String = pathstr.replace('\\', '/')
148+
}

compiler/src/dotty/tools/scripting/ScriptingDriver.scala

Lines changed: 22 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,44 @@ import dotty.tools.dotc.config.CompilerCommand
1313
import dotty.tools.io.{ PlainDirectory, Directory }
1414
import dotty.tools.dotc.reporting.Reporter
1515
import dotty.tools.dotc.config.Settings.Setting._
16+
import dotty.tools.dotc.util.ScriptSourceFile
17+
import dotty.tools.io.AbstractFile
1618

1719
import sys.process._
1820

1921
class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
20-
def compileAndRun(pack:(Path,String) => Unit = null): Unit =
22+
def compileAndRun(pack:(Path, String) => Unit = null): Unit =
2123
val outDir = Files.createTempDirectory("scala3-scripting")
2224
val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh)
25+
2326
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
2427
new PlainDirectory(Directory(outDir)))
2528

26-
if doCompile(newCompiler, toCompile).hasErrors then
27-
throw ScriptingException("Errors encountered during compilation")
29+
val result = doCompile(newCompiler, toCompile)
30+
if result.hasErrors then
31+
throw ScriptingException(s"Errors encountered during compilation to dir [$outDir]")
2832

29-
Option(pack) match {
30-
case None =>
31-
case Some(func) =>
32-
func(outDir,ctx.settings.classpath.value)
33-
}
33+
try
34+
if outDir.toFile.listFiles.toList.isEmpty then
35+
sys.error(s"no files generated by compiling script ${scriptFile}")
3436

35-
try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs)
37+
Option(pack) match {
38+
case None =>
39+
case Some(func) =>
40+
val javaClasspath = sys.props("java.class.path")
41+
val pathsep = sys.props("path.separator")
42+
val runtimeClasspath = s"${ctx.settings.classpath.value}$pathsep$javaClasspath"
43+
func(outDir, runtimeClasspath)
44+
}
3645
catch
3746
case e: java.lang.reflect.InvocationTargetException =>
3847
throw e.getCause
3948
finally
4049
deleteFile(outDir.toFile)
50+
51+
def content(file: Path): Array[Char] = new String(Files.readAllBytes(file)).toCharArray
52+
def scriptSource(file: Path) = ScriptSourceFile(AbstractFile.getFile(file), content(file))
53+
4154
end compileAndRun
4255

4356
private def deleteFile(target: File): Unit =
@@ -47,46 +60,6 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
4760
target.delete()
4861
end deleteFile
4962

50-
private def detectMainMethod(outDir: Path, classpath: String): Method =
51-
val outDirURL = outDir.toUri.toURL
52-
val classpathUrls = classpath.split(":").map(File(_).toURI.toURL)
53-
val cl = URLClassLoader(classpathUrls :+ outDirURL)
54-
55-
def collectMainMethods(target: File, path: String): List[Method] =
56-
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
57-
val targetPath =
58-
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
59-
else nameWithoutExtension
60-
61-
if target.isDirectory then
62-
for
63-
packageMember <- target.listFiles.toList
64-
membersMainMethod <- collectMainMethods(packageMember, targetPath)
65-
yield membersMainMethod
66-
else if target.getName.endsWith(".class") then
67-
val cls = cl.loadClass(targetPath)
68-
try
69-
val method = cls.getMethod("main", classOf[Array[String]])
70-
if Modifier.isStatic(method.getModifiers) then List(method) else Nil
71-
catch
72-
case _: java.lang.NoSuchMethodException => Nil
73-
else Nil
74-
end collectMainMethods
75-
76-
val candidates = for
77-
file <- outDir.toFile.listFiles.toList
78-
method <- collectMainMethods(file, "")
79-
yield method
80-
81-
candidates match
82-
case Nil =>
83-
throw ScriptingException("No main methods detected in your script")
84-
case _ :: _ :: _ =>
85-
throw ScriptingException("A script must contain only one main method. " +
86-
s"Detected the following main methods:\n${candidates.mkString("\n")}")
87-
case m :: Nil => m
88-
end match
89-
end detectMainMethod
9063
end ScriptingDriver
9164

9265
case class ScriptingException(msg: String) extends RuntimeException(msg)

compiler/test/dotty/tools/scripting/ScriptingTests.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ScriptingTests:
1616

1717
@Test def scriptingTests =
1818
val testFiles = scripts("/scripting")
19+
testFiles.foreach { (f:File) => System.err.printf("script[%s]\n",f.toString) }
1920

2021
val argss: Map[String, Array[String]] = (
2122
for
@@ -38,6 +39,6 @@ class ScriptingTests:
3839
scriptArgs = scriptArgs
3940
).compileAndRun { (path:java.nio.file.Path,classpath:String) =>
4041
path.toFile.listFiles.foreach { (f:File) => printf(" [%s]\n",f.getName) }
41-
printf("%s\n%s\n",path,classpath)
42+
printf("%s\n%s\n",path,classpath.length)
4243
}
4344

0 commit comments

Comments
 (0)