Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools: implement version info parser based around rpmspec #6

Merged
merged 1 commit into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 16 additions & 55 deletions tools/parser/git/parser.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package git

import (
"errors"
"fmt"
"io"
"io/fs"
"os/exec"
"strings"

"github.com/UiP9AV6Y/buildinfo"
"github.com/UiP9AV6Y/buildinfo/tools/util"
)

const (
Expand Down Expand Up @@ -43,12 +42,15 @@ func TryParse(cmd, path string) (*Git, error) {
return nil, ErrNoRepository
}

o, e, err := run(cmd, path, "rev-parse", "--show-toplevel")
if strings.Contains(e, errParse) {
// ignore the error type, as long as the error output
// contains hints about the failure cause
return nil, ErrNoRepository
} else if err != nil {
argv := []string{"-C", path, "rev-parse", "--show-toplevel"}
o, err := util.RunCmd(realCmd, argv)
if err != nil {
if strings.Contains(err.Error(), errParse) {
// ignore the error type, as long as the error output
// contains hints about the failure cause
return nil, ErrNoRepository
}

return nil, err
}

Expand Down Expand Up @@ -92,72 +94,31 @@ func (g *Git) Equal(o *Git) bool {
func (g *Git) ParseVersionInfo() (*buildinfo.VersionInfo, error) {
result := buildinfo.NewVersionInfo()

branch, err := g.run("rev-parse", "--abbrev-ref", "HEAD")
branch, err := g.git("rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return nil, fmt.Errorf("Unable to determine current git branch: %w", err)
} else if branch != "" {
result.Branch = branch
}

revision, err := g.run("rev-parse", "HEAD")
revision, err := g.git("rev-parse", "HEAD")
if err != nil {
return nil, fmt.Errorf("Unable to determine git HEAD revision: %w", err)
} else if revision != "" {
result.Revision = revision
}

// ignore error in case the project has no tags
version, _ := g.run("describe", "--tags", "--abbrev=0")
version, _ := g.git("describe", "--tags", "--abbrev=0")
if version != "" {
result.Version = strings.TrimPrefix(version, "v")
}

return result, nil
}

func (g *Git) run(arg ...string) (string, error) {
o, e, err := run(g.cmd, g.root, arg...)
if err == nil {
return o, nil
} else if e != "" {
return "", errors.New(e)
}

return "", err
}

func run(cmd, cwd string, arg ...string) (string, string, error) {
argv := append([]string{"-C", cwd}, arg...)
git := exec.Command(cmd, argv...)
if git.Err != nil {
return "", "", git.Err
}

stderr, err := git.StderrPipe()
if err != nil {
return "", "", err
}

stdout, err := git.StdoutPipe()
if err != nil {
return "", "", err
}

if err := git.Start(); err != nil {
return "", "", err
}

e, err := io.ReadAll(stderr)
if err != nil {
return "", "", err
}

o, _ := io.ReadAll(stdout)
if err != nil {
return "", "", err
}
func (g *Git) git(arg ...string) (string, error) {
argv := append([]string{"-C", g.root}, arg...)

return strings.Trim(string(o), " \n\r"),
strings.Trim(string(e), " \n\r"),
git.Wait()
return util.RunCmd(g.cmd, argv)
}
7 changes: 7 additions & 0 deletions tools/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/UiP9AV6Y/buildinfo/tools/parser/git"
"github.com/UiP9AV6Y/buildinfo/tools/parser/mock"
"github.com/UiP9AV6Y/buildinfo/tools/parser/os"
"github.com/UiP9AV6Y/buildinfo/tools/parser/rpmspec"
)

const (
Expand Down Expand Up @@ -50,6 +51,12 @@ func ParseVersionParser(dir string) (VersionParser, error) {
return nil, err
}

if r, err := rpmspec.TrySystemParse(base); err == nil {
return r, nil
} else if !errors.Is(err, rpmspec.ErrNoSpec) {
return nil, err
}

if g, err := git.TrySystemParse(base); err == nil {
return g, nil
} else if !errors.Is(err, git.ErrNoRepository) {
Expand Down
113 changes: 113 additions & 0 deletions tools/parser/rpmspec/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package rpmspec

import (
"fmt"
"io/fs"
"os/exec"
"path/filepath"

"github.com/UiP9AV6Y/buildinfo"
"github.com/UiP9AV6Y/buildinfo/tools/util"
)

const (
// spec field to render/extract for the version
versionMacro = "%{version}"
// spec field to render/extract for the revision
revisionMacro = "%{release}"
systemCommand = "rpmspec"
)

var (
// Error when no RPM spec file or `rpmspec` command was found
ErrNoSpec = fs.ErrNotExist
)

// parser.VersionParser implementation rendering and parsing a RPM spec file
type RPMSpec struct {
cmd string
file string
}

// TrySystemParse calls TryParse using the rpmspec command found in the PATH
func TrySystemParse(path string) (*RPMSpec, error) {
return TryParse(systemCommand, path)
}

// TryParse attempts to parse the given directory for a RPM spec file.
// If no file was found or the given command was not found
// ErrNoRepository is returned. All other errors are a result of file
// access problems.
func TryParse(cmd, path string) (*RPMSpec, error) {
realCmd, err := exec.LookPath(cmd)
if err != nil {
// unable to parse spec file without `rpmspec`
return nil, ErrNoSpec
}

pattern := filepath.Join(path, "*.spec")
haystack, err := filepath.Glob(pattern)
if err != nil {
return nil, err
} else if haystack == nil || len(haystack) == 0 {
return nil, ErrNoSpec
}

return New(realCmd, haystack[0]), nil
}

// NewSystem creates a new parser.Parser instance using the provided
// RPM spec file. the rpmspec executable is invoked as-is,
// relying on its presence in one of the PATH directories.
func NewSystem(file string) *RPMSpec {
return New(systemCommand, file)
}

// New creates a new parser.Parser instance using the provided
// RPM spec file. the rpmspec executable is invoked using
// the provided path.
func New(cmd, file string) *RPMSpec {
result := &RPMSpec{
cmd: cmd,
file: file,
}

return result
}

// String implements the fmt.Stringer interface
func (s *RPMSpec) String() string {
return fmt.Sprintf("(cmd=%s, spec=%s)", s.cmd, s.file)
}

// Equal compares the fields of this instance to the given one
func (s *RPMSpec) Equal(o *RPMSpec) bool {
if o == nil {
return s == nil
}

return s.cmd == o.cmd && s.file == o.file
}

// ParseVersionInfo implements the parser.VersionParser interface
func (s *RPMSpec) ParseVersionInfo() (*buildinfo.VersionInfo, error) {
result := buildinfo.NewVersionInfo()

if version, err := s.rpmspecQuery(versionMacro); err != nil {
return nil, err
} else if version != "" {
result.Version = version
}

if revision, err := s.rpmspecQuery(revisionMacro); err != nil {
return nil, err
} else if revision != "" {
result.Revision = revision
}

return result, nil
}

func (s *RPMSpec) rpmspecQuery(query string) (string, error) {
return util.RunCmd(s.cmd, []string{"--query", "--queryformat", query, s.file})
}
143 changes: 143 additions & 0 deletions tools/parser/rpmspec/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package rpmspec

import (
"os"
"testing"

"gotest.tools/v3/assert"

"github.com/UiP9AV6Y/buildinfo"
)

func mockRPMSPECBin() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
mockPath := wd + "/testdata"

os.Setenv("PATH", mockPath)

return mockPath + "/rpmspec-mock.sh", nil
}

func TestTryParse(t *testing.T) {
type testCase struct {
haveCmd, havePath string
wantError bool
want *RPMSpec
}

rpmspecBin, err := mockRPMSPECBin()
if err != nil {
t.Fatal(err)
}

testCases := map[string]testCase{
"not in PATH": {
haveCmd: "rpmspec-notexists",
wantError: true,
},
"no spec file": {
haveCmd: "rpmspec-mock.sh",
havePath: "testdata/nospec",
wantError: true,
},
"broken spec file": {
haveCmd: "rpmspec-mock.sh",
havePath: "testdata/broken",
want: New(rpmspecBin, "testdata/broken/broken.spec"),
},
"macro spec file": {
haveCmd: "rpmspec-mock.sh",
havePath: "testdata/macro",
want: New(rpmspecBin, "testdata/macro/macro.spec"),
},
"minimal spec file": {
haveCmd: "rpmspec-mock.sh",
havePath: "testdata/minimal",
want: New(rpmspecBin, "testdata/minimal/minimal.spec"),
},
"multiple spec files": {
haveCmd: "rpmspec-mock.sh",
havePath: "testdata/multiple",
want: New(rpmspecBin, "testdata/multiple/multiple.spec"),
},
"relative bin": {
haveCmd: "rpmspec-mock.sh",
havePath: "testdata/minimal",
want: New(rpmspecBin, "testdata/minimal/minimal.spec"),
},
"absolute bin": {
haveCmd: rpmspecBin,
havePath: "testdata/minimal",
want: New(rpmspecBin, "testdata/minimal/minimal.spec"),
},
}

for ctx, tc := range testCases {
t.Run(ctx, func(t *testing.T) {
got, err := TryParse(tc.haveCmd, tc.havePath)

if tc.wantError {
assert.Assert(t, err != nil)
} else {
assert.Assert(t, err)
assert.Assert(t, tc.want.Equal(got), "want=%s; got=%s", tc.want, got)
}
})
}
}

func TestParseVersionInfo(t *testing.T) {
type testCase struct {
have *RPMSpec
wantError bool
want *buildinfo.VersionInfo
}

rpmspecBin, err := mockRPMSPECBin()
if err != nil {
t.Fatal(err)
}

testCases := map[string]testCase{
"broken": {
have: New(rpmspecBin, "testdata/broken/broken.spec"),
wantError: true,
},
"nospec": {
have: New(rpmspecBin, "testdata/nospec/nospec.spec"),
wantError: true,
},
"macro": {
have: New(rpmspecBin, "testdata/macro/macro.spec"),
want: &buildinfo.VersionInfo{
Version: "1.2.3~19701230gitd5a3191",
Revision: "1.rhel",
Branch: "trunk",
},
},
"minimal": {
have: New(rpmspecBin, "testdata/minimal/minimal.spec"),
want: &buildinfo.VersionInfo{
Version: "1.0",
Revision: "1",
Branch: "trunk",
},
},
}

for ctx, tc := range testCases {
t.Run(ctx, func(t *testing.T) {
got, err := tc.have.ParseVersionInfo()

if tc.wantError {
assert.Assert(t, err != nil)
} else {
assert.Assert(t, err)
assert.Assert(t, tc.want.Equal(got), "want=%s; got=%s", tc.want, got)
}
})
}
}
Loading
Loading