Skip to content

Commit

Permalink
FIX #276 creating directories as necessary and specify top level dir
Browse files Browse the repository at this point in the history
and sadly realizing that apache commons compress is still the best bet
  • Loading branch information
muuki88 committed Feb 14, 2015
1 parent bb0e38f commit 2c39fc4
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 41 deletions.
78 changes: 63 additions & 15 deletions src/main/scala/com/typesafe/sbt/packager/universal/Archives.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,44 @@ import sbt._
/** Helper methods to package up files into compressed archives. */
object Archives {

/** Makes a zip file in the given target directory using the given name. */
def makeZip(target: File, name: String, mappings: Seq[(File, String)]): File = {
/**
* Makes a zip file in the given target directory using the given name.
*
* @param target folder to build package in
* @param name of output (without extension)
* @param mappings included in the output
* @param top level directory
* @return zip file
*/
def makeZip(target: File, name: String, mappings: Seq[(File, String)], top: Option[String]): File = {
val zip = target / (name + ".zip")
// TODO - If mappings already start with the given name, don't add it?
val m2 = mappings map { case (f, p) => f -> (name + "/" + p) }

// add top level directory if defined
val m2 = top map { dir =>
mappings map { case (f, p) => f -> (dir + "/" + p) }
} getOrElse (mappings)

ZipHelper.zip(m2, zip)
zip
}

/** Makes a zip file in the given target directory using the given name. */
def makeNativeZip(target: File, name: String, mappings: Seq[(File, String)]): File = {
/**
* Makes a zip file in the given target directory using the given name.
*
* @param target folder to build package in
* @param name of output (without extension)
* @param mappings included in the output
* @param top level directory
* @return zip file
*/
def makeNativeZip(target: File, name: String, mappings: Seq[(File, String)], top: Option[String]): File = {
val zip = target / (name + ".zip")
// TODO - If mappings already start with the given name, don't add it?
val m2 = mappings map { case (f, p) => f -> (name + "/" + p) }

// add top level directory if defined
val m2 = top map { dir =>
mappings map { case (f, p) => f -> (dir + "/" + p) }
} getOrElse (mappings)

ZipHelper.zipNative(m2, zip)
zip
}
Expand All @@ -29,8 +53,14 @@ object Archives {
* Makes a dmg file in the given target directory using the given name.
*
* Note: Only works on OSX
*
* @param target folder to build package in
* @param name of output (without extension)
* @param mappings included in the output
* @param top level directory : NOT USED
* @return dmg file
*/
def makeDmg(target: File, name: String, mappings: Seq[(File, String)]): File = {
def makeDmg(target: File, name: String, mappings: Seq[(File, String)], top: Option[String]): File = {
val t = target / "dmg"
val dmg = target / (name + ".dmg")
if (!t.isDirectory) IO.createDirectory(t)
Expand Down Expand Up @@ -113,25 +143,43 @@ object Archives {
val makeTgz = makeTarball(gzip, ".tgz") _
val makeTar = makeTarball(identity, ".tar") _

/** Helper method used to construct tar-related compression functions. */
def makeTarball(compressor: File => File, ext: String)(target: File, name: String, mappings: Seq[(File, String)]): File = {
/**
* Helper method used to construct tar-related compression functions.
* @param target folder to build package in
* @param name of output (without extension)
* @param mappings included in the output
* @param top level directory
* @return tar file
*
*/
def makeTarball(compressor: File => File, ext: String)(target: File, name: String, mappings: Seq[(File, String)], top: Option[String]): File = {
val relname = name
val tarball = target / (name + ext)
IO.withTemporaryDirectory { f =>
val rdir = f / relname
val m2 = mappings map { case (f, p) => f -> (rdir / name / p) }
val m2 = top map { dir =>
mappings map { case (f, p) => f -> (rdir / dir / p) }
} getOrElse {
mappings map { case (f, p) => f -> (rdir / p) }
}

IO.copy(m2)
// TODO - Is this enough?
for (f <- (m2 map { case (_, f) => f }); if f.getAbsolutePath contains "/bin/") {
println("Making " + f.getAbsolutePath + " executable")
f.setExecutable(true, false)
}

IO.createDirectory(tarball.getParentFile)
val distdir = IO.listFiles(rdir).headOption.getOrElse {
sys.error("Unable to find tarball in directory: " + rdir.getAbsolutePath + ".\n This could be an issue with the temporary filesystem used to create tarballs.")

// all directories that should be zipped
val distdirs = top map (_ :: Nil) getOrElse {
IO.listFiles(rdir).map(_.getName).toList // no top level dir, use all available
}

val tmptar = f / (relname + ".tar")
Process(Seq("tar", "-pcvf", tmptar.getAbsolutePath, distdir.getName), Some(rdir)).! match {

Process(Seq("tar", "-pcvf", tmptar.getAbsolutePath) ++ distdirs, Some(rdir)).! match {
case 0 => ()
case n => sys.error("Error tarballing " + tarball + ". Exit code: " + n)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ trait UniversalKeys {
val stage = TaskKey[File]("stage", "Create a local directory with all the files laid out as they would be in the final distribution.")
val dist = TaskKey[File]("dist", "Creates the distribution packages.")
val stagingDirectory = SettingKey[File]("stagingDirectory", "Directory where we stage distributions/releases.")
val topLevelDirectory = SettingKey[Option[String]]("topLevelDirectory", "Top level dir in compressed output file.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ object UniversalPlugin extends AutoPlugin {
name in UniversalDocs <<= name in Universal,
name in UniversalSrc <<= name in Universal,
packageName in Universal <<= packageName,
topLevelDirectory := Some((packageName in Universal).value),
executableScriptName in Universal <<= executableScriptName
) ++
makePackageSettingsForConfig(Universal) ++
Expand Down Expand Up @@ -95,12 +96,12 @@ object UniversalPlugin extends AutoPlugin {
dist
}

private type Packager = (File, String, Seq[(File, String)]) => File
private type Packager = (File, String, Seq[(File, String)], Option[String]) => File
/** Creates packaging settings for a given package key, configuration + archive type. */
private[this] def makePackageSettings(packageKey: TaskKey[File], config: Configuration)(packager: Packager): Seq[Setting[_]] =
inConfig(config)(Seq(
mappings in packageKey <<= mappings map checkMappings,
packageKey <<= (target, packageName, mappings in packageKey) map packager
packageKey <<= (target, packageName, mappings in packageKey, topLevelDirectory) map packager
))

/** check that all mapped files actually exist */
Expand Down
93 changes: 70 additions & 23 deletions src/main/scala/com/typesafe/sbt/packager/universal/ZipHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import scala.collection.JavaConverters._
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/io/fsp/zipfilesystemprovider.html
*/
object ZipHelper {
case class FileMapping(file: File, name: String)
case class FileMapping(file: File, name: String, unixMode: Option[Int] = None)

/**
* Creates a zip file attempting to give files the appropriate unix permissions using Java 6 APIs.
Expand Down Expand Up @@ -54,7 +54,7 @@ object ZipHelper {
}

/**
* Creates a zip file attempting to give files the appropriate unix permissions using Java 7 APIs.
* Creates a zip file with the apache commons compressor library.
*
* Note: This is known to have some odd issues on MacOSX whereby executable permissions
* are not actually discovered, even though the Info-Zip headers exist and work on
Expand All @@ -64,10 +64,73 @@ object ZipHelper {
* @param outputZip The location of the output file.
*/
def zip(sources: Traversable[(File, String)], outputZip: File): Unit = {
val mappings =
for {
(file, name) <- sources.toSeq
// TODO - Figure out if this is good enough....
perm = if (file.isDirectory || file.canExecute) 0755 else 0644
} yield FileMapping(file, name, Some(perm))
archive(mappings, outputZip)
}

/**
* Creates a zip file attempting to give files the appropriate unix permissions using Java 7 APIs.
*
* @param sources The files to include in the zip file.
* @param outputZip The location of the output file.
*/
def zipNIO(sources: Traversable[(File, String)], outputZip: File): Unit = {
require(!outputZip.isDirectory, "Specified output file " + outputZip + " is a directory.")
val mappings = sources.toSeq.map {
case (file, name) => FileMapping(file, name)
}
archive(mappings, outputZip)

// make sure everything is available
val outputDir = outputZip.getParentFile
IO createDirectory outputDir

// zipping the sources into the output zip
withZipFilesystem(outputZip) { system =>
mappings foreach {
case FileMapping(dir, name, _) if dir.isDirectory => Files createDirectories (system getPath name)
case FileMapping(file, name, _) =>
val dest = system getPath name
// create parent directories if available
Option(dest.getParent) foreach (Files createDirectories _)
Files copy (file.toPath, dest, StandardCopyOption.COPY_ATTRIBUTES)
}
}
}

private def archive(sources: Seq[FileMapping], outputFile: File): Unit = {
if (outputFile.isDirectory) sys.error("Specified output file " + outputFile + " is a directory.")
else {
val outputDir = outputFile.getParentFile
IO createDirectory outputDir
withZipOutput(outputFile) { output =>
for (FileMapping(file, name, mode) <- sources; if !file.isDirectory) {
val entry = new ZipArchiveEntry(file, normalizePath(name))
// Now check to see if we have permissions for this sucker.
mode foreach (entry.setUnixMode)
output putArchiveEntry entry
// TODO - Write file into output?
IOUtils.copy(new java.io.FileInputStream(file), output)
output.closeArchiveEntry()
}
}
}
}

/**
* using apache commons compress
*/
private def withZipOutput(file: File)(f: ZipArchiveOutputStream => Unit): Unit = {
val zipOut = new ZipArchiveOutputStream(file)
zipOut setLevel Deflater.BEST_COMPRESSION
try { f(zipOut) }
finally {
zipOut.close()
}
}

/**
Expand All @@ -84,32 +147,15 @@ object ZipHelper {
}

/**
* Opens a zip filesystem and creates the file if necessary.
*
*/
private def archive(sources: Seq[FileMapping], outputFile: File): Unit = {
require(!outputFile.isDirectory, "Specified output file " + outputFile + " is a directory.")

// make sure everything is available
val outputDir = outputFile.getParentFile
IO createDirectory outputDir

// zipping the sources into the output zip
withZipFilesystem(outputFile) { system =>
sources foreach {
case FileMapping(dir, name) if dir.isDirectory => Files createDirectories (system getPath name)
case FileMapping(file, name) => Files copy (file.toPath, system getPath name, StandardCopyOption.COPY_ATTRIBUTES)
}
}

}

/**
* Opens a zip filesystem and creates the file if neccessary
* Note: This will override an existing zipFile if existent!
*
* @param zipFile
* @param f: FileSystem => Unit, logic working in the filesystem
*/
def withZipFilesystem(zipFile: File)(f: FileSystem => Unit) {
Files deleteIfExists zipFile.toPath
val env = Map("create" -> "true").asJava
val uri = URI.create("jar:file:" + zipFile.getAbsolutePath)

Expand All @@ -120,4 +166,5 @@ object ZipHelper {
system.close()
}
}

}
7 changes: 7 additions & 0 deletions src/sbt-test/universal/test-zips-no-top-level-dir/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enablePlugins(JavaAppPackaging)

name := "simple-test"

version := "0.1.0"

topLevelDirectory := None
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test configuration to include in zips.
14 changes: 14 additions & 0 deletions src/sbt-test/universal/test-zips-no-top-level-dir/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Run the zip packaging.
> show universal:package-bin
$ exists target/universal/simple-test-0.1.0.zip

# Run the tgz packaging.
> universal:package-zip-tarball
$ exists target/universal/simple-test-0.1.0.tgz

# Run the txz packaging.
> universal:package-xz-tarball
$ exists target/universal/simple-test-0.1.0.txz


# TODO - Check contents of zips
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ class ZipHelperSpec extends FlatSpec with Matchers with BeforeAndAfterAll {
}
}

it should "create directories if necessary" in {
// setup
val out = tmp resolve "dir-creation.zip"
val file = tmp resolve "dir-file.txt"
Files createFile file

ZipHelper.zip(List(file.toFile -> "dir/file.txt"), out.toFile)

ZipHelper.withZipFilesystem(out.toFile) { system =>
val zDir = system getPath "dir"
Files exists zDir should be(true)
Files isDirectory zDir should be(true)

val zFile = zDir resolve "file.txt"
Files exists zFile should be(true)
Files isDirectory zFile should be(false)
}

}

/*
* This is currently not possible.
*/
Expand Down
2 changes: 1 addition & 1 deletion test-project-simple/project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=0.13.7-M3
sbt.version=0.13.7

0 comments on commit 2c39fc4

Please sign in to comment.