|  | # 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 = 120 | 
|  |  | 
|  | 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, | 
|  | local = 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, | 
|  | ) | 
|  |  | 
|  | xcode_configure_extension = module_extension( | 
|  | implementation = lambda ctx: xcode_configure("@bazel_tools//tools/osx:xcode_locator.m"), | 
|  | ) |