Setup interface project for testing

This commit is contained in:
Mark Harrah 2009-08-18 00:51:08 -04:00
parent 5644b936fe
commit 67e13ad887
18 changed files with 374 additions and 116 deletions

View File

@ -4,37 +4,11 @@ import xsbti.{AnalysisCallback, Logger}
import java.io.File
import java.net.URLClassLoader
/** A component manager provides access to the pieces of xsbt that are distributed as components.
* There are two types of components. The first type is compiled subproject jars with their dependencies.
* The second type is a subproject distributed as a source jar so that it can be compiled against a specific
* version of Scala.
*
* The component manager provides services to install and retrieve components to the local repository.
* This is used for source jars so that the compilation need not be repeated for other projects on the same
* machine.
*/
trait ComponentManager extends NotNull
{
def directory(id: String): File =
{
error("TODO: implement")
}
def jars(id: String): Iterable[File] =
{
val dir = directory(id)
if(dir.isDirectory)
FileUtilities.jars(dir)
else
Nil
}
}
/** Interface to the Scala compiler. This class uses the Scala library and compiler obtained through the 'scalaLoader' class
* loader. This class requires a ComponentManager in order to obtain the interface code to scalac and the analysis plugin. Because
* these call Scala code for a different Scala version, they must be compiled for the version of Scala being used.
* It is essential that the provided 'scalaVersion' be a 1:1 mapping to the actual version of Scala being used for compilation
* (-SNAPSHOT is bad). Otherwise, binary compatibility issues will ensue!*/
* (-SNAPSHOT is not acceptable). Otherwise, binary compatibility issues will ensue!*/
class Compiler(scalaLoader: ClassLoader, val scalaVersion: String, private[xsbt] val manager: ComponentManager)
{
// this is the instance used to compile the analysis
@ -46,8 +20,10 @@ class Compiler(scalaLoader: ClassLoader, val scalaVersion: String, private[xsbt]
def apply(arguments: Seq[String])
{
// reflection is required for binary compatibility
// The following inputs ensure there is a compile error if the class names change, but they should not actually be used
import scala.tools.nsc.{CompilerCommand, FatalError, Global, Settings, reporters, util}
// The following import ensures there is a compile error if the class name changes,
// but it should not be otherwise directly referenced
import scala.tools.nsc.Main
val mainClass = Class.forName("scala.tools.nsc.Main", true, scalaLoader)
val main = mainClass.asInstanceOf[{def process(args: Array[String]): Unit }]
main.process(arguments.toArray)
@ -56,82 +32,17 @@ class Compiler(scalaLoader: ClassLoader, val scalaVersion: String, private[xsbt]
/** Interface to the compiler that uses the dependency analysis plugin.*/
object analysis
{
/** The compiled plugin jar. This will be passed to scalac as a compiler plugin.*/
private lazy val analyzerJar =
componentCompiler("analyzerPlugin").toList match
{
case x :: Nil => x
case Nil => error("Analyzer plugin component not found")
case xs => error("Analyzer plugin component must be a single jar (was: " + xs.mkString(", ") + ")")
}
/** The compiled interface jar. This is used to configure and call the compiler. It redirects logging and sets up
* the dependency analysis plugin.*/
private lazy val interfaceJars = componentCompiler("compilerInterface")
/** The jar containing the compiled plugin and the compiler interface code. This will be passed to scalac as a compiler plugin
* and used to load the class that actually interfaces with Global.*/
private lazy val interfaceJar = componentCompiler(ComponentCompiler.compilerInterfaceID)
def apply(arguments: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger)
{
val argsWithPlugin = ("-Xplugin:" + analyzerJar.getAbsolutePath) :: arguments.toList
val interfaceLoader = new URLClassLoader(interfaceJars.toSeq.map(_.toURI.toURL).toArray, scalaLoader)
val argsWithPlugin = ("-Xplugin:" + interfaceJar.getAbsolutePath) :: arguments.toList
val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), scalaLoader)
val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance
val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit }]
runnable.run(argsWithPlugin.toArray, callback, maximumErrors, log) // safe to pass across the ClassLoader boundary because the types are defined in Java
}
def forceInitialization() {interfaceJars; analyzerJar}
}
}
class ComponentCompiler(compiler: Compiler)
{
import compiler.{manager, scalaVersion}
val xsbtiID = "xsbti"
lazy val xsbtiJars =
{
val js = manager.jars(xsbtiID)
if(js.isEmpty)
error("Could not find required xsbti component")
else
js
}
import FileUtilities.{copy, createDirectory, zip, jars, unzip, withTemporaryDirectory}
def apply(id: String): Iterable[File] =
{
val binID = id + "-bin_" + scalaVersion
val binaryDirectory = manager.directory(binID)
if(binaryDirectory.isDirectory)
jars(binaryDirectory)
else
{
createDirectory(binaryDirectory)
val srcID = id + "-src"
val srcDirectory = manager.directory(srcID)
if(srcDirectory.isDirectory)
{
val targetJar = new File(binaryDirectory, id + ".jar")
compileSources(srcDirectory, compiler, targetJar, id)
Seq(targetJar)
}
else
notFound(id)
}
}
private def notFound(id: String) = error("Couldn't find xsbt source component " + id + " for Scala " + scalaVersion)
private def compileSources(srcDirectory: File, compiler: Compiler, targetJar: File, id: String)
{
val sources = jars(srcDirectory)
if(sources.isEmpty)
notFound(id)
else
{
withTemporaryDirectory { dir =>
val extractedSources = (Set[File]() /: sources) { (extracted, sourceJar)=> extracted ++ unzip(sourceJar, dir) }
val (sourceFiles, resources) = extractedSources.partition(_.getName.endsWith(".scala"))
withTemporaryDirectory { outputDirectory =>
val arguments = Seq("-d", outputDirectory.getAbsolutePath, "-cp", xsbtiJars.mkString(File.pathSeparator)) ++ sourceFiles.toSeq.map(_.getAbsolutePath)
compiler.raw(arguments)
copy(resources, outputDirectory, PathMapper.relativeTo(dir))
zip(Seq(outputDirectory), targetJar, true, PathMapper.relativeTo(outputDirectory))
}
}
}
def forceInitialization() {interfaceJar }
}
}

