mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-08 03:31:59 +02:00
b0583c700b
* 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
501 lines
12 KiB
Go
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
|
|
}
|