1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-08 03:31:59 +02:00
goreleaser/internal/pipe/aur/aur.go
Carlos Alexandro Becker b0583c700b
feat: support GOAMD64 (#3016)
* feat: support GOAMD64

* fix: test

* wip

* wip: docker et al

* fix: archive format name

* test: added new test

* feat: nfpm amd4, mips et al

* chore: rm unused file

* fix: brew for multiple goamd64

* fix: krew

* feat: aur

* feat: krew

* docs: brew

* feat: gofis

* feat: scoop

* fix: docker filters

* fix: snapcraft

* fix: improve diff a bit

* fix: snapcraft name template
2022-04-11 22:43:22 -03:00

501 lines
12 KiB
Go

package aur
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"github.com/apex/log"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/client"
"github.com/goreleaser/goreleaser/internal/commitauthor"
"github.com/goreleaser/goreleaser/internal/git"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"golang.org/x/crypto/ssh"
)
const (
aurExtra = "AURConfig"
defaultSSHCommand = "ssh -i {{ .KeyPath }} -o StrictHostKeyChecking=accept-new -F /dev/null"
defaultCommitMsg = "Update to {{ .Tag }}"
)
var ErrNoArchivesFound = errors.New("no linux archives found")
// Pipe for arch linux's AUR pkgbuild.
type Pipe struct{}
func (Pipe) String() string { return "arch user repositories" }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.AURs) == 0 }
func (Pipe) Default(ctx *context.Context) error {
for i := range ctx.Config.AURs {
pkg := &ctx.Config.AURs[i]
pkg.CommitAuthor = commitauthor.Default(pkg.CommitAuthor)
if pkg.CommitMessageTemplate == "" {
pkg.CommitMessageTemplate = defaultCommitMsg
}
if pkg.Name == "" {
pkg.Name = ctx.Config.ProjectName
}
if !strings.HasSuffix(pkg.Name, "-bin") {
pkg.Name += "-bin"
}
if len(pkg.Conflicts) == 0 {
pkg.Conflicts = []string{ctx.Config.ProjectName}
}
if len(pkg.Provides) == 0 {
pkg.Provides = []string{ctx.Config.ProjectName}
}
if pkg.Rel == "" {
pkg.Rel = "1"
}
if pkg.GitSSHCommand == "" {
pkg.GitSSHCommand = defaultSSHCommand
}
if pkg.Goamd64 == "" {
pkg.Goamd64 = "v2"
}
}
return nil
}
func (Pipe) Run(ctx *context.Context) error {
cli, err := client.New(ctx)
if err != nil {
return err
}
return runAll(ctx, cli)
}
func runAll(ctx *context.Context, cli client.Client) error {
for _, aur := range ctx.Config.AURs {
err := doRun(ctx, aur, cli)
if err != nil {
return err
}
}
return nil
}
func doRun(ctx *context.Context, aur config.AUR, cl client.Client) error {
name, err := tmpl.New(ctx).Apply(aur.Name)
if err != nil {
return err
}
aur.Name = name
filters := []artifact.Filter{
artifact.ByGoos("linux"),
artifact.Or(
artifact.And(
artifact.ByGoarch("amd64"),
artifact.ByGoamd64(aur.Goamd64),
),
artifact.ByGoarch("arm64"),
artifact.ByGoarch("386"),
artifact.And(
artifact.ByGoarch("arm"),
artifact.Or(
artifact.ByGoarm("7"),
artifact.ByGoarm("6"),
),
),
),
artifact.Or(
artifact.ByType(artifact.UploadableArchive),
artifact.ByType(artifact.UploadableBinary),
),
}
if len(aur.IDs) > 0 {
filters = append(filters, artifact.ByIDs(aur.IDs...))
}
archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
if len(archives) == 0 {
return ErrNoArchivesFound
}
pkg, err := tmpl.New(ctx).Apply(aur.Package)
if err != nil {
return err
}
if strings.TrimSpace(pkg) == "" {
art := archives[0]
switch art.Type {
case artifact.UploadableBinary:
name := art.Name
bin := art.ExtraOr(artifact.ExtraBinary, art.Name).(string)
pkg = fmt.Sprintf(`install -Dm755 "./%s "${pkgdir}/usr/bin/%s"`, name, bin)
case artifact.UploadableArchive:
for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
pkg = fmt.Sprintf(`install -Dm755 "./%s" "${pkgdir}/usr/bin/%[1]s"`, bin)
break
}
}
log.Warnf("guessing package to be %q", pkg)
}
aur.Package = pkg
for _, info := range []struct {
name, tpl, ext string
kind artifact.Type
}{
{
name: "PKGBUILD",
tpl: aurTemplateData,
ext: ".pkgbuild",
kind: artifact.PkgBuild,
},
{
name: ".SRCINFO",
tpl: srcInfoTemplate,
ext: ".srcinfo",
kind: artifact.SrcInfo,
},
} {
pkgContent, err := buildPkgFile(ctx, aur, cl, archives, info.tpl)
if err != nil {
return err
}
path := filepath.Join(ctx.Config.Dist, "aur", aur.Name+info.ext)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to write %s: %w", info.kind, err)
}
log.WithField("file", path).Info("writing")
if err := os.WriteFile(path, []byte(pkgContent), 0o644); err != nil { //nolint: gosec
return fmt.Errorf("failed to write %s: %w", info.kind, err)
}
ctx.Artifacts.Add(&artifact.Artifact{
Name: info.name,
Path: path,
Type: info.kind,
Extra: map[string]interface{}{
aurExtra: aur,
artifact.ExtraID: aur.Name,
},
})
}
return nil
}
func buildPkgFile(ctx *context.Context, pkg config.AUR, client client.Client, artifacts []*artifact.Artifact, tpl string) (string, error) {
data, err := dataFor(ctx, pkg, client, artifacts)
if err != nil {
return "", err
}
return applyTemplate(ctx, tpl, data)
}
func fixLines(s string) string {
lines := strings.Split(s, "\n")
var result []string
for _, line := range lines {
l := strings.TrimSpace(line)
if l == "" {
result = append(result, "")
continue
}
result = append(result, " "+l)
}
return strings.Join(result, "\n")
}
func applyTemplate(ctx *context.Context, tpl string, data templateData) (string, error) {
t := template.Must(
template.New(data.Name).
Funcs(template.FuncMap{
"fixLines": fixLines,
"pkgArray": toPkgBuildArray,
}).
Parse(tpl),
)
var out bytes.Buffer
if err := t.Execute(&out, data); err != nil {
return "", err
}
content, err := tmpl.New(ctx).Apply(out.String())
if err != nil {
return "", err
}
out.Reset()
// Sanitize the template output and get rid of trailing whitespace.
var (
r = strings.NewReader(content)
s = bufio.NewScanner(r)
)
for s.Scan() {
l := strings.TrimRight(s.Text(), " ")
_, _ = out.WriteString(l)
_ = out.WriteByte('\n')
}
if err := s.Err(); err != nil {
return "", err
}
return out.String(), nil
}
func toPkgBuildArray(ss []string) string {
result := make([]string, 0, len(ss))
for _, s := range ss {
result = append(result, fmt.Sprintf("'%s'", s))
}
return strings.Join(result, " ")
}
func toPkgBuildArch(arch string) string {
switch arch {
case "amd64":
return "x86_64"
case "386":
return "i686"
case "arm64":
return "aarch64"
case "arm6":
return "armv6h"
case "arm7":
return "armv7h"
default:
return "invalid" // should never get here
}
}
func dataFor(ctx *context.Context, cfg config.AUR, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
result := templateData{
Name: cfg.Name,
Desc: cfg.Description,
Homepage: cfg.Homepage,
Version: fmt.Sprintf("%d.%d.%d", ctx.Semver.Major, ctx.Semver.Minor, ctx.Semver.Patch),
License: cfg.License,
Rel: cfg.Rel,
Maintainers: cfg.Maintainers,
Contributors: cfg.Contributors,
Provides: cfg.Provides,
Conflicts: cfg.Conflicts,
Depends: cfg.Depends,
OptDepends: cfg.OptDepends,
Package: cfg.Package,
}
for _, art := range artifacts {
sum, err := art.Checksum("sha256")
if err != nil {
return result, err
}
if cfg.URLTemplate == "" {
url, err := cl.ReleaseURLTemplate(ctx)
if err != nil {
return result, err
}
cfg.URLTemplate = url
}
url, err := tmpl.New(ctx).WithArtifact(art, map[string]string{}).Apply(cfg.URLTemplate)
if err != nil {
return result, err
}
releasePackage := releasePackage{
DownloadURL: url,
SHA256: sum,
Arch: toPkgBuildArch(art.Goarch + art.Goarm),
Format: art.ExtraOr(artifact.ExtraFormat, "").(string),
}
result.ReleasePackages = append(result.ReleasePackages, releasePackage)
result.Arches = append(result.Arches, releasePackage.Arch)
}
sort.Strings(result.Arches)
sort.Slice(result.ReleasePackages, func(i, j int) bool {
return result.ReleasePackages[i].Arch < result.ReleasePackages[j].Arch
})
return result, nil
}
// Publish the PKGBUILD and .SRCINFO files to the AUR repository.
func (Pipe) Publish(ctx *context.Context) error {
skips := pipe.SkipMemento{}
for _, pkgs := range ctx.Artifacts.Filter(
artifact.Or(
artifact.ByType(artifact.PkgBuild),
artifact.ByType(artifact.SrcInfo),
),
).GroupByID() {
err := doPublish(ctx, pkgs)
if err != nil && pipe.IsSkip(err) {
skips.Remember(err)
continue
}
if err != nil {
return err
}
}
return skips.Evaluate()
}
func doPublish(ctx *context.Context, pkgs []*artifact.Artifact) error {
cfg := pkgs[0].Extra[aurExtra].(config.AUR)
if strings.TrimSpace(cfg.SkipUpload) == "true" {
return pipe.Skip("aur.skip_upload is set")
}
if strings.TrimSpace(cfg.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
return pipe.Skip("prerelease detected with 'auto' upload, skipping aur publish")
}
key, err := tmpl.New(ctx).Apply(cfg.PrivateKey)
if err != nil {
return err
}
key, err = keyPath(key)
if err != nil {
return err
}
url, err := tmpl.New(ctx).Apply(cfg.GitURL)
if err != nil {
return err
}
if url == "" {
return pipe.Skip("aur.git_url is empty")
}
sshcmd, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
"KeyPath": key,
}).Apply(cfg.GitSSHCommand)
if err != nil {
return err
}
msg, err := tmpl.New(ctx).Apply(cfg.CommitMessageTemplate)
if err != nil {
return err
}
author, err := commitauthor.Get(ctx, cfg.CommitAuthor)
if err != nil {
return err
}
parent := filepath.Join(ctx.Config.Dist, "aur", "repos")
cwd := filepath.Join(parent, cfg.Name)
if err := os.MkdirAll(parent, 0o755); err != nil {
return err
}
env := []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", sshcmd)}
if err := runGitCmds(parent, env, [][]string{
{"clone", url, cfg.Name},
}); err != nil {
return fmt.Errorf("failed to setup local AUR repo: %w", err)
}
if err := runGitCmds(cwd, env, [][]string{
// setup auth et al
{"config", "--local", "user.name", author.Name},
{"config", "--local", "user.email", author.Email},
{"config", "--local", "commit.gpgSign", "false"},
{"config", "--local", "init.defaultBranch", "master"},
}); err != nil {
return fmt.Errorf("failed to setup local AUR repo: %w", err)
}
for _, pkg := range pkgs {
bts, err := os.ReadFile(pkg.Path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", pkg.Name, err)
}
if err := os.WriteFile(filepath.Join(cwd, pkg.Name), bts, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", pkg.Name, err)
}
}
log.WithField("repo", url).WithField("name", cfg.Name).Info("pushing")
if err := runGitCmds(cwd, env, [][]string{
{"add", "-A", "."},
{"commit", "-m", msg},
{"push", "origin", "HEAD"},
}); err != nil {
return fmt.Errorf("failed to push %q (%q): %w", cfg.Name, url, err)
}
return nil
}
func keyPath(key string) (string, error) {
if key == "" {
return "", pipe.Skip("aur.private_key is empty")
}
path := key
if _, err := ssh.ParsePrivateKey([]byte(key)); err == nil {
// if it can be parsed as a valid private key, we write it to a
// temp file and use that path on GIT_SSH_COMMAND.
f, err := os.CreateTemp("", "id_*")
if err != nil {
return "", fmt.Errorf("failed to store private key: %w", err)
}
defer f.Close()
// the key needs to EOF at an empty line, seems like github actions
// is somehow removing them.
if !strings.HasSuffix(key, "\n") {
key += "\n"
}
if _, err := f.Write([]byte(key)); err != nil {
return "", fmt.Errorf("failed to store private key: %w", err)
}
if err := f.Close(); err != nil {
return "", fmt.Errorf("failed to store private key: %w", err)
}
path = f.Name()
}
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("could not stat aur.private_key: %w", err)
}
// in any case, ensure the key has the correct permissions.
if err := os.Chmod(path, 0o600); err != nil {
return "", fmt.Errorf("failed to ensure aur.private_key permissions: %w", err)
}
return path, nil
}
func runGitCmds(cwd string, env []string, cmds [][]string) error {
for _, cmd := range cmds {
args := append([]string{"-C", cwd}, cmd...)
if _, err := git.Clean(git.RunWithEnv(env, args...)); err != nil {
return fmt.Errorf("%q failed: %w", strings.Join(cmd, " "), err)
}
}
return nil
}