gitee.com/h79/goutils@v1.22.10/build/tmpl.go (about) 1 package build 2 3 import ( 4 "bytes" 5 "fmt" 6 "github.com/Masterminds/semver/v3" 7 "golang.org/x/text/cases" 8 "golang.org/x/text/language" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "text/template" 13 "time" 14 ) 15 16 // Template holds data that can be applied to a template string. 17 type Template struct { 18 fields Fields 19 } 20 21 // Fields that will be available to the template engine. 22 type Fields map[string]interface{} 23 24 const ( 25 // general keys. 26 productId = "ProductId" 27 projectName = "ProjectName" 28 version = "Version" 29 rawVersion = "RawVersion" 30 model = "Model" 31 tag = "Tag" 32 previousTag = "PreviousTag" 33 branch = "Branch" 34 commit = "Commit" 35 shortCommit = "ShortCommit" 36 fullCommit = "FullCommit" 37 commitDate = "CommitDate" 38 commitTimestamp = "CommitTimestamp" 39 gitURL = "GitURL" 40 summary = "Summary" 41 tagSubject = "TagSubject" 42 tagContents = "TagContents" 43 tagBody = "TagBody" 44 isGitDirty = "IsGitDirty" 45 major = "Major" 46 minor = "Minor" 47 patch = "Patch" 48 prerelease = "Prerelease" 49 isSnapshot = "IsSnapshot" 50 isNightly = "IsNightly" 51 isDraft = "IsDraft" 52 env = "Env" 53 date = "BuildDate" 54 now = "Now" 55 timestamp = "Timestamp" 56 runtimeK = "Runtime" 57 releaseNotes = "ReleaseNotes" 58 // artifact-only keys. 59 osKey = "Os" 60 amd64 = "Amd64" 61 arch = "Arch" 62 arm = "Arm" 63 mips = "Mips" 64 65 // build keys. 66 name = "Name" 67 ext = "Ext" 68 path = "Path" 69 target = "Target" 70 ) 71 72 func NewEmptyTemplate() *Template { 73 return &Template{fields: Fields{}} 74 } 75 76 // NewTemplate Template. 77 func NewTemplate(ctx *Context) *Template { 78 rawVer := fmt.Sprintf("%d.%d.%d", ctx.Semver.Major, ctx.Semver.Minor, ctx.Semver.Patch) 79 return &Template{ 80 fields: Fields{ 81 productId: ctx.Config.ProductId, 82 projectName: ctx.Config.ProjectName, 83 version: ctx.Config.Version, 84 model: ctx.Config.Model, 85 osKey: ctx.Config.Os, 86 rawVersion: rawVer, 87 summary: ctx.Git.Summary, 88 tag: ctx.Git.CurrentTag, 89 previousTag: ctx.Git.PreviousTag, 90 branch: ctx.Git.Branch, 91 commit: ctx.Git.Commit, 92 shortCommit: ctx.Git.ShortCommit, 93 fullCommit: ctx.Git.FullCommit, 94 commitDate: ctx.Git.CommitDate.UTC().Format(time.RFC3339), 95 commitTimestamp: ctx.Git.CommitDate.UTC().Unix(), 96 gitURL: ctx.Git.URL, 97 isGitDirty: ctx.Git.Dirty, 98 env: ctx.Env, 99 date: ctx.DateFormat(ctx.Date), 100 timestamp: ctx.Date.UTC().Unix(), 101 now: ctx.Date.UTC(), 102 major: ctx.Semver.Major, 103 minor: ctx.Semver.Minor, 104 patch: ctx.Semver.Patch, 105 prerelease: ctx.Semver.Prerelease, 106 isSnapshot: ctx.Snapshot, 107 isNightly: false, 108 tagSubject: ctx.Git.TagSubject, 109 tagContents: ctx.Git.TagContents, 110 tagBody: ctx.Git.TagBody, 111 runtimeK: ctx.Runtime, 112 }, 113 } 114 } 115 116 // WithEnvS overrides template's env field with the given KEY=VALUE list of 117 // environment variables. 118 func (t *Template) WithEnvS(envs []string) *Template { 119 result := map[string]string{} 120 for _, env := range envs { 121 k, v, ok := strings.Cut(env, "=") 122 if !ok || k == "" { 123 continue 124 } 125 result[k] = v 126 } 127 return t.WithEnv(result) 128 } 129 130 // WithEnv overrides template's env field with the given environment map. 131 func (t *Template) WithEnv(e map[string]string) *Template { 132 t.fields[env] = e 133 return t 134 } 135 136 // WithExtraFields allows to add new more custom fields to the template. 137 // It will override fields with the same name. 138 func (t *Template) WithExtraFields(f Fields) *Template { 139 for k, v := range f { 140 t.fields[k] = v 141 } 142 return t 143 } 144 145 func (t *Template) WithBuildOptions(opts Options) *Template { 146 return t.WithExtraFields(buildOptsToFields(opts)) 147 } 148 149 func (t *Template) AddField(key string, value interface{}) *Template { 150 t.fields[key] = value 151 return t 152 } 153 154 func buildOptsToFields(opts Options) Fields { 155 return Fields{ 156 target: opts.Target, 157 ext: opts.Ext, 158 name: opts.Name, 159 path: opts.Path, 160 osKey: opts.Goos, 161 arch: opts.Goarch, 162 } 163 } 164 165 // Bool Apply the given string, and converts it to a bool. 166 func (t *Template) Bool(s string) (bool, error) { 167 r, err := t.Apply(s) 168 return strings.TrimSpace(strings.ToLower(r)) == "true", err 169 } 170 171 // Apply applies the given string against the Fields stored in the template. 172 func (t *Template) Apply(s string) (string, error) { 173 var out bytes.Buffer 174 tmpl, err := template.New("tmpl"). 175 Option("missingkey=error"). 176 Funcs(template.FuncMap{ 177 "replace": strings.ReplaceAll, 178 "split": strings.Split, 179 "time": func(s string) string { 180 return time.Now().UTC().Format(s) 181 }, 182 "toLower": strings.ToLower, 183 "toUpper": strings.ToUpper, 184 "trim": strings.TrimSpace, 185 "trimPrefix": strings.TrimPrefix, 186 "trimSuffix": strings.TrimSuffix, 187 "title": cases.Title(language.English).String, 188 "dir": filepath.Dir, 189 "base": filepath.Base, 190 "abs": filepath.Abs, 191 "incMajor": incMajor, 192 "incMinor": incMinor, 193 "incPatch": incPatch, 194 "filter": filter(false), 195 "reverseFilter": filter(true), 196 "mdv2escape": mdv2Escape, 197 "envOrDefault": t.envOrDefault, 198 }). 199 Parse(s) 200 if err != nil { 201 return "", newTmplError(s, err) 202 } 203 204 err = tmpl.Execute(&out, t.fields) 205 return out.String(), newTmplError(s, err) 206 } 207 208 // ApplyAll applies all the given strings against the Fields stored in the 209 // template. Application stops as soon as an error is encountered. 210 func (t *Template) ApplyAll(in ...string) ([]string, error) { 211 out := make([]string, len(in)) 212 for i, sp := range in { 213 r, err := t.Apply(sp) 214 if err != nil { 215 return out, newTmplError(sp, err) 216 } 217 out[i] = r 218 } 219 return out, nil 220 } 221 222 func (t *Template) envOrDefault(name, value string) string { 223 s, ok := t.fields[env].(Env)[name] 224 if !ok { 225 return value 226 } 227 return s 228 } 229 230 type ExpectedSingleEnvErr struct{} 231 232 func (e ExpectedSingleEnvErr) Error() string { 233 return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)" 234 } 235 236 // ApplySingleEnvOnly enforces template to only contain a single environment variable 237 // and nothing else. 238 func (t *Template) ApplySingleEnvOnly(s string) (string, error) { 239 s = strings.TrimSpace(s) 240 if len(s) == 0 { 241 return "", nil 242 } 243 244 // text/template/parse (lexer) could be used here too, 245 // but regexp reduces the complexity and should be sufficient, 246 // given the context is mostly discouraging users from bad practice 247 // of hard-coded credentials, rather than catch all possible cases 248 envOnlyRe := regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`) 249 if !envOnlyRe.Match([]byte(s)) { 250 return "", ExpectedSingleEnvErr{} 251 } 252 253 var out bytes.Buffer 254 tmpl, err := template.New("tmpl"). 255 Option("missingkey=error"). 256 Parse(s) 257 if err != nil { 258 return "", err 259 } 260 261 err = tmpl.Execute(&out, t.fields) 262 return out.String(), err 263 } 264 265 func incMajor(v string) string { 266 return prefix(v) + semver.MustParse(v).IncMajor().String() 267 } 268 269 func incMinor(v string) string { 270 return prefix(v) + semver.MustParse(v).IncMinor().String() 271 } 272 273 func incPatch(v string) string { 274 return prefix(v) + semver.MustParse(v).IncPatch().String() 275 } 276 277 func prefix(v string) string { 278 if v != "" && v[0] == 'v' { 279 return "v" 280 } 281 return "" 282 } 283 284 func filter(reverse bool) func(content, exp string) string { 285 return func(content, exp string) string { 286 re := regexp.MustCompilePOSIX(exp) 287 var lines []string 288 for _, line := range strings.Split(content, "\n") { 289 if reverse && re.MatchString(line) { 290 continue 291 } 292 if !reverse && !re.MatchString(line) { 293 continue 294 } 295 lines = append(lines, line) 296 } 297 298 return strings.Join(lines, "\n") 299 } 300 } 301 302 func mdv2Escape(s string) string { 303 return strings.NewReplacer( 304 "_", "\\_", 305 "*", "\\*", 306 "[", "\\[", 307 "]", "\\]", 308 "(", "\\(", 309 ")", "\\)", 310 "~", "\\~", 311 "`", "\\`", 312 ">", "\\>", 313 "#", "\\#", 314 "+", "\\+", 315 "-", "\\-", 316 "=", "\\=", 317 "|", "\\|", 318 "{", "\\{", 319 "}", "\\}", 320 ".", "\\.", 321 "!", "\\!", 322 ).Replace(s) 323 }