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 }