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 }