Skip to content

Commit cb8793a

Browse files
authored
adding get-targets command (#2)
* adding get-targets command * debug * explicitly build bazel diff * add differential build example
1 parent 1cbb3bf commit cb8793a

27 files changed

Lines changed: 835 additions & 132 deletions

.github/workflows/bazel-ci.yaml

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,32 @@ jobs:
2222
~/.cache/bazel
2323
key: ${{ runner.os }}-${{ env.cache-name }}
2424
- uses: actions/checkout@v3
25+
with:
26+
fetch-depth: 0
27+
# You won't do this in your build since you will likely use a prebuilt bazel-differ binary, but we
28+
# need to bootstrap
29+
- name: build
30+
run: bazel build //cli:bazel-differ
31+
# This section starts an example of how to use get-targets in your CI process
32+
- name: Get revisions
33+
id: get-revisions
34+
run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} ./tools/diff-sha.sh
35+
- name: get-test-targets
36+
run: $(bazel info bazel-bin)/cli/bazel-differ_/bazel-differ get-targets -w $(pwd) -b $(which bazelisk) -s ${{ steps.get-revisions.outputs.previous_sha }} -f ${{ steps.get-revisions.outputs.current_sha }} -o test_targets.txt
37+
- name: run-test-targets
38+
run: |
39+
cat test_targets.txt
40+
bazel test --target_pattern_file=test_targets.txt
41+
- name: get-build-targets
42+
run: $(bazel info bazel-bin)/cli/bazel-differ_/bazel-differ get-targets -w $(pwd) -b $(which bazelisk) -s ${{ steps.get-revisions.outputs.previous_sha }} -f ${{ steps.get-revisions.outputs.current_sha }} -o build_targets.txt
43+
- name: run-build-targets
44+
run: |
45+
cat build_targets.txt
46+
bazel build --target_pattern_file=build_targets.txt
47+
# Run tests
2548
- name: gazelle check
2649
run: bazel run //:gazelle_ci
27-
- name: test
28-
run: bazel test //...
29-
- name: build
30-
run: bazel build //...
50+
- name: build bazel-diff
51+
run: bazel build //tools/bazel-diff/...
3152
- name: compatibility tests
3253
run: ./test.sh

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
.idea
2-
bazel-diff
3-
*.txt

README.md

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,79 @@
22

