Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 1 | # Copyright 2016 The Bazel Authors. All rights reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | """Repository rule to generate host xcode_config and xcode_version targets. |
| 15 | |
| 16 | The xcode_config and xcode_version targets are configured for xcodes/SDKs |
| 17 | installed on the local host. |
| 18 | """ |
| 19 | |
schmitt | 818bc01 | 2019-09-30 13:08:19 -0700 | [diff] [blame] | 20 | _EXECUTE_TIMEOUT = 120 |
| 21 | |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 22 | def _search_string(fullstring, prefix, suffix): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 23 | """Returns the substring between two given substrings of a larger string. |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 24 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 25 | Args: |
| 26 | fullstring: The larger string to search. |
| 27 | prefix: The substring that should occur directly before the returned string. |
jingwen | 4a74c52 | 2018-11-20 11:57:15 -0800 | [diff] [blame] | 28 | suffix: The substring that should occur directly after the returned string. |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 29 | Returns: |
| 30 | A string occurring in fullstring exactly prefixed by prefix, and exactly |
| 31 | terminated by suffix. For example, ("hello goodbye", "lo ", " bye") will |
| 32 | return "good". If there is no such string, returns the empty string. |
| 33 | """ |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 34 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 35 | prefix_index = fullstring.find(prefix) |
| 36 | if (prefix_index < 0): |
| 37 | return "" |
| 38 | result_start_index = prefix_index + len(prefix) |
| 39 | suffix_index = fullstring.find(suffix, result_start_index) |
| 40 | if (suffix_index < 0): |
| 41 | return "" |
| 42 | return fullstring[result_start_index:suffix_index] |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 43 | |
| 44 | def _search_sdk_output(output, sdkname): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 45 | """Returns the sdk version given xcodebuild stdout and an sdkname.""" |
| 46 | return _search_string(output, "(%s" % sdkname, ")") |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 47 | |
| 48 | def _xcode_version_output(repository_ctx, name, version, aliases, developer_dir): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 49 | """Returns a string containing an xcode_version build target.""" |
| 50 | build_contents = "" |
| 51 | decorated_aliases = [] |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 52 | error_msg = "" |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 53 | for alias in aliases: |
| 54 | decorated_aliases.append("'%s'" % alias) |
davg | 762f9e2 | 2021-03-08 07:28:00 -0800 | [diff] [blame] | 55 | repository_ctx.report_progress("Fetching SDK information for Xcode %s" % version) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 56 | xcodebuild_result = repository_ctx.execute( |
| 57 | ["xcrun", "xcodebuild", "-version", "-sdk"], |
schmitt | 818bc01 | 2019-09-30 13:08:19 -0700 | [diff] [blame] | 58 | _EXECUTE_TIMEOUT, |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 59 | {"DEVELOPER_DIR": developer_dir}, |
| 60 | ) |
| 61 | if (xcodebuild_result.return_code != 0): |
| 62 | error_msg = ( |
| 63 | "Invoking xcodebuild failed, developer dir: {devdir} ," + |
| 64 | "return code {code}, stderr: {err}, stdout: {out}" |
| 65 | ).format( |
| 66 | devdir = developer_dir, |
| 67 | code = xcodebuild_result.return_code, |
| 68 | err = xcodebuild_result.stderr, |
| 69 | out = xcodebuild_result.stdout, |
| 70 | ) |
| 71 | ios_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "iphoneos") |
| 72 | tvos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "appletvos") |
| 73 | macos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "macosx") |
| 74 | watchos_sdk_version = _search_sdk_output(xcodebuild_result.stdout, "watchos") |
| 75 | build_contents += "xcode_version(\n name = '%s'," % name |
| 76 | build_contents += "\n version = '%s'," % version |
| 77 | if aliases: |
Samuel Giddins | f52e218 | 2020-12-04 14:58:09 -0800 | [diff] [blame] | 78 | build_contents += "\n aliases = [%s]," % ", ".join(decorated_aliases) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 79 | if ios_sdk_version: |
| 80 | build_contents += "\n default_ios_sdk_version = '%s'," % ios_sdk_version |
| 81 | if tvos_sdk_version: |
| 82 | build_contents += "\n default_tvos_sdk_version = '%s'," % tvos_sdk_version |
| 83 | if macos_sdk_version: |
| 84 | build_contents += "\n default_macos_sdk_version = '%s'," % macos_sdk_version |
| 85 | if watchos_sdk_version: |
| 86 | build_contents += "\n default_watchos_sdk_version = '%s'," % watchos_sdk_version |
| 87 | build_contents += "\n)\n" |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 88 | if error_msg: |
| 89 | build_contents += "\n# Error: " + error_msg.replace("\n", " ") + "\n" |
| 90 | print(error_msg) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 91 | return build_contents |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 92 | |
Chris Parsons | cc867d4 | 2016-09-20 15:41:09 +0000 | [diff] [blame] | 93 | VERSION_CONFIG_STUB = "xcode_config(name = 'host_xcodes')" |
| 94 | |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 95 | def run_xcode_locator(repository_ctx, xcode_locator_src_label): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 96 | """Generates xcode-locator from source and runs it. |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 97 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 98 | Builds xcode-locator in the current repository directory. |
| 99 | Returns the standard output of running xcode-locator with -v, which will |
| 100 | return information about locally installed Xcode toolchains and the versions |
| 101 | they are associated with. |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 102 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 103 | This should only be invoked on a darwin OS, as xcode-locator cannot be built |
| 104 | otherwise. |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 105 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 106 | Args: |
| 107 | repository_ctx: The repository context. |
| 108 | xcode_locator_src_label: The label of the source file for xcode-locator. |
| 109 | Returns: |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 110 | A 2-tuple containing: |
| 111 | output: A list representing installed xcode toolchain information. Each |
| 112 | element of the list is a struct containing information for one installed |
| 113 | toolchain. This is an empty list if there was an error building or |
| 114 | running xcode-locator. |
| 115 | err: An error string describing the error that occurred when attempting |
| 116 | to build and run xcode-locator, or None if the run was successful. |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 117 | """ |
davg | 762f9e2 | 2021-03-08 07:28:00 -0800 | [diff] [blame] | 118 | repository_ctx.report_progress("Building xcode-locator") |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 119 | xcodeloc_src_path = str(repository_ctx.path(xcode_locator_src_label)) |
Brentley Jones | 008f743 | 2020-10-22 07:05:52 -0700 | [diff] [blame] | 120 | env = repository_ctx.os.environ |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 121 | xcrun_result = repository_ctx.execute([ |
| 122 | "env", |
| 123 | "-i", |
Brentley Jones | 008f743 | 2020-10-22 07:05:52 -0700 | [diff] [blame] | 124 | "DEVELOPER_DIR={}".format(env.get("DEVELOPER_DIR", default = "")), |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 125 | "xcrun", |
Keith Smiley | ada2c55 | 2019-09-13 08:09:31 -0700 | [diff] [blame] | 126 | "--sdk", |
| 127 | "macosx", |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 128 | "clang", |
Keith Smiley | 4ffb36f | 2019-09-18 09:01:05 -0700 | [diff] [blame] | 129 | "-mmacosx-version-min=10.9", |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 130 | "-fobjc-arc", |
| 131 | "-framework", |
| 132 | "CoreServices", |
| 133 | "-framework", |
| 134 | "Foundation", |
| 135 | "-o", |
| 136 | "xcode-locator-bin", |
| 137 | xcodeloc_src_path, |
schmitt | 818bc01 | 2019-09-30 13:08:19 -0700 | [diff] [blame] | 138 | ], _EXECUTE_TIMEOUT) |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 139 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 140 | if (xcrun_result.return_code != 0): |
| 141 | suggestion = "" |
| 142 | if "Agreeing to the Xcode/iOS license" in xcrun_result.stderr: |
| 143 | suggestion = ("(You may need to sign the xcode license." + |
| 144 | " Try running 'sudo xcodebuild -license')") |
| 145 | error_msg = ( |
| 146 | "Generating xcode-locator-bin failed. {suggestion} " + |
| 147 | "return code {code}, stderr: {err}, stdout: {out}" |
| 148 | ).format( |
| 149 | suggestion = suggestion, |
| 150 | code = xcrun_result.return_code, |
| 151 | err = xcrun_result.stderr, |
| 152 | out = xcrun_result.stdout, |
| 153 | ) |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 154 | return ([], error_msg.replace("\n", " ")) |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 155 | |
davg | 762f9e2 | 2021-03-08 07:28:00 -0800 | [diff] [blame] | 156 | repository_ctx.report_progress("Running xcode-locator") |
schmitt | 818bc01 | 2019-09-30 13:08:19 -0700 | [diff] [blame] | 157 | xcode_locator_result = repository_ctx.execute( |
| 158 | ["./xcode-locator-bin", "-v"], |
| 159 | _EXECUTE_TIMEOUT, |
| 160 | ) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 161 | if (xcode_locator_result.return_code != 0): |
| 162 | error_msg = ( |
| 163 | "Invoking xcode-locator failed, " + |
| 164 | "return code {code}, stderr: {err}, stdout: {out}" |
| 165 | ).format( |
| 166 | code = xcode_locator_result.return_code, |
| 167 | err = xcode_locator_result.stderr, |
| 168 | out = xcode_locator_result.stdout, |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 169 | ) |
| 170 | return ([], error_msg.replace("\n", " ")) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 171 | xcode_toolchains = [] |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 172 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 173 | # xcode_dump is comprised of newlines with different installed xcode versions, |
| 174 | # each line of the form <version>:<comma_separated_aliases>:<developer_dir>. |
| 175 | xcode_dump = xcode_locator_result.stdout |
| 176 | for xcodeversion in xcode_dump.split("\n"): |
| 177 | if ":" in xcodeversion: |
| 178 | infosplit = xcodeversion.split(":") |
| 179 | toolchain = struct( |
| 180 | version = infosplit[0], |
| 181 | aliases = infosplit[1].split(","), |
| 182 | developer_dir = infosplit[2], |
| 183 | ) |
| 184 | xcode_toolchains.append(toolchain) |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 185 | return (xcode_toolchains, None) |
cparsons | cc21998 | 2017-05-03 22:13:44 +0200 | [diff] [blame] | 186 | |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 187 | def _darwin_build_file(repository_ctx): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 188 | """Evaluates local system state to create xcode_config and xcode_version targets.""" |
davg | 762f9e2 | 2021-03-08 07:28:00 -0800 | [diff] [blame] | 189 | repository_ctx.report_progress("Fetching the default Xcode version") |
Brentley Jones | 008f743 | 2020-10-22 07:05:52 -0700 | [diff] [blame] | 190 | env = repository_ctx.os.environ |
| 191 | xcodebuild_result = repository_ctx.execute([ |
| 192 | "env", |
| 193 | "-i", |
| 194 | "DEVELOPER_DIR={}".format(env.get("DEVELOPER_DIR", default = "")), |
| 195 | "xcrun", |
| 196 | "xcodebuild", |
| 197 | "-version", |
| 198 | ], _EXECUTE_TIMEOUT) |
Chris Parsons | 7479f67 | 2016-10-04 18:39:09 +0000 | [diff] [blame] | 199 | |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 200 | (toolchains, xcodeloc_err) = run_xcode_locator( |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 201 | repository_ctx, |
| 202 | Label(repository_ctx.attr.xcode_locator), |
| 203 | ) |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 204 | |
Keith Smiley | c6c9b58 | 2019-02-28 11:27:07 -0800 | [diff] [blame] | 205 | if xcodeloc_err: |
| 206 | return VERSION_CONFIG_STUB + "\n# Error: " + xcodeloc_err + "\n" |
| 207 | |
Keith Smiley | b7d419b | 2020-03-04 02:09:39 -0800 | [diff] [blame] | 208 | default_xcode_version = "" |
| 209 | default_xcode_build_version = "" |
| 210 | if xcodebuild_result.return_code == 0: |
| 211 | default_xcode_version = _search_string(xcodebuild_result.stdout, "Xcode ", "\n") |
| 212 | default_xcode_build_version = _search_string( |
| 213 | xcodebuild_result.stdout, |
| 214 | "Build version ", |
| 215 | "\n", |
| 216 | ) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 217 | default_xcode_target = "" |
| 218 | target_names = [] |
| 219 | buildcontents = "" |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 220 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 221 | for toolchain in toolchains: |
| 222 | version = toolchain.version |
| 223 | aliases = toolchain.aliases |
| 224 | developer_dir = toolchain.developer_dir |
| 225 | target_name = "version%s" % version.replace(".", "_") |
Keith Smiley | b7d419b | 2020-03-04 02:09:39 -0800 | [diff] [blame] | 226 | buildcontents += _xcode_version_output( |
| 227 | repository_ctx, |
| 228 | target_name, |
| 229 | version, |
| 230 | aliases, |
| 231 | developer_dir, |
| 232 | ) |
| 233 | target_label = "':%s'" % target_name |
| 234 | target_names.append(target_label) |
| 235 | if (version.startswith(default_xcode_version) and |
| 236 | version.endswith(default_xcode_build_version)): |
| 237 | default_xcode_target = target_label |
Samuel Giddins | f52e218 | 2020-12-04 14:58:09 -0800 | [diff] [blame] | 238 | buildcontents += "xcode_config(\n name = 'host_xcodes'," |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 239 | if target_names: |
| 240 | buildcontents += "\n versions = [%s]," % ", ".join(target_names) |
Keith Smiley | b7d419b | 2020-03-04 02:09:39 -0800 | [diff] [blame] | 241 | if not default_xcode_target and target_names: |
| 242 | default_xcode_target = sorted(target_names, reverse = True)[0] |
| 243 | print("No default Xcode version is set with 'xcode-select'; picking %s" % |
| 244 | default_xcode_target) |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 245 | if default_xcode_target: |
Keith Smiley | b7d419b | 2020-03-04 02:09:39 -0800 | [diff] [blame] | 246 | buildcontents += "\n default = %s," % default_xcode_target |
| 247 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 248 | buildcontents += "\n)\n" |
Samuel Giddins | f52e218 | 2020-12-04 14:58:09 -0800 | [diff] [blame] | 249 | buildcontents += "available_xcodes(\n name = 'host_available_xcodes'," |
steinman | ec1625f | 2019-12-04 14:54:59 -0800 | [diff] [blame] | 250 | if target_names: |
| 251 | buildcontents += "\n versions = [%s]," % ", ".join(target_names) |
| 252 | if default_xcode_target: |
Keith Smiley | b7d419b | 2020-03-04 02:09:39 -0800 | [diff] [blame] | 253 | buildcontents += "\n default = %s," % default_xcode_target |
steinman | ec1625f | 2019-12-04 14:54:59 -0800 | [diff] [blame] | 254 | buildcontents += "\n)\n" |
steinman | d0b2163 | 2020-01-08 13:27:41 -0800 | [diff] [blame] | 255 | if repository_ctx.attr.remote_xcode: |
| 256 | buildcontents += "xcode_config(name = 'all_xcodes'," |
| 257 | buildcontents += "\n remote_versions = '%s', " % repository_ctx.attr.remote_xcode |
| 258 | buildcontents += "\n local_versions = ':host_available_xcodes', " |
| 259 | buildcontents += "\n)\n" |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 260 | return buildcontents |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 261 | |
| 262 | def _impl(repository_ctx): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 263 | """Implementation for the local_config_xcode repository rule. |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 264 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 265 | Generates a BUILD file containing a root xcode_config target named 'host_xcodes', |
| 266 | which points to an xcode_version target for each version of xcode installed on |
| 267 | the local host machine. If no versions of xcode are present on the machine |
| 268 | (for instance, if this is a non-darwin OS), creates a stub target. |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 269 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 270 | Args: |
| 271 | repository_ctx: The repository context. |
| 272 | """ |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 273 | |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 274 | os_name = repository_ctx.os.name.lower() |
| 275 | build_contents = "package(default_visibility = ['//visibility:public'])\n\n" |
| 276 | if (os_name.startswith("mac os")): |
| 277 | build_contents += _darwin_build_file(repository_ctx) |
| 278 | else: |
| 279 | build_contents += VERSION_CONFIG_STUB |
| 280 | repository_ctx.file("BUILD", build_contents) |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 281 | |
| 282 | xcode_autoconf = repository_rule( |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 283 | implementation = _impl, |
| 284 | local = True, |
| 285 | attrs = { |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 286 | "xcode_locator": attr.string(), |
steinman | d0b2163 | 2020-01-08 13:27:41 -0800 | [diff] [blame] | 287 | "remote_xcode": attr.string(), |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 288 | }, |
Chris Parsons | 381850e | 2016-08-31 17:04:17 +0000 | [diff] [blame] | 289 | ) |
| 290 | |
steinman | d0b2163 | 2020-01-08 13:27:41 -0800 | [diff] [blame] | 291 | def xcode_configure(xcode_locator_label, remote_xcode_label = None): |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 292 | """Generates a repository containing host xcode version information.""" |
| 293 | xcode_autoconf( |
| 294 | name = "local_config_xcode", |
| 295 | xcode_locator = xcode_locator_label, |
steinman | d0b2163 | 2020-01-08 13:27:41 -0800 | [diff] [blame] | 296 | remote_xcode = remote_xcode_label, |
vladmos | 20a042f | 2018-06-01 04:51:21 -0700 | [diff] [blame] | 297 | ) |