diff --git a/main.go b/main.go index cde02e6..41e2e27 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "path" "text/template" + "github.com/dmgk/modules2tuple/spack" "github.com/dmgk/modules2tuple/tuple" ) @@ -28,7 +29,21 @@ func main() { parser := tuple.NewParser(flagPackagePrefix, flagOffline) tuples, errors := parser.Load(args[0]) if len(tuples) != 0 { - fmt.Println(tuples) + if !flagSpack { + fmt.Println(tuples) + } else { + resources, err := spack.ResourcesFromTuples(flagAppVersion, tuples) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + json, err := resources.ToJson() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + fmt.Println(string(json)) + } } if errors != nil { fmt.Println(errors) @@ -53,7 +68,9 @@ this commit ID translation can be disabled with -offline flag. var ( flagOffline = false + flagSpack = false flagPackagePrefix = "vendor" + flagAppVersion = "" flagVersion = false ) @@ -63,7 +80,9 @@ func init() { basename := path.Base(os.Args[0]) flag.BoolVar(&flagOffline, "offline", flagOffline, "disable network access") + flag.BoolVar(&flagSpack, "spack", flagSpack, "print Spack resource stanzas") flag.StringVar(&flagPackagePrefix, "prefix", "vendor", "package prefix") + flag.StringVar(&flagAppVersion, "app_version", "", "application version (for spack)") flag.BoolVar(&flagVersion, "v", flagVersion, "show version") flag.Usage = func() { diff --git a/spack/resource.go b/spack/resource.go new file mode 100644 index 0000000..6c49201 --- /dev/null +++ b/spack/resource.go @@ -0,0 +1,88 @@ +package spack + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + + "github.com/dmgk/modules2tuple/tuple" +) + +type Resource struct { + Name string `json:"name,omitempty"` + Git string `json:"git,omitempty"` + Tag string `json:"tag,omitempty"` + Commit string `json:"commit,omitempty"` + Placement string `json:"placement,omitempty"` + When string `json:"when,omitempty"` + Destination string `json:"destination,omitempty"` +} + +type Resources []*Resource + +func (r Resources) ToJson() ([]byte, error) { + b, err := json.Marshal(r) + if err != nil { + return nil, err + } + return b, nil +} + +func (r Resource) ToJson() ([]byte, error) { + b, err := json.Marshal(r) + if err != nil { + return nil, err + } + return b, nil +} + +func ResourcesFromTuples(appVersion string, tt tuple.Tuples) (Resources, error) { + resources := make(Resources, 0, len(tt)) + + for _, t := range tt { + r, err := resourceFromTuple(appVersion, t) + if err != nil { + return nil, err + } + resources = append(resources, r) + } + + return resources, nil +} + +func resourceFromTuple(appVersion string, t *tuple.Tuple) (*Resource, error) { + r := Resource{ + Name: t.Package, + Destination: ".", + Placement: fmt.Sprintf("%s/%s", t.Prefix, t.Package), + } + + st := reflect.TypeOf(t.Source) + switch st.String() { + case "tuple.GH": + repoSite := "https://github.com" // tuple.GH.Site() returns ""... + r.Git = fmt.Sprintf("%s/%s/%s", repoSite, t.Account, t.Project) + case "tuple.GL": + repoSite := t.Source.Site() + r.Git = fmt.Sprintf("%s/%s/%s", repoSite, t.Account, t.Project) + default: + return nil, fmt.Errorf("Unknown site type: %s", st.String()) + } + + matched, err := regexp.MatchString("[0-9a-f]{12}", t.Tag) + if err != nil { + return nil, err + } + if matched { + r.Commit = t.Tag + } else { + r.Tag = t.Tag + } + + if appVersion != "" { + r.When = "@" + appVersion + } + + return &r, nil +} diff --git a/spack/resource_test.go b/spack/resource_test.go new file mode 100644 index 0000000..17d500c --- /dev/null +++ b/spack/resource_test.go @@ -0,0 +1,119 @@ +package spack + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/dmgk/modules2tuple/tuple" +) + +type testCase struct { + AppVersion string + Tuple *tuple.Tuple + Expected string +} + +// TestResource tests that an input tuple generates the expected +// resource string. +func TestResource(t *testing.T) { + testCases := []testCase{ + // This should use "commit" + {AppVersion: "1.2.3", + Tuple: tParse("github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340"), + Expected: ` + { + "name": "github.com/rivo/uniseg", + "git": "https://github.com/rivo/uniseg", + "commit": "f699dde9c340", + "placement": "vendor/github.com/rivo/uniseg", + "when": "@1.2.3", + "destination": "." + }`, + }, + // This should use "tag" and resolve to github + {AppVersion: "1.2.3", + Tuple: tParse("golang.org/x/text v0.3.2"), + Expected: ` + { + "name": "golang.org/x/text", + "git": "https://github.com/golang/text", + "tag": "v0.3.2", + "placement": "vendor/golang.org/x/text", + "when": "@1.2.3", + "destination": "." + }`, + }, + // empty AppVersion => no when + {AppVersion: "", + Tuple: tParse("golang.org/x/text v0.3.2"), + Expected: ` + { + "name": "golang.org/x/text", + "git": "https://github.com/golang/text", + "tag": "v0.3.2", + "placement": "vendor/golang.org/x/text", + "destination": "." + }`, + }, + // A nonstandard, GitLab repo + {AppVersion: "", + Tuple: tParse("howett.net/plist v0.0.0-20181124034731-591f970eefbb"), + Expected: ` + { + "name": "howett.net/plist", + "git": "https://gitlab.howett.net/go/plist", + "commit": "591f970eefbb", + "placement": "vendor/howett.net/plist", + "destination": "." + }`, + }, + } + + for _, c := range testCases { + expected := []byte(c.Expected) + r, err := resourceFromTuple(c.AppVersion, c.Tuple) + if err != nil { + t.Error(err) + } + got, err := r.ToJson() + if err != nil { + t.Error(err) + } + equal, err := jsonCompare(expected, got) + if err != nil { + t.Error(err) + } + if !equal { + t.Errorf("Got: %s, expected %s", r, c.Expected) + } + } +} + +// tParse is a helper function that parses a tuple string and returns +// the resulting *tuple.Tuple. It panics if there's a problem parsing +// the string, since that's not what we're testing here. +func tParse(t string) *tuple.Tuple { + tuple, err := tuple.Parse(t, "vendor") + if err != nil { + panic(err) + } + return tuple +} + +// After https://gist.github.com/turtlemonvh/e4f7404e28387fadb8ad275a99596f67 +func jsonCompare(j1, j2 []byte) (bool, error) { + var o1 interface{} + var o2 interface{} + + err := json.Unmarshal([]byte(j1), &o1) + if err != nil { + return false, err + } + err = json.Unmarshal([]byte(j2), &o2) + if err != nil { + return false, err + } + + return reflect.DeepEqual(o1, o2), nil +}