The Skia Infrastructure Gazelle extension generates Bazel build targets for front-end code (TypeScript, Sass, HTML) using the rules defined in //infra-sk/index.bzl
. Specifically, it generates the following kinds of targets:
ts_library
karma_test
nodejs_test
sass_library
sk_element
sk_page
sk_element_demo_page_server
sk_element_puppeteer_test
Normally, we use the word “rule” to refer to Bazel rule and macro definitions, e.g.:
def ts_library(name, srcs, ...): ...
And we use the word “target” to refer to a specific instance of a rule or macro, e.g.:
ts_library( name = "my_lib", srcs = ["my_lib.ts"], )
However, the Gazelle API uses the words “rule kind” to refer to what we normally call “rule”, and “rule” to refer to what we normally call “target”. The Skia Infrastructure Gazelle extension uses said words in the same fashion as the Gazelle API to avoid confusion.
This section describes how a typical Gazelle extension works. The Skia Infrastructure Gazelle extension differs in that it uses a custom rule index to resolve dependencies between rules. These differences are pointed out where necessary.
A Gazelle extension is essentially a go_library
with a function named NewLanguage
that provides an implementation of the language.Language
Go interface. This interface provides hooks for generating rules, parsing configuration directives, and resolving imports to Bazel labels.
Gazelle extensions work in (roughly) three steps, each one corresponding to one method in the language.Language
interface:
deps
arguments of the rules we created/updated in step 2 with rules from steps 1 and 2).When the Gazelle binary runs, it will call the language.Language
interface methods corresponding to each step in the above order.
For a more in-depth overview, please see https://github.com/bazelbuild/bazel-gazelle/blob/3fccaeca6a77cc41adcb90c4c8ce0af5c49d2c9d/merger/merger.go#L19.
This step takes place in the implementation of the Imports
method of the language.Language
interface (defined in the resolve.Resolver
interface, which language.Language
embeds).
An import is the path of an “import” statement in a programming language. For example, the path of the following TypeScript import statement is measurements/units/international
:
import { length as meter } from 'measurements/units/international';
Imports
takes as a parameter a Bazel rule (represented as a rule.Rule
struct) and returns the set of imports in the underlying programming language that the rule may provide (represented as a slice of resolve.ImportSpec
structs).
As an example, suppose that Imports
is passed a rule.Rule
struct that represents the following ts_library
rule, defined in a hypothetical //measurements/units
Bazel package:
# //measurements/units/BUILD.bazel ts_library( name = "units", srcs = [ "customary.ts", "imperial.ts", "international.ts", ], )
In this example, Imports
should return the following imports:
measurements/units/customary
measurements/units/imperial
measurements/units/international
Note that the imports returned by Imports
are based exclusively on the file names of the rule's sources (srcs
attribute). At no point does Imports
inspect the contents of the source files.
In step 1, Gazelle invokes Imports
once for each Bazel rule in the workspace. Gazelle uses the returned imports to build a resolve.RuleIndex
struct which maps imports to the rules that might provide them (see e.g. its FindRulesByImport
method.)
In step 2, Gazelle invokes Imports
again for each rule generated or updated by the Gazelle extension, in order to make sure the resolve.RuleIndex
reflects the changes made by the extension.
In step 3, the resolve.RuleIndex
is used to resolve the deps
argument of each rule generated or updated by the extension.
While Gazelle extensions typically rely on the resolve.RuleIndex
to resolve dependencies, the Skia Infrastructure Gazelle extension uses a custom rule index due to limitations with the resolve.RuleIndex
struct. Our implementation of the Imports
method populates the custom rule index with all the information required by this extension, and always returns an empty resolve.ImportSpec
slice. This results in an empty resolve.RuleIndex
, but that is OK because we never use it. In step 3, method Resolve
will query the custom rule index to resolve any dependencies between rules, ignoring the resolve.RuleIndex
built by Gazelle.
This step takes place in the implementation of the GenerateRules
method of the language.Language
interface.
GenerateRules
takes a directory as an argument, and generates rules from source files found in the directory. It returns a language.GenerateResult
struct with the following contents:
rule.Rule
structs (field Gen
).BUILD.bazel
file) that no longer can be built, e.g. because their source files have been deleted (field Empty
).Imports
).As an example, suppose that directory //measurements/conversions
has files conversions.ts
and conversions_test.ts
with the following contents:
// //measurements/conversions/conversions.ts import { mass as lb } from 'measurements/units/customary'; import { mass as kg } from 'measurements/units/international'; export const lbsToKg = (lbs: number) => `${lbs} ${lb} is equal to ${lbs * 0.453592} ${kg}`;
// //measurements/conversions/conversions_test.ts import { lbsToKg } from './conversions'; describe('conversions', () => { it('should convert pounds to kilograms', () => { expect(lbsToKg(1)).to.equal( '1 pound (lb) is equal to 0.45392 kilogram (kg)' ); }); });
In this example, GenerateRules
should generate the following rules:
# //measurements/conversions/BUILD.bazel ts_library( name = "conversions", srcs = ["conversions.ts"], # Note that no "deps" argument is generated in this step. Step 3 populates the "deps" argument. ) karma_test( name = "conversions_test", srcs = ["conversions_test.ts"], # Note that no "deps" argument is generated in this step. Step 3 populates the "deps" argument. )
Field Imports
of the returned language.GenerateResult
should be populated with the following imports, grouped by rule:
conversions
: measurements/units/customary
, measurements/units/international
.conversions_test
: ./conversions
.The above imports must be parsed from the sources of each rule. Gazelle extensions may use a parser for the programming language of the source files, regular expressions, or any other suitable technique.
As mentioned in step 1, Gazelle will call the Imports
method with each rule returned by GenerateRules
in order to keep the rule index up-to-date.
This step takes place in the implementation of the Resolve
method of the language.Language
interface (defined in the resolve.Resolver
interface, which language.Language
embeds).
Resolve
takes as arguments a (rule, imports) pair returned by GenerateRules
in step 2, and populates the deps
argument of the rule. It does so by querying the resolve.RuleIndex
for the rules that provide each import.
Gazelle invokes Resolve
once for each rule returned by GenerateRules
.
The example rules from step 2 might look as follows after having their deps
arguments populated by Resolve
:
# //measurements/conversions/BUILD.bazel ts_library( name = "conversions", srcs = ["conversions.ts"], deps = ["//measurements/units:units"], ) karma_test( name = "conversions_test", srcs = ["conversions_test.ts"], deps = [":conversions"], )
As mentioned in step 1, the Skia Infrastructure Gazelle extension ignores Gazelle's resolve.RuleIndex
, and uses a custom rule index instead, which is populated in the Imports
method. Our implementation of the Resolve
method uses said custom rule index to resolve dependencies between rules.
Support for new rule kinds (e.g. foo_library
, bar_binary
, etc.) can be added in three steps:
Update method GenerateRules
to generate, update and delete rules of the new kind, and parse any imports present in their source files.
Update method Resolve
to resolve the deps
argument of rules of the new kind, if necessary.
Any rule kinds generated by the Skia Infrastructure Gazelle extension must be included in the return values of methods KnownDirectives
, Loads
and Kinds
.