From 43ab74b4c36f7ed5d97392a86e60d8b256ee0abf Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 3 Apr 2020 16:19:22 +0100 Subject: [PATCH] feat(pipe/build): Add support for flexible build hooks --- internal/builders/golang/build_test.go | 4 +- internal/pipe/build/build.go | 90 +++++--- internal/pipe/build/build_test.go | 273 +++++++++++++++++++++++-- internal/tmpl/tmpl.go | 23 +++ pkg/config/config.go | 50 ++++- pkg/config/config_build_hook_test.go | 55 +++++ www/content/build.md | 62 +++++- www/content/templates.md | 1 + 8 files changed, 509 insertions(+), 49 deletions(-) create mode 100644 pkg/config/config_build_hook_test.go diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go index 21376c50423..aa76a30b285 100644 --- a/internal/builders/golang/build_test.go +++ b/internal/builders/golang/build_test.go @@ -400,7 +400,7 @@ func TestRunPipeWithoutMainFunc(t *testing.T) { Builds: []config.Build{ { Binary: "no-main", - Hooks: config.Hooks{}, + Hooks: config.HookConfig{}, Targets: []string{ runtimeTarget, }, @@ -448,7 +448,7 @@ func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) { { Env: []string{"GO111MODULE=off"}, Binary: "foo", - Hooks: config.Hooks{}, + Hooks: config.HookConfig{}, Targets: []string{ runtimeTarget, }, diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go index d1193fd6ea1..18b7915128f 100644 --- a/internal/pipe/build/build.go +++ b/internal/pipe/build/build.go @@ -76,43 +76,84 @@ func buildWithDefaults(ctx *context.Context, build config.Build) config.Build { } func runPipeOnBuild(ctx *context.Context, build config.Build) error { - if err := runHook(ctx, build.Env, build.Hooks.Pre); err != nil { - return errors.Wrap(err, "pre hook failed") - } var g = semerrgroup.New(ctx.Parallelism) for _, target := range build.Targets { target := target build := build g.Go(func() error { - return doBuild(ctx, build, target) + opts, err := buildOptionsForTarget(ctx, build, target) + if err != nil { + return err + } + + if err := runHook(ctx, *opts, build.Env, build.Hooks.Pre); err != nil { + return errors.Wrap(err, "pre hook failed") + } + if err := doBuild(ctx, build, *opts); err != nil { + return err + } + if err := runHook(ctx, *opts, build.Env, build.Hooks.Post); err != nil { + return errors.Wrap(err, "post hook failed") + } + return nil }) } - if err := g.Wait(); err != nil { - return err - } - return errors.Wrap(runHook(ctx, build.Env, build.Hooks.Post), "post hook failed") + + return g.Wait() } -func runHook(ctx *context.Context, env []string, hook string) error { - if hook == "" { +func runHook(ctx *context.Context, opts builders.Options, buildEnv []string, hooks config.BuildHooks) error { + if len(hooks) == 0 { return nil } - sh, err := tmpl.New(ctx).WithEnvS(env).Apply(hook) - if err != nil { - return err + + for _, hook := range hooks { + var env []string + + env = append(env, ctx.Env.Strings()...) + env = append(env, buildEnv...) + + for _, rawEnv := range hook.Env { + e, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(rawEnv) + if err != nil { + return err + } + env = append(env, e) + } + + dir, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(hook.Dir) + if err != nil { + return err + } + + sh, err := tmpl.New(ctx).WithBuildOptions(opts). + WithEnvS(env). + Apply(hook.Cmd) + if err != nil { + return err + } + + log.WithField("hook", sh).Info("running hook") + cmd := strings.Fields(sh) + + if err := run(ctx, dir, cmd, env); err != nil { + return err + } } - log.WithField("hook", sh).Info("running hook") - cmd := strings.Fields(sh) - env = append(env, ctx.Env.Strings()...) - return run(ctx, cmd, env) + + return nil } -func doBuild(ctx *context.Context, build config.Build, target string) error { +func doBuild(ctx *context.Context, build config.Build, opts builders.Options) error { + return builders.For(build.Lang).Build(ctx, build, opts) +} + +func buildOptionsForTarget(ctx *context.Context, build config.Build, target string) (*builders.Options, error) { var ext = extFor(target, build.Flags) binary, err := tmpl.New(ctx).Apply(build.Binary) if err != nil { - return err + return nil, err } build.Binary = binary @@ -125,15 +166,15 @@ func doBuild(ctx *context.Context, build config.Build, target string) error { ), ) if err != nil { - return err + return nil, err } log.WithField("binary", path).Info("building") - return builders.For(build.Lang).Build(ctx, build, builders.Options{ + return &builders.Options{ Target: target, Name: name, Path: path, Ext: ext, - }) + }, nil } func extFor(target string, flags config.FlagArray) string { @@ -154,11 +195,14 @@ func extFor(target string, flags config.FlagArray) string { return "" } -func run(ctx *context.Context, command, env []string) error { +func run(ctx *context.Context, dir string, command, env []string) error { /* #nosec */ var cmd = exec.CommandContext(ctx, command[0], command[1:]...) var log = log.WithField("env", env).WithField("cmd", command) cmd.Env = env + if dir != "" { + cmd.Dir = dir + } log.Debug("running") if out, err := cmd.CombinedOutput(); err != nil { log.WithError(err).Debug("failed") diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go index d8579859edf..0dbf063dd7a 100644 --- a/internal/pipe/build/build_test.go +++ b/internal/pipe/build/build_test.go @@ -79,7 +79,9 @@ func TestBuild(t *testing.T) { Version: "1.2.3", Config: config, } - error := doBuild(ctx, ctx.Config.Builds[0], "darwin_amd64") + opts, err := buildOptionsForTarget(ctx, ctx.Config.Builds[0], "darwin_amd64") + assert.NoError(t, err) + error := doBuild(ctx, ctx.Config.Builds[0], *opts) assert.NoError(t, error) } @@ -117,9 +119,13 @@ func TestRunFullPipe(t *testing.T) { Binary: "testing", Flags: []string{"-v"}, Ldflags: []string{"-X main.test=testing"}, - Hooks: config.Hooks{ - Pre: "touch " + pre, - Post: "touch " + post, + Hooks: config.HookConfig{ + Pre: []config.BuildHook{ + {Cmd: "touch " + pre}, + }, + Post: []config.BuildHook{ + {Cmd: "touch " + post}, + }, }, Targets: []string{"whatever"}, }, @@ -148,9 +154,13 @@ func TestRunFullPipeFail(t *testing.T) { Binary: "testing", Flags: []string{"-v"}, Ldflags: []string{"-X main.test=testing"}, - Hooks: config.Hooks{ - Pre: "touch " + pre, - Post: "touch " + post, + Hooks: config.HookConfig{ + Pre: []config.BuildHook{ + {Cmd: "touch " + pre}, + }, + Post: []config.BuildHook{ + {Cmd: "touch " + post}, + }, }, Targets: []string{"whatever"}, }, @@ -166,29 +176,29 @@ func TestRunFullPipeFail(t *testing.T) { func TestRunPipeFailingHooks(t *testing.T) { folder, back := testlib.Mktmp(t) defer back() - var config = config.Project{ + var cfg = config.Project{ Dist: folder, Builds: []config.Build{ { Lang: "fake", Binary: "hooks", - Hooks: config.Hooks{}, + Hooks: config.HookConfig{}, Targets: []string{"whatever"}, }, }, } t.Run("pre-hook", func(t *testing.T) { - var ctx = context.New(config) + var ctx = context.New(cfg) ctx.Git.CurrentTag = "2.3.4" - ctx.Config.Builds[0].Hooks.Pre = "exit 1" - ctx.Config.Builds[0].Hooks.Post = "echo post" + ctx.Config.Builds[0].Hooks.Pre = []config.BuildHook{{Cmd: "exit 1"}} + ctx.Config.Builds[0].Hooks.Post = []config.BuildHook{{Cmd: "echo post"}} assert.EqualError(t, Pipe{}.Run(ctx), `pre hook failed: "": exec: "exit": executable file not found in $PATH`) }) t.Run("post-hook", func(t *testing.T) { - var ctx = context.New(config) + var ctx = context.New(cfg) ctx.Git.CurrentTag = "2.3.4" - ctx.Config.Builds[0].Hooks.Pre = "echo pre" - ctx.Config.Builds[0].Hooks.Post = "exit 1" + ctx.Config.Builds[0].Hooks.Pre = []config.BuildHook{{Cmd: "echo pre"}} + ctx.Config.Builds[0].Hooks.Post = []config.BuildHook{{Cmd: "exit 1"}} assert.EqualError(t, Pipe{}.Run(ctx), `post hook failed: "": exec: "exit": executable file not found in $PATH`) }) } @@ -395,7 +405,7 @@ func TestTemplate(t *testing.T) { assert.Contains(t, binary, `-X "main.foo=123"`) } -func TestHookEnvs(t *testing.T) { +func TestRunHookEnvs(t *testing.T) { tmp, back := testlib.Mktmp(t) defer back() @@ -406,26 +416,166 @@ func TestHookEnvs(t *testing.T) { }, } - t.Run("valid template", func(t *testing.T) { + var opts = api.Options{ + Name: "binary-name", + Path: "./binary-name", + Target: "darwin_amd64", + } + + simpleHook := func(cmd string) config.BuildHooks { + return []config.BuildHook{{Cmd: cmd}} + } + + t.Run("valid cmd template with ctx env", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + Env: []string{ + fmt.Sprintf("CTXFOO=%s/foo", tmp), + }, + }), opts, []string{}, simpleHook("touch {{ .Env.CTXFOO }}")) + assert.NoError(t, err) + assert.FileExists(t, filepath.Join(tmp, "foo")) + }) + + t.Run("valid cmd template with build env", func(t *testing.T) { var err = runHook(context.New(config.Project{ Builds: []config.Build{ build, }, - }), build.Env, "touch {{ .Env.FOO }}") + }), opts, build.Env, simpleHook("touch {{ .Env.FOO }}")) assert.NoError(t, err) assert.FileExists(t, filepath.Join(tmp, "foo")) }) - t.Run("invalid template", func(t *testing.T) { + t.Run("valid cmd template with hook env", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + }), opts, []string{}, []config.BuildHook{{ + Cmd: "touch {{ .Env.HOOK_ONLY_FOO }}", + Env: []string{ + fmt.Sprintf("HOOK_ONLY_FOO=%s/hook_only", tmp), + }, + }}) + assert.NoError(t, err) + assert.FileExists(t, filepath.Join(tmp, "hook_only")) + }) + + t.Run("valid cmd template with ctx and build env", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + Env: []string{ + fmt.Sprintf("OVER_FOO=%s/ctx_over_build", tmp), + }, + }), opts, []string{ + fmt.Sprintf("OVER_FOO=%s/build_over_ctx", tmp), + }, simpleHook("touch {{ .Env.OVER_FOO }}")) + assert.NoError(t, err) + + assert.FileExists(t, filepath.Join(tmp, "build_over_ctx")) + assert.NoFileExists(t, filepath.Join(tmp, "ctx_over_build")) + }) + + t.Run("valid cmd template with ctx and hook env", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + Env: []string{ + fmt.Sprintf("CTX_OR_HOOK_FOO=%s/ctx_over_hook", tmp), + }, + }), opts, []string{}, []config.BuildHook{{ + Cmd: "touch {{ .Env.CTX_OR_HOOK_FOO }}", + Env: []string{ + fmt.Sprintf("CTX_OR_HOOK_FOO=%s/hook_over_ctx", tmp), + }, + }}) + assert.NoError(t, err) + assert.FileExists(t, filepath.Join(tmp, "hook_over_ctx")) + assert.NoFileExists(t, filepath.Join(tmp, "ctx_over_hook")) + }) + + t.Run("valid cmd template with build and hook env", func(t *testing.T) { var err = runHook(context.New(config.Project{ Builds: []config.Build{ build, }, - }), build.Env, "touch {{ .Env.FOOss }}") + }), opts, []string{ + fmt.Sprintf("BUILD_OR_HOOK_FOO=%s/build_over_hook", tmp), + }, []config.BuildHook{{ + Cmd: "touch {{ .Env.BUILD_OR_HOOK_FOO }}", + Env: []string{ + fmt.Sprintf("BUILD_OR_HOOK_FOO=%s/hook_over_build", tmp), + }, + }}) + assert.NoError(t, err) + assert.FileExists(t, filepath.Join(tmp, "hook_over_build")) + assert.NoFileExists(t, filepath.Join(tmp, "build_over_hook")) + }) + + t.Run("valid cmd template with ctx, build and hook env", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + Env: []string{ + fmt.Sprintf("CTX_OR_BUILD_OR_HOOK_FOO=%s/ctx_wins", tmp), + }, + }), opts, []string{ + fmt.Sprintf("CTX_OR_BUILD_OR_HOOK_FOO=%s/build_wins", tmp), + }, []config.BuildHook{{ + Cmd: "touch {{ .Env.CTX_OR_BUILD_OR_HOOK_FOO }}", + Env: []string{ + fmt.Sprintf("CTX_OR_BUILD_OR_HOOK_FOO=%s/hook_wins", tmp), + }, + }}) + assert.NoError(t, err) + assert.FileExists(t, filepath.Join(tmp, "hook_wins")) + assert.NoFileExists(t, filepath.Join(tmp, "ctx_wins")) + assert.NoFileExists(t, filepath.Join(tmp, "build_wins")) + }) + + t.Run("invalid cmd template", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + }), opts, build.Env, simpleHook("touch {{ .Env.FOOss }}")) assert.EqualError(t, err, `template: tmpl:1:13: executing "tmpl" at <.Env.FOOss>: map has no entry for key "FOOss"`) }) - t.Run("env inside shell", func(t *testing.T) { + t.Run("invalid dir template", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + }), opts, build.Env, []config.BuildHook{{ + Cmd: "echo something", + Dir: "{{ .Env.INVALID_ENV }}", + }}) + assert.EqualError(t, err, `template: tmpl:1:7: executing "tmpl" at <.Env.INVALID_ENV>: map has no entry for key "INVALID_ENV"`) + }) + + t.Run("invalid hook env template", func(t *testing.T) { + var err = runHook(context.New(config.Project{ + Builds: []config.Build{ + build, + }, + }), opts, build.Env, []config.BuildHook{{ + Cmd: "echo something", + Env: []string{ + "TEST={{ .Env.MISSING_ENV }}", + }, + }}) + assert.EqualError(t, err, `template: tmpl:1:12: executing "tmpl" at <.Env.MISSING_ENV>: map has no entry for key "MISSING_ENV"`) + }) + + t.Run("build env inside shell", func(t *testing.T) { var shell = `#!/bin/sh -e touch "$BAR"` err := ioutil.WriteFile(filepath.Join(tmp, "test.sh"), []byte(shell), 0750) @@ -434,8 +584,87 @@ touch "$BAR"` Builds: []config.Build{ build, }, - }), build.Env, "sh test.sh") + }), opts, build.Env, simpleHook("sh test.sh")) assert.NoError(t, err) assert.FileExists(t, filepath.Join(tmp, "bar")) }) } + +func TestPipeOnBuild_hooksRunPerTarget(t *testing.T) { + tmpDir, back := testlib.Mktmp(t) + defer back() + + build := config.Build{ + Lang: "fake", + Binary: "testing.v{{.Version}}", + Targets: []string{ + "linux_amd64", + "darwin_amd64", + "windows_amd64", + }, + Hooks: config.HookConfig{ + Pre: []config.BuildHook{ + {Cmd: "touch pre-hook-{{.Target}}", Dir: tmpDir}, + }, + Post: config.BuildHooks{ + {Cmd: "touch post-hook-{{.Target}}", Dir: tmpDir}, + }, + }, + } + ctx := context.New(config.Project{ + Builds: []config.Build{ + build, + }, + }) + err := runPipeOnBuild(ctx, build) + assert.NoError(t, err) + assert.FileExists(t, filepath.Join(tmpDir, "pre-hook-linux_amd64")) + assert.FileExists(t, filepath.Join(tmpDir, "pre-hook-darwin_amd64")) + assert.FileExists(t, filepath.Join(tmpDir, "pre-hook-windows_amd64")) + assert.FileExists(t, filepath.Join(tmpDir, "post-hook-linux_amd64")) + assert.FileExists(t, filepath.Join(tmpDir, "post-hook-darwin_amd64")) + assert.FileExists(t, filepath.Join(tmpDir, "post-hook-windows_amd64")) +} + +func TestPipeOnBuild_invalidBinaryTpl(t *testing.T) { + build := config.Build{ + Lang: "fake", + Binary: "testing.v{{.XYZ}}", + Targets: []string{ + "linux_amd64", + }, + } + ctx := context.New(config.Project{ + Builds: []config.Build{ + build, + }, + }) + err := runPipeOnBuild(ctx, build) + assert.EqualError(t, err, `template: tmpl:1:11: executing "tmpl" at <.XYZ>: map has no entry for key "XYZ"`) +} + +func TestBuildOptionsForTarget(t *testing.T) { + tmpDir, back := testlib.Mktmp(t) + defer back() + + build := config.Build{ + ID: "testid", + Binary: "testbinary", + Targets: []string{ + "linux_amd64", + "darwin_amd64", + "windows_amd64", + }, + } + ctx := context.New(config.Project{ + Dist: tmpDir, + Builds: []config.Build{build}, + }) + opts, err := buildOptionsForTarget(ctx, build, "linux_amd64") + assert.NoError(t, err) + assert.Equal(t, &api.Options{ + Name: "testbinary", + Path: filepath.Join(tmpDir, "testid_linux_amd64", "testbinary"), + Target: "linux_amd64", + }, opts) +} diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go index e3f6ee199fd..e8da6260ac4 100644 --- a/internal/tmpl/tmpl.go +++ b/internal/tmpl/tmpl.go @@ -3,11 +3,13 @@ package tmpl import ( "bytes" + "path/filepath" "strings" "text/template" "time" "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/pkg/build" "github.com/goreleaser/goreleaser/pkg/context" ) @@ -42,8 +44,15 @@ const ( mips = "Mips" binary = "Binary" artifactName = "ArtifactName" + // gitlab only artifactUploadHash = "ArtifactUploadHash" + + // build keys + name = "Name" + ext = "Ext" + path = "Path" + target = "Target" ) // New Template @@ -114,6 +123,19 @@ func (t *Template) WithArtifact(a *artifact.Artifact, replacements map[string]st return t } +func (t *Template) WithBuildOptions(opts build.Options) *Template { + return t.WithExtraFields(buildOptsToFields(opts)) +} + +func buildOptsToFields(opts build.Options) Fields { + return Fields{ + target: opts.Target, + ext: opts.Ext, + name: opts.Name, + path: opts.Path, + } +} + // Apply applies the given string against the Fields stored in the template. func (t *Template) Apply(s string) (string, error) { var out bytes.Buffer @@ -127,6 +149,7 @@ func (t *Template) Apply(s string) (string, error) { "tolower": strings.ToLower, "toupper": strings.ToUpper, "trim": strings.TrimSpace, + "dir": filepath.Dir, }). Parse(s) if err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index 55fcada0111..146e61504be 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -151,7 +151,7 @@ type Build struct { Ldflags StringArray `yaml:",omitempty"` Flags FlagArray `yaml:",omitempty"` Binary string `yaml:",omitempty"` - Hooks Hooks `yaml:",omitempty"` + Hooks HookConfig `yaml:",omitempty"` Env []string `yaml:",omitempty"` Lang string `yaml:",omitempty"` Asmflags StringArray `yaml:",omitempty"` @@ -159,6 +159,54 @@ type Build struct { Skip bool `yaml:",omitempty"` } +type HookConfig struct { + Pre BuildHooks `yaml:",omitempty"` + Post BuildHooks `yaml:",omitempty"` +} + +type BuildHooks []BuildHook + +// UnmarshalYAML is a custom unmarshaler that allows simplified declaration of single command +func (bhc *BuildHooks) UnmarshalYAML(unmarshal func(interface{}) error) error { + var singleCmd string + err := unmarshal(&singleCmd) + if err == nil { + *bhc = []BuildHook{{Cmd: singleCmd}} + return nil + } + + type t BuildHooks + var hooks t + if err := unmarshal(&hooks); err != nil { + return err + } + *bhc = (BuildHooks)(hooks) + return nil +} + +type BuildHook struct { + Dir string `yaml:",omitempty"` + Cmd string `yaml:",omitempty"` + Env []string `yaml:",omitempty"` +} + +// UnmarshalYAML is a custom unmarshaler that allows simplified declarations of commands as strings +func (bh *BuildHook) UnmarshalYAML(unmarshal func(interface{}) error) error { + var cmd string + if err := unmarshal(&cmd); err != nil { + type t BuildHook + var hook t + if err := unmarshal(&hook); err != nil { + return err + } + *bh = (BuildHook)(hook) + return nil + } + + bh.Cmd = cmd + return nil +} + // FormatOverride is used to specify a custom format for a specific GOOS. type FormatOverride struct { Goos string `yaml:",omitempty"` diff --git a/pkg/config/config_build_hook_test.go b/pkg/config/config_build_hook_test.go new file mode 100644 index 00000000000..355e23285f3 --- /dev/null +++ b/pkg/config/config_build_hook_test.go @@ -0,0 +1,55 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + yaml "gopkg.in/yaml.v2" +) + +func TestBuildHook_justString(t *testing.T) { + var actual HookConfig + + err := yaml.UnmarshalStrict([]byte(`pre: ./script.sh`), &actual) + assert.NoError(t, err) + assert.Equal(t, BuildHook{ + Cmd: "./script.sh", + Env: nil, + }, actual.Pre[0]) +} + +func TestBuildHook_stringCmds(t *testing.T) { + var actual HookConfig + + err := yaml.UnmarshalStrict([]byte(`pre: + - ./script.sh + - second-script.sh +`), &actual) + assert.NoError(t, err) + + assert.Equal(t, BuildHooks{ + { + Cmd: "./script.sh", + Env: nil, + }, + { + Cmd: "second-script.sh", + Env: nil, + }, + }, actual.Pre) +} + +func TestBuildHook_complex(t *testing.T) { + var actual HookConfig + + err := yaml.UnmarshalStrict([]byte(`pre: + - cmd: ./script.sh + env: + - TEST=value +`), &actual) + assert.NoError(t, err) + assert.Equal(t, BuildHook{ + Cmd: "./script.sh", + Env: []string{"TEST=value"}, + }, actual.Pre[0]) +} diff --git a/www/content/build.md b/www/content/build.md index 660280b94d7..79e497b59b6 100644 --- a/www/content/build.md +++ b/www/content/build.md @@ -110,7 +110,7 @@ builds: # Default is both hooks empty. hooks: pre: rice embed-go - post: ./script.sh + post: ./script.sh {{ .Path }} # If true, skip the build. # Useful for library projects. @@ -137,6 +137,66 @@ Then you can run: GOVERSION=$(go version) goreleaser ``` +## Build Hooks + +Both pre and post hooks run **for each build target**, regardless of whether +these targets are generated via a matrix of OSes and architectures +or defined explicitly. + +In addition to simple declarations as shown above _multiple_ hooks can be declared +to help retaining reusability of config between different build environments. + +```yml +builds: + - + id: "with-hooks" + targets: + - "darwin_amd64" + - "windows_amd64" + hooks: + pre: + - first-script.sh + - second-script.sh + post: + - upx "{{ .Path }}" + - codesign -project="{{ .ProjectName }}" "{{ .Path }}" +``` + +Each hook can also have its own work directory and environment variables: + +```yml +builds: + - + id: "with-hooks" + targets: + - "darwin_amd64" + - "windows_amd64" + hooks: + pre: + - cmd: first-script.sh + dir: "{{ dir .Dist}}" + env: + - HOOK_SPECIFIC_VAR={{ .Env.GLOBAL_VAR }} + - second-script.sh +``` + +All properties of a hook (`cmd`, `dir` and `env`) support [templating](/customization/#Name%20Templates) +with `post` hooks having binary artifact available (as these run _after_ the build). +Additionally the following build details are exposed to both `pre` and `post` hooks: + +| Key | Description | +| :-------------: | :------------------------------------: | +| .Name | Filename of the binary, e.g. `bin.exe` | +| .Ext | Extension, e.g. `.exe` | +| .Path | Absolute path to the binary | +| .Target | Build target, e.g. `darwin_amd64` | + +Environment variables are inherited and overridden in the following order: + + - global (`env`) + - build (`builds[].env`) + - hook (`builds[].hooks.pre[].env` and `builds[].hooks.post[].env`) + ## Go Modules If you use Go 1.11+ with go modules or vgo, when GoReleaser runs it may diff --git a/www/content/templates.md b/www/content/templates.md index 1f3e86bf8d5..25eada2eece 100644 --- a/www/content/templates.md +++ b/www/content/templates.md @@ -57,6 +57,7 @@ On all fields, you have these available functions: | `tolower "V1.2"` | makes input string lowercase. See [ToLower](https://golang.org/pkg/strings/#ToLower) | | `toupper "v1.2"` | makes input string uppercase. See [ToUpper](https://golang.org/pkg/strings/#ToUpper) | | `trim " v1.2 "` | removes all leading and trailing white space. See [TrimSpace](https://golang.org/pkg/strings/#TrimSpace) | +| `dir .Path` | returns all but the last element of path, typically the path's directory. See [Dir](https://golang.org/pkg/path/filepath/#Dir) With all those fields, you may be able to compose the name of your artifacts pretty much the way you want: