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

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