diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000..e69de29b diff --git a/cmd/gazelle/BUILD.bazel b/cmd/gazelle/BUILD.bazel index a76806e3..d6c3343e 100644 --- a/cmd/gazelle/BUILD.bazel +++ b/cmd/gazelle/BUILD.bazel @@ -29,6 +29,7 @@ go_library( "//cmd/gazelle/internal/wspace", "//language/proto_go_modules", "//language/protobuf", + "//language/starlarkrepository", "@bazel_gazelle//config", "@bazel_gazelle//flag", "@bazel_gazelle//label", diff --git a/cmd/gazelle/langs.go b/cmd/gazelle/langs.go index a84ea14b..8be63c40 100644 --- a/cmd/gazelle/langs.go +++ b/cmd/gazelle/langs.go @@ -21,6 +21,7 @@ import ( "github.com/bazelbuild/bazel-gazelle/language/proto" "github.com/stackb/rules_proto/v4/language/proto_go_modules" "github.com/stackb/rules_proto/v4/language/protobuf" + "github.com/stackb/rules_proto/v4/language/starlarkrepository" ) var languages = []language.Language{ @@ -28,4 +29,5 @@ var languages = []language.Language{ protobuf.NewLanguage(), golang.NewLanguage(), proto_go_modules.NewLanguage(), + starlarkrepository.NewLanguage(), } diff --git a/extensions/starlark_repository.bzl b/extensions/starlark_repository.bzl new file mode 100644 index 00000000..12beb0e7 --- /dev/null +++ b/extensions/starlark_repository.bzl @@ -0,0 +1,135 @@ +"""proto_repostitory.bzl provides the starlark_repository rule.""" + +# Copyright 2014 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. + +load("@bazel_features//:features.bzl", "bazel_features") +load("@build_stack_rules_proto//rules/proto:starlark_repository.bzl", "starlark_repository_attrs", starlark_repository_repo_rule = "starlark_repository") + +def _extension_metadata( + module_ctx, + *, + root_module_direct_deps = None, + root_module_direct_dev_deps = None, + reproducible = False): + """returns the module_ctx.extension_metadata in a bazel-version-aware way + + This function was copied from the bazel-gazelle repository. + """ + + if not hasattr(module_ctx, "extension_metadata"): + return None + metadata_kwargs = {} + if bazel_features.external_deps.extension_metadata_has_reproducible: + metadata_kwargs["reproducible"] = reproducible + return module_ctx.extension_metadata( + root_module_direct_deps = root_module_direct_deps, + root_module_direct_dev_deps = root_module_direct_dev_deps, + **metadata_kwargs + ) + +def _starlark_repository_impl(module_ctx): + # named_archives / named_locals are dicts where V is the kwargs for + # the underlying "starlark_repository" repo rule and K is the tag.name + # (the name given by the MODULE.bazel author). + named_archives = {} + named_locals = {} + + # iterate all the module tags and gather a list of named repos. + # + # TODO(pcj): what is the best practice for version selection here? Do I need + # to check if module.is_root and handle that differently? + # + for module in module_ctx.modules: + for tag in module.tags.archive: + kwargs = { + attr: getattr(tag, attr) + for attr in _starlark_repository_archive_attrs.keys() + if hasattr(tag, attr) + } + named_archives[tag.name] = kwargs + for tag in module.tags.local: + kwargs = { + attr: getattr(tag, attr) + for attr in _starlark_repository_local_attrs.keys() + if hasattr(tag, attr) + } + + # The user-facing attr is "path"; the underlying repo rule expects + # "local_path" (a sibling of "urls" / "commit" / "version"). + kwargs["local_path"] = kwargs.pop("path") + named_locals[tag.name] = kwargs + + # declare a repository rule foreach one + for apparent_name, kwargs in named_archives.items(): + starlark_repository_repo_rule( + apparent_name = apparent_name, + **kwargs + ) + for apparent_name, kwargs in named_locals.items(): + starlark_repository_repo_rule( + apparent_name = apparent_name, + **kwargs + ) + + return _extension_metadata( + module_ctx, + reproducible = True, + ) + +_starlark_repository_archive_attrs = starlark_repository_attrs | { + "name": attr.string( + doc = "The repo name.", + mandatory = True, + ), +} +_starlark_repository_archive_attrs.pop("apparent_name") + +# Attrs for the .local() tag class. Excludes archive-only attrs (urls, sha256, +# strip_prefix, type, integrity, canonical_id, auth_patterns, commit, tag, +# vcs, remote, version, sum, replace) and instead takes a single `path` +# (mapped to the underlying rule's `local_path`). +_starlark_repository_local_attrs = { + "name": attr.string( + doc = "The repo name.", + mandatory = True, + ), + "path": attr.string( + doc = "Filesystem path (workspace-relative or absolute) to the repository contents.", + mandatory = True, + ), + "build_directives": attr.string_list(), + "build_file_generation": attr.string(), + "languages": attr.string_list(), + "cfgs": attr.label_list(allow_files = True), + "imports": attr.label_list(allow_files = True), + "imports_out": attr.string(default = "imports.csv"), + "deleted_files": attr.string_list(), + "reresolve_known_proto_imports": attr.bool(), + "importpath": attr.string(), +} + +starlark_repository = module_extension( + implementation = _starlark_repository_impl, + tag_classes = dict( + archive = tag_class( + doc = "declare an http_archive repository that is post-processed by a custom version of gazelle that includes the 'protobuf' language", + attrs = _starlark_repository_archive_attrs, + ), + local = tag_class( + doc = "declare a local-path repository that is post-processed by gazelle's starlarkrepository language. Useful when the source already lives on disk (e.g. a git submodule) and we want to avoid network fetches.", + attrs = _starlark_repository_local_attrs, + ), + ), +) diff --git a/go.mod b/go.mod index b110e4a7..da5afb98 100644 --- a/go.mod +++ b/go.mod @@ -22,15 +22,11 @@ require ( require ( github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang/mock v1.7.0-rc.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.35.0 // indirect golang.org/x/tools/go/vcs v0.1.0-deprecated // indirect - google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect ) diff --git a/go.sum b/go.sum index 94e660cd..26e3fa25 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,11 @@ -github.com/bazelbuild/bazel-gazelle v0.45.0 h1:ZfbDRyNppw0Sd42lXVX7ybar63MJofb58Yvl4SvbtYY= -github.com/bazelbuild/bazel-gazelle v0.45.0/go.mod h1:XdBdWhrTc5x50CKzKXOcwrZWdLuX58IX1KcSaWPtEGo= github.com/bazelbuild/bazel-gazelle v0.47.0 h1:g3Rr1ZbkC1Pk20aOgBITxSD/efS1WbaSty5jC786Z3Q= github.com/bazelbuild/bazel-gazelle v0.47.0/go.mod h1:8Ozf20jhv+in87nCUHdmUPPcVGTfKg/gotZ/hce3T+w= -github.com/bazelbuild/buildtools v0.0.0-20250826111327-4006b543a694 h1:LiKs9FsSfMx3NomNclXYkv9enY77oft5Mc/vX/AKHgI= -github.com/bazelbuild/buildtools v0.0.0-20250826111327-4006b543a694/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52 h1:njQAmjTv/YHRm/0Lfv9DXHFZ4MdT2IA/RKHTnqZkgDw= github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= -github.com/bazelbuild/rules_go v0.57.0 h1:qBFxjy29iJg22xWlu5A3mNwrXtCHiEnHcIt91SsiFGU= -github.com/bazelbuild/rules_go v0.57.0/go.mod h1:Pn30cb4M513fe2rQ6GiJ3q8QyrRsgC7zhuDvi50Lw4Y= github.com/bazelbuild/rules_go v0.59.0 h1:RLhOwYIqeMgBpKelHEWTfIPjA37so3oa/rX+/qqq/P4= github.com/bazelbuild/rules_go v0.59.0/go.mod h1:Pn30cb4M513fe2rQ6GiJ3q8QyrRsgC7zhuDvi50Lw4Y= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= -github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= -github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -26,8 +18,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= -github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -42,7 +32,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -62,52 +51,32 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -116,8 +85,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f h1:387Y+JbxF52bmesc8kq1NyYIp33dnxCw6eiA7JMsTmw= -google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:0joYwWwLQh18AOj8zMYeZLjzuqcYTU3/nC5JdCvC3JI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= diff --git a/language/starlarkrepository/BUILD.bazel b/language/starlarkrepository/BUILD.bazel new file mode 100644 index 00000000..9053aa3e --- /dev/null +++ b/language/starlarkrepository/BUILD.bazel @@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "starlarkrepository", + srcs = ["language.go"], + importpath = "github.com/stackb/rules_proto/v4/language/starlarkrepository", + visibility = ["//visibility:public"], + deps = [ + "@bazel_gazelle//config", + "@bazel_gazelle//label", + "@bazel_gazelle//language", + "@bazel_gazelle//repo", + "@bazel_gazelle//resolve", + "@bazel_gazelle//rule", + "@com_github_bazelbuild_buildtools//build", + ], +) + +go_test( + name = "starlarkrepository_test", + srcs = ["language_test.go"], + embed = [":starlarkrepository"], + deps = ["@com_github_bazelbuild_buildtools//build"], +) diff --git a/language/starlarkrepository/language.go b/language/starlarkrepository/language.go new file mode 100644 index 00000000..841f4931 --- /dev/null +++ b/language/starlarkrepository/language.go @@ -0,0 +1,572 @@ +/* Copyright 2020 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. +*/ + +// Package symbol generates a `starlark_library` target for every `.bzl` file in +// each package. At the root of the module, a single starlark_library is +// populated with deps that include all other starlark_libraries. +// +// The original code for this gazelle extension started from +// https://github.com/bazelbuild/bazel-skylib/blob/main/gazelle/bzl/gazelle.go. +package starlarkrepository + +import ( + "bufio" + "context" + "flag" + "fmt" + "io" + "log" + "maps" + "os" + "path/filepath" + "slices" + "sort" + "strings" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/language" + "github.com/bazelbuild/bazel-gazelle/repo" + "github.com/bazelbuild/bazel-gazelle/resolve" + "github.com/bazelbuild/bazel-gazelle/rule" + "github.com/bazelbuild/buildtools/build" +) + +const ( + languageName = "starlarkrepository" + repoNameDirectiveName = languageName + "_repo_name" + rootDirectiveName = languageName + "_root" + excludeDirectiveName = languageName + "_exclude" + logFileDirectiveName = languageName + "_log_file" + starlarkModuleKind = "starlark_module" + starlarkModuleLibraryKind = "starlark_module_library" + starlarkModuleLibraryName = "modules" + fileType = ".bzl" + visibilityPublic = "//visibility:public" +) + +var ( + ignoreSuffix = suffixes{ + "_tests.bzl", + "_test.bzl", + } + starlarkModuleLibraryKindInfo = map[string]rule.KindInfo{ + starlarkModuleLibraryKind: { + NonEmptyAttrs: map[string]bool{"modules": true}, + ResolveAttrs: map[string]bool{"modules": true}, + }, + } + starlarkModuleKindInfo = map[string]rule.KindInfo{ + starlarkModuleKind: { + NonEmptyAttrs: map[string]bool{"src": true}, + }, + } + starlarkModuleLibraryLoadInfo = rule.LoadInfo{ + Name: "@build_stack_rules_proto//rules:starlark_module_library.bzl", + Symbols: []string{starlarkModuleLibraryKind}, + } + starlarkModuleLoadInfo = rule.LoadInfo{ + Name: "@build_stack_rules_proto//rules:starlark_module.bzl", + Symbols: []string{starlarkModuleKind}, + } +) + +type starlarkRepositoryLang struct { + roots []string + repoName string + excludeDirs []string + bazelVersion string + bazelIgnore []string + logFile string + logWriter *os.File + logger *log.Logger +} + +// NewLanguage is called by Gazelle to install this language extension in a +// binary. +func NewLanguage() language.Language { + return &starlarkRepositoryLang{ + excludeDirs: []string{ + "docs", + "javatests", + "testdata", + "tests", + }, + } +} + +// Name returns the name of the language. This should be a prefix of the kinds +// of rules generated by the language, e.g., "go" for the Go extension since it +// generates "go_library" rules. +func (*starlarkRepositoryLang) Name() string { + return languageName +} + +// The following methods are implemented to satisfy the +// https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve?tab=doc#Resolver +// interface, but are otherwise unused. +func (ext *starlarkRepositoryLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { + fs.StringVar(&ext.logFile, logFileDirectiveName, "", "path to log file for this extension") +} + +func (ext *starlarkRepositoryLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { + if ext.logFile != "" { + ext.createLogger(ext.logFile) + ext.logf("CheckFlags: log file initialized from flag: %s", ext.logFile) + } + if ext.logger == nil { + ext.logger = log.New(io.Discard, "", 0) + } + + // REPO.bazel causes visibility issues if the root BUILD file has been + // deleted but still referencing non-existent rules. This should be moved + // to the part where gazelle cleans up build files. + repoBazelFilename := filepath.Join(c.RepoRoot, "REPO.bazel") + deleteFile(repoBazelFilename, ext.logf) + + bazelVersionFilename := filepath.Join(c.RepoRoot, ".bazelversion") + if lines, err := readFileLines(bazelVersionFilename, ext.logf); err == nil { + ext.bazelVersion = strings.Join(lines, ":") + deleteFile(bazelVersionFilename, ext.logf) + } + + bazelIgnoreFilename := filepath.Join(c.RepoRoot, ".bazelignore") + if lines, err := readFileLines(bazelIgnoreFilename, ext.logf); err == nil { + ext.bazelIgnore = lines + deleteFile(bazelIgnoreFilename, ext.logf) + } + + return nil +} + +func (*starlarkRepositoryLang) KnownDirectives() []string { + return []string{ + rootDirectiveName, + excludeDirectiveName, + logFileDirectiveName, + } +} + +func (ext *starlarkRepositoryLang) Configure(c *config.Config, rel string, f *rule.File) { + if f != nil { + ext.logf("Configure: processing %d directives in %s", len(f.Directives), rel) + for _, d := range f.Directives { + switch d.Key { + case rootDirectiveName: + ext.roots = append(ext.roots, d.Value) + case repoNameDirectiveName: + ext.repoName = d.Value + case excludeDirectiveName: + ext.excludeDirs = append(ext.excludeDirs, d.Value) + case logFileDirectiveName: + ext.createLogger(d.Value) + } + } + } +} + +func (ext *starlarkRepositoryLang) createLogger(logFile string) { + if logFile != "" { + f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + if err == nil { + ext.logWriter = f + ext.logger = log.New(f, "", log.LstdFlags) + } else { + log.Fatalf("attempting to open log file: %v", err) + } + } + if false && ext.logger == nil { + ext.logger = log.New(io.Discard, "", 0) + } + + ext.logf("Log initialized") +} + +// Kinds returns a map of maps rule names (kinds) and information on how to +// match and merge attributes that may be found in rules of those kinds. All +// kinds of rules generated for this language may be found here. +func (*starlarkRepositoryLang) Kinds() map[string]rule.KindInfo { + kinds := map[string]rule.KindInfo{} + maps.Copy(kinds, starlarkModuleLibraryKindInfo) + maps.Copy(kinds, starlarkModuleKindInfo) + return kinds +} + +// Loads returns .bzl files and symbols they define. Every rule generated by +// GenerateRules, now or in the past, should be loadable from one of these +// files. +func (*starlarkRepositoryLang) Loads() []rule.LoadInfo { + return []rule.LoadInfo{ + starlarkModuleLibraryLoadInfo, + starlarkModuleLoadInfo, + } +} + +// Fix repairs deprecated usage of language-specific rules in f. This is called +// before the file is indexed. Unless c.ShouldFix is true, fixes that delete or +// rename rules should not be performed. +func (*starlarkRepositoryLang) Fix(c *config.Config, f *rule.File) { +} + +// Imports returns a list of ImportSpecs that can be used to import the rule r. +// This is used to populate RuleIndex. +// +// If nil is returned, the rule will not be indexed. If any non-nil slice is +// returned, including an empty slice, the rule will be indexed. +func (ext *starlarkRepositoryLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { + switch r.Kind() { + case starlarkModuleKind: + return ext.starlarkModuleImports(c, r, f) + default: + return nil + } +} + +func (ext *starlarkRepositoryLang) starlarkModuleImports(_ *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { + // return one for file level dep resolution, one for the rule kind itself + return []resolve.ImportSpec{ + {Lang: languageName, Imp: fmt.Sprintf("//%s:%s", f.Pkg, r.AttrString("src"))}, + {Lang: languageName, Imp: starlarkModuleKind}, + } +} + +// Embeds returns a list of labels of rules that the given rule embeds. If a +// rule is embedded by another importable rule of the same language, only the +// embedding rule will be indexed. The embedding rule will inherit the imports +// of the embedded rule. Since SkyLark doesn't support embedding this should +// always return nil. +func (*starlarkRepositoryLang) Embeds(r *rule.Rule, from label.Label) []label.Label { + return nil +} + +// Resolve translates imported libraries for a given rule into Bazel +// dependencies. Information about imported libraries is returned for each rule +// generated by language.GenerateRules in language.GenerateResult.Imports. +// Resolve generates a "deps" attribute (or the appropriate language-specific +// equivalent) for each import according to language-specific rules and +// heuristics. +func (ext *starlarkRepositoryLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) { + switch r.Kind() { + case starlarkModuleLibraryKind: + ext.starlarkModuleLibraryResolve(c, ix, rc, r, importsRaw, from) + } +} + +// GenerateRules extracts build metadata from source files in a directory. +// GenerateRules is called in each directory where an update is requested in +// depth-first post-order. +// +// args contains the arguments for GenerateRules. This is passed as a struct to +// avoid breaking implementations in the future when new fields are added. +// +// A GenerateResult struct is returned. Optional fields may be added to this +// type in the future. +// +// Any non-fatal errors this function encounters should be logged using +// log.Print. +func (ext *starlarkRepositoryLang) GenerateRules(args language.GenerateArgs) (result language.GenerateResult) { + // extension is effectively disabled without specifying at least one root + if len(ext.roots) == 0 { + return + } + // skip excluded dirs + for _, dir := range ext.excludeDirs { + if strings.HasPrefix(args.Rel, dir) { + return + } + } + + ext.logf("GenerateRules %v: visiting %s: %v", ext.roots, args.Rel, args.RegularFiles) + mustListFiles(ext.logf, filepath.Join(args.Config.WorkDir, args.Rel)) + for _, f := range args.RegularFiles { + if !isBzlSourceFile(f) { + continue + } + relPath := filepath.Join(args.Rel, f) + fullPath := filepath.Join(args.Config.WorkDir, relPath) + + _, loadStmts, err := getBzlFileLoadsStmts(fullPath, args.Rel, ext.logf) + if err != nil { + ext.logf("%s: contains syntax errors: %v", fullPath, err) + continue + } + + r, imports := ext.starlarkModuleRule(args, f, loadStmts) + result.Gen = append(result.Gen, r) + result.Imports = append(result.Imports, imports) + log.Printf("generated %s %s/%s", r.Kind(), args.Rel, r.Name()) + } + + if _, ok := getMatchingRoot(args.Rel, ext.roots); ok { + r, imports := ext.starlarkModuleLibraryRule(args) + result.Gen = append(result.Gen, r) + result.Imports = append(result.Imports, imports) + } + + return +} + +// mustListFiles - convenience debugging function to log the files under a given +// dir, excluding proto files and the extra binaries here. +func mustListFiles(logf LogFunc, dir string) []string { + files := make([]string, 0) + + if err := filepath.Walk(dir, func(relname string, info os.FileInfo, err error) error { + if err != nil { + log.Fatal(err) + } + if info.IsDir() { + return nil + } + if filepath.Ext(relname) == ".proto" { + return nil + } + files = append(files, relname) + logf("file: %s", relname) + return nil + }); err != nil { + logf("walk error: %v", err) + } + + return files +} +func (ext *starlarkRepositoryLang) starlarkModuleRule(args language.GenerateArgs, src string, loadStmts []*build.LoadStmt) (*rule.Rule, []any) { + + name := strings.TrimSuffix(src, fileType) + ext.logf("generating %s rule for %s //%s:%s", starlarkModuleKind, src, args.Rel, name) + + loads := make([]string, 0, len(loadStmts)) + for _, stmt := range loadStmts { + loads = append(loads, stmt.Module.Value) + } + sort.Strings(loads) + + r := rule.NewRule(starlarkModuleKind, name) + r.SetAttr("src", src) + r.SetAttr("loads", loads) + r.SetAttr("visibility", []string{visibilityPublic}) + + return r, []any{} +} + +func (ext *starlarkRepositoryLang) starlarkModuleLibraryRule(_ language.GenerateArgs) (*rule.Rule, []any) { + r := rule.NewRule(starlarkModuleLibraryKind, starlarkModuleLibraryName) + if ext.repoName != "" { + r.SetAttr("repo_name", ext.repoName) + } + if ext.bazelVersion != "" { + r.SetAttr("bazelversion", ext.bazelVersion) + } + if len(ext.bazelIgnore) > 0 { + r.SetAttr("bazelignore", ext.bazelIgnore) + } + r.SetAttr("visibility", []string{visibilityPublic}) + return r, []any{} +} + +func (ext *starlarkRepositoryLang) starlarkModuleLibraryResolve(c *config.Config, ix *resolve.RuleIndex, _ *repo.RemoteCache, r *rule.Rule, _ interface{}, from label.Label) { + // only perform resolve if this is one of the roots + root, isRoot := getMatchingRoot(from.Pkg, ext.roots) + if !isRoot { + ext.logf("skipping modules resolution for %v (not a root: %v)", from, ext.roots) + return + } + + var modules []string + + // fetch all starlark_module rules, then filter by the ones below our root + matches := ix.FindRulesByImportWithConfig(c, resolve.ImportSpec{ + Lang: languageName, + Imp: starlarkModuleKind, + }, languageName) + for _, m := range matches { + depLabel := m.Label.Rel(from.Repo, from.Pkg) + if strings.HasPrefix(depLabel.Pkg, root) { + modules = append(modules, depLabel.String()) + } + } + + if len(modules) > 0 { + sort.Strings(modules) + r.SetAttr("modules", modules) + } +} + +// Before implements part of the language.LifecycleManager interface. +func (ext *starlarkRepositoryLang) Before(context.Context) { + ext.logf("Lifecycle: Before() called") + ext.logf("roots: %v", ext.roots) +} + +// DoneGeneratingRules implements part of the language.FinishableLanguage interface. +func (ext *starlarkRepositoryLang) DoneGeneratingRules() { + ext.logf("Lifecycle: DoneGeneratingRules() called") +} + +// AfterResolvingDeps implements part of the language.LifecycleManager interface. +// This is where we flush and close the log file. +func (ext *starlarkRepositoryLang) AfterResolvingDeps(context.Context) { + ext.logf("Lifecycle: AfterResolvingDeps() called - flushing and closing log file") + if ext.logWriter != nil { + // Sync flushes any buffered data to disk + _ = ext.logWriter.Sync() + // Close the file + _ = ext.logWriter.Close() + ext.logWriter = nil + } +} + +// logf logs a message using the extension's logger if configured +func (ext *starlarkRepositoryLang) logf(format string, args ...any) { + if ext.logger != nil { + ext.logger.Printf(format, args...) + } +} + +func getMatchingRoot(rel string, roots []string) (string, bool) { + for _, root := range roots { + if root == rel { + return root, true + } + } + return "", false +} + +type suffixes []string + +func (s suffixes) Matches(test string) bool { + for _, v := range s { + if strings.HasSuffix(test, v) { + return true + } + } + return false +} + +// LogFunc is a function type for logging messages +type LogFunc func(format string, args ...any) + +// deleteFile removes a file and logs any errors +func deleteFile(filename string, logf LogFunc) { + if err := os.Remove(filename); err != nil { + logf("ERROR: failed to remove %s: %v", filename, err) + } +} + +// readFileLines reads a file and returns a slice of trimmed non-empty lines, +// excluding comments (lines starting with #). +func readFileLines(filePath string, logf LogFunc) ([]string, error) { + logf("Reading lines from file: %s", filePath) + + // Open the file + file, err := os.Open(filePath) + if err != nil { + if os.IsNotExist(err) { + logf(" File does not exist") + return nil, err + } + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + // Scan lines using bufio.Scanner + scanner := bufio.NewScanner(file) + var lines []string + + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.TrimSpace(line) + + // Skip empty lines + if trimmed == "" { + continue + } + + // Skip comments + if strings.HasPrefix(trimmed, "#") { + continue + } + + lines = append(lines, trimmed) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to scan file: %w", err) + } + + logf(" Read %d non-empty lines", len(lines)) + return lines, nil +} + +func isBzlSourceFile(f string) bool { + return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f) +} + +func getBzlFileLoadsStmts(path, rel string, logf LogFunc) (*build.File, []*build.LoadStmt, error) { + f, err := os.ReadFile(path) + if err != nil { + return nil, nil, fmt.Errorf("os.ReadFile(%q) error: %v", path, err) + } + ast, err := build.ParseBzl(path, f) + if err != nil { + return nil, nil, fmt.Errorf("build.Parse(%q) error: %v", f, err) + } + + var loads []*build.LoadStmt + build.WalkOnce(ast, func(expr *build.Expr) { + n := *expr + if l, ok := n.(*build.LoadStmt); ok { + if lbl, err := label.Parse(l.Module.Value); err == nil { + l.Module.Value = lbl.Abs("", rel).String() + } else { + logf("rel=%q: load parse error: %s %v", rel, l.Module.Value, err) + } + loads = append(loads, l) + } + }) + + return ast, loads, nil +} + +// stringListDict creates a Dict expression with string keys and List values +// containing StringExpr elements. Used for representing load statements by file. +func stringListDict(data map[string][]string) *build.DictExpr { + if len(data) == 0 { + return &build.DictExpr{} + } + + // Sort keys for deterministic output + keys := slices.Sorted(maps.Keys(data)) + + var keyValues []*build.KeyValueExpr + for _, key := range keys { + values := data[key] + + // Create list of string expressions for the values + var listExprs []build.Expr + for _, value := range values { + listExprs = append(listExprs, &build.StringExpr{Value: value}) + } + + keyValues = append(keyValues, &build.KeyValueExpr{ + Key: &build.StringExpr{Value: key}, + Value: &build.ListExpr{List: listExprs}, + }) + } + + return &build.DictExpr{ + List: keyValues, + } +} diff --git a/language/starlarkrepository/language_test.go b/language/starlarkrepository/language_test.go new file mode 100644 index 00000000..3c0c6123 --- /dev/null +++ b/language/starlarkrepository/language_test.go @@ -0,0 +1,237 @@ +package starlarkrepository + +import ( + "os" + "path/filepath" + "testing" + + "github.com/bazelbuild/buildtools/build" +) + +// readFileLines tests + +func TestReadLines(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "filters empty lines and comments", + input: `.build/ +# This is a comment +.swiftpm/ +.vscode/ + +`, + expected: []string{".build/", ".swiftpm/", ".vscode/"}, + }, + { + name: "handles only comments", + input: `# Comment 1 +# Comment 2 +# Comment 3 +`, + expected: []string{}, + }, + { + name: "handles mixed content", + input: ` .build/ +# Comment + +.swiftpm/ + # Another comment +.vscode/ +`, + expected: []string{".build/", ".swiftpm/", ".vscode/"}, + }, + { + name: "empty file", + input: "", + expected: []string{}, + }, + { + name: "only whitespace", + input: ` + + +`, + expected: []string{}, + }, + { + name: "trims whitespace", + input: ` .build/ + .swiftpm/ + .vscode/ +`, + expected: []string{".build/", ".swiftpm/", ".vscode/"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temp directory + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.txt") + + // Write input content + if err := os.WriteFile(testFile, []byte(tt.input), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + // Create a no-op log function for tests + noop := func(format string, args ...any) {} + + // Run the function + lines, err := readFileLines(testFile, noop) + if err != nil { + t.Fatalf("readLines failed: %v", err) + } + + // Handle nil vs empty slice + if tt.expected == nil || len(tt.expected) == 0 { + if len(lines) != 0 { + t.Errorf("Expected empty result, got %d lines: %v", len(lines), lines) + } + return + } + + // Compare results + if len(lines) != len(tt.expected) { + t.Errorf("Length mismatch: got %d lines, want %d lines", len(lines), len(tt.expected)) + t.Errorf("got: %v", lines) + t.Errorf("want: %v", tt.expected) + return + } + + for i, line := range lines { + if line != tt.expected[i] { + t.Errorf("Line %d mismatch:\ngot: %q\nwant: %q", i, line, tt.expected[i]) + } + } + }) + } +} + +func TestReadLines_NonExistent(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "nonexistent.txt") + + noop := func(format string, args ...any) {} + + // Should return error for non-existent file + lines, err := readFileLines(testFile, noop) + if err == nil { + t.Errorf("expected error for non-existent file, got nil") + } + if lines != nil { + t.Errorf("expected nil slice for non-existent file, got: %v", lines) + } +} + +// stringListDict tests + +func TestStringListDict(t *testing.T) { + tests := []struct { + name string + input map[string][]string + }{ + { + name: "empty map", + input: map[string][]string{}, + }, + { + name: "single key with single value", + input: map[string][]string{ + "foo.bzl": {"@repo//pkg:file.bzl"}, + }, + }, + { + name: "single key with multiple values", + input: map[string][]string{ + "foo.bzl": { + "@repo1//pkg:file1.bzl", + "@repo2//pkg:file2.bzl", + }, + }, + }, + { + name: "multiple keys sorted alphabetically", + input: map[string][]string{ + "zebra.bzl": {"@repo//z:z.bzl"}, + "alpha.bzl": {"@repo//a:a.bzl"}, + "beta.bzl": {"@repo//b:b.bzl"}, + }, + }, + { + name: "multiple keys with multiple values", + input: map[string][]string{ + "rules.bzl": { + "@rules_proto//proto:defs.bzl", + "@bazel_skylib//lib:paths.bzl", + }, + "providers.bzl": { + "@rules_proto//proto:providers.bzl", + }, + }, + }, + { + name: "empty values list", + input: map[string][]string{ + "empty.bzl": {}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := stringListDict(tt.input) + + // Verify the structure + if result == nil { + t.Fatal("result should not be nil") + } + + if len(tt.input) == 0 && len(result.List) != 0 { + t.Errorf("empty input should produce empty dict, got %d entries", len(result.List)) + } + + if len(tt.input) != 0 && len(result.List) != len(tt.input) { + t.Errorf("expected %d dict entries, got %d", len(tt.input), len(result.List)) + } + + // Verify each key-value pair + for _, kv := range result.List { + keyExpr, ok := kv.Key.(*build.StringExpr) + if !ok { + t.Errorf("key should be StringExpr, got %T", kv.Key) + continue + } + + listExpr, ok := kv.Value.(*build.ListExpr) + if !ok { + t.Errorf("value should be ListExpr, got %T", kv.Value) + continue + } + + // Verify all list elements are StringExpr + for i, elem := range listExpr.List { + if _, ok := elem.(*build.StringExpr); !ok { + t.Errorf("list element %d should be StringExpr, got %T", i, elem) + } + } + + // Verify the values match + expectedValues, exists := tt.input[keyExpr.Value] + if !exists { + t.Errorf("unexpected key %q in result", keyExpr.Value) + continue + } + + if len(listExpr.List) != len(expectedValues) { + t.Errorf("key %q: expected %d values, got %d", keyExpr.Value, len(expectedValues), len(listExpr.List)) + } + } + }) + } +} diff --git a/rules/private/proto_repository_tools_srcs.bzl b/rules/private/proto_repository_tools_srcs.bzl index 74675fce..af796bcc 100644 --- a/rules/private/proto_repository_tools_srcs.bzl +++ b/rules/private/proto_repository_tools_srcs.bzl @@ -43,6 +43,8 @@ PROTO_REPOSITORY_TOOLS_SRCS = [ "@build_stack_rules_proto//language/proto_go_modules:rule.go", "@build_stack_rules_proto//language/protobuf:BUILD.bazel", "@build_stack_rules_proto//language/protobuf:protobuf.go", + "@build_stack_rules_proto//language/starlarkrepository:BUILD.bazel", + "@build_stack_rules_proto//language/starlarkrepository:language.go", "@build_stack_rules_proto//pkg:BUILD.bazel", "@build_stack_rules_proto//pkg/goldentest:BUILD.bazel", "@build_stack_rules_proto//pkg/goldentest:cases.go", diff --git a/rules/proto/proto_repository.bzl b/rules/proto/proto_repository.bzl index 8f87f99e..071649c7 100644 --- a/rules/proto/proto_repository.bzl +++ b/rules/proto/proto_repository.bzl @@ -84,16 +84,25 @@ def _proto_repository_impl(ctx): reproducible = False if ctx.attr.local_path: + # Resolve relative paths against the main workspace root. Bare strings + # passed to watch_tree() and the --path arg of fetch_repo are otherwise + # interpreted relative to the external repo's own directory, which is + # never what the caller wants. + local_path = ctx.attr.local_path + if not local_path.startswith("/"): + workspace_root = str(ctx.path(Label("@@//:MODULE.bazel")).dirname) + local_path = workspace_root + "/" + local_path + if hasattr(ctx, "watch_tree"): # https://github.com/bazelbuild/bazel/commit/fffa0affebbacf1961a97ef7cd248be64487d480 - ctx.watch_tree(ctx.attr.local_path) + ctx.watch_tree(local_path) else: # buildifier: disable=print print(""" WARNING: go.mod replace directives to module paths is only supported in bazel 7.1.0-rc1 or later, - Because of this changes to %s will not be detected by your version of Bazel.""" % ctx.attr.local_path) + Because of this changes to %s will not be detected by your version of Bazel.""" % local_path) - fetch_repo_args = ["--path", ctx.attr.local_path, "--dest", ctx.path("")] + fetch_repo_args = ["--path", local_path, "--dest", ctx.path("")] elif ctx.attr.urls: # HTTP mode for key in ("commit", "tag", "vcs", "remote", "version", "sum", "replace"): @@ -461,6 +470,8 @@ package_info( ) def _generate_proto_repository_info(ctx): + if not ctx.attr.imports_out: + return "" return """ exports_files(["{imports_out}"]) """.format( diff --git a/rules/proto/starlark_repository.bzl b/rules/proto/starlark_repository.bzl new file mode 100644 index 00000000..604b80e6 --- /dev/null +++ b/rules/proto/starlark_repository.bzl @@ -0,0 +1,31 @@ +"""proto_repository.bzl provides the proto_repository rule.""" + +# Copyright 2014 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. + +load("@build_stack_rules_proto//rules/proto:proto_repository.bzl", "protobuf_go_repository", _proto_repository_attrs = "proto_repository_attrs") + +# TODO: narrow the set of available attrs +starlark_repository_attrs = _proto_repository_attrs + +def starlark_repository(**kwargs): + """starlark_repository wraps proto_repository and sets the language to starlark_bundle + + Args: + **kwargs: the kwargs dict passed to protobuf_go_repository + """ + name = kwargs.get("name") + kwargs.setdefault("apparent_name", name) + + protobuf_go_repository(**kwargs) diff --git a/rules/starlark_module.bzl b/rules/starlark_module.bzl new file mode 100644 index 00000000..60c85768 --- /dev/null +++ b/rules/starlark_module.bzl @@ -0,0 +1,39 @@ +"""starlark_module.bzl is similar to bzl_library but also provides load statement foreach file.""" + +load("@bazel_skylib//:bzl_library.bzl", "StarlarkLibraryInfo") + +StarlarkModuleInfo = provider( + "Information about a single .bzl file.", + fields = { + "label": "Label: The label of the target rule", + "loads": "List[str]: load statements for this file", + "src": "File: The .bzl file", + }, +) + +def _starlark_module_impl(ctx): + return [ + StarlarkLibraryInfo( + srcs = [ctx.file.src], + transitive_srcs = depset([ctx.file.src]), + ), + StarlarkModuleInfo( + label = ctx.label, + loads = ctx.attr.loads, + src = ctx.file.src, + ), + ] + +starlark_module = rule( + implementation = _starlark_module_impl, + attrs = { + "loads": attr.string_list( + doc = "the load statements in this file", + ), + "src": attr.label( + doc = "the .bzl source file", + allow_single_file = True, + ), + }, + provides = [StarlarkLibraryInfo, StarlarkModuleInfo], +) diff --git a/rules/starlark_module_library.bzl b/rules/starlark_module_library.bzl new file mode 100644 index 00000000..2dfa53e3 --- /dev/null +++ b/rules/starlark_module_library.bzl @@ -0,0 +1,43 @@ +"""starlark_module_library.bzl is similar to bzl_library but also provides load statement foreach file.""" + +load("//rules:starlark_module.bzl", "StarlarkModuleInfo") + +StarlarkModuleLibraryInfo = provider( + "Information on a set of starlark modules. This is a flat list, non-transitive.", + fields = { + "label": "The label of the target rule", + "modules": "List[StarlarkModuleInfo]: modules deps of this rule", + "srcs": "List[File]: source files for the modules, for convenience", + "bazelignore": "List[str] value of ctx.attr.bazelignore", + "bazelversion": "str: the value of ctx.attr.bazelversion", + }, +) + +def _starlark_module_library_impl(ctx): + modules = [m[StarlarkModuleInfo] for m in ctx.attr.modules] + return [ + StarlarkModuleLibraryInfo( + label = ctx.label, + bazelignore = ctx.attr.bazelignore, + bazelversion = ctx.attr.bazelversion, + modules = modules, + srcs = [m.src for m in modules], + ), + ] + +starlark_module_library = rule( + implementation = _starlark_module_library_impl, + attrs = { + "bazelignore": attr.string_list( + doc = "contents of the .bazelignore file, if present", + ), + "bazelversion": attr.string( + doc = "contents of the .bazelversion file, if present", + ), + "modules": attr.label_list( + doc = "list of starlark_module rule dependencies.", + providers = [StarlarkModuleInfo], + ), + }, + provides = [StarlarkModuleLibraryInfo], +) diff --git a/vendor/modules.txt b/vendor/modules.txt index e9dcf605..7dd708fd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -40,10 +40,6 @@ github.com/emicklei/proto # github.com/gogo/protobuf v1.3.2 ## explicit; go 1.15 github.com/gogo/protobuf/proto -# github.com/golang/mock v1.7.0-rc.1 -## explicit; go 1.15 -# github.com/golang/protobuf v1.5.4 -## explicit; go 1.17 # github.com/google/go-cmp v0.7.0 ## explicit; go 1.21 github.com/google/go-cmp/cmp @@ -96,13 +92,9 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm -# golang.org/x/tools v0.35.0 -## explicit; go 1.23.0 # golang.org/x/tools/go/vcs v0.1.0-deprecated ## explicit; go 1.19 golang.org/x/tools/go/vcs -# google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f -## explicit; go 1.22 # google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 ## explicit; go 1.23.0 google.golang.org/genproto/googleapis/rpc/status