33
`bazel-differ` is a command line interface for Bazel that helps you do incremental builds across different Git versions. `bazel-differ` is a mostly a pure Go port of the excellent [bazel-diff](https://github.com/tinder/bazel-diff) and should be able to function as a drop-in replacement for most use cases.
44

5+
## CLI
6+
7+
```console
8+
bazel-differ is a CLI tool to assist with doing differential Bazel builds
9+
10+
Usage:
11+
bazel-differ [command]
12+
13+
Available Commands:
14+
completion Generate the autocompletion script for the specified shell
15+
diff Writes to a file the impacted targets between two Bazel graph JSON files
16+
generate-hashes Writes to a file the SHA256 hashes for each Bazel Target in the provided workspace.
17+
get-targets Collects a set of a targets between the specified commit ranges
18+
help Help about any command
19+
20+
Flags:
21+
-z, --bazelCommandOptions string Additional space separated Bazel command options used when invoking Bazel
22+
-b, --bazelPath string Path to Bazel binary
23+
-y, --bazelStartupOptions string Additional space separated Bazel client startup options used when invoking Bazel
24+
-h, --help help for bazel-differ
25+
-k, --keep_going bazel query This flag controls if bazel query will be executed with the `--keep_going` flag or not. Disabling this flag allows you to catch configuration issues in your Bazel graph, but may not work for some Bazel setups. Defaults to `true` (default true)
26+
-v, --verbose enables verbose output
27+
-w, --workspacePath string Path to Bazel workspace directory.
28+
29+
Use "bazel-differ [command] --help" for more information about a command.
30+
```
31+
32+
If you are using as a replacement for `bazel-diff`, then you will probably use the `generate-hashes` and `diff` commands. Alternatively, you can make use of the `get-targets` command to eliminate the need for an external shell script:
33+
34+
```console
35+
Collects a set of a targets between the specified commit ranges. By default,
36+
the final set of targets is run through a Bazel query "set({{.Targets}})" which can be customized by the -q parameter.
37+
If you want to query all tests impacted for the given commit range, you can do:
38+
39+
$ bazel-differ get-targets -w path/to/workspace -b $(which bazel) -s START_HASH -f FINAL_HASH -q 'kind(".*_test",set({{.Targets}}))' -o test_targets.txt
40+
41+
Usage:
42+
bazel-differ get-targets [flags]
43+
44+
Flags:
45+
--cache-dir string Directory to cache hashes associated with commits
46+
-f, --finalRevision string The Git revision to use to generate the ending hashes
47+
-h, --help help for get-targets
48+
--nocache Disables hash caching (default false)
49+
-o, --output string Filepath to write the impacted Bazel targets to, newline separated
50+
-q, --query string The query template to use when querying for changed targets
51+
-s, --startingRevision string The Git revision to generate the ending hashes
52+
53+
Global Flags:
54+
-z, --bazelCommandOptions string Additional space separated Bazel command options used when invoking Bazel
55+
-b, --bazelPath string Path to Bazel binary
56+
-y, --bazelStartupOptions string Additional space separated Bazel client startup options used when invoking Bazel
57+
-k, --keep_going bazel query This flag controls if bazel query will be executed with the `--keep_going` flag or not. Disabling this flag allows you to catch configuration issues in your Bazel graph, but may not work for some Bazel setups. Defaults to `true` (default true)
58+
-v, --verbose enables verbose output
59+
-w, --workspacePath string Path to Bazel workspace directory.
60+
```
61+
62+
Running the example above, then allows you to run:
63+
64+
```console
65+
$ bazel test --target_pattern_file=test_targets.txt
66+
```
67+
68+
By default, the hashes for a given Git revision are cached to disk so you can run multiple invocations of `get-target` without paying a penalty for recalculating the hashes every time.
69+
570
# FAQ
671

772
1. Why did you port `bazel-diff`?
873

974
A couple reasons:
1075

11-
* The projects I work with don't have any JVM dependencies so using `bazel-diff` was adding some additional build time I wanted to eliminate.
12-
* With the exception of the Bazel server itself, much of the tooling in the Bazel ecosystem is written in Go.
76+
* The projects I work with don't have any JVM dependencies so using `bazel-diff` was adding some additional build time we wanted to eliminate
77+
* We wanted to add a fair amount of functionality on top of `bazel-diff` and porting it to Go wasn't a huge lift. Most tooling in the Bazel ecosystem is written in Go (with the exception of the Bazel server)
1378

1479
2. What are the differences from `bazel-diff`?
1580

cmd/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ go_library(
55
srcs = [
66
"diff.go",
77
"generate_hashes.go",
8+
"get_targets.go",
89
"root.go",
910
],
1011
importpath = "github.com/ewhauser/bazel-differ/cmd",
1112
visibility = ["//visibility:public"],
1213
deps = [
1314
"//internal",
15+
"//internal/cache",
1416
"@com_github_spf13_cobra//:cobra",
1517
],
1618
)

cmd/diff.go

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
package cmd
22

33
import (
4-
"bufio"
5-
"encoding/json"
64
"errors"
75
"fmt"
86
"github.com/ewhauser/bazel-differ/internal"
97
"github.com/spf13/cobra"
10-
"io/ioutil"
11-
"log"
128
"os"
139
)
1410

@@ -41,46 +37,23 @@ var diffCmd = &cobra.Command{
4137
targetHasher := internal.NewTargetHashingClient(GetBazelClient(), internal.Filesystem,
4238
internal.NewRuleProvider())
4339

44-
startingHashes := readHashFile(StartingHashes)
45-
finalHashes := readHashFile(FinalHashes)
40+
startingHashes, err := internal.ReadHashFile(StartingHashes)
41+
ExitIfError(err, "")
42+
finalHashes, err := internal.ReadHashFile(FinalHashes)
43+
ExitIfError(err, "")
4644
targets, err := targetHasher.GetImpactedTargets(startingHashes, finalHashes)
47-
if err != nil {
48-
panic(err)
49-
}
50-
writeFile(targets, Output)
45+
ExitIfError(err, "")
46+
internal.WriteTargetsFile(targets, Output)
5147
},
5248
}
5349