View File

@ -0,0 +1,49 @@
package xsbt
import java.io.File
object ComponentCompiler
{
val xsbtiID = "xsbti"
val compilerInterfaceID = "compilerInterface"
}
class ComponentCompiler(compiler: Compiler)
{
import compiler.{manager, scalaVersion}
import ComponentCompiler._
lazy val xsbtiJars = manager.files(xsbtiID)
import FileUtilities.{copy, createDirectory, zip, jars, unzip, withTemporaryDirectory}
def apply(id: String): File =
{
val binID = id + "-bin_" + scalaVersion
try { manager.file(binID) }
catch { case e: Exception => compileAndInstall(id, binID) }
}
private def compileAndInstall(id: String, binID: String): File =
{
val srcID = id + "-src_" + scalaVersion
val binaryDirectory = manager.location(binID)
createDirectory(binaryDirectory)
val targetJar = new File(binaryDirectory, id + ".jar")
compileSources(manager.files(srcID), compiler, targetJar, id)
manager.cache(binID)
targetJar
}
/** Extract sources from source jars, compile them with the xsbti interfaces on the classpath, and package the compiled classes and
* any resources from the source jars into a final jar.*/
private def compileSources(sourceJars: Iterable[File], compiler: Compiler, targetJar: File, id: String)
{
withTemporaryDirectory { dir =>
val extractedSources = (Set[File]() /: sourceJars) { (extracted, sourceJar)=> extracted ++ unzip(sourceJar, dir) }
val (sourceFiles, resources) = extractedSources.partition(_.getName.endsWith(".scala"))
withTemporaryDirectory { outputDirectory =>
val arguments = Seq("-d", outputDirectory.getAbsolutePath, "-cp", xsbtiJars.mkString(File.pathSeparator)) ++ sourceFiles.toSeq.map(_.getAbsolutePath)
compiler.raw(arguments)
copy(resources, outputDirectory, PathMapper.relativeTo(dir))
zip(Seq(outputDirectory), targetJar, true, PathMapper.relativeTo(outputDirectory))
}
}
}
}

View File

@ -18,7 +18,7 @@ class Analyzer(val global: Global) extends Plugin
import global._
val name = "sbt-analyzer"
val name = "xsbt-analyze"
val description = "A plugin to find all concrete instances of a given class and extract dependency information."
val components = List[PluginComponent](Component)

