github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/internal/pipe/brew/brew.go (about)

     1  package brew
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  	"text/template"
    13  
    14  	"github.com/apex/log"
    15  	"github.com/goreleaser/goreleaser/internal/artifact"
    16  	"github.com/goreleaser/goreleaser/internal/client"
    17  	"github.com/goreleaser/goreleaser/internal/pipe"
    18  	"github.com/goreleaser/goreleaser/internal/tmpl"
    19  	"github.com/goreleaser/goreleaser/pkg/config"
    20  	"github.com/goreleaser/goreleaser/pkg/context"
    21  )
    22  
    23  // ErrNoArchivesFound happens when 0 archives are found.
    24  var ErrNoArchivesFound = errors.New("no linux/macos archives found")
    25  
    26  // ErrMultipleArchivesSameOS happens when the config yields multiple archives
    27  // for linux or windows.
    28  var ErrMultipleArchivesSameOS = errors.New("one tap can handle only archive of an OS/Arch combination. Consider using ids in the brew section")
    29  
    30  // ErrTokenTypeNotImplementedForBrew indicates that a new token type was not implemented for this pipe.
    31  type ErrTokenTypeNotImplementedForBrew struct {
    32  	TokenType context.TokenType
    33  }
    34  
    35  func (e ErrTokenTypeNotImplementedForBrew) Error() string {
    36  	if e.TokenType != "" {
    37  		return fmt.Sprintf("token type %q not implemented for brew pipe", e.TokenType)
    38  	}
    39  	return "token type not implemented for brew pipe"
    40  }
    41  
    42  // Pipe for brew deployment.
    43  type Pipe struct{}
    44  
    45  func (Pipe) String() string {
    46  	return "homebrew tap formula"
    47  }
    48  
    49  // Publish brew formula.
    50  func (Pipe) Publish(ctx *context.Context) error {
    51  	// we keep GitHub as default for now, in line with releases
    52  	if string(ctx.TokenType) == "" {
    53  		ctx.TokenType = context.TokenTypeGitHub
    54  	}
    55  
    56  	cli, err := client.New(ctx)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	return publishAll(ctx, cli)
    61  }
    62  
    63  func publishAll(ctx *context.Context, cli client.Client) error {
    64  	// even if one of them skips, we run them all, and then show return the skips all at once.
    65  	// this is needed so we actually create the `dist/foo.rb` file, which is useful for debugging.
    66  	var skips = pipe.SkipMemento{}
    67  	for _, brew := range ctx.Config.Brews {
    68  		var err = doRun(ctx, brew, cli)
    69  		if err != nil && pipe.IsSkip(err) {
    70  			skips.Remember(err)
    71  			continue
    72  		}
    73  		if err != nil {
    74  			return err
    75  		}
    76  	}
    77  	return skips.Evaluate()
    78  }
    79  
    80  // Default sets the pipe defaults.
    81  func (Pipe) Default(ctx *context.Context) error {
    82  	for i := range ctx.Config.Brews {
    83  		var brew = &ctx.Config.Brews[i]
    84  
    85  		if brew.Install == "" {
    86  			brew.Install = fmt.Sprintf(`bin.install "%s"`, ctx.Config.ProjectName)
    87  			log.Warnf("optimistically guessing `brew[%d].install` to be `%s`", i, brew.Install)
    88  		}
    89  		if brew.CommitAuthor.Name == "" {
    90  			brew.CommitAuthor.Name = "goreleaserbot"
    91  		}
    92  		if brew.CommitAuthor.Email == "" {
    93  			brew.CommitAuthor.Email = "goreleaser@carlosbecker.com"
    94  		}
    95  		if brew.Name == "" {
    96  			brew.Name = ctx.Config.ProjectName
    97  		}
    98  		if brew.Goarm == "" {
    99  			brew.Goarm = "6"
   100  		}
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
   107  	if brew.Tap.Name == "" {
   108  		return pipe.Skip("brew section is not configured")
   109  	}
   110  
   111  	if brew.Tap.Token != "" {
   112  		token, err := tmpl.New(ctx).ApplySingleEnvOnly(brew.Tap.Token)
   113  		if err != nil {
   114  			return err
   115  		}
   116  		log.Debug("using custom token to publish homebrew formula")
   117  		c, err := client.NewWithToken(ctx, token)
   118  		if err != nil {
   119  			return err
   120  		}
   121  		cl = c
   122  	}
   123  
   124  	// TODO: properly cover this with tests
   125  	var filters = []artifact.Filter{
   126  		artifact.Or(
   127  			artifact.ByGoos("darwin"),
   128  			artifact.ByGoos("linux"),
   129  		),
   130  		artifact.ByFormats("zip", "tar.gz"),
   131  		artifact.Or(
   132  			artifact.ByGoarch("amd64"),
   133  			artifact.ByGoarch("arm64"),
   134  			artifact.And(
   135  				artifact.ByGoarch("arm"),
   136  				artifact.ByGoarm(brew.Goarm),
   137  			),
   138  		),
   139  		artifact.ByType(artifact.UploadableArchive),
   140  	}
   141  	if len(brew.IDs) > 0 {
   142  		filters = append(filters, artifact.ByIDs(brew.IDs...))
   143  	}
   144  
   145  	var archives = ctx.Artifacts.Filter(artifact.And(filters...)).List()
   146  	if len(archives) == 0 {
   147  		return ErrNoArchivesFound
   148  	}
   149  
   150  	name, err := tmpl.New(ctx).Apply(brew.Name)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	brew.Name = name
   155  
   156  	content, err := buildFormula(ctx, brew, cl, archives)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	var filename = brew.Name + ".rb"
   162  	var path = filepath.Join(ctx.Config.Dist, filename)
   163  	log.WithField("formula", path).Info("writing")
   164  	if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil { //nolint: gosec
   165  		return fmt.Errorf("failed to write brew formula: %w", err)
   166  	}
   167  
   168  	if strings.TrimSpace(brew.SkipUpload) == "true" {
   169  		return pipe.Skip("brew.skip_upload is set")
   170  	}
   171  	if ctx.SkipPublish {
   172  		return pipe.ErrSkipPublishEnabled
   173  	}
   174  	if strings.TrimSpace(brew.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
   175  		return pipe.Skip("prerelease detected with 'auto' upload, skipping homebrew publish")
   176  	}
   177  
   178  	repo := client.RepoFromRef(brew.Tap)
   179  
   180  	var gpath = buildFormulaPath(brew.Folder, filename)
   181  	log.WithField("formula", gpath).
   182  		WithField("repo", repo.String()).
   183  		Info("pushing")
   184  
   185  	var msg = fmt.Sprintf("Brew formula update for %s version %s", ctx.Config.ProjectName, ctx.Git.CurrentTag)
   186  	return cl.CreateFile(ctx, brew.CommitAuthor, repo, []byte(content), gpath, msg)
   187  }
   188  
   189  func buildFormulaPath(folder, filename string) string {
   190  	return path.Join(folder, filename)
   191  }
   192  
   193  func buildFormula(ctx *context.Context, brew config.Homebrew, client client.Client, artifacts []*artifact.Artifact) (string, error) {
   194  	data, err := dataFor(ctx, brew, client, artifacts)
   195  	if err != nil {
   196  		return "", err
   197  	}
   198  	return doBuildFormula(ctx, data)
   199  }
   200  
   201  func doBuildFormula(ctx *context.Context, data templateData) (string, error) {
   202  	t, err := template.New(data.Name).Parse(formulaTemplate)
   203  	if err != nil {
   204  		return "", err
   205  	}
   206  	var out bytes.Buffer
   207  	if err := t.Execute(&out, data); err != nil {
   208  		return "", err
   209  	}
   210  
   211  	content, err := tmpl.New(ctx).Apply(out.String())
   212  	if err != nil {
   213  		return "", err
   214  	}
   215  	out.Reset()
   216  
   217  	// Sanitize the template output and get rid of trailing whitespace.
   218  	var (
   219  		r = strings.NewReader(content)
   220  		s = bufio.NewScanner(r)
   221  	)
   222  	for s.Scan() {
   223  		l := strings.TrimRight(s.Text(), " ")
   224  		_, _ = out.WriteString(l)
   225  		_ = out.WriteByte('\n')
   226  	}
   227  	if err := s.Err(); err != nil {
   228  		return "", err
   229  	}
   230  
   231  	return out.String(), nil
   232  }
   233  
   234  func dataFor(ctx *context.Context, cfg config.Homebrew, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
   235  	var result = templateData{
   236  		Name:             formulaNameFor(cfg.Name),
   237  		Desc:             cfg.Description,
   238  		Homepage:         cfg.Homepage,
   239  		Version:          ctx.Version,
   240  		License:          cfg.License,
   241  		Caveats:          split(cfg.Caveats),
   242  		Dependencies:     cfg.Dependencies,
   243  		Conflicts:        cfg.Conflicts,
   244  		Plist:            cfg.Plist,
   245  		Install:          split(cfg.Install),
   246  		PostInstall:      cfg.PostInstall,
   247  		Tests:            split(cfg.Test),
   248  		DownloadStrategy: cfg.DownloadStrategy,
   249  		CustomRequire:    cfg.CustomRequire,
   250  		CustomBlock:      split(cfg.CustomBlock),
   251  	}
   252  
   253  	for _, artifact := range artifacts {
   254  		sum, err := artifact.Checksum("sha256")
   255  		if err != nil {
   256  			return result, err
   257  		}
   258  
   259  		if cfg.URLTemplate == "" {
   260  			url, err := cl.ReleaseURLTemplate(ctx)
   261  			if err != nil {
   262  				if client.IsNotImplementedErr(err) {
   263  					return result, ErrTokenTypeNotImplementedForBrew{ctx.TokenType}
   264  				}
   265  				return result, err
   266  			}
   267  			cfg.URLTemplate = url
   268  		}
   269  		url, err := tmpl.New(ctx).WithArtifact(artifact, map[string]string{}).Apply(cfg.URLTemplate)
   270  		if err != nil {
   271  			return result, err
   272  		}
   273  		var down = downloadable{
   274  			DownloadURL: url,
   275  			SHA256:      sum,
   276  		}
   277  		// TODO: refactor
   278  		if artifact.Goos == "darwin" { // nolint: nestif
   279  			if result.MacOS.DownloadURL != "" {
   280  				return result, ErrMultipleArchivesSameOS
   281  			}
   282  			result.MacOS = down
   283  		} else if artifact.Goos == "linux" {
   284  			switch artifact.Goarch {
   285  			case "amd64":
   286  				if result.LinuxAmd64.DownloadURL != "" {
   287  					return result, ErrMultipleArchivesSameOS
   288  				}
   289  				result.LinuxAmd64 = down
   290  			case "arm":
   291  				if result.LinuxArm.DownloadURL != "" {
   292  					return result, ErrMultipleArchivesSameOS
   293  				}
   294  				result.LinuxArm = down
   295  			case "arm64":
   296  				if result.LinuxArm64.DownloadURL != "" {
   297  					return result, ErrMultipleArchivesSameOS
   298  				}
   299  				result.LinuxArm64 = down
   300  			}
   301  		}
   302  	}
   303  
   304  	return result, nil
   305  }
   306  
   307  func split(s string) []string {
   308  	strings := strings.Split(strings.TrimSpace(s), "\n")
   309  	if len(strings) == 1 && strings[0] == "" {
   310  		return []string{}
   311  	}
   312  	return strings
   313  }
   314  
   315  func formulaNameFor(name string) string {
   316  	name = strings.ReplaceAll(name, "-", " ")
   317  	name = strings.ReplaceAll(name, "_", " ")
   318  	name = strings.ReplaceAll(name, "@", "AT")
   319  	return strings.ReplaceAll(strings.Title(name), " ", "")
   320  }