| # Copyright 2018 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. | 
 |  | 
 | # WARNING: | 
 | # https://github.com/bazelbuild/bazel/issues/17713 | 
 | # .bzl files in this package (tools/build_defs/repo) are evaluated | 
 | # in a Starlark environment without "@_builtins" injection, and must not refer | 
 | # to symbols associated with build/workspace .bzl files | 
 |  | 
 | """Utils for manipulating external repositories, once fetched. | 
 |  | 
 | ### Setup | 
 |  | 
 | These utilities are intended to be used by other repository rules. They | 
 | can be loaded as follows. | 
 |  | 
 | ```python | 
 | load( | 
 |     "@bazel_tools//tools/build_defs/repo:utils.bzl", | 
 |     "workspace_and_buildfile", | 
 |     "patch", | 
 |     "update_attrs", | 
 | ) | 
 | ``` | 
 | """ | 
 |  | 
 | # Temporary directory for downloading remote patch files. | 
 | _REMOTE_PATCH_DIR = ".tmp_remote_patches" | 
 |  | 
 | # Name preserved for backwards compatibility only - this function used to | 
 | # write a WORKSPACE file if requested. | 
 | def workspace_and_buildfile(ctx): | 
 |     """Utility function for writing a BUILD file. | 
 |  | 
 |     This rule is intended to be used in the implementation function of a | 
 |     repository rule. | 
 |     It assumes the parameters `name`, `build_file`, and `build_file_content` to | 
 |     be present in `ctx.attr`; the latter two possibly with value None. | 
 |  | 
 |     Args: | 
 |       ctx: The repository context of the repository rule calling this utility | 
 |         function. | 
 |     """ | 
 |     if ctx.attr.build_file and ctx.attr.build_file_content: | 
 |         ctx.fail("Only one of build_file and build_file_content can be provided.") | 
 |  | 
 |     if ctx.attr.build_file: | 
 |         ctx.file("BUILD.bazel", ctx.read(ctx.attr.build_file)) | 
 |     elif ctx.attr.build_file_content: | 
 |         ctx.file("BUILD.bazel", ctx.attr.build_file_content) | 
 |  | 
 | def _is_windows(ctx): | 
 |     return ctx.os.name.lower().find("windows") != -1 | 
 |  | 
 | def _use_native_patch(patch_args): | 
 |     """If patch_args only contains -p<NUM> options, we can use the native patch implementation.""" | 
 |     for arg in patch_args: | 
 |         if not arg.startswith("-p"): | 
 |             return False | 
 |     return True | 
 |  | 
 | def _download_patch(ctx, patch_url, integrity, auth = None): | 
 |     name = patch_url.split("/")[-1] | 
 |     patch_path = ctx.path(_REMOTE_PATCH_DIR).get_child(name) | 
 |     ctx.download( | 
 |         patch_url, | 
 |         patch_path, | 
 |         canonical_id = ctx.attr.canonical_id, | 
 |         auth = get_auth(ctx, [patch_url]) if auth == None else auth, | 
 |         integrity = integrity, | 
 |     ) | 
 |     return patch_path | 
 |  | 
 | def download_remote_files(ctx, auth = None): | 
 |     """Utility function for downloading remote files. | 
 |  | 
 |     This rule is intended to be used in the implementation function of | 
 |     a repository rule. It assumes the parameters `remote_file_urls` and | 
 |     `remote_file_integrity` to be present in `ctx.attr`. | 
 |  | 
 |     Args: | 
 |       ctx: The repository context of the repository rule calling this utility | 
 |         function. | 
 |       auth: An optional dict specifying authentication information for some of the URLs. | 
 |     """ | 
 |     pending = [ | 
 |         ctx.download( | 
 |             remote_file_urls, | 
 |             path, | 
 |             canonical_id = ctx.attr.canonical_id, | 
 |             auth = get_auth(ctx, remote_file_urls) if auth == None else auth, | 
 |             integrity = ctx.attr.remote_file_integrity.get(path, ""), | 
 |             block = False, | 
 |         ) | 
 |         for path, remote_file_urls in ctx.attr.remote_file_urls.items() | 
 |     ] | 
 |  | 
 |     # Wait until the requests are done | 
 |     for p in pending: | 
 |         p.wait() | 
 |  | 
 | def patch(ctx, patches = None, patch_cmds = None, patch_cmds_win = None, patch_tool = None, patch_args = None, auth = None): | 
 |     """Implementation of patching an already extracted repository. | 
 |  | 
 |     This rule is intended to be used in the implementation function of | 
 |     a repository rule. If the parameters `patches`, `patch_tool`, | 
 |     `patch_args`, `patch_cmds` and `patch_cmds_win` are not specified | 
 |     then they are taken from `ctx.attr`. | 
 |  | 
 |     Args: | 
 |       ctx: The repository context of the repository rule calling this utility | 
 |         function. | 
 |       patches: The patch files to apply. List of strings, Labels, or paths. | 
 |       patch_cmds: Bash commands to run for patching, passed one at a | 
 |         time to bash -c. List of strings | 
 |       patch_cmds_win: Powershell commands to run for patching, passed | 
 |         one at a time to powershell /c. List of strings. If the | 
 |         boolean value of this parameter is false, patch_cmds will be | 
 |         used and this parameter will be ignored. | 
 |       patch_tool: Path of the patch tool to execute for applying | 
 |         patches. String. | 
 |       patch_args: Arguments to pass to the patch tool. List of strings. | 
 |       auth: An optional dict specifying authentication information for some of the URLs. | 
 |  | 
 |     """ | 
 |     bash_exe = ctx.os.environ["BAZEL_SH"] if "BAZEL_SH" in ctx.os.environ else "bash" | 
 |     powershell_exe = ctx.os.environ["BAZEL_POWERSHELL"] if "BAZEL_POWERSHELL" in ctx.os.environ else "powershell.exe" | 
 |  | 
 |     if patches == None: | 
 |         patches = [] | 
 |     if hasattr(ctx.attr, "patches") and ctx.attr.patches: | 
 |         patches += ctx.attr.patches | 
 |  | 
 |     remote_patches = {} | 
 |     remote_patch_strip = 0 | 
 |     if hasattr(ctx.attr, "remote_patches") and ctx.attr.remote_patches: | 
 |         if hasattr(ctx.attr, "remote_patch_strip"): | 
 |             remote_patch_strip = ctx.attr.remote_patch_strip | 
 |         remote_patches = ctx.attr.remote_patches | 
 |  | 
 |     if patch_cmds == None and hasattr(ctx.attr, "patch_cmds"): | 
 |         patch_cmds = ctx.attr.patch_cmds | 
 |     if patch_cmds == None: | 
 |         patch_cmds = [] | 
 |  | 
 |     if patch_cmds_win == None and hasattr(ctx.attr, "patch_cmds_win"): | 
 |         patch_cmds_win = ctx.attr.patch_cmds_win | 
 |     if patch_cmds_win == None: | 
 |         patch_cmds_win = [] | 
 |  | 
 |     if patch_tool == None and hasattr(ctx.attr, "patch_tool"): | 
 |         patch_tool = ctx.attr.patch_tool | 
 |     if not patch_tool: | 
 |         patch_tool = "patch" | 
 |         native_patch = True | 
 |     else: | 
 |         native_patch = False | 
 |  | 
 |     if patch_args == None and hasattr(ctx.attr, "patch_args"): | 
 |         patch_args = ctx.attr.patch_args | 
 |     if patch_args == None: | 
 |         patch_args = [] | 
 |  | 
 |     if hasattr(ctx.attr, "patch_strip"): | 
 |         new_patch_args = ["-p%s" % ctx.attr.patch_strip] | 
 |         new_patch_args.extend(patch_args) | 
 |         patch_args = new_patch_args | 
 |  | 
 |     if len(remote_patches) > 0 or len(patches) > 0 or len(patch_cmds) > 0: | 
 |         ctx.report_progress("Patching repository") | 
 |  | 
 |     # Apply remote patches | 
 |     for patch_url in remote_patches: | 
 |         integrity = remote_patches[patch_url] | 
 |         patchfile = _download_patch(ctx, patch_url, integrity, auth) | 
 |         ctx.patch(patchfile, remote_patch_strip) | 
 |         ctx.delete(patchfile) | 
 |     ctx.delete(ctx.path(_REMOTE_PATCH_DIR)) | 
 |  | 
 |     # Apply local patches | 
 |     if native_patch and _use_native_patch(patch_args): | 
 |         if patch_args: | 
 |             strip = int(patch_args[-1][2:]) | 
 |         else: | 
 |             strip = 0 | 
 |         for patchfile in patches: | 
 |             ctx.patch(patchfile, strip) | 
 |     else: | 
 |         for patchfile in patches: | 
 |             command = "{patchtool} {patch_args} < {patchfile}".format( | 
 |                 patchtool = patch_tool, | 
 |                 patchfile = ctx.path(patchfile), | 
 |                 patch_args = " ".join([ | 
 |                     "'%s'" % arg | 
 |                     for arg in patch_args | 
 |                 ]), | 
 |             ) | 
 |             st = ctx.execute([bash_exe, "-c", command]) | 
 |             if st.return_code: | 
 |                 fail("Error applying patch %s:\n%s%s" % | 
 |                      (str(patchfile), st.stderr, st.stdout)) | 
 |  | 
 |     if _is_windows(ctx) and patch_cmds_win: | 
 |         for cmd in patch_cmds_win: | 
 |             st = ctx.execute([powershell_exe, "/c", cmd]) | 
 |             if st.return_code: | 
 |                 fail("Error applying patch command %s:\n%s%s" % | 
 |                      (cmd, st.stdout, st.stderr)) | 
 |     else: | 
 |         for cmd in patch_cmds: | 
 |             st = ctx.execute([bash_exe, "-c", cmd]) | 
 |             if st.return_code: | 
 |                 fail("Error applying patch command %s:\n%s%s" % | 
 |                      (cmd, st.stdout, st.stderr)) | 
 |  | 
 | def update_attrs(orig, keys, override): | 
 |     """Utility function for altering and adding the specified attributes to a particular repository rule invocation. | 
 |  | 
 |      This is used to make a rule reproducible. | 
 |  | 
 |     Args: | 
 |         orig: dict of actually set attributes (either explicitly or implicitly) | 
 |             by a particular rule invocation | 
 |         keys: complete set of attributes defined on this rule | 
 |         override: dict of attributes to override or add to orig | 
 |  | 
 |     Returns: | 
 |         dict of attributes with the keys from override inserted/updated | 
 |     """ | 
 |     result = {} | 
 |     for key in keys: | 
 |         if getattr(orig, key) != None: | 
 |             result[key] = getattr(orig, key) | 
 |     result["name"] = orig.name | 
 |     result.update(override) | 
 |     return result | 
 |  | 
 | def maybe(repo_rule, name, **kwargs): | 
 |     """Utility function for only adding a repository if it's not already present. | 
 |  | 
 |     This is to implement safe repositories.bzl macro documented in | 
 |     https://bazel.build/rules/deploying#dependencies. | 
 |  | 
 |     Args: | 
 |         repo_rule: repository rule function. | 
 |         name: name of the repository to create. | 
 |         **kwargs: remaining arguments that are passed to the repo_rule function. | 
 |  | 
 |     Returns: | 
 |         Nothing, defines the repository when needed as a side-effect. | 
 |     """ | 
 |     if not native.existing_rule(name): | 
 |         repo_rule(name = name, **kwargs) | 
 |  | 
 | def read_netrc(ctx, filename): | 
 |     """Utility function to parse at least a basic .netrc file. | 
 |  | 
 |     Args: | 
 |       ctx: The repository context of the repository rule calling this utility | 
 |         function. | 
 |       filename: the name of the .netrc file to read | 
 |  | 
 |     Returns: | 
 |       dict mapping a machine names to a dict with the information provided | 
 |       about them | 
 |     """ | 
 |  | 
 |     # Do not cause the repo rule to rerun due to changes to auth info when it is | 
 |     # successful. Failures are not cached. | 
 |     contents = ctx.read(filename, watch = "no") | 
 |     return parse_netrc(contents, filename) | 
 |  | 
 | def parse_netrc(contents, filename = "a .netrc file"): | 
 |     """Utility function to parse at least a basic .netrc file. | 
 |  | 
 |     Args: | 
 |       contents: input for the parser. | 
 |       filename: filename to use in error messages, if any. | 
 |  | 
 |     Returns: | 
 |       dict mapping a machine names to a dict with the information provided | 
 |       about them | 
 |     """ | 
 |  | 
 |     # Parse the file. This is mainly a token-based update of a simple state | 
 |     # machine, but we need to keep the line structure to correctly determine | 
 |     # the end of a `macdef` command. | 
 |     netrc = {} | 
 |     currentmachinename = None | 
 |     currentmachine = {} | 
 |     macdef = None | 
 |     currentmacro = "" | 
 |     cmd = None | 
 |     for line in contents.splitlines(): | 
 |         if line.startswith("#"): | 
 |             # Comments start with #. Ignore these lines. | 
 |             continue | 
 |         elif macdef: | 
 |             # as we're in a macro, just determine if we reached the end. | 
 |             if line: | 
 |                 currentmacro += line + "\n" | 
 |             else: | 
 |                 # reached end of macro, add it | 
 |                 currentmachine[macdef] = currentmacro | 
 |                 macdef = None | 
 |                 currentmacro = "" | 
 |         else: | 
 |             line = line.replace("\t", " ") | 
 |  | 
 |             # Essentially line.split(None) which starlark does not support. | 
 |             tokens = [ | 
 |                 w.strip() | 
 |                 for w in line.split(" ") | 
 |                 if len(w.strip()) > 0 | 
 |             ] | 
 |             for token in tokens: | 
 |                 if cmd: | 
 |                     # we have a command that expects another argument | 
 |                     if cmd == "machine": | 
 |                         # a new machine definition was provided, so save the | 
 |                         # old one, if present | 
 |                         if not currentmachinename == None: | 
 |                             netrc[currentmachinename] = currentmachine | 
 |                         currentmachine = {} | 
 |                         currentmachinename = token | 
 |                     elif cmd == "macdef": | 
 |                         macdef = "macdef %s" % (token,) | 
 |                         # a new macro definition; the documentation says | 
 |                         # "its contents begin with the next .netrc line [...]", | 
 |                         # so should there really be tokens left in the current | 
 |                         # line, they're not part of the macro. | 
 |  | 
 |                     else: | 
 |                         currentmachine[cmd] = token | 
 |                     cmd = None | 
 |                 elif token in [ | 
 |                     "machine", | 
 |                     "login", | 
 |                     "password", | 
 |                     "account", | 
 |                     "macdef", | 
 |                 ]: | 
 |                     # command takes one argument | 
 |                     cmd = token | 
 |                 elif token == "default": | 
 |                     # defines the default machine; again, store old machine | 
 |                     if not currentmachinename == None: | 
 |                         netrc[currentmachinename] = currentmachine | 
 |  | 
 |                     # We use the empty string for the default machine, as that | 
 |                     # can never be a valid hostname ("default" could be, in the | 
 |                     # default search domain). | 
 |                     currentmachinename = "" | 
 |                     currentmachine = {} | 
 |                 else: | 
 |                     fail("Unexpected token '%s' while reading %s" % (token, filename)) | 
 |     if not currentmachinename == None: | 
 |         netrc[currentmachinename] = currentmachine | 
 |     return netrc | 
 |  | 
 | def use_netrc(netrc, urls, patterns): | 
 |     """Compute an auth dict from a parsed netrc file and a list of URLs. | 
 |  | 
 |     Args: | 
 |       netrc: a netrc file already parsed to a dict, e.g., as obtained from | 
 |         read_netrc | 
 |       urls: a list of URLs. | 
 |       patterns: optional dict of url to authorization patterns | 
 |  | 
 |     Returns: | 
 |       dict suitable as auth argument for ctx.download; more precisely, the dict | 
 |       will map all URLs where the netrc file provides login and password to a | 
 |       dict containing the corresponding login, password and optional authorization pattern, | 
 |       as well as the mapping of "type" to "basic" or "pattern". | 
 |     """ | 
 |     auth = {} | 
 |     for url in urls: | 
 |         schemerest = url.split("://", 1) | 
 |         if len(schemerest) < 2: | 
 |             continue | 
 |         if not (schemerest[0] in ["http", "https"]): | 
 |             # For other protocols, bazel currently does not support | 
 |             # authentication. So ignore them. | 
 |             continue | 
 |         host = schemerest[1].split("/")[0].split(":")[0] | 
 |         if host in netrc: | 
 |             authforhost = netrc[host] | 
 |         elif "" in netrc: | 
 |             authforhost = netrc[""] | 
 |         else: | 
 |             continue | 
 |  | 
 |         if host in patterns: | 
 |             auth_dict = { | 
 |                 "type": "pattern", | 
 |                 "pattern": patterns[host], | 
 |             } | 
 |  | 
 |             if "login" in authforhost: | 
 |                 auth_dict["login"] = authforhost["login"] | 
 |  | 
 |             if "password" in authforhost: | 
 |                 auth_dict["password"] = authforhost["password"] | 
 |  | 
 |             auth[url] = auth_dict | 
 |         elif "password" in authforhost: | 
 |             if "login" in authforhost: | 
 |                 auth[url] = { | 
 |                     "type": "basic", | 
 |                     "login": authforhost["login"], | 
 |                     "password": authforhost["password"], | 
 |                 } | 
 |             else: | 
 |                 auth[url] = { | 
 |                     "type": "pattern", | 
 |                     "pattern": "Bearer <password>", | 
 |                     "password": authforhost["password"], | 
 |                 } | 
 |         else: | 
 |             # buildifier: disable=print | 
 |             print("WARNING: Found machine in .netrc for URL %s, but no password." % url) | 
 |  | 
 |     return auth | 
 |  | 
 | def read_user_netrc(ctx): | 
 |     """Read user's default netrc file. | 
 |  | 
 |     Args: | 
 |       ctx: The repository context of the repository rule calling this utility function. | 
 |  | 
 |     Returns: | 
 |       dict mapping a machine names to a dict with the information provided about them. | 
 |     """ | 
 |     if ctx.os.name.startswith("windows"): | 
 |         home_dir = ctx.os.environ.get("USERPROFILE", "") | 
 |     else: | 
 |         home_dir = ctx.os.environ.get("HOME", "") | 
 |  | 
 |     if not home_dir: | 
 |         return {} | 
 |  | 
 |     netrcfile = "{}/.netrc".format(home_dir) | 
 |     if not ctx.path(netrcfile).exists: | 
 |         return {} | 
 |     return read_netrc(ctx, netrcfile) | 
 |  | 
 | def get_auth(ctx, urls): | 
 |     """Utility function to obtain the correct auth dict for a list of urls from .netrc file. | 
 |  | 
 |     Support optional netrc and auth_patterns attributes if available. | 
 |  | 
 |     Args: | 
 |       ctx: The repository context of the repository rule calling this utility | 
 |         function. | 
 |       urls: the list of urls to read | 
 |  | 
 |     Returns: | 
 |       the auth dict which can be passed to repository_ctx.download | 
 |     """ | 
 |     if hasattr(ctx.attr, "netrc") and ctx.attr.netrc: | 
 |         netrc = read_netrc(ctx, ctx.attr.netrc) | 
 |     elif "NETRC" in ctx.os.environ: | 
 |         netrc = read_netrc(ctx, ctx.os.environ["NETRC"]) | 
 |     else: | 
 |         netrc = read_user_netrc(ctx) | 
 |     auth_patterns = {} | 
 |     if hasattr(ctx.attr, "auth_patterns") and ctx.attr.auth_patterns: | 
 |         auth_patterns = ctx.attr.auth_patterns | 
 |     return use_netrc(netrc, urls, auth_patterns) |