Merge pull request #332 from timsutton/4df9617b8a3e71ac82b4dadb8cad28cded66159f

OS X package support
This commit is contained in:
Jordan Sissel 2013-01-07 10:06:43 -08:00
commit 78e5d168e3
5 changed files with 254 additions and 0 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -6,3 +6,4 @@ require "fpm/package/gem"
require "fpm/package/deb"
require "fpm/package/rpm"
require "fpm/package/python"
require "fpm/package/osxpkg"

165
lib/fpm/package/osxpkg.rb Normal file
View File

@ -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

View File

@ -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 Normal file
View File

@ -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>