Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #332 from timsutton/4df9617b8a3e71ac82b4dadb8cad28…

…cded66159f

OS X package support
  • Loading branch information...
commit 7ce72649473e8dbbf132520c2d6f64518aa87b3c 2 parents afc2daa + 4df9617
Jordan Sissel authored
4 .gitignore
View
@@ -5,6 +5,7 @@
build-*/*
fpm.wiki
*.gem
+*.pkg
# python
*.pyc
@@ -16,3 +17,6 @@ fpm.wiki
coverage
test/tmp
Gemfile.lock
+
+# OS X
+.DS_Store
1  lib/fpm.rb
View
@@ -6,3 +6,4 @@
require "fpm/package/deb"
require "fpm/package/rpm"
require "fpm/package/python"
+require "fpm/package/osxpkg"
165 lib/fpm/package/osxpkg.rb
View
@@ -0,0 +1,165 @@
+require "fpm/package"
+require "fpm/util"
+require "fileutils"
+require "fpm/package/dir"
+require 'tempfile' # stdlib
+require 'pathname' # stdlib
+require 'rexml/document' # stdlib
+
+# Use an OS X pkg built with pkgbuild.
+#
+# Supports input and output. Requires pkgbuild and (for input) pkgutil, part of a
+# standard OS X install in 10.7 and higher.
+class FPM::Package::OSXpkg < FPM::Package
+
+ # Map of what scripts are named.
+ SCRIPT_MAP = {
+ :before_install => "preinstall",
+ :after_install => "postinstall",
+ } unless defined?(SCRIPT_MAP)
+
+ POSTINSTALL_ACTIONS = [ "logout", "restart", "shutdown" ]
+ OWNERSHIP_OPTIONS = ["recommended", "preserve", "preserve-other"]
+
+ option "--identifier-prefix", "IDENTIFIER_PREFIX",
+ "Reverse domain prefix prepended to package identifier, " \
+ "ie. 'org.great.my'. If this is omitted, the identifer " \
+ "will be the package name."
+ option "--payload-free", :flag, "Define no payload, assumes use of script options.",
+ :default => false
+ option "--ownership", "OWNERSHIP",
+ "--ownership option passed to pkgbuild. Defaults to 'recommended'. " \
+ "See pkgbuild(1).", :default => 'recommended' do |value|
+ if !OWNERSHIP_OPTIONS.include?(value)
+ raise ArgumentError, "osxpkg-ownership value of '#{value}' is invalid. " \
+ "Must be one of #{OWNERSHIP_OPTIONS.join(", ")}"
+ end
+ value
+ end
+
+ option "--postinstall-action", "POSTINSTALL_ACTION",
+ "Post-install action provided in package metadata. " \
+ "Optionally one of '#{POSTINSTALL_ACTIONS.join("', '")}'." do |value|
+ if !POSTINSTALL_ACTIONS.include?(value)
+ raise ArgumentError, "osxpkg-postinstall-action value of '#{value}' is invalid. " \
+ "Must be one of #{POSTINSTALL_ACTIONS.join(", ")}"
+ end
+ value
+ end
+
+ dont_obsolete_paths = []
+ option "--dont-obsolete", "DONT_OBSOLETE_PATH",
+ "A file path for which to 'dont-obsolete' in the built PackageInfo. " \
+ "Can be specified multiple times." do |path|
+ dont_obsolete_paths << path
+ end
+
+ private
+ # return the identifier by prepending the reverse-domain prefix
+ # to the package name, else return just the name
+ def identifier
+ identifier = name.dup
+ if self.attributes[:osxpkg_identifier_prefix]
+ identifier.insert(0, "#{self.attributes[:osxpkg_identifier_prefix]}.")
+ end
+ identifier
+ end # def identifier
+
+ # scripts_path and write_scripts cribbed from deb.rb
+ def scripts_path(path=nil)
+ @scripts_path ||= build_path("Scripts")
+ FileUtils.mkdir(@scripts_path) if !File.directory?(@scripts_path)
+
+ if path.nil?
+ return @scripts_path
+ else
+ return File.join(@scripts_path, path)
+ end
+ end # def scripts_path
+
+ def write_scripts
+ SCRIPT_MAP.each do |scriptname, filename|
+ next unless script?(scriptname)
+
+ with(scripts_path(filename)) do |pkgscript|
+ @logger.info("Writing pkg script", :source => filename, :target => pkgscript)
+ File.write(pkgscript, script(scriptname))
+ # scripts are required to be executable
+ File.chmod(0755, pkgscript)
+ end
+ end
+ end # def write_scripts
+
+ # Returns path of a processed template PackageInfo given to 'pkgbuild --info'
+ # note: '--info' is undocumented:
+ # http://managingosx.wordpress.com/2012/07/05/stupid-tricks-with-pkgbuild
+ def pkginfo_template_path
+ pkginfo_template = Tempfile.open("fpm-PackageInfo")
+ pkginfo_data = template("osxpkg.erb").result(binding)
+ pkginfo_template.write(pkginfo_data)
+ pkginfo_template.close
+ pkginfo_template.path
+ end # def write_pkginfo_template
+
+ # Extract name and version from PackageInfo XML
+ def extract_info(package)
+ with(build_path("expand")) do |path|
+ doc = REXML::Document.new File.open(File.join(path, "PackageInfo"))
+ pkginfo_elem = doc.elements["pkg-info"]
+ identifier = pkginfo_elem.attribute("identifier").value
+ self.version = pkginfo_elem.attribute("version").value
+ # set name to the last dot element of the identifier
+ self.name = identifier.split(".").last
+ @logger.info("inferring name #{self.name} from pkg-id #{identifier}")
+ end
+ end # def extract_info
+
+ # Take a flat package as input
+ def input(input_path)
+ # TODO: Fail if it's a Distribution pkg or old-fashioned
+ expand_dir = File.join(build_path, "expand")
+ # expand_dir must not already exist for pkgutil --expand
+ safesystem("pkgutil --expand #{input_path} #{expand_dir}")
+
+ extract_info(input_path)
+
+ # extract Payload
+ safesystem("tar -xz -f #{expand_dir}/Payload -C #{staging_path}")
+ end # def input
+
+ # Output a pkgbuild pkg.
+ def output(output_path)
+ output_check(output_path)
+ raise FileAlreadyExists.new(output_path) if File.exists?(output_path)
+
+ temp_info = pkginfo_template_path
+
+ args = ["--identifier", identifier,
+ "--info", temp_info,
+ "--version", version.to_s,
+ "--ownership", attributes[:osxpkg_ownership]]
+
+ if self.attributes[:osxpkg_payload_free?]
+ args << "--nopayload"
+ else
+ args += ["--root", staging_path]
+ end
+
+ if attributes[:before_install_given?] or attributes[:after_install_given?]
+ write_scripts
+ args += ["--scripts", scripts_path]
+ end
+ args << output_path
+
+ safesystem("pkgbuild", *args)
+ FileUtils.remove_file(temp_info)
+ end # def output
+
+ def to_s(format=nil)
+ return super("NAME-VERSION.pkg") if format.nil?
+ return super(format)
+ end # def to_s
+
+ public(:input, :output, :identifier, :to_s)
+
+end # class FPM::Package::OSXpkg
73 spec/fpm/package/osxpkg_spec.rb
View
@@ -0,0 +1,73 @@
+require "spec_setup"
+require "fpm" # local
+require "fpm/package/osxpkg" # local
+
+describe FPM::Package::OSXpkg do
+
+ if %x{uname -s}.chomp != "Darwin"
+ Cabin::Channel.get("rspec").warn("Skipping OS X tests because " \
+ "this system is #{%x{uname -s}.chomp}, Darwin required")
+ end
+
+ describe "#identifier" do
+ it "should be of the form reverse.domain.pkgname" do
+ subject.name = "name"
+ subject.attributes[:osxpkg_identifier_prefix] = "org.great"
+ insist { subject.identifier } == \
+ "#{subject.attributes[:osxpkg_identifier_prefix]}.#{subject.name}"
+ end
+
+ it "should be the name only if a prefix was not given" do
+ subject.name = "name"
+ subject.attributes[:osxpkg_identifier_prefix] = nil
+ insist { subject.identifier } == subject.name
+ end
+ end
+
+ describe "#to_s" do
+ it "should have a default output usable as a filename" do
+ subject.name = "name"
+ subject.version = "123"
+
+ # We like the format 'name-version.pkg'
+ insist { subject.to_s } == "name-123.pkg"
+ end
+ end
+
+ describe "#output" do
+ before :all do
+ # output a package, use it as the input, set the subject to that input
+ # package. This helps ensure that we can write and read packages
+ # properly.
+ tmpfile = Tempfile.new("fpm-test-osxpkg")
+ @target = tmpfile.path
+ # The target file must not exist.
+ tmpfile.unlink
+
+ @original = FPM::Package::OSXpkg.new
+ @original.name = "name"
+ @original.version = "123"
+ @original.attributes[:osxpkg_identifier_prefix] = "org.my"
+ @original.output(@target)
+
+ @input = FPM::Package::OSXpkg.new
+ @input.input(@target)
+ end
+
+ after :all do
+ @original.cleanup
+ @input.cleanup
+ end # after
+
+ context "package attributes" do
+ it "should have the correct name" do
+ insist { @input.name } == @original.name
+ end
+
+ it "should have the correct version" do
+ insist { @input.version } == @original.version
+ end
+ end # package attributes
+ end # #output
+
+end # describe FPM::Package:OSXpkg
11 templates/osxpkg.erb
View
@@ -0,0 +1,11 @@
+<pkg-info
+<% if !attributes[:osxpkg_postinstall_action].nil? -%>postinstall-action="<%= attributes[:osxpkg_postinstall_action] %>"<% end -%>
+>
+<% if !attributes[:osxpkg_dont_obsolete].nil? -%>
+ <dont-obsolete>
+ <% attributes[:osxpkg_dont_obsolete].each do |filepath| -%>
+ <file path="<%= filepath %>"/>
+ <% end -%>
+ </dont-obsolete>
+<% end -%>
+</pkg-info>
Please sign in to comment.
Something went wrong with that request. Please try again.