diff --git a/README.md b/README.md index 0169597d1..35d39e1a2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Add the following to your `project/plugins.sbt` or `~/.sbt/plugins.sbt` file: resolvers += Resolver.url("scalasbt", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns) - addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.5.4") + addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.6.0-symlink-3") Then, in the project you wish to use the plugin, add the following settings: @@ -25,51 +25,26 @@ or Using the sbt-native-packger plugin requires a bit of understanding of the underlying packaging mechanisms for each operating system it supports. The [generated documentation](http://scala-sbt.org/sbt-native-packager) for the plugin is still a work in progress. -Here's an example excerpt for the debian + rpm package of [sbt-extras](http://github.com/paulp/sbt-extras) project: +Here's an example excerpt for the native packaging of [sbt-launcher-packge](http://github.com/sbt/sbt-launcher-package) project: // GENERAL LINUX PACKAGING STUFFS maintainer := "Josh Suereth ", packageSummary := "Simple Build Tool for Scala-driven builds", packageDescription := """This script provides a native way to run the Simple Build Tool, - a build tool for Scala software, also called SBT.""", - linuxPackageMappings <+= (baseDirectory) map { bd => - (packageMapping((bd / "sbt") -> "/usr/bin/sbt") - withUser "root" withGroup "root" withPerms "0755") + a build tool for Scala software, also called SBT.""", + // Here we remove the jar file and launch lib from the symlinks: + linuxPackageSymlinks <<= linuxPackageSymlinks map { links => + for { + link <- links + if !(link.destination endsWith "sbt-launch-lib.bash") + if !(link.destination endsWith "sbt-launch.jar") + } yield link }, - linuxPackageMappings <+= (sourceDirectory) map { bd => - (packageMapping( - (bd / "linux" / "usr/share/man/man1/sbt.1") -> "/usr/share/man/man1/sbt.1.gz" - ) withPerms "0644" gzipped) asDocs() - }, - linuxPackageMappings <+= (sourceDirectory in Linux) map { bd => - packageMapping( - (bd / "usr/share/doc/sbt/copyright") -> "/usr/share/doc/sbt/copyright" - ) withPerms "0644" asDocs() - }, - linuxPackageMappings <+= (sourceDirectory in Linux) map { bd => - packageMapping( - (bd / "usr/share/doc/sbt") -> "/usr/share/doc/sbt" - ) asDocs() - }, - linuxPackageMappings <+= (sourceDirectory in Linux) map { bd => - packageMapping( - (bd / "etc/sbt") -> "/etc/sbt" - ) withConfig() - }, - linuxPackageMappings <+= (sourceDirectory in Linux) map { bd => - packageMapping( - (bd / "etc/sbt/sbtopts") -> "/etc/sbt/sbtopts" - ) withPerms "0644" withConfig("noreplace") - }, - linuxPackageMappings <+= (sbtLaunchJar, sourceDirectory in Linux, sbtVersion) map { (jar, dir, v) => - packageMapping(dir -> "/usr/lib/sbt", - dir -> ("/usr/lib/sbt/" + v), - jar -> ("/usr/lib/sbt/"+v+"/sbt-launch.jar")) withPerms "0755" - }, - // DEBIAN SPECIFIC - name in Debian := "sbt", + // DEBIAN SPECIFIC + name in Debian <<= (sbtVersion) apply { (sv) => "sbt" /* + "-" + (sv split "[^\\d]" take 3 mkString ".")*/ }, version in Debian <<= (version, sbtVersion) apply { (v, sv) => - sv + "-build-" + (v split "\\." map (_.toInt) dropWhile (_ == 0) map ("%02d" format _) mkString "") + val nums = (v split "[^\\d]") + "%s-%s-build-%03d" format (sv, (nums.init mkString "."), nums.last.toInt + 1) }, debianPackageDependencies in Debian ++= Seq("curl", "java2-runtime", "bash (>= 2.05a-11)"), debianPackageRecommends in Debian += "git", @@ -81,11 +56,30 @@ Here's an example excerpt for the debian + rpm package of [sbt-extras](http://gi // RPM SPECIFIC name in Rpm := "sbt", - version in Rpm <<= sbtVersion.identity, + version in Rpm <<= sbtVersion apply { sv => (sv split "[^\\d]" filterNot (_.isEmpty) mkString ".") }, rpmRelease := "1", rpmVendor := "typesafe", rpmUrl := Some("http://github.com/paulp/sbt-extras"), rpmLicense := Some("BSD"), - + + + // WINDOWS SPECIFIC + name in Windows := "sbt", + version in Windows <<= (sbtVersion) apply { sv => + (sv split "[^\\d]" filterNot (_.isEmpty)) match { + case Array(major,minor,bugfix, _*) => Seq(major,minor,bugfix, "1") mkString "." + case Array(major,minor) => Seq(major,minor,"0","1") mkString "." + case Array(major) => Seq(major,"0","0","1") mkString "." + } + }, + maintainer in Windows := "Typesafe, Inc.", + packageSummary in Windows := "Simple Build Tool", + packageDescription in Windows := "THE reactive build tool.", + wixProductId := "ce07be71-510d-414a-92d4-dff47631848a", + wixProductUpgradeId := "4552fb0e-e257-4dbd-9ecb-dba9dbacf424", + javacOptions := Seq("-source", "1.5", "-target", "1.5"), + + // Universal ZIP download install. + name in Universal := "sbt" The full build, including windows MSI generation, can be found [here](https://github.com/sbt/sbt-launcher-package/blob/full-packaging/project/packaging.scala). diff --git a/build.sbt b/build.sbt index b90d016bd..04b05ff63 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ name := "sbt-native-packager" organization := "com.typesafe.sbt" -version := "0.5.5" +version := "0.6.0" scalacOptions in Compile += "-deprecation" diff --git a/project/build.properties b/project/build.properties index 4474a03e1..5e96e9672 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.12.1 +sbt.version=0.12.4 diff --git a/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala b/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala index c4170c837..f7a3ee930 100644 --- a/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala @@ -13,7 +13,8 @@ object SbtNativePackager extends Plugin with debian.DebianPlugin with rpm.RpmPlugin with windows.WindowsPlugin - with universal.UniversalPlugin { + with universal.UniversalPlugin + with GenericPackageSettings { def packagerSettings = linuxSettings ++ debianSettings ++ @@ -38,4 +39,5 @@ object SbtNativePackager extends Plugin } // TODO - Add a few targets that detect the current OS and build a package for that OS. + } \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala b/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala new file mode 100644 index 000000000..9ae441730 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala @@ -0,0 +1,149 @@ +package com.typesafe.sbt +package packager + +import Keys._ +import sbt._ +import sbt.Keys.{name, mappings, sourceDirectory} +import linux.LinuxSymlink +import linux.LinuxPackageMapping + +object GenericPackageSettings { + val installLocation = "/usr/share" +} +trait GenericPackageSettings + extends linux.LinuxPlugin + with debian.DebianPlugin + with rpm.RpmPlugin + with windows.WindowsPlugin + with universal.UniversalPlugin { + import GenericPackageSettings._ + + // This method wires a lot of hand-coded generalities about how to map directories + // into linux, and the conventions we expect. + // It is by no means 100% accurate, but should be ok for the simplest cases. + // For advanced users, use the underlying APIs. + // Right now, it's also pretty focused on command line scripts packages. + + /** + * Maps linux file format from the universal from the conventions: + * + * `/src/linux` files are mapped directly into linux packages. + * `` files are placed under `/usr/share/` + * `/bin` files are given symlinks in `/usr/bin` + * `/conf` directory is given a symlink to `/etc/` + * Files in `conf/` or `etc/` directories are automatically marked as configuration. + * `../man/...1` files are automatically compressed into .gz files. + */ + def mapGenericMappingsToLinux(mappings: Seq[(File, String)])(rename: String => String): Seq[LinuxPackageMapping] = { + val (directories, nondirectories) = mappings.partition(_._1.isDirectory) + val (binaries, nonbinaries) = nondirectories.partition(_._1.canExecute) + val (manPages, nonManPages) = nonbinaries partition { + case (file, name) => (name contains "man/") && (name endsWith ".1") + } + val compressedManPages = + for((file, name) <- manPages) + yield file -> (name + ".gz") + val (configFiles, remaining) = nonManPages partition { + case (file, name) => (name contains "etc/") || (name contains "conf/") + } + def packageMappingWithRename(mappings: (File, String)*): LinuxPackageMapping = { + val renamed = + for((file, name) <- mappings) + yield file -> rename(name) + packageMapping(renamed:_*) + } + + Seq( + packageMappingWithRename((binaries ++ directories):_*) withUser "root" withGroup "root" withPerms "0755", + packageMappingWithRename(compressedManPages:_*).gzipped withUser "root" withGroup "root" withPerms "0644", + packageMappingWithRename(configFiles:_*) withConfig() withUser "root" withGroup "root" withPerms "0644", + packageMappingWithRename(remaining:_*) withUser "root" withGroup "root" withPerms "0644" + ) + } + + def mapGenericFilesToLinux: Seq[Setting[_]] = Seq( + // First we look at the src/linux files + linuxPackageMappings <++= (name in Universal, sourceDirectory in Linux) map { (pkg, dir) => + mapGenericMappingsToLinux((dir.*** --- dir) x relativeTo(dir))(identity) + }, + // Now we look at the src/universal files. + linuxPackageMappings <++= (name in Universal, mappings in Universal) map { (pkg, mappings) => + // TODO - More windows filters... + def isWindowsFile(f: (File, String)): Boolean = + f._2 endsWith ".bat" + + mapGenericMappingsToLinux(mappings filterNot isWindowsFile) { name => + installLocation + "/" + pkg + "/" + name + } + }, + // Now we generate symlinks. + linuxPackageSymlinks <++= (name in Universal, mappings in Universal) map { (pkg, mappings) => + for { + (file, name) <- mappings + if !file.isDirectory + if name startsWith "bin/" + if !(name endsWith ".bat") // IGNORE windows-y things. + } yield LinuxSymlink("/usr/" + name, installLocation+"/"+pkg+"/"+name) + }, + // Map configuration files + linuxPackageSymlinks <++= (name in Universal, mappings in Universal) map { (pkg, mappings) => + val needsConfLink = + mappings exists { case (file, name) => + (name startsWith "conf/") && !file.isDirectory + } + if(needsConfLink) Seq(LinuxSymlink( + link="/etc/" + pkg, + destination=installLocation+"/"+pkg+"/conf")) + else Seq.empty + } + ) + + def mapGenericFilesToWindows: Seq[Setting[_]] = Seq( + mappings in Windows <<= mappings in Universal, + wixFeatures <<= (name in Windows, mappings in Windows) map makeWindowsFeatures + ) + // TODO select main script! Filter Config links! + def makeWindowsFeatures(name: String, mappings: Seq[(File, String)]): Seq[windows.WindowsFeature] = { + import windows._ + + val files = + for { + (file, name) <- mappings + if !file.isDirectory + } yield ComponentFile(name, editable = (name startsWith "conf")) + val corePackage = + WindowsFeature( + id=name+"Core", + title=name, + desc="All core files.", + absent="disallow", + components = files + ) + // TODO - Detect bat files to add paths... + val homeEnvVar = name.toUpperCase +"_HOME" + val addBinToPath = + // TODO - we may have issues here... + WindowsFeature( + id="AddBinToPath", + title="Update Enviornment Variables", + desc="Update PATH environment variables (requires restart).", + components = Seq(AddDirectoryToPath("bin")) + ) + val configLinks = for { + (file, name) <- mappings + if !file.isDirectory + if name startsWith "conf/" + } yield name + val menuLinks = + WindowsFeature( + id="AddConfigLinks", + title="Configuration start menu links", + desc="Adds start menu shortcuts to edit configuration files.", + components = Seq(AddShortCuts(configLinks)) + ) + // TODO - Add feature for shortcuts to binary scripts. + Seq(corePackage, addBinToPath, menuLinks) + } + + +} \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala index 1e8297024..e9f77d8ff 100644 --- a/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala @@ -5,15 +5,25 @@ package debian import Keys._ import sbt._ import linux.LinuxPackageMapping +import linux.LinuxSymlink import linux.LinuxFileMetaData import com.typesafe.sbt.packager.Hashing +import com.typesafe.sbt.packager.linux.LinuxSymlink trait DebianPlugin extends Plugin with linux.LinuxPlugin { val Debian = config("debian") extend Linux + import com.typesafe.sbt.packager.universal.Archives + private[this] final def copyAndFixPerms(from: File, to: File, perms: LinuxFileMetaData, zipped: Boolean = false): Unit = { - if(zipped) IO.gzip(from, to) - else IO.copyFile(from, to, true) + if(zipped) { + IO.withTemporaryDirectory { dir => + val tmp = dir / from.getName + IO.copyFile(from, tmp) + val zipped = Archives.gzip(tmp) + IO.copyFile(zipped, to, true) + } + } else IO.copyFile(from, to, true) // If we have a directory, we need to alter the perms. chmod(to, perms.permissions) // TODO - Can we do anything about user/group ownership? @@ -32,15 +42,15 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { debianPackageRecommends := Seq.empty, debianSignRole := "builder", target in Debian <<= (target, name in Debian, version in Debian) apply ((t,n,v) => t / (n +"-"+ v)), + name in Debian <<= (name in Linux), + version in Debian <<= (version in Linux), linuxPackageMappings in Debian <<= linuxPackageMappings, packageDescription in Debian <<= packageDescription in Linux, packageSummary in Debian <<= packageSummary in Linux, + maintainer in Debian <<= maintainer in Linux, debianMaintainerScripts := Seq.empty ) ++ inConfig(Debian)(Seq( - name <<= name, - version <<= version, packageArchitecture := "all", - maintainer := "", debianPackageInfo <<= (name, version, maintainer, packageSummary, packageDescription) apply PackageInfo, debianPackageMetadata <<= @@ -59,6 +69,7 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { (data, size, dir) => val cfile = dir / "DEBIAN" / "control" IO.write(cfile, data.makeContent(size), java.nio.charset.Charset.defaultCharset) + chmod(cfile, "0644") cfile }, debianConffilesFile <<= (linuxPackageMappings, target) map { @@ -71,9 +82,20 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { if file.isFile } yield name IO.writeLines(cfile, conffiles) + chmod(cfile, "0644") cfile }, - debianExplodedPackage <<= (linuxPackageMappings, debianControlFile, debianMaintainerScripts, debianConffilesFile, target) map { (mappings, _, maintScripts, _, t) => + /*debianLinksfile <<= (name, linuxPackageSymlinks, target) map { (name, symlinks, dir) => + val lfile = dir / "DEBIAN" / (name + ".links") + val content = + for { + LinuxSymlink(link, destination) <- symlinks + } yield link + " " + destination + IO.writeLines(lfile, content) + chmod(lfile, "0644") + lfile + },*/ + debianExplodedPackage <<= (linuxPackageMappings, debianControlFile, debianMaintainerScripts, debianConffilesFile, linuxPackageSymlinks, target) map { (mappings, _, maintScripts, _, symlinks, t) => // First Create directories, in case we have any without files in them. for { LinuxPackageMapping(files, perms, zipped) <- mappings @@ -88,11 +110,20 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { if !file.isDirectory && file.exists tfile = t / name } copyAndFixPerms(file, tfile, perms, zipped) + + // Now generate relative symlinks + LinuxSymlink.makeSymLinks(symlinks, t) + // TODO: Fix this ugly hack to permission directories correctly! - for(file <- (t.***).get; if file.isDirectory) chmod(file, "0755") + for { + file <- (t.***).get + if file.isDirectory + if file.getCanonicalPath == file.getAbsolutePath // Ignore symlinks. + } chmod(file, "0755") // Put the maintainer files in `dir / "DEBIAN"` named as specified. // Valid values for the name are preinst,postinst,prerm,postrm for ((file, name) <- maintScripts) copyAndFixPerms(file, t / "DEBIAN" / name, LinuxFileMetaData()) + t }, debianMD5sumsFile <<= (debianExplodedPackage, target) map { @@ -103,9 +134,12 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { if file.isFile if !(name startsWith "DEBIAN") if !(name contains "debian-binary") + // TODO - detect symlinks... + if file.getCanonicalPath == file.getAbsolutePath fixedName = if(name startsWith "/") name drop 1 else name } yield Hashing.md5Sum(file) + " " + fixedName IO.writeLines(md5file, md5sums) + chmod(md5file, "0644") md5file }, packageBin <<= (debianExplodedPackage, debianMD5sumsFile, target, streams) map { (pkgdir, _, tdir, s) => diff --git a/src/main/scala/com/typesafe/sbt/packager/debian/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/debian/Keys.scala index 108f9cc8d..6396801e8 100644 --- a/src/main/scala/com/typesafe/sbt/packager/debian/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/debian/Keys.scala @@ -18,6 +18,7 @@ trait DebianKeys { val debianControlFile = TaskKey[File]("debian-control-file", "Makes the debian package control file.") val debianMaintainerScripts = TaskKey[Seq[(File, String)]]("debian-maintainer-scripts", "Makes the debian maintainer scripts.") val debianConffilesFile = TaskKey[File]("debian-conffiles-file", "Makes the debian package conffiles file.") + val debianLinksfile= TaskKey[File]("debian-links-file", "Makes the debian package links file.") val debianMD5sumsFile = TaskKey[File]("debian-md5sums-file", "Makes the debian package md5sums file.") val debianZippedMappings = TaskKey[Seq[LinuxPackageMapping]]("debian-zipped-mappings", "Files that need to be gzipped when they hit debian.") val debianCombinedMappings = TaskKey[Seq[LinuxPackageMapping]]("debian-combined-mappings", "All the mappings of files for the final package.") @@ -40,6 +41,7 @@ object Keys extends DebianKeys { // Package building def sourceDirectory = sbt.Keys.sourceDirectory def linuxPackageMappings = linux.Keys.linuxPackageMappings + def linuxPackageSymlinks = linux.Keys.linuxPackageSymlinks def packageBin = sbt.Keys.packageBin def target = sbt.Keys.target def streams = sbt.Keys.streams diff --git a/src/main/scala/com/typesafe/sbt/packager/linux/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/linux/Keys.scala index 313fb2743..fc6d6fce7 100644 --- a/src/main/scala/com/typesafe/sbt/packager/linux/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/linux/Keys.scala @@ -11,6 +11,7 @@ trait Keys { val packageDescription = SettingKey[String]("package-description", "The description of the package. Used when searching.") val maintainer = SettingKey[String]("maintainer", "The name/email address of a maintainer for the native package.") val linuxPackageMappings = TaskKey[Seq[LinuxPackageMapping]]("linux-package-mappings", "File to install location mappings including owner and privileges.") + val linuxPackageSymlinks = TaskKey[Seq[LinuxSymlink]]("linux-package-symlinks", "Symlinks we should produce in the underlying package.") val generateManPages = TaskKey[Unit]("generate-man-pages", "Shows all the man files in the current project") } diff --git a/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPackageMapping.scala b/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPackageMapping.scala index 482cb31f7..f46df34c7 100644 --- a/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPackageMapping.scala +++ b/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPackageMapping.scala @@ -32,3 +32,49 @@ case class LinuxPackageMapping( /** Modifies the current package mapping to have gzipped data. */ def gzipped = copy(zipped = true) } + +// TODO - Maybe this can support globbing symlinks? +// Maybe it should share an ancestor with LinuxPackageMapping so we can configure symlinks the same time as normal files? +case class LinuxSymlink(link: String, destination: String) +object LinuxSymlink { + + def makeRelative(from: String, to: String): String = { + val partsFrom: Seq[String] = from split "/" filterNot (_.isEmpty) + val partsTo: Seq[String] = to split "/" filterNot (_.isEmpty) + + val prefixAndOne = (1 to partsFrom.length).map(partsFrom.take).dropWhile(seq => partsTo.startsWith(seq)).headOption getOrElse sys.error("Cannot symlink to yourself!") + val prefix = prefixAndOne dropRight 1 + if(prefix.length > 0) { + val escapeCount = (partsTo.length - 1) - prefix.length + val escapes = (0 until escapeCount) map (i => "..") + val remainder = partsFrom drop prefix.length + (escapes ++ remainder).mkString("/") + } else from + } + // TODO - Does this belong here? + def makeSymLinks(symlinks: Seq[LinuxSymlink], pkgDir: File, relativeLinks: Boolean = true): Unit = { + for(link <- symlinks) { + // TODO - drop preceeding '/' + def dropFirstSlash(n: String): String = + if(n startsWith "/") n drop 1 + else n + def addFirstSlash(n: String): String = + if(n startsWith "/") n + else "/" + n + val to = pkgDir / dropFirstSlash(link.link) + val linkDir = to.getParentFile + if(!linkDir.isDirectory) IO.createDirectory(linkDir) + val name = IO.relativize(linkDir, to).getOrElse { + sys.error("Could not relativize names ("+to+") ("+linkDir+")!!! *(logic error)*") + } + val linkFinal = + if(relativeLinks) makeRelative(link.destination, link.link) + else addFirstSlash(link.destination) + // TODO - if it already exists, delete it, or check accuracy... + if(!to.exists) Process(Seq("ln", "-s", linkFinal, name), linkDir).! match { + case 0 => () + case n => sys.error("Failed to symlink " + link.destination + " to " + to) + } + } + } +} diff --git a/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPlugin.scala index 0eed77257..c0fd865cd 100644 --- a/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/linux/LinuxPlugin.scala @@ -8,12 +8,13 @@ import sbt._ /** Plugin trait containing all the generic values used for * packaging linux software. */ -trait LinuxPlugin extends Plugin{ +trait LinuxPlugin extends Plugin { // TODO - is this needed val Linux = config("linux") def linuxSettings: Seq[Setting[_]] = Seq( linuxPackageMappings := Seq.empty, + linuxPackageSymlinks := Seq.empty, sourceDirectory in Linux <<= sourceDirectory apply (_ / "linux"), generateManPages <<= (sourceDirectory in Linux, sbt.Keys.streams) map { (dir, s) => for( file <- (dir / "usr/share/man/man1" ** "*.1").get ) { @@ -22,14 +23,15 @@ trait LinuxPlugin extends Plugin{ s.log.info(man) } }, - packageSummary in Linux := "", - packageDescription in Linux := "" + packageSummary in Linux <<= packageSummary, + packageDescription in Linux <<= packageDescription ) /** DSL for packaging files into .deb */ def packageMapping(files: (File, String)*) = LinuxPackageMapping(files) - + // TODO - we'd like a set of conventions to take universal mappings and create linux package mappings. + /** Create a ascii friendly string for a man page. */ final def makeMan(file: File): String = Process("groff -man -Tascii " + file.getAbsolutePath).!! diff --git a/src/main/scala/com/typesafe/sbt/packager/rpm/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/rpm/Keys.scala index 0ae141433..61a28c47d 100644 --- a/src/main/scala/com/typesafe/sbt/packager/rpm/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/rpm/Keys.scala @@ -67,6 +67,7 @@ object Keys extends RpmKeys { // SPEC def linuxPackageMappings = linux.Keys.linuxPackageMappings + def linuxPackageSymlinks = linux.Keys.linuxPackageSymlinks // Building def target = sbt.Keys.target diff --git a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmHelper.scala b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmHelper.scala index e126c886e..afedeee98 100644 --- a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmHelper.scala +++ b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmHelper.scala @@ -3,6 +3,7 @@ package packager package rpm import sbt._ +import com.typesafe.sbt.packager.linux.LinuxSymlink object RpmHelper { @@ -47,6 +48,10 @@ object RpmHelper { if file.exists && !file.isDirectory() target = buildroot / dest } copyWithZip(file, target, mapping.zipped) + + + // Now we create symlinks + LinuxSymlink.makeSymLinks(spec.symlinks, buildroot) } private[this] def writeSpecFile(spec: RpmSpec, workArea: File, log: sbt.Logger): File = { diff --git a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmMetadata.scala b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmMetadata.scala index 3449d50bf..c7ff615c6 100644 --- a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmMetadata.scala +++ b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmMetadata.scala @@ -4,6 +4,7 @@ package rpm import linux.{LinuxPackageMapping,LinuxFileMetaData} import sbt._ +import com.typesafe.sbt.packager.linux.LinuxSymlink case class RpmMetadata( name: String, @@ -71,9 +72,20 @@ case class RpmSpec(meta: RpmMetadata, desc: RpmDescription = RpmDescription(), deps: RpmDependencies = RpmDependencies(), scriptlets: RpmScripts = RpmScripts(), - mappings: Seq[LinuxPackageMapping] = Seq.empty) { + mappings: Seq[LinuxPackageMapping] = Seq.empty, + symlinks: Seq[LinuxSymlink] = Seq.empty) { + + private[this] def fixFilename(n: String): String = { + val tmp = + if(n startsWith "/") n + else "/" + n + if(tmp.contains(' ')) "\"%s\"" format tmp + else tmp + } private[this] def makeFilesLine(target: String, meta: LinuxFileMetaData, isDir: Boolean): String = { + + val sb = new StringBuilder meta.config.toLowerCase match { case "false" => () @@ -90,10 +102,7 @@ case class RpmSpec(meta: RpmMetadata, sb append ',' sb append meta.group sb append ") " - sb append (target.contains(' ') match { - case true => "\"%s\"" format target - case false => target - }) + sb append fixFilename(target) sb append '\n' sb.toString } @@ -106,6 +115,9 @@ case class RpmSpec(meta: RpmMetadata, mapping <- mappings (file, dest) <- mapping.mappings } sb append makeFilesLine(dest, mapping.fileData, file.isDirectory) + for { + link <- symlinks + } sb append (fixFilename(link.link) + "\n") sb.toString } diff --git a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala index bbf7e1558..a2d811ebf 100644 --- a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala @@ -48,7 +48,7 @@ trait RpmPlugin extends Plugin with LinuxPlugin { rpmScripts <<= (rpmPretrans,rpmPre,rpmPost,rpmVerifyscript,rpmPosttrans,rpmPreun,rpmPostun) apply RpmScripts, rpmSpecConfig <<= - (rpmMetadata, rpmDescription, rpmDependencies, rpmScripts, linuxPackageMappings) map RpmSpec, + (rpmMetadata, rpmDescription, rpmDependencies, rpmScripts, linuxPackageMappings, linuxPackageSymlinks) map RpmSpec, packageBin <<= (rpmSpecConfig, target, streams) map { (spec, dir, s) => RpmHelper.buildRpm(spec, dir, s.log) }, diff --git a/src/main/scala/com/typesafe/sbt/packager/windows/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/windows/Keys.scala index 2efbb49b3..7c04b7128 100644 --- a/src/main/scala/com/typesafe/sbt/packager/windows/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/windows/Keys.scala @@ -5,17 +5,31 @@ package windows import sbt._ trait WindowsKeys { + + val wixProductId = SettingKey[String]("wix-product-id", "The uuid of the windows package.") + val wixProductUpgradeId = SettingKey[String]("wix-product-upgrade-id", "The uuid associated with upgrades for this package.") + val wixPackageInfo = SettingKey[WindowsProductInfo]("wix-package-info", "The configuration for this package.") + val wixProductLicense = TaskKey[Option[File]]("wix-product-license", "The RTF file to display with licensing.") + val wixFeatures = TaskKey[Seq[WindowsFeature]]("wix-features", "Configuration of the windows installable features for this package.") + val wixProductConfig = TaskKey[xml.Node]("wix-product-xml", "The WIX XML configuration for a product (nested in Wix/Product elements).") val wixConfig = TaskKey[xml.Node]("wix-xml", "The WIX XML configuration for this package.") val wixFile = TaskKey[File]("wix-file", "The WIX XML file to package with.") + @deprecated("use packageBin instead!") val packageMsi = TaskKey[File]("package-msi", "creates a new windows CAB file containing everything for the installation.") val candleOptions = SettingKey[Seq[String]]("candle-options", "Options to pass to the candle.exe program.") val lightOptions = SettingKey[Seq[String]]("light-options", "Options to pass to the light.exe program.") } object Keys extends WindowsKeys { + def version = sbt.Keys.version def target = sbt.Keys.target def mappings = sbt.Keys.mappings def name = sbt.Keys.name def streams = sbt.Keys.streams def sourceDirectory = sbt.Keys.sourceDirectory + def packageBin = sbt.Keys.packageBin + // TODO - move this somewhere generic. + def maintainer = linux.Keys.maintainer + def packageSummary = linux.Keys.packageSummary + def packageDescription = linux.Keys.packageDescription } \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/windows/WindowsPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/windows/WindowsPlugin.scala index c6a6d806f..2150e09fa 100644 --- a/src/main/scala/com/typesafe/sbt/packager/windows/WindowsPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/windows/WindowsPlugin.scala @@ -12,8 +12,48 @@ trait WindowsPlugin extends Plugin { sourceDirectory in Windows <<= sourceDirectory(_ / "windows"), target in Windows <<= target apply (_ / "windows"), name in Windows <<= name, - lightOptions := Seq.empty, - candleOptions := Seq.empty, + // Defaults so that our simplified building works + candleOptions := Seq("-ext", "WixUtilExtension"), + lightOptions := Seq("-ext", "WixUIExtension", + "-ext", "WixUtilExtension", + "-cultures:en-us"), + wixProductId := WixHelper.makeGUID, + wixProductUpgradeId := WixHelper.makeGUID, + maintainer in Windows <<= maintainer, + packageSummary in Windows <<= packageSummary, + packageDescription in Windows <<= packageDescription, + wixProductLicense <<= (sourceDirectory in Windows) map { dir => + // TODO - document this default. + val default = dir / "License.rtf" + if(default.exists) Some(default) + else None + }, + wixPackageInfo <<= ( + wixProductId, + wixProductUpgradeId, + version in Windows, + maintainer in Windows, + packageSummary in Windows, + packageDescription in Windows) apply { (id, uid, version, mtr, title, desc) => + WindowsProductInfo( + id = id, + title = title, + version = version, + maintainer = mtr, + description = desc, + upgradeId = uid, + comments = "TODO - we need comments." // TODO - allow comments + ) + }, + wixFeatures := Seq.empty, + wixProductConfig <<= (name in Windows, wixPackageInfo, wixFeatures, wixProductLicense) map { (name, product, features, license) => + WixHelper.makeWixProductConfig(name, product, features, license) + }, + wixConfig <<= (name in Windows, wixPackageInfo, wixProductConfig) map { (name, product, nested) => + WixHelper.makeWixConfig(name, product, nested) + }, + wixConfig in Windows <<= wixConfig, + wixProductConfig in Windows <<= wixProductConfig, wixFile <<= (wixConfig in Windows, name in Windows, target in Windows) map { (c, n, t) => val f = t / (n + ".wxs") IO.write(f, c.toString) @@ -21,10 +61,12 @@ trait WindowsPlugin extends Plugin { } ) ++ inConfig(Windows)(Seq( // Disable windows generation by default. - wixConfig := , mappings := Seq.empty, - mappings in packageMsi <<= mappings, - packageMsi <<= (mappings in packageMsi, wixFile, name, target, candleOptions, lightOptions, streams) map {(m, f, n, t, co, lo, s) => + mappings in packageBin <<= mappings, + // TODO - Remove packageMsi after next major release. + mappings in packageMsi <<= mappings in packageBin, + packageMsi <<= packageBin, + packageBin <<= (mappings in packageMsi, wixFile, name, target, candleOptions, lightOptions, streams) map {(m, f, n, t, co, lo, s) => val msi = t / (n + ".msi") // First we have to move everything (including the wix file) to our target directory. val wix = t / (n + ".wix") diff --git a/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala b/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala index 9120b167a..5759e1904 100644 --- a/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala +++ b/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala @@ -7,11 +7,222 @@ import sbt._ import collection.mutable.ArrayBuffer +case class WindowsProductInfo( + id: String, // UUID of the package + title: String, // Human readable name of the package + version: String, // Windows version + maintainer: String, + description: String, + upgradeId: String, // UUID for upgrading + comments: String = "", + installScope: String = "perMachine", + installerVersion: String = "200", + compressed: Boolean = true +) + +sealed trait FeatureComponent +/** Define a new feature, that will be selectable in the default MSI. */ +case class WindowsFeature( + id: String, + title: String, + desc: String, + absent: String="allow", + level: String="1", + display: String="collapse", + components: Seq[FeatureComponent] = Seq.empty) extends FeatureComponent {} +/** Adds a file into a given windows feature. */ +case class ComponentFile( + source: String, + editable: Boolean = false +) extends FeatureComponent +/** Will add the directory to the windows path. NOTE: Only one of these + * per MSI. + */ +case class AddDirectoryToPath(dir: String = "") extends FeatureComponent +case class AddShortCuts( + target: Seq[String], + workingDir: String="INSTALLDIR" +) extends FeatureComponent + + +// TODO - Shortcut as a component element. + /** Helper functions to deal with Wix/CAB craziness. */ object WixHelper { /** Generates a windows friendly GUID for use in random locations in the build. */ - //def makeGUID = java.util.UUID.generateUUID + def makeGUID: String = java.util.UUID.randomUUID.toString + + // TODO - Fragment out this function a bit so it's not so ugly/random. + def makeWixProductConfig(name: String, product: WindowsProductInfo, features: Seq[WindowsFeature], license: Option[File] = None): scala.xml.Node = { + // TODO - First we find directories... + val filenames = + for { + f <- features + ComponentFile(name, _) <- f.components + } yield name.replaceAll("\\\\","/") + // Now for directories... + def parentDir(filename: String) = filename take (filename lastIndexOf '/') + def simpleName(filename: String) = { + val lastSlash = filename lastIndexOf '/' + filename drop (lastSlash + 1) + } + val dirs = (filenames map parentDir).distinct + // Now we need our directory tree xml? + val dirToChilren = dirs groupBy parentDir + def dirXml(currentDir: String): scala.xml.Node = if(!currentDir.isEmpty){ + val children = dirToChilren.getOrElse(currentDir, Seq.empty) + + { + children map dirXml + } + + } else + + // We need component helpers... + case class ComponentInfo(id: String, xml: scala.xml.Node) + def makeComponentInfo(c: FeatureComponent): ComponentInfo = c match { + case w: WindowsFeature => sys.error("Nested windows features currently unsupported!") + case AddDirectoryToPath(dir) => + val dirRef = if(dir.isEmpty) "INSTALLDIR" else cleanStringForId(dir) + val homeEnvVar = name.toUpperCase + "_HOME" + val pathAddition = + if(dir.isEmpty) "%"+homeEnvVar+"%" + else "[INSTALLDIR]\\"+dir.replaceAll("\\/", "\\\\") + val id = cleanStringForId(dir) + "PathC" + val guid = makeGUID + val xml = + + + + + + + + ComponentInfo(id, xml) + case ComponentFile(name, editable) => + val uname = name.replaceAll("\\\\", "/") + val dir = parentDir(uname) + val dirRef = if(dir.isEmpty) "INSTALLDIR" else cleanStringForId(dir) + val fname = simpleName(uname) + val id = cleanStringForId(uname) + val xml = + + + + { + if(editable) { + + + + + } else Seq.empty + } + + + + ComponentInfo(id, xml) + case AddShortCuts(targets, workingDir) => + val id = cleanStringForId("shortcut_"+makeGUID) + val xml = + + + { + for(target <- targets) yield { + val name = simpleName(target) + val desc = "Edit configuration file: " + name + + } + } + + + + + ComponentInfo(id, xml) + } + + val componentMap = + (for(f <- features) yield { + // TODO - we need to support more than "Component File". + val componentInfos = + f.components map makeComponentInfo + f.id -> componentInfos + }).toMap + + + + + + + + + + + {dirToChilren("") map dirXml} + + + + + { + for { + (fid, components) <- componentMap + ComponentInfo(cid, xml) <- components + } yield xml + } + + + { for(f <- features) + yield + { + for(ComponentInfo(id, _) <- componentMap.getOrElse(f.id, Seq.empty)) + yield + } + + + } + + + + + + { + license.toSeq map { file => + + } + } + + } + def makeWixConfig( + name: String, // package name + product: WindowsProductInfo, + rest: xml.Node): xml.Node = { + + + + + {rest} + + + } /** Modifies a string to be Wix ID friendly by removing all the bad * characters and replacing with _. Also limits the width to 70 (rather than @@ -20,7 +231,9 @@ object WixHelper { def cleanStringForId(n: String) = n.replaceAll("[^0-9a-zA-Z_]", "_").takeRight(70) /** Cleans a file name for the Wix pre-processor. Every $ should be doubled. */ - def cleanFileName(n: String) = n.replaceAll("\\$", "\\$\\$") + def cleanFileName(n: String) = { + n.replaceAll("\\$", "\\$\\$").replaceAll("\\/", "\\\\") + } /** Takes a file and generates an ID for it. */ def makeIdFromFile(f: File) = cleanStringForId(f.getName) @@ -30,6 +243,7 @@ object WixHelper { * @return A tuple where the first item is all the Component Ids created, * and the second is the Directory/File/Component XML. */ + @deprecated def generateComponentsAndDirectoryXml(dir: File, id_prefix: String =""): (Seq[String], scala.xml.Node) = { def makeId(f: File) = cleanStringForId(IO.relativize(dir, f) map (id_prefix+) getOrElse (id_prefix+f.getName)) def handleFile(f: File): (Seq[String], scala.xml.Node) = { diff --git a/src/sphinx/archetypes.rst b/src/sphinx/archetypes.rst index a0e510912..3b627ddd7 100644 --- a/src/sphinx/archetypes.rst +++ b/src/sphinx/archetypes.rst @@ -1,3 +1,5 @@ +.. _Archetypes: + Project Archetypes ================== @@ -5,26 +7,45 @@ Project archetypes are default deployment scripts that try to "do the right thin Because not all projects are created equal, there is no one single archetype for all native packages, but a set of them for usage. +The architecture of the plugin is set up so that you can customize your packages at any level of complexity. +For example, if you'd like to write Windows Installer XML by hand and manually map files, you should be able to do this while +still leveraging the default configuration for other platforms. + + Curently, in the nativepackager these archetypes are available: - * Java Vanilla Application + * Java Command Line Application (Experimental) + + -Java Vanilla Application ------------------------- +Java Command Line Application +----------------------------- -A Java vanilla application is a Java application that consists of a set of JARs and a main method. There is no +A Java Command Line application is a Java application that consists of a set of JARs and a main method. There is no custom start scripts, or services. It is just a bash/bat script that starts up a Java project. To use this archetype in your build, do the following in your ``build.sbt``: archetypes.java_application + + mapGenericFilesToLinux + + mapGenericFilesToWindows name := "A-package-friendly-name" - packageSummary in Linux := "The name you want displayed in package summaries", + packageSummary in Linux := "The name you want displayed in package summaries" + + packageSummary in Windows := "The name you want displayed in Add/Remove Programs" + + packageDescription := " A descriptioin of your project" - packageDescription in Linux := " A descriptioin of your project", + maintainer in Windows := "Company" - maintainer in Debian := "Your Name " \ No newline at end of file + maintainer in Debian := "Your Name " + + wixProductId := "ce07be71-510d-414a-92d4-dff47631848a" + + wixProductUpgradeId := "4552fb0e-e257-4dbd-9ecb-dba9dbacf424" diff --git a/src/sphinx/gettingstarted.rst b/src/sphinx/gettingstarted.rst index be237681b..404fc957f 100644 --- a/src/sphinx/gettingstarted.rst +++ b/src/sphinx/gettingstarted.rst @@ -1,27 +1,34 @@ Getting Started =============== -The sbt-native-packager is an sbt plugin. Please follow the [[installation]] instructions for how to set it up on a project. +The sbt-native-packager is an sbt plugin. Please follow the :ref:`Installation` instructions for how to set it up on a project. The sbt-native-packager attempts to make building packages for different operating systems easier. While it provides some basic abstractions around packaging, it also allows you to dig down into the nuts and bolts of each platform as neeeded to generate the best package possible. -Most packages are split into two types of deployments: -1. 'nix-style deployments. Pushing scripts to ``/usr/bin``, docs to ``/usr/share/man``, etc. -2. Windows/Universal-style deployments. A single directory holds all the artifacts needed to run the application. +Here's the basic architecture of the plugin: -Because of this, the Native packager splits defining of packages into two pieces: ``mappings in Universal`` and ``linuxPackageMappings``. +.. image:: https://docs.google.com/drawings/d/1ASOPHY8UUGLDHrYYXFWqfYOuQe5sBioX8GKkeN3Yvd0/pub?w=960&h=720 + :height: 720 px + :width: 960 px + :alt: Architecture diagram. + +When using the full power of the plugin, all of the packaging is driven from the ``mappings in Universal`` setting, which defines +what files will be included in the package. These files are automatically moved around for the appropriate native packaging as needed. + +We'll examine each level of packaging. Defining a new package ~~~~~~~~~~~~~~~~~~~~~~ -TODO - Write more. +To define a new package, after installing the plugin and ensuring the basic settings are on the project, start configuring your package contents +either using :ref:`Archetypes` or :ref:`Universal` hooks. These will describe the appropriate way to begin packaging for your applciation. + + -Note: That as of the latest version of the native packager plugin, project Archetypes are coming into existing, which -drastically simplify creating native packages if your application fits a certain mold. \ No newline at end of file diff --git a/src/sphinx/index.rst b/src/sphinx/index.rst index cc7515e73..6b2446235 100644 --- a/src/sphinx/index.rst +++ b/src/sphinx/index.rst @@ -16,7 +16,8 @@ to allow native packages to be created for all major operating systems, includin installation.rst gettingstarted.rst - archetypes.rst + universal.rst linux.rst windows.rst - universal.rst + archetypes.rst + diff --git a/src/sphinx/installation.rst b/src/sphinx/installation.rst index 3db3c713e..e64c3c2fa 100644 --- a/src/sphinx/installation.rst +++ b/src/sphinx/installation.rst @@ -1,3 +1,5 @@ +.. _Installation: + Installation =============== diff --git a/src/sphinx/linux.rst b/src/sphinx/linux.rst index 752dd7904..ccc724fb7 100644 --- a/src/sphinx/linux.rst +++ b/src/sphinx/linux.rst @@ -1,3 +1,5 @@ +.. _Linux: + Writing Linux Packages ====================== diff --git a/src/sphinx/universal.rst b/src/sphinx/universal.rst index 4c6c16066..9a043ee7a 100644 --- a/src/sphinx/universal.rst +++ b/src/sphinx/universal.rst @@ -1,3 +1,5 @@ +.. _Universal: + Universal ========= @@ -5,6 +7,108 @@ Universal packaging just takes a plain ``mappings`` configuration and generates package files for distribution. It allows you to provide your users a distribution that is not tied to any particular platform, but may require manual labor to set up. + +Getting Started with Universal Packaging +---------------------------------------- +By default, all files found in the ``src/universal`` directory are included in the distribution. So, the first step +in creating a a distribution is to place files in this directory in the layout you would like in the distributed zip file. + +To add build generated files to the distribution, simple add a *mapping* to the ``mappings in Universal`` setting. Let's +look at an example where we add the packaged jar of a project to the lib folder of a distribution: + +.. code-block:: scala + + mappings in Universal <+= (packageBin in Compile) map { jar => + jar -> ("lib/" + jar.getName) + } + +The above does two things: + +1. It depends on ``packageBin in Compile`` which will generate a jar file form the project. +2. It creates a *mapping* (a ``Tuple2[File, String]``) which denotes the file and the location in the distribution as a string. + +You can use this to add anything you desire to the package. + + +Universal Conventions +--------------------- +This plugin has a set of conventions for universal packages that enable the automatic generation of native packages. The +universal convention has the following package layout: + + +.. code-block:: none + + bin/ + + lib/ + + conf/ + + doc/ + + +If your plugin matches these conventions, you can enable the settings to automatically generate native layouts based on your universal package. To do +so, add the following to your build.sbt: + + +.. code-block:: scala + + mapGenericFilesToLinux + + mapGenericFilesToWinows + + +In Linux, this mapping creates symlinks from platform locations to the install location of the universal package. For example, +given the following packaging: + + +.. code-block:: none + + bin/ + cool-tool + lib/ + cool-tool.jar + conf/ + cool-tool.conf + + +The ``mapGenericFilesToLinux`` settings will create the following package (symlinks denoted with ``->``): + + +.. code-block:: none + + /usr/share// + bin/ + cool-tool + lib/ + cool-tool.jar + conf/ + cool-tool.conf + /usr/bin/ + cool-tool -> /usr/share//bin/cool-tool + /etc/ -> /usr/share//conf + +The ``mapGenericFilesToWindows`` will construct an MSI that installs the application in ``\`` and include +the ``bin`` directory on Windows ``PATH`` environment variable (optionally disabled). While these mappings provide a great start to nice packaging, it still +may be necessary to customize the native packaging for each platform. This can be done by configuring those settings directly. + +For example, even using generic mapping, debian has a requirement for changelog files to be fully formed. Using the above generic mapping, we can configure just this +changelog in addition to the generic packaging by first defining a changelog in ``src/debian/changelog`` and then adding the following setting: + + +.. code-block:: scala + + linuxPackageMappings in Debian <+= (name in Universal, sourceDirectory in Debian) map { (name, dir) => + (packageMapping( + (dir / "changelog") -> "/usr/share/doc/sbt/changelog.gz" + ) withUser "root" withGroup "root" withPerms "0644" gzipped) asDocs() + } + +Notice how we're *only* modifying the package mappings for Debian linux packages. For more information on the underlying packaging settings, see +:ref:`Windows` and :ref:`Linux` documentation. + + + Configurations -------------- Universal packaging provides three Configurations: @@ -19,16 +123,22 @@ Universal packaging provides three Configurations: Settings -------- -The Universal packages are completely configured through the use of the mappings key. Simply +As we showed before, the Universal packages are completely configured through the use of the mappings key. Simply specify the desired mappings for a given configuration. For Example: - ``mappings in Universal <+= packageBin in Compile map { p => p -> "lib/foo.jar" }`` +.. code-block:: scala + + mappings in Universal <+= packageBin in Compile map { p => p -> "lib/foo.jar" } + +However, sometimes it may be advantageous to customize the files for each archive separately. For example, perhaps +the .tar.gz has an additional README plaintext file in additon to a README.html. To add this just to the .tar.gz file, +use the task-scope feature of sbt: -The different types of archives can also be configured through selection. +.. code-block:: scala - ``mappings in Universal in package-zip-tarball += file("README") -> "README"`` + mappings in Universal in package-zip-tarball += file("README") -> "README -Besides ``mappings``, the ``name``, ``sourceDirectory`` and ``target`` configurations are all respected. +Besides ``mappings``, the ``name``, ``sourceDirectory`` and ``target`` configurations are all respected by universal packaging. **Note: The Universal plugin will make anything in a bin/ directory executable. This is to work around issues with JVM and file system manipulations.** @@ -45,6 +155,9 @@ Commands ``universal:package-xz-tarball`` Creates the ``txz`` universal package. The ``xz`` command can get better compression for some types of archives. + + ``universal:package-osx-dmg`` + Creates the ``dmg`` universal package. This only work on OSX or systems with ``hdiutil``. ``universal-docs:package-bin`` Creates the ``zip`` universal documentation package. @@ -54,4 +167,4 @@ Commands ``universal-docs:package-xz-tarball`` Creates the ``txz`` universal documentation package. The ``xz`` command can get better compression - for some types of archives. \ No newline at end of file + for some types of archives. diff --git a/src/sphinx/windows.rst b/src/sphinx/windows.rst index 4c7eb8778..e685a7a77 100644 --- a/src/sphinx/windows.rst +++ b/src/sphinx/windows.rst @@ -1,7 +1,64 @@ -Windows -======= +.. _Windows: -The windows packaging is completely tied to the WIX installer toolset. It's important to understand how WIX works. http://wix.tramontana.co.hu/ is an excellent tutorial to how to create packages using wix. +Packaging for Windows +===================== + +The windows packaging is completely tied to the WIX installer toolset. For any non-trivial package, it's important to understand how WIX works. http://wix.tramontana.co.hu/ is an excellent tutorial to how to create packages using wix. + +However, the native-packager provides a simple layer on top of wix that *may* be enough for most projects. If it is not, just override ``wixConfig`` or ``wixFile`` settings. Let's look at the layer above direct xml configuration. + + +Feature configuration +--------------------- +The abstraction over wix allows you to configure "features" that users may optionally install. These feature are higher level things, like a set of files or menu links. +The currently supported compoennts of features are: + +1. Files (``ComponentFile``) +2. Path Configuration (``AddDirectoryToPath``) +3. Menu Shortcuts (``AddShortCuts``) + + +To create a new feature, simple instantiate the ``WindowsFeature`` class with the desired feature components that are included. + +Here's an example feature that installs a binary and a script, as well as path settings: + +.. code-block:: scala + + wixFeatures += WindowsFeature( + id="BinaryAndPath", + title="My Project's Binaries and updated PATH settings", + desc="Update PATH environment variables (requires restart).", + components = Seq( + ComponentFile("bin/cool.bat"), + ComponentFile("lib/cool.jar"), + AddDirectoryToPath("bin")) + ) + +All file references should line up exactly with those found in the ``mappings in Windows`` configuration. When generating an MSI, the plugin will first create +a directory using all the ``mappings in Windows`` and configure this for inclusion in a ``cab`` file. If you'd like to add files to include, these must *first* +be added to the mappings, and then to a feature. For example, if we complete the above setting to include file mappings, we'd have the following: + +.. code-block:: scala + + mappings in Windows ++= (packageBin in Compile, sourceDirectory in Windows) map { (jar, dir) => + Seq(jar -> "lib/cool.jar", (dir / "cool.bat") -> "bin/cool.bat") + } + + wixFeatures += WindowsFeature( + id="BinaryAndPath", + title="My Project's Binaries and updated PATH settings", + desc="Update PATH environment variables (requires restart).", + components = Seq( + ComponentFile("bin/cool.bat"), + ComponentFile("lib/cool.jar"), + AddDirectoryToPath("bin")) + ) + +Right now this layer is *very* limited in what it can accomplish, and hasn't been heavily debugged. If you're interested in helping contribute, please +do so! However, for most command line tools, it should be sufficient for generating a basic ``msi`` that windows users can install. + + +Now, let's look at the full set of windows settings. Settings -------- @@ -15,6 +72,24 @@ Settings ``lightOptions`` the list of options to pass to the ``light.exe`` command. Most likely setting is: ``Seq("-ext", "WixUIExtension", "-cultures:en-us")`` for UI. + ``wixProductId`` + The GUID to use to identify the windows package/product. + + ``wixProductUpgradeId`` + The GUID to use to identify the windows package/product *upgrade* identifier (see wix docs). + + ``wixPackageInfo`` + The information used to autoconstruct the ```` portion of the wix xml. **Note: unused if ``wixConfig`` is overridden** + + ``wixProductLicense`` + An (optional) ``rtf`` file to display as the product license during installation. Default to looking for ``src/windows/License.rtf`` + + ``wixFeatures`` + A set of windows features that users can install with this package. **Note: unused if ``wixConfig`` is overridden** + + ``wixProductConfig`` + inline XML to use for wix configuration. This is everything nested inside the ```` element. + ``wixConfig`` inline XML to use for wix configuration. This is used if the ``wixFile`` setting is not specified. @@ -28,11 +103,11 @@ Settings Commands -------- - ``windows:package-msi`` + ``windows:package-bin`` Creates the ``msi`` package. ``wix-file`` - Generates the Wix xml file from `wixConfig` setings, unless overriden. + Generates the Wix xml file from `wixConfig` and `wixProductConfig` setings, unless overriden. Utilities ---------