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