diff --git a/sbt/src/main/scala/sbt/FileTask.scala b/sbt/src/main/scala/sbt/FileTask.scala
index 80ba1f9d6..a59883083 100644
--- a/sbt/src/main/scala/sbt/FileTask.scala
+++ b/sbt/src/main/scala/sbt/FileTask.scala
@@ -59,6 +59,7 @@ object FileTasks
def apply[T](label: String, files: ProductsSources, log: Logger)(ifOutofdate: => T)(ifUptodate: => T): T =
{
val products = files.products
+ require(!products.isEmpty, "No products were specified; products must be known in advance.")
existenceCheck[T](label, products, log)(ifOutofdate)
{
val sources = files.sources
diff --git a/sbt/src/main/scala/sbt/FileUtilities.scala b/sbt/src/main/scala/sbt/FileUtilities.scala
index 1474a941e..975f8bf47 100644
--- a/sbt/src/main/scala/sbt/FileUtilities.scala
+++ b/sbt/src/main/scala/sbt/FileUtilities.scala
@@ -1,5 +1,5 @@
/* sbt -- Simple Build Tool
- * Copyright 2008, 2009 Mark Harrah, Nathan Hamblen
+ * Copyright 2008, 2009, 2010 Mark Harrah, Nathan Hamblen, Justin Caballero
*/
package sbt
@@ -57,7 +57,7 @@ object FileUtilities
new Preserved(readOnly(pathMap), tmp)
}
}
-
+
/** Gzips the file 'in' and writes it to 'out'. 'in' cannot be the same file as 'out'. */
def gzip(in: Path, out: Path, log: Logger): Option[String] =
{
@@ -84,7 +84,7 @@ object FileUtilities
}
}
}
-
+
/** Creates a jar file.
* @param sources The files to include in the jar file. The path used for the jar is
* relative to the base directory for the source. That is, the path in the jar for source
@@ -108,7 +108,7 @@ object FileUtilities
* @param log The Logger to use. */
def zip(sources: Iterable[Path], outputZip: Path, recursive: Boolean, log: Logger) =
archive(sources, outputZip, None, recursive, log)
-
+
private def archive(sources: Iterable[Path], outputPath: Path, manifest: Option[Manifest], recursive: Boolean, log: Logger) =
{
log.info("Packaging " + outputPath + " ...")
@@ -129,7 +129,7 @@ object FileUtilities
result
}
}
-
+
private def writeZip(sources: Iterable[Path], output: ZipOutputStream, recursive: Boolean, log: Logger)(createEntry: String => ZipEntry) =
{
def add(source: Path)
@@ -156,7 +156,7 @@ object FileUtilities
sources.foreach(add)
None
}
-
+
private def withZipOutput(file: File, manifest: Option[Manifest], log: Logger)(f: ZipOutputStream => Option[String]): Option[String] =
{
writeStream(file, log)
@@ -194,7 +194,7 @@ object FileUtilities
/** Unzips the contents of the zip file from to the toDirectory directory.*/
def unzip(from: URL, toDirectory: Path, log: Logger): Either[String, Set[Path]] =
unzip(from, toDirectory, AllPassFilter, log)
-
+
/** Unzips the contents of the zip file from to the toDirectory directory.
* Only the entries that match the given filter are extracted. */
def unzip(from: Path, toDirectory: Path, filter: NameFilter, log: Logger): Either[String, Set[Path]] =
@@ -220,6 +220,9 @@ object FileUtilities
private def extract(from: ZipInputStream, toDirectory: Path, filter: NameFilter, log: Logger) =
{
val set = new scala.collection.mutable.HashSet[Path]
+ // don't touch dirs as we unzip because we don't know order of zip entires (any child will
+ // update the dir's time)
+ val dirTimes = new scala.collection.mutable.HashMap[Path, Long]
def next(): Option[String] =
{
val entry = from.getNextEntry
@@ -228,21 +231,23 @@ object FileUtilities
else
{
val name = entry.getName
- val result =
+ val entryErr =
if(filter.accept(name))
{
val target = Path.fromString(toDirectory, name)
log.debug("Extracting zip entry '" + name + "' to '" + target + "'")
- val result =
- if(entry.isDirectory)
- createDirectory(target, log)
- else
+ if(entry.isDirectory)
+ {
+ dirTimes += target -> entry.getTime
+ createDirectory(target, log)
+ }
+ else
+ writeStream(target.asFile, log) { out => FileUtilities.transfer(from, out, log) } orElse
{
set += target
- writeStream(target.asFile, log) { out => FileUtilities.transfer(from, out, log) }
+ touchExisting(target.asFile, entry.getTime, log)
+ None
}
- //target.asFile.setLastModified(entry.getTime)
- result
}
else
{
@@ -250,12 +255,14 @@ object FileUtilities
None
}
from.closeEntry()
- result match { case None => next(); case x => x }
+ entryErr orElse next()
}
}
- next().toLeft(readOnly(set))
+ val result = next()
+ for ((dir, time) <- dirTimes) touchExisting(dir.asFile, time, log)
+ result.toLeft(readOnly(set))
}
-
+
/** Copies all bytes from the given input stream to the given output stream.
* Neither stream is closed.*/
def transfer(in: InputStream, out: OutputStream, log: Logger): Option[String] =
@@ -293,15 +300,18 @@ object FileUtilities
Control.trapUnit("Could not create file " + file + ": ", log)
{
if(file.exists)
- {
- def updateFailBase = "Could not update last modified for file " + file
- Control.trapUnit(updateFailBase + ": ", log)
- { if(file.setLastModified(System.currentTimeMillis)) None else Some(updateFailBase) }
- }
+ touchExisting(file, System.currentTimeMillis, log)
else
createDirectory(file.getParentFile, log) orElse { file.createNewFile(); None }
}
}
+ /** Sets the last mod time on the given {@code file}, which must already exist */
+ private def touchExisting(file: File, time: Long, log: Logger): Option[String] =
+ {
+ def updateFailBase = "Could not update last modified for file " + file
+ Control.trapUnit(updateFailBase + ": ", log)
+ { if(file.setLastModified(time)) None else Some(updateFailBase) }
+ }
/** Creates a directory at the given location.*/
def createDirectory(dir: Path, log: Logger): Option[String] = createDirectory(dir.asFile, log)
/** Creates a directory at the given location.*/
@@ -346,7 +356,7 @@ object FileUtilities
{
val randomName = "sbt_" + java.lang.Integer.toHexString(random.nextInt)
val f = new File(temporaryDirectory, randomName)
-
+
if(createDirectory(f, log).isEmpty)
Right(f)
else
@@ -380,7 +390,7 @@ object FileUtilities
{ file.delete() }
}
}
-
+
/** Copies the files declared in sources to the destinationDirectory
* directory. The source directory hierarchy is flattened so that all copies are immediate
* children of destinationDirectory. Directories are not recursively entered.*/
@@ -435,18 +445,36 @@ object FileUtilities
def download(url: URL, to: File, log: Logger) =
{
readStream(url, log) { inputStream =>
- writeStream(to, log) { outputStream =>
+ writeStream(to, log) { outputStream =>
transfer(inputStream, outputStream, log)
}
}
}
+
+ /**
+ * Equivalent to {@code copy(sources, destinationDirectory, false, log)}.
+ */
+ def copy(sources: Iterable[Path], destinationDirectory: Path, log: Logger): Either[String, Set[Path]] =
+ copy(sources, destinationDirectory, false, log)
+
+ /**
+ * Equivalent to {@code copy(sources, destinationDirectory, overwrite, false, log)}.
+ */
+ def copy(sources: Iterable[Path], destinationDirectory: Path, overwrite: Boolean, log: Logger): Either[String, Set[Path]] =
+ copy(sources, destinationDirectory, overwrite, false, log)
+
/** Copies the files declared in sources to the destinationDirectory
* directory. Directories are not recursively entered. The destination hierarchy matches the
* source paths relative to any base directories. For example:
*
* A source (basePath ##) / x / y is copied to destinationDirectory / x / y.
- * */
- def copy(sources: Iterable[Path], destinationDirectory: Path, log: Logger) =
+ *
+ * @param overwrite if true, existing destination files are always overwritten
+ * @param preserveLastModified if true, the last modified time of copied files will be set equal to
+ * their corresponding source files.
+ */
+ def copy(sources: Iterable[Path], destinationDirectory: Path,
+ overwrite: Boolean, preserveLastModified: Boolean, log: Logger): Either[String, Set[Path]] =
{
val targetSet = new scala.collection.mutable.HashSet[Path]
copyImpl(sources, destinationDirectory, log)
@@ -457,22 +485,27 @@ object FileUtilities
val toPath = Path.fromString(destinationDirectory, source.relativePath)
targetSet += toPath
val to = toPath.asFile
- if(!to.exists || from.lastModified > to.lastModified)
+ if(!to.exists || overwrite || from.lastModified > to.lastModified)
{
- if(from.isDirectory)
- createDirectory(to, log)
+ val result =
+ if(from.isDirectory)
+ createDirectory(to, log)
+ else
+ {
+ log.debug("Copying " + source + " to " + toPath)
+ copyFile(from, to, log)
+ }
+ if (result.isEmpty && preserveLastModified)
+ touchExisting(to, from.lastModified, log)
else
- {
- log.debug("Copying " + source + " to " + toPath)
- copyFile(from, to, log)
- }
+ result
}
else
None
}
}.toLeft(readOnly(targetSet))
}
-
+
/** Copies the files declared in sources to the targetDirectory
* directory. The source directory hierarchy is flattened so that all copies are immediate
* children of targetDirectory. Directories are not recursively entered.*/
@@ -513,7 +546,7 @@ object FileUtilities
}
case Nil => None
}
-
+
Control.trap("Error copying files: ", log) { copyAll(uniquelyNamedSources.toList).toLeft(readOnly(targetSet)) }
}
/** Copies sourceFile to targetFile. If targetFile
@@ -540,7 +573,7 @@ object FileUtilities
}
)
}
-
+
/** Synchronizes the contents of the sourceDirectory directory to the
* targetDirectory directory.*/
def sync(sourceDirectory: Path, targetDirectory: Path, log: Logger): Option[String] =
@@ -559,7 +592,7 @@ object FileUtilities
toRemove.foreach(r => log.debug("Pruning " + r))
clean(toRemove, true, log)
}
-
+
/** Copies the contents of the source directory to the target directory .*/
def copyDirectory(source: Path, target: Path, log: Logger): Option[String] =
copyDirectory(source.asFile, target.asFile, log)
@@ -586,7 +619,7 @@ object FileUtilities
copyDirectory(source, target)
}
-
+
/** Deletes the given file recursively.*/
def clean(file: Path, log: Logger): Option[String] = clean(file :: Nil, log)
/** Deletes the given files recursively.*/
@@ -621,7 +654,7 @@ object FileUtilities
None
}
}
-
+
/** Appends the given String content to the provided file using the default encoding.
* A new file is created if it does not exist.*/
def append(file: File, content: String, log: Logger): Option[String] = append(file, content, Charset.defaultCharset, log)
@@ -629,7 +662,7 @@ object FileUtilities
* A new file is created if it does not exist.*/
def append(file: File, content: String, charset: Charset, log: Logger): Option[String] =
write(file, content, charset, true, log)
-
+
/** Writes the given String content to the provided file using the default encoding.
* If the file exists, it is overwritten.*/
def write(file: File, content: String, log: Logger): Option[String] = write(file, content, Charset.defaultCharset, log)
@@ -644,7 +677,7 @@ object FileUtilities
else
Some("String cannot be encoded by charset " + charset.name)
}
-
+
/** Opens a Writer on the given file using the default encoding,
* passes it to the provided function, and closes the Writer.*/
def write(file: File, log: Logger)(f: Writer => Option[String]): Option[String] =
@@ -655,7 +688,7 @@ object FileUtilities
write(file, charset, false, log)(f)
private def write(file: File, charset: Charset, append: Boolean, log: Logger)(f: Writer => Option[String]): Option[String] =
fileWriter(charset, append).ioOption(file, Writing, log)(f)
-
+
/** Opens a Reader on the given file using the default encoding,
* passes it to the provided function, and closes the Reader.*/
def read(file: File, log: Logger)(f: Reader => Option[String]): Option[String] =
@@ -672,14 +705,14 @@ object FileUtilities
* passes it to the provided function, and closes the Reader.*/
def readValue[R](file: File, charset: Charset, log: Logger)(f: Reader => Either[String, R]): Either[String, R] =
fileReader(charset).io(file, Reading, log)(f)
-
+
/** Reads the contents of the given file into a String using the default encoding.
* The resulting String is wrapped in Right.*/
def readString(file: File, log: Logger): Either[String, String] = readString(file, Charset.defaultCharset, log)
/** Reads the contents of the given file into a String using the given encoding.
* The resulting String is wrapped in Right.*/
def readString(file: File, charset: Charset, log: Logger): Either[String, String] = readValue(file, charset, log)(readString)
-
+
def readString(in: InputStream, log: Logger): Either[String, String] = readString(in, Charset.defaultCharset, log)
def readString(in: InputStream, charset: Charset, log: Logger): Either[String, String] =
streamReader.io((in, charset), Reading, log)(readString)
@@ -713,7 +746,7 @@ object FileUtilities
writeBytes(file, bytes, false, log)
private def writeBytes(file: File, bytes: Array[Byte], append: Boolean, log: Logger): Option[String] =
writeStream(file, append, log) { out => out.write(bytes); None }
-
+
/** Reads the entire file into a byte array. */
def readBytes(file: File, log: Logger): Either[String, Array[Byte]] = readStreamValue(file, log)(readBytes)
def readBytes(in: InputStream, log: Logger): Either[String, Array[Byte]] =
@@ -736,7 +769,7 @@ object FileUtilities
readNext()
Right(out.toByteArray)
}
-
+
/** Opens an OutputStream on the given file with append=true and passes the stream
* to the provided function. The stream is closed before this function returns.*/
def appendStream(file: File, log: Logger)(f: OutputStream => Option[String]): Option[String] =
@@ -763,7 +796,7 @@ object FileUtilities
* to the provided function. The stream is closed before this function returns.*/
def readStreamValue[R](url: URL, log: Logger)(f: InputStream => Either[String, R]): Either[String, R] =
urlInputStream.io(url, Reading, log)(f)
-
+
/** Opens a FileChannel on the given file for writing and passes the channel
* to the given function. The channel is closed before this function returns.*/
def writeChannel(file: File, log: Logger)(f: FileChannel => Option[String]): Option[String] =
@@ -776,13 +809,13 @@ object FileUtilities
* to the given function. The channel is closed before this function returns.*/
def readChannelValue[R](file: File, log: Logger)(f: FileChannel => Either[String, R]): Either[String, R] =
fileInputChannel.io(file, Reading, log)(f)
-
+
private[sbt] def wrapNull(a: Array[File]): Array[File] =
if(a == null)
new Array[File](0)
else
a
-
+
/** Writes the given string to the writer followed by a newline.*/
private[sbt] def writeLine(writer: Writer, line: String)
{
@@ -793,7 +826,7 @@ object FileUtilities
def toFile(url: URL) =
try { new File(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
-
+
/** The directory in which temporary files are placed.*/
val temporaryDirectory = new File(System.getProperty("java.io.tmpdir"))
def classLocation(cl: Class[_]): URL =
@@ -805,11 +838,11 @@ object FileUtilities
def classLocationFile(cl: Class[_]): File = toFile(classLocation(cl))
def classLocation[T](implicit mf: scala.reflect.Manifest[T]): URL = classLocation(mf.erasure)
def classLocationFile[T](implicit mf: scala.reflect.Manifest[T]): File = classLocationFile(mf.erasure)
-
+
lazy val scalaLibraryJar: File = classLocationFile[scala.ScalaObject]
lazy val scalaCompilerJar: File = classLocationFile[scala.tools.nsc.Settings]
def scalaJars: Iterable[File] = List(scalaLibraryJar, scalaCompilerJar)
-
+
/** The producer of randomness for unique name generation.*/
private val random = new java.util.Random
@@ -861,7 +894,7 @@ private object OpenResource
{
private def wrapEither[R](f: R => Option[String]): (R => Either[String, Unit]) = (r: R) => f(r).toLeft(())
private def unwrapEither(e: Either[String, Unit]): Option[String] = e.left.toOption
-
+
def fileOutputStream(append: Boolean) =
new CloseableOpenFile[FileOutputStream] { protected def open(file: File) = new FileOutputStream(file, append) }
def fileInputStream = new CloseableOpenFile[FileInputStream]