diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 00000000..d7b07bb7 --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +index/vendor/integration_tests/testcases \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 104510ab..d30c8397 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ concurrency: jobs: build: runs-on: ubuntu-24.04 - timeout-minutes: 5 + timeout-minutes: 15 steps: - uses: actions/checkout@v4 diff --git a/MODULE.bazel b/MODULE.bazel index 83ccfb03..39f35f26 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -3,8 +3,16 @@ module( ) bazel_dep(name = "rules_go", version = "0.53.0") +bazel_dep(name = "rules_proto", version = "7.1.0") bazel_dep(name = "gazelle", version = "0.42.0") bazel_dep(name = "bazel_skylib", version = "1.7.1") go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") go_sdk.download(version = "1.24.0") + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") +use_repo( + go_deps, + "org_golang_google_protobuf", +) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index eda787b6..27d97fad 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -61,7 +61,8 @@ "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", - "https://bcr.bazel.build/modules/protobuf/29.0/source.json": "b857f93c796750eef95f0d61ee378f3420d00ee1dd38627b27193aa482f4f981", + "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://bcr.bazel.build/modules/protobuf/29.1/source.json": "04cca85dce26b895ed037d98336d860367fe09919208f2ad383f0df1aff63199", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", @@ -126,7 +127,8 @@ "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", - "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1", + "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", + "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", diff --git a/README.md b/README.md index 10811001..0a6e5f33 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,14 @@ Controls how to handle cyclic dependencies between translation units: - `merge`: All groups forming a cycle will be merged into a single one **(default)** - `warn`: Don't modify rules forming a cycle, let user handle it manually +### `# gazelle:cc_indexfile ` + +Adds index file, containing mapping between headers to rules providing their definitions and/or implementation. +Index allow for automatic dependency resolution for targets not managed by the gazelle, eg. provided by integrated package manager or vendored inside repository. +Multiple `cc_indexfile` are allowed. It's recommended to use it only in top-level BUILD file. +Argument of the directive needs to be a repository-root relative or absolute path. +Visit [external dependenices section](#external-dependencies) to learn how to obtain indexes. + ## Rules for target rule selection The extension automatically selects the appropriate rule type based on the following criteria: @@ -142,6 +150,27 @@ bazel_dep(name = "fmt", version = "11.1.4", repo_name = "fmt_repo") #include "boost/chrono.hpp" // Warning: defined in @boost.chrono//:boost.chrono but not added as bazel_dep ``` +#### Vendored external dependencies + +External dependenices vendored as part of Bazel repository typically might not be managed by Gazelle. To allow for dependency resolution based on these sources it is required to create a index using `@gazelle_cc//index/vendor` binary. + +```bash +bazel run @gazelle_cc//index/rules_foreign_cc -- --output=vendored.ccindex $PWD +``` + +The resulting index needs to be added to Gazelle directive in top-level `BUILD` file. + +```bazel +# gazelle cc_indexfile vendored.ccindex +``` + +Additional options for `@gazelle_cc//index/rules_foreign_cc`: + +| Flag | Default | Definition | +| ---- | ------- | ---------- | +| --select=\ | //third_party/..., //external/..., //vendored/... | Provides a selector for rules that should be indexed. Multiple --select flags are allowed | +| --output=\ | ./vendor.ccidx | Output file for created index | +| --verbose | false | Enable verbose logging and debug information | #### Other package managers diff --git a/go.mod b/go.mod index 1ac55bd4..42d04a35 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/EngFlow/gazelle_cc go 1.23.2 -require github.com/bazelbuild/bazel-gazelle v0.42.0 +require ( + github.com/bazelbuild/bazel-gazelle v0.42.0 + google.golang.org/protobuf v1.36.6 +) require ( github.com/bazelbuild/buildtools v0.0.0-20240918101019-be1c24cc9a44 // indirect diff --git a/go.sum b/go.sum index a6aeceeb..f019b6bf 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,17 @@ github.com/bazelbuild/buildtools v0.0.0-20240918101019-be1c24cc9a44 h1:FGzENZi+S github.com/bazelbuild/buildtools v0.0.0-20240918101019-be1c24cc9a44/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= github.com/bazelbuild/rules_go v0.50.1 h1:/BUvuaB8MEiUA2oLPPCGtuw5V+doAYyiGTFyoSWlkrw= github.com/bazelbuild/rules_go v0.50.1/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/index/BUILD.bazel b/index/BUILD.bazel new file mode 100644 index 00000000..9f4d86a2 --- /dev/null +++ b/index/BUILD.bazel @@ -0,0 +1,7 @@ +test_suite( + name = "integration_tests", + tags = ["manual"], + tests = [ + "//index/vendor/integration_tests:integration_tests_test", + ], +) diff --git a/index/internal/bazel/BUILD.bazel b/index/internal/bazel/BUILD.bazel new file mode 100644 index 00000000..920f4fd5 --- /dev/null +++ b/index/internal/bazel/BUILD.bazel @@ -0,0 +1,30 @@ +load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto//proto:defs.bzl", "proto_library") + +# gazelle:proto package + +proto_library( + name = "bazel_proto", + srcs = ["build.proto"], + visibility = ["//index:__subpackages__"], +) + +go_proto_library( + name = "bazel_go_proto", + importpath = "github.com/EngFlow/gazelle_cc/index/internal/bazel", + proto = ":bazel_proto", + visibility = ["//index:__subpackages__"], +) + +go_library( + name = "bazel", + srcs = ["query.go"], + embed = [":bazel_go_proto"], + importpath = "github.com/EngFlow/gazelle_cc/index/internal/bazel", + visibility = ["//index:__subpackages__"], + deps = [ + "//index/internal/collections", + "@org_golang_google_protobuf//proto", + ], +) diff --git a/index/internal/bazel/build.proto b/index/internal/bazel/build.proto new file mode 100644 index 00000000..d148f37e --- /dev/null +++ b/index/internal/bazel/build.proto @@ -0,0 +1,569 @@ +// 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. +// +// This file contains the protocol buffer representation of a build +// file or 'blaze query --output=proto' call. + +syntax = "proto2"; + +package bazel; + +message License { + repeated string license_type = 1; + repeated string exception = 2; +} + +message StringDictEntry { + required string key = 1; + required string value = 2; +} + +message LabelDictUnaryEntry { + required string key = 1; + required string value = 2; +} + +message LabelListDictEntry { + required string key = 1; + repeated string value = 2; +} + +message LabelKeyedStringDictEntry { + required string key = 1; + required string value = 2; +} + +message StringListDictEntry { + required string key = 1; + repeated string value = 2; +} + +// Represents an entry attribute of a Fileset rule in a build file. +message FilesetEntry { + // Indicates what to do when a source file is actually a symlink. + enum SymlinkBehavior { + COPY = 1; + DEREFERENCE = 2; + } + + // The label pointing to the source target where files are copied from. + required string source = 1; + + // The relative path within the fileset rule where files will be mapped. + required string destination_directory = 2; + + // Whether the files= attribute was specified. This is necessary because + // no files= attribute and files=[] mean different things. + optional bool files_present = 7; + + // A list of file labels to include from the source directory. + repeated string file = 3; + + // If this is a fileset entry representing files within the rule + // package, this lists relative paths to files that should be excluded from + // the set. This cannot contain values if 'file' also has values. + repeated string exclude = 4; + + // This field is optional because there will be some time when the new + // PB is used by tools depending on blaze query, but the new blaze version + // is not yet released. + // TODO(bazel-team): Make this field required once a version of Blaze is + // released that outputs this field. + optional SymlinkBehavior symlink_behavior = 5 [default = COPY]; + + // The prefix to strip from the path of the files in this FilesetEntry. Note + // that no value and the empty string as the value mean different things here. + optional string strip_prefix = 6; +} + +// A rule attribute. Each attribute must have a type and one of the various +// value fields populated - for the most part. +// +// Attributes of BOOLEAN and TRISTATE type may set all of the int, bool, and +// string values for backwards compatibility with clients that expect them to +// be set. +// +// Attributes of INTEGER, STRING, LABEL, LICENSE, BOOLEAN, and TRISTATE type +// may set *none* of the values. This can happen if the Attribute message is +// prepared for a client that doesn't support SELECTOR_LIST, but the rule has +// a selector list value for the attribute. (Selector lists for attributes of +// other types--the collection types--are handled differently when prepared +// for such a client. The possible collection values are gathered together +// and flattened.) +// +// By checking the type, the appropriate value can be extracted - see the +// comments on each type for the associated value. The order of lists comes +// from the blaze parsing. If an attribute is of a list type, the associated +// list should never be empty. +message Attribute { + reserved 12, 16; + + // Indicates the type of attribute. + enum Discriminator { + INTEGER = 1; // int_value + STRING = 2; // string_value + LABEL = 3; // string_value + OUTPUT = 4; // string_value + STRING_LIST = 5; // string_list_value + LABEL_LIST = 6; // string_list_value + OUTPUT_LIST = 7; // string_list_value + DISTRIBUTION_SET = 8; // string_list_value - order is unimportant + LICENSE = 9; // license + STRING_DICT = 10; // string_dict_value + FILESET_ENTRY_LIST = 11; // fileset_list_value + LABEL_LIST_DICT = 12; // label_list_dict_value + STRING_LIST_DICT = 13; // string_list_dict_value + BOOLEAN = 14; // int, bool and string value + TRISTATE = 15; // tristate, int and string value + INTEGER_LIST = 16; // int_list_value + UNKNOWN = 18; // unknown type, use only for build extensions + LABEL_DICT_UNARY = 19; // label_dict_unary_value + SELECTOR_LIST = 20; // selector_list + LABEL_KEYED_STRING_DICT = 21; // label_keyed_string_dict + + DEPRECATED_STRING_DICT_UNARY = 17; + } + + // Values for the TriState field type. + enum Tristate { + NO = 0; + YES = 1; + AUTO = 2; + } + + message SelectorEntry { + reserved 12; + + // The key of the selector entry. At this time, this is the label of a + // config_setting rule, or the pseudo-label "//conditions:default". + optional string label = 1; + + // True if the entry's value is the default value for the type as a + // result of the condition value being specified as None (ie: + // {"//condition": None}). + optional bool is_default_value = 16; + + // Exactly one of the following fields (except for glob_criteria) must be + // populated - note that the BOOLEAN and TRISTATE caveat in Attribute's + // comment does not apply here. The type field in the SelectorList + // containing this entry indicates which of these fields is populated, + // in accordance with the comments on Discriminator enum values above. + // (To be explicit: BOOLEAN populates the boolean_value field and TRISTATE + // populates the tristate_value field.) + optional int32 int_value = 2; + optional string string_value = 3; + optional bool boolean_value = 4; + optional Tristate tristate_value = 5; + repeated string string_list_value = 6; + optional License license = 7; + repeated StringDictEntry string_dict_value = 8; + repeated FilesetEntry fileset_list_value = 9; + repeated LabelListDictEntry label_list_dict_value = 10; + repeated StringListDictEntry string_list_dict_value = 11; + repeated int32 int_list_value = 13; + repeated LabelDictUnaryEntry label_dict_unary_value = 15; + repeated LabelKeyedStringDictEntry label_keyed_string_dict_value = 17; + + repeated bytes DEPRECATED_string_dict_unary_value = 14; + } + + message Selector { + // The list of (label, value) pairs in the map that defines the selector. + // At this time, this cannot be empty, i.e. a selector has at least one + // entry. + repeated SelectorEntry entries = 1; + + // Whether or not this has any default values. + optional bool has_default_value = 2; + + // The error message when no condition matches. + optional string no_match_error = 3; + } + + message SelectorList { + // The type that this selector list evaluates to, and the type that each + // selector in the list evaluates to. At this time, this cannot be + // SELECTOR_LIST, i.e. selector lists do not nest. + optional Discriminator type = 1; + + // The list of selector elements in this selector list. At this time, this + // cannot be empty, i.e. a selector list is never empty. + repeated Selector elements = 2; + } + + // The name of the attribute + required string name = 1; + + // Whether the attribute was explicitly specified + optional bool explicitly_specified = 13; + + // If this attribute has a string value or a string list value, then this + // may be set to indicate that the value may be treated as a label that + // isn't a dependency of this attribute's rule. + optional bool nodep = 20; + + // Represents the aspect that this attribute comes from. It is set to an + // empty string if it does not come from an aspect. + optional string source_aspect_name = 23; + + // The type of attribute. This message is used for all of the different + // attribute types so the discriminator helps for figuring out what is + // stored in the message. + required Discriminator type = 2; + + // If this attribute has an integer value this will be populated. + // Boolean and TriState also use this field as [0,1] and [-1,0,1] + // for [false, true] and [auto, no, yes] respectively. + optional int32 int_value = 3; + + // If the attribute has a string value this will be populated. Label and + // path attributes use this field as the value even though the type may + // be LABEL or something else other than STRING. + optional string string_value = 5; + + // If the attribute has a boolean value this will be populated. + optional bool boolean_value = 14; + + // If the attribute is a Tristate value, this will be populated. + optional Tristate tristate_value = 15; + + // The value of the attribute has a list of string values (label and path + // note from STRING applies here as well). + repeated string string_list_value = 6; + + // If this is a license attribute, the license information is stored here. + optional License license = 7; + + // If this is a string dict, each entry will be stored here. + repeated StringDictEntry string_dict_value = 8; + + // If the attribute is part of a Fileset, the fileset entries are stored in + // this field. + repeated FilesetEntry fileset_list_value = 9; + + // If this is a label list dict, each entry will be stored here. + repeated LabelListDictEntry label_list_dict_value = 10; + + // If this is a string list dict, each entry will be stored here. + repeated StringListDictEntry string_list_dict_value = 11; + + // The value of the attribute has a list of int32 values + repeated int32 int_list_value = 17; + + // If this is a label dict unary, each entry will be stored here. + repeated LabelDictUnaryEntry label_dict_unary_value = 19; + + // If this is a label-keyed string dict, each entry will be stored here. + repeated LabelKeyedStringDictEntry label_keyed_string_dict_value = 22; + + // If this attribute's value is an expression containing one or more select + // expressions, then its type is SELECTOR_LIST and a SelectorList will be + // stored here. + optional SelectorList selector_list = 21; + + repeated bytes DEPRECATED_string_dict_unary_value = 18; +} + +// A rule instance (e.g., cc_library foo, java_binary bar). +message Rule { + reserved 8, 11; + + // The name of the rule (formatted as an absolute label, e.g. //foo/bar:baz). + required string name = 1; + + // The rule class name (e.g., java_library). + // + // Note that the rule class name may not uniquely identify a rule class, since + // two different .bzl files may define different rule classes with the same + // name. To uniquely identify the rule class, see rule_class_key field below. + required string rule_class = 2; + + // The BUILD file and line number of the location (formatted as + // ::) in the rule's package's + // BUILD file where the rule instance was instantiated. The line number will + // be that of a rule invocation or macro call (that in turn invoked a + // rule). See + // https://bazel.build/rules/macros#macro-creation + optional string location = 3; + + // All of the attributes that describe the rule. + repeated Attribute attribute = 4; + + // All of the inputs to the rule (formatted as absolute labels). These are + // predecessors in the dependency graph. + repeated string rule_input = 5; + + repeated ConfiguredRuleInput configured_rule_input = 15; + + // All of the outputs of the rule (formatted as absolute labels). These are + // successors in the dependency graph. + repeated string rule_output = 6; + + // The set of all "features" inherited from the rule's package declaration. + repeated string default_setting = 7; + + // The rule's class's public by default value. + optional bool DEPRECATED_public_by_default = 9; + + optional bool DEPRECATED_is_skylark = 10; + + // Hash encapsulating the behavior of this Starlark rule. Any change to this + // rule's definition that could change its behavior will be reflected here. + optional string skylark_environment_hash_code = 12; + + // The Starlark call stack at the moment the rule was instantiated. + // Each entry has the form "file:line:col: function". + // The outermost stack frame ("", the BUILD file) appears first; + // the frame for the rule function itself is omitted. + // The file name may be relative to package's source root directory. + // + // Requires --proto:instantiation_stack=true. + repeated string instantiation_stack = 13; + + // The Starlark call stack for the definition of the rule class of this + // particular rule instance. If empty, either populating the field was not + // enabled on the command line with the --proto:definition_stack flag or the + // rule is a native one. + repeated string definition_stack = 14; + + // A key uniquely identifying the rule's rule class. Stable between repeated + // blaze query invocations (assuming that there are no changes to Starlark + // files and the same blaze binary is invoked with the same options). + // + // Requires --proto:rule_classes=true + optional string rule_class_key = 16; + + // Stardoc-format rule class API definition for this rule. Includes both + // Starlark-defined and native (including inherited) attributes; does not + // include hidden or explicitly undocumented attributes. + // + // Populated only for the first rule in the stream with a given + // rule_class_key. + // + // Requires --proto:rule_classes=true + // optional RuleInfo rule_class_info = 17; +} + +// Direct dependencies of a rule in form. +message ConfiguredRuleInput { + // Dep's target label. + optional string label = 1; + // Dep's configuration if the dep isn't a source file, else unset. + optional string configuration_checksum = 2; + // Reference to this dep's configuration if --proto:include_configurations is + // set: see + // https://github.com/bazelbuild/bazel/blob/7278be3f9b0c26842ecb8225f0215c1e4aede5a9/src/main/protobuf/analysis_v2.proto#L189. + // If this dep is a source file, this is unset. + optional uint32 configuration_id = 3; +} + +// Summary of all transitive dependencies of 'rule,' where each dependent +// rule is included only once in the 'dependency' field. Gives complete +// information to analyze the single build target labeled rule.name, +// including optional location of target in BUILD file. +message RuleSummary { + required Rule rule = 1; + repeated Rule dependency = 2; + optional string location = 3; +} + +// A package group. Aside from the name, it contains the list of packages +// present in the group (as specified in the BUILD file). +message PackageGroup { + reserved 4; + + // The name of the package group + required string name = 1; + + // The list of packages as specified in the BUILD file. Currently this is + // only a list of packages, but some time in the future, there might be + // some type of wildcard mechanism. + repeated string contained_package = 2; + + // The list of sub package groups included in this one. + repeated string included_package_group = 3; +} + +// An environment group. +message EnvironmentGroup { + // The name of the environment group. + required string name = 1; + + // The environments that belong to this group (as labels). + repeated string environment = 2; + + // The member environments that rules implicitly support if not otherwise + // specified. + repeated string default = 3; +} + +// A file that is an input into the build system. +// Next-Id: 10 +message SourceFile { + reserved 7; + + // The name of the source file (a label). + required string name = 1; + + // The location of the source file. This is a path with a line number and a + // column number not a label in the build system. + optional string location = 2; + + // Labels of .bzl (Starlark) files that are transitively loaded in this BUILD + // file. This is present only when the SourceFile represents a BUILD file that + // loaded .bzl files. + // TODO(bazel-team): Rename this field. + repeated string subinclude = 3; + + // Labels of package groups that are mentioned in the visibility declaration + // for this source file. + repeated string package_group = 4; + + // Labels mentioned in the visibility declaration (including :__pkg__ and + // //visibility: ones) + repeated string visibility_label = 5; + + // The package-level features enabled for this package. Only present if the + // SourceFile represents a BUILD file. + repeated string feature = 6; + + // License attribute for the file. + optional License license = 8; + + // True if the package contains an error. Only present if the SourceFile + // represents a BUILD file. + optional bool package_contains_errors = 9; +} + +// A file that is the output of a build rule. +message GeneratedFile { + // The name of the generated file (a label). + required string name = 1; + + // The label of the target that generates the file. + required string generating_rule = 2; + + // The path, line number, and column number of the output file (not a label). + optional string location = 3; +} + +// A target from a blaze query execution. Similar to the Attribute message, +// the Discriminator is used to determine which field contains information. +// For any given type, only one of these can be populated in a single Target. +message Target { + enum Discriminator { + RULE = 1; + SOURCE_FILE = 2; + GENERATED_FILE = 3; + PACKAGE_GROUP = 4; + ENVIRONMENT_GROUP = 5; + } + + // The type of target contained in the message. + required Discriminator type = 1; + + // If this target represents a rule, the rule is stored here. + optional Rule rule = 2; + + // A file that is not generated by the build system (version controlled + // or created by the test harness). + optional SourceFile source_file = 3; + + // A generated file that is the output of a rule. + optional GeneratedFile generated_file = 4; + + // A package group. + optional PackageGroup package_group = 5; + + // An environment group. + optional EnvironmentGroup environment_group = 6; +} + +// Container for all of the blaze query results. +message QueryResult { + // All of the targets returned by the blaze query. + repeated Target target = 1; +} + +//////////////////////////////////////////////////////////////////////////// +// Messages dealing with querying the BUILD language itself. For now, this is +// quite simplistic: Blaze can only tell the names of the rule classes, their +// attributes with their type. + +// Information about allowed rule classes for a specific attribute of a rule. +message AllowedRuleClassInfo { + enum AllowedRuleClasses { + ANY = 1; // Any rule is allowed to be in this attribute + SPECIFIED = 2; // Only the explicitly listed rules are allowed + } + + required AllowedRuleClasses policy = 1; + + // Rule class names of rules allowed in this attribute, e.g "cc_library", + // "py_binary". Only present if the allowed_rule_classes field is set to + // SPECIFIED. + repeated string allowed_rule_class = 2; +} + +// This message represents a single attribute of a single rule. +// See https://bazel.build/rules/lib/attr. +message AttributeDefinition { + required string name = 1; // e.g. "name", "srcs" + required Attribute.Discriminator type = 2; + optional bool mandatory = 3; + optional AllowedRuleClassInfo allowed_rule_classes = 4; // type=label* + optional string documentation = 5; + optional bool allow_empty = 6; // type=*_list|*_dict + optional bool allow_single_file = 7; // type=label + optional AttributeValue default = + 9; // simple (not computed/late-bound) values only + optional bool executable = 10; // type=label + optional bool configurable = 11; + optional bool nodep = + 12; // label-valued edge does not establish a dependency + optional bool cfg_is_host = + 13; // edge entails a transition to "host" configuration +} + +// An AttributeValue represents the value of an attribute. +// A single field, determined by the attribute type, is populated. +// +// It is used only for AttributeDefinition.default. Attribute and +// SelectorEntry do their own thing for unfortunate historical reasons. +message AttributeValue { + optional int32 int = 1; // type=int|tristate + optional string string = 2; // type=string|label|output + optional bool bool = 3; // type=bool + repeated AttributeValue list = 4; // type=*_list|distrib + repeated DictEntry dict = 5; // type=*_dict + + message DictEntry { + required string key = 1; + required AttributeValue value = 2; + } +} + +message RuleDefinition { + required string name = 1; + // Only contains documented attributes + repeated AttributeDefinition attribute = 2; + optional string documentation = 3; + // Only for build extensions: label to file that defines the extension + optional string label = 4; +} + +message BuildLanguage { + // Only contains documented rule definitions + repeated RuleDefinition rule = 1; +} \ No newline at end of file diff --git a/index/internal/bazel/query.go b/index/internal/bazel/query.go new file mode 100644 index 00000000..e13ca570 --- /dev/null +++ b/index/internal/bazel/query.go @@ -0,0 +1,55 @@ +// Copyright 2025 EngFlow Inc. 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 bazel + +import ( + "bytes" + "log" + "os" + "os/exec" + + "github.com/EngFlow/gazelle_cc/index/internal/collections" + "google.golang.org/protobuf/proto" +) + +// Execute given bazel query inside directory. Returns nil if query fails +func Query(cwd string, query string) *QueryResult { + var bufStdout bytes.Buffer + var bufStderr bytes.Buffer + cmd := exec.Command("bazel", "query", query, "--output=proto", "--incompatible_disallow_empty_glob=false") + cmd.Dir = cwd + cmd.Stdout = &bufStdout + cmd.Stderr = os.Stderr // &bufStderr + if err := cmd.Run(); err != nil { + log.Printf("Bazel query failed for %s: %v. Stderr: %v", cmd.Args, err, bufStderr.String()) + return nil + } + var result QueryResult + if err := proto.Unmarshal(bufStdout.Bytes(), &result); err != nil { + log.Fatalf("Failed to unmarshal query result: %v", err) + } + return &result +} + +// Select attribute that defined with given name. Returns nil if no such attribute can be found +func (target *Target) GetNamedAttribute(name string) *Attribute { + found := collections.Find(target.GetRule().GetAttribute(), func(attr *Attribute) bool { + return attr.GetName() == name + }) + if found != nil { + return *found + } + return nil +} diff --git a/index/internal/collections/BUILD.bazel b/index/internal/collections/BUILD.bazel new file mode 100644 index 00000000..9ceae50a --- /dev/null +++ b/index/internal/collections/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "collections", + srcs = ["collections.go"], + importpath = "github.com/EngFlow/gazelle_cc/index/internal/collections", + visibility = ["//index:__subpackages__"], +) diff --git a/index/internal/collections/collections.go b/index/internal/collections/collections.go new file mode 100644 index 00000000..fe75a8ae --- /dev/null +++ b/index/internal/collections/collections.go @@ -0,0 +1,121 @@ +// Copyright 2025 EngFlow Inc. 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 collections + +import ( + "maps" + "slices" +) + +func Map[T, V any](ts []T, fn func(T) V) []V { + result := make([]V, len(ts)) + for i, t := range ts { + result[i] = fn(t) + } + return result +} + +func FlatMap[T, V any](ts []T, fn func(T) []V) []V { + result := []V{} + for _, t := range ts { + result = slices.AppendSeq(result, slices.Values(fn(t))) + } + return result +} + +func Collect[T, V any](ts []T, fn func(T) (V, error)) []V { + result := []V{} + for _, t := range ts { + transformed, err := fn(t) + if err == nil { + result = append(result, transformed) + } + } + return result +} + +func Find[T any](ts []T, predicate func(T) bool) *T { + for _, t := range ts { + if predicate(t) { + return &t + } + } + return nil +} + +func Filter[T any](ts []T, predicate func(T) bool) []T { + result := make([]T, 0, len(ts)) + for _, elem := range ts { + if predicate(elem) { + result = append(result, elem) + } + } + return result +} + +type Set[T comparable] map[T]bool + +func ToSet[T comparable](slice []T) Set[T] { + set := make(Set[T]) + for _, elem := range slice { + set[elem] = true + } + return set +} + +func (s Set[T]) Diff(other Set[T]) Set[T] { + diff := make(Set[T]) + for elem := range other { + if _, exists := (s)[elem]; !exists { + diff[elem] = true + } + } + return diff +} + +func (s *Set[T]) Add(elem T) *Set[T] { + (*s)[elem] = true + return s +} + +func (s *Set[T]) Join(other Set[T]) *Set[T] { + for elem := range other { + s.Add(elem) + } + return s +} + +func (s Set[T]) Intersect(other Set[T]) Set[T] { + result := make(Set[T]) + for elem := range s { + if (other)[elem] { + result[elem] = true + } + } + return result +} + +func (s Set[T]) Intersects(other Set[T]) bool { + for elem := range s { + if (other)[elem] { + return true + } + } + return false +} + +func (s Set[T]) Values() []T { + return slices.Collect(maps.Keys(s)) +} diff --git a/index/internal/indexer/BUILD.bazel b/index/internal/indexer/BUILD.bazel new file mode 100644 index 00000000..340a7c35 --- /dev/null +++ b/index/internal/indexer/BUILD.bazel @@ -0,0 +1,12 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "indexer", + srcs = ["indexer.go"], + importpath = "github.com/EngFlow/gazelle_cc/index/internal/indexer", + visibility = ["//index:__subpackages__"], + deps = [ + "//index/internal/collections", + "@gazelle//label", + ], +) diff --git a/index/internal/indexer/indexer.go b/index/internal/indexer/indexer.go new file mode 100644 index 00000000..e6c3477d --- /dev/null +++ b/index/internal/indexer/indexer.go @@ -0,0 +1,302 @@ +// Copyright 2025 EngFlow Inc. 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 indexer + +import ( + "encoding/json" + "log" + "maps" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/EngFlow/gazelle_cc/index/internal/collections" + "github.com/bazelbuild/bazel-gazelle/label" +) + +type ( + // Represents information about structure of possibly external dependency + Module struct { + // Name of external repository, or empty if targets are defined in the same Bazel repository + Repository string + // List of targets defined in given module, typically a single cc_library + Targets []*ModuleTarget + } + // Defines information about structure of rule that might be indexed, typically based on cc_library + ModuleTarget struct { + Name label.Label + Hdrs collections.Set[label.Label] // header files (each header is represented as a Label) + Includes collections.Set[string] // list of include paths + StripIncludePrefix string // optional prefix to remove + IncludePrefix string // optional prefix to add + Deps collections.Set[label.Label] // dependencies on other targets + } +) + +type IndexingResult struct { + // Headers mapping to exactly one Bazel rule + HeaderToRule map[string]label.Label + // Headers defined in multiple rules + Ambiguous map[string]collections.Set[label.Label] +} + +// Process list of modules to create an unfiorm index mapping header to exactly one rule that provides their definition. +// In case if multiple modules define same headers might try to select one that behaves as clousers over remaining ambigious rules. +func CreateHeaderIndex(modules []Module) IndexingResult { + // headersMapping will store header paths to a collections.Set of Labels. + headersMapping := map[string]*collections.Set[label.Label]{} + for _, module := range modules { + for _, target := range module.Targets { + // Create a targetLabel for the target using the module repository. + // It's required to correctly map external module to sources found possibly in other rules + targetLabel := label.New(module.Repository, target.Name.Pkg, target.Name.Name) + if shouldExcludeTarget(targetLabel) { + continue + } + + // Normalize headers and add to mapping + for hdr := range target.Hdrs { + normalizedPath := normalizeHeaderPath(hdr.Name, *target) + if shouldExcludeHeader(normalizedPath) { + continue + } + if _, exists := headersMapping[normalizedPath]; !exists { + headersMapping[normalizedPath] = &collections.Set[label.Label]{} + } + headersMapping[normalizedPath].Add(targetLabel) + } + } + } + + // Partition the headers into non-conflicting (exactly one label) and ambiguous (multiple labels). + headerToRule := make(map[string]label.Label) + ambiguous := make(map[string]collections.Set[label.Label]) + for path, labels := range headersMapping { + switch len(*labels) { + case 1: + // Extract the only label in the collections.Set. + for l := range *labels { + headerToRule[path] = l + break + } + default: + ambiguous[path] = *labels + } + } + + return IndexingResult{ + HeaderToRule: headerToRule, + Ambiguous: ambiguous, + } +} + +// Writes the mapping of IndexingResult.HeaderToRule to disk in JSON format. +// Labels are stored as renered strings +func (result IndexingResult) WriteToFile(outputFile string) { + mappings := make(map[string]string, len(result.HeaderToRule)) + for hdr, lbl := range result.HeaderToRule { + mappings[hdr] = lbl.String() + } + + data, err := json.MarshalIndent(mappings, "", " ") + if err != nil { + log.Fatalf("Failed to serialize header index to JSON: %v", err.Error()) + } + + os.MkdirAll(filepath.Dir(outputFile), 0644) + if err := os.WriteFile(outputFile, data, 0644); err != nil { + log.Fatalf("Failed to write index file: %v", err.Error()) + } +} + +// Prints to stdout detailed information about headers with resolved mappings and the ambigious header definitions. +func (result IndexingResult) Show() { + log.Printf("Indexing result:") + log.Printf("Headers with mapping: %v", len(result.HeaderToRule)) + for _, hdr := range slices.Sorted(maps.Keys(result.HeaderToRule)) { + log.Printf("%-80s: %v", hdr, result.HeaderToRule[hdr]) + } + + log.Printf("Ambigious headers: %v", len(result.Ambiguous)) + for _, hdr := range slices.Sorted(maps.Keys(result.Ambiguous)) { + log.Printf("%-80s: %v", hdr, result.Ambiguous[hdr]) + } +} + +// Groups targets into disjoint groups based on the their defined headers. +// Allows to find targets that contain at least 1 common header defined in their definition. +func (module Module) GroupTargetsByHeaders() []collections.Set[*ModuleTarget] { + targets := module.Targets + var groups []collections.Set[*ModuleTarget] + + // Build adjacency list: map each target index to its neighbors + adj := make(map[int][]int) + n := len(targets) + for i := range n { + for j := i + 1; j < n; j++ { + if (targets[i]).Hdrs.Intersects(targets[j].Hdrs) { + adj[i] = append(adj[i], j) + adj[j] = append(adj[j], i) + } + } + } + + // DFS to find connected components + visited := make([]bool, n) + for i := range n { + if visited[i] { + continue + } + stack := []int{i} + component := make(collections.Set[*ModuleTarget]) + visited[i] = true + + for len(stack) > 0 { + curr := stack[len(stack)-1] + stack = stack[:len(stack)-1] + component[targets[curr]] = true + for _, neighbor := range adj[curr] { + if !visited[neighbor] { + stack = append(stack, neighbor) + visited[neighbor] = true + } + } + } + groups = append(groups, component) + } + return groups +} + +// Given set of targets that define the same headers try to select ones that contain other targets as their direct or transitive dependencies +func SelectRootTargets(targets collections.Set[*ModuleTarget]) []*ModuleTarget { + allTargets := make(map[label.Label]*ModuleTarget) + dependentTargets := make(collections.Set[label.Label]) + + // Collect all target names + for target := range targets { + allTargets[target.Name] = target + } + + // Mark all targets that are listed as dependencies + for target := range targets { + for dep := range target.Deps { + dependentTargets[dep] = true + } + } + + // Any target not in the dependency map is a root + roots := make(collections.Set[*ModuleTarget]) + for name, target := range allTargets { + if !dependentTargets[name] { + roots.Add(target) + } + } + + return roots.Values() +} + +func shouldExcludeHeader(path string) bool { + // Exclude blank paths. + if strings.TrimSpace(path) == "" { + return true + } + + // Exlucde possily hidden files + segments := filepath.SplitList(path) + for _, segment := range segments { + if strings.HasPrefix(segment, ".") || strings.HasPrefix(segment, "_") { + return true + } + } + return false +} + +// shouldExcludeTarget determines if the given target (label) is possibly internal. +func shouldExcludeTarget(label label.Label) bool { + // Check target's path segments: if any segment (split on non-word characters and filtered to letters) + for _, segment := range filepath.SplitList(label.Pkg) { + tokens := splitWords(segment) + for _, token := range tokens { + switch token { + case "internal", "impl": + return true + } + } + } + return false +} + +// splits a string on non-letter characters. +func splitWords(s string) []string { + isLetter := func(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') + } + tokens := strings.FieldsFunc(s, func(r rune) bool { + return !isLetter(r) + }) + // Filter out any empty tokens. + var result []string + for _, t := range tokens { + if t != "" { + result = append(result, t) + } + } + return result +} + +// normalizeHeaderPath applies several normalization steps to make the header path +// conform to a format that a C compiler or cc_rules can correctly resolve. +func normalizeHeaderPath(hdrPath string, target ModuleTarget) string { + path := hdrPath + + // Step 1: If a stripIncludePrefix exists and hdrPath starts with it, remove it. + if target.StripIncludePrefix != "" { + if relativized, err := filepath.Rel(target.StripIncludePrefix, path); err == nil { + path = relativized + } + } + + // Step 2: If an includePrefix exists, prepend it (i.e. join it with path). + if target.IncludePrefix != "" { + path = filepath.Join(target.IncludePrefix, path) + } + + // Step 3: From target.Includes, find the longest include path that is a prefix of 'path' + // and make 'path' relative to it. + matchingIncludes := collections.Filter(target.Includes.Values(), func(include string) bool { + return strings.HasPrefix(string(path), include) + }) + switch len(matchingIncludes) { + case 0: // no-op + default: + longestInclude := slices.MaxFunc(matchingIncludes, func(l, r string) int { + return len(l) - len(r) + }) + if relativize, err := filepath.Rel(longestInclude, path); err == nil { + path = relativize + } + } + + // Step 4: If no normalization was applied (i.e. path remains equal to hdrPath) + // and the target's name defines a package relative path, then prepend it. + // (This step’s condition is taken to mean that if target.Includes was empty and hdrPath + // was not modified, we use target.Name.Pkg) + if target.Name.Pkg != "" && path == hdrPath && len(target.Includes) == 0 { + path = filepath.Join(target.Name.Pkg, path) + } + + return path +} diff --git a/index/internal/tests/BUILD.bazel b/index/internal/tests/BUILD.bazel new file mode 100644 index 00000000..ba175779 --- /dev/null +++ b/index/internal/tests/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "tests", + srcs = ["utils.go"], + importpath = "github.com/EngFlow/gazelle_cc/index/internal/tests", + visibility = ["//index:__subpackages__"], +) diff --git a/index/internal/tests/utils.go b/index/internal/tests/utils.go new file mode 100644 index 00000000..ece8abfe --- /dev/null +++ b/index/internal/tests/utils.go @@ -0,0 +1,100 @@ +// Copyright 2025 EngFlow Inc. 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 tests + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +type ExecConfig struct { + Dir string + Env []string +} + +// Utility to execute commands +func Execute(config ExecConfig, t *testing.T, program string, args ...string) exec.Cmd { + cmd := exec.Command(program, args...) + if config.Dir != "" { + cmd.Dir = config.Dir + } + if config.Env != nil { + cmd.Env = config.Env + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + t.Errorf("Failed to execute %v: %v", cmd.Args, err) + } + return *cmd +} + +func JsonEqual(a, b []byte) bool { + var j1, j2 any + return json.Unmarshal(a, &j1) == nil && json.Unmarshal(b, &j2) == nil && deepEqual(j1, j2) +} + +func deepEqual(a, b any) bool { + return strings.TrimSpace(fmtJSON(a)) == strings.TrimSpace(fmtJSON(b)) +} + +func fmtJSON(v any) string { + out, _ := json.MarshalIndent(v, "", " ") + return string(out) +} + +// copies all files recursively +func CopyDir(src, dst string) error { + return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + target := filepath.Join(dst, rel) + if info.IsDir() { + return os.MkdirAll(target, info.Mode()) + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(target, data, info.Mode()) + }) +} + +func ReplaceAllInFile(filePath string, replacements map[string]string) error { + contentBytes, err := os.ReadFile(filePath) + if err != nil { + return err + } + content := string(contentBytes) + for from, to := range replacements { + content = strings.ReplaceAll(content, from, to) + } + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write modified file: %w", err) + } + + return nil +} diff --git a/index/vendor/BUILD.bazel b/index/vendor/BUILD.bazel new file mode 100644 index 00000000..939e8a5a --- /dev/null +++ b/index/vendor/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "lib", + srcs = ["main.go"], + importmap = "github.com/EngFlow/gazelle_cc/index/vendor", + importpath = "", + visibility = ["//visibility:private"], + deps = [ + "//index/internal/bazel", + "//index/internal/collections", + "//index/internal/indexer", + "@gazelle//label", + ], +) + +go_binary( + name = "vendor", + embed = [":lib"], + visibility = ["//visibility:public"], +) diff --git a/index/vendor/integration_tests/BUILD.bazel b/index/vendor/integration_tests/BUILD.bazel new file mode 100644 index 00000000..38cc0e28 --- /dev/null +++ b/index/vendor/integration_tests/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_go//go:def.bzl", "go_test") + +# gazelle:exclude testcases +go_test( + name = "integration_tests_test", + timeout = "moderate", + srcs = ["integration_test.go"], + data = [":testcases"], + tags = ["manual"], + deps = ["//index/internal/tests"], +) diff --git a/index/vendor/integration_tests/integration_test.go b/index/vendor/integration_tests/integration_test.go new file mode 100644 index 00000000..77bde037 --- /dev/null +++ b/index/vendor/integration_tests/integration_test.go @@ -0,0 +1,117 @@ +// Copyright 2025 EngFlow Inc. 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 test + +import ( + "bytes" + "log" + "os" + "path/filepath" + "testing" + + "github.com/EngFlow/gazelle_cc/index/internal/tests" +) + +func TestRulesForeignCCIndexerIntegration(t *testing.T) { + testCasesDir := filepath.Join(".", "testcases") + repositoryDir, exists := os.LookupEnv("REPOSITORY_ROOT") + if !exists { + t.Fatalf("Missing required env variable REPOSITORY_ROOT pointing to root directory of this bazel repository") + } + entries, err := os.ReadDir(testCasesDir) + if err != nil { + t.Fatalf("failed to read test dir: %v", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + tcName := entry.Name() + t.Run(tcName, func(t *testing.T) { + runTestCase(t, filepath.Join(testCasesDir, tcName), repositoryDir) + }) + } +} + +func runTestCase(t *testing.T, readOnlyTestDir, repositoryDir string) { + testDir, err := os.MkdirTemp(os.TempDir(), "test"+filepath.Base(readOnlyTestDir)) + if err != nil { + t.Fatalf("Failed to create tmp dir") + } + tests.CopyDir(readOnlyTestDir, testDir) + log.Printf("testDir: %v", testDir) + + if err := tests.ReplaceAllInFile(filepath.Join(testDir, "MODULE.bazel"), map[string]string{ + "": repositoryDir, + }); err != nil { + t.Fatalf("Failed to prepare module file: %v", err) + } + + indexPath := filepath.Join(testDir, "generated.ccindex") + expectedIndexPath := filepath.Join(testDir, "expected.ccindex") + + t.Logf("==> [%s] Running indexer...", testDir) + bazelConfig := tests.ExecConfig{Dir: testDir} + bazelOutputBase, err := os.MkdirTemp(os.TempDir(), "bazel-outputs"+filepath.Base(readOnlyTestDir)) + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + tests.Execute(bazelConfig, t, "bazel", "--output_base="+bazelOutputBase, + "run", "@gazelle_cc//index/vendor", + "--", "--verbose", "--select=//third_party/...", "--output="+indexPath, testDir) + + t.Logf("==> [%s] Checking index file...", testDir) + expectedIndex, _ := os.ReadFile(expectedIndexPath) + actualIndex, _ := os.ReadFile(indexPath) + if !tests.JsonEqual(expectedIndex, actualIndex) { + t.Errorf("index.json doesn't match expected") + } + + t.Logf("==> [%s] Running gazelle...", testDir) + tests.Execute(bazelConfig, t, "bazel", "--output_base="+bazelOutputBase, + "run", "//:gazelle") + + t.Logf("==> [%s] Validating generated BUILD.bazel", testDir) + err = filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() && filepath.Base(path) == "BUILD.expected" { + dir := filepath.Dir(path) + buildPath := filepath.Join(dir, "BUILD") + if _, err := os.Stat(buildPath); os.IsNotExist(err) { + t.Errorf("Missing BUILD file: %v", buildPath) + } else if err != nil { + return err // propagate errors + } + expected, _ := os.ReadFile(path) + actual, _ := os.ReadFile(buildPath) + if !bytes.Equal(bytes.TrimSpace(expected), bytes.TrimSpace(actual)) { + t.Errorf("BUILD.bazel doesn't match expected.\nExpected:\n%s\nActual:\n%s", expected, actual) + } + } + return nil + }) + if err != nil { + t.Errorf("Error during walk: %v\n", err) + } + + t.Logf("==> [%s] Building project with bazel...", testDir) + tests.Execute(bazelConfig, t, "bazel", "--output_base="+bazelOutputBase, + "build", "//...", + "--incompatible_disallow_empty_glob=false") +} diff --git a/index/vendor/integration_tests/testcases/basic/BUILD b/index/vendor/integration_tests/testcases/basic/BUILD new file mode 100644 index 00000000..29b9a238 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/BUILD @@ -0,0 +1,17 @@ +load("@gazelle//:def.bzl", "gazelle", "gazelle_binary") + +gazelle_binary( + name = "gazelle_cc", + languages = [ + "@gazelle_cc//language/cc", + ], + visibility = ["//visibility:public"], +) + +# gazelle:cc_group unit +# gazelle:cc_indexfile generated.ccindex +# gazelle:exclude third_party +gazelle( + name = "gazelle", + gazelle = ":gazelle_cc", +) diff --git a/index/vendor/integration_tests/testcases/basic/MODULE.bazel b/index/vendor/integration_tests/testcases/basic/MODULE.bazel new file mode 100644 index 00000000..9f2ddc31 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/MODULE.bazel @@ -0,0 +1,7 @@ +bazel_dep(name = "rules_cc", version = "0.1.1") +bazel_dep(name = "gazelle", version = "0.42.0") +bazel_dep(name = "gazelle_cc", version = "0.1.0") +local_path_override( + module_name = "gazelle_cc", + path = "", +) \ No newline at end of file diff --git a/index/vendor/integration_tests/testcases/basic/expected.ccindex b/index/vendor/integration_tests/testcases/basic/expected.ccindex new file mode 100644 index 00000000..88064cd5 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/expected.ccindex @@ -0,0 +1,5 @@ +{ + "bar.h": "//third_party/b:bar", + "lib_a/foo.h": "//third_party/a:foo", + "third_party/c/baz.h": "//third_party/c:baz" +} \ No newline at end of file diff --git a/index/vendor/integration_tests/testcases/basic/src/BUILD.expected b/index/vendor/integration_tests/testcases/basic/src/BUILD.expected new file mode 100644 index 00000000..a30f7f12 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/src/BUILD.expected @@ -0,0 +1,11 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary") + +cc_binary( + name = "example", + srcs = ["example.cpp"], + deps = [ + "//third_party/a:foo", + "//third_party/b:bar", + "//third_party/c:baz", + ], +) diff --git a/index/vendor/integration_tests/testcases/basic/src/example.cpp b/index/vendor/integration_tests/testcases/basic/src/example.cpp new file mode 100644 index 00000000..9893cce5 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/src/example.cpp @@ -0,0 +1,12 @@ +#include +#include "lib_a/foo.h" +#include "bar.h" +#include "third_party/c/baz.h" + + +int main() { + foo(); + bar(); + baz(); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/index/vendor/integration_tests/testcases/basic/third_party/a/BUILD b/index/vendor/integration_tests/testcases/basic/third_party/a/BUILD new file mode 100644 index 00000000..91744b8e --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/third_party/a/BUILD @@ -0,0 +1,9 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "foo", + hdrs = ["foo.h"], + include_prefix = "lib_a", + includes = ["third_party"], + visibility = ["//visibility:public"], +) diff --git a/index/vendor/integration_tests/testcases/basic/third_party/a/foo.h b/index/vendor/integration_tests/testcases/basic/third_party/a/foo.h new file mode 100644 index 00000000..9a99fd39 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/third_party/a/foo.h @@ -0,0 +1,5 @@ +#include + +inline void foo(){ + printf("foo"); +} \ No newline at end of file diff --git a/index/vendor/integration_tests/testcases/basic/third_party/b/BUILD b/index/vendor/integration_tests/testcases/basic/third_party/b/BUILD new file mode 100644 index 00000000..38cefc96 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/third_party/b/BUILD @@ -0,0 +1,8 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "bar", + hdrs = ["bar.h"], + includes = ["."], + visibility = ["//visibility:public"], +) diff --git a/index/vendor/integration_tests/testcases/basic/third_party/b/bar.h b/index/vendor/integration_tests/testcases/basic/third_party/b/bar.h new file mode 100644 index 00000000..9f823243 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/third_party/b/bar.h @@ -0,0 +1,5 @@ +#include + +inline void bar(){ + printf("bar"); +} \ No newline at end of file diff --git a/index/vendor/integration_tests/testcases/basic/third_party/c/BUILD b/index/vendor/integration_tests/testcases/basic/third_party/c/BUILD new file mode 100644 index 00000000..e8ceaeaf --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/third_party/c/BUILD @@ -0,0 +1,7 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "baz", + hdrs = ["baz.h"], + visibility = ["//visibility:public"], +) diff --git a/index/vendor/integration_tests/testcases/basic/third_party/c/baz.h b/index/vendor/integration_tests/testcases/basic/third_party/c/baz.h new file mode 100644 index 00000000..8e662cb2 --- /dev/null +++ b/index/vendor/integration_tests/testcases/basic/third_party/c/baz.h @@ -0,0 +1,5 @@ +#include + +inline void baz(){ + printf("baz"); +} \ No newline at end of file diff --git a/index/vendor/main.go b/index/vendor/main.go new file mode 100644 index 00000000..a20c53f8 --- /dev/null +++ b/index/vendor/main.go @@ -0,0 +1,116 @@ +// Copyright 2025 EngFlow Inc. 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 main + +import ( + "flag" + "fmt" + "log" + "path/filepath" + "strings" + + "github.com/EngFlow/gazelle_cc/index/internal/bazel" + "github.com/EngFlow/gazelle_cc/index/internal/collections" + "github.com/EngFlow/gazelle_cc/index/internal/indexer" + "github.com/bazelbuild/bazel-gazelle/label" +) + +// Creates an index defining mapping between header and the Bazel rule that defines it, based on the cc_library definitions found in the project. +// The created index can be used as input for gazelle_cc allowing to resolve external dependenices. +func main() { + selectors := defaultSelectors + verbose := flag.Bool("verbose", false, "Enable verbose logging") + output := flag.String("output", "./vendor.ccindex", "Output file path for index") + flag.Var(&selectors, "select", "Repeated selectors for paths that should be indexed") + flag.Parse() + + if flag.NArg() != 1 { + flag.Usage() + log.Fatalf("Program requires exactly 1 argument - a path to the caller project directory, typically $PWD. Flags needs to be defined before arguments") + } + callerRoot := flag.Arg(0) + outputFile := *output + if !filepath.IsAbs(outputFile) { + outputFile = filepath.Join(callerRoot, outputFile) + } + + modules := []indexer.Module{} + for _, selector := range selectors.values { + query := bazel.Query(callerRoot, fmt.Sprintf("kind('cc_library', %s)", selector)) + if query == nil { + log.Printf("Bazel query failed for selector: '%s', it would be skipped", selector) + continue + } + modules = append(modules, indexer.Module{ + Repository: "", + Targets: collectTargets(query), + }) + } + + indexingResult := indexer.CreateHeaderIndex(modules) + indexingResult.WriteToFile(outputFile) + + if *verbose { + indexingResult.Show() + } +} + +type selectorsList struct { + values []string + isDefault bool +} + +var defaultSelectors = selectorsList{ + values: []string{"//third_party/...", "//external/...", "//vendored/..."}, + isDefault: true, +} + +func (s *selectorsList) String() string { + return strings.Join(s.values, ",") +} + +func (s *selectorsList) Set(value string) error { + if s.isDefault { + s.values = []string{} + } + s.values = append(s.values, value) + return nil +} + +func collectTargets(query *bazel.QueryResult) []*indexer.ModuleTarget { + targets := []*indexer.ModuleTarget{} + for _, ccLib := range query.GetTarget() { + name, err := label.Parse(ccLib.GetRule().GetName()) + if err != nil { + log.Printf("Failed to parse queried target label: %v", ccLib.GetRule().GetName()) + continue + } + + target := &indexer.ModuleTarget{ + Name: name, + Hdrs: collections.ToSet(collections.Collect( + ccLib.GetNamedAttribute("hdrs").GetStringListValue(), + label.Parse)), + Includes: collections.ToSet(ccLib.GetNamedAttribute("includes").GetStringListValue()), + StripIncludePrefix: ccLib.GetNamedAttribute("strip_include_prefix").GetStringValue(), + IncludePrefix: ccLib.GetNamedAttribute("include_prefix").GetStringValue(), + Deps: collections.ToSet(collections.Collect( + ccLib.GetNamedAttribute("deps").GetStringListValue(), + label.Parse)), + } + targets = append(targets, target) + } + return targets +} diff --git a/language/cc/config.go b/language/cc/config.go index 3976c9c7..b6beb207 100644 --- a/language/cc/config.go +++ b/language/cc/config.go @@ -17,6 +17,7 @@ package cc import ( "flag" "log" + "path/filepath" "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/rule" @@ -29,16 +30,18 @@ func (*ccLanguage) CheckFlags(fs *flag.FlagSet, c *config.Config) error const ( cc_group_directive = "cc_group" cc_group_unit_cycles = "cc_group_unit_cycles" + cc_indexfile = "cc_indexfile" ) func (c *ccLanguage) KnownDirectives() []string { return []string{ cc_group_directive, cc_group_unit_cycles, + cc_indexfile, } } -func (*ccLanguage) Configure(c *config.Config, rel string, f *rule.File) { +func (lang *ccLanguage) Configure(c *config.Config, rel string, f *rule.File) { var conf *cppConfig if parentConf, ok := c.Exts[languageName]; !ok { conf = newCppConfig() @@ -51,12 +54,28 @@ func (*ccLanguage) Configure(c *config.Config, rel string, f *rule.File) { return } + warnedOnNonRootIndexFileDeclaration := false for _, d := range f.Directives { switch d.Key { case cc_group_directive: selectDirectiveChoice(&conf.groupingMode, sourceGroupingModes, d) case cc_group_unit_cycles: selectDirectiveChoice(&conf.groupsCycleHandlingMode, groupsCycleHandlingModes, d) + case cc_indexfile: + if rel != "" && !warnedOnNonRootIndexFileDeclaration { + log.Printf("gazelle_cc: directive %v should be used only in the top-level BUILD file, found usage in %v", cc_indexfile, f.Path) + warnedOnNonRootIndexFileDeclaration = true + } + path := d.Value + if !filepath.IsAbs(path) { + path = filepath.Join(c.WorkDir, path) + } + index, err := loadDependencyIndex(path) + if err == nil { + lang.dependencyIndexes[path] = index + } else { + log.Printf("gazelle_cc: failed to load cc dependencies index: %v, it would be ignored. Reason: %v", path, err) + } } } } diff --git a/language/cc/generate.go b/language/cc/generate.go index a9b5852c..ddea0a19 100644 --- a/language/cc/generate.go +++ b/language/cc/generate.go @@ -47,19 +47,19 @@ func (c *ccLanguage) GenerateRules(args language.GenerateArgs) language.Generate return result } -func extractImports(args language.GenerateArgs, files []sourceFile, sourceInfos map[sourceFile]parser.SourceInfo) cppImports { - includes := []cppInclude{} +func extractImports(args language.GenerateArgs, files []sourceFile, sourceInfos map[sourceFile]parser.SourceInfo) ccImports { + includes := []ccInclude{} for _, file := range files { sourceInfo := sourceInfos[file] for _, include := range sourceInfo.Includes.DoubleQuote { rawPath := path.Clean(include) - includes = append(includes, cppInclude{rawPath: rawPath, normalizedPath: path.Join(args.Rel, rawPath), isSystemInclude: false}) + includes = append(includes, ccInclude{rawPath: rawPath, normalizedPath: path.Join(args.Rel, rawPath), isSystemInclude: false}) } for _, include := range sourceInfo.Includes.Bracket { - includes = append(includes, cppInclude{rawPath: include, normalizedPath: include, isSystemInclude: true}) + includes = append(includes, ccInclude{rawPath: include, normalizedPath: include, isSystemInclude: true}) } } - return cppImports{includes: includes} + return ccImports{includes: includes} } func splitSourcesIntoGroups(args language.GenerateArgs, srcs []sourceFile, srcInfo ccSourceInfoSet) sourceGroups { @@ -224,7 +224,7 @@ func (c *ccLanguage) generateProtoLibraryRules(args language.GenerateArgs, rules } result.Gen = append(result.Gen, newRule) - result.Imports = append(result.Imports, cppImports{}) + result.Imports = append(result.Imports, ccImports{}) } } for _, r := range args.OtherEmpty { diff --git a/language/cc/lang.go b/language/cc/lang.go index 163fd416..7529ee37 100644 --- a/language/cc/lang.go +++ b/language/cc/lang.go @@ -18,9 +18,12 @@ import ( _ "embed" "encoding/json" "log" + "os" "path/filepath" "strings" + "maps" + "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/label" "github.com/bazelbuild/bazel-gazelle/language" @@ -29,34 +32,39 @@ import ( const languageName = "cc" -type ccLanguage struct { - // Index of header includes parsed from Bazel Central Registry - bzlmodDependenciesIndex map[string]label.Label - // Set of missing bazel_dep modules referenced in includes but not defined - // Used for deduplication of missing modul_dep warnings - notFoundBzlModDeps map[string]bool -} +type ( + ccLanguage struct { + // Index of header includes parsed from Bazel Central Registry + bzlmodBuiltInIndex ccDependencyIndex + // Set of missing bazel_dep modules referenced in includes but not defined + // Used for deduplication of missing modul_dep warnings + notFoundBzlModDeps map[string]bool -type cppInclude struct { - // Include path extracted from brackets or double quotes - rawPath string - // Repository root directory relative rawPath for quoted include, rawPath otherwise - normalizedPath string - // True when include defined using brackets - isSystemInclude bool -} - -type cppImports struct { - includes []cppInclude - // TODO: module imports / exports -} + // User defined dependency indexes based on the filename + dependencyIndexes map[string]ccDependencyIndex + } + ccInclude struct { + // Include path extracted from brackets or double quotes + rawPath string + // Repository root directory relative rawPath for quoted include, rawPath otherwise + normalizedPath string + // True when include defined using brackets + isSystemInclude bool + } + ccImports struct { + includes []ccInclude + // TODO: module imports / exports + } + ccDependencyIndex map[string]label.Label +) const ccProtoLibraryFilesKey = "_protos" func NewLanguage() language.Language { return &ccLanguage{ - bzlmodDependenciesIndex: loadBzlModDependenciesIndex(), - notFoundBzlModDeps: make(map[string]bool), + bzlmodBuiltInIndex: loadBuiltInBzlModDependenciesIndex(), + notFoundBzlModDeps: make(map[string]bool), + dependencyIndexes: make(map[string]ccDependencyIndex), } } @@ -65,12 +73,8 @@ func (c *ccLanguage) Kinds() map[string]rule.KindInfo { kinds := make(map[string]rule.KindInfo) mergeMaps := func(m1, m2 map[string]bool) map[string]bool { result := make(map[string]bool, len(m1)+len(m2)) - for k, v := range m1 { - result[k] = v - } - for k, v := range m2 { - result[k] = v - } + maps.Copy(result, m1) + maps.Copy(result, m2) return result } @@ -140,18 +144,34 @@ func hasMatchingExtension(filename string, extensions []string) bool { //go:embed bzldep-index.json var bzlDepHeadersIndex string -func loadBzlModDependenciesIndex() map[string]label.Label { - var parsedLabels map[string]string - if err := json.Unmarshal([]byte(bzlDepHeadersIndex), &parsedLabels); err != nil { +func loadBuiltInBzlModDependenciesIndex() ccDependencyIndex { + index, err := unmarshalDependencyIndex([]byte(bzlDepHeadersIndex)) + if err != nil { + index = make(ccDependencyIndex) + } + return index +} + +func loadDependencyIndex(file string) (ccDependencyIndex, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + return unmarshalDependencyIndex(data) +} + +func unmarshalDependencyIndex(data []byte) (ccDependencyIndex, error) { + var rawLabels map[string]string + if err := json.Unmarshal(data, &rawLabels); err != nil { log.Printf("Failed to parsed Bazel modules dependencies index, resolving external dependenices would not work: %v", err) - return make(map[string]label.Label) + return nil, err } - index := make(map[string]label.Label, len(parsedLabels)) - for hdr, target := range parsedLabels { + index := make(ccDependencyIndex, len(rawLabels)) + for hdr, target := range rawLabels { if decoded, err := label.Parse(target); err == nil { index[hdr] = decoded } } - return index + return index, nil } diff --git a/language/cc/resolve.go b/language/cc/resolve.go index e50edae8..ed2c7c84 100644 --- a/language/cc/resolve.go +++ b/language/cc/resolve.go @@ -63,7 +63,7 @@ func (lang *ccLanguage) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *rep return } - cppImports := imports.(cppImports) + cppImports := imports.(ccImports) deps := make(map[label.Label]bool) for _, include := range cppImports.includes { @@ -100,7 +100,13 @@ func (lang *ccLanguage) resolveImportSpec(c *config.Config, ix *resolve.RuleInde } } - if label, exists := lang.bzlmodDependenciesIndex[importSpec.Imp]; exists { + for _, index := range lang.dependencyIndexes { + if label, exists := index[importSpec.Imp]; exists { + return label + } + } + + if label, exists := lang.bzlmodBuiltInIndex[importSpec.Imp]; exists { apparantName := c.ModuleToApparentName(label.Repo) // Empty apparentName means that there is no such a repository added by bazel_dep if apparantName != "" { diff --git a/language/cc/testdata/deps_index/BUILD.in b/language/cc/testdata/deps_index/BUILD.in new file mode 100644 index 00000000..bc9193fb --- /dev/null +++ b/language/cc/testdata/deps_index/BUILD.in @@ -0,0 +1 @@ +# gazelle:cc_indexfile first.ccindex \ No newline at end of file diff --git a/language/cc/testdata/deps_index/BUILD.out b/language/cc/testdata/deps_index/BUILD.out new file mode 100644 index 00000000..da1bed25 --- /dev/null +++ b/language/cc/testdata/deps_index/BUILD.out @@ -0,0 +1,13 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +# gazelle:cc_indexfile first.ccindex + +cc_library( + name = "deps_index", + srcs = ["app.cc"], + visibility = ["//visibility:public"], + deps = [ + "//second:lib", + "@external//first:lib", + ], +) \ No newline at end of file diff --git a/language/cc/testdata/deps_index/MODULE.bazel b/language/cc/testdata/deps_index/MODULE.bazel new file mode 100644 index 00000000..e69de29b diff --git a/language/cc/testdata/deps_index/app.cc b/language/cc/testdata/deps_index/app.cc new file mode 100644 index 00000000..368532f3 --- /dev/null +++ b/language/cc/testdata/deps_index/app.cc @@ -0,0 +1,4 @@ +#include "first/lib/interface.h" +#include +#include +#include "second/lib/utils.h" \ No newline at end of file diff --git a/language/cc/testdata/deps_index/expectedStderr.txt b/language/cc/testdata/deps_index/expectedStderr.txt new file mode 100644 index 00000000..a793137f --- /dev/null +++ b/language/cc/testdata/deps_index/expectedStderr.txt @@ -0,0 +1,2 @@ +gazelle: gazelle_cc: directive cc_indexfile should be used only in the top-level BUILD file, found usage in %WORKSPACEPATH%/internal/BUILD.bazel +gazelle: gazelle_cc: failed to load cc dependencies index: %WORKSPACEPATH%/invalid.ccIndex, it would be ignored. Reason: open %WORKSPACEPATH%/invalid.ccIndex: no such file or directory \ No newline at end of file diff --git a/language/cc/testdata/deps_index/first.ccindex b/language/cc/testdata/deps_index/first.ccindex new file mode 100644 index 00000000..b4c1f638 --- /dev/null +++ b/language/cc/testdata/deps_index/first.ccindex @@ -0,0 +1,4 @@ +{ + "first/lib/interface.h": "@external//first:lib", + "first/lib/utils.hpp": "@external//first:lib" +} \ No newline at end of file diff --git a/language/cc/testdata/deps_index/internal/BUILD.in b/language/cc/testdata/deps_index/internal/BUILD.in new file mode 100644 index 00000000..e2778043 --- /dev/null +++ b/language/cc/testdata/deps_index/internal/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:cc_indexfile internal/second.ccIndex +# gazelle:cc_indexfile ./invalid.ccIndex diff --git a/language/cc/testdata/deps_index/internal/BUILD.out b/language/cc/testdata/deps_index/internal/BUILD.out new file mode 100644 index 00000000..c41b4dbe --- /dev/null +++ b/language/cc/testdata/deps_index/internal/BUILD.out @@ -0,0 +1,14 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +# gazelle:cc_indexfile internal/second.ccIndex +# gazelle:cc_indexfile ./invalid.ccIndex + +cc_library( + name = "internal", + srcs = ["lib.cpp"], + visibility = ["//visibility:public"], + deps = [ + "//second:lib", + "@external//first:lib", + ], +) \ No newline at end of file diff --git a/language/cc/testdata/deps_index/internal/lib.cpp b/language/cc/testdata/deps_index/internal/lib.cpp new file mode 100644 index 00000000..368532f3 --- /dev/null +++ b/language/cc/testdata/deps_index/internal/lib.cpp @@ -0,0 +1,4 @@ +#include "first/lib/interface.h" +#include +#include +#include "second/lib/utils.h" \ No newline at end of file diff --git a/language/cc/testdata/deps_index/internal/second.ccIndex b/language/cc/testdata/deps_index/internal/second.ccIndex new file mode 100644 index 00000000..de4b84f3 --- /dev/null +++ b/language/cc/testdata/deps_index/internal/second.ccIndex @@ -0,0 +1,4 @@ +{ + "second/lib/interface.hpp": "//second:lib", + "second/lib/utils.h": "//second:lib" +} \ No newline at end of file diff --git a/runIndexersIntegrationTest.sh b/runIndexersIntegrationTest.sh new file mode 100755 index 00000000..a68c4a8b --- /dev/null +++ b/runIndexersIntegrationTest.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +REPO_ROOT=$(bazel info workspace) + +bazel test "//index:integration_tests" \ + --test_env=REPOSITORY_ROOT="$REPO_ROOT" \ + # --test_output=streamed