github.com/lbryio/lbcd@v0.22.119/version/version.go (about)

     1  package version
     2  
     3  import (
     4  	"fmt"
     5  	"runtime/debug"
     6  	"strconv"
     7  	"strings"
     8  )
     9  
    10  var appTag = "v0.0.0-local.0"
    11  
    12  // Full returns full version string conforming to semantic versioning 2.0.0
    13  // spec (http://semver.org/).
    14  //
    15  //   Major.Minor.Patch-Prerelease+Buildmeta
    16  //
    17  // Prerelease must be either empty or in the form of Phase.Revision. The Phase
    18  // must be local, dev, alpha, beta, or rc.
    19  // Buildmeta is full length of 40-digit git commit ID with "-dirty" appended
    20  // refelecting uncommited chanegs.
    21  //
    22  // This function relies injected git version tag in the form of:
    23  //
    24  //   vMajor.Minor.Patch-Prerelease
    25  //
    26  // The injection can be done with go build flags for example:
    27  //
    28  //   go build -ldflags "-X github.com/lbryio/lbcd/version.appTag=v1.2.3-beta.45"
    29  //
    30  // Without explicitly injected tag, a default one - "v0.0.0-local.0" is used
    31  // indicating a local development build.
    32  
    33  // The version is encoded into a int32 numeric form, which imposes valid ranges
    34  // on each component:
    35  //
    36  //   Major: 0 - 41
    37  //   Minor: 0 - 99
    38  //   Patch: 0 - 999
    39  //
    40  //   Prerelease: Phase.Revision
    41  //     Phase: [ local | dev | alpha | beta | rc | ]
    42  //     Revision: 0 - 99
    43  //
    44  //   Buildmeta: CommitID or CommitID-dirty
    45  //
    46  // Examples:
    47  //
    48  //   1.2.3-beta.45+950b68348261e0b4ff288d216269b8ad2a384411
    49  //   2.6.4-alpha.3+92d00aaee19d1709ae64b36682ae9897ef91a2ca-dirty
    50  
    51  func Full() string {
    52  	return parsed.full()
    53  }
    54  
    55  // Numeric returns numeric form of full version (excluding meta) in a 32-bit decimal number.
    56  // See Full() for more details.
    57  func Numeric() int32 {
    58  	numeric := parsed.major*100000000 +
    59  		parsed.minor*1000000 +
    60  		parsed.patch*1000 +
    61  		parsed.phase.numeric()*100 +
    62  		parsed.revision
    63  
    64  	return int32(numeric)
    65  }
    66  
    67  func init() {
    68  
    69  	version, prerelease, err := parseTag(appTag)
    70  	if err != nil {
    71  		panic(fmt.Errorf("parse tag: %s; %w", appTag, err))
    72  	}
    73  
    74  	major, minor, patch, err := parseVersion(version)
    75  	if err != nil {
    76  		panic(fmt.Errorf("parse version: %s; %w", version, err))
    77  	}
    78  
    79  	phase, revision, err := parsePrerelease(prerelease)
    80  	if err != nil {
    81  		panic(fmt.Errorf("parse prerelease: %s; %w", prerelease, err))
    82  	}
    83  
    84  	info, ok := debug.ReadBuildInfo()
    85  	if !ok {
    86  		panic(fmt.Errorf("binary must be built with Go 1.18+ with module support"))
    87  	}
    88  
    89  	var commit string
    90  	var modified bool
    91  	for _, s := range info.Settings {
    92  		if s.Key == "vcs.revision" {
    93  			commit = s.Value
    94  		}
    95  		if s.Key == "vcs.modified" && s.Value == "true" {
    96  			modified = true
    97  		}
    98  	}
    99  
   100  	parsed = parsedVersion{
   101  		version: version,
   102  		major:   major,
   103  		minor:   minor,
   104  		patch:   patch,
   105  
   106  		prerelease: prerelease,
   107  		phase:      phase,
   108  		revision:   revision,
   109  
   110  		commit:   commit,
   111  		modified: modified,
   112  	}
   113  }
   114  
   115  var parsed parsedVersion
   116  
   117  type parsedVersion struct {
   118  	version string
   119  	// Semantic Version
   120  	major int
   121  	minor int
   122  	patch int
   123  
   124  	// Prerelease
   125  	prerelease string
   126  	phase      releasePhase
   127  	revision   int
   128  
   129  	// Build Metadata
   130  	commit   string
   131  	modified bool
   132  }
   133  
   134  func (v parsedVersion) buildmeta() string {
   135  	if !v.modified {
   136  		return v.commit
   137  	}
   138  	return v.commit + "-dirty"
   139  }
   140  
   141  func (v parsedVersion) full() string {
   142  	if len(v.prerelease) > 0 {
   143  		return fmt.Sprintf("%s-%s+%s", v.version, v.prerelease, v.buildmeta())
   144  	}
   145  	return fmt.Sprintf("%s+%s", v.version, v.buildmeta())
   146  }
   147  
   148  func parseTag(tag string) (version string, prerelease string, err error) {
   149  
   150  	if len(tag) == 0 || tag[0] != 'v' {
   151  		return "", "", fmt.Errorf("tag must be prefixed with v; %s", tag)
   152  	}
   153  
   154  	tag = tag[1:]
   155  
   156  	if !strings.Contains(tag, "-") {
   157  		return tag, "", nil
   158  	}
   159  
   160  	strs := strings.Split(tag, "-")
   161  
   162  	if len(strs) != 2 {
   163  		return "", "", fmt.Errorf("tag must be in the form of Version.Revision; %s", tag)
   164  	}
   165  
   166  	version = strs[0]
   167  	prerelease = strs[1]
   168  
   169  	return version, prerelease, nil
   170  }
   171  
   172  func parseVersion(ver string) (major int, minor int, patch int, err error) {
   173  
   174  	strs := strings.Split(ver, ".")
   175  
   176  	if len(strs) != 3 {
   177  		return major, minor, patch, fmt.Errorf("invalid format; must be in the form of Major.Minor.Patch")
   178  	}
   179  
   180  	major, err = strconv.Atoi(strs[0])
   181  	if err != nil {
   182  		return major, minor, patch, fmt.Errorf("invalid major: %s", strs[0])
   183  	}
   184  	if major < 0 || major > 41 {
   185  		return major, minor, patch, fmt.Errorf("major must between 0 - 41; got %d", major)
   186  	}
   187  
   188  	minor, err = strconv.Atoi(strs[1])
   189  	if err != nil {
   190  		return major, minor, patch, fmt.Errorf("invalid minor: %s", strs[1])
   191  	}
   192  	if minor < 0 || minor > 99 {
   193  		return major, minor, patch, fmt.Errorf("minor must between 0 - 99; got %d", minor)
   194  	}
   195  
   196  	patch, err = strconv.Atoi(strs[2])
   197  	if err != nil {
   198  		return major, minor, patch, fmt.Errorf("invalid patch: %s", strs[2])
   199  	}
   200  	if patch < 0 || patch > 999 {
   201  		return major, minor, patch, fmt.Errorf("patch must between 0 - 999; got %d", patch)
   202  	}
   203  
   204  	return major, minor, patch, nil
   205  }
   206  
   207  func parsePrerelease(pre string) (phase releasePhase, revision int, err error) {
   208  
   209  	phase = Unkown
   210  
   211  	if pre == "" {
   212  		return GA, 0, nil
   213  	}
   214  
   215  	strs := strings.Split(pre, ".")
   216  	if len(strs) != 2 {
   217  		return phase, revision, fmt.Errorf("prerelease must be in the form of Phase.Revision; got: %s", pre)
   218  	}
   219  
   220  	phase = releasePhase(strs[0])
   221  	if phase.numeric() == -1 {
   222  		return phase, revision, fmt.Errorf("phase must be local, dev, alpha, beta, or rc; got: %s", strs[0])
   223  	}
   224  
   225  	revision, err = strconv.Atoi(strs[1])
   226  	if err != nil {
   227  		return phase, revision, fmt.Errorf("invalid revision: %s", strs[0])
   228  	}
   229  	if revision < 0 || revision > 99 {
   230  		return phase, revision, fmt.Errorf("revision must between 0 - 999; got %d", revision)
   231  	}
   232  
   233  	return phase, revision, nil
   234  }
   235  
   236  type releasePhase string
   237  
   238  const (
   239  	Unkown releasePhase = "unkown"
   240  	Local  releasePhase = "local"
   241  	Dev    releasePhase = "dev"
   242  	Alpha  releasePhase = "alpha"
   243  	Beta   releasePhase = "beta"
   244  	RC     releasePhase = "rc"
   245  	GA     releasePhase = ""
   246  )
   247  
   248  func (p releasePhase) numeric() int {
   249  
   250  	switch p {
   251  	case Local:
   252  		return 0
   253  	case Dev:
   254  		return 1
   255  	case Alpha:
   256  		return 2
   257  	case Beta:
   258  		return 3
   259  	case RC:
   260  		return 4
   261  	case GA:
   262  		return 5
   263  	}
   264  
   265  	// Unknown phase
   266  	return -1
   267  }