Skip to content

Commit

Permalink
Merge pull request #6 from UiP9AV6Y/feature/tools_rpmspec_parser
Browse files Browse the repository at this point in the history
tools: implement version info parser based around `rpmspec`
  • Loading branch information
UiP9AV6Y authored Mar 16, 2024
2 parents 3abef7d + 2131606 commit 2a0a49f
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 55 deletions.
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

0 comments on commit 2a0a49f

Please sign in to comment.