github.com/triarius/goreleaser@v1.12.5/internal/pipe/aur/aur.go (about) 1 package aur 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "sort" 12 "strings" 13 "text/template" 14 15 "github.com/caarlos0/log" 16 "github.com/triarius/goreleaser/internal/artifact" 17 "github.com/triarius/goreleaser/internal/client" 18 "github.com/triarius/goreleaser/internal/commitauthor" 19 "github.com/triarius/goreleaser/internal/git" 20 "github.com/triarius/goreleaser/internal/pipe" 21 "github.com/triarius/goreleaser/internal/tmpl" 22 "github.com/triarius/goreleaser/pkg/config" 23 "github.com/triarius/goreleaser/pkg/context" 24 "golang.org/x/crypto/ssh" 25 ) 26 27 const ( 28 aurExtra = "AURConfig" 29 defaultSSHCommand = "ssh -i {{ .KeyPath }} -o StrictHostKeyChecking=accept-new -F /dev/null" 30 defaultCommitMsg = "Update to {{ .Tag }}" 31 ) 32 33 var ErrNoArchivesFound = errors.New("no linux archives found") 34 35 // Pipe for arch linux's AUR pkgbuild. 36 type Pipe struct{} 37 38 func (Pipe) String() string { return "arch user repositories" } 39 func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.AURs) == 0 } 40 41 func (Pipe) Default(ctx *context.Context) error { 42 for i := range ctx.Config.AURs { 43 pkg := &ctx.Config.AURs[i] 44 45 pkg.CommitAuthor = commitauthor.Default(pkg.CommitAuthor) 46 if pkg.CommitMessageTemplate == "" { 47 pkg.CommitMessageTemplate = defaultCommitMsg 48 } 49 if pkg.Name == "" { 50 pkg.Name = ctx.Config.ProjectName 51 } 52 if !strings.HasSuffix(pkg.Name, "-bin") { 53 pkg.Name += "-bin" 54 } 55 if len(pkg.Conflicts) == 0 { 56 pkg.Conflicts = []string{ctx.Config.ProjectName} 57 } 58 if len(pkg.Provides) == 0 { 59 pkg.Provides = []string{ctx.Config.ProjectName} 60 } 61 if pkg.Rel == "" { 62 pkg.Rel = "1" 63 } 64 if pkg.GitSSHCommand == "" { 65 pkg.GitSSHCommand = defaultSSHCommand 66 } 67 if pkg.Goamd64 == "" { 68 pkg.Goamd64 = "v1" 69 } 70 } 71 72 return nil 73 } 74 75 func (Pipe) Run(ctx *context.Context) error { 76 cli, err := client.New(ctx) 77 if err != nil { 78 return err 79 } 80 81 return runAll(ctx, cli) 82 } 83 84 func runAll(ctx *context.Context, cli client.Client) error { 85 for _, aur := range ctx.Config.AURs { 86 err := doRun(ctx, aur, cli) 87 if err != nil { 88 return err 89 } 90 } 91 return nil 92 } 93 94 func doRun(ctx *context.Context, aur config.AUR, cl client.Client) error { 95 name, err := tmpl.New(ctx).Apply(aur.Name) 96 if err != nil { 97 return err 98 } 99 aur.Name = name 100 101 filters := []artifact.Filter{ 102 artifact.ByGoos("linux"), 103 artifact.Or( 104 artifact.And( 105 artifact.ByGoarch("amd64"), 106 artifact.ByGoamd64(aur.Goamd64), 107 ), 108 artifact.ByGoarch("arm64"), 109 artifact.ByGoarch("386"), 110 artifact.And( 111 artifact.ByGoarch("arm"), 112 artifact.Or( 113 artifact.ByGoarm("7"), 114 artifact.ByGoarm("6"), 115 ), 116 ), 117 ), 118 artifact.Or( 119 artifact.ByType(artifact.UploadableArchive), 120 artifact.ByType(artifact.UploadableBinary), 121 ), 122 } 123 if len(aur.IDs) > 0 { 124 filters = append(filters, artifact.ByIDs(aur.IDs...)) 125 } 126 127 archives := ctx.Artifacts.Filter(artifact.And(filters...)).List() 128 if len(archives) == 0 { 129 return ErrNoArchivesFound 130 } 131 132 pkg, err := tmpl.New(ctx).Apply(aur.Package) 133 if err != nil { 134 return err 135 } 136 if strings.TrimSpace(pkg) == "" { 137 art := archives[0] 138 switch art.Type { 139 case artifact.UploadableBinary: 140 name := art.Name 141 bin := artifact.ExtraOr(*art, artifact.ExtraBinary, art.Name) 142 pkg = fmt.Sprintf(`install -Dm755 "./%s "${pkgdir}/usr/bin/%s"`, name, bin) 143 case artifact.UploadableArchive: 144 for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) { 145 pkg = fmt.Sprintf(`install -Dm755 "./%s" "${pkgdir}/usr/bin/%[1]s"`, bin) 146 break 147 } 148 } 149 log.Warnf("guessing package to be %q", pkg) 150 } 151 aur.Package = pkg 152 153 for _, info := range []struct { 154 name, tpl, ext string 155 kind artifact.Type 156 }{ 157 { 158 name: "PKGBUILD", 159 tpl: aurTemplateData, 160 ext: ".pkgbuild", 161 kind: artifact.PkgBuild, 162 }, 163 { 164 name: ".SRCINFO", 165 tpl: srcInfoTemplate, 166 ext: ".srcinfo", 167 kind: artifact.SrcInfo, 168 }, 169 } { 170 pkgContent, err := buildPkgFile(ctx, aur, cl, archives, info.tpl) 171 if err != nil { 172 return err 173 } 174 175 path := filepath.Join(ctx.Config.Dist, "aur", aur.Name+info.ext) 176 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 177 return fmt.Errorf("failed to write %s: %w", info.kind, err) 178 } 179 log.WithField("file", path).Info("writing") 180 if err := os.WriteFile(path, []byte(pkgContent), 0o644); err != nil { //nolint: gosec 181 return fmt.Errorf("failed to write %s: %w", info.kind, err) 182 } 183 184 ctx.Artifacts.Add(&artifact.Artifact{ 185 Name: info.name, 186 Path: path, 187 Type: info.kind, 188 Extra: map[string]interface{}{ 189 aurExtra: aur, 190 artifact.ExtraID: aur.Name, 191 }, 192 }) 193 } 194 195 return nil 196 } 197 198 func buildPkgFile(ctx *context.Context, pkg config.AUR, client client.Client, artifacts []*artifact.Artifact, tpl string) (string, error) { 199 data, err := dataFor(ctx, pkg, client, artifacts) 200 if err != nil { 201 return "", err 202 } 203 return applyTemplate(ctx, tpl, data) 204 } 205 206 func fixLines(s string) string { 207 lines := strings.Split(s, "\n") 208 var result []string 209 for _, line := range lines { 210 l := strings.TrimSpace(line) 211 if l == "" { 212 result = append(result, "") 213 continue 214 } 215 result = append(result, " "+l) 216 } 217 return strings.Join(result, "\n") 218 } 219 220 func applyTemplate(ctx *context.Context, tpl string, data templateData) (string, error) { 221 t := template.Must( 222 template.New(data.Name). 223 Funcs(template.FuncMap{ 224 "fixLines": fixLines, 225 "pkgArray": toPkgBuildArray, 226 }). 227 Parse(tpl), 228 ) 229 230 var out bytes.Buffer 231 if err := t.Execute(&out, data); err != nil { 232 return "", err 233 } 234 235 content, err := tmpl.New(ctx).Apply(out.String()) 236 if err != nil { 237 return "", err 238 } 239 out.Reset() 240 241 // Sanitize the template output and get rid of trailing whitespace. 242 var ( 243 r = strings.NewReader(content) 244 s = bufio.NewScanner(r) 245 ) 246 for s.Scan() { 247 l := strings.TrimRight(s.Text(), " ") 248 _, _ = out.WriteString(l) 249 _ = out.WriteByte('\n') 250 } 251 if err := s.Err(); err != nil { 252 return "", err 253 } 254 255 return out.String(), nil 256 } 257 258 func toPkgBuildArray(ss []string) string { 259 result := make([]string, 0, len(ss)) 260 for _, s := range ss { 261 result = append(result, fmt.Sprintf("'%s'", s)) 262 } 263 return strings.Join(result, " ") 264 } 265 266 func toPkgBuildArch(arch string) string { 267 switch arch { 268 case "amd64": 269 return "x86_64" 270 case "386": 271 return "i686" 272 case "arm64": 273 return "aarch64" 274 case "arm6": 275 return "armv6h" 276 case "arm7": 277 return "armv7h" 278 default: 279 return "invalid" // should never get here 280 } 281 } 282 283 func dataFor(ctx *context.Context, cfg config.AUR, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) { 284 result := templateData{ 285 Name: cfg.Name, 286 Desc: cfg.Description, 287 Homepage: cfg.Homepage, 288 Version: fmt.Sprintf("%d.%d.%d", ctx.Semver.Major, ctx.Semver.Minor, ctx.Semver.Patch), 289 License: cfg.License, 290 Rel: cfg.Rel, 291 Maintainers: cfg.Maintainers, 292 Contributors: cfg.Contributors, 293 Provides: cfg.Provides, 294 Conflicts: cfg.Conflicts, 295 Backup: cfg.Backup, 296 Depends: cfg.Depends, 297 OptDepends: cfg.OptDepends, 298 Package: cfg.Package, 299 } 300 301 for _, art := range artifacts { 302 sum, err := art.Checksum("sha256") 303 if err != nil { 304 return result, err 305 } 306 307 if cfg.URLTemplate == "" { 308 url, err := cl.ReleaseURLTemplate(ctx) 309 if err != nil { 310 return result, err 311 } 312 cfg.URLTemplate = url 313 } 314 url, err := tmpl.New(ctx).WithArtifact(art, map[string]string{}).Apply(cfg.URLTemplate) 315 if err != nil { 316 return result, err 317 } 318 319 releasePackage := releasePackage{ 320 DownloadURL: url, 321 SHA256: sum, 322 Arch: toPkgBuildArch(art.Goarch + art.Goarm), 323 Format: artifact.ExtraOr(*art, artifact.ExtraFormat, ""), 324 } 325 result.ReleasePackages = append(result.ReleasePackages, releasePackage) 326 result.Arches = append(result.Arches, releasePackage.Arch) 327 } 328 329 sort.Strings(result.Arches) 330 sort.Slice(result.ReleasePackages, func(i, j int) bool { 331 return result.ReleasePackages[i].Arch < result.ReleasePackages[j].Arch 332 }) 333 return result, nil 334 } 335 336 // Publish the PKGBUILD and .SRCINFO files to the AUR repository. 337 func (Pipe) Publish(ctx *context.Context) error { 338 skips := pipe.SkipMemento{} 339 for _, pkgs := range ctx.Artifacts.Filter( 340 artifact.Or( 341 artifact.ByType(artifact.PkgBuild), 342 artifact.ByType(artifact.SrcInfo), 343 ), 344 ).GroupByID() { 345 err := doPublish(ctx, pkgs) 346 if err != nil && pipe.IsSkip(err) { 347 skips.Remember(err) 348 continue 349 } 350 if err != nil { 351 return err 352 } 353 } 354 return skips.Evaluate() 355 } 356 357 func doPublish(ctx *context.Context, pkgs []*artifact.Artifact) error { 358 cfg, err := artifact.Extra[config.AUR](*pkgs[0], aurExtra) 359 if err != nil { 360 return err 361 } 362 363 if strings.TrimSpace(cfg.SkipUpload) == "true" { 364 return pipe.Skip("aur.skip_upload is set") 365 } 366 367 if strings.TrimSpace(cfg.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" { 368 return pipe.Skip("prerelease detected with 'auto' upload, skipping aur publish") 369 } 370 371 key, err := tmpl.New(ctx).Apply(cfg.PrivateKey) 372 if err != nil { 373 return err 374 } 375 376 key, err = keyPath(key) 377 if err != nil { 378 return err 379 } 380 381 url, err := tmpl.New(ctx).Apply(cfg.GitURL) 382 if err != nil { 383 return err 384 } 385 386 if url == "" { 387 return pipe.Skip("aur.git_url is empty") 388 } 389 390 sshcmd, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{ 391 "KeyPath": key, 392 }).Apply(cfg.GitSSHCommand) 393 if err != nil { 394 return err 395 } 396 397 msg, err := tmpl.New(ctx).Apply(cfg.CommitMessageTemplate) 398 if err != nil { 399 return err 400 } 401 402 author, err := commitauthor.Get(ctx, cfg.CommitAuthor) 403 if err != nil { 404 return err 405 } 406 407 parent := filepath.Join(ctx.Config.Dist, "aur", "repos") 408 cwd := filepath.Join(parent, cfg.Name) 409 410 if err := os.MkdirAll(parent, 0o755); err != nil { 411 return err 412 } 413 414 env := []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", sshcmd)} 415 416 if err := runGitCmds(ctx, parent, env, [][]string{ 417 {"clone", url, cfg.Name}, 418 }); err != nil { 419 return fmt.Errorf("failed to setup local AUR repo: %w", err) 420 } 421 422 if err := runGitCmds(ctx, cwd, env, [][]string{ 423 // setup auth et al 424 {"config", "--local", "user.name", author.Name}, 425 {"config", "--local", "user.email", author.Email}, 426 {"config", "--local", "commit.gpgSign", "false"}, 427 {"config", "--local", "init.defaultBranch", "master"}, 428 }); err != nil { 429 return fmt.Errorf("failed to setup local AUR repo: %w", err) 430 } 431 432 for _, pkg := range pkgs { 433 bts, err := os.ReadFile(pkg.Path) 434 if err != nil { 435 return fmt.Errorf("failed to read %s: %w", pkg.Name, err) 436 } 437 438 if err := os.WriteFile(filepath.Join(cwd, pkg.Name), bts, 0o644); err != nil { 439 return fmt.Errorf("failed to write %s: %w", pkg.Name, err) 440 } 441 } 442 443 log.WithField("repo", url).WithField("name", cfg.Name).Info("pushing") 444 if err := runGitCmds(ctx, cwd, env, [][]string{ 445 {"add", "-A", "."}, 446 {"commit", "-m", msg}, 447 {"push", "origin", "HEAD"}, 448 }); err != nil { 449 return fmt.Errorf("failed to push %q (%q): %w", cfg.Name, url, err) 450 } 451 452 return nil 453 } 454 455 func keyPath(key string) (string, error) { 456 if key == "" { 457 return "", pipe.Skip("aur.private_key is empty") 458 } 459 460 path := key 461 if _, err := ssh.ParsePrivateKey([]byte(key)); err == nil { 462 // if it can be parsed as a valid private key, we write it to a 463 // temp file and use that path on GIT_SSH_COMMAND. 464 f, err := os.CreateTemp("", "id_*") 465 if err != nil { 466 return "", fmt.Errorf("failed to store private key: %w", err) 467 } 468 defer f.Close() 469 470 // the key needs to EOF at an empty line, seems like github actions 471 // is somehow removing them. 472 if !strings.HasSuffix(key, "\n") { 473 key += "\n" 474 } 475 476 if _, err := io.WriteString(f, key); err != nil { 477 return "", fmt.Errorf("failed to store private key: %w", err) 478 } 479 if err := f.Close(); err != nil { 480 return "", fmt.Errorf("failed to store private key: %w", err) 481 } 482 path = f.Name() 483 } 484 485 if _, err := os.Stat(path); err != nil { 486 return "", fmt.Errorf("could not stat aur.private_key: %w", err) 487 } 488 489 // in any case, ensure the key has the correct permissions. 490 if err := os.Chmod(path, 0o600); err != nil { 491 return "", fmt.Errorf("failed to ensure aur.private_key permissions: %w", err) 492 } 493 494 return path, nil 495 } 496 497 func runGitCmds(ctx *context.Context, cwd string, env []string, cmds [][]string) error { 498 for _, cmd := range cmds { 499 args := append([]string{"-C", cwd}, cmd...) 500 if _, err := git.Clean(git.RunWithEnv(ctx, env, args...)); err != nil { 501 return fmt.Errorf("%q failed: %w", strings.Join(cmd, " "), err) 502 } 503 } 504 return nil 505 }