diff --git a/build.sbt b/build.sbt index 4cfd197d5..9e003c13e 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,10 @@ organization := "com.typesafe.sbt" scalacOptions in Compile += "-deprecation" -libraryDependencies += "org.apache.commons" % "commons-compress" % "1.4.1" +libraryDependencies ++= Seq( + "org.apache.commons" % "commons-compress" % "1.4.1", + "org.vafer" % "jdeb" % "1.2" artifacts (Artifact("jdeb", "jar", "jar")) +) site.settings 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 85964d70f..0d96a7ac0 100644 --- a/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala @@ -10,7 +10,7 @@ import linux.Keys.{ linuxScriptReplacements, daemonShell } import com.typesafe.sbt.packager.Hashing import com.typesafe.sbt.packager.archetypes.TemplateWriter -trait DebianPlugin extends Plugin with linux.LinuxPlugin { +trait DebianPlugin extends Plugin with linux.LinuxPlugin with NativePackaging with JDebPackaging { val Debian = config("debian") extend Linux val UserNamePattern = "^[a-z][-a-z0-9_]*$".r @@ -18,7 +18,178 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { import DebianPlugin.Names import linux.LinuxPlugin.Users - private[this] final def copyAndFixPerms(from: File, to: File, perms: LinuxFileMetaData, zipped: Boolean = false): Unit = { + def debianSettings: Seq[Setting[_]] = Seq( + /* ==== Debian default settings ==== */ + debianPriority := "optional", + debianSection := "java", + debianPackageDependencies := Seq.empty, + 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, + + /* ==== Debian configuration settings ==== */ + debianControlScriptsDirectory <<= (sourceDirectory) apply (_ / "debian" / Names.Debian), + debianMaintainerScripts := Seq.empty, + debianMakePreinstScript := None, + debianMakePrermScript := None, + debianMakePostinstScript := None, + debianMakePostrmScript := None, + debianChangelog := None, + + /* ==== Debian maintainer scripts ==== */ + debianMaintainerScripts <++= (debianMakePrermScript, debianControlScriptsDirectory) map scriptMapping(Names.Prerm), + debianMaintainerScripts <++= (debianMakePreinstScript, debianControlScriptsDirectory) map scriptMapping(Names.Preinst), + debianMaintainerScripts <++= (debianMakePostinstScript, debianControlScriptsDirectory) map scriptMapping(Names.Postinst), + debianMaintainerScripts <++= (debianMakePostrmScript, debianControlScriptsDirectory) map scriptMapping(Names.Postrm)) ++ inConfig(Debian)( + /* ==== Debian scoped settings ==== */ + Seq( + packageArchitecture := "all", + debianPackageInfo <<= + (normalizedName, version, maintainer, packageSummary, packageDescription) apply PackageInfo, + debianPackageMetadata <<= + (debianPackageInfo, + debianPriority, packageArchitecture, debianSection, + debianPackageDependencies, debianPackageRecommends) apply PackageMetaData, + debianPackageInstallSize <<= linuxPackageMappings map { mappings => + (for { + LinuxPackageMapping(files, _, zipped) <- mappings + (file, _) <- files + if !file.isDirectory && file.exists + // TODO - If zipped, heuristically figure out a reduction factor. + } yield file.length).sum / 1024 + }, + debianControlFile <<= (debianPackageMetadata, debianPackageInstallSize, target) map { + (data, size, dir) => + if (data.info.description == null || data.info.description.isEmpty) { + sys.error( + """packageDescription in Debian cannot be empty. Use + packageDescription in Debian := "My package Description"""") + } + val cfile = dir / Names.Debian / Names.Control + IO.write(cfile, data.makeContent(size), java.nio.charset.Charset.defaultCharset) + chmod(cfile, "0644") + cfile + }, + debianConffilesFile <<= (linuxPackageMappings, target) map { + (mappings, dir) => + val cfile = dir / Names.Debian / Names.Conffiles + val conffiles = for { + LinuxPackageMapping(files, meta, _) <- mappings + if meta.config != "false" + (file, name) <- files + if file.isFile + } yield name + IO.writeLines(cfile, conffiles) + chmod(cfile, "0644") + cfile + }, + debianMD5sumsFile <<= (debianExplodedPackage, target) map { + (mappings, dir) => + val md5file = dir / Names.Debian / "md5sums" + val md5sums = for { + (file, name) <- (dir.*** --- dir x relativeTo(dir)) + if file.isFile + if !(name startsWith Names.Debian) + if !(name contains "debian-binary") + // TODO - detect symlinks with Java7 (when we can) rather than hackery... + 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 + }, + debianExplodedPackage <<= (linuxPackageMappings, debianControlFile, debianMaintainerScripts, debianConffilesFile, debianChangelog, daemonShell in Linux, linuxScriptReplacements, linuxPackageSymlinks, target, streams) + map { (mappings, _, maintScripts, _, changelog, shell, replacements, symlinks, t, streams) => + + // Create files and directories + mappings foreach { + case LinuxPackageMapping(paths, perms, zipped) => + val (dirs, files) = paths.partition(_._1.isDirectory) + dirs map { + case (_, name) => t / name + } foreach { targetDir => + targetDir mkdirs () + chmod(targetDir, perms.permissions) + } + + files map { + case (file, name) => (file, t / name) + } foreach { + case (source, target) => copyAndFixPerms(source, target, perms, zipped) + } + } + // Now generate relative symlinks + LinuxSymlink.makeSymLinks(symlinks, t, false) + + // Put the maintainer files in `dir / "DEBIAN"` named as specified. + // Valid values for the name are preinst,postinst,prerm,postrm + for ((file, name) <- maintScripts) { + val targetFile = t / Names.Debian / name + copyAndFixPerms(file, targetFile, LinuxFileMetaData()) + filterAndFixPerms(targetFile, replacements, LinuxFileMetaData()) + } + + // Check for non root user/group and append to postinst / postrm + // filter all root mappings, map to (user,group) key, group by, append everything + mappings filter { + case LinuxPackageMapping(_, LinuxFileMetaData(Users.Root, Users.Root, _, _, _), _) => false + case _ => true + } map { + case LinuxPackageMapping(paths, LinuxFileMetaData(user, group, _, _, _), _) => (user, group) -> paths + } groupBy (_._1) foreach { + case ((user, group), pathList) => + streams.log info ("Altering postrm/postinst files to add user " + user + " and group " + group) + val postinst = createFileIfRequired(t / Names.Debian / Names.Postinst, LinuxFileMetaData()) + val postrm = createFileIfRequired(t / Names.Debian / Names.Postrm, LinuxFileMetaData()) + val prerm = createFileIfRequired(t / Names.Debian / Names.Prerm, LinuxFileMetaData()) + val headerScript = IO.readLinesURL(Native.headerSource) + + val replacements = Seq("group" -> group, "user" -> user, "shell" -> shell) + + prependAndFixPerms(prerm, headerScript, LinuxFileMetaData()) + + // remove key, flatten it and then go through each file + pathList.map(_._2).flatten foreach { + case (_, target) => + val pathReplacements = replacements :+ ("path" -> target.toString) + val chownAdd = Seq(TemplateWriter.generateScript(Native.postinstChownTemplateSource, pathReplacements)) + prependAndFixPerms(postinst, chownAdd, LinuxFileMetaData()) + } + + validateUserGroupNames(user, streams) + validateUserGroupNames(group, streams) + + val userGroupAdd = Seq( + TemplateWriter.generateScript(Native.postinstGroupaddTemplateSource, replacements), + TemplateWriter.generateScript(Native.postinstUseraddTemplateSource, replacements)) + + prependAndFixPerms(postinst, userGroupAdd, LinuxFileMetaData()) + prependAndFixPerms(postinst, headerScript, LinuxFileMetaData()) + + val purgeAdd = Seq(TemplateWriter.generateScript(Native.postrmPurgeTemplateSource, replacements)) + appendAndFixPerms(postrm, purgeAdd, LinuxFileMetaData()) + prependAndFixPerms(postrm, headerScript, LinuxFileMetaData()) + } + t + }, + // Setting the packaging strategy + packageBin <<= debianNativePackaging + + // Adding package specific implementation settings + ) ++ debianNativeSettings ++ debianJDebSettings) + + /* ============================================= */ + /* ========== Debian Helper Methods ============ */ + /* ============================================= */ + + private[debian] final def copyAndFixPerms(from: File, to: File, perms: LinuxFileMetaData, zipped: Boolean = false): Unit = { if (zipped) { IO.withTemporaryDirectory { dir => val tmp = dir / from.getName @@ -32,7 +203,7 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { // TODO - Can we do anything about user/group ownership? } - private[this] def filterAndFixPerms(script: File, replacements: Seq[(String, String)], perms: LinuxFileMetaData): File = { + private[debian] final def filterAndFixPerms(script: File, replacements: Seq[(String, String)], perms: LinuxFileMetaData): File = { val filtered = TemplateWriter.generateScript(script.toURI.toURL, replacements) IO.delete(script) IO.write(script, filtered) @@ -40,20 +211,20 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { script } - private[this] def prependAndFixPerms(script: File, lines: Seq[String], perms: LinuxFileMetaData): File = { + private[debian] final def prependAndFixPerms(script: File, lines: Seq[String], perms: LinuxFileMetaData): File = { val old = IO.readLines(script) IO.writeLines(script, lines ++ old, append = false) chmod(script, perms.permissions) script } - private[this] def appendAndFixPerms(script: File, lines: Seq[String], perms: LinuxFileMetaData): File = { + private[debian] final def appendAndFixPerms(script: File, lines: Seq[String], perms: LinuxFileMetaData): File = { IO.writeLines(script, lines, append = true) chmod(script, perms.permissions) script } - private[this] def createFileIfRequired(script: File, perms: LinuxFileMetaData): File = { + private[debian] final def createFileIfRequired(script: File, perms: LinuxFileMetaData): File = { if (!script.exists()) { script.createNewFile() chmod(script, perms.permissions) @@ -61,17 +232,7 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { script } - private[this] def scriptMapping(scriptName: String)(script: Option[File], controlDir: File): Seq[(File, String)] = { - (script, controlDir) match { - // check if user defined script exists - case (_, dir) if (dir / scriptName).exists => - Seq(file((dir / scriptName).getAbsolutePath) -> scriptName) - // create mappings for generated script - case (scr, _) => scr.toSeq.map(_ -> scriptName) - } - } - - private[this] def validateUserGroupNames(user: String, streams: TaskStreams) { + private[debian] final def validateUserGroupNames(user: String, streams: TaskStreams) { if ((UserNamePattern findFirstIn user).isEmpty) { streams.log.warn("The user or group '" + user + "' may contain invalid characters for Debian based distributions") } @@ -80,211 +241,20 @@ trait DebianPlugin extends Plugin with linux.LinuxPlugin { } } - def debianSettings: Seq[Setting[_]] = Seq( - debianPriority := "optional", - debianSection := "java", - debianPackageDependencies := Seq.empty, - 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, - - debianControlScriptsDirectory <<= (sourceDirectory) apply (_ / "debian" / Names.Debian), - debianMaintainerScripts := Seq.empty, - debianMakePreinstScript := None, - debianMakePrermScript := None, - debianMakePostinstScript := None, - debianMakePostrmScript := None, - debianChangelog := None, - - debianMaintainerScripts <++= (debianMakePrermScript, debianControlScriptsDirectory) map scriptMapping(Names.Prerm), - debianMaintainerScripts <++= (debianMakePreinstScript, debianControlScriptsDirectory) map scriptMapping(Names.Preinst), - debianMaintainerScripts <++= (debianMakePostinstScript, debianControlScriptsDirectory) map scriptMapping(Names.Postinst), - debianMaintainerScripts <++= (debianMakePostrmScript, debianControlScriptsDirectory) map scriptMapping(Names.Postrm)) ++ inConfig(Debian)(Seq( - packageArchitecture := "all", - debianPackageInfo <<= - (normalizedName, version, maintainer, packageSummary, packageDescription) apply PackageInfo, - debianPackageMetadata <<= - (debianPackageInfo, - debianPriority, packageArchitecture, debianSection, - debianPackageDependencies, debianPackageRecommends) apply PackageMetaData, - debianPackageInstallSize <<= linuxPackageMappings map { mappings => - (for { - LinuxPackageMapping(files, _, zipped) <- mappings - (file, _) <- files - if !file.isDirectory && file.exists - // TODO - If zipped, heuristically figure out a reduction factor. - } yield file.length).sum / 1024 - }, - debianControlFile <<= (debianPackageMetadata, debianPackageInstallSize, target) map { - (data, size, dir) => - if (data.info.description == null || data.info.description.isEmpty) { - sys.error( - """packageDescription in Debian cannot be empty. Use - packageDescription in Debian := "My package Description"""") - } - val cfile = dir / Names.Debian / Names.Control - IO.write(cfile, data.makeContent(size), java.nio.charset.Charset.defaultCharset) - chmod(cfile, "0644") - cfile - }, - debianConffilesFile <<= (linuxPackageMappings, target) map { - (mappings, dir) => - val cfile = dir / Names.Debian / Names.Conffiles - val conffiles = for { - LinuxPackageMapping(files, meta, _) <- mappings - if meta.config != "false" - (file, name) <- files - if file.isFile - } yield name - IO.writeLines(cfile, conffiles) - chmod(cfile, "0644") - cfile - }, - debianExplodedPackage <<= (linuxPackageMappings, debianControlFile, debianMaintainerScripts, debianConffilesFile, debianChangelog, daemonShell in Linux, linuxScriptReplacements, linuxPackageSymlinks, target, streams) - map { (mappings, _, maintScripts, _, changelog, shell, replacements, symlinks, t, streams) => - - // Create files and directories - mappings foreach { - case LinuxPackageMapping(paths, perms, zipped) => - val (dirs, files) = paths.partition(_._1.isDirectory) - dirs map { - case (_, name) => t / name - } foreach { targetDir => - targetDir mkdirs () - chmod(targetDir, perms.permissions) - } - - files map { - case (file, name) => (file, t / name) - } foreach { - case (source, target) => copyAndFixPerms(source, target, perms, zipped) - } - } - // Now generate relative symlinks - LinuxSymlink.makeSymLinks(symlinks, t, false) - - // Put the maintainer files in `dir / "DEBIAN"` named as specified. - // Valid values for the name are preinst,postinst,prerm,postrm - for ((file, name) <- maintScripts) { - val targetFile = t / Names.Debian / name - copyAndFixPerms(file, targetFile, LinuxFileMetaData()) - filterAndFixPerms(targetFile, replacements, LinuxFileMetaData()) - } - - // Check for non root user/group and append to postinst / postrm - // filter all root mappings, map to (user,group) key, group by, append everything - mappings filter { - case LinuxPackageMapping(_, LinuxFileMetaData(Users.Root, Users.Root, _, _, _), _) => false - case _ => true - } map { - case LinuxPackageMapping(paths, LinuxFileMetaData(user, group, _, _, _), _) => (user, group) -> paths - } groupBy (_._1) foreach { - case ((user, group), pathList) => - streams.log info ("Altering postrm/postinst files to add user " + user + " and group " + group) - val postinst = createFileIfRequired(t / Names.Debian / Names.Postinst, LinuxFileMetaData()) - val postrm = createFileIfRequired(t / Names.Debian / Names.Postrm, LinuxFileMetaData()) - val prerm = createFileIfRequired(t / Names.Debian / Names.Prerm, LinuxFileMetaData()) - val headerScript = IO.readLinesURL(DebianPlugin.headerSource) - - val replacements = Seq("group" -> group, "user" -> user, "shell" -> shell) - - prependAndFixPerms(prerm, headerScript, LinuxFileMetaData()) - - // remove key, flatten it and then go through each file - pathList.map(_._2).flatten foreach { - case (_, target) => - val pathReplacements = replacements :+ ("path" -> target.toString) - val chownAdd = Seq(TemplateWriter.generateScript(DebianPlugin.postinstChownTemplateSource, pathReplacements)) - prependAndFixPerms(postinst, chownAdd, LinuxFileMetaData()) - } - - validateUserGroupNames(user, streams) - validateUserGroupNames(group, streams) - - val userGroupAdd = Seq( - TemplateWriter.generateScript(DebianPlugin.postinstGroupaddTemplateSource, replacements), - TemplateWriter.generateScript(DebianPlugin.postinstUseraddTemplateSource, replacements)) - - prependAndFixPerms(postinst, userGroupAdd, LinuxFileMetaData()) - prependAndFixPerms(postinst, headerScript, LinuxFileMetaData()) - - val purgeAdd = Seq(TemplateWriter.generateScript(DebianPlugin.postrmPurgeTemplateSource, replacements)) - appendAndFixPerms(postrm, purgeAdd, LinuxFileMetaData()) - prependAndFixPerms(postrm, headerScript, LinuxFileMetaData()) - } - t - }, - debianMD5sumsFile <<= (debianExplodedPackage, target) map { - (mappings, dir) => - val md5file = dir / Names.Debian / "md5sums" - val md5sums = for { - (file, name) <- (dir.*** --- dir x relativeTo(dir)) - if file.isFile - if !(name startsWith Names.Debian) - if !(name contains "debian-binary") - // TODO - detect symlinks with Java7 (when we can) rather than hackery... - 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, debianSection, debianPriority, name, version, packageArchitecture, target, streams) - map { (pkgdir, _, section, priority, name, version, arch, tdir, s) => - // Make the package. We put this in fakeroot, so we can build the package with root owning files. - val archive = name + "_" + version + "_" + arch + ".deb" - Process(Seq("fakeroot", "--", "dpkg-deb", "--build", pkgdir.getAbsolutePath, "../" + archive), Some(tdir)) ! s.log match { - case 0 => () - case x => sys.error("Failure packaging debian file. Exit code: " + x) - } - tdir / ".." / archive - }, - debianSign <<= (packageBin, debianSignRole, streams) map { (deb, role, s) => - Process(Seq("dpkg-sig", "-s", role, deb.getAbsolutePath), Some(deb.getParentFile())) ! s.log match { - case 0 => () - case x => sys.error("Failed to sign debian package! exit code: " + x) - } - deb - }, - lintian <<= packageBin map { file => - Process(Seq("lintian", "-c", "-v", file.getName), Some(file.getParentFile)).! - }, - genChanges <<= (packageBin, target, debianChangelog, name, version, debianPackageMetadata) map { - (pkg, tdir, changelog, name, version, data) => - changelog match { - case None => sys.error("Cannot generate .changes file without a changelog") - case Some(chlog) => { - // dpkg-genchanges needs a debian "source" directory, different from the DEBIAN "binary" directory - val debSrc = tdir / "../tmp" / Names.DebianSource - debSrc.mkdirs() - copyAndFixPerms(chlog, debSrc / Names.Changelog, LinuxFileMetaData("0644")) - IO.writeLines(debSrc / Names.Files, List(pkg.getName + " " + data.section + " " + data.priority)) - // dpkg-genchanges needs a "source" control file, located in a "debian" directory - IO.writeLines(debSrc / Names.Control, List(data.makeSourceControl())) - val changesFileName = name + "_" + version + "_" + data.architecture + ".changes" - val changesFile: File = tdir / ".." / changesFileName - try { - val changes = Process(Seq("dpkg-genchanges", "-b"), Some(tdir / "../tmp")) !! - val allChanges = List(changes) - IO.writeLines(changesFile, allChanges) - } catch { - case e: Exception => sys.error("Failure generating changes file." + e.getStackTraceString) - } - changesFile - } - } - - })) - + private[debian] def scriptMapping(scriptName: String)(script: Option[File], controlDir: File): Seq[(File, String)] = { + (script, controlDir) match { + // check if user defined script exists + case (_, dir) if (dir / scriptName).exists => + Seq(file((dir / scriptName).getAbsolutePath) -> scriptName) + // create mappings for generated script + case (scr, _) => scr.toSeq.map(_ -> scriptName) + } + } } +/** + * Contains debian specific constants + */ object DebianPlugin { object Names { val DebianSource = "debian" @@ -302,10 +272,4 @@ object DebianPlugin { val Changelog = "changelog" val Files = "files" } - - private def postinstGroupaddTemplateSource: java.net.URL = getClass.getResource("postinst-groupadd") - private def postinstUseraddTemplateSource: java.net.URL = getClass.getResource("postinst-useradd") - private def postinstChownTemplateSource: java.net.URL = getClass.getResource("postinst-chown") - private def postrmPurgeTemplateSource: java.net.URL = getClass.getResource("postrm-purge") - private def headerSource: java.net.URL = getClass.getResource("header") } diff --git a/src/main/scala/com/typesafe/sbt/packager/debian/JDebPackaging.scala b/src/main/scala/com/typesafe/sbt/packager/debian/JDebPackaging.scala new file mode 100644 index 000000000..634ec96b6 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/debian/JDebPackaging.scala @@ -0,0 +1,104 @@ +package com.typesafe.sbt +package packager +package debian + +import Keys._ +import sbt._ +import sbt.Keys.{ target, name, normalizedName, TaskStreams } +import linux.{ LinuxFileMetaData, LinuxPackageMapping, LinuxSymlink } +import linux.Keys.{ linuxScriptReplacements, daemonShell } +import com.typesafe.sbt.packager.linux.LinuxPackageMapping +import scala.collection.JavaConversions._ + +import org.vafer.jdeb.{ DebMaker, DataProducer } +import org.vafer.jdeb.mapping._ +import org.vafer.jdeb.producers._ +import DebianPlugin.Names + +/** + * This provides a java based debian packaging implementation based + * on the jdeb maven-plugin. To use this, put this into your build.sbt + * + * {{ + * packageBin in Debian <<= debianJDebPackaging in Debian + * }} + * + * @author Nepomuk Seiler + * @see https://github.com/tcurdt/jdeb/blob/master/src/main/java/org/vafer/jdeb/maven/DebMojo.java#L503 + * + */ +trait JDebPackaging { this: DebianPlugin with linux.LinuxPlugin => + + private[debian] def debianJDebSettings: Seq[Setting[_]] = Seq( + + /** + * Depends on the 'debianExplodedPackage' task as this creates all the files + * which are defined in the mappings. + */ + debianJDebPackaging <<= (debianExplodedPackage, linuxPackageMappings, linuxPackageSymlinks, + debianControlFile, debianMaintainerScripts, debianConffilesFile, + normalizedName, version, target, streams) map { + (_, mappings, symlinks, controlfile, controlscripts, conffile, + name, version, target, s) => + s.log.info("Building debian package with java based implementation 'jdeb'") + val console = new JDebConsole(s.log) + + val debianFile = target.getParentFile / "%s_%s_all.deb".format(name, version) + val debMaker = new DebMaker(console, + fileAndDirectoryProducers(mappings, target) ++ linkProducers(symlinks), + conffileProducers() + ) + debMaker setDeb debianFile + debMaker setControl (target / Names.Debian) + + // TODO set compression, gzip is default + // TODO add signing with setKeyring, setKey, setPassphrase, setSignPackage, setSignMethod, setSignRole + debMaker validate () + debMaker makeDeb () + debianFile + }) + + /** + * Creating file and directory producers. These "produce" the + * files for the debian packaging + */ + private[debian] def fileAndDirectoryProducers(mappings: Seq[LinuxPackageMapping], target: File): Seq[DataProducer] = mappings.map { + case LinuxPackageMapping(paths, perms, zipped) => + // TODO implement mappers here or use the maintainerscripts logic? + val (dirs, files) = paths.partition(_._1.isDirectory) + paths map { + case (path, name) if path.isDirectory => + new DataProducerDirectory(target / name, null, Array("**"), null) + case (path, name) => + new DataProducerFile(target / name, name, null, null, null) + } + }.flatten + + /** + * Creating link producers for symlinks. + */ + private[debian] def linkProducers(symlinks: Seq[LinuxSymlink]): Seq[DataProducer] = symlinks map { + case LinuxSymlink(link, destination) => + new DataProducerLink(link, destination, true, null, null, null) + } + + /** + * Creating the files which should be added as conffiles. + * This is currently handled by the debian plugin itself. + */ + private[debian] def conffileProducers(): Seq[DataProducer] = Seq.empty + +} + +/** + * This provides the task for building a debian packaging with + * the java-based implementation jdeb + */ +class JDebConsole(log: Logger) extends org.vafer.jdeb.Console { + + def debug(message: String) = log debug message + + def info(message: String) = log info message + + def warn(message: String) = log warn message +} 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 ea39e26df..b6b9ebce3 100644 --- a/src/main/scala/com/typesafe/sbt/packager/debian/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/debian/Keys.scala @@ -16,6 +16,9 @@ trait DebianKeys { val debianPackageMetadata = SettingKey[PackageMetaData]("debian-package-metadata", "Meta data used when constructing a debian package.") val debianChangelog = SettingKey[Option[File]]("debian-changelog", "The changelog for this deb file") // Package building + val debianNativePackaging = TaskKey[File]("debian-packaging-native", "Builds the debian package with native cli tools") + val debianJDebPackaging = TaskKey[File]("debian-packaging-jdeb", "Builds the debian package with jdeb (java-based)") + 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.") diff --git a/src/main/scala/com/typesafe/sbt/packager/debian/NativePackaging.scala b/src/main/scala/com/typesafe/sbt/packager/debian/NativePackaging.scala new file mode 100644 index 000000000..6db378a87 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/debian/NativePackaging.scala @@ -0,0 +1,98 @@ +package com.typesafe.sbt +package packager +package debian + +import Keys._ +import sbt._ +import sbt.Keys.{ target, name, normalizedName, TaskStreams } +import linux.{ LinuxFileMetaData, LinuxPackageMapping, LinuxSymlink } +import linux.Keys.{ linuxScriptReplacements, daemonShell } +import com.typesafe.sbt.packager.Hashing +import com.typesafe.sbt.packager.archetypes.TemplateWriter + +/** + * This provides a dpgk based implementation for debian packaging. + * Your machine must have dpkg installed to use this. + * + * {{ + * packageBin in Debian <<= debianNativePackaging in Debian + * }} + * + * + * + */ +trait NativePackaging { this: DebianPlugin with linux.LinuxPlugin => + + import com.typesafe.sbt.packager.universal.Archives + import DebianPlugin.Names + import linux.LinuxPlugin.Users + + private[debian] def debianNativeSettings: Seq[Setting[_]] = Seq( + genChanges <<= (packageBin, target, debianChangelog, name, version, debianPackageMetadata) map { + (pkg, tdir, changelog, name, version, data) => + changelog match { + case None => sys.error("Cannot generate .changes file without a changelog") + case Some(chlog) => { + // dpkg-genchanges needs a debian "source" directory, different from the DEBIAN "binary" directory + val debSrc = tdir / "../tmp" / Names.DebianSource + debSrc.mkdirs() + copyAndFixPerms(chlog, debSrc / Names.Changelog, LinuxFileMetaData("0644")) + IO.writeLines(debSrc / Names.Files, List(pkg.getName + " " + data.section + " " + data.priority)) + // dpkg-genchanges needs a "source" control file, located in a "debian" directory + IO.writeLines(debSrc / Names.Control, List(data.makeSourceControl())) + val changesFileName = name + "_" + version + "_" + data.architecture + ".changes" + val changesFile: File = tdir / ".." / changesFileName + try { + val changes = Process(Seq("dpkg-genchanges", "-b"), Some(tdir / "../tmp")) !! + val allChanges = List(changes) + IO.writeLines(changesFile, allChanges) + } catch { + case e: Exception => sys.error("Failure generating changes file." + e.getStackTraceString) + } + changesFile + } + } + + }, + debianSign <<= (packageBin, debianSignRole, streams) map { (deb, role, s) => + Process(Seq("dpkg-sig", "-s", role, deb.getAbsolutePath), Some(deb.getParentFile())) ! s.log match { + case 0 => () + case x => sys.error("Failed to sign debian package! exit code: " + x) + } + deb + }, + lintian <<= packageBin map { file => + Process(Seq("lintian", "-c", "-v", file.getName), Some(file.getParentFile)).! + }, + + /** Implementation of the actual packaging */ + debianNativePackaging <<= (debianExplodedPackage, debianMD5sumsFile, debianSection, debianPriority, name, version, packageArchitecture, target, streams) map { + (pkgdir, _, section, priority, name, version, arch, tdir, s) => + s.log.info("Building debian package with native implementation") + // Make the package. We put this in fakeroot, so we can build the package with root owning files. + val archive = name + "_" + version + "_" + arch + ".deb" + Process(Seq("fakeroot", "--", "dpkg-deb", "--build", pkgdir.getAbsolutePath, "../" + archive), Some(tdir)) ! s.log match { + case 0 => () + case x => sys.error("Failure packaging debian file. Exit code: " + x) + } + tdir / ".." / archive + } + ) + +} + +/** + * This provides the task for building a debian packaging with + * native tools + * + */ +object Native { + + /* static assets definitions */ + + private[debian] def postinstGroupaddTemplateSource: java.net.URL = getClass.getResource("postinst-groupadd") + private[debian] def postinstUseraddTemplateSource: java.net.URL = getClass.getResource("postinst-useradd") + private[debian] def postinstChownTemplateSource: java.net.URL = getClass.getResource("postinst-chown") + private[debian] def postrmPurgeTemplateSource: java.net.URL = getClass.getResource("postrm-purge") + private[debian] def headerSource: java.net.URL = getClass.getResource("header") +} \ No newline at end of file diff --git a/src/sbt-test/debian/simple-jdeb/build.sbt b/src/sbt-test/debian/simple-jdeb/build.sbt new file mode 100644 index 000000000..9351893a3 --- /dev/null +++ b/src/sbt-test/debian/simple-jdeb/build.sbt @@ -0,0 +1,22 @@ +import NativePackagerKeys._ + +packagerSettings + +mapGenericFilesToLinux + +name := "debian-test" + +version := "0.1.0" + +maintainer := "Josh Suereth " + +packageSummary := "Test debian package" + +packageDescription := """A fun package description of our software, + with multiple lines.""" + +debianPackageDependencies in Debian ++= Seq("java2-runtime", "bash (>= 2.05a-11)") + +debianPackageRecommends in Debian += "git" + +packageBin in Debian <<= debianJDebPackaging in Debian diff --git a/src/sbt-test/debian/simple-jdeb/project/plugins.sbt b/src/sbt-test/debian/simple-jdeb/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/debian/simple-jdeb/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/debian/simple-jdeb/test b/src/sbt-test/debian/simple-jdeb/test new file mode 100644 index 000000000..5a0cd1e63 --- /dev/null +++ b/src/sbt-test/debian/simple-jdeb/test @@ -0,0 +1,3 @@ +# Run the debian packaging. +> debian:package-bin +$ exists target/debian-test_0.1.0_all.deb