259 lines
5.6 KiB
Go
259 lines
5.6 KiB
Go
|
package version
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"runtime/debug"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
var appTag = "v0.0.0-local.0"
|
||
|
|
||
|
// Full returns full version string conforming to semantic versioning 2.0.0
|
||
|
// spec (http://semver.org/).
|
||
|
//
|
||
|
// Major.Minor.Patch-Prerelease+Buildmeta
|
||
|
//
|
||
|
// Prerelease must be either empty or in the form of Phase.Revision. The Phase
|
||
|
// must be local, dev, alpha, beta, or rc.
|
||
|
// Buildmeta is full length of 40-digit git commit ID with "-dirty" appended
|
||
|
// refelecting uncommited chanegs.
|
||
|
//
|
||
|
// This function relies injected git version tag in the form of:
|
||
|
//
|
||
|
// vMajor.Minor.Patch-Prerelease
|
||
|
//
|
||
|
// The injection can be done with go build flags for example:
|
||
|
//
|
||
|
// go build -ldflags "-X github.com/lbryio/lbcd/version.appTag=v1.2.3-beta.45"
|
||
|
//
|
||
|
// Without explicitly injected tag, a default one - "v0.0.0-local.0" is used
|
||
|
// indicating a local development build.
|
||
|
|
||
|
// The version is encoded into a int32 numeric form, which imposes valid ranges
|
||
|
// on each component:
|
||
|
//
|
||
|
// Major: 0 - 41
|
||
|
// Minor: 0 - 99
|
||
|
// Patch: 0 - 999
|
||
|
//
|
||
|
// Prerelease: Phase.Revision
|
||
|
// Phase: [ local | dev | alpha | beta | rc | ]
|
||
|
// Revision: 0 - 99
|
||
|
//
|
||
|
// Buildmeta: CommitID or CommitID-dirty
|
||
|
//
|
||
|
// Examples:
|
||
|
//
|
||
|
// 1.2.3-beta.45+950b68348261e0b4ff288d216269b8ad2a384411
|
||
|
// 2.6.4-alpha.3+92d00aaee19d1709ae64b36682ae9897ef91a2ca-dirty
|
||
|
|
||
|
func Full() string {
|
||
|
return parsed.full()
|
||
|
}
|
||
|
|
||
|
// Numeric returns numeric form of full version (excluding meta) in a 32-bit decimal number.
|
||
|
// See Full() for more details.
|
||
|
func Numeric() int32 {
|
||
|
numeric := parsed.major*100000000 +
|
||
|
parsed.minor*1000000 +
|
||
|
parsed.patch*1000 +
|
||
|
parsed.phase.numeric()*100 +
|
||
|
parsed.revision
|
||
|
|
||
|
return int32(numeric)
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
|
||
|
version, prerelease, err := parseTag(appTag)
|
||
|
if err != nil {
|
||
|
panic(fmt.Errorf("parse tag: %s; %w", appTag, err))
|
||
|
}
|
||
|
|
||
|
major, minor, patch, err := parseVersion(version)
|
||
|
if err != nil {
|
||
|
panic(fmt.Errorf("parse version: %s; %w", version, err))
|
||
|
}
|
||
|
|
||
|
phase, revision, err := parsePrerelease(prerelease)
|
||
|
if err != nil {
|
||
|
panic(fmt.Errorf("parse prerelease: %s; %w", prerelease, err))
|
||
|
}
|
||
|
|
||
|
info, ok := debug.ReadBuildInfo()
|
||
|
if !ok {
|
||
|
panic(fmt.Errorf("binary must be built with Go 1.18+ with module support"))
|
||
|
}
|
||
|
|
||
|
var commit string
|
||
|
var modified bool
|
||
|
for _, s := range info.Settings {
|
||
|
if s.Key == "vcs.revision" {
|
||
|
commit = s.Value
|
||
|
}
|
||
|
if s.Key == "vcs.modified" && s.Value == "true" {
|
||
|
modified = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parsed = parsedVersion{
|
||
|
version: version,
|
||
|
major: major,
|
||
|
minor: minor,
|
||
|
patch: patch,
|
||
|
|
||
|
prerelease: prerelease,
|
||
|
phase: phase,
|
||
|
revision: revision,
|
||
|
|
||
|
commit: commit,
|
||
|
modified: modified,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var parsed parsedVersion
|
||
|
|
||
|
type parsedVersion struct {
|
||
|
version string
|
||
|
// Semantic Version
|
||
|
major int
|
||
|
minor int
|
||
|
patch int
|
||
|
|
||
|
// Prerelease
|
||
|
prerelease string
|
||
|
phase releasePhase
|
||
|
revision int
|
||
|
|
||
|
// Build Metadata
|
||
|
commit string
|
||
|
modified bool
|
||
|
}
|
||
|
|
||
|
func (v parsedVersion) buildmeta() string {
|
||
|
if !v.modified {
|
||
|
return v.commit
|
||
|
}
|
||
|
return v.commit + "-dirty"
|
||
|
}
|
||
|
|
||
|
func (v parsedVersion) full() string {
|
||
|
return fmt.Sprintf("%s-%s+%s", v.version, v.prerelease, v.buildmeta())
|
||
|
}
|
||
|
|
||
|
func parseTag(tag string) (version string, prerelease string, err error) {
|
||
|
|
||
|
if len(tag) == 0 || tag[0] != 'v' {
|
||
|
return "", "", fmt.Errorf("tag must be prefixed with v; %s", tag)
|
||
|
}
|
||
|
|
||
|
strs := strings.Split(tag[1:], "-")
|
||
|
|
||
|
if len(strs) != 2 {
|
||
|
return "", "", fmt.Errorf("tag must be in the form of Version.Revision; %s", tag)
|
||
|
}
|
||
|
|
||
|
version = strs[0]
|
||
|
prerelease = strs[1]
|
||
|
|
||
|
return version, prerelease, nil
|
||
|
}
|
||
|
|
||
|
func parseVersion(ver string) (major int, minor int, patch int, err error) {
|
||
|
|
||
|
strs := strings.Split(ver, ".")
|
||
|
|
||
|
if len(strs) != 3 {
|
||
|
return major, minor, patch, fmt.Errorf("invalid format; must be in the form of Major.Minor.Patch")
|
||
|
}
|
||
|
|
||
|
major, err = strconv.Atoi(strs[0])
|
||
|
if err != nil {
|
||
|
return major, minor, patch, fmt.Errorf("invalid major: %s", strs[0])
|
||
|
}
|
||
|
if major < 0 || major > 41 {
|
||
|
return major, minor, patch, fmt.Errorf("major must between 0 - 41; got %d", major)
|
||
|
}
|
||
|
|
||
|
minor, err = strconv.Atoi(strs[1])
|
||
|
if err != nil {
|
||
|
return major, minor, patch, fmt.Errorf("invalid minor: %s", strs[1])
|
||
|
}
|
||
|
if minor < 0 || minor > 99 {
|
||
|
return major, minor, patch, fmt.Errorf("minor must between 0 - 99; got %d", minor)
|
||
|
}
|
||
|
|
||
|
patch, err = strconv.Atoi(strs[2])
|
||
|
if err != nil {
|
||
|
return major, minor, patch, fmt.Errorf("invalid patch: %s", strs[2])
|
||
|
}
|
||
|
if patch < 0 || patch > 999 {
|
||
|
return major, minor, patch, fmt.Errorf("patch must between 0 - 999; got %d", patch)
|
||
|
}
|
||
|
|
||
|
return major, minor, patch, nil
|
||
|
}
|
||
|
|
||
|
func parsePrerelease(pre string) (phase releasePhase, revision int, err error) {
|
||
|
|
||
|
phase = Unkown
|
||
|
|
||
|
if pre == "" {
|
||
|
return GA, 0, nil
|
||
|
}
|
||
|
|
||
|
strs := strings.Split(pre, ".")
|
||
|
if len(strs) != 2 {
|
||
|
return phase, revision, fmt.Errorf("prerelease must be in the form of Phase.Revision; got: %s", pre)
|
||
|
}
|
||
|
|
||
|
phase = releasePhase(strs[0])
|
||
|
if phase.numeric() == -1 {
|
||
|
return phase, revision, fmt.Errorf("phase must be local, dev, alpha, beta, or rc; got: %s", strs[0])
|
||
|
}
|
||
|
|
||
|
revision, err = strconv.Atoi(strs[1])
|
||
|
if err != nil {
|
||
|
return phase, revision, fmt.Errorf("invalid revision: %s", strs[0])
|
||
|
}
|
||
|
if revision < 0 || revision > 99 {
|
||
|
return phase, revision, fmt.Errorf("revision must between 0 - 999; got %d", revision)
|
||
|
}
|
||
|
|
||
|
return phase, revision, nil
|
||
|
}
|
||
|
|
||
|
type releasePhase string
|
||
|
|
||
|
const (
|
||
|
Unkown releasePhase = "unkown"
|
||
|
Local releasePhase = "local"
|
||
|
Dev releasePhase = "dev"
|
||
|
Alpha releasePhase = "alpha"
|
||
|
Beta releasePhase = "beta"
|
||
|
RC releasePhase = "rc"
|
||
|
GA releasePhase = ""
|
||
|
)
|
||
|
|
||
|
func (p releasePhase) numeric() int {
|
||
|
|
||
|
switch p {
|
||
|
case Local:
|
||
|
return 0
|
||
|
case Dev:
|
||
|
return 1
|
||
|
case Alpha:
|
||
|
return 2
|
||
|
case Beta:
|
||
|
return 3
|
||
|
case RC:
|
||
|
return 4
|
||
|
case GA:
|
||
|
return 5
|
||
|
}
|
||
|
|
||
|
// Unknown phase
|
||
|
return -1
|
||
|
}
|