diff --git a/command/install.go b/command/install.go index 5deb2ca..7a57607 100644 --- a/command/install.go +++ b/command/install.go @@ -5,7 +5,6 @@ import ( "fmt" "runtime" "errors" - "sort" "strings" "os" "io/ioutil" @@ -14,40 +13,47 @@ import ( "io" "github.com/shyiko/jabba/cfg" "github.com/shyiko/jabba/semver" - wmark "github.com/wmark/semver" log "github.com/Sirupsen/logrus" "regexp" "github.com/mitchellh/ioprogress" + "sort" + "archive/zip" ) -func Install(qualifier string) (ver string, err error) { - var releaseMap map[string]string - if strings.Contains(qualifier, "=") { - // = - split := strings.SplitN(qualifier, "=", 2) +func Install(selector string) (string, error) { + var releaseMap map[*semver.Version]string + var ver *semver.Version + var err error + // selector can be in form of = + if strings.Contains(selector, "=") { + split := strings.SplitN(selector, "=", 2) + selector = split[0] // has to be valid per semver - _, err = wmark.NewVersion(split[0]) + ver, err = semver.ParseVersion(selector) if err != nil { - return + return "", err } - qualifier = split[0] - ver = qualifier - releaseMap = map[string]string{qualifier: split[1]} - } - // check whether it's already installed - local, err := Ls() - if err != nil { - return + releaseMap = map[*semver.Version]string{ver: split[1]} + } else { + // ... or a version (range will be tried over remote targets) + ver, _ = semver.ParseVersion(selector) } - i := sort.Search(len(local), func(i int) bool { - return local[i] <= qualifier - }) - if i < len(local) && local[i] == qualifier { - // already installed - return qualifier, nil + // check whether requested version is already installed + if ver != nil { + local, err := Ls() + if err != nil { + return "", err + } + for _, v := range local { + if ver.Equals(v) { + return ver.String(), nil + } + } } + // ... apparently it's not if releaseMap == nil { - rng, err := wmark.NewRange(qualifier) + ver = nil + rng, err := semver.ParseRange(selector) if err != nil { return "", err } @@ -55,31 +61,34 @@ func Install(qualifier string) (ver string, err error) { if err != nil { return "", err } - var vs = make([]string, len(releaseMap)) + var vs = make([]*semver.Version, len(releaseMap)) var i = 0 for k := range releaseMap { vs[i] = k i++ } - vs = semver.Sort(vs) - for i := range vs { - v, _ := wmark.NewVersion(vs[i]) + sort.Sort(sort.Reverse(semver.VersionSlice(vs))) + for _, v := range vs { if rng.Contains(v) { - ver = vs[i] + ver = v break } } - if ver == "" { - return ver, errors.New("No compatible version found for " + qualifier + - "\nValid install targets: " + strings.Join(vs, ", ")) + if ver == nil { + tt := make([]string, len(vs)) + for i, v := range vs { + tt[i] = v.String() + } + return "", errors.New("No compatible version found for " + selector + + "\nValid install targets: " + strings.Join(tt, ", ")) } } url := releaseMap[ver] if matched, _ := regexp.MatchString("^\\w+[+]\\w+://", url); !matched { - return ver, errors.New("URL must contain qualifier, e.g. tgz+http://...") + return "", errors.New("URL must contain qualifier, e.g. tgz+http://...") } var fileType string = url[0:strings.Index(url, "+")] - url = url[strings.Index(url, "+") + 1:len(url)] + url = url[strings.Index(url, "+") + 1:] var file string var deleteFileWhenFinnished bool if strings.HasPrefix(url, "file://") { @@ -88,22 +97,22 @@ func Install(qualifier string) (ver string, err error) { log.Info("Downloading ", ver, " (", url, ")") file, err = download(url) if err != nil { - return + return "", err } deleteFileWhenFinnished = true } switch runtime.GOOS { case "darwin": - err = installOnDarwin(ver, file, fileType) + err = installOnDarwin(ver.String(), file, fileType) case "linux": - err = installOnLinux(ver, file, fileType) + err = installOnLinux(ver.String(), file, fileType) default: err = errors.New(runtime.GOOS + " OS is not supported") } if err == nil && deleteFileWhenFinnished { os.Remove(file) } - return + return ver.String(), err } type RedirectTracer struct { @@ -167,47 +176,57 @@ func download(url string) (file string, err error) { return } -func installOnDarwin(ver string, file string, fileType string) error { - if fileType != "dmg" { +func installOnDarwin(ver string, file string, fileType string) (err error) { + target := cfg.Dir() + "/jdk/" + ver + switch fileType { + case "dmg": + err = installFromDmg(file, target) + case "zip": + err = installFromZip(file, target + "/Contents/Home") + default: return errors.New(fileType + " is not supported") } + if err == nil { + err = assertContentIsValid(target + "/Contents/Home") + } + if err != nil { + os.RemoveAll(target) + } + return +} + +func installFromDmg(source string, target string) error { tmp, err := ioutil.TempDir("", "jabba-i-") if err != nil { return err } - basename := path.Base(file) + basename := path.Base(source) mountpoint := tmp + "/" + basename - target := cfg.Dir() + "/jdk/" + ver - err = executeSH([][]string{ - []string{"Mounting " + file, "hdiutil mount -mountpoint " + mountpoint + " " + file}, - []string{"Extracting " + file + " to " + target, - "pkgutil --expand " + mountpoint + "/*.pkg " + tmp + "/" + basename + "-pkg"}, + pkgdir := tmp + "/" + basename + "-pkg" + err = executeInShell([][]string{ + []string{"Mounting " + source, "hdiutil mount -mountpoint " + mountpoint + " " + source}, + []string{"Extracting " + source + " to " + target, + "pkgutil --expand " + mountpoint + "/*.pkg " + pkgdir}, []string{"", "mkdir -p " + target}, + // todo: instead of relying on a certain pkg structure - find'n'extract all **/*/Payload + // oracle - []string{"", "if [ -f " + tmp + "/" + basename + "-pkg/jdk*.pkg/Payload" + " ]; then " + - "tar xvf " + tmp + "/" + basename + "-pkg/jdk*.pkg/Payload -C " + target + - "; fi"}, + []string{"", + "if [ -f " + pkgdir + "/jdk*.pkg/Payload" + " ]; then " + + "tar xvf " + pkgdir + "/jdk*.pkg/Payload -C " + target + + "; fi"}, // apple - []string{"", "if [ -f " + tmp + "/" + basename + "-pkg/JavaForOSX.pkg/Payload" + " ]; then " + - "tar -xzf " + tmp + "/" + basename + "-pkg/JavaForOSX.pkg/Payload -C " + tmp + "/" + basename + "-pkg &&" + - "mv " + tmp + "/" + basename + "-pkg/Library/Java/JavaVirtualMachines/*/Contents " + target + "/Contents" + - "; fi"}, + []string{"", + "if [ -f " + pkgdir + "/JavaForOSX.pkg/Payload" + " ]; then " + + "tar xzf " + pkgdir + "/JavaForOSX.pkg/Payload -C " + pkgdir + " &&" + + "mv " + pkgdir + "/Library/Java/JavaVirtualMachines/*/Contents " + target + "/Contents" + + "; fi"}, - []string{"Unmounting " + file, "hdiutil unmount " + mountpoint}, + []string{"Unmounting " + source, "hdiutil unmount " + mountpoint}, }) if err == nil { - if _, err := os.Stat(target + "/Contents/Home/bin/java"); os.IsNotExist(err) { - err = errors.New("Unsupported DMG structure. " + - "Please open a ticket at https://github.com/shyiko/jabba/issue " + - "(specify URI you tried to install)") - } - } - if err != nil { - // remove target ~/.jabba/jdk/ - os.RemoveAll(target) - } else { os.RemoveAll(tmp) } return err @@ -215,41 +234,109 @@ func installOnDarwin(ver string, file string, fileType string) error { func installOnLinux(ver string, file string, fileType string) (err error) { target := cfg.Dir() + "/jdk/" + ver - var cmd [][]string - var tmp string switch fileType { case "bin": - tmp, err = ioutil.TempDir("", "jabba-i-") - if err != nil { - return - } - cmd = [][]string{ - []string{"", "mv " + file + " " + tmp}, - []string{"Extracting " + path.Join(tmp, path.Base(file)) + " to " + target, - "cd " + tmp + " && echo | sh " + path.Base(file) + " && mv jdk*/ " + target}, - } + err = installFromBin(file, target) case "tgz": - cmd = [][]string{ - []string{"", "mkdir -p " + target}, - []string{"Extracting " + file + " to " + target, - "tar xvf " + file + " --strip-components=1 -C " + target}, - } + err = installFromTgz(file, target) + case "zip": + err = installFromZip(file, target) default: return errors.New(fileType + " is not supported") } - err = executeSH(cmd) + if err == nil { + err = assertContentIsValid(target) + } if err != nil { - // remove target ~/.jabba/jdk/ os.RemoveAll(target) - } else { - if tmp != "" { - os.RemoveAll(tmp) - } } return } -func executeSH(cmd [][]string) error { +func installFromBin(source string, target string) (err error) { + tmp, err := ioutil.TempDir("", "jabba-i-") + if err != nil { + return + } + err = executeInShell([][]string{ + []string{"", "mv " + source + " " + tmp}, + []string{"Extracting " + path.Join(tmp, path.Base(source)) + " to " + target, + "cd " + tmp + " && echo | sh " + path.Base(source) + " && mv jdk*/ " + target}, + }) + if err == nil { + os.RemoveAll(tmp) + } + return +} + +func installFromTgz(source string, target string) error { + return executeInShell([][]string{ + []string{"", "mkdir -p " + target}, + []string{"Extracting " + source + " to " + target, + "tar xvf " + source + " --strip-components=1 -C " + target}, + }) +} + +func installFromZip(source string, target string) error { + log.Info("Extracting " + source + " to " + target) + return unzip(source, target, true) +} + +func unzip(source string, target string, strip bool) error { + r, err := zip.OpenReader(source) + if err != nil { + return err + } + defer r.Close() + var prefixToStrip = "" + if strip { + entriesPerLevel := make(map[int]int) + prefixMap := make(map[int]string) + for _, f := range r.File { + level := 0 + for _, c := range f.Name { + if c == '/' { + level++ + } + } + if !f.Mode().IsDir() { + level++ + } else { + prefixMap[level] = f.Name + } + entriesPerLevel[level]++ + } + for i := 0; i < len(entriesPerLevel); i++ { + if entriesPerLevel[i] > 1 && i > 0 { + prefixToStrip = prefixMap[i - 1] + break + } + } + } + for _, f := range r.File { + name := strings.TrimPrefix(f.Name, prefixToStrip) + if f.Mode().IsDir() { + os.MkdirAll(path.Join(target, name), 0755) + } else { + fr, err := f.Open() + if err != nil { + return err + } + f, err := os.OpenFile(path.Join(target, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + _, err = io.Copy(f, fr) + if err != nil { + return err + } + f.Close() + } + } + return nil +} + +func executeInShell(cmd [][]string) error { for _, command := range cmd { if command[0] != "" { log.Info(command[0]) @@ -262,3 +349,13 @@ func executeSH(cmd [][]string) error { } return nil } + +func assertContentIsValid(target string) error { + var err error + if _, err = os.Stat(target + "/bin/java"); os.IsNotExist(err) { + err = errors.New("/bin/java wasn't found. " + + "If you believe this is an error - please create a ticket at https://github.com/shyiko/jabba/issue " + + "(specify OS and version/URL you tried to install)") + } + return err +} diff --git a/command/ls-remote.go b/command/ls-remote.go index 89327c1..43546a0 100644 --- a/command/ls-remote.go +++ b/command/ls-remote.go @@ -5,24 +5,45 @@ import ( "encoding/json" "runtime" "net/http" - "github.com/shyiko/jabba/cfg" "errors" "strconv" + "strings" + "github.com/shyiko/jabba/cfg" + "github.com/shyiko/jabba/semver" ) type byOS map[string]byArch type byArch map[string]byDistribution type byDistribution map[string]map[string]string -func LsRemote() (map[string]string, error) { +func LsRemote() (map[*semver.Version]string, error) { cnt, err := fetch(cfg.Index()) if err != nil { return nil, err } var index byOS - // todo: handle deserialization error - json.Unmarshal(cnt, &index) - return index[runtime.GOOS][runtime.GOARCH]["jdk"], nil + err = json.Unmarshal(cnt, &index) + if err != nil { + return nil, err + } + releaseMap := make(map[*semver.Version]string) + for key, value := range index[runtime.GOOS][runtime.GOARCH] { + var prefix string + if key != "jdk" { + if !strings.Contains(key, "@") { + continue + } + prefix = key[strings.Index(key, "@") + 1:] + "@" + } + for ver, url := range value { + v, err := semver.ParseVersion(prefix + ver) + if err != nil { + return nil, err + } + releaseMap[v] = url + } + } + return releaseMap, nil } func fetch(url string) (content []byte, err error) { diff --git a/command/ls.go b/command/ls.go index 42abbe9..259432b 100644 --- a/command/ls.go +++ b/command/ls.go @@ -5,18 +5,45 @@ import ( "github.com/shyiko/jabba/cfg" "github.com/shyiko/jabba/semver" "path" + "sort" + "fmt" ) var readDir = ioutil.ReadDir -// returns installed versions in descending order -func Ls() ([]string, error) { +func Ls() ([]*semver.Version, error) { files, _ := readDir(path.Join(cfg.Dir(), "jdk")) - var r []string + var r []*semver.Version for _, f := range files { if f.IsDir() { - r = append(r, f.Name()) + v, err := semver.ParseVersion(f.Name()) + if err != nil { + return nil, err + } + r = append(r, v) } } - return semver.Sort(r), nil + sort.Sort(sort.Reverse(semver.VersionSlice(r))) + return r, nil +} + +func LsBestMatch(selector string) (ver string, err error) { + local, err := Ls() + if err != nil { + return + } + rng, err := semver.ParseRange(selector) + if err != nil { + return + } + for _, v := range local { + if rng.Contains(v) { + ver = v.String() + break + } + } + if ver == "" { + err = fmt.Errorf("%s isn't installed", rng) + } + return } diff --git a/command/uninstall.go b/command/uninstall.go index e13de29..20209bb 100644 --- a/command/uninstall.go +++ b/command/uninstall.go @@ -4,47 +4,12 @@ import ( "github.com/shyiko/jabba/cfg" "path" "os" - "sort" - "github.com/wmark/semver" - "errors" ) -func Uninstall(ver string) error { - resolved, err := resolveLocal(ver) +func Uninstall(selector string) error { + ver, err := LsBestMatch(selector) if err != nil { return err } - return os.RemoveAll(path.Join(cfg.Dir(), "jdk", resolved)) -} - -func resolveLocal(ver string) (string, error) { - local, err := Ls() - if err != nil { - return "", err - } - i := sort.Search(len(local), func(i int) bool { - return local[i] <= ver - }) - var resolved string - if i < len(local) && local[i] == ver { - resolved = ver - } - if resolved == "" { - // ver might be a range - rng, err := semver.NewRange(ver) - if err != nil { - return "", err - } - for i := range local { - v, _ := semver.NewVersion(local[i]) - if rng.Contains(v) { - resolved = local[i] - break - } - } - } - if resolved == "" { - return "", errors.New(ver + " isn't installed") - } - return resolved, nil + return os.RemoveAll(path.Join(cfg.Dir(), "jdk", ver)) } diff --git a/command/use.go b/command/use.go index d53b485..6da5397 100644 --- a/command/use.go +++ b/command/use.go @@ -8,12 +8,12 @@ import ( "regexp" ) -func Use(ver string) ([]string, error) { - aliasValue := GetAlias(ver) +func Use(selector string) ([]string, error) { + aliasValue := GetAlias(selector) if aliasValue != "" { - ver = aliasValue + selector = aliasValue } - resolved, err := resolveLocal(ver) + ver, err := LsBestMatch(selector) if err != nil { return nil, err } @@ -21,7 +21,7 @@ func Use(ver string) ([]string, error) { rgxp := regexp.MustCompile(regexp.QuoteMeta(path.Join(cfg.Dir(), "jdk")) + "[^:]+[:]") // strip references to ~/.jabba/jdk/*, otherwise leave unchanged pth = rgxp.ReplaceAllString(pth, "") - javaHome := path.Join(cfg.Dir(), "jdk", resolved) + javaHome := path.Join(cfg.Dir(), "jdk", ver) if runtime.GOOS == "darwin" { javaHome = path.Join(javaHome, "Contents", "Home") } diff --git a/command/which.go b/command/which.go index 85cf14c..7accabe 100644 --- a/command/which.go +++ b/command/which.go @@ -5,10 +5,14 @@ import ( "github.com/shyiko/jabba/cfg" ) -func Which(ver string) (string, error) { - resolved, err := resolveLocal(ver) +func Which(selector string) (string, error) { + aliasValue := GetAlias(selector) + if aliasValue != "" { + selector = aliasValue + } + ver, err := LsBestMatch(selector) if err != nil { return "", err } - return path.Join(cfg.Dir(), "jdk", resolved), nil + return path.Join(cfg.Dir(), "jdk", ver), nil } diff --git a/jabba.go b/jabba.go index 93cc34d..bc9fd72 100644 --- a/jabba.go +++ b/jabba.go @@ -8,6 +8,7 @@ import ( "github.com/shyiko/jabba/command" "github.com/shyiko/jabba/semver" log "github.com/Sirupsen/logrus" + "sort" ) var version string @@ -105,13 +106,14 @@ func main() { if err != nil { log.Fatal(err) } - var vs = make([]string, len(releaseMap)) + var vs = make([]*semver.Version, len(releaseMap)) var i = 0 for k := range releaseMap { vs[i] = k i++ } - for _, v := range semver.Sort(vs) { + sort.Sort(sort.Reverse(semver.VersionSlice(vs))) + for _, v := range vs { fmt.Println(v) } return nil diff --git a/semver/range.go b/semver/range.go new file mode 100644 index 0000000..afab617 --- /dev/null +++ b/semver/range.go @@ -0,0 +1,37 @@ +package semver + +import ( + "github.com/wmark/semver" + "strings" + "fmt" +) + +type Range struct { + qualifier string + raw string + rng *semver.Range +} + +func (l *Range) Contains(r *Version) bool { + return l.qualifier == r.qualifier && l.rng.Contains(r.ver) +} + +func (t *Range) String() string { + return t.raw +} + +func ParseRange(raw string) (*Range, error) { + p := new(Range) + p.raw = raw + // selector can be either or @ + if strings.Contains(raw, "@") { + p.qualifier = raw[0:strings.Index(raw, "@")] + raw = raw[strings.Index(raw, "@") + 1:len(raw)] + } + parsed, err := semver.NewRange(raw) + if err != nil { + return nil, fmt.Errorf("%s is not a valid version", raw) + } + p.rng = parsed + return p, nil +} diff --git a/semver/range_test.go b/semver/range_test.go new file mode 100644 index 0000000..90fc1c7 --- /dev/null +++ b/semver/range_test.go @@ -0,0 +1,32 @@ +package semver + +import ( + "testing" +) + +func TestContains(t *testing.T) { + assertWithinRange(t, "1.8", "1.8.0", true) + assertWithinRange(t, "1.7", "1.8.0", false) + assertWithinRange(t, "1.8.0-0", "1.8.0-0", true) + assertWithinRange(t, "1.8.0-0", "1.8.0-1", false) + assertWithinRange(t, "~1.8", "1.8.99", true) + assertWithinRange(t, "~1.8", "1.9.0", false) + assertWithinRange(t, "a@1.8", "a@1.8.72", true) + assertWithinRange(t, "1.8", "a@1.8.72", false) + assertWithinRange(t, "a@1.8", "b@1.8.72", false) + assertWithinRange(t, "a@1.8", "1.8.72", false) +} + +func assertWithinRange(t *testing.T, rng string, ver string, value bool) { + r, err := ParseRange(rng) + if err != nil { + t.Fatalf("err: %v", err) + } + v, err := ParseVersion(ver) + if err != nil { + t.Fatalf("err: %v", err) + } + if r.Contains(v) != value { + t.Fatalf("expected range %v to contain %v (%v)", rng, ver, value) + } +} diff --git a/semver/sort.go b/semver/sort.go deleted file mode 100644 index b34d21a..0000000 --- a/semver/sort.go +++ /dev/null @@ -1,47 +0,0 @@ -package semver - -import ( - "sort" - "github.com/wmark/semver" -) - -type Version struct { - Raw string - Parsed *semver.Version -} - -func NewVersion(raw string) *Version { - p := new(Version) - p.Raw = raw - parsed, err := semver.NewVersion(raw) - if err != nil { - panic(raw + " is not a valid version") - } - p.Parsed = parsed - return p -} - -type VersionSlice []*Version - -func (c VersionSlice) Len() int { - return len(c) -} -func (c VersionSlice) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} -func (c VersionSlice) Less(i, j int) bool { - return c[i].Parsed.Less(c[j].Parsed) -} - -func Sort(vs []string) []string { - var svs = make([]*Version, len(vs)) - for i, v := range vs { - svs[i] = NewVersion(v) - } - sort.Sort(sort.Reverse(VersionSlice(svs))) - for i, v := range svs { - vs[i] = v.Raw - } - return vs -} - diff --git a/semver/sort_test.go b/semver/sort_test.go deleted file mode 100644 index 616d4a1..0000000 --- a/semver/sort_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package semver - -import ( - "testing" - "reflect" -) - -func TestSort(t *testing.T) { - actual := Sort([]string{"0.2.0", "0.1.20", "0.1.10", "0.1.2"}) - expected := []string{"0.2.0", "0.1.20", "0.1.10", "0.1.2"} - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("actual: %v != expected: %v", actual, expected) - } -} diff --git a/semver/version.go b/semver/version.go new file mode 100644 index 0000000..bf48ff4 --- /dev/null +++ b/semver/version.go @@ -0,0 +1,58 @@ +package semver + +import ( + "github.com/wmark/semver" + "strings" + "fmt" +) + +type Version struct { + qualifier string + raw string + ver *semver.Version +} + +func (l *Version) LessThan(r *Version) bool { + if l.qualifier == r.qualifier { + return l.ver.Less(r.ver) + } + return l.qualifier > r.qualifier +} + +func (l *Version) Equals(r *Version) bool { + return l.raw == r.raw +} + +func (t *Version) String() string { + return t.raw +} + +func ParseVersion(raw string) (*Version, error) { + p := new(Version) + p.raw = raw + // selector can be either or @ + if strings.Contains(raw, "@") { + p.qualifier = raw[0:strings.Index(raw, "@")] + raw = raw[strings.Index(raw, "@") + 1:len(raw)] + } + parsed, err := semver.NewVersion(raw) + if err != nil { + return nil, fmt.Errorf("%s is not a valid version", raw) + } + p.ver = parsed + return p, nil +} + +type VersionSlice []*Version + +// impl sort.Interface: + +func (c VersionSlice) Len() int { + return len(c) +} +func (c VersionSlice) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} +func (c VersionSlice) Less(i, j int) bool { + return c[i].LessThan(c[j]) +} diff --git a/semver/version_test.go b/semver/version_test.go new file mode 100644 index 0000000..f253c35 --- /dev/null +++ b/semver/version_test.go @@ -0,0 +1,29 @@ +package semver + +import ( + "testing" + "reflect" + "sort" +) + +func TestSort(t *testing.T) { + actual := asVersionSlice(t, + "0.2.0", "a@1.8.10", "b@1.8.2", "0.1.20", "a@1.8.2", "0.1.10", "0.1.2") + sort.Sort(sort.Reverse(VersionSlice(actual))) + expected := asVersionSlice(t, + "0.2.0", "0.1.20", "0.1.10", "0.1.2", "a@1.8.10", "a@1.8.2", "b@1.8.2") + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("actual: %v != expected: %v", actual, expected) + } +} + +func asVersionSlice(t *testing.T, slice ...string) (r []*Version) { + for _, value := range slice { + ver, err := ParseVersion(value) + if err != nil { + t.Fatalf("err: %v", err) + } + r = append(r, ver) + } + return +}