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

     1  package nix
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"path"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  	"text/template"
    15  
    16  	"github.com/caarlos0/log"
    17  	"github.com/windmeup/goreleaser/internal/artifact"
    18  	"github.com/windmeup/goreleaser/internal/client"
    19  	"github.com/windmeup/goreleaser/internal/commitauthor"
    20  	"github.com/windmeup/goreleaser/internal/pipe"
    21  	"github.com/windmeup/goreleaser/internal/skips"
    22  	"github.com/windmeup/goreleaser/internal/tmpl"
    23  	"github.com/windmeup/goreleaser/pkg/config"
    24  	"github.com/windmeup/goreleaser/pkg/context"
    25  )
    26  
    27  const nixConfigExtra = "NixConfig"
    28  
    29  // ErrMultipleArchivesSamePlatform happens when the config yields multiple
    30  // archives for the same platform.
    31  var ErrMultipleArchivesSamePlatform = errors.New("one nixpkg can handle only one archive of each OS/Arch combination")
    32  
    33  type errNoArchivesFound struct {
    34  	goamd64 string
    35  	ids     []string
    36  }
    37  
    38  func (e errNoArchivesFound) Error() string {
    39  	return fmt.Sprintf("no archives found matching goos=[darwin linux] goarch=[amd64 arm arm64 386] goarm=[6 7] goamd64=%s ids=%v", e.goamd64, e.ids)
    40  }
    41  
    42  var (
    43  	errNoRepoName     = pipe.Skip("repository name is not set")
    44  	errSkipUpload     = pipe.Skip("nix.skip_upload is set")
    45  	errSkipUploadAuto = pipe.Skip("nix.skip_upload is set to 'auto', and current version is a pre-release")
    46  )
    47  
    48  // NewBuild returns a pipe to be used in the build phase.
    49  func NewBuild() Pipe {
    50  	return Pipe{buildShaPrefetcher{}}
    51  }
    52  
    53  // NewPublish returns a pipe to be used in the publish phase.
    54  func NewPublish() Pipe {
    55  	return Pipe{publishShaPrefetcher{
    56  		bin: nixPrefetchURLBin,
    57  	}}
    58  }
    59  
    60  type Pipe struct {
    61  	prefetcher shaPrefetcher
    62  }
    63  
    64  func (Pipe) String() string                           { return "nixpkgs" }
    65  func (Pipe) ContinueOnError() bool                    { return true }
    66  func (Pipe) Dependencies(_ *context.Context) []string { return []string{"nix-prefetch-url"} }
    67  func (p Pipe) Skip(ctx *context.Context) bool {
    68  	return skips.Any(ctx, skips.Nix) || len(ctx.Config.Nix) == 0 || !p.prefetcher.Available()
    69  }
    70  
    71  func (Pipe) Default(ctx *context.Context) error {
    72  	for i := range ctx.Config.Nix {
    73  		nix := &ctx.Config.Nix[i]
    74  
    75  		nix.CommitAuthor = commitauthor.Default(nix.CommitAuthor)
    76  
    77  		if nix.CommitMessageTemplate == "" {
    78  			nix.CommitMessageTemplate = "{{ .ProjectName }}: {{ .PreviousTag }} -> {{ .Tag }}"
    79  		}
    80  		if nix.Name == "" {
    81  			nix.Name = ctx.Config.ProjectName
    82  		}
    83  		if nix.Goamd64 == "" {
    84  			nix.Goamd64 = "v1"
    85  		}
    86  	}
    87  
    88  	return nil
    89  }
    90  
    91  func (p Pipe) Run(ctx *context.Context) error {
    92  	cli, err := client.NewReleaseClient(ctx)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	return p.runAll(ctx, cli)
    98  }
    99  
   100  // Publish .
   101  func (p Pipe) Publish(ctx *context.Context) error {
   102  	cli, err := client.New(ctx)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	return p.publishAll(ctx, cli)
   107  }
   108  
   109  func (p Pipe) runAll(ctx *context.Context, cli client.ReleaseURLTemplater) error {
   110  	for _, nix := range ctx.Config.Nix {
   111  		err := p.doRun(ctx, nix, cli)
   112  		if err != nil {
   113  			return err
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  func (p Pipe) publishAll(ctx *context.Context, cli client.Client) error {
   120  	skips := pipe.SkipMemento{}
   121  	for _, nix := range ctx.Artifacts.Filter(artifact.ByType(artifact.Nixpkg)).List() {
   122  		err := doPublish(ctx, p.prefetcher, cli, nix)
   123  		if err != nil && pipe.IsSkip(err) {
   124  			skips.Remember(err)
   125  			continue
   126  		}
   127  		if err != nil {
   128  			return err
   129  		}
   130  	}
   131  	return skips.Evaluate()
   132  }
   133  
   134  func (p Pipe) doRun(ctx *context.Context, nix config.Nix, cl client.ReleaseURLTemplater) error {
   135  	if nix.Repository.Name == "" {
   136  		return errNoRepoName
   137  	}
   138  
   139  	tp := tmpl.New(ctx)
   140  
   141  	err := tp.ApplyAll(
   142  		&nix.Name,
   143  		&nix.SkipUpload,
   144  		&nix.Homepage,
   145  		&nix.Description,
   146  		&nix.Path,
   147  	)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	nix.Repository, err = client.TemplateRef(tmpl.New(ctx).Apply, nix.Repository)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	if nix.Path == "" {
   158  		nix.Path = path.Join("pkgs", nix.Name, "default.nix")
   159  	}
   160  
   161  	path := filepath.Join(ctx.Config.Dist, "nix", nix.Path)
   162  	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
   163  		return err
   164  	}
   165  
   166  	content, err := preparePkg(ctx, nix, cl, p.prefetcher)
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	log.WithField("nixpkg", path).Info("writing")
   172  	if err := os.WriteFile(path, []byte(content), 0o644); err != nil { // nolint: gosec
   173  		return fmt.Errorf("failed to write nixpkg: %w", err)
   174  	}
   175  
   176  	ctx.Artifacts.Add(&artifact.Artifact{
   177  		Name: filepath.Base(path),
   178  		Path: path,
   179  		Type: artifact.Nixpkg,
   180  		Extra: map[string]interface{}{
   181  			nixConfigExtra: nix,
   182  		},
   183  	})
   184  
   185  	return nil
   186  }
   187  
   188  func preparePkg(
   189  	ctx *context.Context,
   190  	nix config.Nix,
   191  	cli client.ReleaseURLTemplater,
   192  	prefetcher shaPrefetcher,
   193  ) (string, error) {
   194  	filters := []artifact.Filter{
   195  		artifact.Or(
   196  			artifact.ByGoos("darwin"),
   197  			artifact.ByGoos("linux"),
   198  		),
   199  		artifact.Or(
   200  			artifact.And(
   201  				artifact.ByGoarch("amd64"),
   202  				artifact.ByGoamd64(nix.Goamd64),
   203  			),
   204  			artifact.And(
   205  				artifact.ByGoarch("arm"),
   206  				artifact.Or(
   207  					artifact.ByGoarm("6"),
   208  					artifact.ByGoarm("7"),
   209  				),
   210  			),
   211  			artifact.ByGoarch("arm64"),
   212  			artifact.ByGoarch("386"),
   213  			artifact.ByGoarch("all"),
   214  		),
   215  		artifact.And(
   216  			artifact.ByFormats("zip", "tar.gz"),
   217  			artifact.ByType(artifact.UploadableArchive),
   218  		),
   219  		artifact.OnlyReplacingUnibins,
   220  	}
   221  	if len(nix.IDs) > 0 {
   222  		filters = append(filters, artifact.ByIDs(nix.IDs...))
   223  	}
   224  
   225  	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
   226  	if len(archives) == 0 {
   227  		return "", errNoArchivesFound{
   228  			goamd64: nix.Goamd64,
   229  			ids:     nix.IDs,
   230  		}
   231  	}
   232  
   233  	if nix.URLTemplate == "" {
   234  		url, err := cli.ReleaseURLTemplate(ctx)
   235  		if err != nil {
   236  			return "", err
   237  		}
   238  		nix.URLTemplate = url
   239  	}
   240  
   241  	installs, err := installs(ctx, nix, archives[0])
   242  	if err != nil {
   243  		return "", err
   244  	}
   245  
   246  	postInstall, err := postInstall(ctx, nix, archives[0])
   247  	if err != nil {
   248  		return "", err
   249  	}
   250  
   251  	folder := artifact.ExtraOr(*archives[0], artifact.ExtraWrappedIn, ".")
   252  	if folder == "" {
   253  		folder = "."
   254  	}
   255  
   256  	inputs := []string{"installShellFiles"}
   257  	dependencies := depNames(nix.Dependencies)
   258  	if len(dependencies) > 0 {
   259  		inputs = append(inputs, "makeWrapper")
   260  	}
   261  	if archives[0].Format() == "zip" {
   262  		inputs = append(inputs, "unzip")
   263  		dependencies = append(dependencies, "unzip")
   264  	}
   265  
   266  	data := templateData{
   267  		Name:         nix.Name,
   268  		Version:      ctx.Version,
   269  		Install:      installs,
   270  		PostInstall:  postInstall,
   271  		Archives:     map[string]Archive{},
   272  		SourceRoot:   folder,
   273  		Description:  nix.Description,
   274  		Homepage:     nix.Homepage,
   275  		License:      nix.License,
   276  		Inputs:       inputs,
   277  		Dependencies: dependencies,
   278  	}
   279  
   280  	platforms := map[string]bool{}
   281  	for _, art := range archives {
   282  		url, err := tmpl.New(ctx).WithArtifact(art).Apply(nix.URLTemplate)
   283  		if err != nil {
   284  			return "", err
   285  		}
   286  		sha, err := prefetcher.Prefetch(url)
   287  		if err != nil {
   288  			return "", err
   289  		}
   290  		archive := Archive{
   291  			URL: url,
   292  			Sha: sha,
   293  		}
   294  
   295  		for _, goarch := range expandGoarch(art.Goarch) {
   296  			key := art.Goos + goarch + art.Goarm
   297  			if _, ok := data.Archives[key]; ok {
   298  				return "", ErrMultipleArchivesSamePlatform
   299  			}
   300  			data.Archives[key] = archive
   301  			plat := goosToPlatform[art.Goos+goarch+art.Goarm]
   302  			platforms[plat] = true
   303  		}
   304  	}
   305  	data.Platforms = keys(platforms)
   306  	sort.Strings(data.Platforms)
   307  
   308  	return doBuildPkg(ctx, data)
   309  }
   310  
   311  func expandGoarch(goarch string) []string {
   312  	if goarch == "all" {
   313  		return []string{"amd64", "arm64"}
   314  	}
   315  	return []string{goarch}
   316  }
   317  
   318  var goosToPlatform = map[string]string{
   319  	"linuxamd64":  "x86_64-linux",
   320  	"linuxarm64":  "aarch64-linux",
   321  	"linuxarm6":   "armv6l-linux",
   322  	"linuxarm7":   "armv7l-linux",
   323  	"linux386":    "i686-linux",
   324  	"darwinamd64": "x86_64-darwin",
   325  	"darwinarm64": "aarch64-darwin",
   326  }
   327  
   328  func keys(m map[string]bool) []string {
   329  	keys := make([]string, 0, len(m))
   330  	for k := range m {
   331  		keys = append(keys, k)
   332  	}
   333  	return keys
   334  }
   335  
   336  func doPublish(ctx *context.Context, prefetcher shaPrefetcher, cl client.Client, pkg *artifact.Artifact) error {
   337  	nix, err := artifact.Extra[config.Nix](*pkg, nixConfigExtra)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	if strings.TrimSpace(nix.SkipUpload) == "true" {
   343  		return errSkipUpload
   344  	}
   345  
   346  	if strings.TrimSpace(nix.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
   347  		return errSkipUploadAuto
   348  	}
   349  
   350  	repo := client.RepoFromRef(nix.Repository)
   351  
   352  	gpath := nix.Path
   353  
   354  	msg, err := tmpl.New(ctx).Apply(nix.CommitMessageTemplate)
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	author, err := commitauthor.Get(ctx, nix.CommitAuthor)
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	content, err := preparePkg(ctx, nix, cl, prefetcher)
   365  	if err != nil {
   366  		return err
   367  	}
   368  
   369  	if nix.Repository.Git.URL != "" {
   370  		return client.NewGitUploadClient(repo.Branch).
   371  			CreateFile(ctx, author, repo, []byte(content), gpath, msg)
   372  	}
   373  
   374  	cl, err = client.NewIfToken(ctx, cl, nix.Repository.Token)
   375  	if err != nil {
   376  		return err
   377  	}
   378  
   379  	if !nix.Repository.PullRequest.Enabled {
   380  		return cl.CreateFile(ctx, author, repo, []byte(content), gpath, msg)
   381  	}
   382  
   383  	log.Info("nix.pull_request enabled, creating a PR")
   384  	pcl, ok := cl.(client.PullRequestOpener)
   385  	if !ok {
   386  		return fmt.Errorf("client does not support pull requests")
   387  	}
   388  
   389  	if err := cl.CreateFile(ctx, author, repo, []byte(content), gpath, msg); err != nil {
   390  		return err
   391  	}
   392  
   393  	return pcl.OpenPullRequest(ctx, client.Repo{
   394  		Name:   nix.Repository.PullRequest.Base.Name,
   395  		Owner:  nix.Repository.PullRequest.Base.Owner,
   396  		Branch: nix.Repository.PullRequest.Base.Branch,
   397  	}, repo, msg, nix.Repository.PullRequest.Draft)
   398  }
   399  
   400  func doBuildPkg(ctx *context.Context, data templateData) (string, error) {
   401  	t, err := template.
   402  		New(data.Name).
   403  		Parse(string(pkgTmpl))
   404  	if err != nil {
   405  		return "", err
   406  	}
   407  	var out bytes.Buffer
   408  	if err := t.Execute(&out, data); err != nil {
   409  		return "", err
   410  	}
   411  
   412  	content, err := tmpl.New(ctx).Apply(out.String())
   413  	if err != nil {
   414  		return "", err
   415  	}
   416  	out.Reset()
   417  
   418  	// Sanitize the template output and get rid of trailing whitespace.
   419  	var (
   420  		r = strings.NewReader(content)
   421  		s = bufio.NewScanner(r)
   422  	)
   423  	for s.Scan() {
   424  		l := strings.TrimRight(s.Text(), " ")
   425  		_, _ = out.WriteString(l)
   426  		_ = out.WriteByte('\n')
   427  	}
   428  	if err := s.Err(); err != nil {
   429  		return "", err
   430  	}
   431  
   432  	return out.String(), nil
   433  }
   434  
   435  func postInstall(ctx *context.Context, nix config.Nix, art *artifact.Artifact) ([]string, error) {
   436  	applied, err := tmpl.New(ctx).WithArtifact(art).Apply(nix.PostInstall)
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  	return split(applied), nil
   441  }
   442  
   443  func installs(ctx *context.Context, nix config.Nix, art *artifact.Artifact) ([]string, error) {
   444  	tpl := tmpl.New(ctx).WithArtifact(art)
   445  
   446  	extraInstall, err := tpl.Apply(nix.ExtraInstall)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	install, err := tpl.Apply(nix.Install)
   452  	if err != nil {
   453  		return nil, err
   454  	}
   455  	if install != "" {
   456  		return append(split(install), split(extraInstall)...), nil
   457  	}
   458  
   459  	result := []string{"mkdir -p $out/bin"}
   460  	binInstallFormat := binInstallFormats(nix)
   461  	for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) {
   462  		for _, format := range binInstallFormat {
   463  			result = append(result, fmt.Sprintf(format, bin))
   464  		}
   465  	}
   466  
   467  	log.WithField("install", result).Info("guessing install")
   468  
   469  	return append(result, split(extraInstall)...), nil
   470  }
   471  
   472  func binInstallFormats(nix config.Nix) []string {
   473  	formats := []string{"cp -vr ./%[1]s $out/bin/%[1]s"}
   474  	if len(nix.Dependencies) == 0 {
   475  		return formats
   476  	}
   477  	var deps, linuxDeps, darwinDeps []string
   478  
   479  	for _, dep := range nix.Dependencies {
   480  		switch dep.OS {
   481  		case "darwin":
   482  			darwinDeps = append(darwinDeps, dep.Name)
   483  		case "linux":
   484  			linuxDeps = append(linuxDeps, dep.Name)
   485  		default:
   486  			deps = append(deps, dep.Name)
   487  		}
   488  	}
   489  
   490  	var depStrings []string
   491  
   492  	if len(darwinDeps) > 0 {
   493  		depStrings = append(depStrings, fmt.Sprintf("lib.optionals stdenv.isDarwin [ %s ]", strings.Join(darwinDeps, " ")))
   494  	}
   495  	if len(linuxDeps) > 0 {
   496  		depStrings = append(depStrings, fmt.Sprintf("lib.optionals stdenv.isLinux [ %s ]", strings.Join(linuxDeps, " ")))
   497  	}
   498  	if len(deps) > 0 {
   499  		depStrings = append(depStrings, fmt.Sprintf("[ %s ]", strings.Join(deps, " ")))
   500  	}
   501  
   502  	depString := strings.Join(depStrings, " ++ ")
   503  	return append(
   504  		formats,
   505  		"wrapProgram $out/bin/%[1]s --prefix PATH : ${lib.makeBinPath ("+depString+")}",
   506  	)
   507  }
   508  
   509  func split(s string) []string {
   510  	var result []string
   511  	for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
   512  		line := strings.TrimSpace(line)
   513  		if line == "" {
   514  			continue
   515  		}
   516  		result = append(result, line)
   517  	}
   518  	return result
   519  }
   520  
   521  func depNames(deps []config.NixDependency) []string {
   522  	var result []string
   523  	for _, dep := range deps {
   524  		result = append(result, dep.Name)
   525  	}
   526  	return result
   527  }
   528  
   529  type shaPrefetcher interface {
   530  	Prefetch(url string) (string, error)
   531  	Available() bool
   532  }
   533  
   534  const (
   535  	zeroHash          = "0000000000000000000000000000000000000000000000000000"
   536  	nixPrefetchURLBin = "nix-prefetch-url"
   537  )
   538  
   539  type buildShaPrefetcher struct{}
   540  
   541  func (buildShaPrefetcher) Prefetch(_ string) (string, error) { return zeroHash, nil }
   542  func (buildShaPrefetcher) Available() bool                   { return true }
   543  
   544  type publishShaPrefetcher struct {
   545  	bin string
   546  }
   547  
   548  func (p publishShaPrefetcher) Available() bool {
   549  	_, err := exec.LookPath(p.bin)
   550  	if err != nil {
   551  		log.Warnf("%s is not available", p.bin)
   552  	}
   553  	return err == nil
   554  }
   555  
   556  func (p publishShaPrefetcher) Prefetch(url string) (string, error) {
   557  	out, err := exec.Command(p.bin, url).Output()
   558  	outStr := strings.TrimSpace(string(out))
   559  	if err != nil {
   560  		return "", fmt.Errorf("could not prefetch url: %s: %w: %s", url, err, outStr)
   561  	}
   562  	return outStr, nil
   563  }