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