1# Copyright 2023 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 15"""This module provides the gazelle_python_manifest macro that contains targets 16for updating and testing the Gazelle manifest file. 17""" 18 19load("@bazel_skylib//rules:diff_test.bzl", "diff_test") 20load("@io_bazel_rules_go//go:def.bzl", "GoSource", "go_test") 21load("@rules_python//python:defs.bzl", "py_binary") 22 23def gazelle_python_manifest( 24 name, 25 modules_mapping, 26 requirements = [], 27 pip_repository_name = "", 28 pip_deps_repository_name = "", 29 manifest = ":gazelle_python.yaml", 30 **kwargs): 31 """A macro for defining the updating and testing targets for the Gazelle manifest file. 32 33 Args: 34 name: the name used as a base for the targets. 35 modules_mapping: the target for the generated modules_mapping.json file. 36 requirements: the target for the requirements.txt file or a list of 37 requirements files that will be concatenated before passing on to 38 the manifest generator. If unset, no integrity field is added to the 39 manifest, meaning testing it is just as expensive as generating it, 40 but modifying it is much less likely to result in a merge conflict. 41 pip_repository_name: the name of the pip_install or pip_repository target. 42 pip_deps_repository_name: deprecated - the old pip_install target name. 43 manifest: the Gazelle manifest file. 44 defaults to the same value as manifest. 45 **kwargs: other bazel attributes passed to the generate and test targets 46 generated by this macro. 47 """ 48 if pip_deps_repository_name != "": 49 # buildifier: disable=print 50 print("DEPRECATED pip_deps_repository_name in //{}:{}. Please use pip_repository_name instead.".format( 51 native.package_name(), 52 name, 53 )) 54 pip_repository_name = pip_deps_repository_name 55 56 if pip_repository_name == "": 57 # This is a temporary check while pip_deps_repository_name exists as deprecated. 58 fail("pip_repository_name must be set in //{}:{}".format(native.package_name(), name)) 59 60 test_target = "{}.test".format(name) 61 update_target = "{}.update".format(name) 62 update_target_label = "//{}:{}".format(native.package_name(), update_target) 63 64 manifest_genrule = name + ".genrule" 65 generated_manifest = name + ".generated_manifest" 66 manifest_generator = Label("//manifest/generate:generate") 67 manifest_generator_hash = Label("//manifest/generate:generate_lib_sources_hash") 68 69 if requirements and type(requirements) == "list": 70 # This runs if requirements is a list or is unset (default value is empty list) 71 native.genrule( 72 name = name + "_requirements_gen", 73 srcs = sorted(requirements), 74 outs = [name + "_requirements.txt"], 75 cmd_bash = "cat $(SRCS) > $@", 76 cmd_bat = "type $(SRCS) > $@", 77 ) 78 requirements = name + "_requirements_gen" 79 80 update_args = [ 81 "--manifest-generator-hash=$(execpath {})".format(manifest_generator_hash), 82 "--requirements=$(rootpath {})".format(requirements) if requirements else "--requirements=", 83 "--pip-repository-name={}".format(pip_repository_name), 84 "--modules-mapping=$(execpath {})".format(modules_mapping), 85 "--output=$(execpath {})".format(generated_manifest), 86 "--update-target={}".format(update_target_label), 87 ] 88 89 native.genrule( 90 name = manifest_genrule, 91 outs = [generated_manifest], 92 cmd = "$(execpath {}) {}".format(manifest_generator, " ".join(update_args)), 93 tools = [manifest_generator], 94 srcs = [ 95 modules_mapping, 96 manifest_generator_hash, 97 ] + ([requirements] if requirements else []), 98 tags = ["manual"], 99 ) 100 101 py_binary( 102 name = update_target, 103 srcs = [Label("//manifest:copy_to_source.py")], 104 main = Label("//manifest:copy_to_source.py"), 105 args = [ 106 "$(rootpath {})".format(generated_manifest), 107 "$(rootpath {})".format(manifest), 108 ], 109 data = [ 110 generated_manifest, 111 manifest, 112 ], 113 tags = kwargs.get("tags", []) + ["manual"], 114 **{k: v for k, v in kwargs.items() if k != "tags"} 115 ) 116 117 if requirements: 118 attrs = { 119 "env": { 120 "_TEST_MANIFEST": "$(rootpath {})".format(manifest), 121 "_TEST_MANIFEST_GENERATOR_HASH": "$(rlocationpath {})".format(manifest_generator_hash), 122 "_TEST_REQUIREMENTS": "$(rootpath {})".format(requirements), 123 }, 124 "size": "small", 125 } 126 go_test( 127 name = test_target, 128 srcs = [Label("//manifest/test:test.go")], 129 data = [ 130 manifest, 131 requirements, 132 manifest_generator_hash, 133 ], 134 rundir = ".", 135 deps = [ 136 Label("//manifest"), 137 Label("@io_bazel_rules_go//go/runfiles"), 138 ], 139 # kwargs could contain test-specific attributes like size or timeout 140 **dict(attrs, **kwargs) 141 ) 142 else: 143 diff_test( 144 name = test_target, 145 file1 = generated_manifest, 146 file2 = manifest, 147 failure_message = "Gazelle manifest is out of date. Run 'bazel run {}' to update it.".format(native.package_relative_label(update_target)), 148 **kwargs 149 ) 150 151 native.filegroup( 152 name = name, 153 srcs = [manifest], 154 tags = ["manual"], 155 visibility = ["//visibility:public"], 156 ) 157 158# buildifier: disable=provider-params 159AllSourcesInfo = provider(fields = {"all_srcs": "All sources collected from the target and dependencies."}) 160 161_rules_python_workspace = Label("@rules_python//:WORKSPACE") 162 163def _get_all_sources_impl(target, ctx): 164 is_rules_python = target.label.workspace_name == _rules_python_workspace.workspace_name 165 if not is_rules_python: 166 # Avoid adding third-party dependency files to the checksum of the srcs. 167 return AllSourcesInfo(all_srcs = depset()) 168 srcs = depset( 169 target[GoSource].orig_srcs, 170 transitive = [dep[AllSourcesInfo].all_srcs for dep in ctx.rule.attr.deps], 171 ) 172 return [AllSourcesInfo(all_srcs = srcs)] 173 174_get_all_sources = aspect( 175 implementation = _get_all_sources_impl, 176 attr_aspects = ["deps"], 177) 178 179def _sources_hash_impl(ctx): 180 all_srcs = ctx.attr.go_library[AllSourcesInfo].all_srcs 181 hash_file = ctx.actions.declare_file(ctx.attr.name + ".hash") 182 args = ctx.actions.args() 183 args.add(hash_file) 184 args.add_all(all_srcs) 185 ctx.actions.run( 186 outputs = [hash_file], 187 inputs = all_srcs, 188 arguments = [args], 189 executable = ctx.executable._hasher, 190 ) 191 return [DefaultInfo( 192 files = depset([hash_file]), 193 runfiles = ctx.runfiles([hash_file]), 194 )] 195 196sources_hash = rule( 197 _sources_hash_impl, 198 attrs = { 199 "go_library": attr.label( 200 aspects = [_get_all_sources], 201 providers = [GoSource], 202 ), 203 "_hasher": attr.label( 204 cfg = "exec", 205 default = Label("//manifest/hasher"), 206 executable = True, 207 ), 208 }, 209) 210