54-
func writeFile(targets map[string]bool, output string) {
55-
file, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
56-
57-
if err != nil {
58-
log.Fatalf("failed creating file: %s", err)
59-
}
60-
61-
defer file.Close()
62-
63-
datawriter := bufio.NewWriter(file)
64-
defer datawriter.Flush()
65-
66-
for k := range targets {
67-
_, _ = datawriter.WriteString(k + "\n")
68-
}
69-
}
70-
71-
func readHashFile(filename string) map[string]string {
72-
x := map[string]string{}
73-
startingContent, err := ioutil.ReadFile(filename)
74-
if err != nil {
75-
panic(err)
76-
}
77-
err = json.Unmarshal(startingContent, &x)
78-
if err != nil {
79-
panic(err)
80-
}
81-
return x
82-
}
83-
8450
func init() {
8551
rootCmd.AddCommand(diffCmd)
52+
diffCmd.PersistentFlags().StringVarP(&StartingHashes, "startingHashes", "s", "",
53+
"The path to the JSON file of target hashes for the initial revision. Run 'generate-hashes' to get this value.")
54+
diffCmd.PersistentFlags().StringVarP(&FinalHashes, "finalHashes", "f", "",
55+
"The path to the JSON file of target hashes for the final revision. Run 'generate-hashes' to get this value.")
56+
diffCmd.PersistentFlags().StringVarP(&Output, "output", "o", "",
57+
"Filepath to write the impacted Bazel targets to, "+
58+
"newline separated")
8659
}

cmd/generate_hashes.go

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@ package cmd
22

33
import (
44
"bufio"
5-
"bytes"
6-
"encoding/json"
75
"fmt"
86
"github.com/ewhauser/bazel-differ/internal"
9-
"io"
10-
"io/ioutil"
117
"os"
128

139
"github.com/spf13/cobra"
1410
)
1511

1612
var SeedFilepaths string
17-
var DisplayElapsedTime bool
13+
var displayElapsedTime bool
1814

1915
// generateHashesCmd represents the generateHashes command
2016
var generateHashesCmd = &cobra.Command{
@@ -32,30 +28,18 @@ var generateHashesCmd = &cobra.Command{
3228
}
3329

3430
hashes, err := targetHasher.HashAllBazelTargetsAndSourcefiles(seedfilePaths)
35-
if err != nil {
36-
panic(err)
37-
}
38-
39-
var buffer bytes.Buffer
40-
err = prettyEncode(hashes, &buffer)
41-
if err != nil {
42-
panic(err)
43-
}
31+
ExitIfError(err, "")
4432

45-
err = ioutil.WriteFile(args[0], buffer.Bytes(), 0644)
46-
if err != nil {
47-
panic(err)
48-
}
49-
fmt.Println(buffer.String())
33+
res, err := internal.WriteHashFile(args[0], hashes)
34+
ExitIfError(err, "")
35+
fmt.Println(res)
5036
},
5137
}
5238

