lib/xcode/install.rb in xcode-install-1.0.1 vs lib/xcode/install.rb in xcode-install-1.1.0

- old
+ new

@@ -1,13 +1,15 @@ require 'fileutils' require 'pathname' require 'spaceship' +require 'json' require 'rubygems/version' require 'xcode/install/command' require 'xcode/install/version' module XcodeInstall + CACHE_DIR = Pathname.new("#{ENV['HOME']}/Library/Caches/XcodeInstall") class Curl COOKIES_PATH = Pathname.new('/tmp/curl-cookies.txt') def fetch(url, directory = nil, cookies = nil, output = nil, progress = true) options = cookies.nil? ? '' : "-b '#{cookies}' -c #{COOKIES_PATH}" @@ -169,11 +171,10 @@ end Spaceship.client end end - CACHE_DIR = Pathname.new("#{ENV['HOME']}/Library/Caches/XcodeInstall") LIST_FILE = CACHE_DIR + Pathname.new('xcodes.bin') MINIMUM_VERSION = Gem::Version.new('4.3') SYMLINK_PATH = Pathname.new('/Applications/Xcode.app') def enable_developer_mode @@ -265,36 +266,166 @@ puts `/usr/sbin/spctl --assess --verbose=4 --type execute #{path}` $?.exitstatus == 0 end end + class Simulator + attr_reader :version + attr_reader :name + attr_reader :identifier + attr_reader :source + attr_reader :xcode + + def initialize(downloadable) + @version = Gem::Version.new(downloadable['version']) + @install_prefix = apply_variables(downloadable['userInfo']['InstallPrefix']) + @name = apply_variables(downloadable['name']) + @identifier = apply_variables(downloadable['identifier']) + @source = apply_variables(downloadable['source']) + end + + def installed? + # FIXME: use downloadables' `InstalledIfAllReceiptsArePresentOrNewer` key + File.directory?(@install_prefix) + end + + def installed_string + installed? ? 'installed' : 'not installed' + end + + def to_s + "#{name} (#{installed_string})" + end + + def xcode + Installer.new.installed_versions.find do |x| + x.available_simulators.find do |s| + s.version == version + end + end + end + + def download + result = Curl.new.fetch(source, CACHE_DIR) + result ? dmg_path : nil + end + + def install + download unless dmg_path.exist? + prepare_package unless pkg_path.exist? + puts "Please authenticate to install #{name}..." + `sudo installer -pkg #{pkg_path} -target /` + fail Informative, "Could not install #{name}, please try again" unless installed? + source_receipts_dir = '/private/var/db/receipts' + target_receipts_dir = "#{@install_prefix}/System/Library/Receipts" + FileUtils.mkdir_p(target_receipts_dir) + FileUtils.cp("#{source_receipts_dir}/#{@identifier}.bom", target_receipts_dir) + FileUtils.cp("#{source_receipts_dir}/#{@identifier}.plist", target_receipts_dir) + puts "Successfully installed #{name}" + end + + :private + + def prepare_package + puts 'Mounting DMG' + mount_location = `hdiutil mount -nobrowse -noverify #{dmg_path}`.scan(/\/Volumes.*\n/).first.chomp + puts 'Expanding pkg' + expanded_pkg_path = CACHE_DIR + identifier + FileUtils.rm_rf(expanded_pkg_path) + `pkgutil --expand #{mount_location}/*.pkg #{expanded_pkg_path}` + puts "Expanded pkg into #{expanded_pkg_path}" + puts 'Unmounting DMG' + `umount #{mount_location}` + puts 'Setting package installation location' + package_info_path = expanded_pkg_path + 'PackageInfo' + package_info_contents = File.read(package_info_path) + File.open(package_info_path, 'w') do |f| + f << package_info_contents.sub('pkg-info', %(pkg-info install-location="#{@install_prefix}")) + end + puts 'Rebuilding package' + `pkgutil --flatten #{expanded_pkg_path} #{pkg_path}` + FileUtils.rm_rf(expanded_pkg_path) + end + + def dmg_path + CACHE_DIR + Pathname.new(source).basename + end + + def pkg_path + CACHE_DIR + "#{identifier}.pkg" + end + + def apply_variables(template) + variable_map = { + '$(DOWNLOADABLE_VERSION_MAJOR)' => version.to_s.split('.')[0], + '$(DOWNLOADABLE_VERSION_MINOR)' => version.to_s.split('.')[1], + '$(DOWNLOADABLE_IDENTIFIER)' => identifier, + '$(DOWNLOADABLE_VERSION)' => version.to_s + }.freeze + variable_map.each do |key, value| + next unless template.include?(key) + template.sub!(key, value) + end + template + end + end + class InstalledXcode attr_reader :path attr_reader :version + attr_reader :bundle_version + attr_reader :uuid + attr_reader :downloadable_index_url + attr_reader :available_simulators def initialize(path) @path = Pathname.new(path) - @version = get_version end + def version + @version ||= get_version + end + + def bundle_version + @bundle_version ||= Gem::Version.new(plist_entry(':DTXcode').to_i.to_s.split(//).join('.')) + end + + def uuid + @uuid ||= plist_entry(':DVTPlugInCompatibilityUUID') + end + + def downloadable_index_url + @downloadable_index_url ||= "https://devimages.apple.com.edgekey.net/downloads/xcode/simulators/index-#{bundle_version}-#{uuid}.dvtdownloadableindex" + end + def approve_license license_path = "#{@path}/Contents/Resources/English.lproj/License.rtf" license_id = IO.read(license_path).match(/^EA\d{4}/) license_plist_path = '/Library/Preferences/com.apple.dt.Xcode.plist' `sudo rm -rf #{license_plist_path}` `sudo /usr/libexec/PlistBuddy -c "add :IDELastGMLicenseAgreedTo string #{license_id}" #{license_plist_path}` `sudo /usr/libexec/PlistBuddy -c "add :IDEXcodeVersionForAgreedToGMLicense string #{@version}" #{license_plist_path}` end + def available_simulators + @available_simulators ||= JSON.parse(`curl -Ls #{downloadable_index_url} | plutil -convert json -o - -`)['downloadables'].map do |downloadable| + Simulator.new(downloadable) + end + end + def install_components `sudo installer -pkg #{@path}/Contents/Resources/Packages/MobileDevice.pkg -target /` osx_build_version = `sw_vers -buildVersion`.chomp tools_version = `/usr/libexec/PlistBuddy -c "Print :ProductBuildVersion" "#{@path}/Contents/version.plist"`.chomp cache_dir = `getconf DARWIN_USER_CACHE_DIR`.chomp `touch #{cache_dir}com.apple.dt.Xcode.InstallCheckCache_#{osx_build_version}_#{tools_version}` end :private + + def plist_entry(keypath) + `/usr/libexec/PlistBuddy -c "Print :#{keypath}" "#{path}/Contents/Info.plist"`.chomp + end def get_version output = `DEVELOPER_DIR='' "#{@path}/Contents/Developer/usr/bin/xcodebuild" -version` return '0.0' if output.nil? # ¯\_(ツ)_/¯ output.split("\n").first.split(' ')[1]