From 3662f316aba3a89601bc880a57f5fcfe297b3a40 Mon Sep 17 00:00:00 2001 From: Roy Lee Date: Mon, 23 May 2022 20:48:01 -0700 Subject: [PATCH] [lbry] version: add version package --- version/version.go | 258 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 version/version.go diff --git a/version/version.go b/version/version.go new file mode 100644 index 00000000..a4b33597 --- /dev/null +++ b/version/version.go @@ -0,0 +1,258 @@ +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 +}