Module: Jamf::Composer

Defined in:
lib/jamf/composer.rb

Overview

This module provides two methods for building very simple Casper-happy .pkg and .dmg packages for deployment.

Unlike Composer.app from JAMF, this module currently doesn’t offer a way to do a before/after disk scan and use the differences to build the root folder from which the package is built. Nor does the module support editing the pre/post install scripts in .pkgs.

The ‘root folder’, a folder representing the root filesystem of the target machine where the package will be installed, must already exist and be fully populated and with correct permissions.

Constant Summary collapse

PKG_UTIL =

the apple pkgutil tool

Pathname.new '/usr/sbin/pkgutil'
PKGBUILD =

The location of the cli tool for making .pkgs

Pathname.new '/usr/bin/pkgbuild'
PKG_BUNDLE_ID_PFX =

the default bundle identifier prefix for pkgs

'ruby-jss-composer'.freeze
HDI_UTIL =

Apple’s hdiutil for making dmgs

'/usr/bin/hdiutil'.freeze
DEFAULT_OUT_DIR =

Where to save the output ?

Pathname.new '/Users/Shared'

Class Method Summary collapse

Class Method Details

.mk_dmg(name, root, out_dir = DEFAULT_OUT_DIR) ⇒ Pathname

Make a casper-happy .dmg out of a root folder, permissions are assumed to be correct.

Parameters:

  • name (String)

    The name of the .dmg, the suffix will be added if needed

  • root (String, Pathname)

    the path to the “root folder” representing the root file system of the target install drive

  • out_dir (String, Pathname) (defaults to: DEFAULT_OUT_DIR)

    the folder in which the .pkg will be created. Defaults to DEFAULT_OUT_DIR

Returns:

  • (Pathname)

    the local path to the new .dmg



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/jamf/composer.rb', line 172

def self.mk_dmg(name, root, out_dir = DEFAULT_OUT_DIR)
  dmg_filename = "#{name}.dmg"
  dmg_vol = name
  dmg_out = Pathname.new "#{out_dir}/#{dmg_filename}"
  if dmg_out.exist?
    mv_to = dmg_out.dirname + "#{dmg_out.basename}.#{Time.now.strftime('%Y%m%d%H%M%S')}"
    dmg_out.rename mv_to
  end # if dmg out exist

  ### TODO - this may need to be sudo'd to handle proper internal permissions.
  system "#{HDI_UTIL} create -volname '#{dmg_vol}' -scrub -srcfolder '#{root}' '#{dmg_out}'"

  raise 'There was an error building the .dmg' unless $?.exitstatus.zero?
  Pathname.new dmg_out
end

.mk_pkg(name, version, root, **opts) ⇒ Pathname

Make a casper-happy .pkg out of a root folder, permissions are assumed to be correct.

Parameters:

  • name (String)

    the name of the .pkg. The .pkg suffix will be added if not present

  • version (String)

    the version of the .pkg, needed for building the .pkg

  • root (String, Pathname)

    the path to the ‘root folder’ representing the root file system of the target install drive

  • opts (Hash)

    the options for building the .pkg

Options Hash (**opts):

  • :bundle_id_prefix (String)

    the pkg bundle identifier prefix. If no :pkg_id is provided, one is made using this prefix and the name provided. e.g. ‘com.mycompany’ Defaults to ‘PKG_BUNDLE_ID_PFX’. See ‘man pkgbuild’ for more info

  • :out_dir (String, Pathname)

    he folder in which the .pkg will be created. Defaults to DEFAULT_OUT_DIR

  • :preserve_ownership (Boolean)

    If true, the owner/group of the rootpath are preserved. Default is false: they become the pkgbuild/installer ‘recommended’ (root/wheel or root/admin)

  • :signing_identity (String)

    the optional name of the signing identity (certificate) to use for signing the pkg. See ‘man pkgbuild` for details

  • :signing_options (String)

    the optional string of options to pass to pkgbuild. See ‘man pkgbuild` for details

Returns:

  • (Pathname)

    the local path to the new .pkg

Raises:



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/jamf/composer.rb', line 95

def self.mk_pkg(name, version, root, **opts)
  raise NoSuchItemError, "Missing pkgbuild tool. Please make sure you're running 10.8 or later." unless PKGBUILD.executable?

  opts[:out_dir] ||= DEFAULT_OUT_DIR
  opts[:bundle_id_prefix] ||= PKG_BUNDLE_ID_PFX

  pkg_filename = name.end_with?('.pkg') ? name : name + '.pkg'
  pkg_id = opts[:pkg_id]
  pkg_id ||= opts[:bundle_id_prefix] + '.' + name
  pkg_out = "#{opts[:out_dir]}/#{pkg_filename}"
  pkg_ownership = opts[:preserve_ownership] ? 'preserve' : 'recommended'

  if opts[:signing_identity]
    signing = "--sign '#{opts[:signing_identity]}'"
    signing << " #{opts[:signing_options]}" if opts[:signing_options]
  else
    signing = ''
  end # if opts[:signing_identity]

  ### first, run 'analyze' to get a 'component plist' in which we can change some settings
  ### for any bundles in the root (bundles like .apps, frameworks, plugins, etc..)
  ###
  ### we edit the settings thus:
  ### BundleOverwriteAction = upgrade, totally replace any version current on disk
  ### BundleIsVersionChecked = false, allow us to install regardless of what version is currently installed
  ### BundleIsRelocatable = false,  if there's a version of this in some other location, Do Not move this one there after installation
  ### BundleHasStrictIdentifier = false, don't care if there's something at the install path with a different bundle id.
  ###
  ### In other words, just install the thing!
  ### (see 'man pkgbuild' for more info)
  ###
  ###
  comp_plist_out = Pathname.new "/tmp/#{PKG_BUNDLE_ID_PFX}-#{pkg_filename}.plist"
  system "#{PKGBUILD} --analyze --root '#{root}' '#{comp_plist_out}'"
  comp_plist = JSS.parse_plist comp_plist_out

  ### if the plist is empty, there are no bundles in the pkg
  if comp_plist[0].nil?
    comp_plist_arg = ''
  else
    ### otherwise, edit the bundle dictionaries
    comp_plist.each do |bndl|
      bndl.delete 'ChildBundles' if bndl['ChildBundles']
      bndl['BundleOverwriteAction'] = 'upgrade'
      bndl['BundleIsVersionChecked'] = false
      bndl['BundleIsRelocatable'] = false
      bndl['BundleHasStrictIdentifier'] = false
    end
    ### write out the edits
    comp_plist_out.open('w') { |f| f.write JSS.xml_plist_from(comp_plist) }
    comp_plist_arg = "--component-plist '#{comp_plist_out}'"
  end

  ### now build the pkg
  begin
    it_built = system "#{PKGBUILD} --identifier '#{pkg_id}' --version '#{version}' --ownership #{pkg_ownership} --install-location / --root '#{root}' #{signing} #{comp_plist_arg} '#{pkg_out}'"

    raise 'There was an error building the .pkg' unless it_built
  ensure
    comp_plist_out.delete if comp_plist_out.exist?
  end

  Pathname.new pkg_out
end