View File

@ -0,0 +1,4 @@
<plugin>
<name>xsbt-analyze</name>
<classname>xsbt.Analyzer</classname>
</plugin>

View File

@ -0,0 +1,36 @@
package xsbt
import java.io.File
import org.specs.Specification
object CheckBasic extends Specification
{
"Compiling basic file should succeed" in {
val name = new File("Basic.scala")
WithFiles( name -> "package org.example { object Basic }" ){ files =>
TestCompile(files){ loader => Class.forName("org.example.Basic", false, loader) }
}
}
"Analysis plugin" should {
"send source begin and end" in {
val name = new File("Basic.scala")
WithFiles(name -> "object Basic" ) { files =>
CallbackTest(files) { callback =>
(callback.beganSources) must haveTheSameElementsAs(files)
(callback.endedSources) must haveTheSameElementsAs(files)
}
}
}
"detect applications" in {
val name = new File("Main.scala")
WithFiles(name -> "object Main { def main(args: Array[String]) {} }" ) { files =>
CallbackTest(files) { callback =>
println(callback.applications)
(callback.applications) must haveTheSameElementsAs(files.map(file => (file, "Main")))
}
}
}
}
}

View File

@ -0,0 +1,55 @@
package xsbt
import java.io.File
import java.net.URLClassLoader
import xsbti.{Logger, TestCallback, TestLogger}
import FileUtilities.{classLocationFile, withTemporaryDirectory, write}
object TestCompile
{
def apply[T](arguments: Seq[String], superclassNames: Seq[String])(f: (TestCallback, Logger) => T): T =
{
val pluginLocation = classLocationFile[Analyzer]
assert(pluginLocation.exists)
val path = pluginLocation.getAbsolutePath
val pluginArg = if(pluginLocation.getName.endsWith(".jar")) List("-Xplugin:" + path) else List("-Xpluginsdir", path)
val testCallback = new TestCallback(superclassNames.toArray)
val i = new CompilerInterface
val newArgs = "-Xplugin-require:xsbt-analyze" :: pluginArg ::: arguments.toList
TestLogger { log =>
i.run(newArgs.toArray, testCallback, 5, log)
f(testCallback, log)
}
}
def apply[T](sources: Seq[File])(f: ClassLoader => T): T =
CallbackTest.apply(sources, Nil){ case (callback, outputDir, log) => f(new URLClassLoader(Array(outputDir.toURI.toURL))) }
}
object CallbackTest
{
def apply[T](sources: Iterable[File])(f: TestCallback => T): T =
apply(sources.toSeq, Nil){ case (callback, outputDir, log) => f(callback) }
def apply[T](sources: Seq[File], superclassNames: Seq[String])(f: (TestCallback, File, Logger) => T): T =
{
withTemporaryDirectory { outputDir =>
val newArgs = "-d" :: outputDir.getAbsolutePath :: sources.map(_.getAbsolutePath).toList
TestCompile(newArgs, superclassNames) { case (callback, log) => f(callback, outputDir, log) }
}
}
}
object WithFiles
{
def apply[T](sources: (File, String)*)(f: Seq[File] => T): T =
{
withTemporaryDirectory { dir =>
val sourceFiles =
for((file, content) <- sources) yield
{
assert(!file.isAbsolute)
val to = new File(dir, file.getPath)
write(to, content)
to
}
f(sourceFiles)
}
}
}

View File

@ -0,0 +1,11 @@
/* sbt -- Simple Build Tool
* Copyright 2009 Mark Harrah
*/
package xsbti;
public interface Versions
{
public static final String Sbt = "0.7";
public static final int Interface = 1;
public static final int BootInterface = 1;
}

View File

@ -0,0 +1,6 @@
package xsbti
object f0
{
def apply[T](s: => T) = new F0[T] { def apply = s }
}

View File

