diff --git a/src/main/scala/com/typesafe/packager/debian/DebianPlugin.scala b/src/main/scala/com/typesafe/packager/debian/DebianPlugin.scala index c639b2590..36c80729b 100644 --- a/src/main/scala/com/typesafe/packager/debian/DebianPlugin.scala +++ b/src/main/scala/com/typesafe/packager/debian/DebianPlugin.scala @@ -1,4 +1,5 @@ -package com.typesafe.packager.debian +package com.typesafe.packager +package debian import Keys._ import sbt._ @@ -26,20 +27,23 @@ object DebianPlugin extends Plugin { Process("groff -man -Tascii " + file.getAbsolutePath).!! def debianSettings: Seq[Setting[_]] = Seq( + // TODO - These settings should move to a common 'linux packaging' plugin location. + linuxPackageMappings := Seq.empty, + packageArchitecture := "all", sourceDiectory in Debian <<= sourceDiectory apply (_ / "linux"), - target in Debian <<= (target, name in Debian, version in Debian) apply ((t,n,v) => t / (n +"-"+ v)) - ) ++ inConfig(Debian)(Seq( - name <<= name, - version<<= version, - packageDescription := "", debianPriority := "optional", - debianArchitecture := "all", debianSection := "java", debianPackageDependencies := Seq.empty, debianPackageRecommends := Seq.empty, + target in Debian <<= (target, name in Debian, version in Debian) apply ((t,n,v) => t / (n +"-"+ v)), + linuxPackageMappings in Debian <<= (linuxPackageMappings).identity + ) ++ inConfig(Debian)(Seq( + name <<= name, + version <<= version, + packageDescription := "", debianPackageMetadata <<= (name, version, maintainer, packageDescription, - debianPriority, debianArchitecture, debianSection, + debianPriority, packageArchitecture, debianSection, debianPackageDependencies, debianPackageRecommends) apply PackageMetaData, debianControlFile <<= (debianPackageMetadata, target) map { (data, dir) => @@ -47,7 +51,6 @@ object DebianPlugin extends Plugin { IO.write(cfile, data.makeContent, java.nio.charset.Charset.defaultCharset) cfile }, - linuxPackageMappings := Seq.empty, debianExplodedPackage <<= (linuxPackageMappings, debianControlFile, target) map { (mappings, _, t) => for { LinuxPackageMapping(files, perms, zipped) <- mappings diff --git a/src/main/scala/com/typesafe/packager/debian/Keys.scala b/src/main/scala/com/typesafe/packager/debian/Keys.scala index 2a9cb566a..300bc9342 100644 --- a/src/main/scala/com/typesafe/packager/debian/Keys.scala +++ b/src/main/scala/com/typesafe/packager/debian/Keys.scala @@ -9,8 +9,8 @@ object Keys { def name = sbt.Keys.name def version = sbt.Keys.version def maintainer = linux.Keys.maintainer - val packageDescription = SettingKey[String]("package-description", "The description of the package. Used when searching.") - val debianArchitecture = SettingKey[String]("debian-architecture", "The architecture to package for, defaults to all.") + def packageArchitecture = linux.Keys.packageArchitecture + def packageDescription = linux.Keys.packageDescription val debianSection = SettingKey[String]("debian-section", "The section category for this deb file.") val debianPriority = SettingKey[String]("debian-priority") val debianPackageDependencies = SettingKey[Seq[String]]("debian-package-dependencies", "Packages that this debian package depends on.") diff --git a/src/main/scala/com/typesafe/packager/linux/Keys.scala b/src/main/scala/com/typesafe/packager/linux/Keys.scala index aa23fed01..58ec268dd 100644 --- a/src/main/scala/com/typesafe/packager/linux/Keys.scala +++ b/src/main/scala/com/typesafe/packager/linux/Keys.scala @@ -3,6 +3,8 @@ package com.typesafe.packager.linux import sbt._ object Keys { + val packageArchitecture = SettingKey[String]("package-architecture", "The architecture used for this linux package.") + 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.") } \ No newline at end of file diff --git a/src/main/scala/com/typesafe/packager/linux/LinuxPackageMapping.scala b/src/main/scala/com/typesafe/packager/linux/LinuxPackageMapping.scala index 7c0e17dcc..3fd1ac322 100644 --- a/src/main/scala/com/typesafe/packager/linux/LinuxPackageMapping.scala +++ b/src/main/scala/com/typesafe/packager/linux/LinuxPackageMapping.scala @@ -5,11 +5,15 @@ import sbt._ case class LinuxFileMetaData( user: String = "root", group: String = "root", - permissions: String = "755") { + permissions: String = "755", + config: String = "false", + docs: Boolean = false) { def withUser(u: String) = copy(user = u) def withGroup(g: String) = copy(group = g) def withPerms(p: String) = copy(permissions = p) + def withConfig(value:String = "true") = copy(config = value) + def asDocs() = copy(docs = true) } case class LinuxPackageMapping( @@ -20,6 +24,8 @@ case class LinuxPackageMapping( def withUser(user: String) = copy(fileData = fileData withUser user) def withGroup(group: String) = copy(fileData = fileData withGroup group) def withPerms(perms: String) = copy(fileData = fileData withPerms perms) + def withConfig(c: String) = copy(fileData = fileData withConfig c) + def asDocs() = copy(fileData = fileData asDocs ()) /** Modifies the current package mapping to have gzipped data. */ def gzipped = copy(zipped = true) diff --git a/src/main/scala/com/typesafe/packager/rpm/RpmHelper.scala b/src/main/scala/com/typesafe/packager/rpm/RpmHelper.scala new file mode 100644 index 000000000..b87c6e6af --- /dev/null +++ b/src/main/scala/com/typesafe/packager/rpm/RpmHelper.scala @@ -0,0 +1,86 @@ +package com.typesafe.packager.rpm + +import sbt._ + +object RpmHelper { + + /** Returns the host vendor for an rpm. */ + def hostVendor = + Process(Seq("rpm", "-E", "%{_host_vendor}")) !! + + def buildRpm(spec: RpmSpec, workArea: File, log: sbt.Logger): File = { + // TODO - check the spec for errors. + buildWorkArea(workArea) + copyFiles(spec,workArea, log) + writeSpecFile(spec, workArea, log) + + buildPackage(workArea, spec, log) + // We should probably return the File that was created. + val rpmname = "%s-%s-%s-%s.rpm" format (spec.meta.name, spec.meta.version, spec.meta.release, spec.meta.arch) + workArea / "RPMS" / spec.meta.arch / rpmname + } + + private[this] def copyFiles(spec: RpmSpec, workArea: File, log: sbt.Logger): Unit = { + // TODO - special treatment of icon... + val buildroot = workArea / "tmp-buildroot" + + def copyWithZip(from: File, to: File, zipped: Boolean): Unit = + if(zipped) IO.gzip(from, to) + else IO.copyFile(from, to, true) + // We don't have to do any permission modifications since that's in the + // the .spec file. + for { + mapping <- spec.mappings + (file, dest) <- mapping.mappings + if file.exists && !file.isDirectory() + target = buildroot / dest + } copyWithZip(file, target, mapping.zipped) + } + + private[this] def writeSpecFile(spec: RpmSpec, workArea: File, log: sbt.Logger): File = { + val specdir = workArea / "SPECS" + val rpmBuildroot = workArea / "buildroot" + val tmpBuildRoot = workArea / "tmp-buildroot" + val specfile = specdir / (spec.meta.name + ".spec") + log.debug("Creating SPEC file: " + specfile.getAbsolutePath) + IO.write(specfile, spec.writeSpec(rpmBuildroot, tmpBuildRoot)) + specfile + } + + private[this] def buildPackage( + workArea: File, + spec: RpmSpec, + log: sbt.Logger): Unit = { + val buildRoot = workArea / "buildroot" + val specsDir = workArea / "SPECS" + val gpg = false + // TODO - Full GPG support (with GPG plugin). + val args: Seq[String] = Seq( + "rpmbuild", + "-bb", + "-buildroot", buildRoot.getAbsolutePath, + "--define", "_topdir " + workArea.getAbsolutePath, + "--target", spec.meta.arch + '-' + spec.meta.vendor + '-' + spec.meta.os + ) ++ ( + if(gpg) Seq("--define", "_gpg_name " + "", "--sign") + else Seq.empty + ) ++ Seq(spec.meta.name + ".spec") + Process(args, Some(specsDir)) ! log + } + + private[this] val topleveldirs = Seq("BUILD","RPMS","SOURCES","SPECS","SRPMS","tmp-buildroot","buildroot") + + /** Builds the work area and returns the tmp build root, and rpm build root. */ + private[this] def buildWorkArea(workArea: File): Unit = { + if(!workArea.exists) workArea.mkdirs() + // TODO - validate workarea + // Clean out work area + topleveldirs map (workArea / _) foreach { d => + if(d.exists()) IO.delete(d) + d.mkdir() + } + } + + def evalMacro(macro: String): String = + Process(Seq("rpm", "--eval", '%' + macro)) !! +} \ No newline at end of file diff --git a/src/main/scala/com/typesafe/packager/rpm/RpmMetadata.scala b/src/main/scala/com/typesafe/packager/rpm/RpmMetadata.scala new file mode 100644 index 000000000..7fde3a1da --- /dev/null +++ b/src/main/scala/com/typesafe/packager/rpm/RpmMetadata.scala @@ -0,0 +1,137 @@ +package com.typesafe.packager +package rpm + +import linux.{LinuxPackageMapping,LinuxFileMetaData} +import sbt._ + +case class RpmMetadata( + name: String, + version: String, + release: String, + arch: String, + vendor: String, + os: String) { +} + + +case class RpmDescription( + summary: Option[String] = None, + license: Option[String] = None, + distribution: Option[String] = None, + //vendor: Option[String] = None, + url: Option[String] = None, + group: Option[String] = None, + packager: Option[String] = None, + icon: Option[String] = None + ) + +case class RpmDependencies( + provides: Seq[String] = Seq.empty, + requirements: Seq[String] = Seq.empty, + prereq: Seq[String] = Seq.empty, + obsoletes: Seq[String] = Seq.empty, + conflicts: Seq[String] = Seq.empty) { + def contents: String = { + val sb = new StringBuilder + def appendSetting(prefix: String, values: Seq[String]) = + values foreach (v => sb append (prefix + v + "\n")) + appendSetting("Provides: ", provides) + appendSetting("Requires: ", requirements) + appendSetting("PreReq: ", prereq) + appendSetting("Obsoletes: ", obsoletes) + appendSetting("Conflicts: ", conflicts) + sb.toString + } +} + +case class RpmSpec(meta: RpmMetadata, + desc: RpmDescription = RpmDescription(), + deps: RpmDependencies = RpmDependencies(), + mappings: Seq[LinuxPackageMapping] = Seq.empty) { + + private[this] def makeFilesLine(target: String, meta: LinuxFileMetaData, isDir: Boolean): String = { + val sb = new StringBuilder + meta.config.toLowerCase match { + case "false" => () + case "true" => sb append "%config " + case x => sb append ("%config("+x+") ") + } + if(meta.docs) sb append "%doc " + if(isDir) sb append "%dir " + // TODO - map dirs... + sb append "%attr(" + sb append meta.permissions + sb append ',' + sb append meta.user + sb append ',' + sb append meta.group + sb append ") " + sb append target + sb append '\n' + sb.toString + } + + private[this] def fileSection: String = { + val sb = new StringBuilder + sb append "\n%files\n" + // TODO - default attribute string. + for { + mapping <- mappings + (file, dest) <- mapping.mappings + } sb append makeFilesLine(dest, mapping.fileData, file.isDirectory) + sb.toString + } + + private[this] def installSection(root: File): String = { + val sb = new StringBuilder + sb append "\n" + sb append "%install\n" + sb append "if [ -e $RPM_BUILD_ROOT ]; " + sb append "then\n" + sb append " mv " + sb append root.getAbsolutePath + sb append "/* $RPM_BUILD_ROOT\n" + sb append "else\n" + sb append " mv " + sb append root.getAbsolutePath + sb append " $RPM_BUILD_ROOT\n" + sb append "fi\n" + sb.toString + } + + // TODO - This is *very* tied to RPM helper, may belong *in* RpmHelper + def writeSpec(rpmRoot: File, tmpRoot: File): String = { + val sb = new StringBuilder + sb append ("Name: %s\n" format meta.name) + sb append ("Version: %s\n" format meta.version) + sb append ("Release: %s\n" format meta.release) + + desc.summary foreach { v => sb append ("Summary: %s\n" format v)} + desc.license foreach { v => sb append ("License: %s\n" format v)} + desc.distribution foreach { v => sb append ("Distribution: %s\n" format v)} + // TODO - Icon + + sb append ("Vendor: %s\n" format meta.vendor) + desc.url foreach { v => sb append ("URL: %s\n" format v)} + desc.group foreach { v => sb append ("Group: %s\n" format v)} + desc.packager foreach { v => sb append ("Packager: %s\n" format v)} + + sb append deps.contents + + // TODO - autoprov + autoreq + + sb append ("BuildRoot: %s\n\n" format rpmRoot.getAbsolutePath) + + // write build as moving everything into RPM directory. + sb append installSection(tmpRoot) + // TODO - Allow symlinks + // TODO - Allow scriptlets for installation + // "%prep", "%pretrans", "%pre", "%post", "%preun", "%postun", "%posttrans", "%verifyscript", "%clean" + // Write file mappings + sb append fileSection + // TODO - Write triggers... + // TODO - Write changelog... + + sb.toString + } +} \ No newline at end of file diff --git a/src/main/scala/com/typesafe/packager/rpm/RpmPlugin.scala b/src/main/scala/com/typesafe/packager/rpm/RpmPlugin.scala new file mode 100644 index 000000000..a84a76d8d --- /dev/null +++ b/src/main/scala/com/typesafe/packager/rpm/RpmPlugin.scala @@ -0,0 +1,80 @@ +package com.typesafe.packager +package rpm + +import linux._ +import sbt._ + +object Keys { + // METADATA keys. + def name = sbt.Keys.name + def version = sbt.Keys.version + def maintainer = linux.Keys.maintainer + def packageArchitecture = linux.Keys.packageArchitecture + def packageDescription = linux.Keys.packageDescription + val rpmVendor = SettingKey[String]("rpm-vendor", "Name of the vendor for this RPM.") + val rpmOs = SettingKey[String]("rpm-os", "Name of the os for this RPM.") + val rpmRelease = SettingKey[String]("rpm-release", "Special release number for this rpm (vs. the software).") + val rpmMetadata = SettingKey[RpmMetadata]("rpm-metadata", "Metadata associated with the generated RPM.") + + // DESCRIPTION KEYS + val rpmSummary = SettingKey[Option[String]]("rpm-summary", "Summary of the contents of an RPM package.") + val rpmLicense = SettingKey[Option[String]]("rpm-license", "License of the code within the RPM.") + val rpmDistribution = SettingKey[Option[String]]("rpm-distribution") + val rpmUrl = SettingKey[Option[String]]("rpm-url", "Url to include in the RPM.") + val rpmGroup = SettingKey[Option[String]]("rpm-group", "Group to associate with the RPM.") + val rpmPackager = SettingKey[Option[String]]("rpm-packger", "Person who packaged this rpm.") + val rpmIcon = SettingKey[Option[String]]("rpm-icon", "name of the icon to use with this RPM.") + val rpmDescription = SettingKey[RpmDescription]("rpm-description", "Description of this rpm.") + + // DEPENDENCIES + val rpmProvides = SettingKey[Seq[String]]("rpm-provides", "Packages this RPM provides.") + val rpmRequirements = SettingKey[Seq[String]]("rpm-requirements", "Packages this RPM requires.") + val rpmPrerequisites = SettingKey[Seq[String]]("rpm-prerequisites", "Packages this RPM need *before* installation.") + val rpmObsoletes = SettingKey[Seq[String]]("rpm-obsoletes", "Packages this RPM makes obsolete.") + val rpmConflicts = SettingKey[Seq[String]]("rpm-conflicts", "Packages this RPM conflicts with.") + val rpmDependencies = SettingKey[RpmDependencies]("rpm-dependencies", "Configuration of dependency info for this RPM.") + + // SPEC + def linuxPackageMappings = linux.Keys.linuxPackageMappings + val rpmSpecConfig = TaskKey[RpmSpec]("rpm-spec-config", "All the configuration for an RPM .spec file.") + + // Building + def target = sbt.Keys.target + def packageBin = sbt.Keys.packageBin + + def streams = sbt.Keys.streams +} + +object RpmPlugin extends Plugin { + import Keys._ + + val Rpm = config("rpm") + + def rpmSettings: Seq[Setting[_]] = Seq( + rpmSummary := None, + rpmLicense := None, + rpmDistribution := None, + rpmUrl := None, + rpmGroup := None, + rpmPackager := None, + rpmIcon := None, + rpmProvides := Seq.empty, + rpmRequirements := Seq.empty, + rpmPrerequisites := Seq.empty, + rpmObsoletes := Seq.empty, + rpmConflicts := Seq.empty, + target in Rpm <<= target(_ / "rpm") + ) ++ inConfig(Rpm)(Seq( + rpmMetadata <<= + (name, version, rpmRelease, packageArchitecture, rpmVendor, rpmOs) apply (RpmMetadata.apply), + rpmDescription <<= + (rpmSummary, rpmLicense, rpmDistribution, rpmUrl, rpmGroup, rpmPackager, rpmIcon) apply RpmDescription, + rpmDependencies <<= + (rpmProvides, rpmRequirements, rpmPrerequisites, rpmObsoletes, rpmConflicts) apply RpmDependencies, + rpmSpecConfig <<= + (rpmMetadata, rpmDescription, rpmDependencies, linuxPackageMappings) map RpmSpec, + packageBin <<= (rpmSpecConfig, target, streams) map { (spec, dir, s) => + RpmHelper.buildRpm(spec, dir, s.log) + } + )) +} \ No newline at end of file