5339
func readSeedFile() map[string]bool {
5440
readFile, err := os.Open(SeedFilepaths)
5541

56-
if err != nil {
57-
panic(err)
58-
}
42+
ExitIfError(err, fmt.Sprintf("Error reading seedfile: %s", SeedFilepaths))
5943

6044
defer readFile.Close()
6145

@@ -68,20 +52,15 @@ func readSeedFile() map[string]bool {
6852
return seedFilepaths
6953
}
7054

71-
func prettyEncode(data interface{}, out io.Writer) error {
72-
enc := json.NewEncoder(out)
73-
enc.SetIndent("", " ")
74-
if err := enc.Encode(data); err != nil {
75-
return err
76-
}
77-
return nil
78-
}
79-
8055
func init() {
8156
rootCmd.AddCommand(generateHashesCmd)
8257
generateHashesCmd.Flags().StringVarP(&SeedFilepaths, "seed-filepaths", "", "",
8358
"A text file containing a newline separated list of filepaths, "+
8459
"each of these filepaths will be read and used as a seed for all targets.")
85-
generateHashesCmd.Flags().BoolVarP(&DisplayElapsedTime, "displayElapsedTime", "d", false,
60+
generateHashesCmd.Flags().BoolVarP(&displayElapsedTime, "displayElapsedTime", "d", false,
8661
"This flag controls whether to print out elapsed time for bazel query and content hashing")
62+
generateHashesCmd.PersistentFlags().StringVarP(&StartingHashes, "startingHashes", "s", "",
63+
"The path to the JSON file of target hashes for the initial revision. Run 'generate-hashes' to get this value.")
64+
generateHashesCmd.PersistentFlags().StringVarP(&FinalHashes, "finalHashes", "f", "",
65+
"The path to the JSON file of target hashes for the final revision. Run 'generate-hashes' to get this value.")
8766
}

cmd/get_targets.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/ewhauser/bazel-differ/internal"
7+
"github.com/ewhauser/bazel-differ/internal/cache"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var startingRevision string
12+
var finalRevision string
13+
var query string
14+
var cacheDir string
15+
var cacheDisabled bool
16+
17+
// getTargets represents the get-targets command
18+
var getTargets = &cobra.Command{
19+
Use: "get-targets",
20+
Short: "Collects a set of a targets between the specified commit ranges",
21+
Long: `Collects a set of a targets between the specified commit ranges. By default,
22+
the final set of targets is run through a Bazel query "set({{.Targets}})" which can be customized by the -q parameter.
23+
If you want to query all tests impacted for the given commit range, you can do:
24+
25+
$ bazel-differ get-targets -w path/to/workspace -b $(which bazel) -s START_HASH -f FINAL_HASH -q 'kind(".*_test",set({{.Targets}}))'" -o test_targets.txt
26+
`,
27+
Run: func(cmd *cobra.Command, args []string) {
28+
gitClient := internal.NewGitClient(WorkspacePath)
29+
bazelClient := GetBazelClient()
30+
cacheManager, err := cache.NewHashCacheManager(!cacheDisabled, cacheDir)
31+
ExitIfError(err, "")
32+
targetHasher := internal.NewTargetHashingClient(GetBazelClient(), internal.Filesystem,
33+
internal.NewRuleProvider())
34+
35+
startingHashes := getHashes(startingRevision, gitClient, cacheManager, targetHasher)
36+
endingHashes := getHashes(finalRevision, gitClient, cacheManager, targetHasher)
37+
38+
targets, err := targetHasher.GetImpactedTargets(startingHashes, endingHashes)
39+
ExitIfError(err, "")
40+
41+
queriedTargets, err := bazelClient.QueryTarget(query, targets)
42+
ExitIfError(err, "")
43+
targetNames := targetHasher.GetNames(queriedTargets)
44+
45+
if Output != "" {
46+
internal.WriteTargetsFile(targetNames, Output)
47+
} else {
48+
for k := range targetNames {
49+
fmt.Println(k)
50+
}
51+
}
52+
},
53+
}
54+
55+
func getHashes(revision string, gitClient internal.GitClient, cacheManager cache.HashCacheManager,
56+
targetHasher internal.TargetHashingClient) map[string]string {
57+
err := gitClient.Checkout(revision)
58+
ExitIfError(err, fmt.Sprintf("Unable to checkout revision: %s", revision))
59+
60+
var seedfilePaths = make(map[string]bool)
61+
if SeedFilepaths != "" {
62+
seedfilePaths = readSeedFile()
63+
}
64+
65+
hashes, err := cacheManager.Get(context.Background(), revision)
66+
ExitIfError(err, fmt.Sprintf("Error retrieving hashes for revision %s from cache", revision))
67+
68+
if hashes == nil {
69+
hashes, err = targetHasher.HashAllBazelTargetsAndSourcefiles(seedfilePaths)
70+
}
71+
ExitIfError(err, "")
72+
73+
err = cacheManager.Put(context.Background(), revision, hashes)
74+
ExitIfError(err, "")
75+
76+
return hashes
77+
}
78+
79+
func init() {
80+
rootCmd.AddCommand(getTargets)
81+
getTargets.PersistentFlags().StringVarP(&startingRevision, "startingRevision", "s", "",
82+
"The Git revision to generate the ending hashes")
83+
getTargets.PersistentFlags().StringVarP(&finalRevision, "finalRevision", "f", "",
84+
"The Git revision to use to generate the ending hashes")
85+
getTargets.Flags().StringVarP(&SeedFilepaths, "seed-filepaths", "", "",
86+
"A text file containing a newline separated list of filepaths, "+
87+
"each of these filepaths will be read and used as a seed for all targets.")
88+
getTargets.PersistentFlags().StringVarP(&query, "query", "q", "",
89+
"The query template to use when querying for changed targets")
90+
getTargets.PersistentFlags().StringVarP(&Output, "output", "o", "",
91+
"Filepath to write the impacted Bazel targets to, "+
92+
"newline separated")
93+
getTargets.PersistentFlags().StringVar(&cacheDir, "cache-dir", "",
94+
"Directory to cache hashes associated with commits")
95+
getTargets.PersistentFlags().BoolVar(&cacheDisabled, "nocache", false,
96+
"Disables hash caching")
97+
}

0 commit comments

Comments
 (0)