@ -0,0 +1,28 @@
package xsbti
import java.io.File
import scala.collection.mutable.ArrayBuffer
class TestCallback(val superclassNames: Array[String]) extends AnalysisCallback
{
val invalidSuperclasses = new ArrayBuffer[String]
val beganSources = new ArrayBuffer[File]
val endedSources = new ArrayBuffer[File]
val foundSubclasses = new ArrayBuffer[(File, String, String, Boolean)]
val sourceDependencies = new ArrayBuffer[(File, File)]
val jarDependencies = new ArrayBuffer[(File, File)]
val classDependencies = new ArrayBuffer[(File, File)]
val products = new ArrayBuffer[(File, File)]
val applications = new ArrayBuffer[(File, String)]
def superclassNotFound(superclassName: String) { invalidSuperclasses += superclassName }
def beginSource(source: File) { beganSources += source }
def foundSubclass(source: File, subclassName: String, superclassName: String, isModule: Boolean): Unit =
foundSubclasses += ((source, subclassName, superclassName, isModule))
def sourceDependency(dependsOn: File, source: File) { sourceDependencies += ((dependsOn, source)) }
def jarDependency(jar: File, source: File) { jarDependencies += ((jar, source)) }
def classDependency(clazz: File, source: File) { classDependencies += ((clazz, source)) }
def generatedClass(source: File, module: File) { products += ((source, module)) }
def endSource(source: File) { endedSources += source }
def foundApplication(source: File, className: String) { applications += ((source, className)) }
}

View File

@ -0,0 +1,25 @@
package xsbti
class TestLogger extends Logger
{
private val buffer = new scala.collection.mutable.ArrayBuffer[F0[Unit]]
def info(msg: F0[String]) = buffer("[info] ", msg)
def warn(msg: F0[String]) = buffer("[warn] ", msg)
def debug(msg: F0[String]) = buffer("[debug] ", msg)
def error(msg: F0[String]) = buffer("[error] ", msg)
def verbose(msg: F0[String]) = buffer("[verbose] ", msg)
def show() { buffer.foreach(_()) }
def clear() { buffer.clear() }
def trace(t: F0[Throwable]) { buffer += f0(t().printStackTrace) }
private def buffer(s: String, msg: F0[String]) { buffer += f0(println(s + msg())) }
}
object TestLogger
{
def apply[T](f: Logger => T): T =
{
val log = new TestLogger
try { f(log) }
catch { case e: Exception => log.show(); throw e }
finally { log.clear() }
}
}

View File

@ -0,0 +1,45 @@
package xsbt
import java.io.File
import xsbti.Versions
/** A component manager provides access to the pieces of xsbt that are distributed as components.
* There are two types of components. The first type is compiled subproject jars with their dependencies.
* The second type is a subproject distributed as a source jar so that it can be compiled against a specific
* version of Scala.
*
* The component manager provides services to install and retrieve components to the local repository.
* This is used for source jars so that the compilation need not be repeated for other projects on the same
* machine.
*/
class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull
{
def location(id: String): File = new File(baseDirectory, id)
def directory(id: String): File =
{
val dir = location(id)
if(!dir.exists)
update(id)
dir
}
private def contents(dir: File): Seq[File] =
{
val fs = dir.listFiles
if(fs == null) Nil else fs
}
def files(id: String): Iterable[File] =
{
val fs = contents(directory(id))
if(!fs.isEmpty) fs else error("Could not find required component '" + id + "'")
}
def file(id: String): File =
files(id).toList match {
case x :: Nil => x
case xs => error("Expected single file for component '" + id + "', found: " + xs.mkString(", "))
}
def update(id: String): Unit =
IvyActions.basicRetrieveLocal(sbtModuleID("manager"), Seq(sbtModuleID(id)), location(id), log)
def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, Versions.Sbt)
def cache(id: String): Unit = IvyActions.basicPublishLocal(sbtModuleID(id), Nil, files(id), log)
}

View File

