Skip to content

Instantly share code, notes, and snippets.

@AlanQuatermain
Created January 6, 2013 16:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlanQuatermain/4468499 to your computer and use it in GitHub Desktop.
Save AlanQuatermain/4468499 to your computer and use it in GitHub Desktop.
A simple script which will change the root-paths of components in a PackageMaker document at the base of a new Xcode Archive, all without opening PackageMaker itself. Very useful for projects where you have a lot of separate components (and thus separate .xcarchive folders), which can take a while to update from the PackageMaker application afte…
#!/usr/bin/ruby
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A copy of the GNU General Public License can be found at
# <http://www.gnu.org/licenses/gpl-3.0.html>.
require 'optparse'
require 'rexml/document'
require 'rexml/formatters/transitive'
@@Doc = nil
@@ComponentRefs = Hash.new
@@CurrentID = nil
@@ComponentPaths = Hash.new
@@Verbose = false
@@TrialRun = false
OptionParser.new do |opts|
opts.banner = "Usage: update_package_archive (-c <component ID> -r <.../app.xcarchive>)... -d <x.pmdoc>"
opts.separator ""
opts.separator "Modifies an existing (and fully-set-up) PackageMaker document to point a package component"
opts.separator "at a new Xcode Archive root directory."
opts.separator ""
opts.separator "This script is designed to work only with components based on Xcode 4 archives (folders ending"
opts.separator "in .xcarchive) containing a 'Products' subfolder which is used as the component's root folder."
opts.separator ""
opts.separator "Each time a new build is made, a new .xcarchive folder is created. It is a fairly involved"
opts.separator "process to edit an existing PackageMaker document to point its root at the new archive and to"
opts.separator "set recommended permissions on everything within that folder. Internally however the document"
opts.separator "contains only relative path information along with (in a few different places) the path to the"
opts.separator "component root. This script will find the references to the package component designated by the"
opts.separator "--component-id parameter and replace the portion of that path ending in a .xcarchive item with a"
opts.separator "new .xcarchive path specified using the --component-root parameter. It can operate on multiple"
opts.separator "sub-packages/components at a time, meaning that you can supply multiple --component-id and"
opts.separator "--component-root parameters, however they *must* be paired, with each id preceding a root."
opts.separator ""
opts.separator "Options:"
opts.on_tail( "-h", "--help", "Show this message.", :NONE) do |arg|
puts opts
exit
end
opts.on_tail("-v", "--verbose", Boolean, "Print out scanning/replacement information while running.", :NONE) do |arg|
@@Verbose = arg
end
opts.on_tail("-t", "--trial-run", Boolean, "Don't update any files. Implies -v.", :NONE) do |arg|
@@DryRun = arg
@@Verbose = arg
end
opts.on("-c", "--component-id",
"The identifier (i.e. com.mycompany.mycomponent) specifying the component to act upon.", :REQUIRED) do |arg|
@@CurrentID = arg
end
opts.on("-r", "--component-root",
"The new .xcarchive folder to use as the component's root. Actually uses the 'Products' folder",
"within the .xcarchive bundle.", :REQUIRED) do |arg|
fail "Must specify a package ID before each package root." if @@CurrentID.nil?
path = arg.chomp('/')
fail "Root path '#{path}' does not refer to a .xcarchive file." unless File.basename(path) =~ /\.xcarchive$/
@@ComponentRefs[@@CurrentID] = path
@@CurrentID = nil
end
opts.on("-d", "--doc", "The PackageMaker document upon which to act.", :REQUIRED) do |arg|
@@Doc = arg
end
end.parse!
if @@ComponentRefs.empty? or @@Doc.nil?
fail "#{opts}"
end
# Look for the document & scan its contents
Dir.glob("#{@@Doc}/*.xml") do |filename|
# skip index.xml, it's not interesting to us
next if File.basename(filename) == 'index.xml'
# skip the contents files at this point
next if File.basename(filename) =~ /contents.xml$/
# we now have a package info file, read it
File.open(filename) do |file|
xmldoc = REXML::Document.new(file)
# store component refs
xmldoc.elements.each("pkgref/contents/component") do |element|
unless element.attributes['path'] =~ /\.xcarchive\/Products/
puts "Skipping non-xcarchive component '#{element.attributes['id']}'" if @@Verbose
next
end
puts "Found xcarchive component '#{element.attributes['id']}'" if @@Verbose
@@ComponentPaths[element.attributes['id']] = filename
end
# store package refs (not all packages install something PackageMaker considers a 'component')
xmldoc.elements.each("pkgref/config") do |config|
identifier = config.get_elements('identifier').first.text
puts "#{identifier}"
unless config.get_elements('installFrom').first.text =~ /\.xcarchive\/Products/
puts "Skipping non-xcarchive package ref '#{identifier}" if @@Verbose
next
end
puts "Found xcarchive package '#{identifier}" if @@Verbose
@@ComponentPaths[identifier] = filename
end
end
end
# Now go through each requested package ID and update its root
@@ComponentRefs.each do |id, root|
filename = @@ComponentPaths[id]
fail "Package ID '#{id}' not found." if filename.nil?
xmldoc = nil
File.open(filename) do |file|
xmldoc = REXML::Document.new(file)
xmldoc.inspect
# might be a component that needs tweaking
xmldoc.elements.each("pkgref/contents/component[@id='#{id}']") do |element|
path = element.attributes['path']
puts "Old path = #{path}" if @@Verbose
# Replace everything up to '.xcarchive' with the new root...
path.gsub!(/^.*\.xcarchive/, root)
puts "New path for component '#{id}' is '#{path}'" if @@Verbose
# ...and put it back into the element
element.add_attribute('path', path) unless @@TrialRun
puts "#{element.inspect}" if @@Verbose
end
# might be a package-- check if that's the case
next unless xmldoc.get_elements("/pkgref/config/identifier").first.text == id rescue next
# it's a package id-- almost certain to have an 'installFrom'
puts "#{xmldoc.get_elements('pkgref/config/installFrom')}"
xmldoc.elements.each('pkgref/config/installFrom') do |element|
path = element.text
puts "Old path = #{path}" if @@Verbose
# Replace everything up to '.xcarchive' with the new root...
path.gsub!(/^.*\.xcarchive/, root)
puts "New path for package '#{id}' is '#{path}'" if @@Verbose
# ...and put it back into the element
unless @@TrialRun
element.text = path
end
end
# close the file
file.close
end
# open it truncated for writing
unless @@TrialRun
File.open(filename, "w+") do |outfile|
# Write out the modified document
formatter = REXML::Formatters::Transitive.new(2)
formatter.write(xmldoc, outfile)
outfile.close
end
end
# now look at the xxx-contents.xml file -- only one change in here
contents = File.join(File.dirname(filename), File.basename(filename, '.xml') + '-contents.xml')
File.open(contents) do |file|
xmldoc = REXML::Document.new(file)
# first 'f' element below root (pkg-contents) is the one with a path (pt) attribute
element = xmldoc.get_elements("/pkg-contents/f").first
next if element.nil?
path = element.attributes['pt']
puts "Old path = #{path}" if @@Verbose
if path =~ /\.xcarchive\/Products/
# Replace everything up to '.xcarchive' with the new root
path.gsub!(/^.*\.xcarchive/, root)
puts "New path in '#{contents}' is '#{path}'" if @@Verbose
# ...and put it back into the element
element.add_attribute('pt', path) unless @@TrialRun
end
# close the file
file.close
end
# open it trancated for writing
unless @@TrialRun
File.open(contents, "w+") do |outfile|
# Write out the modified document
formatter = REXML::Formatters::Transitive.new(2)
formatter.write(xmldoc, outfile)
outfile.close
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment