308 lines
8.3 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
module OS
module Mac
module Xcode
2016-09-24 20:11:54 +02:00
module_function
DEFAULT_BUNDLE_PATH = Pathname.new("/Applications/Xcode.app").freeze
BUNDLE_ID = "com.apple.dt.Xcode"
OLD_BUNDLE_ID = "com.apple.Xcode"
def latest_version
case MacOS.version
when "10.9" then "6.2"
2016-02-07 19:30:51 +00:00
when "10.10" then "7.2.1"
when "10.11" then "8.2.1"
when "10.12" then "9.2"
when "10.13" then "10.1"
2019-12-11 13:48:32 +00:00
when "10.14" then "11.3"
when "10.15" then "11.3"
else
2016-09-23 22:02:23 +02:00
raise "macOS '#{MacOS.version}' is invalid" unless OS::Mac.prerelease?
# Default to newest known version of Xcode for unreleased macOS versions.
2019-12-11 13:48:32 +00:00
"11.3"
end
end
def minimum_version
case MacOS.version
when "10.15" then "11.0"
when "10.14" then "10.2"
2017-06-06 04:33:55 +01:00
when "10.13" then "9.0"
when "10.12" then "8.0"
else "2.0"
end
end
def below_minimum_version?
return false unless installed?
2018-09-17 02:45:00 +02:00
version < minimum_version
end
def latest_sdk_version?
OS::Mac.version >= OS::Mac.latest_sdk_version
end
def needs_clt_installed?
return false if latest_sdk_version?
2018-09-17 02:45:00 +02:00
without_clt?
end
def outdated?
return false unless installed?
2018-09-17 02:45:00 +02:00
version < latest_version
end
2013-05-22 22:26:09 -05:00
def without_clt?
!MacOS::CLT.installed?
end
2016-03-16 22:51:39 -07:00
# Returns a Pathname object corresponding to Xcode.app's Developer
# directory or nil if Xcode.app is not installed
def prefix
2014-05-02 16:48:59 -05:00
@prefix ||=
begin
dir = MacOS.active_developer_dir
2017-01-09 21:30:20 +00:00
if dir.empty? || dir == CLT::PKG_PATH || !File.directory?(dir)
2014-05-02 16:48:59 -05:00
path = bundle_path
2017-01-09 21:30:20 +00:00
path/"Contents/Developer" if path
2014-05-02 16:48:59 -05:00
else
# Use cleanpath to avoid pathological trailing slash
Pathname.new(dir).cleanpath
2014-05-02 16:48:59 -05:00
end
end
end
def toolchain_path
Pathname.new("#{prefix}/Toolchains/XcodeDefault.xctoolchain")
end
def bundle_path
# Use the default location if it exists.
return DEFAULT_BUNDLE_PATH if DEFAULT_BUNDLE_PATH.exist?
# Ask Spotlight where Xcode is. If the user didn't install the
# helper tools and installed Xcode in a non-conventional place, this
# is our only option. See: https://superuser.com/questions/390757
MacOS.app_with_bundle_id(BUNDLE_ID, OLD_BUNDLE_ID)
end
def installed?
!prefix.nil?
end
def sdk(v = nil)
@locator ||= XcodeSDKLocator.new
@locator.sdk_if_applicable(v)
end
def sdk_path(v = nil)
sdk(v)&.path
end
2016-07-16 21:01:34 +01:00
def update_instructions
if OS::Mac.prerelease?
2017-10-15 02:28:32 +02:00
<<~EOS
Xcode can be updated from:
2019-04-05 12:24:10 -04:00
#{Formatter.url("https://developer.apple.com/download/more/")}
2016-07-16 21:01:34 +01:00
EOS
else
2017-10-15 02:28:32 +02:00
<<~EOS
Xcode can be updated from the App Store.
2016-07-16 21:01:34 +01:00
EOS
end
end
def version
# may return a version string
# that is guessed based on the compiler, so do not
# use it in order to check if Xcode is installed.
if @version ||= detect_version
::Version.new @version
else
::Version::NULL
end
end
def detect_version
# This is a separate function as you can't cache the value out of a block
# if return is used in the middle, which we do many times in here.
return if !MacOS::Xcode.installed? && !MacOS::CLT.installed?
%W[
#{prefix}/usr/bin/xcodebuild
#{which("xcodebuild")}
].uniq.each do |xcodebuild_path|
2016-09-11 17:49:27 +01:00
next unless File.executable? xcodebuild_path
2018-09-17 02:45:00 +02:00
2016-09-11 17:49:27 +01:00
xcodebuild_output = Utils.popen_read(xcodebuild_path, "-version")
next unless $CHILD_STATUS.success?
xcode_version = xcodebuild_output[/Xcode (\d+(\.\d+)*)/, 1]
2016-09-11 17:49:27 +01:00
return xcode_version if xcode_version
2016-09-11 17:49:27 +01:00
# Xcode 2.x's xcodebuild has a different version string
case xcodebuild_output[/DevToolsCore-(\d+\.\d)/, 1]
when "515.0" then return "2.0"
when "798.0" then return "2.5"
2014-04-15 14:18:45 -05:00
end
end
detect_version_from_clang_version
end
def detect_version_from_clang_version
return "dunno" if DevelopmentTools.clang_version.null?
2018-09-17 02:45:00 +02:00
# This logic provides a fake Xcode version based on the
# installed CLT version. This is useful as they are packaged
# simultaneously so workarounds need to apply to both based on their
# comparable version.
case (DevelopmentTools.clang_version.to_f * 10).to_i
2016-09-11 17:49:27 +01:00
when 0 then "dunno"
when 60 then "6.0"
when 61 then "6.1"
when 70 then "7.0"
when 73 then "7.3"
when 80 then "8.0"
when 81 then "8.3"
2018-04-17 06:26:12 -07:00
when 90 then "9.2"
when 91 then "9.4"
when 100 then "10.2.1"
2019-12-11 13:48:32 +00:00
when 110 then "11.3"
else "11.3"
end
end
def default_prefix?
prefix.to_s == "/Applications/Xcode.app/Contents/Developer"
end
end
module CLT
module_function
2018-07-06 15:15:23 -07:00
# The original Mavericks CLT package ID
EXECUTABLE_PKG_ID = "com.apple.pkg.CLTools_Executables"
MAVERICKS_NEW_PKG_ID = "com.apple.pkg.CLTools_Base" # obsolete
PKG_PATH = "/Library/Developer/CommandLineTools"
# Returns true even if outdated tools are installed
def installed?
!version.null?
end
2013-07-21 16:41:50 -05:00
2018-06-12 14:30:27 -07:00
def separate_header_package?
version >= "10"
2018-06-12 14:30:27 -07:00
end
def provides_sdk?
version >= "8"
end
def sdk(v = nil)
@locator ||= CLTSDKLocator.new
@locator.sdk_if_applicable(v)
end
def sdk_path(v = nil)
sdk(v)&.path
end
2016-07-16 21:01:34 +01:00
def update_instructions
if MacOS.version >= "10.14"
<<~EOS
Update them from Software Update in System Preferences or
#{Formatter.url("https://developer.apple.com/download/more/")}.
EOS
else
2017-10-15 02:28:32 +02:00
<<~EOS
2019-11-29 14:53:01 -05:00
Update them from Software Update in the App Store or
#{Formatter.url("https://developer.apple.com/download/more/")}.
2016-07-16 21:01:34 +01:00
EOS
end
end
def latest_version
# As of Xcode 8 CLT releases are no longer in sync with Xcode releases
# on the older supported platform for that Xcode release, i.e there's no
# CLT package for 10.11 that contains the Clang version from Xcode 8.
case MacOS.version
2019-12-11 13:48:32 +00:00
when "10.15" then "1100.0.33.16"
when "10.14" then "1001.0.46.4"
when "10.13" then "1000.10.44.2"
when "10.12" then "900.0.39.2"
when "10.11" then "800.0.42.1"
2015-12-09 05:06:00 +00:00
when "10.10" then "700.1.81"
else "600.0.57"
end
end
def minimum_version
case MacOS.version
when "10.15" then "11.0.0"
when "10.14" then "10.0.0"
when "10.13" then "9.0.0"
when "10.12" then "8.0.0"
else "1.0.0"
end
end
def below_minimum_version?
return false unless installed?
2018-09-17 02:45:00 +02:00
version < minimum_version
end
def outdated?
clang_version = detect_clang_version
return false unless clang_version
2018-09-17 02:45:00 +02:00
::Version.new(clang_version) < latest_version
end
def detect_clang_version
path = if MacOS.version >= :mavericks
"#{PKG_PATH}/usr/bin/clang"
else
"/usr/bin/clang"
end
version_output = Utils.popen_read("#{path} --version")
version_output[/clang-(\d+\.\d+\.\d+(\.\d+)?)/, 1]
end
2013-05-22 22:26:09 -05:00
2014-03-22 10:14:20 +00:00
# Version string (a pretty long one) of the CLT package.
# Note, that different ways to install the CLTs lead to different
# version numbers.
def version
if @version ||= detect_version
::Version.new @version
else
::Version::NULL
end
end
2013-07-21 20:09:55 -05:00
def detect_version
version = nil
[EXECUTABLE_PKG_ID, MAVERICKS_NEW_PKG_ID].each do |id|
next unless File.exist?("#{PKG_PATH}/usr/bin/clang")
version = MacOS.pkgutil_info(id)[/version: (.+)$/, 1]
return version if version
end
detect_clang_version
end
2013-07-21 20:09:55 -05:00
end
end
end