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 }