blob: b897ddba2c2c5b1e7941424b736d938f3efc5810 [file] [log] [blame]
# Copyright 2016 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Repository rule to generate host xcode_config and xcode_version targets.
The xcode_config and xcode_version targets are configured for xcodes/SDKs
installed on the local host.
"""
OSX_EXECUTE_TIMEOUT = 600
def _search_string(fullstring, prefix, suffix):
"""Returns the substring between two given substrings of a larger string.
Args:
fullstring: The larger string to search.
prefix: The substring that should occur directly before the returned string.
suffix: The substring that should occur directly after the returned string.
Returns:
A string occurring in fullstring exactly prefixed by prefix, and exactly
terminated by suffix. For example, ("hello goodbye", "lo ", " bye") will
return "good". If there is no such string, returns the empty string.
"""
prefix_index = fullstring.find(prefix)
if (prefix_index < 0):
return ""
result_start_index = prefix_index + len(prefix)
suffix_index = fullstring.find(suffix, result_start_index)
if (suffix_index < 0):
return ""
return fullstring[result_start_index:suffix_index]
def _search_sdk_output(output, sdkname):
"""Returns the SDK version given xcodebuild stdout and an sdkname."""
return _search_string(output, "(%s" % sdkname, ")")
def _xcode_version_output(repository_ctx, name, version, aliases, developer_dir, timeout):
"""Returns a string containing an xcode_version build target."""
build_contents = ""
decorated_aliases = []
error_msg = ""
for alias in aliases:
decorated_aliases.append("'%s'" % alias)
repository_ctx.report_progress("Fetching SDK information for Xcode %s" % version)
xcodebuild_result = repository_ctx.execute(
["xcrun", "xcodebuild", "-version", "-sdk"],
timeout,
{"DEVELOPER_DIR": developer_dir},
)
if (xcodebuild_result.return_code != 0):
error_msg = (
"Invoking xcodebuild failed, developer dir: {devdir} ," +
"return code {code}, stderr: {err}, stdout: {out}"
).format(
devdir = developer_dir,
code = xcodebuild_result.return_code,
err = xcodebuild_result.stderr,
out = xcodebuild_result.stdout,
)
ios_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "iphoneos")
tvos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "appletvos")
macos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "macosx")
visionos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "xros")
watchos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "watchos")
build_contents += "xcode_version(\n name = '%s'," % name
build_contents += "\n version = '%s'," % version
if aliases:
build_contents += "\n aliases = [%s]," % ", ".join(decorated_aliases)
if ios_sdk_version:
build_contents += "\n default_ios_sdk_version = '%s'," % ios_sdk_version
if tvos_sdk_version:
build_contents += "\n default_tvos_sdk_version = '%s'," % tvos_sdk_version
if macos_sdk_version:
build_contents += "\n default_macos_sdk_version = '%s'," % macos_sdk_version
if visionos_sdk_version:
build_contents += "\n default_visionos_sdk_version = '%s'," % visionos_sdk_version
if watchos_sdk_version:
build_contents += "\n default_watchos_sdk_version = '%s'," % watchos_sdk_version
build_contents += "\n)\n"
if error_msg:
build_contents += "\n# Error: " + error_msg.replace("\n", " ") + "\n"
print(error_msg)
return build_contents
VERSION_CONFIG_STUB = "xcode_config(name = 'host_xcodes')"
def run_xcode_locator(repository_ctx, xcode_locator_src_label):
"""Generates xcode-locator from source and runs it.
Builds xcode-locator in the current repository directory.
Returns the standard output of running xcode-locator with -v, which will
return information about locally installed Xcode toolchains and the versions
they are associated with.
This should only be invoked on a darwin OS, as xcode-locator cannot be built
otherwise.
Args:
repository_ctx: The repository context.
xcode_locator_src_label: The label of the source file for xcode-locator.
Returns:
A 2-tuple containing:
output: A list representing installed xcode toolchain information. Each
element of the list is a struct containing information for one installed
toolchain. This is an empty list if there was an error building or
running xcode-locator.
err: An error string describing the error that occurred when attempting
to build and run xcode-locator, or None if the run was successful.
"""
repository_ctx.report_progress("Building xcode-locator")
xcodeloc_src_path = str(repository_ctx.path(xcode_locator_src_label))
env = repository_ctx.os.environ
if "BAZEL_OSX_EXECUTE_TIMEOUT" in env:
timeout = int(env["BAZEL_OSX_EXECUTE_TIMEOUT"])
else:
timeout = OSX_EXECUTE_TIMEOUT
xcrun_result = repository_ctx.execute([
"env",
"-i",
"DEVELOPER_DIR={}".format(env.get("DEVELOPER_DIR", default = "")),
"xcrun",
"--sdk",
"macosx",
"clang",
"-mmacosx-version-min=10.13",
"-fobjc-arc",
"-framework",
"CoreServices",
"-framework",
"Foundation",
"-o",
"xcode-locator-bin",
xcodeloc_src_path,
], timeout)
if (xcrun_result.return_code != 0):
suggestion = ""
if "Agreeing to the Xcode/iOS license" in xcrun_result.stderr:
suggestion = ("(You may need to sign the Xcode license." +
" Try running 'sudo xcodebuild -license')")
error_msg = (
"Generating xcode-locator-bin failed. {suggestion} " +
"return code {code}, stderr: {err}, stdout: {out}"
).format(
suggestion = suggestion,
code = xcrun_result.return_code,
err = xcrun_result.stderr,
out = xcrun_result.stdout,
)
return ([], error_msg.replace("\n", " "))
repository_ctx.report_progress("Running xcode-locator")
xcode_locator_result = repository_ctx.execute(
["./xcode-locator-bin", "-v"],
timeout,
)
if (xcode_locator_result.return_code != 0):
error_msg = (
"Invoking xcode-locator failed, " +
"return code {code}, stderr: {err}, stdout: {out}"
).format(
code = xcode_locator_result.return_code,
err = xcode_locator_result.stderr,
out = xcode_locator_result.stdout,
)
return ([], error_msg.replace("\n", " "))
xcode_toolchains = []
# xcode_dump is comprised of newlines with different installed Xcode versions,
# each line of the form <version>:<comma_separated_aliases>:<developer_dir>.
xcode_dump = xcode_locator_result.stdout
for xcodeversion in xcode_dump.split("\n"):
if ":" in xcodeversion:
infosplit = xcodeversion.split(":")
toolchain = struct(
version = infosplit[0],
aliases = infosplit[1].split(","),
developer_dir = infosplit[2],
)
xcode_toolchains.append(toolchain)
return (xcode_toolchains, None)
def _darwin_build_file(repository_ctx):
"""Evaluates local system state to create xcode_config and xcode_version targets."""
repository_ctx.report_progress("Fetching the default Xcode version")
env = repository_ctx.os.environ
if "BAZEL_OSX_EXECUTE_TIMEOUT" in env:
timeout = int(env["BAZEL_OSX_EXECUTE_TIMEOUT"])
else:
timeout = OSX_EXECUTE_TIMEOUT
xcodebuild_result = repository_ctx.execute([
"env",
"-i",
"DEVELOPER_DIR={}".format(env.get("DEVELOPER_DIR", default = "")),
"xcrun",
"xcodebuild",
"-version",
], timeout)
(toolchains, xcodeloc_err) = run_xcode_locator(
repository_ctx,
Label(repository_ctx.attr.xcode_locator),
)
if xcodeloc_err:
return VERSION_CONFIG_STUB + "\n# Error: " + xcodeloc_err + "\n"
default_xcode_version = ""
default_xcode_build_version = ""
if xcodebuild_result.return_code == 0:
default_xcode_version = _search_string(xcodebuild_result.stdout, "Xcode ", "\n")
default_xcode_build_version = _search_string(
xcodebuild_result.stdout,
"Build version ",
"\n",
)
default_xcode_target = ""
target_names = []
buildcontents = ""
for toolchain in toolchains:
version = toolchain.version
aliases = toolchain.aliases
developer_dir = toolchain.developer_dir
target_name = "version%s" % version.replace(".", "_")
buildcontents += _xcode_version_output(
repository_ctx,
target_name,
version,
aliases,
developer_dir,
timeout,
)
target_label = "':%s'" % target_name
target_names.append(target_label)
if (version.startswith(default_xcode_version) and
version.endswith(default_xcode_build_version)):
default_xcode_target = target_label
buildcontents += "xcode_config(\n name = 'host_xcodes',"
if target_names:
buildcontents += "\n versions = [%s]," % ", ".join(target_names)
if not default_xcode_target and target_names:
default_xcode_target = sorted(target_names, reverse = True)[0]
print("No default Xcode version is set with 'xcode-select'; picking %s" %
default_xcode_target)
if default_xcode_target:
buildcontents += "\n default = %s," % default_xcode_target
buildcontents += "\n)\n"
buildcontents += "available_xcodes(\n name = 'host_available_xcodes',"
if target_names:
buildcontents += "\n versions = [%s]," % ", ".join(target_names)
if default_xcode_target:
buildcontents += "\n default = %s," % default_xcode_target
buildcontents += "\n)\n"
if repository_ctx.attr.remote_xcode:
buildcontents += "xcode_config(name = 'all_xcodes',"
buildcontents += "\n remote_versions = '%s', " % repository_ctx.attr.remote_xcode
buildcontents += "\n local_versions = ':host_available_xcodes', "
buildcontents += "\n)\n"
return buildcontents
def _impl(repository_ctx):
"""Implementation for the local_config_xcode repository rule.
Generates a BUILD file containing a root xcode_config target named 'host_xcodes',
which points to an xcode_version target for each version of Xcode installed on
the local host machine. If no versions of Xcode are present on the machine
(for instance, if this is a non-darwin OS), creates a stub target.
Args:
repository_ctx: The repository context.
"""
os_name = repository_ctx.os.name
build_contents = "package(default_visibility = ['//visibility:public'])\n\n"
if (os_name.startswith("mac os")):
build_contents += _darwin_build_file(repository_ctx)
else:
build_contents += VERSION_CONFIG_STUB
repository_ctx.file("BUILD", build_contents)
xcode_autoconf = repository_rule(
environ = [
"DEVELOPER_DIR",
"XCODE_VERSION",
],
implementation = _impl,
configure = True,
attrs = {
"xcode_locator": attr.string(),
"remote_xcode": attr.string(),
},
)
def xcode_configure(xcode_locator_label, remote_xcode_label = None):
"""Generates a repository containing host Xcode version information."""
xcode_autoconf(
name = "local_config_xcode",
xcode_locator = xcode_locator_label,
remote_xcode = remote_xcode_label,
)
def _xcode_configure_extension_impl(module_ctx):
xcode_configure("@bazel_tools//tools/osx:xcode_locator.m")
return module_ctx.extension_metadata(reproducible = True)
xcode_configure_extension = module_extension(implementation = _xcode_configure_extension_impl)