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  }