github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/tools/releaser/main.go (about)

     1  /* Copyright 2023 The Bazel Authors. All rights reserved.
     2  
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7     http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  // releaser is a tool for managing part of the process to release a new version of gazelle.
    17  package main
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"context"
    23  	"errors"
    24  	"flag"
    25  	"fmt"
    26  	"github.com/bazelbuild/bazel-gazelle/rule"
    27  	bzl "github.com/bazelbuild/buildtools/build"
    28  	"io"
    29  	"os"
    30  	"os/exec"
    31  	"os/signal"
    32  	"path"
    33  	"strconv"
    34  	"strings"
    35  )
    36  
    37  func main() {
    38  	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    39  	defer cancel()
    40  	if err := run(ctx, os.Stderr); err != nil {
    41  		fmt.Fprintln(os.Stderr, err)
    42  		os.Exit(1)
    43  	}
    44  }
    45  
    46  func run(ctx context.Context, stderr *os.File) error {
    47  	var (
    48  		verbose   bool
    49  		goVersion string
    50  		repoRoot  string
    51  	)
    52  
    53  	flag.BoolVar(&verbose, "verbose", false, "increase verbosity")
    54  	flag.BoolVar(&verbose, "v", false, "increase verbosity (shorthand)")
    55  	flag.StringVar(&goVersion, "go_version", "", "go version for go.mod")
    56  	flag.StringVar(&repoRoot, "repo_root", os.Getenv("BUILD_WORKSPACE_DIRECTORY"), "root directory of Gazelle repo")
    57  	flag.Usage = func() {
    58  		fmt.Fprint(flag.CommandLine.Output(), `usage: bazel run //tools/releaser -- -go_version <version>
    59  
    60  This utility is intended to handle many of the steps to release a new version.
    61  
    62  `)
    63  		flag.PrintDefaults()
    64  	}
    65  
    66  	flag.Parse()
    67  
    68  	var goVersionArgs []string
    69  	if goVersion != "" {
    70  		versionParts := strings.Split(goVersion, ".")
    71  		if len(versionParts) < 2 {
    72  			flag.Usage()
    73  			return errors.New("please provide a valid Go version")
    74  		}
    75  		if minorVersion, err := strconv.Atoi(versionParts[1]); err != nil {
    76  			return fmt.Errorf("%q is not a valid Go version", goVersion)
    77  		} else if minorVersion > 0 {
    78  			versionParts[1] = strconv.Itoa(minorVersion - 1)
    79  		}
    80  		goVersionArgs = append(goVersionArgs, "-go", goVersion, "-compat", strings.Join(versionParts, "."))
    81  	}
    82  
    83  	workspacePath := path.Join(repoRoot, "WORKSPACE")
    84  	depsPath := path.Join(repoRoot, "deps.bzl")
    85  	_tmpBzl := "tmp.bzl"
    86  	tmpBzlPath := path.Join(repoRoot, _tmpBzl)
    87  
    88  	if verbose {
    89  		fmt.Println("Running initial go update commands")
    90  	}
    91  	initialCommands := []struct {
    92  		cmd  string
    93  		args []string
    94  	}{
    95  		{cmd: "go", args: []string{"get", "-t", "-u", "./..."}},
    96  		{cmd: "go", args: append([]string{"mod", "tidy"}, goVersionArgs...)},
    97  		{cmd: "go", args: []string{"mod", "vendor"}},
    98  		{cmd: "find", args: []string{"vendor", "-name", "BUILD.bazel", "-delete"}},
    99  	}
   100  	for _, c := range initialCommands {
   101  		cmd := exec.CommandContext(ctx, c.cmd, c.args...)
   102  		cmd.Dir = repoRoot
   103  		if out, err := cmd.CombinedOutput(); err != nil {
   104  			fmt.Println(string(out))
   105  			return err
   106  		}
   107  	}
   108  
   109  	workspace, err := os.OpenFile(workspacePath, os.O_RDWR, 0644)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	defer workspace.Close()
   114  
   115  	if verbose {
   116  		fmt.Println("Preparing temporary WORKSPACE without gazelle directives.")
   117  	}
   118  	workspaceWithoutDirectives, err := getWorkspaceWithoutDirectives(workspace)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	// reuse the open workspace file, so first we empty it and rewind
   124  	err = workspace.Truncate(0)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	_ /* new offset */, err = workspace.Seek(0, os.SEEK_SET)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	// write the directive-less workspace and update repos
   134  	if _, err := workspace.Write(workspaceWithoutDirectives); err != nil {
   135  		return err
   136  	}
   137  
   138  	if verbose {
   139  		fmt.Println("Running update-repos outputting to temporary file.")
   140  	}
   141  	cmd := exec.CommandContext(ctx, "bazel", "run", "//:gazelle", "--", "update-repos", "-from_file=go.mod", fmt.Sprintf("-to_macro=%s%%gazelle_dependencies", _tmpBzl))
   142  	cmd.Dir = os.Getenv("BUILD_WORKSPACE_DIRECTORY")
   143  	if out, err := cmd.CombinedOutput(); err != nil {
   144  		fmt.Println(string(out))
   145  		return err
   146  	}
   147  	defer os.Remove(tmpBzlPath)
   148  
   149  	// parse the resulting tmp.bzl for deps.bzl and WORKSPACE updates
   150  	if verbose {
   151  		fmt.Println("Parsing temporary bzl file to prepare deps.bzl and WORKSPACE modifications.")
   152  	}
   153  	maybeRules, workspaceDirectives, err := readFromTmp(tmpBzlPath)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	// update deps
   159  	if verbose {
   160  		fmt.Println("Writing new deps.bzl")
   161  	}
   162  	if err := updateDepsBzlWithRules(depsPath, maybeRules); err != nil {
   163  		return err
   164  	}
   165  
   166  	// append WORKSPACE with directives at the end.
   167  	// except we cannot append directly because the earlier bazel //:gazelle run modified WORKSPACE
   168  	// so we truncate and seek to the beginning again before writing all of what we want
   169  	if verbose {
   170  		fmt.Println("Append WORKSPACE with directives")
   171  	}
   172  	_ /* new offset */, err = workspace.Seek(0, os.SEEK_SET)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	// write the directive-less workspace and update repos
   178  	if _, err := workspace.Write(workspaceWithoutDirectives); err != nil {
   179  		return err
   180  	}
   181  	if _, err := workspace.Write(workspaceDirectives); err != nil {
   182  		return err
   183  	}
   184  
   185  	// cleanup before final gazelle run
   186  	//
   187  	// note that we also have a defer for os.Remove so it gets cleaned up if there are earlier errors.
   188  	// This defer will throw an error from this point on, but we're swallowing it anyways.
   189  	if verbose {
   190  		fmt.Println("Cleaning up temporary files")
   191  	}
   192  	if err := os.Remove(tmpBzlPath); err != nil {
   193  		return err
   194  	}
   195  
   196  	if verbose {
   197  		fmt.Println("Running final gazelle run, and copying some language specific build files.")
   198  	}
   199  	cmd = exec.CommandContext(ctx, "bazel", "run", "//:gazelle")
   200  	cmd.Dir = repoRoot
   201  	if out, err := cmd.CombinedOutput(); err != nil {
   202  		fmt.Println(string(out))
   203  		return err
   204  	}
   205  
   206  	cmd = exec.CommandContext(ctx, "bazel", "build",
   207  		"//language/go:std_package_list",
   208  		"//language/proto:known_go_imports",
   209  		"//language/proto:known_imports",
   210  		"//language/proto:known_proto_imports",
   211  	)
   212  	cmd.Dir = repoRoot
   213  	if out, err := cmd.CombinedOutput(); err != nil {
   214  		fmt.Println(string(out))
   215  		return err
   216  	}
   217  
   218  	generatedFiles := []string{
   219  		"language/go/std_package_list.go",
   220  		"language/proto/known_go_imports.go",
   221  		"language/proto/known_imports.go",
   222  		"language/proto/known_proto_imports.go",
   223  	}
   224  	for _, f := range generatedFiles {
   225  		if err := updateFile(repoRoot, f); err != nil {
   226  			return err
   227  		}
   228  	}
   229  
   230  	if verbose {
   231  		fmt.Println("Release prepared.")
   232  	}
   233  	return nil
   234  }
   235  
   236  func updateFile(repoRoot, filePath string) error {
   237  	destPath := path.Join(repoRoot, filePath)
   238  	dest, err := os.Create(destPath)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	srcPath := path.Join(repoRoot, "bazel-bin", filePath)
   243  	src, err := os.Open(srcPath)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	_, err = io.Copy(dest, src)
   248  	return err
   249  }
   250  
   251  func getWorkspaceWithoutDirectives(workspace io.Reader) ([]byte, error) {
   252  	workspaceScanner := bufio.NewScanner(workspace)
   253  	var workspaceWithoutDirectives bytes.Buffer
   254  	for workspaceScanner.Scan() {
   255  		currentLine := workspaceScanner.Text()
   256  		if strings.HasPrefix(currentLine, "# gazelle:repository go_repository") {
   257  			continue
   258  		}
   259  		_, err := workspaceWithoutDirectives.WriteString(currentLine + "\n")
   260  		if err != nil {
   261  			return nil, err
   262  		}
   263  	}
   264  	// leave some buffering at the end of the bytes
   265  	_, err := workspaceWithoutDirectives.WriteString("\n\n")
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	return workspaceWithoutDirectives.Bytes(), workspaceScanner.Err()
   270  }
   271  
   272  func readFromTmp(tmpBzlPath string) ([]*rule.Rule, []byte, error) {
   273  	workspaceDirectivesBuff := new(bytes.Buffer)
   274  	var rules []*rule.Rule
   275  	tmpBzl, err := rule.LoadMacroFile(tmpBzlPath, "tmp" /* pkg */, "gazelle_dependencies" /* DefName */)
   276  	if err != nil {
   277  		return nil, nil, err
   278  	}
   279  	for _, r := range tmpBzl.Rules {
   280  		maybeRule := rule.NewRule("_maybe", r.Name())
   281  		maybeRule.AddArg(&bzl.Ident{
   282  			Name: r.Kind(),
   283  		})
   284  
   285  		for _, k := range r.AttrKeys() {
   286  			maybeRule.SetAttr(k, r.Attr(k))
   287  		}
   288  
   289  		var suffix string
   290  		rules = append(rules, maybeRule)
   291  		fmt.Fprintf(workspaceDirectivesBuff, "# gazelle:repository go_repository name=%s importpath=%s%s\n",
   292  			r.Name(),
   293  			r.AttrString("importpath"),
   294  			suffix,
   295  		)
   296  	}
   297  	return rules, workspaceDirectivesBuff.Bytes(), nil
   298  }
   299  
   300  func updateDepsBzlWithRules(depsPath string, maybeRules []*rule.Rule) error {
   301  	depsBzl, err := rule.LoadMacroFile(depsPath, "deps" /* pkg */, "gazelle_dependencies" /* DefName */)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	for _, r := range depsBzl.Rules {
   307  		if r.Kind() == "_maybe" && len(r.Args()) == 1 {
   308  			// We can't actually delete all _maybe's because http_archive uses it too in here!
   309  			if ident, ok := r.Args()[0].(*bzl.Ident); ok && ident.Name == "go_repository" {
   310  				r.Delete()
   311  			}
   312  		}
   313  	}
   314  
   315  	for _, r := range maybeRules {
   316  		r.Insert(depsBzl)
   317  	}
   318  
   319  	return depsBzl.Save(depsPath)
   320  }