├── lib ├── version.rb ├── bin │ └── unsign ├── update_xcode_plugins.rb ├── bundle.rb ├── xcode_plugin.rb ├── plugins_updater.rb ├── cli.rb ├── xcode_unsigner.rb ├── xcode.rb └── launch_agent.rb ├── test ├── HelloWorld │ ├── HelloWorld │ │ ├── HelloWorld.h │ │ ├── HelloWorld.m │ │ └── Info.plist │ └── HelloWorld.xcodeproj │ │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── inket.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── xcuserdata │ │ └── inket.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── HelloWorld.xcscheme │ │ └── project.pbxproj └── xcode.rb ├── Gemfile ├── Rakefile ├── .travis.yml ├── .gitignore ├── bin └── update_xcode_plugins ├── update_xcode_plugins.gemspec ├── LICENSE └── README.md /lib/version.rb: -------------------------------------------------------------------------------- 1 | class UpdateXcodePlugins 2 | VERSION = '0.4.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/bin/unsign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inket/update_xcode_plugins/HEAD/lib/bin/unsign -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld/HelloWorld.h: -------------------------------------------------------------------------------- 1 | // 2 | // HelloWorld.h 3 | // HelloWorld 4 | // 5 | // Created by inket on 30/7/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface HelloWorld : NSObject 12 | @end -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld.xcodeproj/project.xcworkspace/xcuserdata/inket.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inket/update_xcode_plugins/HEAD/test/HelloWorld/HelloWorld.xcodeproj/project.xcworkspace/xcuserdata/inket.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | # Will automatically pull in this gem and all its 4 | # dependencies specified in the gemspec 5 | gem 'update_xcode_plugins', path: File.expand_path('..', __FILE__) 6 | 7 | # These are development dependencies 8 | gem 'rake' 9 | gem 'minitest' 10 | gem 'coveralls', require: false 11 | -------------------------------------------------------------------------------- /lib/update_xcode_plugins.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'fileutils' 3 | require_relative 'cli' 4 | require 'colorize' unless CLI.no_colors? 5 | require 'inquirer' unless CLI.non_interactive? 6 | require_relative 'version' 7 | require_relative 'plugins_updater' 8 | require_relative 'xcode_unsigner' 9 | require_relative 'launch_agent' 10 | -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld.xcodeproj/xcuserdata/inket.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SuppressBuildableAutocreation 6 | 7 | B21177F61D4C896700F528CC 8 | 9 | primary 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | 4 | require 'rake/testtask' 5 | 6 | Rake::TestTask.new do |t| 7 | t.pattern = "test/*.rb" 8 | end 9 | 10 | gemspec_filename = 'update_xcode_plugins.gemspec' 11 | gemspec = eval(File.read(gemspec_filename)) 12 | gem_filename = "#{gemspec.full_name}.gem" 13 | 14 | task default: gem_filename 15 | 16 | file gem_filename => gemspec.files + [gemspec_filename] do 17 | system "rm #{gem_filename} 2>/dev/null" 18 | system "gem uninstall -ax #{gemspec.name} 2>/dev/null" 19 | system "gem build #{gemspec_filename}" 20 | system "gem install #{gemspec.full_name}.gem" 21 | end 22 | -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld/HelloWorld.m: -------------------------------------------------------------------------------- 1 | // 2 | // HelloWorld.m 3 | // HelloWorld 4 | // 5 | // Created by inket on 30/7/16. 6 | // 7 | // 8 | 9 | #import "HelloWorld.h" 10 | 11 | @implementation HelloWorld 12 | 13 | + (void)pluginDidLoad:(NSBundle *)plugin { 14 | NSArray *allowedLoaders = [plugin objectForInfoDictionaryKey:@"me.delisa.XcodePluginBase.AllowedLoaders"]; 15 | if ([allowedLoaders containsObject:[[NSBundle mainBundle] bundleIdentifier]]) { 16 | NSLog(@"HelloWorld plugin loaded!"); 17 | 18 | NSString* path = [@"~/Desktop/success" stringByExpandingTildeInPath]; 19 | [[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil]; 20 | } 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | osx_image: xcode9.3 5 | env: TRAVIS_XCODE_VERSION=93 6 | - os: osx 7 | osx_image: xcode9.2 8 | env: TRAVIS_XCODE_VERSION=92 9 | - os: osx 10 | osx_image: xcode9.1 11 | env: TRAVIS_XCODE_VERSION=91 12 | - os: osx 13 | osx_image: xcode9 14 | env: TRAVIS_XCODE_VERSION=9 15 | - os: osx 16 | osx_image: xcode8.3 17 | env: TRAVIS_XCODE_VERSION=83 18 | - os: osx 19 | osx_image: xcode8 20 | env: TRAVIS_XCODE_VERSION=8 21 | - os: osx 22 | osx_image: xcode7.3 23 | env: TRAVIS_XCODE_VERSION=73 24 | install: 25 | - bundle install 26 | - rake 27 | script: 28 | - rake test 29 | notifications: 30 | email: false 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | Gemfile.lock 32 | # .ruby-version 33 | # .ruby-gemset 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | -------------------------------------------------------------------------------- /lib/bundle.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cli' 2 | 3 | class Bundle 4 | attr_accessor :path 5 | 6 | def initialize(path) 7 | self.path = path.strip 8 | end 9 | 10 | def valid? 11 | false 12 | end 13 | 14 | def info_path 15 | "#{path}/Contents/Info.plist" 16 | end 17 | 18 | def bundle_identifier 19 | defaults_read("CFBundleIdentifier") 20 | end 21 | 22 | def version 23 | defaults_read('CFBundleShortVersionString') 24 | end 25 | 26 | def defaults_read(key) 27 | plist_path = "#{path}/Contents/Info" 28 | `defaults read "#{plist_path}" #{key}`.strip 29 | end 30 | 31 | def defaults_write(*args) 32 | plist_path = "#{path}/Contents/Info" 33 | command = "defaults write \"#{plist_path}\" #{args.join(' ')}" 34 | 35 | if CLI.dry_run? 36 | puts command 37 | else 38 | `#{command}`.strip 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /bin/update_xcode_plugins: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/update_xcode_plugins' 4 | 5 | begin 6 | if CLI.uninstall_launch_agent? 7 | LaunchAgent.uninstall 8 | else 9 | LaunchAgent.update_if_stale(__FILE__) 10 | 11 | if CLI.install_launch_agent? 12 | LaunchAgent.install(__FILE__) 13 | elsif CLI.unsign_xcode? 14 | XcodeUnsigner.unsign_xcode 15 | elsif CLI.restore_xcode? 16 | XcodeUnsigner.restore_xcode 17 | else 18 | if CLI.non_interactive? 19 | # This tool is ran by launchctl whenever a plugin is added/modified, 20 | # which presents a race condition if the plugin is still in the process 21 | # of being modified. Counter that by sleeping for a few seconds. 22 | sleep 2 23 | end 24 | 25 | PluginsUpdater.update_plugins 26 | end 27 | end 28 | rescue Interrupt 29 | end 30 | -------------------------------------------------------------------------------- /update_xcode_plugins.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'update_xcode_plugins' 5 | s.version = UpdateXcodePlugins::VERSION 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ['Mahdi Bchetnia'] 8 | s.email = ['injekter@gmail.com'] # Real email is on my website ;) 9 | s.homepage = 'http://github.com/inket/update_xcode_plugins' 10 | s.summary = 'Updates Xcode plug-ins to match the installed Xcode versions.' 11 | s.description = 'This tool adds the missing UUIDs into the installed Xcode plug-ins so that they can be loaded by newer versions of Xcode.' 12 | 13 | s.required_rubygems_version = '>= 1.3.6' 14 | s.rubyforge_project = 'update_xcode_plugins' 15 | s.files = Dir['{lib}/**/*.rb', 'lib/bin/*', 'bin/*', 'LICENSE', '*.md'] 16 | s.require_path = 'lib' 17 | s.executables = ['update_xcode_plugins'] 18 | s.license = 'MIT' 19 | 20 | s.add_runtime_dependency 'colorize', '~> 0.8.1' 21 | s.add_runtime_dependency 'inquirer', '~> 0.2.1' 22 | end 23 | -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | DVTPlugInCompatibilityUUIDs 22 | 23 | NSPrincipalClass 24 | HelloWorld 25 | XC4Compatible 26 | 27 | XCPluginHasUI 28 | 29 | me.delisa.XcodePluginBase.AllowedLoaders 30 | 31 | com.apple.dt.Xcode 32 | com.apple.dt.xcodebuild 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/xcode_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle' 2 | 3 | class XcodePlugin < Bundle 4 | def self.find_plugins 5 | plugins_path = "#{Dir.home}/Library/Application Support/Developer/Shared/Xcode/Plug-ins/" 6 | 7 | unless Dir.exist?(plugins_path) 8 | puts "Couldn't find Plug-ins directory." 9 | return [] 10 | end 11 | 12 | Dir.entries(plugins_path).collect do |plugin_path| 13 | XcodePlugin.from_bundle("#{plugins_path}#{plugin_path}") 14 | end.compact.keep_if(&:valid?) 15 | end 16 | 17 | def self.from_bundle(path) 18 | plugin = new(path) 19 | plugin.valid? ? plugin : nil 20 | end 21 | 22 | def valid? 23 | not_hidden = !path.split('/').last.start_with?('.') 24 | is_plugin = path.end_with?('.xcplugin') 25 | has_info = File.exist?(info_path) 26 | 27 | not_hidden && is_plugin && has_info 28 | end 29 | 30 | def has_uuid?(uuid) 31 | defaults_read('DVTPlugInCompatibilityUUIDs').include?(uuid) 32 | end 33 | 34 | def add_uuid(uuid) 35 | return false if has_uuid?(uuid) 36 | 37 | defaults_write('DVTPlugInCompatibilityUUIDs', '-array-add', uuid) 38 | true 39 | end 40 | 41 | def to_s 42 | "#{path.split('/').last.sub(/\.xcplugin$/, '')} (#{version})" 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/plugins_updater.rb: -------------------------------------------------------------------------------- 1 | require_relative 'xcode' 2 | require_relative 'xcode_plugin' 3 | 4 | class PluginsUpdater 5 | extend CLI 6 | 7 | def self.update_plugins 8 | xcodes = Xcode.find_xcodes 9 | 10 | if xcodes.empty? 11 | error "Didn't find any Xcode installed on your system." 12 | return 13 | else 14 | title 'Found:' 15 | puts xcodes.map { |xcode| "- #{xcode.detailed_description}" } 16 | end 17 | 18 | separator 19 | 20 | plugins = XcodePlugin.find_plugins 21 | 22 | if plugins.empty? 23 | error "Didn't find any Xcode Plug-in installed on your system." 24 | return 25 | else 26 | title 'Plugins:' 27 | puts plugins.map { |s| "- #{s}" } 28 | end 29 | 30 | separator 31 | process 'Updating...' 32 | 33 | uuids = xcodes.collect(&:uuid) 34 | uuids.each do |uuid| 35 | plugins.each do |plugin| 36 | if plugin.add_uuid(uuid) && !CLI.dry_run? 37 | success "Added #{uuid} to #{plugin}" 38 | end 39 | end 40 | end 41 | 42 | separator 43 | success 'Finished! 🎉' 44 | 45 | return if CLI.no_colors? 46 | 47 | if xcodes.any? { |xcode| xcode.version.to_f >= 8 } 48 | separator 49 | warning 'It seems that you have Xcode 8+ installed!' 50 | puts 'Some plugins might not work on recent versions of Xcode because of library validation.', 51 | "See #{'https://github.com/alcatraz/Alcatraz/issues/475'.underline}" 52 | 53 | separator 54 | puts "Run `#{'update_xcode_plugins --unsign'.bold}` to fix this." 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/cli.rb: -------------------------------------------------------------------------------- 1 | module CLI 2 | def self.dry_run? 3 | ARGV.include?('-d') || ARGV.include?('--dry-run') 4 | end 5 | 6 | def self.install_launch_agent? 7 | ARGV.include?('--install-launch-agent') 8 | end 9 | 10 | def self.uninstall_launch_agent? 11 | ARGV.include?('--uninstall-launch-agent') 12 | end 13 | 14 | def self.unsign_xcode? 15 | ARGV.include?('--unsign') 16 | end 17 | 18 | def self.restore_xcode? 19 | ARGV.include?('--restore') 20 | end 21 | 22 | def self.no_colors? 23 | ARGV.include?('--no-colors') 24 | end 25 | 26 | def self.non_interactive? 27 | ARGV.include?('--non-interactive') 28 | end 29 | 30 | def self.codesign_exists? 31 | `which codesign` && $CHILD_STATUS.exitstatus == 0 32 | end 33 | 34 | def self.chown_if_required(path) 35 | return yield if File.owned?(path) 36 | 37 | puts 38 | puts "* Changing ownership of #{path} (will be restored after)".colorize(:light_blue) 39 | 40 | previous_owner = File.stat(path).uid 41 | system("sudo chown $(whoami) \"#{path}\"") 42 | 43 | raise "Could not change ownership of #{path}" unless File.owned?(path) 44 | 45 | result = yield 46 | system("sudo chown #{previous_owner} \"#{path}\"") 47 | puts "* Restored ownership of #{path}".colorize(:light_blue) 48 | 49 | result 50 | end 51 | 52 | { 53 | title: :blue, 54 | process: :light_blue, 55 | warning: :yellow, 56 | error: :red, 57 | success: :green 58 | }.each do |type, color| 59 | if CLI.no_colors? 60 | define_method type.to_sym do |str| puts str end 61 | else 62 | define_method type.to_sym do |str| puts str.colorize(color) end 63 | end 64 | end 65 | 66 | def separator 67 | puts 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/xcode_unsigner.rb: -------------------------------------------------------------------------------- 1 | require_relative 'xcode' 2 | 3 | class XcodeUnsigner 4 | extend CLI 5 | 6 | def self.unsign_xcode 7 | process 'Looking for Xcode...' 8 | xcodes = Xcode.find_xcodes 9 | .select { |xcode| xcode.version.to_f >= 8 } 10 | .select(&:signed?) 11 | 12 | separator 13 | 14 | if xcodes.empty? 15 | error "Didn't find any signed Xcode 8+ on your system." 16 | return 17 | end 18 | 19 | notice 20 | separator 21 | 22 | selection = Ask.list "Choose which Xcode you would like to unsign (use arrows)", xcodes 23 | return unless selection 24 | 25 | xcode = xcodes[selection] 26 | 27 | unsign_xcodebuild = Ask.confirm "Unsign xcodebuild too?" 28 | 29 | separator 30 | 31 | process 'Unsigning...' 32 | if xcode.unsign_binary! && 33 | (!unsign_xcodebuild || (unsign_xcodebuild && xcode.unsign_xcodebuild!)) 34 | success 'Finished! 🎉' 35 | else 36 | error "Could not unsign #{xcode.path}\n"\ 37 | 'Create an issue on https://github.com/inket/update_xcode_plugins/issues' 38 | end 39 | end 40 | 41 | def self.restore_xcode 42 | process 'Looking for Xcode...' 43 | xcodes = Xcode.find_xcodes 44 | .select { |xcode| xcode.version.to_f >= 8 } 45 | .select(&:restorable?) 46 | 47 | separator 48 | 49 | if xcodes.empty? 50 | error "Didn't find any Xcode 8+ that can be restored on your system." 51 | return 52 | end 53 | 54 | selection = Ask.list "Choose which Xcode you would like to restore (use arrows)", xcodes 55 | return unless selection 56 | 57 | xcode = xcodes[selection] 58 | 59 | separator 60 | 61 | process 'Restoring...' 62 | 63 | success = true 64 | 65 | if xcode.binary_restorable? && !xcode.restore_binary! 66 | error "Could not restore binary for #{xcode.path}" 67 | success = false 68 | end 69 | 70 | if xcode.xcodebuild_restorable? && !xcode.restore_xcodebuild! 71 | error "Could not restore xcodebuild for #{xcode.path}" 72 | success = false 73 | end 74 | 75 | success 'Finished! 🎉' if success 76 | end 77 | 78 | def self.notice 79 | puts [ 80 | 'Unsigning Xcode will make it skip library validation allowing it to load plugins.'.colorize(:yellow), 81 | 'However, an unsigned Xcode presents security risks, '\ 82 | 'and will be untrusted by both Apple and your system.'.colorize(:red), 83 | "This tool will create a backup and allow you to restore Xcode's signature by running\n", 84 | '$ update_xcode_plugins --restore'.colorize(:light_blue) 85 | ] 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/xcode.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle' 2 | 3 | class Xcode < Bundle 4 | attr_accessor :signed 5 | 6 | # Hardcoded paths in case mdfind is not working because Spotlight is disabled 7 | DEFAULT_XCODE_PATHS = [ 8 | "/Applications/Xcode.app", 9 | "/Applications/Xcode-beta.app", 10 | "/Applications/Xcode-unsigned.app" 11 | ] 12 | 13 | XCODE_BUNDLE_IDENTIFIER = "com.apple.dt.Xcode" 14 | 15 | def self.find_xcodes 16 | output = `mdfind kMDItemCFBundleIdentifier = "#{XCODE_BUNDLE_IDENTIFIER}"` 17 | paths = output.lines + DEFAULT_XCODE_PATHS 18 | 19 | paths.map(&:strip).uniq.collect do |xcode_path| 20 | Xcode.from_bundle(xcode_path) 21 | end.compact.keep_if(&:valid?) 22 | end 23 | 24 | def self.from_bundle(path) 25 | xcode = new(path) 26 | xcode.valid? ? xcode : nil 27 | end 28 | 29 | def valid? 30 | is_app = path.end_with?('.app') 31 | has_info = File.exist?(info_path) 32 | return false unless is_app && has_info 33 | 34 | bundle_identifier == XCODE_BUNDLE_IDENTIFIER 35 | end 36 | 37 | def signed? 38 | if signed.nil? 39 | self.signed = `codesign -dv "#{path}" 2>/dev/null` && 40 | $CHILD_STATUS.exitstatus == 0 41 | end 42 | 43 | signed 44 | end 45 | 46 | def restorable? 47 | binary_restorable? || xcodebuild_restorable? 48 | end 49 | 50 | def binary_restorable? 51 | File.exist?("#{binary_path}.signed") 52 | end 53 | 54 | def xcodebuild_restorable? 55 | File.exist?("#{xcodebuild_path}.signed") 56 | end 57 | 58 | def unsign_binary! 59 | unsign!(binary_path) 60 | end 61 | 62 | def unsign_xcodebuild! 63 | unsign!(xcodebuild_path) 64 | end 65 | 66 | def restore_binary! 67 | restore!(binary_path) 68 | end 69 | 70 | def restore_xcodebuild! 71 | restore!(xcodebuild_path) 72 | end 73 | 74 | def uuid 75 | defaults_read('DVTPlugInCompatibilityUUID') 76 | end 77 | 78 | def to_s 79 | unless signed.nil? 80 | codesign_status = signed ? ' [Signed]' : ' [Unsigned]' 81 | end 82 | 83 | "Xcode (#{version})#{codesign_status}: #{path}" 84 | end 85 | 86 | def detailed_description 87 | "Xcode (#{version}) [#{uuid}]: #{path}" 88 | end 89 | 90 | private 91 | 92 | def binary_path 93 | "#{path}/Contents/MacOS/Xcode" 94 | end 95 | 96 | def xcodebuild_path 97 | "#{path}/Contents/Developer/usr/bin/xcodebuild" 98 | end 99 | 100 | def unsign_path 101 | lib_path = File.expand_path(File.dirname(__FILE__)) 102 | 103 | "#{lib_path}/bin/unsign" 104 | end 105 | 106 | def unsign!(target) 107 | unsigned_target = "#{target}.unsigned" 108 | signed_target = "#{target}.signed" 109 | 110 | CLI.chown_if_required(File.dirname(target)) do 111 | `#{unsign_path} "#{target}"` && 112 | $CHILD_STATUS.exitstatus == 0 113 | File.exist?(unsigned_target) && 114 | FileUtils.mv(target, signed_target) && 115 | File.exist?(signed_target) && 116 | FileUtils.mv(unsigned_target, target) 117 | end 118 | end 119 | 120 | def restore!(target) 121 | signed_target = "#{target}.signed" 122 | 123 | CLI.chown_if_required(File.dirname(target)) do 124 | File.exist?(signed_target) && 125 | File.exist?(target) && 126 | FileUtils.mv(signed_target, target) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/launch_agent.rb: -------------------------------------------------------------------------------- 1 | class LaunchAgent 2 | extend CLI 3 | 4 | attr_accessor :bin_path 5 | 6 | def self.install(bin_path) 7 | if !installed? 8 | LaunchAgent.new(File.expand_path(bin_path)).install 9 | success 'Installed! 🎉' 10 | else 11 | warning 'Launch agent is already installed!' 12 | end 13 | end 14 | 15 | def self.uninstall 16 | if installed? 17 | LaunchAgent.new.uninstall 18 | success 'Uninstalled! 🎉' 19 | else 20 | warning 'Launch agent is not installed!' 21 | end 22 | end 23 | 24 | def self.update_if_stale(bin_path) 25 | return unless stale? 26 | 27 | launch_agent = LaunchAgent.new(File.expand_path(bin_path)) 28 | launch_agent.uninstall 29 | launch_agent.install 30 | success 'Updated launch agent.' 31 | end 32 | 33 | def self.stale? 34 | if installed? 35 | path = LaunchAgent.new.launch_agent_path 36 | 37 | agent_xml = '' 38 | File.open(path, 'r') do |file| 39 | agent_xml = file.read 40 | end 41 | 42 | match = agent_xml.match(/update_xcode_plugins-(.*?)\//) 43 | installed_version = match ? match[1] : nil 44 | 45 | if installed_version && UpdateXcodePlugins::VERSION != installed_version 46 | return true 47 | end 48 | end 49 | 50 | false 51 | end 52 | 53 | def self.installed? 54 | File.exist?(LaunchAgent.new.launch_agent_path) 55 | end 56 | 57 | def initialize(bin_path = nil) 58 | self.bin_path = bin_path 59 | end 60 | 61 | def install 62 | File.open(launch_agent_path, 'w') do |file| 63 | file.write(xml) 64 | end 65 | 66 | `launchctl load "#{launch_agent_path}"` 67 | end 68 | 69 | def uninstall 70 | `launchctl unload "#{launch_agent_path}"` 71 | 72 | File.delete(launch_agent_path) if File.exist?(launch_agent_path) 73 | end 74 | 75 | def identifier 76 | 'jp.mahdi.update_xcode_plugins' 77 | end 78 | 79 | def launch_agent_path 80 | "#{Dir.home}/Library/LaunchAgents/#{identifier}.plist" 81 | end 82 | 83 | def watch_paths 84 | [ 85 | '/Applications/Xcode.app', 86 | '/Applications/Xcode-beta.app', 87 | '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/' 88 | ] 89 | end 90 | 91 | def watch_paths_xml 92 | watch_paths.map do |path| 93 | "#{path}" 94 | end.join("\n") 95 | end 96 | 97 | def xml 98 | " 99 | 100 | 101 | 102 | Label 103 | #{identifier} 104 | ProgramArguments 105 | 106 | /usr/bin/env 107 | ruby 108 | #{bin_path} 109 | --no-colors 110 | --non-interactive 111 | 112 | RunAtLoad 113 | 114 | StandardErrorPath 115 | /tmp/#{identifier}.err 116 | StandardOutPath 117 | /tmp/#{identifier}.out 118 | WatchPaths 119 | 120 | #{watch_paths_xml} 121 | 122 | 123 | " 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 58 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Mahdi Bchetnia 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | --------------------------------------------------------- 25 | 26 | --------------------------------------------------------- 27 | unsign binary copyright notices 28 | --------------------------------------------------------- 29 | 30 | --- 31 | unsign.c (ISC License) 32 | Retrieved from https://github.com/steakknife/unsign 33 | --- 34 | 35 | Copyright (c) 2010 36 | Permission to use, copy, modify, and/or distribute this software for any 37 | purpose with or without fee is hereby granted, provided that the above 38 | copyright notice and this permission notice appear in all copies. 39 | The software is provided "as is" and the author disclaims all warranties 40 | with regard to this software including all implied warranties of 41 | merchantability and fitness. In no event shall the author be liable for 42 | any special, direct, indirect, or consequential damages or any damages 43 | whatsoever resulting from loss of use, data or profits, whether in an 44 | action of contract, negligence or other tortious action, arising out of 45 | or in connection with the use or performance of this software. 46 | 47 | 48 | --- 49 | endian.c 50 | --- 51 | 52 | Copyright (c) 2002 Thomas Moestl 53 | All rights reserved. 54 | 55 | Redistribution and use in source and binary forms, with or without 56 | modification, are permitted provided that the following conditions 57 | are met: 58 | 1. Redistributions of source code must retain the above copyright 59 | notice, this list of conditions and the following disclaimer. 60 | 2. Redistributions in binary form must reproduce the above copyright 61 | notice, this list of conditions and the following disclaimer in the 62 | documentation and/or other materials provided with the distribution. 63 | 64 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 65 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 66 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 67 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 68 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 69 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 70 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 71 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 72 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 73 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 74 | SUCH DAMAGE. 75 | 76 | $FreeBSD$ 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://travis-ci.org/inket/update_xcode_plugins.svg?branch=master)](https://travis-ci.org/inket/update_xcode_plugins) [![Coverage Status](https://coveralls.io/repos/github/inket/update_xcode_plugins/badge.svg?branch=master)](https://coveralls.io/github/inket/update_xcode_plugins?branch=master) 2 | 3 | ![](https://img.shields.io/badge/xcode%207.3-supported-brightgreen.svg) 4 | 5 | ![](https://img.shields.io/badge/xcode%208.0-supported-brightgreen.svg) 6 | ![](https://img.shields.io/badge/xcode%208.1-supported-brightgreen.svg) 7 | ![](https://img.shields.io/badge/xcode%208.2-supported-brightgreen.svg) 8 | ![](https://img.shields.io/badge/xcode%208.3-supported-brightgreen.svg) 9 | 10 | ![](https://img.shields.io/badge/xcode%209.0-supported-brightgreen.svg) 11 | ![](https://img.shields.io/badge/xcode%209.1-supported-brightgreen.svg) 12 | ![](https://img.shields.io/badge/xcode%209.2-supported-brightgreen.svg) 13 | ![](https://img.shields.io/badge/xcode%209.3-supported-brightgreen.svg) 14 | ![](https://img.shields.io/badge/xcode%209.4-supported-brightgreen.svg) 15 | 16 | ![](https://img.shields.io/badge/xcode%2010.0b1-supported-brightgreen.svg) 17 | 18 | ### $ update\_xcode\_plugins 19 | 20 | This tool adds the missing UUIDs into the installed Xcode plugins so that they can be loaded by newer versions of Xcode. 21 | 22 | You can choose to run it once or install a **launch agent** that will trigger the tool every time any of your installed plugins are modified or Xcode/Xcode-beta gets updated. 23 | 24 | This tool also allows you to unsign Xcode in order to run plugins on Xcode 8 and later. For more information on why this is needed, see [alcatraz/Alcatraz#475](https://github.com/alcatraz/Alcatraz/issues/475). 25 | 26 | When unsigning Xcode, you will also be prompted to unsign `xcodebuild`; Doing so will allow `xcodebuild` to load plugins and silence the library validation warnings. More info at [#8](https://github.com/inket/update_xcode_plugins/issues/8#issuecomment-247881598). 27 | 28 | If you are having any issues, please check [common issues](#common-issues) before creating an issue. 29 | 30 | #### Install 31 | 32 | ```shell 33 | $ gem install update_xcode_plugins 34 | ``` 35 | 36 | (if using system ruby: `sudo gem install update_xcode_plugins`) 37 | 38 | (if still having problems: `sudo gem install -n /usr/local/bin update_xcode_plugins` [#10](https://github.com/inket/update_xcode_plugins/issues/10)) 39 | 40 | #### Usage 41 | 42 | In Terminal: 43 | 44 | ```shell 45 | $ update_xcode_plugins 46 | ``` 47 | 48 | ![](http://i.imgur.com/0aw1bW4.png) 49 | 50 | To use plugins on Xcode 8 and later, unsign Xcode with: 51 | 52 | ```shell 53 | $ update_xcode_plugins --unsign 54 | ``` 55 | 56 | ![](http://i.imgur.com/XUco0su.png) 57 | 58 | If you need to restore Xcode, use the command: 59 | 60 | ```shell 61 | $ update_xcode_plugins --restore 62 | ``` 63 | 64 | ##### Other options 65 | 66 | For a dry run to see which plugins will be updated, 67 | 68 | ```shell 69 | $ update_xcode_plugins --dry-run 70 | ``` 71 | 72 | To install the launch agent for automatically updating plugins, 73 | 74 | ```shell 75 | $ update_xcode_plugins --install-launch-agent 76 | ``` 77 | 78 | or to uninstall the launch agent, 79 | 80 | ```shell 81 | $ update_xcode_plugins --uninstall-launch-agent 82 | ``` 83 | 84 | ##### Common Issues 85 | 86 | ###### Xcode crashes: 87 | 88 | One or more of the plugins you are using are incompatible with your version of Xcode and are causing it to crash. The crash report will generally include the name of the responsible plugin. If unsure, start removing your plugins one by one until you find the culprit. 89 | 90 | #### Contact 91 | 92 | [@inket](https://github.com/inket) / [@inket](https://twitter.com/inket) on Twitter / [mahdi.jp](https://mahdi.jp) 93 | -------------------------------------------------------------------------------- /test/HelloWorld/HelloWorld.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B21177FC1D4C896700F528CC /* HelloWorld.xcscheme in Resources */ = {isa = PBXBuildFile; fileRef = B21177FB1D4C896700F528CC /* HelloWorld.xcscheme */; }; 11 | B21177FF1D4C896700F528CC /* HelloWorld.m in Sources */ = {isa = PBXBuildFile; fileRef = B21177FE1D4C896700F528CC /* HelloWorld.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | B21177F71D4C896700F528CC /* HelloWorld.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HelloWorld.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | B21177FB1D4C896700F528CC /* HelloWorld.xcscheme */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = HelloWorld.xcscheme; path = HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme; sourceTree = SOURCE_ROOT; }; 17 | B21177FD1D4C896700F528CC /* HelloWorld.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HelloWorld.h; sourceTree = ""; }; 18 | B21177FE1D4C896700F528CC /* HelloWorld.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HelloWorld.m; sourceTree = ""; }; 19 | B21178001D4C896700F528CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXGroup section */ 23 | B21177EF1D4C896700F528CC = { 24 | isa = PBXGroup; 25 | children = ( 26 | B21177F91D4C896700F528CC /* HelloWorld */, 27 | B21177F81D4C896700F528CC /* Products */, 28 | ); 29 | sourceTree = ""; 30 | }; 31 | B21177F81D4C896700F528CC /* Products */ = { 32 | isa = PBXGroup; 33 | children = ( 34 | B21177F71D4C896700F528CC /* HelloWorld.xcplugin */, 35 | ); 36 | name = Products; 37 | sourceTree = ""; 38 | }; 39 | B21177F91D4C896700F528CC /* HelloWorld */ = { 40 | isa = PBXGroup; 41 | children = ( 42 | B21177FD1D4C896700F528CC /* HelloWorld.h */, 43 | B21177FE1D4C896700F528CC /* HelloWorld.m */, 44 | B21178001D4C896700F528CC /* Info.plist */, 45 | B21177FA1D4C896700F528CC /* Supporting Files */, 46 | ); 47 | path = HelloWorld; 48 | sourceTree = ""; 49 | }; 50 | B21177FA1D4C896700F528CC /* Supporting Files */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | B21177FB1D4C896700F528CC /* HelloWorld.xcscheme */, 54 | ); 55 | name = "Supporting Files"; 56 | sourceTree = ""; 57 | }; 58 | /* End PBXGroup section */ 59 | 60 | /* Begin PBXNativeTarget section */ 61 | B21177F61D4C896700F528CC /* HelloWorld */ = { 62 | isa = PBXNativeTarget; 63 | buildConfigurationList = B21178031D4C896700F528CC /* Build configuration list for PBXNativeTarget "HelloWorld" */; 64 | buildPhases = ( 65 | B21177F41D4C896700F528CC /* Sources */, 66 | B21177F51D4C896700F528CC /* Resources */, 67 | ); 68 | buildRules = ( 69 | ); 70 | dependencies = ( 71 | ); 72 | name = HelloWorld; 73 | productName = HelloWorld; 74 | productReference = B21177F71D4C896700F528CC /* HelloWorld.xcplugin */; 75 | productType = "com.apple.product-type.bundle"; 76 | }; 77 | /* End PBXNativeTarget section */ 78 | 79 | /* Begin PBXProject section */ 80 | B21177F01D4C896700F528CC /* Project object */ = { 81 | isa = PBXProject; 82 | attributes = { 83 | LastUpgradeCheck = 0730; 84 | TargetAttributes = { 85 | B21177F61D4C896700F528CC = { 86 | CreatedOnToolsVersion = 7.3.1; 87 | }; 88 | }; 89 | }; 90 | buildConfigurationList = B21177F31D4C896700F528CC /* Build configuration list for PBXProject "HelloWorld" */; 91 | compatibilityVersion = "Xcode 3.2"; 92 | developmentRegion = English; 93 | hasScannedForEncodings = 0; 94 | knownRegions = ( 95 | en, 96 | ); 97 | mainGroup = B21177EF1D4C896700F528CC; 98 | productRefGroup = B21177F81D4C896700F528CC /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | B21177F61D4C896700F528CC /* HelloWorld */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | B21177F51D4C896700F528CC /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | B21177FC1D4C896700F528CC /* HelloWorld.xcscheme in Resources */, 113 | ); 114 | runOnlyForDeploymentPostprocessing = 0; 115 | }; 116 | /* End PBXResourcesBuildPhase section */ 117 | 118 | /* Begin PBXSourcesBuildPhase section */ 119 | B21177F41D4C896700F528CC /* Sources */ = { 120 | isa = PBXSourcesBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | B21177FF1D4C896700F528CC /* HelloWorld.m in Sources */, 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | /* End PBXSourcesBuildPhase section */ 128 | 129 | /* Begin XCBuildConfiguration section */ 130 | B21178011D4C896700F528CC /* Debug */ = { 131 | isa = XCBuildConfiguration; 132 | buildSettings = { 133 | ALWAYS_SEARCH_USER_PATHS = NO; 134 | CLANG_ANALYZER_NONNULL = YES; 135 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 136 | CLANG_CXX_LIBRARY = "libc++"; 137 | CLANG_ENABLE_MODULES = YES; 138 | CLANG_ENABLE_OBJC_ARC = YES; 139 | CLANG_WARN_BOOL_CONVERSION = YES; 140 | CLANG_WARN_CONSTANT_CONVERSION = YES; 141 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 142 | CLANG_WARN_EMPTY_BODY = YES; 143 | CLANG_WARN_ENUM_CONVERSION = YES; 144 | CLANG_WARN_INT_CONVERSION = YES; 145 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 146 | CLANG_WARN_UNREACHABLE_CODE = YES; 147 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 148 | CODE_SIGN_IDENTITY = "-"; 149 | COPY_PHASE_STRIP = NO; 150 | DEBUG_INFORMATION_FORMAT = dwarf; 151 | ENABLE_STRICT_OBJC_MSGSEND = YES; 152 | ENABLE_TESTABILITY = YES; 153 | GCC_C_LANGUAGE_STANDARD = gnu99; 154 | GCC_DYNAMIC_NO_PIC = NO; 155 | GCC_NO_COMMON_BLOCKS = YES; 156 | GCC_OPTIMIZATION_LEVEL = 0; 157 | GCC_PREPROCESSOR_DEFINITIONS = ( 158 | "DEBUG=1", 159 | "$(inherited)", 160 | ); 161 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 162 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 163 | GCC_WARN_UNDECLARED_SELECTOR = YES; 164 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 165 | GCC_WARN_UNUSED_FUNCTION = YES; 166 | GCC_WARN_UNUSED_VARIABLE = YES; 167 | MACOSX_DEPLOYMENT_TARGET = 10.9; 168 | MTL_ENABLE_DEBUG_INFO = YES; 169 | ONLY_ACTIVE_ARCH = YES; 170 | SDKROOT = macosx; 171 | }; 172 | name = Debug; 173 | }; 174 | B21178021D4C896700F528CC /* Release */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | CLANG_ANALYZER_NONNULL = YES; 179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 180 | CLANG_CXX_LIBRARY = "libc++"; 181 | CLANG_ENABLE_MODULES = YES; 182 | CLANG_ENABLE_OBJC_ARC = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_CONSTANT_CONVERSION = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INT_CONVERSION = YES; 189 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 190 | CLANG_WARN_UNREACHABLE_CODE = YES; 191 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 192 | CODE_SIGN_IDENTITY = "-"; 193 | COPY_PHASE_STRIP = NO; 194 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 195 | ENABLE_NS_ASSERTIONS = NO; 196 | ENABLE_STRICT_OBJC_MSGSEND = YES; 197 | GCC_C_LANGUAGE_STANDARD = gnu99; 198 | GCC_NO_COMMON_BLOCKS = YES; 199 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 200 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 201 | GCC_WARN_UNDECLARED_SELECTOR = YES; 202 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 203 | GCC_WARN_UNUSED_FUNCTION = YES; 204 | GCC_WARN_UNUSED_VARIABLE = YES; 205 | MACOSX_DEPLOYMENT_TARGET = 10.9; 206 | MTL_ENABLE_DEBUG_INFO = NO; 207 | SDKROOT = macosx; 208 | }; 209 | name = Release; 210 | }; 211 | B21178041D4C896700F528CC /* Debug */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | COMBINE_HIDPI_IMAGES = YES; 215 | DEPLOYMENT_LOCATION = YES; 216 | DSTROOT = "$(HOME)"; 217 | INFOPLIST_FILE = HelloWorld/Info.plist; 218 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 219 | PRODUCT_BUNDLE_IDENTIFIER = jp.mahdi.HelloWorld; 220 | PRODUCT_NAME = "$(TARGET_NAME)"; 221 | WRAPPER_EXTENSION = xcplugin; 222 | }; 223 | name = Debug; 224 | }; 225 | B21178051D4C896700F528CC /* Release */ = { 226 | isa = XCBuildConfiguration; 227 | buildSettings = { 228 | COMBINE_HIDPI_IMAGES = YES; 229 | DEPLOYMENT_LOCATION = YES; 230 | DSTROOT = "$(HOME)"; 231 | INFOPLIST_FILE = HelloWorld/Info.plist; 232 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 233 | PRODUCT_BUNDLE_IDENTIFIER = jp.mahdi.HelloWorld; 234 | PRODUCT_NAME = "$(TARGET_NAME)"; 235 | WRAPPER_EXTENSION = xcplugin; 236 | }; 237 | name = Release; 238 | }; 239 | /* End XCBuildConfiguration section */ 240 | 241 | /* Begin XCConfigurationList section */ 242 | B21177F31D4C896700F528CC /* Build configuration list for PBXProject "HelloWorld" */ = { 243 | isa = XCConfigurationList; 244 | buildConfigurations = ( 245 | B21178011D4C896700F528CC /* Debug */, 246 | B21178021D4C896700F528CC /* Release */, 247 | ); 248 | defaultConfigurationIsVisible = 0; 249 | defaultConfigurationName = Release; 250 | }; 251 | B21178031D4C896700F528CC /* Build configuration list for PBXNativeTarget "HelloWorld" */ = { 252 | isa = XCConfigurationList; 253 | buildConfigurations = ( 254 | B21178041D4C896700F528CC /* Debug */, 255 | B21178051D4C896700F528CC /* Release */, 256 | ); 257 | defaultConfigurationIsVisible = 0; 258 | defaultConfigurationName = Release; 259 | }; 260 | /* End XCConfigurationList section */ 261 | }; 262 | rootObject = B21177F01D4C896700F528CC /* Project object */; 263 | } 264 | -------------------------------------------------------------------------------- /test/xcode.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | require "coveralls" 3 | 4 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 5 | SimpleCov.start do 6 | # Ignore interactive scripts (and helper) that can't be tested 7 | add_filter "lib/plugins_updater.rb" 8 | add_filter "lib/xcode_unsigner.rb" 9 | add_filter "lib/cli.rb" 10 | end 11 | 12 | require "minitest/autorun" 13 | require_relative "../lib/update_xcode_plugins" 14 | 15 | unless ENV["CI"] == "true" && ENV["TRAVIS"] == "true" 16 | warn "Tests should only be run on CI as they'll overwrite Xcode." 17 | exit 18 | end 19 | 20 | class TestXcode < Minitest::Test 21 | extend Minitest::Spec::DSL 22 | 23 | let(:xcode) { Xcode.new("/Applications/Xcode.app") } 24 | let(:plugin) do 25 | XcodePlugin.new( 26 | "#{Dir.home}/Library/Application Support/Developer/Shared/Xcode"\ 27 | "/Plug-ins/HelloWorld.xcplugin" 28 | ) 29 | end 30 | let(:launch_agent) do 31 | LaunchAgent.new( 32 | Gem.bin_path("update_xcode_plugins", "update_xcode_plugins") 33 | ) 34 | end 35 | 36 | # Test execution order needs to be set manually, which is why all tests are limited to one file 37 | def self.runnable_methods 38 | [ 39 | :test_that_bundle_cannot_be_used_directly, 40 | :test_that_xcode_has_correct_path, 41 | :test_that_xcode_bundle_is_valid, 42 | :test_that_xcode_has_correct_version, 43 | :test_that_xcode_returns_correct_uuid, 44 | :test_that_xcode_returns_appropriate_detailed_description, 45 | :test_that_xcode_is_signed_by_default, 46 | :test_that_xcodebuild_is_signed_by_default, 47 | :test_that_test_plugin_is_invalid_when_nonexistent, 48 | :test_that_find_plugins_returns_empty_array_when_no_plugins_are_installed, 49 | :test_that_test_plugin_builds_correctly, 50 | :test_that_find_plugins_returns_installed_plugins, 51 | :test_that_test_plugin_doesnt_include_uuid_by_default, 52 | :test_that_uuid_isnt_added_to_test_plugin_in_dry_run, 53 | :test_that_uuid_is_added_correctly_to_test_plugin, 54 | :test_that_plugin_injects_into_xcodebuild_with_xcode7, 55 | :test_that_plugin_doesnt_inject_into_xcodebuild_with_xcode8, 56 | :test_that_xcode_is_unsigned_correctly, 57 | :test_that_xcodebuild_is_unsigned_correctly, 58 | :test_that_plugin_injects_into_xcodebuild_with_xcode8_after_unsign, 59 | :test_that_launch_agent_is_installed_correctly_via_cli, 60 | :test_that_launch_agent_is_updated_correctly_via_cli, 61 | :test_that_launch_agent_is_uninstalled_correctly_via_cli, 62 | :test_that_launch_agent_is_installed_correctly, 63 | :test_that_launch_agent_updates_plugins_when_plugins_are_changed, 64 | :test_that_launch_agent_is_uninstalled_correctly, 65 | :test_that_xcode_cannot_be_found_using_mdfind_with_spotlight_disabled, 66 | :test_that_xcode_can_be_found_using_fallback_with_spotlight_disabled, 67 | :test_that_xcode_is_restored_correctly, 68 | :test_that_xcodebuild_is_restored_correctly 69 | ] 70 | end 71 | 72 | def skip_if_xcode_7 73 | skip "Unnecessary test for old Xcode version." if xcode.version.to_f < 8 74 | end 75 | 76 | def plugin_injection_success_path 77 | "#{Dir.home}/Desktop/success" 78 | end 79 | 80 | def teardown 81 | FileUtils.remove(plugin_injection_success_path, force: true) 82 | end 83 | 84 | def test_that_bundle_cannot_be_used_directly 85 | refute Bundle.new("/Applications/Xcode.app").valid? 86 | end 87 | 88 | def test_that_xcode_has_correct_path 89 | assert_equal "/Applications/Xcode.app", xcode.path 90 | end 91 | 92 | def test_that_xcode_bundle_is_valid 93 | assert xcode.valid? 94 | end 95 | 96 | def test_that_xcode_has_correct_version 97 | if ENV["TRAVIS_XCODE_VERSION"] == "73" 98 | assert_equal "7.3.1", xcode.version 99 | elsif ENV["TRAVIS_XCODE_VERSION"] == "8" 100 | assert_equal "8.0", xcode.version 101 | elsif ENV["TRAVIS_XCODE_VERSION"] == "83" 102 | assert_equal "8.3.3", xcode.version 103 | elsif ENV["TRAVIS_XCODE_VERSION"] == "9" 104 | assert_equal "9.0", xcode.version 105 | elsif ENV["TRAVIS_XCODE_VERSION"] == "91" 106 | assert_equal "9.1", xcode.version 107 | elsif ENV["TRAVIS_XCODE_VERSION"] == "92" 108 | assert_equal "9.2", xcode.version 109 | elsif ENV["TRAVIS_XCODE_VERSION"] == "93" 110 | assert xcode.version.start_with?("9.3") 111 | else 112 | fail "Unexpected Xcode version #{xcode.version}" 113 | end 114 | end 115 | 116 | def test_that_xcode_returns_correct_uuid 117 | plist_path = "#{xcode.path}/Contents/Info" 118 | uuid = `defaults read "#{plist_path}" DVTPlugInCompatibilityUUID`.strip 119 | 120 | assert_equal uuid, xcode.uuid 121 | refute_nil xcode.uuid 122 | refute_empty xcode.uuid 123 | assert xcode.uuid.match(/\A\h{8}-(?:\h{4}-){3}\h{12}\z/) 124 | end 125 | 126 | def test_that_xcode_returns_appropriate_detailed_description 127 | assert_equal "Xcode (#{xcode.version}) [#{xcode.uuid}]: #{xcode.path}", xcode.detailed_description 128 | end 129 | 130 | def test_that_xcode_is_signed_by_default 131 | skip_if_xcode_7 132 | 133 | assert xcode.signed? 134 | assert_equal "Xcode (#{xcode.version}) [Signed]: #{xcode.path}", xcode.to_s 135 | end 136 | 137 | def test_that_xcodebuild_is_signed_by_default 138 | skip_if_xcode_7 139 | 140 | is_signed = `codesign -dv "#{xcode.send(:xcodebuild_path)}" 2>/dev/null` && 141 | $CHILD_STATUS.exitstatus == 0 142 | assert is_signed 143 | end 144 | 145 | def test_that_test_plugin_is_invalid_when_nonexistent 146 | refute plugin.valid? 147 | assert_nil XcodePlugin.from_bundle(plugin.path) 148 | end 149 | 150 | def test_that_find_plugins_returns_empty_array_when_no_plugins_are_installed 151 | assert_equal [], XcodePlugin.find_plugins 152 | end 153 | 154 | def test_that_test_plugin_builds_correctly 155 | Dir.chdir("test/HelloWorld") do 156 | `xcodebuild` 157 | assert_equal 0, $CHILD_STATUS.exitstatus 158 | end 159 | 160 | assert File.exist?(plugin.path) 161 | assert plugin.valid? 162 | refute_nil XcodePlugin.from_bundle(plugin.path) 163 | 164 | assert_equal "HelloWorld (1.0)", plugin.to_s 165 | end 166 | 167 | def test_that_find_plugins_returns_installed_plugins 168 | found_plugins = XcodePlugin.find_plugins 169 | assert_equal 1, found_plugins.count 170 | assert_equal plugin.bundle_identifier, found_plugins.first.bundle_identifier 171 | end 172 | 173 | def test_that_test_plugin_doesnt_include_uuid_by_default 174 | refute_nil plugin 175 | 176 | plist_path = "#{plugin.path}/Contents/Info" 177 | uuids = `defaults read "#{plist_path}" DVTPlugInCompatibilityUUIDs`.strip 178 | 179 | assert_equal "(\n)", uuids 180 | refute plugin.has_uuid?(xcode.uuid) 181 | end 182 | 183 | def test_that_uuid_isnt_added_to_test_plugin_in_dry_run 184 | refute_nil plugin 185 | 186 | CLI.stub :dry_run?, true do 187 | plugin.add_uuid(xcode.uuid) 188 | end 189 | 190 | refute plugin.has_uuid?(xcode.uuid) 191 | end 192 | 193 | def test_that_uuid_is_added_correctly_to_test_plugin 194 | refute_nil plugin 195 | 196 | plugin.add_uuid(xcode.uuid) 197 | 198 | plist_path = "#{plugin.path}/Contents/Info" 199 | uuids = `defaults read "#{plist_path}" DVTPlugInCompatibilityUUIDs`.strip 200 | 201 | assert uuids.include?(xcode.uuid) 202 | assert plugin.has_uuid?(xcode.uuid) 203 | end 204 | 205 | def test_that_plugin_injects_into_xcodebuild_with_xcode7 206 | skip unless xcode.version.to_f < 8 207 | 208 | refute File.exist?(plugin_injection_success_path) 209 | `xcodebuild` 210 | assert File.exist?(plugin_injection_success_path) 211 | end 212 | 213 | def test_that_plugin_doesnt_inject_into_xcodebuild_with_xcode8 214 | skip_if_xcode_7 215 | 216 | refute File.exist?(plugin_injection_success_path) 217 | `xcodebuild` 218 | refute File.exist?(plugin_injection_success_path) 219 | end 220 | 221 | def test_that_xcode_is_unsigned_correctly 222 | skip_if_xcode_7 223 | 224 | xcode.unsign_binary! 225 | refute xcode.signed? 226 | 227 | new_xcode = Xcode.new("/Applications/Xcode.app") 228 | refute new_xcode.signed? 229 | assert_equal "Xcode (#{xcode.version}) [Unsigned]: #{xcode.path}", new_xcode.to_s 230 | end 231 | 232 | def test_that_xcodebuild_is_unsigned_correctly 233 | skip_if_xcode_7 234 | 235 | xcode.unsign_xcodebuild! 236 | is_signed = `codesign -dv "#{xcode.send(:xcodebuild_path)}" 2>/dev/null` && 237 | $CHILD_STATUS.exitstatus == 0 238 | refute is_signed 239 | end 240 | 241 | def test_that_plugin_injects_into_xcodebuild_with_xcode8_after_unsign 242 | skip_if_xcode_7 243 | 244 | refute File.exist?(plugin_injection_success_path) 245 | `xcodebuild` 246 | assert File.exist?(plugin_injection_success_path) 247 | end 248 | 249 | def test_that_launch_agent_is_installed_correctly_via_cli 250 | refute LaunchAgent.installed? 251 | 252 | LaunchAgent.install("/some/random/path/update_xcode_plugins-0.0/bin/update_xcode_plugins") 253 | assert LaunchAgent.installed? 254 | 255 | # LaunchAgent should be stale because the version doesn't match 256 | assert LaunchAgent.stale? 257 | 258 | # Expect LaunchAgent.warning to be called because it's warning us about 259 | # the plugin being already installed 260 | mock = MiniTest::Mock.new 261 | mock.expect(:call, nil, ["Launch agent is already installed!"]) 262 | LaunchAgent.stub(:warning, mock) do 263 | LaunchAgent.install("") 264 | end 265 | mock.verify 266 | end 267 | 268 | def test_that_launch_agent_is_updated_correctly_via_cli 269 | assert LaunchAgent.installed? 270 | 271 | LaunchAgent.update_if_stale(launch_agent.bin_path) 272 | assert LaunchAgent.installed? 273 | 274 | refute LaunchAgent.stale? 275 | end 276 | 277 | def test_that_launch_agent_is_uninstalled_correctly_via_cli 278 | assert LaunchAgent.installed? 279 | 280 | LaunchAgent.uninstall 281 | refute LaunchAgent.installed? 282 | refute LaunchAgent.stale? 283 | 284 | # Expect LaunchAgent.warning to be called because it's warning us about 285 | # the plugin being already uninstalled 286 | mock = MiniTest::Mock.new 287 | mock.expect(:call, nil, ["Launch agent is not installed!"]) 288 | LaunchAgent.stub(:warning, mock) do 289 | LaunchAgent.uninstall 290 | end 291 | mock.verify 292 | end 293 | 294 | def test_that_launch_agent_is_installed_correctly 295 | refute LaunchAgent.installed? 296 | refute File.exist?(launch_agent.launch_agent_path) 297 | launchctl_out = `launchctl list | grep #{launch_agent.identifier} | wc -l` 298 | refute launchctl_out.strip == "1" 299 | 300 | launch_agent.install 301 | 302 | assert File.exist?(launch_agent.launch_agent_path) 303 | launchctl_out = `launchctl list | grep #{launch_agent.identifier} | wc -l` 304 | assert_equal "1", launchctl_out.strip 305 | assert LaunchAgent.installed? 306 | 307 | refute LaunchAgent.stale? 308 | end 309 | 310 | def test_that_launch_agent_updates_plugins_when_plugins_are_changed 311 | FileUtils.remove_dir(plugin.path, true) 312 | refute Dir.exist?(plugin.path) 313 | 314 | Dir.chdir("test/HelloWorld") { `xcodebuild` } 315 | assert Dir.exist?(plugin.path) 316 | 317 | refute plugin.has_uuid?(xcode.uuid) 318 | sleep 10 319 | assert plugin.has_uuid?(xcode.uuid) 320 | end 321 | 322 | def test_that_launch_agent_is_uninstalled_correctly 323 | assert File.exist?(launch_agent.launch_agent_path) 324 | launchctl_out = `launchctl list | grep #{launch_agent.identifier} | wc -l` 325 | assert_equal "1", launchctl_out.strip 326 | 327 | launch_agent.uninstall 328 | 329 | refute File.exist?(launch_agent.launch_agent_path) 330 | launchctl_out = `launchctl list | grep #{launch_agent.identifier} | wc -l` 331 | assert_equal "0", launchctl_out.strip 332 | refute LaunchAgent.installed? 333 | end 334 | 335 | def test_that_xcode_cannot_be_found_using_mdfind_with_spotlight_disabled 336 | `sudo mdutil -a -i off` 337 | mdfind = `mdfind kMDItemCFBundleIdentifier = "com.apple.dt.Xcode" | wc -l` 338 | assert_equal "0", mdfind.strip 339 | end 340 | 341 | def test_that_xcode_can_be_found_using_fallback_with_spotlight_disabled 342 | mdfind = `mdfind kMDItemCFBundleIdentifier = "com.apple.dt.Xcode" | wc -l` 343 | assert_equal "0", mdfind.strip 344 | 345 | refute Xcode.find_xcodes.empty? 346 | end 347 | 348 | def test_that_xcode_is_restored_correctly 349 | skip_if_xcode_7 350 | 351 | assert xcode.restorable? 352 | assert xcode.binary_restorable? 353 | 354 | refute xcode.signed? 355 | assert xcode.restore_binary! 356 | refute xcode.signed? 357 | 358 | assert xcode.restorable? # xcodebuild is still unsigned 359 | refute xcode.binary_restorable? 360 | end 361 | 362 | def test_that_xcodebuild_is_restored_correctly 363 | skip_if_xcode_7 364 | 365 | assert xcode.restorable? 366 | assert xcode.xcodebuild_restorable? 367 | 368 | assert xcode.restore_xcodebuild! 369 | 370 | refute xcode.restorable? 371 | refute xcode.xcodebuild_restorable? 372 | end 373 | end 374 | --------------------------------------------------------------------------------