github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/internal/tmpl/tmpl.go (about)

     1  // Package tmpl provides templating utilities for goreleaser.
     2  package tmpl
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  	"text/template"
    11  	"time"
    12  
    13  	"github.com/Masterminds/semver/v3"
    14  	"github.com/goreleaser/goreleaser/internal/artifact"
    15  	"github.com/goreleaser/goreleaser/pkg/build"
    16  	"github.com/goreleaser/goreleaser/pkg/context"
    17  )
    18  
    19  // Template holds data that can be applied to a template string.
    20  type Template struct {
    21  	fields Fields
    22  }
    23  
    24  // Fields that will be available to the template engine.
    25  type Fields map[string]interface{}
    26  
    27  const (
    28  	// general keys.
    29  	projectName     = "ProjectName"
    30  	version         = "Version"
    31  	rawVersion      = "RawVersion"
    32  	tag             = "Tag"
    33  	branch          = "Branch"
    34  	commit          = "Commit"
    35  	shortCommit     = "ShortCommit"
    36  	fullCommit      = "FullCommit"
    37  	commitDate      = "CommitDate"
    38  	commitTimestamp = "CommitTimestamp"
    39  	gitURL          = "GitURL"
    40  	major           = "Major"
    41  	minor           = "Minor"
    42  	patch           = "Patch"
    43  	prerelease      = "Prerelease"
    44  	isSnapshot      = "IsSnapshot"
    45  	env             = "Env"
    46  	date            = "Date"
    47  	timestamp       = "Timestamp"
    48  	modulePath      = "ModulePath"
    49  
    50  	// artifact-only keys.
    51  	osKey        = "Os"
    52  	arch         = "Arch"
    53  	arm          = "Arm"
    54  	mips         = "Mips"
    55  	binary       = "Binary"
    56  	artifactName = "ArtifactName"
    57  	artifactPath = "ArtifactPath"
    58  
    59  	// build keys.
    60  	name   = "Name"
    61  	ext    = "Ext"
    62  	path   = "Path"
    63  	target = "Target"
    64  )
    65  
    66  // New Template.
    67  func New(ctx *context.Context) *Template {
    68  	sv := ctx.Semver
    69  	rawVersionV := fmt.Sprintf("%d.%d.%d", sv.Major, sv.Minor, sv.Patch)
    70  
    71  	return &Template{
    72  		fields: Fields{
    73  			projectName:     ctx.Config.ProjectName,
    74  			modulePath:      ctx.ModulePath,
    75  			version:         ctx.Version,
    76  			rawVersion:      rawVersionV,
    77  			tag:             ctx.Git.CurrentTag,
    78  			branch:          ctx.Git.Branch,
    79  			commit:          ctx.Git.Commit,
    80  			shortCommit:     ctx.Git.ShortCommit,
    81  			fullCommit:      ctx.Git.FullCommit,
    82  			commitDate:      ctx.Git.CommitDate.UTC().Format(time.RFC3339),
    83  			commitTimestamp: ctx.Git.CommitDate.UTC().Unix(),
    84  			gitURL:          ctx.Git.URL,
    85  			env:             ctx.Env,
    86  			date:            ctx.Date.UTC().Format(time.RFC3339),
    87  			timestamp:       ctx.Date.UTC().Unix(),
    88  			major:           ctx.Semver.Major,
    89  			minor:           ctx.Semver.Minor,
    90  			patch:           ctx.Semver.Patch,
    91  			prerelease:      ctx.Semver.Prerelease,
    92  			isSnapshot:      ctx.Snapshot,
    93  		},
    94  	}
    95  }
    96  
    97  // WithEnvS overrides template's env field with the given KEY=VALUE list of
    98  // environment variables.
    99  func (t *Template) WithEnvS(envs []string) *Template {
   100  	result := map[string]string{}
   101  	for _, env := range envs {
   102  		parts := strings.SplitN(env, "=", 2)
   103  		result[parts[0]] = parts[1]
   104  	}
   105  	return t.WithEnv(result)
   106  }
   107  
   108  // WithEnv overrides template's env field with the given environment map.
   109  func (t *Template) WithEnv(e map[string]string) *Template {
   110  	t.fields[env] = e
   111  	return t
   112  }
   113  
   114  // WithExtraFields allows to add new more custom fields to the template.
   115  // It will override fields with the same name.
   116  func (t *Template) WithExtraFields(f Fields) *Template {
   117  	for k, v := range f {
   118  		t.fields[k] = v
   119  	}
   120  	return t
   121  }
   122  
   123  // WithArtifact populates Fields from the artifact and replacements.
   124  func (t *Template) WithArtifact(a *artifact.Artifact, replacements map[string]string) *Template {
   125  	bin := a.Extra[binary]
   126  	if bin == nil {
   127  		bin = t.fields[projectName]
   128  	}
   129  	t.fields[osKey] = replace(replacements, a.Goos)
   130  	t.fields[arch] = replace(replacements, a.Goarch)
   131  	t.fields[arm] = replace(replacements, a.Goarm)
   132  	t.fields[mips] = replace(replacements, a.Gomips)
   133  	t.fields[binary] = bin.(string)
   134  	t.fields[artifactName] = a.Name
   135  	t.fields[artifactPath] = a.Path
   136  	return t
   137  }
   138  
   139  func (t *Template) WithBuildOptions(opts build.Options) *Template {
   140  	return t.WithExtraFields(buildOptsToFields(opts))
   141  }
   142  
   143  func buildOptsToFields(opts build.Options) Fields {
   144  	return Fields{
   145  		target: opts.Target,
   146  		ext:    opts.Ext,
   147  		name:   opts.Name,
   148  		path:   opts.Path,
   149  		osKey:  opts.Goos,
   150  		arch:   opts.Goarch,
   151  		arm:    opts.Goarm,
   152  		mips:   opts.Gomips,
   153  	}
   154  }
   155  
   156  // Apply applies the given string against the Fields stored in the template.
   157  func (t *Template) Apply(s string) (string, error) {
   158  	var out bytes.Buffer
   159  	tmpl, err := template.New("tmpl").
   160  		Option("missingkey=error").
   161  		Funcs(template.FuncMap{
   162  			"replace": strings.ReplaceAll,
   163  			"time": func(s string) string {
   164  				return time.Now().UTC().Format(s)
   165  			},
   166  			"tolower":    strings.ToLower,
   167  			"toupper":    strings.ToUpper,
   168  			"trim":       strings.TrimSpace,
   169  			"trimprefix": strings.TrimPrefix,
   170  			"dir":        filepath.Dir,
   171  			"abs":        filepath.Abs,
   172  			"incmajor":   incMajor,
   173  			"incminor":   incMinor,
   174  			"incpatch":   incPatch,
   175  		}).
   176  		Parse(s)
   177  	if err != nil {
   178  		return "", err
   179  	}
   180  
   181  	err = tmpl.Execute(&out, t.fields)
   182  	return out.String(), err
   183  }
   184  
   185  type ExpectedSingleEnvErr struct{}
   186  
   187  func (e ExpectedSingleEnvErr) Error() string {
   188  	return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)"
   189  }
   190  
   191  // ApplySingleEnvOnly enforces template to only contain a single environment variable
   192  // and nothing else.
   193  func (t *Template) ApplySingleEnvOnly(s string) (string, error) {
   194  	s = strings.TrimSpace(s)
   195  	if len(s) == 0 {
   196  		return "", nil
   197  	}
   198  
   199  	// text/template/parse (lexer) could be used here too,
   200  	// but regexp reduces the complexity and should be sufficient,
   201  	// given the context is mostly discouraging users from bad practice
   202  	// of hard-coded credentials, rather than catch all possible cases
   203  	envOnlyRe := regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`)
   204  	if !envOnlyRe.Match([]byte(s)) {
   205  		return "", ExpectedSingleEnvErr{}
   206  	}
   207  
   208  	var out bytes.Buffer
   209  	tmpl, err := template.New("tmpl").
   210  		Option("missingkey=error").
   211  		Parse(s)
   212  	if err != nil {
   213  		return "", err
   214  	}
   215  
   216  	err = tmpl.Execute(&out, t.fields)
   217  	return out.String(), err
   218  }
   219  
   220  func replace(replacements map[string]string, original string) string {
   221  	result := replacements[original]
   222  	if result == "" {
   223  		return original
   224  	}
   225  	return result
   226  }
   227  
   228  func incMajor(v string) string {
   229  	return prefix(v) + semver.MustParse(v).IncMajor().String()
   230  }
   231  
   232  func incMinor(v string) string {
   233  	return prefix(v) + semver.MustParse(v).IncMinor().String()
   234  }
   235  
   236  func incPatch(v string) string {
   237  	return prefix(v) + semver.MustParse(v).IncPatch().String()
   238  }
   239  
   240  func prefix(v string) string {
   241  	if v != "" && v[0] == 'v' {
   242  		return "v"
   243  	}
   244  	return ""
   245  }