From 2e9a2739175c0fcbae400eb0cd3913ec1c79f048 Mon Sep 17 00:00:00 2001 From: Brian Wallace Date: Mon, 21 Nov 2016 14:35:43 -0800 Subject: [PATCH] Introduce support for xip files which is the new distribution vehicle for xcode v8.0+. --- README.md | 1 + attributes/default.rb | 26 ++++++++-- files/parse_pbzx2.py | 80 +++++++++++++++++++++++++++++++ recipes/default.rb | 107 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 files/parse_pbzx2.py diff --git a/README.md b/README.md index 53456c1..974b6da 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Attributes |--------------------------------|--------|-----------------------------------------|--------------------------| | `['xcode']['url']` | String | URL to the Xcode DMG | `nil` | | `['xcode']['checksum']` | String | Checksum of the Xcode DMG | (in the attributes file) | +| `['xcode']['package_type']` | String | Type of Package ('dmg' or 'zip') | (in the attributes file) | | `['xcode']['cli']['url']` | String | URL to the Xcode Command-Line Tools DMG | `nil` | | `['xcode']['cli']['checksum']` | String | Checksum of the Xcode CLI DMG | (in the attributes file) | diff --git a/attributes/default.rb b/attributes/default.rb index 4699701..4219364 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -22,6 +22,7 @@ when /^10\.7/ default['xcode']['url'] = nil # should point to xcode4620419895a.dmg default['xcode']['checksum'] = '3057224339823dae8a56943380a438065e92cff1ad4ab5a6a84f94f7a94dc035' + default["xcode"]["package_type"] = "dmg" default['xcode']['last_gm_license'] = "EA0720" default['xcode']['version'] = "4.6.2" @@ -34,6 +35,7 @@ when /^10\.8/ default["xcode"]["url"] = nil # should point to xcode_5.1.1.dmg default["xcode"]["checksum"] = "5bd3c1792b695dae3c96065a9cc02215ec2fab6aecbf708a66b7d19fa65ff967" + default["xcode"]["package_type"] = "dmg" default["xcode"]["last_gm_license"] = "EA0720" default["xcode"]["version"] = "5.1.1" @@ -46,6 +48,7 @@ when /^10\.9/ default["xcode"]["url"] = nil # should point to Xcode_6.2.dmg default["xcode"]["checksum"] = "00545c078470c14e6a53204324e2c10283c18c86d3a9f580bf90cbe97c6c28ec" + default["xcode"]["package_type"] = "dmg" default["xcode"]["last_gm_license"] = "EA1187" default["xcode"]["version"] = "6.2" @@ -58,6 +61,7 @@ when /^10\.10/ default["xcode"]["url"] = nil # should point to Xcode_7.2.dmg default["xcode"]["checksum"] = "6f08ddabfb159143e9857de1668e5fdb04ba92b77297f7a1d50cf467be7222c8" + default["xcode"]["package_type"] = "dmg" default["xcode"]["last_gm_license"] = "EA1327" default["xcode"]["version"] = "7.2" @@ -68,10 +72,24 @@ default["xcode"]["cli"]["package_id"] = "com.apple.pkg.CLTools_Executables" default["xcode"]["cli"]["volumes_dir"] = "Command Line Developer Tools" when /^10\.11/ - default["xcode"]["url"] = nil # should point to Xcode_7.3.1.dmg - default["xcode"]["checksum"] = "bb0dedf613e86ecb46ced945913fa5069ab716a0ade1035e239d70dee0b2de1b" - default["xcode"]["last_gm_license"] = "EA1327" - default["xcode"]["version"] = "7.3.1" + default["xcode"]["url"] = nil # should point to Xcode_7.3.1.dmg or Xcode_8.1.0.xip + default["xcode"]["checksum"] = "30378e76f7d1adcf3573fc990bd7b46e0939b466b83338ba8f4290444462e5da" + default["xcode"]["package_type"] = "xip" + default["xcode"]["last_gm_license"] = "EA1421" + default["xcode"]["version"] = "8.1" + + default["xcode"]["cli"]["url"] = nil # should point to Command_Line_Tools_OS_X_10.11_for_Xcode_7.3.1.dmg + default["xcode"]["cli"]["checksum"] = "0c80753d207fa2254bcc1c880d4d8907071241f3f2e092c7caa87e340245835a" + default["xcode"]["cli"]["package_name"] = "Command Line Tools (OS X 10.11)" + default["xcode"]["cli"]["package_type"] = "pkg" + default["xcode"]["cli"]["package_id"] = "com.apple.pkg.CLTools_Executables" + default["xcode"]["cli"]["volumes_dir"] = "Command Line Developer Tools" +when /^10\.12/ + default["xcode"]["url"] = nil # should point to Xcode_7.3.1.dmg or Xcode_8.1.0.xip + default["xcode"]["checksum"] = "30378e76f7d1adcf3573fc990bd7b46e0939b466b83338ba8f4290444462e5da" + default["xcode"]["package_type"] = "xip" + default["xcode"]["last_gm_license"] = "EA1421" + default["xcode"]["version"] = "8.1" default["xcode"]["cli"]["url"] = nil # should point to Command_Line_Tools_OS_X_10.11_for_Xcode_7.3.1.dmg default["xcode"]["cli"]["checksum"] = "0c80753d207fa2254bcc1c880d4d8907071241f3f2e092c7caa87e340245835a" diff --git a/files/parse_pbzx2.py b/files/parse_pbzx2.py new file mode 100644 index 0000000..0e6cecf --- /dev/null +++ b/files/parse_pbzx2.py @@ -0,0 +1,80 @@ +# Source: https://gist.github.com/pudquick/ff412bcb29c9c1fa4b8d#file-parse_pbzx2-py +# +# v2 pbzx stream handler +# My personal writeup on the differences here: https://gist.github.com/pudquick/29fcfe09c326a9b96cf5 +# +# Pure python reimplementation of .cpio.xz content extraction from pbzx file payload originally here: +# http://www.tonymacx86.com/general-help/135458-pbzx-stream-parser.html +# +# Cleaned up C version (as the basis for my code) here, thanks to Pepijn Bruienne / @bruienne +# https://gist.github.com/bruienne/029494bbcfb358098b41 + +import struct, sys + +def seekread(f, offset=None, length=0, relative=True): + if (offset != None): + # offset provided, let's seek + f.seek(offset, [0,1,2][relative]) + if (length != 0): + return f.read(length) + +def parse_pbzx(pbzx_path): + section = 0 + xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section) + f = open(pbzx_path, 'rb') + # pbzx = f.read() + # f.close() + magic = seekread(f,length=4) + if magic != 'pbzx': + raise "Error: Not a pbzx file" + # Read 8 bytes for initial flags + flags = seekread(f,length=8) + # Interpret the flags as a 64-bit big-endian unsigned int + flags = struct.unpack('>Q', flags)[0] + xar_f = open(xar_out_path, 'wb') + while (flags & (1 << 24)): + # Read in more flags + flags = seekread(f,length=8) + flags = struct.unpack('>Q', flags)[0] + # Read in length + f_length = seekread(f,length=8) + f_length = struct.unpack('>Q', f_length)[0] + xzmagic = seekread(f,length=6) + if xzmagic != '\xfd7zXZ\x00': + # This isn't xz content, this is actually _raw decompressed cpio_ chunk of 16MB in size... + # Let's back up ... + seekread(f,offset=-6,length=0) + # ... and split it out ... + f_content = seekread(f,length=f_length) + section += 1 + decomp_out = '%s.part%02d.cpio' % (pbzx_path, section) + g = open(decomp_out, 'wb') + g.write(f_content) + g.close() + # Now to start the next section, which should hopefully be .xz (we'll just assume it is ...) + xar_f.close() + section += 1 + new_out = '%s.part%02d.cpio.xz' % (pbzx_path, section) + xar_f = open(new_out, 'wb') + else: + f_length -= 6 + # This part needs buffering + f_content = seekread(f,length=f_length) + tail = seekread(f,offset=-2,length=2) + xar_f.write(xzmagic) + xar_f.write(f_content) + if tail != 'YZ': + xar_f.close() + raise "Error: Footer is not xar file footer" + try: + f.close() + xar_f.close() + except: + pass + +def main(): + result = parse_pbzx(sys.argv[1]) + print "Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file" + +if __name__ == '__main__': + main() diff --git a/recipes/default.rb b/recipes/default.rb index 3a05274..122935b 100644 --- a/recipes/default.rb +++ b/recipes/default.rb @@ -17,10 +17,109 @@ # limitations under the License. # -dmg_package "Xcode" do - source node['xcode']['url'] - checksum node['xcode']['checksum'] - action :install +Chef::Log.info("xcode package type: '#{node['xcode']['package_type']}'.") + +case node['xcode']['package_type'] +when 'dmg' + dmg_package "Xcode" do + source node['xcode']['url'] + checksum node['xcode']['checksum'] + action :install + end +when 'xip' + + # TODO: add a version check. (i.e. "xcodebuild -version | grep #{node['xcode']['version']}") + if ::Dir.exist? "/Applications/Xcode.app" + Chef::Log.info("xcode version #{node['xcode']['version']} is alread installed. Nothing to do.") + else + # Instructions for how to install XCode via the command line were taken from: + # http://stackoverflow.com/a/39489446 + Chef::Log.info("xcode is NOT installed. Installing...") + + # extract the name of the archive from the download URL + file_name = File.basename(node['xcode']['url']) + + # download the remote xip archive + remote_file "#{Chef::Config[:file_cache_path]}/#{file_name}" do + source node['xcode']['url'] + checksum node['xcode']['checksum'] + backup false + mode 0644 + owner "root" + group "wheel" + action :create + not_if { ::File.exist? "#{Chef::Config[:file_cache_path]}/#{file_name}" } + end + + execute 'Verify the signature and certificate chain that signed the archive' do + command "pkgutil --check-signature #{Chef::Config[:file_cache_path]}/#{file_name}" + cwd Chef::Config[:file_cache_path] + end + + execute 'Extract the xcode xip archive' do + command "xar -xf #{Chef::Config[:file_cache_path]}/#{file_name}" + cwd Chef::Config[:file_cache_path] + end + + # Obtain a PBZX v2 unpacker and... unpack the packed stuff. + # Install PBZX v2 unpacker + cookbook_file "#{Chef::Config[:file_cache_path]}/parse_pbzx2.py" do + source 'parse_pbzx2.py' + owner 'root' + group 'wheel' + mode '0755' + action :create + end + + # Unpack the PBZX stream + execute 'Unpack the PBZX stream' do + command 'python parse_pbzx2.py Content' + cwd Chef::Config[:file_cache_path] + end + + # Decompress the archive (there should only be one chunk, "part00"). + execute 'Unpack the CPIO archive as a privileged user' do + command 'cpio -izmdu 0