github.com/windmeup/goreleaser@v1.21.95/internal/pipe/aur/aur.go (about)

     1  package aur
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  	"text/template"
    13  
    14  	"github.com/caarlos0/log"
    15  	"github.com/windmeup/goreleaser/internal/artifact"
    16  	"github.com/windmeup/goreleaser/internal/client"
    17  	"github.com/windmeup/goreleaser/internal/commitauthor"
    18  	"github.com/windmeup/goreleaser/internal/pipe"
    19  	"github.com/windmeup/goreleaser/internal/skips"
    20  	"github.com/windmeup/goreleaser/internal/tmpl"
    21  	"github.com/windmeup/goreleaser/pkg/config"
    22  	"github.com/windmeup/goreleaser/pkg/context"
    23  )
    24  
    25  const (
    26  	aurExtra         = "AURConfig"
    27  	defaultCommitMsg = "Update to {{ .Tag }}"
    28  )
    29  
    30  var ErrNoArchivesFound = errors.New("no linux archives found")
    31  
    32  // Pipe for arch linux's AUR pkgbuild.
    33  type Pipe struct{}
    34  
    35  func (Pipe) String() string        { return "arch user repositories" }
    36  func (Pipe) ContinueOnError() bool { return true }
    37  func (Pipe) Skip(ctx *context.Context) bool {
    38  	return skips.Any(ctx, skips.AUR) || len(ctx.Config.AURs) == 0
    39  }
    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.Goamd64 == "" {
    65  			pkg.Goamd64 = "v1"
    66  		}
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  func (Pipe) Run(ctx *context.Context) error {
    73  	cli, err := client.NewReleaseClient(ctx)
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	return runAll(ctx, cli)
    79  }
    80  
    81  func runAll(ctx *context.Context, cli client.ReleaseURLTemplater) error {
    82  	for _, aur := range ctx.Config.AURs {
    83  		err := doRun(ctx, aur, cli)
    84  		if err != nil {
    85  			return err
    86  		}
    87  	}
    88  	return nil
    89  }
    90  
    91  func doRun(ctx *context.Context, aur config.AUR, cl client.ReleaseURLTemplater) error {
    92  	name, err := tmpl.New(ctx).Apply(aur.Name)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	aur.Name = name
    97  
    98  	filters := []artifact.Filter{
    99  		artifact.ByGoos("linux"),
   100  		artifact.Or(
   101  			artifact.And(
   102  				artifact.ByGoarch("amd64"),
   103  				artifact.ByGoamd64(aur.Goamd64),
   104  			),
   105  			artifact.ByGoarch("arm64"),
   106  			artifact.ByGoarch("386"),
   107  			artifact.And(
   108  				artifact.ByGoarch("arm"),
   109  				artifact.Or(
   110  					artifact.ByGoarm("7"),
   111  					artifact.ByGoarm("6"),
   112  				),
   113  			),
   114  		),
   115  		artifact.Or(
   116  			artifact.ByType(artifact.UploadableArchive),
   117  			artifact.ByType(artifact.UploadableBinary),
   118  		),
   119  	}
   120  	if len(aur.IDs) > 0 {
   121  		filters = append(filters, artifact.ByIDs(aur.IDs...))
   122  	}
   123  
   124  	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
   125  	if len(archives) == 0 {
   126  		return ErrNoArchivesFound
   127  	}
   128  
   129  	pkg, err := tmpl.New(ctx).Apply(aur.Package)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	if strings.TrimSpace(pkg) == "" {
   134  		art := archives[0]
   135  		switch art.Type {
   136  		case artifact.UploadableBinary:
   137  			name := art.Name
   138  			bin := artifact.ExtraOr(*art, artifact.ExtraBinary, art.Name)
   139  			pkg = fmt.Sprintf(`install -Dm755 "./%s "${pkgdir}/usr/bin/%s"`, name, bin)
   140  		case artifact.UploadableArchive:
   141  			for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) {
   142  				pkg = fmt.Sprintf(`install -Dm755 "./%s" "${pkgdir}/usr/bin/%[1]s"`, bin)
   143  				break
   144  			}
   145  		}
   146  		log.Warnf("guessing package to be %q", pkg)
   147  	}
   148  	aur.Package = pkg
   149  
   150  	for _, info := range []struct {
   151  		name, tpl, ext string
   152  		kind           artifact.Type
   153  	}{
   154  		{
   155  			name: "PKGBUILD",
   156  			tpl:  aurTemplateData,
   157  			ext:  ".pkgbuild",
   158  			kind: artifact.PkgBuild,
   159  		},
   160  		{
   161  			name: ".SRCINFO",
   162  			tpl:  srcInfoTemplate,
   163  			ext:  ".srcinfo",
   164  			kind: artifact.SrcInfo,
   165  		},
   166  	} {
   167  		pkgContent, err := buildPkgFile(ctx, aur, cl, archives, info.tpl)
   168  		if err != nil {
   169  			return err
   170  		}
   171  
   172  		path := filepath.Join(ctx.Config.Dist, "aur", aur.Name+info.ext)
   173  		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
   174  			return fmt.Errorf("failed to write %s: %w", info.kind, err)
   175  		}
   176  		log.WithField("file", path).Info("writing")
   177  		if err := os.WriteFile(path, []byte(pkgContent), 0o644); err != nil { // nolint: gosec
   178  			return fmt.Errorf("failed to write %s: %w", info.kind, err)
   179  		}
   180  
   181  		ctx.Artifacts.Add(&artifact.Artifact{
   182  			Name: info.name,
   183  			Path: path,
   184  			Type: info.kind,
   185  			Extra: map[string]interface{}{
   186  				aurExtra:         aur,
   187  				artifact.ExtraID: aur.Name,
   188  			},
   189  		})
   190  	}
   191  
   192  	return nil
   193  }
   194  
   195  func buildPkgFile(ctx *context.Context, pkg config.AUR, client client.ReleaseURLTemplater, artifacts []*artifact.Artifact, tpl string) (string, error) {
   196  	data, err := dataFor(ctx, pkg, client, artifacts)
   197  	if err != nil {
   198  		return "", err
   199  	}
   200  	return applyTemplate(ctx, tpl, data)
   201  }
   202  
   203  func fixLines(s string) string {
   204  	lines := strings.Split(s, "\n")
   205  	var result []string
   206  	for _, line := range lines {
   207  		l := strings.TrimSpace(line)
   208  		if l == "" {
   209  			result = append(result, "")
   210  			continue
   211  		}
   212  		result = append(result, "  "+l)
   213  	}
   214  	return strings.Join(result, "\n")
   215  }
   216  
   217  func applyTemplate(ctx *context.Context, tpl string, data templateData) (string, error) {
   218  	t := template.Must(
   219  		template.New(data.Name).
   220  			Funcs(template.FuncMap{
   221  				"fixLines": fixLines,
   222  				"pkgArray": toPkgBuildArray,
   223  			}).
   224  			Parse(tpl),
   225  	)
   226  
   227  	var out bytes.Buffer
   228  	if err := t.Execute(&out, data); err != nil {
   229  		return "", err
   230  	}
   231  
   232  	content, err := tmpl.New(ctx).Apply(out.String())
   233  	if err != nil {
   234  		return "", err
   235  	}
   236  	out.Reset()
   237  
   238  	// Sanitize the template output and get rid of trailing whitespace.
   239  	var (
   240  		r = strings.NewReader(content)
   241  		s = bufio.NewScanner(r)
   242  	)
   243  	for s.Scan() {
   244  		l := strings.TrimRight(s.Text(), " ")
   245  		_, _ = out.WriteString(l)
   246  		_ = out.WriteByte('\n')
   247  	}
   248  	if err := s.Err(); err != nil {
   249  		return "", err
   250  	}
   251  
   252  	return out.String(), nil
   253  }
   254  
   255  func toPkgBuildArray(ss []string) string {
   256  	result := make([]string, 0, len(ss))
   257  	for _, s := range ss {
   258  		result = append(result, fmt.Sprintf("'%s'", s))
   259  	}
   260  	return strings.Join(result, " ")
   261  }
   262  
   263  func toPkgBuildArch(arch string) string {
   264  	switch arch {
   265  	case "amd64":
   266  		return "x86_64"
   267  	case "386":
   268  		return "i686"
   269  	case "arm64":
   270  		return "aarch64"
   271  	case "arm6":
   272  		return "armv6h"
   273  	case "arm7":
   274  		return "armv7h"
   275  	default:
   276  		return "invalid" // should never get here
   277  	}
   278  }
   279  
   280  func dataFor(ctx *context.Context, cfg config.AUR, cl client.ReleaseURLTemplater, artifacts []*artifact.Artifact) (templateData, error) {
   281  	result := templateData{
   282  		Name:         cfg.Name,
   283  		Desc:         cfg.Description,
   284  		Homepage:     cfg.Homepage,
   285  		Version:      fmt.Sprintf("%d.%d.%d", ctx.Semver.Major, ctx.Semver.Minor, ctx.Semver.Patch),
   286  		License:      cfg.License,
   287  		Rel:          cfg.Rel,
   288  		Maintainers:  cfg.Maintainers,
   289  		Contributors: cfg.Contributors,
   290  		Provides:     cfg.Provides,
   291  		Conflicts:    cfg.Conflicts,
   292  		Backup:       cfg.Backup,
   293  		Depends:      cfg.Depends,
   294  		OptDepends:   cfg.OptDepends,
   295  		Package:      cfg.Package,
   296  	}
   297  
   298  	for _, art := range artifacts {
   299  		sum, err := art.Checksum("sha256")
   300  		if err != nil {
   301  			return result, err
   302  		}
   303  
   304  		if cfg.URLTemplate == "" {
   305  			url, err := cl.ReleaseURLTemplate(ctx)
   306  			if err != nil {
   307  				return result, err
   308  			}
   309  			cfg.URLTemplate = url
   310  		}
   311  		url, err := tmpl.New(ctx).WithArtifact(art).Apply(cfg.URLTemplate)
   312  		if err != nil {
   313  			return result, err
   314  		}
   315  
   316  		releasePackage := releasePackage{
   317  			DownloadURL: url,
   318  			SHA256:      sum,
   319  			Arch:        toPkgBuildArch(art.Goarch + art.Goarm),
   320  			Format:      artifact.ExtraOr(*art, artifact.ExtraFormat, ""),
   321  		}
   322  		result.ReleasePackages = append(result.ReleasePackages, releasePackage)
   323  		result.Arches = append(result.Arches, releasePackage.Arch)
   324  	}
   325  
   326  	sort.Strings(result.Arches)
   327  	sort.Slice(result.ReleasePackages, func(i, j int) bool {
   328  		return result.ReleasePackages[i].Arch < result.ReleasePackages[j].Arch
   329  	})
   330  	return result, nil
   331  }
   332  
   333  // Publish the PKGBUILD and .SRCINFO files to the AUR repository.
   334  func (Pipe) Publish(ctx *context.Context) error {
   335  	skips := pipe.SkipMemento{}
   336  	for _, pkgs := range ctx.Artifacts.Filter(
   337  		artifact.Or(
   338  			artifact.ByType(artifact.PkgBuild),
   339  			artifact.ByType(artifact.SrcInfo),
   340  		),
   341  	).GroupByID() {
   342  		err := doPublish(ctx, pkgs)
   343  		if err != nil && pipe.IsSkip(err) {
   344  			skips.Remember(err)
   345  			continue
   346  		}
   347  		if err != nil {
   348  			return err
   349  		}
   350  	}
   351  	return skips.Evaluate()
   352  }
   353  
   354  func doPublish(ctx *context.Context, pkgs []*artifact.Artifact) error {
   355  	cfg, err := artifact.Extra[config.AUR](*pkgs[0], aurExtra)
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	if strings.TrimSpace(cfg.SkipUpload) == "true" {
   361  		return pipe.Skip("aur.skip_upload is set")
   362  	}
   363  
   364  	if strings.TrimSpace(cfg.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
   365  		return pipe.Skip("prerelease detected with 'auto' upload, skipping aur publish")
   366  	}
   367  
   368  	author, err := commitauthor.Get(ctx, cfg.CommitAuthor)
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	msg, err := tmpl.New(ctx).Apply(cfg.CommitMessageTemplate)
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	cli := client.NewGitUploadClient("master")
   379  	repo := client.RepoFromRef(config.RepoRef{
   380  		Git: config.GitRepoRef{
   381  			PrivateKey: cfg.PrivateKey,
   382  			URL:        cfg.GitURL,
   383  			SSHCommand: cfg.GitSSHCommand,
   384  		},
   385  		Name: cfg.Name,
   386  	})
   387  
   388  	files := make([]client.RepoFile, 0, len(pkgs))
   389  	for _, pkg := range pkgs {
   390  		content, err := os.ReadFile(pkg.Path)
   391  		if err != nil {
   392  			return err
   393  		}
   394  		files = append(files, client.RepoFile{
   395  			Path:    pkg.Name,
   396  			Content: content,
   397  		})
   398  	}
   399  	return cli.CreateFiles(ctx, author, repo, msg, files)
   400  }