github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/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/goreleaser/goreleaser/internal/artifact"
    14  	"github.com/goreleaser/goreleaser/pkg/build"
    15  	"github.com/goreleaser/goreleaser/pkg/context"
    16  )
    17  
    18  // Template holds data that can be applied to a template string.
    19  type Template struct {
    20  	fields Fields
    21  }
    22  
    23  // Fields that will be available to the template engine.
    24  type Fields map[string]interface{}
    25  
    26  const (
    27  	// general keys.
    28  	projectName     = "ProjectName"
    29  	version         = "Version"
    30  	rawVersion      = "RawVersion"
    31  	tag             = "Tag"
    32  	branch          = "Branch"
    33  	commit          = "Commit"
    34  	shortCommit     = "ShortCommit"
    35  	fullCommit      = "FullCommit"
    36  	commitDate      = "CommitDate"
    37  	commitTimestamp = "CommitTimestamp"
    38  	gitURL          = "GitURL"
    39  	major           = "Major"
    40  	minor           = "Minor"
    41  	patch           = "Patch"
    42  	prerelease      = "Prerelease"
    43  	isSnapshot      = "IsSnapshot"
    44  	env             = "Env"
    45  	date            = "Date"
    46  	timestamp       = "Timestamp"
    47  
    48  	// artifact-only keys.
    49  	osKey        = "Os"
    50  	arch         = "Arch"
    51  	arm          = "Arm"
    52  	mips         = "Mips"
    53  	binary       = "Binary"
    54  	artifactName = "ArtifactName"
    55  	artifactPath = "ArtifactPath"
    56  
    57  	// gitlab only.
    58  	artifactUploadHash = "ArtifactUploadHash"
    59  
    60  	// build keys.
    61  	name   = "Name"
    62  	ext    = "Ext"
    63  	path   = "Path"
    64  	target = "Target"
    65  )
    66  
    67  // New Template.
    68  func New(ctx *context.Context) *Template {
    69  	sv := ctx.Semver
    70  	rawVersionV := fmt.Sprintf("%d.%d.%d", sv.Major, sv.Minor, sv.Patch)
    71  
    72  	return &Template{
    73  		fields: Fields{
    74  			projectName:     ctx.Config.ProjectName,
    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  	var result = map[string]string{}
   101  	for _, env := range envs {
   102  		var 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  	var 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  	if val, ok := a.Extra["ArtifactUploadHash"]; ok {
   137  		t.fields[artifactUploadHash] = val
   138  	} else {
   139  		t.fields[artifactUploadHash] = ""
   140  	}
   141  	return t
   142  }
   143  
   144  func (t *Template) WithBuildOptions(opts build.Options) *Template {
   145  	return t.WithExtraFields(buildOptsToFields(opts))
   146  }
   147  
   148  func buildOptsToFields(opts build.Options) Fields {
   149  	return Fields{
   150  		target: opts.Target,
   151  		ext:    opts.Ext,
   152  		name:   opts.Name,
   153  		path:   opts.Path,
   154  		osKey:  opts.Os,
   155  		arch:   opts.Arch,
   156  	}
   157  }
   158  
   159  // Apply applies the given string against the Fields stored in the template.
   160  func (t *Template) Apply(s string) (string, error) {
   161  	var out bytes.Buffer
   162  	tmpl, err := template.New("tmpl").
   163  		Option("missingkey=error").
   164  		Funcs(template.FuncMap{
   165  			"replace": strings.ReplaceAll,
   166  			"time": func(s string) string {
   167  				return time.Now().UTC().Format(s)
   168  			},
   169  			"tolower": strings.ToLower,
   170  			"toupper": strings.ToUpper,
   171  			"trim":    strings.TrimSpace,
   172  			"dir":     filepath.Dir,
   173  			"abs":     filepath.Abs,
   174  		}).
   175  		Parse(s)
   176  	if err != nil {
   177  		return "", err
   178  	}
   179  
   180  	err = tmpl.Execute(&out, t.fields)
   181  	return out.String(), err
   182  }
   183  
   184  type ExpectedSingleEnvErr struct{}
   185  
   186  func (e ExpectedSingleEnvErr) Error() string {
   187  	return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)"
   188  }
   189  
   190  // ApplySingleEnvOnly enforces template to only contain a single environment variable
   191  // and nothing else.
   192  func (t *Template) ApplySingleEnvOnly(s string) (string, error) {
   193  	s = strings.TrimSpace(s)
   194  	if len(s) == 0 {
   195  		return "", nil
   196  	}
   197  
   198  	// text/template/parse (lexer) could be used here too,
   199  	// but regexp reduces the complexity and should be sufficient,
   200  	// given the context is mostly discouraging users from bad practice
   201  	// of hard-coded credentials, rather than catch all possible cases
   202  	envOnlyRe := regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`)
   203  	if !envOnlyRe.Match([]byte(s)) {
   204  		return "", ExpectedSingleEnvErr{}
   205  	}
   206  
   207  	var out bytes.Buffer
   208  	tmpl, err := template.New("tmpl").
   209  		Option("missingkey=error").
   210  		Parse(s)
   211  	if err != nil {
   212  		return "", err
   213  	}
   214  
   215  	err = tmpl.Execute(&out, t.fields)
   216  	return out.String(), err
   217  }
   218  
   219  func replace(replacements map[string]string, original string) string {
   220  	result := replacements[original]
   221  	if result == "" {
   222  		return original
   223  	}
   224  	return result
   225  }