From fad08500a536d591c557af3016314af2d427a0b9 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Thu, 1 Apr 2010 09:02:10 -0400 Subject: [PATCH] Justin's changes: * preserve modification times when unzipping * error message when products for a fileTask are empty --- sbt/src/main/scala/sbt/FileTask.scala | 1 + sbt/src/main/scala/sbt/FileUtilities.scala | 143 +++++++++++++-------- 2 files changed, 89 insertions(+), 55 deletions(-) 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]