| Tim Windelschmidt | f5c4510 | 2025-02-08 22:30:58 +0000 | [diff] [blame] | 1 | load( |
| 2 | "@bazel_tools//tools/build_defs/repo:utils.bzl", |
| 3 | "patch", |
| 4 | "update_attrs", |
| 5 | "workspace_and_buildfile", |
| 6 | ) |
| 7 | |
| 8 | def _build_archive_url(owner, repo, ref): |
| 9 | return "https://github.com/{owner}/{repo}/archive/{ref}.tar.gz".format( |
| 10 | owner = owner, |
| 11 | repo = repo, |
| 12 | ref = ref, |
| 13 | ), "{repo}-{ref}".format(repo = repo, ref = ref) |
| 14 | |
| 15 | def build_submodule_info_url(owner, repo, submodule, ref): |
| 16 | return "https://api.github.com/repos/{owner}/{repo}/contents/{submodule}?ref={ref}".format( |
| 17 | owner = owner, |
| 18 | repo = repo, |
| 19 | submodule = submodule, |
| 20 | ref = ref, |
| 21 | ) |
| 22 | |
| 23 | def parse_github_url(url): |
| 24 | url = url.removeprefix("https://github.com/") |
| 25 | url = url.removesuffix(".git") |
| 26 | (owner, repo) = url.split("/") |
| 27 | return owner, repo |
| 28 | |
| 29 | def _github_repository(ctx): |
| 30 | base_repo_archive_url, base_repo_archive_prefix = _build_archive_url( |
| 31 | owner = ctx.attr.owner, |
| 32 | repo = ctx.attr.repo, |
| 33 | ref = ctx.attr.ref, |
| 34 | ) |
| 35 | |
| 36 | base_repo_download_info = ctx.download_and_extract( |
| 37 | url = base_repo_archive_url, |
| 38 | stripPrefix = base_repo_archive_prefix, |
| 39 | integrity = ctx.attr.integrity, |
| 40 | type = "tar.gz", |
| 41 | ) |
| 42 | |
| 43 | for submodule, integrity in ctx.attr.submodules.items(): |
| Tim Windelschmidt | db08522 | 2025-04-28 20:58:18 +0200 | [diff] [blame] | 44 | if submodule not in ctx.attr.submodule_info: |
| 45 | url = build_submodule_info_url( |
| 46 | owner = ctx.attr.owner, |
| 47 | repo = ctx.attr.repo, |
| 48 | ref = ctx.attr.ref, |
| 49 | submodule = submodule, |
| 50 | ) |
| Tim Windelschmidt | f5c4510 | 2025-02-08 22:30:58 +0000 | [diff] [blame] | 51 | |
| Tim Windelschmidt | db08522 | 2025-04-28 20:58:18 +0200 | [diff] [blame] | 52 | submodule_info_path = submodule + ".submodule_info" |
| 53 | ctx.download( |
| 54 | url = url, |
| 55 | headers = { |
| 56 | "Accept": "application/vnd.github+json", |
| 57 | "X-GitHub-Api-Version": "2022-11-28", |
| 58 | }, |
| 59 | output = submodule_info_path, |
| 60 | ) |
| Tim Windelschmidt | f5c4510 | 2025-02-08 22:30:58 +0000 | [diff] [blame] | 61 | |
| Tim Windelschmidt | db08522 | 2025-04-28 20:58:18 +0200 | [diff] [blame] | 62 | submodule_info = json.decode(ctx.read(submodule_info_path)) |
| 63 | |
| 64 | # buildifier: disable=print |
| 65 | print("Missing submodule_info for submodule %s. Consider adding it: \n%s" % (submodule, submodule_info)) |
| 66 | else: |
| 67 | submodule_info = json.decode(ctx.attr.submodule_info[submodule]) |
| 68 | |
| Tim Windelschmidt | f5c4510 | 2025-02-08 22:30:58 +0000 | [diff] [blame] | 69 | if submodule_info["type"] != "submodule": |
| 70 | fail("provided submodule path is not a submodule") |
| 71 | |
| 72 | submodule_owner, submodule_repo = parse_github_url( |
| 73 | url = submodule_info["submodule_git_url"], |
| 74 | ) |
| 75 | |
| 76 | submodule_url, submodule_strip_prefix = _build_archive_url( |
| 77 | owner = submodule_owner, |
| 78 | repo = submodule_repo, |
| 79 | ref = submodule_info["sha"], |
| 80 | ) |
| 81 | |
| 82 | download_info = ctx.download_and_extract( |
| 83 | url = submodule_url, |
| 84 | stripPrefix = submodule_strip_prefix, |
| 85 | integrity = integrity, |
| 86 | type = "tar.gz", |
| 87 | output = submodule_info["path"], |
| 88 | ) |
| 89 | if integrity == "": |
| 90 | # buildifier: disable=print |
| Tim Windelschmidt | db08522 | 2025-04-28 20:58:18 +0200 | [diff] [blame] | 91 | print("Missing integrity for submodule {submodule}. Consider adding it:\n\"{submodule}\": \"{integrity}\".".format( |
| Tim Windelschmidt | f5c4510 | 2025-02-08 22:30:58 +0000 | [diff] [blame] | 92 | submodule = submodule, |
| 93 | integrity = download_info.integrity, |
| 94 | )) |
| 95 | |
| 96 | workspace_and_buildfile(ctx) |
| 97 | |
| 98 | patch(ctx) |
| 99 | |
| 100 | return update_attrs(ctx.attr, _github_repository_attrs.keys(), {"integrity": base_repo_download_info.integrity}) |
| 101 | |
| 102 | _github_repository_attrs = { |
| 103 | "owner": attr.string( |
| 104 | mandatory = True, |
| 105 | doc = "The Owner of the Github repository", |
| 106 | ), |
| 107 | "repo": attr.string( |
| 108 | mandatory = True, |
| 109 | doc = "The Name of Github repository", |
| 110 | ), |
| 111 | "submodules": attr.string_dict( |
| 112 | mandatory = False, |
| 113 | default = {}, |
| 114 | doc = "The list of submodules with their integrity as value", |
| 115 | ), |
| Tim Windelschmidt | db08522 | 2025-04-28 20:58:18 +0200 | [diff] [blame] | 116 | "submodule_info": attr.string_dict( |
| 117 | mandatory = False, |
| 118 | default = {}, |
| 119 | doc = """The list of submodules with their GitHub API response as value. |
| 120 | This is a workaround until https://github.com/bazelbuild/bazel/issues/24777 |
| 121 | is implemented to prevent hitting GitHub API limits.""", |
| 122 | ), |
| Tim Windelschmidt | f5c4510 | 2025-02-08 22:30:58 +0000 | [diff] [blame] | 123 | "ref": attr.string( |
| 124 | default = "", |
| 125 | doc = |
| 126 | "The specific ref to be checked out.", |
| 127 | ), |
| 128 | "integrity": attr.string( |
| 129 | doc = """Expected checksum in Subresource Integrity format of the file downloaded. |
| 130 | |
| 131 | This must match the checksum of the file downloaded. _It is a security risk |
| 132 | to omit the checksum as remote files can change._ At best omitting this |
| 133 | field will make your build non-hermetic. It is optional to make development |
| 134 | easier but either this attribute or `sha256` should be set before shipping.""", |
| 135 | ), |
| 136 | "patches": attr.label_list( |
| 137 | default = [], |
| 138 | doc = |
| 139 | "A list of files that are to be applied as patches after " + |
| 140 | "extracting the archive. By default, it uses the Bazel-native patch implementation " + |
| 141 | "which doesn't support fuzz match and binary patch, but Bazel will fall back to use " + |
| 142 | "patch command line tool if `patch_tool` attribute is specified or there are " + |
| 143 | "arguments other than `-p` in `patch_args` attribute.", |
| 144 | ), |
| 145 | "patch_tool": attr.string( |
| 146 | default = "", |
| 147 | doc = "The patch(1) utility to use. If this is specified, Bazel will use the specified " + |
| 148 | "patch tool instead of the Bazel-native patch implementation.", |
| 149 | ), |
| 150 | "patch_args": attr.string_list( |
| 151 | default = ["-p0"], |
| 152 | doc = |
| 153 | "The arguments given to the patch tool. Defaults to -p0, " + |
| 154 | "however -p1 will usually be needed for patches generated by " + |
| 155 | "git. If multiple -p arguments are specified, the last one will take effect." + |
| 156 | "If arguments other than -p are specified, Bazel will fall back to use patch " + |
| 157 | "command line tool instead of the Bazel-native patch implementation. When falling " + |
| 158 | "back to patch command line tool and patch_tool attribute is not specified, " + |
| 159 | "`patch` will be used. This only affects patch files in the `patches` attribute.", |
| 160 | ), |
| 161 | "patch_cmds": attr.string_list( |
| 162 | default = [], |
| 163 | doc = "Sequence of Bash commands to be applied on Linux/Macos after patches are applied.", |
| 164 | ), |
| 165 | "build_file": attr.label( |
| 166 | allow_single_file = True, |
| 167 | doc = |
| 168 | "The file to use as the BUILD file for this repository." + |
| 169 | "This attribute is an absolute label (use '@//' for the main " + |
| 170 | "repo). The file does not need to be named BUILD, but can " + |
| 171 | "be (something like BUILD.new-repo-name may work well for " + |
| 172 | "distinguishing it from the repository's actual BUILD files. " + |
| 173 | "Either build_file or build_file_content can be specified, but " + |
| 174 | "not both.", |
| 175 | ), |
| 176 | "build_file_content": attr.string( |
| 177 | doc = |
| 178 | "The content for the BUILD file for this repository. " + |
| 179 | "Either build_file or build_file_content can be specified, but " + |
| 180 | "not both.", |
| 181 | ), |
| 182 | "workspace_file": attr.label( |
| 183 | doc = |
| 184 | "The file to use as the `WORKSPACE` file for this repository. " + |
| 185 | "Either `workspace_file` or `workspace_file_content` can be " + |
| 186 | "specified, or neither, but not both.", |
| 187 | ), |
| 188 | "workspace_file_content": attr.string( |
| 189 | doc = |
| 190 | "The content for the WORKSPACE file for this repository. " + |
| 191 | "Either `workspace_file` or `workspace_file_content` can be " + |
| 192 | "specified, or neither, but not both.", |
| 193 | ), |
| 194 | } |
| 195 | |
| 196 | github_repository = repository_rule( |
| 197 | implementation = _github_repository, |
| 198 | attrs = _github_repository_attrs, |
| 199 | ) |