@ -14,7 +14,7 @@ import plugins.repository.Resource
import plugins.repository.url.URLResource
/** Subclasses the default Ivy file parser in order to provide access to protected methods.*/
private object CustomXmlParser extends XmlModuleDescriptorParser with NotNull
private[xsbt] object CustomXmlParser extends XmlModuleDescriptorParser with NotNull
{
import XmlModuleDescriptorParser.Parser
class CustomParser(settings: IvySettings) extends Parser(CustomXmlParser, settings) with NotNull

View File

@ -18,6 +18,30 @@ final class UpdateConfiguration(val retrieveDirectory: File, val outputPattern:
object IvyActions
{
def basicPublishLocal(moduleID: ModuleID, dependencies: Iterable[ModuleID], artifactFiles: Iterable[File], log: IvyLogger)
{
val artifacts = artifactFiles.map(Artifact.defaultArtifact)
val (ivy, local) = basicLocalIvy(log)
val module = new ivy.Module(ModuleConfiguration(moduleID, dependencies, artifacts))
val srcArtifactPatterns = artifactFiles.map(_.getAbsolutePath)
publish(module, local.name, srcArtifactPatterns, None, None)
}
def basicRetrieveLocal(moduleID: ModuleID, dependencies: Iterable[ModuleID], to: File, log: IvyLogger)
{
val (ivy, local) = basicLocalIvy(log)
val module = new ivy.Module(ModuleConfiguration(moduleID, dependencies, Nil))
val up = new UpdateConfiguration(to, defaultOutputPattern, false, true)
update(module, up)
}
def defaultOutputPattern = "[artifact]-[revision](-[classifier]).[ext]"
private def basicLocalIvy(log: IvyLogger) =
{
val local = Resolver.defaultLocal
val paths = new IvyPaths(new File("."), None)
val conf = new IvyConfiguration(paths, Seq(local), log)
(new IvySbt(conf), local)
}
/** Clears the Ivy cache, as configured by 'config'. */
def cleanCache(ivy: IvySbt) = ivy.withIvy { _.getSettings.getRepositoryCacheManagers.foreach(_.clean()) }

View File

@ -18,6 +18,8 @@ final class ModuleConfiguration(val module: ModuleID, val dependencies: Iterable
}
object ModuleConfiguration
{
def apply(module: ModuleID, dependencies: Iterable[ModuleID], artifacts: Iterable[Artifact]) =
new ModuleConfiguration(module, dependencies, NodeSeq.Empty, Nil, None, None, artifacts, false)
def configurations(explicitConfigurations: Iterable[Configuration], defaultConfiguration: Option[Configuration]) =
if(explicitConfigurations.isEmpty)
{

View File

@ -311,15 +311,22 @@ object Artifact
def apply(name: String, url: URL): Artifact =Artifact(name, extract(url, defaultType), extract(url, defaultExtension), None, Nil, Some(url))
val defaultExtension = "jar"
val defaultType = "jar"
private[this] def extract(url: URL, default: String) =
private[this] def extract(url: URL, default: String): String = extract(url.toString, default)
private[this] def extract(name: String, default: String): String =
{
val s = url.toString
val i = s.lastIndexOf('.')
val i = name.lastIndexOf('.')
if(i >= 0)
s.substring(i+1)
name.substring(i+1)
else
default
}
def defaultArtifact(file: File) =
{
val name = file.getName
val i = name.lastIndexOf('.')
val base = if(i >= 0) name.substring(0, i) else name
Artifact(name, extract(name, defaultType), extract(name, defaultExtension), None, Nil, Some(file.toURI.toURL))
}
}
/*
object Credentials

View File

@ -5,7 +5,7 @@ package xsbt
import org.apache.ivy.util.{Message, MessageLogger}
trait IvyLogger
trait IvyLogger extends NotNull
{
def info(msg: => String)
def debug(msg: => String)

View File

@ -2,9 +2,6 @@ import sbt._
class XSbt(info: ProjectInfo) extends ParentProject(info)
{
def utilPath = path("util")
def compilePath = path("compile")
val commonDeps = project("common", "Dependencies", new CommonDependencies(_))
val interfaceSub = project("interface", "Interface", new InterfaceProject(_))
@ -13,19 +10,23 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
val ioSub = project(utilPath / "io", "IO", new Base(_), controlSub, commonDeps)
val classpathSub = project(utilPath / "classpath", "Classpath", new Base(_))
val analysisPluginSub = project(compilePath / "plugin", "Analyzer Compiler Plugin", new Base(_), interfaceSub)
val compilerInterfaceSub = project(compilePath / "interface", "Compiler Interface", new Base(_), interfaceSub)
val compilerInterfaceSub = project(compilePath / "interface", "Compiler Interface", new CompilerInterfaceProject(_), interfaceSub)
val ivySub = project("ivy", "Ivy", new IvyProject(_))
val ivySub = project("ivy", "Ivy", new IvyProject(_), interfaceSub)
val logSub = project(utilPath / "log", "Logging", new Base(_))
val taskSub = project("tasks", "Tasks", new TaskProject(_), controlSub, collectionSub, commonDeps)
val cacheSub = project("cache", "Cache", new CacheProject(_), taskSub, ioSub)
val compilerSub = project(compilePath, "Compile", new Base(_), interfaceSub, ivySub, analysisPluginSub, ioSub, compilerInterfaceSub)
val compilerSub = project(compilePath, "Compile", new Base(_), interfaceSub, ivySub, ioSub, compilerInterfaceSub)
class CommonDependencies(info: ProjectInfo) extends ParentProject(info)
def utilPath = path("util")
def compilePath = path("compile")
class CommonDependencies(info: ProjectInfo) extends DefaultProject(info)
{
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
val sp = "org.scala-tools.testing" % "specs" % "1.5.0" % "test->default"
val ju = "junit" % "junit" % "4.5" % "test->default" // required by specs to compile properly
}
override def parallelExecution = true
@ -44,7 +45,22 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
}
class InterfaceProject(info: ProjectInfo) extends DefaultProject(info)
{
override def sourceExtensions: NameFilter = "*.java"
override def mainSources = descendents(mainSourceRoots, "*.java")
override def compileOrder = CompileOrder.JavaThenScala
}
class CompilerInterfaceProject(info: ProjectInfo) extends Base(info) with SourceProject
{
// these set up the test so that the classes and resources are both in the output resource directory
// the main output path is removed so that the plugin (xsbt.Analyzer) is found in the output resource directory so that
// the tests can configure that directory as -Xpluginsdir (which requires the scalac-plugin.xml and the classes to be together)
override def testCompileAction = super.testCompileAction dependsOn(packageForTest)
override def mainResources = super.mainResources +++ "scalac-plugin.xml"
override def testClasspath = (super.testClasspath --- super.mainCompilePath) +++ ioSub.testClasspath +++ testPackagePath
def testPackagePath = outputPath / "test.jar"
lazy val packageForTest = packageTask(mainClasses +++ mainResources, testPackagePath, packageOptions).dependsOn(compile)
}
}
trait SourceProject extends BasicScalaProject
{
override def packagePaths = packageSourcePaths
}

View File

@ -7,9 +7,12 @@ import OpenResource._
import ErrorHandling.translate
import java.io.{File, FileInputStream, InputStream, OutputStream}
import java.net.{URISyntaxException, URL}
import java.nio.charset.Charset
import java.util.jar.{Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest}
import java.util.zip.{GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOutputStream}
import scala.collection.mutable.HashSet
import scala.reflect.{Manifest => SManifest}
object FileUtilities
{
@ -22,6 +25,31 @@ object FileUtilities
private val BufferSize = 8192
private val Newline = System.getProperty("line.separator")
def classLocation(cl: Class[_]): URL =
{
val codeSource = cl.getProtectionDomain.getCodeSource
if(codeSource == null) error("No class location for " + cl)
else codeSource.getLocation
}
def classLocationFile(cl: Class[_]): File = toFile(classLocation(cl))
def classLocation[T](implicit mf: SManifest[T]): URL = classLocation(mf.erasure)
def classLocationFile[T](implicit mf: SManifest[T]): File = classLocationFile(mf.erasure)
def toFile(url: URL) =
try { new File(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
// "base.extension" -> (base, extension)
def split(name: String): (String, String) =
{
val lastDot = name.lastIndexOf('.')
if(lastDot >= 0)
(name.substring(0, lastDot), name.substring(lastDot+1))
else
(name, "")
}
def createDirectory(dir: File): Unit =
translate("Could not create directory " + dir + ": ")
{
@ -294,4 +322,15 @@ object FileUtilities
}
}
}
def defaultCharset = Charset.forName("UTF-8")
def write(toFile: File, content: String): Unit = write(toFile, content, defaultCharset)
def write(toFile: File, content: String, charset: Charset): Unit = write(toFile, content, charset, false)
def write(file: File, content: String, charset: Charset, append: Boolean)
{
if(charset.newEncoder.canEncode(content))
fileWriter(charset, append)(file) { w => w.write(content); None }
else
error("String cannot be encoded by charset " + charset.name)
}
}