├── .gitattributes ├── .gitignore ├── w32 ├── w32_darwin.go ├── w32_linux.go ├── w32.go └── w32_windows.go ├── .editorconfig ├── command ├── uninstall.go ├── current_test.go ├── which.go ├── alias.go ├── current.go ├── deactivate.go ├── ls-alias.go ├── ls.go ├── use.go ├── deactivate_test.go ├── ls-remote.go ├── use_test.go ├── fileiter │ ├── iterator.go │ └── iterator_test.go ├── install_test.go ├── link.go └── install.go ├── .github ├── workflows │ └── build.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── cfg └── cfg.go ├── semver ├── version_test.go ├── range_test.go ├── range.go └── version.go ├── jabbaw.ps1 ├── jabbaw ├── jabbaw.md ├── go.mod ├── Makefile ├── install.ps1 ├── go.sum ├── CHANGELOG.md ├── install.sh ├── README.md ├── LICENSE └── jabba.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | jabba 2 | coverage.html 3 | release 4 | vendor 5 | .tmp 6 | -------------------------------------------------------------------------------- /w32/w32_darwin.go: -------------------------------------------------------------------------------- 1 | package w32 2 | 3 | func ShellExecuteAndWait(hwnd HWND, lpOperation, lpFile, lpParameters, lpDirectory string, nShowCmd int) error { 4 | panic("Unsupported OS") 5 | } 6 | 7 | func ShellExecuteEx(pExecInfo *SHELLEXECUTEINFO) error { 8 | panic("Unsupported OS") 9 | } 10 | -------------------------------------------------------------------------------- /w32/w32_linux.go: -------------------------------------------------------------------------------- 1 | package w32 2 | 3 | func ShellExecuteAndWait(hwnd HWND, lpOperation, lpFile, lpParameters, lpDirectory string, nShowCmd int) error { 4 | panic("Unsupported OS") 5 | } 6 | 7 | func ShellExecuteEx(pExecInfo *SHELLEXECUTEINFO) error { 8 | panic("Unsupported OS") 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.go] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [Makefile] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /command/uninstall.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/Jabba-Team/jabba/cfg" 8 | ) 9 | 10 | func Uninstall(selector string) error { 11 | ver, err := LsBestMatch(selector) 12 | if err != nil { 13 | return err 14 | } 15 | return os.RemoveAll(filepath.Join(cfg.Dir(), "jdk", ver)) 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: make Jabba 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | make: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-go@v2 15 | with: 16 | stable: 'false' 17 | go-version: '1.19.4' 18 | - name: fetch 19 | run: make fetch 20 | - name: test 21 | run: make test 22 | 23 | -------------------------------------------------------------------------------- /command/current_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/Jabba-Team/jabba/cfg" 8 | ) 9 | 10 | func TestCurrent(t *testing.T) { 11 | var prevLookPath = lookPath 12 | defer func() { lookPath = prevLookPath }() 13 | lookPath = func(file string) (string, error) { 14 | return filepath.Join(cfg.Dir(), "jdk", "1.8.0", "Contents", "Home", "bin", "java"), nil 15 | } 16 | actual := Current() 17 | expected := "1.8.0" 18 | if actual != expected { 19 | t.Fatalf("actual: %v != expected: %v", actual, expected) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /command/which.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | 7 | "github.com/Jabba-Team/jabba/cfg" 8 | ) 9 | 10 | func Which(selector string, home bool) (string, error) { 11 | aliasValue := GetAlias(selector) 12 | if aliasValue != "" { 13 | selector = aliasValue 14 | } 15 | ver, err := LsBestMatch(selector) 16 | if err != nil { 17 | return "", err 18 | } 19 | path := filepath.Join(cfg.Dir(), "jdk", ver) 20 | if home && runtime.GOOS == "darwin" { 21 | path = filepath.Join(path, "Contents", "Home") 22 | } 23 | return path, nil 24 | } 25 | -------------------------------------------------------------------------------- /command/alias.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/Jabba-Team/jabba/cfg" 9 | ) 10 | 11 | func SetAlias(name string, ver string) (err error) { 12 | if ver == "" { 13 | err = os.Remove(filepath.Join(cfg.Dir(), name+".alias")) 14 | } else { 15 | err = ioutil.WriteFile(filepath.Join(cfg.Dir(), name+".alias"), []byte(ver), 0666) 16 | } 17 | return 18 | } 19 | 20 | func GetAlias(name string) string { 21 | b, err := ioutil.ReadFile(filepath.Join(cfg.Dir(), name+".alias")) 22 | if err != nil { 23 | return "" 24 | } 25 | return string(b) 26 | } 27 | -------------------------------------------------------------------------------- /command/current.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/Jabba-Team/jabba/cfg" 10 | ) 11 | 12 | var lookPath = exec.LookPath 13 | 14 | func Current() string { 15 | javaPath, err := lookPath("java") 16 | if err == nil { 17 | prefix := filepath.Join(cfg.Dir(), "jdk") + string(os.PathSeparator) 18 | if strings.HasPrefix(javaPath, prefix) { 19 | index := strings.Index(javaPath[len(prefix):], string(os.PathSeparator)) 20 | if index != -1 { 21 | return javaPath[len(prefix) : len(prefix)+index] 22 | } 23 | } 24 | } 25 | return "" 26 | } 27 | -------------------------------------------------------------------------------- /cfg/cfg.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | log "github.com/Sirupsen/logrus" 8 | "github.com/mitchellh/go-homedir" 9 | ) 10 | 11 | func Dir() string { 12 | home := os.Getenv("JABBA_HOME") 13 | if home != "" { 14 | return filepath.Clean(home) 15 | } 16 | dir, err := homedir.Dir() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | return filepath.Join(dir, ".jabba") 21 | } 22 | 23 | func Index() string { 24 | registry := os.Getenv("JABBA_INDEX") 25 | if registry == "" { 26 | registry = "https://github.com/Jabba-Team/index/raw/main/index.json" 27 | } 28 | return registry 29 | } 30 | -------------------------------------------------------------------------------- /command/deactivate.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | 8 | "github.com/Jabba-Team/jabba/cfg" 9 | ) 10 | 11 | func Deactivate() ([]string, error) { 12 | pth, _ := os.LookupEnv("PATH") 13 | rgxp := regexp.MustCompile(regexp.QuoteMeta(filepath.Join(cfg.Dir(), "jdk")) + "[^:]+[:]") 14 | // strip references to ~/.jabba/jdk/*, otherwise leave unchanged 15 | pth = rgxp.ReplaceAllString(pth, "") 16 | javaHome, overrideWasSet := os.LookupEnv("JAVA_HOME_BEFORE_JABBA") 17 | if !overrideWasSet { 18 | javaHome, _ = os.LookupEnv("JAVA_HOME") 19 | } 20 | return []string{ 21 | "export PATH=\"" + pth + "\"", 22 | "export JAVA_HOME=\"" + javaHome + "\"", 23 | "unset JAVA_HOME_BEFORE_JABBA", 24 | }, nil 25 | } 26 | -------------------------------------------------------------------------------- /command/ls-alias.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | 7 | "github.com/Jabba-Team/jabba/cfg" 8 | "strings" 9 | ) 10 | 11 | func LsAlias() (map[string]string, error) { 12 | dir := cfg.Dir() // Assuming cfg.Dir() provides the directory containing alias files 13 | files, err := ioutil.ReadDir(dir) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | aliases := make(map[string]string) 19 | for _, file := range files { 20 | if strings.HasSuffix(file.Name(), ".alias") { 21 | filePath := filepath.Join(dir, file.Name()) 22 | content, err := ioutil.ReadFile(filePath) 23 | if err != nil { 24 | return nil, err 25 | } 26 | name := strings.TrimSuffix(file.Name(), ".alias") 27 | aliases[name] = string(content) 28 | } 29 | } 30 | 31 | return aliases, nil 32 | } 33 | -------------------------------------------------------------------------------- /semver/version_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func TestSort(t *testing.T) { 10 | actual := asVersionSlice(t, 11 | "0.2.0", "a@1.8.10", "b@1.8.2", "0.1.20", "a@1.8.2", "0.1.10", "0.1.2", "1.9.0-10.1", "1.9.0-9.60") 12 | sort.Sort(sort.Reverse(VersionSlice(actual))) 13 | expected := asVersionSlice(t, 14 | "1.9.0-10.1", "1.9.0-9.60", "0.2.0", "0.1.20", "0.1.10", "0.1.2", "a@1.8.10", "a@1.8.2", "b@1.8.2") 15 | if !reflect.DeepEqual(actual, expected) { 16 | t.Fatalf("actual: %v != expected: %v", actual, expected) 17 | } 18 | } 19 | 20 | func asVersionSlice(t *testing.T, slice ...string) (r []*Version) { 21 | for _, value := range slice { 22 | ver, err := ParseVersion(value) 23 | if err != nil { 24 | t.Fatalf("err: %v", err) 25 | } 26 | r = append(r, ver) 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. MacOS, Linux, Windows] 28 | - CPU Architecture: [e.g. x86_64, arm64] 29 | - Terminal [e.g. iTerm2, kitty, Windows Terminal] 30 | - Jabba Version [e.g. 0.14.0] 31 | - Install Method [e.g. Script, brew, scoop] 32 | 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /jabbaw.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $jabbaHome = if ($env:JABBA_HOME) { $env:JABBA_HOME } else { if ($env:JABBA_DIR) { $env:JABBA_DIR } else { "$env:USERPROFILE\.jabba" } } 4 | $jabbaVersion = if ($env:JABBA_VERSION) { $env:JABBA_VERSION } else { "latest" } 5 | 6 | if ($jabbaVersion -eq "latest") { 7 | # resolving "latest" to an actual tag 8 | $jabbaVersion = (Invoke-RestMethod https://api.github.com/repos/Jabba-Team/jabba/releases/latest).body 9 | } 10 | 11 | $ErrorActionPreference = "SilentlyContinue" 12 | & $jabbaHome\bin\jabba.exe --version | Out-Null 13 | $binaryValid = $? 14 | $ErrorActionPreference = "Continue" 15 | 16 | if ($binaryValid) { 17 | $realVersion = Invoke-Expression "$jabbaHome\bin\jabba.exe --version" 18 | } 19 | 20 | if (-not $binaryValid -or $jabbaVersion -ne $realVersion) { 21 | 22 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 23 | Invoke-Expression ( 24 | Invoke-WebRequest https://github.com/Jabba-Team/jabba/raw/main/install.ps1 -UseBasicParsing 25 | ).Content 26 | 27 | } 28 | 29 | Invoke-Expression "$jabbaHome\bin\jabba.exe install" 30 | $env:JAVA_HOME = Invoke-Expression "$jabbaHome\bin\jabba.exe which --home" 31 | Invoke-Expression "& $args" 32 | -------------------------------------------------------------------------------- /jabbaw: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | JABBA_HOME=${JABBA_HOME:-$JABBA_DIR} # JABBA_DIR is here for backward-compatibility 4 | JABBA_HOME=${JABBA_HOME:-$HOME/.jabba} 5 | JABBA_VERSION=${JABBA_VERSION:-latest} 6 | 7 | has_command() { 8 | command -v "$1" > /dev/null 2>&1 9 | } 10 | 11 | get() { 12 | if has_command curl ; then 13 | # curl looks for HTTPS_PROXY while wget for https_proxy 14 | https_proxy=${https_proxy:-$HTTPS_PROXY} 15 | curl -sL "$1" 16 | elif has_command wget ; then 17 | # curl looks for HTTPS_PROXY while wget for https_proxy 18 | HTTPS_PROXY=${HTTPS_PROXY:-$https_proxy} 19 | wget -qO- "$1" 20 | else 21 | echo "[ERROR] This script needs wget or curl to be installed." 22 | exit 1 23 | fi 24 | } 25 | 26 | if [ "$JABBA_VERSION" == "latest" ]; then 27 | # resolving "latest" to an actual tag 28 | JABBA_VERSION=$(get https://Jabba-Team.github.io/jabba/latest) 29 | fi 30 | 31 | if [ ! -f "${JABBA_HOME}/bin/jabba" ] || [ "$JABBA_VERSION" != $(${JABBA_HOME}/bin/jabba --version) ]; then 32 | get https://github.com/Jabba-Team/jabba/raw/main/install.sh | bash -s -- --skip-rc 33 | fi 34 | 35 | ${JABBA_HOME}/bin/jabba install 36 | export JAVA_HOME=$(${JABBA_HOME}/bin/jabba which --home) 37 | 38 | $* 39 | -------------------------------------------------------------------------------- /command/ls.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/Jabba-Team/jabba/cfg" 12 | "github.com/Jabba-Team/jabba/semver" 13 | ) 14 | 15 | var readDir = ioutil.ReadDir 16 | 17 | func Ls() ([]*semver.Version, error) { 18 | files, _ := readDir(filepath.Join(cfg.Dir(), "jdk")) 19 | var r []*semver.Version 20 | for _, f := range files { 21 | if f.IsDir() || (f.Mode()&os.ModeSymlink == os.ModeSymlink && strings.HasPrefix(f.Name(), "system@")) { 22 | v, err := semver.ParseVersion(f.Name()) 23 | if err != nil { 24 | return nil, err 25 | } 26 | r = append(r, v) 27 | } 28 | } 29 | sort.Sort(sort.Reverse(semver.VersionSlice(r))) 30 | return r, nil 31 | } 32 | 33 | func LsBestMatch(selector string) (ver string, err error) { 34 | vs, err := Ls() 35 | if err != nil { 36 | return 37 | } 38 | return LsBestMatchWithVersionSlice(vs, selector) 39 | } 40 | 41 | func LsBestMatchWithVersionSlice(vs []*semver.Version, selector string) (ver string, err error) { 42 | rng, err := semver.ParseRange(selector) 43 | if err != nil { 44 | return 45 | } 46 | for _, v := range vs { 47 | if rng.Contains(v) { 48 | ver = v.String() 49 | break 50 | } 51 | } 52 | if ver == "" { 53 | err = fmt.Errorf("%s isn't installed", rng) 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /command/use.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "runtime" 8 | 9 | "github.com/Jabba-Team/jabba/cfg" 10 | ) 11 | 12 | func Use(selector string) ([]string, error) { 13 | aliasValue := GetAlias(selector) 14 | if aliasValue != "" { 15 | selector = aliasValue 16 | } 17 | ver, err := LsBestMatch(selector) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return usePath(filepath.Join(cfg.Dir(), "jdk", ver)) 22 | } 23 | 24 | func usePath(path string) ([]string, error) { 25 | path, err := filepath.Abs(path) 26 | if err != nil { 27 | return nil, err 28 | } 29 | pth, _ := os.LookupEnv("PATH") 30 | rgxp := regexp.MustCompile(regexp.QuoteMeta(filepath.Join(cfg.Dir(), "jdk")) + "[^:]+[:]") 31 | // strip references to ~/.jabba/jdk/*, otherwise leave unchanged 32 | pth = rgxp.ReplaceAllString(pth, "") 33 | if runtime.GOOS == "darwin" { 34 | path = filepath.Join(path, "Contents", "Home") 35 | } 36 | systemJavaHome, overrideWasSet := os.LookupEnv("JAVA_HOME_BEFORE_JABBA") 37 | if !overrideWasSet { 38 | systemJavaHome, _ = os.LookupEnv("JAVA_HOME") 39 | } 40 | return []string{ 41 | "export PATH=\"" + filepath.Join(path, "bin") + string(os.PathListSeparator) + pth + "\"", 42 | "export JAVA_HOME=\"" + path + "\"", 43 | "export JAVA_HOME_BEFORE_JABBA=\"" + systemJavaHome + "\"", 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /jabbaw.md: -------------------------------------------------------------------------------- 1 | # jabba-wrapper 2 | 3 | Simple scripts to use jabba (https://github.com/Jabba-Team/jabba) without installation. 4 | 5 | Inspired by and tested with maven-wrapper (https://maven.apache.org/wrapper/). 6 | 7 | Assembled from jabba install scripts. 8 | 9 | ## Installation 10 | 11 | Simply copy the scripts in the root project folder, keeping the unix shell version executable. 12 | 13 | Setup .jabbarc file with the desired JDK version (https://github.com/Jabba-Team/jabba#usage) 14 | 15 | Enjoy the wrapper :) 16 | 17 | #### macOS / Linux 18 | 19 | https://raw.githubusercontent.com/nicerloop/jabba-wrapper/main/jabbaw 20 | 21 | ```sh 22 | curl -sLO https://raw.githubusercontent.com/Jabba-Team/jabba/main/jabbaw 23 | chmod +x jabbaw 24 | curl -sLO https://raw.githubusercontent.com/Jabba-Team/jabba/main/jabbaw.ps1 25 | echo "zulu@8" > .jabbarc 26 | ./jabbaw ./mvnw -v 27 | ``` 28 | 29 | #### Windows 10 30 | 31 | ```powershell 32 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 33 | Invoke-WebRequest -Uri https://raw.githubusercontent.com/Jabba-Team/jabba/main/jabbaw.ps1 -OutFile ./jabbaw.ps1 34 | Invoke-WebRequest -Uri https://raw.githubusercontent.com/Jabba-Team/jabba/main/jabbaw -OutFile ./jabbaw 35 | # don't forget to set executable bit for shell script in git 36 | # git update-index --chmod=+x jabbaw 37 | "zulu@8" | Out-File .jabbarc 38 | ./jabbaw ./mvnw -v 39 | ``` 40 | -------------------------------------------------------------------------------- /w32/w32.go: -------------------------------------------------------------------------------- 1 | package w32 2 | 3 | type ( 4 | DWORD uint32 5 | HANDLE uintptr 6 | HINSTANCE HANDLE 7 | HKEY HANDLE 8 | HWND HANDLE 9 | ULONG uint32 10 | LPCTSTR uintptr 11 | LPVOID uintptr 12 | ) 13 | 14 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx 15 | const ( 16 | ERROR_BAD_FORMAT = 11 17 | ) 18 | 19 | // https://msdn.microsoft.com/en-us/library/windows/desktop/bb759784(v=vs.85).aspx 20 | const ( 21 | SE_ERR_FNF = 2 22 | SE_ERR_PNF = 3 23 | SE_ERR_ACCESSDENIED = 5 24 | SE_ERR_OOM = 8 25 | SE_ERR_DLLNOTFOUND = 32 26 | SE_ERR_SHARE = 26 27 | SE_ERR_ASSOCINCOMPLETE = 27 28 | SE_ERR_DDETIMEOUT = 28 29 | SE_ERR_DDEFAIL = 29 30 | SE_ERR_DDEBUSY = 30 31 | SE_ERR_NOASSOC = 31 32 | ) 33 | 34 | // https://msdn.microsoft.com/en-us/library/windows/desktop/bb759784(v=vs.85).aspx 35 | const ( 36 | SEE_MASK_NOCLOSEPROCESS = 0x00000040 37 | ) 38 | 39 | type SHELLEXECUTEINFO struct { 40 | cbSize DWORD 41 | fMask ULONG 42 | hwnd HWND 43 | lpVerb LPCTSTR 44 | lpFile LPCTSTR 45 | lpParameters LPCTSTR 46 | lpDirectory LPCTSTR 47 | nShow int 48 | hInstApp HINSTANCE 49 | lpIDList LPVOID 50 | lpClass LPCTSTR 51 | hkeyClass HKEY 52 | dwHotKey DWORD 53 | hIconOrMonitor HANDLE 54 | hProcess HANDLE 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Jabba-Team/jabba 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Masterminds/semver v1.3.1 7 | github.com/Sirupsen/logrus v0.10.0 8 | github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 9 | github.com/mitchellh/go-homedir v0.0.0-20160301183130-981ab348d865 10 | github.com/mitchellh/ioprogress v0.0.0-20150521211556-816395526456 11 | github.com/spf13/cobra v0.0.0-20160322171042-c678ff029ee2 12 | github.com/spf13/pflag v0.0.0-20151218134703-7f60f83a2c81 13 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 14 | gopkg.in/yaml.v2 v2.0.0-20160928153709-a5b47d31c556 15 | ) 16 | 17 | require ( 18 | github.com/aktau/github-release v0.10.0 // indirect 19 | github.com/dustin/go-humanize v1.0.0 // indirect 20 | github.com/github-release/github-release v0.10.0 // indirect 21 | github.com/hashicorp/go-version v1.0.0 // indirect 22 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 23 | github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect 24 | github.com/mitchellh/gox v1.0.1 // indirect 25 | github.com/mitchellh/iochan v1.0.0 // indirect 26 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 // indirect 27 | github.com/stretchr/testify v1.7.0 // indirect 28 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect 29 | github.com/voxelbrain/goptions v0.0.0-20180630082107-58cddc247ea2 // indirect 30 | golang.org/x/sys v0.0.0-20160322232243-afce3de5756c // indirect 31 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /semver/range_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestContains(t *testing.T) { 8 | assertWithinRange(t, "1.8", "1.8.0", true) 9 | assertWithinRange(t, "1.7", "1.8.0", false) 10 | assertWithinRange(t, "1.8.0-0", "1.8.0-0", true) 11 | assertWithinRange(t, "1.8.0-0", "1.8.0-1", false) 12 | assertWithinRange(t, "~1.8", "1.8.99", true) 13 | assertWithinRange(t, "~1.8", "1.9.0", false) 14 | assertWithinRange(t, "a@1.8", "a@1.8.72", true) 15 | assertWithinRange(t, "1.8", "a@1.8.72", false) 16 | assertWithinRange(t, "a@1.8", "b@1.8.72", false) 17 | assertWithinRange(t, "a@1.8", "1.8.72", false) 18 | assertWithinRange(t, "a@1.7.x", "a@1.7.72", true) 19 | assertWithinRange(t, "a@1.7.x", "a@1.8.72", false) 20 | assertWithinRange(t, "a@>=1.7 <=1.8.75", "a@1.8.72", true) 21 | assertWithinRange(t, "a@>=1.7 <=1.8.75", "a@1.8.80", false) 22 | } 23 | 24 | func TestPre070Compat(t *testing.T) { 25 | actual := pre070Compat("1.1, >= 1.2 <= 1.3.0, 4, 1.5,1.6.0, ~1.2") 26 | expected := "1.1.x, >= 1.2, <= 1.3.0, 4.x, 1.5.x,1.6.0, ~1.2" 27 | if actual != expected { 28 | t.Fatalf("actual: %#v != expected: %#v", actual, expected) 29 | } 30 | } 31 | 32 | func assertWithinRange(t *testing.T, rng string, ver string, value bool) { 33 | r, err := ParseRange(rng) 34 | if err != nil { 35 | t.Fatalf("err: %v", err) 36 | } 37 | v, err := ParseVersion(ver) 38 | if err != nil { 39 | t.Fatalf("err: %v", err) 40 | } 41 | if r.Contains(v) != value { 42 | t.Fatalf("expected range %v to contain %v (%v)", rng, ver, value) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /command/deactivate_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/Jabba-Team/jabba/cfg" 9 | ) 10 | 11 | func TestDeactivate(t *testing.T) { 12 | prevPath := os.Getenv("PATH") 13 | defer func() { os.Setenv("PATH", prevPath) }() 14 | os.Setenv("PATH", "/usr/local/bin:"+cfg.Dir()+"/jdk/zulu@1.8.72/bin:/system-jdk/bin:/usr/bin") 15 | os.Setenv("JAVA_HOME", cfg.Dir()+"/jdk/zulu@1.8.72") 16 | os.Setenv("JAVA_HOME_BEFORE_JABBA", "/system-jdk") 17 | actual, err := Deactivate() 18 | if err != nil { 19 | t.Fatalf("err: %v", err) 20 | } 21 | expected := []string{ 22 | "export PATH=\"/usr/local/bin:/system-jdk/bin:/usr/bin\"", 23 | "export JAVA_HOME=\"/system-jdk\"", 24 | "unset JAVA_HOME_BEFORE_JABBA", 25 | } 26 | if !reflect.DeepEqual(actual, expected) { 27 | t.Fatalf("actual: %v != expected: %v", actual, expected) 28 | } 29 | } 30 | 31 | func TestDeactivateInUnusedEnv(t *testing.T) { 32 | prevPath := os.Getenv("PATH") 33 | defer func() { os.Setenv("PATH", prevPath) }() 34 | os.Setenv("PATH", "/usr/local/bin:/system-jdk/bin:/usr/bin") 35 | os.Setenv("JAVA_HOME", "/system-jdk") 36 | os.Unsetenv("JAVA_HOME_BEFORE_JABBA") 37 | actual, err := Deactivate() 38 | if err != nil { 39 | t.Fatalf("err: %v", err) 40 | } 41 | expected := []string{ 42 | "export PATH=\"/usr/local/bin:/system-jdk/bin:/usr/bin\"", 43 | "export JAVA_HOME=\"/system-jdk\"", 44 | "unset JAVA_HOME_BEFORE_JABBA", 45 | } 46 | if !reflect.DeepEqual(actual, expected) { 47 | t.Fatalf("actual: %v != expected: %v", actual, expected) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /command/ls-remote.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/Jabba-Team/jabba/cfg" 12 | "github.com/Jabba-Team/jabba/semver" 13 | ) 14 | 15 | type byOS map[string]byArch 16 | type byArch map[string]byDistribution 17 | type byDistribution map[string]map[string]string 18 | 19 | func LsRemote(os, arch string) (map[*semver.Version]string, error) { 20 | cnt, err := fetch(cfg.Index()) 21 | if err != nil { 22 | return nil, err 23 | } 24 | var index byOS 25 | err = json.Unmarshal(cnt, &index) 26 | if err != nil { 27 | return nil, err 28 | } 29 | releaseMap := make(map[*semver.Version]string) 30 | for key, value := range index[os][arch] { 31 | var prefix string 32 | if key != "jdk" { 33 | if !strings.Contains(key, "@") { 34 | continue 35 | } 36 | prefix = key[strings.Index(key, "@")+1:] + "@" 37 | } 38 | for ver, url := range value { 39 | v, err := semver.ParseVersion(prefix + ver) 40 | if err != nil { 41 | return nil, err 42 | } 43 | releaseMap[v] = url 44 | } 45 | } 46 | return releaseMap, nil 47 | } 48 | 49 | func fetch(url string) (content []byte, err error) { 50 | client := http.Client{Transport: RedirectTracer{}} 51 | res, err := client.Get(url) 52 | if err != nil { 53 | return 54 | } 55 | defer res.Body.Close() 56 | if res.StatusCode >= 400 { 57 | return nil, errors.New("GET " + url + " returned " + strconv.Itoa(res.StatusCode)) 58 | } 59 | content, err = ioutil.ReadAll(res.Body) 60 | if err != nil { 61 | return 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /command/use_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "runtime" 7 | "testing" 8 | "time" 9 | 10 | "github.com/Jabba-Team/jabba/cfg" 11 | ) 12 | 13 | type FileInfoMock string 14 | 15 | func (f FileInfoMock) Name() string { return string(f) } 16 | func (f FileInfoMock) Size() int64 { return 0 } 17 | func (f FileInfoMock) Mode() os.FileMode { return os.FileMode(0) } 18 | func (f FileInfoMock) ModTime() time.Time { return time.Time{} } 19 | func (f FileInfoMock) IsDir() bool { return true } 20 | func (f FileInfoMock) Sys() interface{} { return nil } 21 | 22 | func TestUse(t *testing.T) { 23 | prevPath := os.Getenv("PATH") 24 | defer func() { os.Setenv("PATH", prevPath) }() 25 | var prevReadDir = readDir 26 | defer func() { readDir = prevReadDir }() 27 | readDir = func(dirname string) ([]os.FileInfo, error) { 28 | return []os.FileInfo{ 29 | FileInfoMock("1.6.0"), FileInfoMock("1.7.0"), FileInfoMock("1.7.2"), FileInfoMock("1.8.0"), 30 | }, nil 31 | } 32 | os.Setenv("PATH", "/usr/local/bin:"+cfg.Dir()+"/jdk/1.6.0/bin:/usr/bin") 33 | os.Setenv("JAVA_HOME", "/system-jdk") 34 | actual, err := Use("1.7") 35 | if err != nil { 36 | t.Fatalf("err: %v", err) 37 | } 38 | var suffix string 39 | if runtime.GOOS == "darwin" { 40 | suffix = "/Contents/Home" 41 | } 42 | expected := []string{ 43 | "export PATH=\"" + cfg.Dir() + "/jdk/1.7.2" + suffix + "/bin:/usr/local/bin:/usr/bin\"", 44 | "export JAVA_HOME=\"" + cfg.Dir() + "/jdk/1.7.2" + suffix + "\"", 45 | "export JAVA_HOME_BEFORE_JABBA=\"/system-jdk\"", 46 | } 47 | if !reflect.DeepEqual(actual, expected) { 48 | t.Fatalf("actual: %v != expected: %v", actual, expected) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /semver/range.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Masterminds/semver" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var pre070CompatRegexp = regexp.MustCompile("(^|,\\s*)\\d+([.]\\d+)?[.]?") 11 | var pre070CompatRepl = func(input string) string { 12 | if strings.HasSuffix(input, ".") { 13 | return input 14 | } 15 | return input + ".x" 16 | } 17 | 18 | func pre070Compat(version string) string { 19 | // 1.2 -> 1.2.x 20 | s := pre070CompatRegexp.ReplaceAllStringFunc(version, pre070CompatRepl) 21 | // >= 1.2 <= 2.4 -> >= 1.2, <= 2.4 22 | split := regexp.MustCompile("\\s+").Split(s, -1) 23 | for i, v := range split { 24 | if i > 0 { 25 | switch v[0] { 26 | case '!', '=', '<', '>', '~', '^': 27 | pv := split[i-1] 28 | pvc := pv[len(pv)-1] 29 | if pvc != '|' && pvc != ',' { 30 | split[i-1] = pv + "," 31 | } 32 | } 33 | } 34 | } 35 | return strings.Join(split, " ") 36 | } 37 | 38 | type Range struct { 39 | qualifier string 40 | raw string 41 | rng *semver.Constraints 42 | } 43 | 44 | func (l *Range) Contains(r *Version) bool { 45 | return (l.qualifier == r.qualifier || l.qualifier == "*") && l.rng.Check(r.ver) 46 | } 47 | 48 | func (t *Range) String() string { 49 | return t.raw 50 | } 51 | 52 | func ParseRange(raw string) (*Range, error) { 53 | p := new(Range) 54 | p.raw = raw 55 | // selector can be either or @ 56 | if strings.Contains(raw, "@") { 57 | p.qualifier = raw[0:strings.Index(raw, "@")] 58 | raw = raw[strings.Index(raw, "@")+1:] 59 | if raw == "" { 60 | // `jabba ls-remote zulu@` convenience 61 | raw = ">=0.0.0-0" 62 | } 63 | } 64 | constraint := pre070Compat(raw) 65 | parsed, err := semver.NewConstraint(constraint) 66 | if err != nil { 67 | return nil, fmt.Errorf("%s is not a valid version", p.raw) 68 | } 69 | p.rng = parsed 70 | return p, nil 71 | } 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash -o pipefail 2 | VERSION := $(shell git describe --tags --abbrev=0) 3 | 4 | fetch: 5 | go get \ 6 | github.com/mitchellh/gox \ 7 | github.com/modocache/gover \ 8 | github.com/aktau/github-release 9 | 10 | clean: 11 | rm -f ./jabba 12 | rm -rf ./build 13 | 14 | fmt: 15 | gofmt -l -s -w `find . -type f -name '*.go' -not -path "./vendor/*"` 16 | 17 | test: 18 | go vet `go list ./... | grep -v /vendor/` 19 | SRC=`find . -type f -name '*.go' -not -path "./vendor/*"` && gofmt -l -s $$SRC | read && gofmt -l -s -d $$SRC && exit 1 || true 20 | go test `go list ./... | grep -v /vendor/` 21 | 22 | test-coverage: 23 | go list ./... | grep -v /vendor/ | xargs -L1 -I{} sh -c 'go test -coverprofile `basename {}`.coverprofile {}' && \ 24 | gover && \ 25 | go tool cover -html=gover.coverprofile -o coverage.html && \ 26 | rm *.coverprofile 27 | 28 | build: 29 | CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION}" 30 | 31 | build-release: 32 | CGO_ENABLED=0 gox -verbose \ 33 | -ldflags "-X main.version=${VERSION}" \ 34 | -osarch="windows/386 windows/amd64 linux/386 linux/amd64 darwin/amd64 darwin/arm64 linux/arm linux/arm64" \ 35 | -output="release/{{.Dir}}-${VERSION}-{{.OS}}-{{.Arch}}" . 36 | 37 | install: build 38 | JABBA_MAKE_INSTALL=true JABBA_VERSION=${VERSION} bash install.sh 39 | 40 | publish: clean build-release 41 | test -n "$(GITHUB_TOKEN)" # $$GITHUB_TOKEN must be set 42 | github-release release --user patrick-mccourt --repo Jabba-Team/jabba --tag ${VERSION} \ 43 | --name "${VERSION}" --description "${VERSION}" && \ 44 | github-release upload --user patrick-mccourt --repo Jabba-Team/jabba --tag ${VERSION} \ 45 | --name "jabba-${VERSION}-windows-amd64.exe" --file release/jabba-${VERSION}-windows-amd64.exe; \ 46 | for qualifier in darwin-amd64 linux-386 linux-amd64 linux-arm linux-arm64; do \ 47 | github-release upload --user patrick-mccourt --repo Jabba-Team/jabba --tag ${VERSION} \ 48 | --name "jabba-${VERSION}-$$qualifier" --file release/jabba-${VERSION}-$$qualifier; \ 49 | done 50 | -------------------------------------------------------------------------------- /command/fileiter/iterator.go: -------------------------------------------------------------------------------- 1 | package fileiter 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Iterator struct { 10 | head *node 11 | tail *node 12 | breadthFirst bool 13 | } 14 | 15 | type node struct { 16 | dir string 17 | stat os.FileInfo 18 | err error 19 | skip bool 20 | next *node 21 | } 22 | 23 | type IterationOption = func(*Iterator) 24 | 25 | func BreadthFirst() IterationOption { 26 | return func(w *Iterator) { 27 | w.breadthFirst = true 28 | } 29 | } 30 | 31 | func New(dir string, opts ...IterationOption) *Iterator { 32 | stat, err := os.Lstat(dir) 33 | n := &node{dir: filepath.Dir(dir), stat: stat, err: err} 34 | w := &Iterator{head: n, tail: n} 35 | for _, opt := range opts { 36 | opt(w) 37 | } 38 | return w 39 | } 40 | 41 | func (w *Iterator) Next() bool { 42 | if w.head == nil { 43 | return false 44 | } 45 | if !w.head.skip && w.head.err == nil && w.head.stat.IsDir() { 46 | path := filepath.Join(w.head.dir, w.head.stat.Name()) 47 | if ff, err := ioutil.ReadDir(path); len(ff) != 0 { // sorts ASC 48 | if err == nil { 49 | if w.breadthFirst { 50 | // add one-by-one to the tail 51 | for _, f := range ff { 52 | w.tail.next = &node{path, f, nil, false, nil} 53 | w.tail = w.tail.next 54 | } 55 | return w.next(w.head.next) 56 | } else { 57 | w.head = w.head.next // drop current 58 | // add one-by-one to the head (in reverse order) 59 | for i := len(ff) - 1; i > -1; i-- { 60 | w.head = &node{path, ff[i], nil, false, w.head} 61 | } 62 | return w.next(w.head) 63 | } 64 | } else { 65 | if w.breadthFirst { 66 | w.tail.next = &node{w.head.dir, w.head.stat, err, false, nil} 67 | w.tail = w.tail.next 68 | w.head = w.head.next // drop current 69 | return true 70 | } else { 71 | w.head.err = err 72 | return true 73 | } 74 | } 75 | } 76 | } 77 | return w.next(w.head.next) 78 | } 79 | 80 | func (w *Iterator) next(v *node) bool { 81 | w.head = v 82 | if v == nil { 83 | w.tail = nil 84 | return false 85 | } 86 | return true 87 | } 88 | 89 | func (w *Iterator) Dir() string { 90 | return w.head.dir 91 | } 92 | 93 | func (w *Iterator) Name() string { 94 | return w.head.stat.Name() 95 | } 96 | 97 | func (w *Iterator) IsDir() bool { 98 | return w.head.stat.IsDir() 99 | } 100 | 101 | func (w *Iterator) Err() error { 102 | return w.head.err 103 | } 104 | 105 | func (w *Iterator) SkipDir() { 106 | w.head.skip = true 107 | } 108 | -------------------------------------------------------------------------------- /command/install_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestBinJavaRelocation(t *testing.T) { 11 | ok := func(err error) { 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | } 16 | nok := func(err error) { 17 | if err == nil { 18 | t.Fatal(err) 19 | } 20 | } 21 | dir, err := ioutil.TempDir("", "install_test") 22 | ok(err) 23 | for _, scenario := range []struct { 24 | os string 25 | bin string 26 | prefix string 27 | paths []string 28 | }{ 29 | { 30 | os: "linux", 31 | bin: "java", 32 | prefix: "", 33 | paths: []string{""}, 34 | }, 35 | { 36 | os: "darwin", 37 | bin: "java", 38 | prefix: filepath.Join("Contents", "Home"), 39 | paths: []string{ 40 | "", 41 | filepath.Join("Home"), 42 | filepath.Join("Contents", "Home"), 43 | }, 44 | }, 45 | { 46 | os: "windows", 47 | bin: "java.exe", 48 | prefix: "", 49 | paths: []string{""}, 50 | }, 51 | } { 52 | for _, p := range scenario.paths { 53 | test1 := filepath.Join(dir, "test1") 54 | ok(touch(test1, p, "bin", scenario.bin)) 55 | ok(normalizePathToBinJava(test1, scenario.os)) 56 | ok(file(test1, scenario.prefix, "bin", scenario.bin)) 57 | 58 | test2 := filepath.Join(dir, "test2") 59 | ok(touch(test2, "subdir", p, "bin", scenario.bin)) 60 | ok(normalizePathToBinJava(test2, scenario.os)) 61 | ok(file(test2, scenario.prefix, "bin", scenario.bin)) 62 | 63 | test3 := filepath.Join(dir, "test3") 64 | ok(touch(test3, "subdir", "subdir", p, "bin", scenario.bin)) 65 | ok(normalizePathToBinJava(test3, scenario.os)) 66 | ok(file(test3, scenario.prefix, "bin", scenario.bin)) 67 | 68 | test4 := filepath.Join(dir, "test4") 69 | ok(touch(test4, "file")) 70 | ok(touch(test4, "subdir", "subdir", p, "bin", scenario.bin)) 71 | ok(normalizePathToBinJava(test4, scenario.os)) 72 | ok(file(test4, scenario.prefix, "bin", scenario.bin)) 73 | 74 | test5 := filepath.Join(dir, "test5") 75 | ok(touch(test5, "bin", "file")) 76 | nok(normalizePathToBinJava(test5, scenario.os)) 77 | ok(file(test5, "bin", "file")) 78 | } 79 | } 80 | } 81 | 82 | func touch(path ...string) error { 83 | filename := filepath.Join(path...) 84 | if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { 85 | return err 86 | } 87 | if err := ioutil.WriteFile(filename, nil, 0755); err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | func file(path ...string) error { 94 | if _, err := os.Stat(filepath.Join(path...)); os.IsNotExist(err) { 95 | return err 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /command/fileiter/iterator_test.go: -------------------------------------------------------------------------------- 1 | package fileiter 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestDFS(t *testing.T) { 11 | ok := func(err error) { 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | } 16 | dir, err := ioutil.TempDir("", "fileiter_test") 17 | ok(err) 18 | ok(touch(dir, "a", "b", "c")) 19 | ok(touch(dir, "b", "c")) 20 | ok(touch(dir, "c")) 21 | ok(mkdir(dir, "d")) 22 | expectedSeq := []string{ 23 | "a", 24 | filepath.Join("a", "b"), 25 | filepath.Join("a", "b", "c"), 26 | "b", 27 | filepath.Join("b", "c"), 28 | "c", 29 | "d", 30 | } 31 | test(t, dir, expectedSeq) 32 | test(t, filepath.Join(dir, "d"), nil) 33 | test(t, filepath.Join(dir, "non-existent"), nil) 34 | } 35 | 36 | func TestBFS(t *testing.T) { 37 | ok := func(err error) { 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | dir, err := ioutil.TempDir("", "fileiter_test") 43 | ok(err) 44 | ok(touch(dir, "a", "b", "c")) 45 | ok(touch(dir, "b", "c")) 46 | ok(touch(dir, "c")) 47 | ok(mkdir(dir, "d")) 48 | expectedSeq := []string{ 49 | "a", 50 | "b", 51 | "c", 52 | "d", 53 | filepath.Join("a", "b"), 54 | filepath.Join("b", "c"), 55 | filepath.Join("a", "b", "c"), 56 | } 57 | test(t, dir, expectedSeq, BreadthFirst()) 58 | test(t, filepath.Join(dir, "d"), nil, BreadthFirst()) 59 | test(t, filepath.Join(dir, "non-existent"), nil, BreadthFirst()) 60 | } 61 | 62 | func TestNew(t *testing.T) { 63 | ok := func(err error) { 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | } 68 | dir, err := ioutil.TempDir("", "fileiter_test") 69 | ok(err) 70 | ok(touch(dir, "a", "b")) 71 | if it := New(filepath.Join(dir, "a", "b")); it.Err() != nil || it.IsDir() { 72 | t.Fatal(it.Err()) 73 | } 74 | if it := New(filepath.Join(dir, "a")); it.Err() != nil || !it.IsDir() { 75 | t.Fatal(it.Err()) 76 | } 77 | if it := New(filepath.Join(dir, "b")); it.Err() == nil { 78 | t.Fatal() 79 | } 80 | } 81 | 82 | func test(t *testing.T, dir string, expectedSeq []string, opts ...IterationOption) { 83 | i := 0 84 | for w := New(dir, opts...); w.Next(); { 85 | if w.Err() != nil { 86 | t.Fatal(w.Err()) 87 | } 88 | expected := filepath.Join(dir, expectedSeq[i]) 89 | actual := filepath.Join(w.Dir(), w.Name()) 90 | if actual != expected { 91 | t.Fatalf("actual: %v != expected: %v", actual, expected) 92 | } 93 | i++ 94 | } 95 | } 96 | 97 | func touch(path ...string) error { 98 | filename := filepath.Join(path...) 99 | if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { 100 | return err 101 | } 102 | if err := ioutil.WriteFile(filename, nil, 0755); err != nil { 103 | return err 104 | } 105 | return nil 106 | } 107 | 108 | func mkdir(path ...string) error { 109 | if err := os.MkdirAll(filepath.Join(path...), 0755); err != nil { 110 | return err 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /semver/version.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Masterminds/semver" 6 | "strings" 7 | ) 8 | 9 | type Version struct { 10 | qualifier string 11 | raw string 12 | ver *semver.Version 13 | } 14 | 15 | func (l *Version) LessThan(r *Version) bool { 16 | if l.qualifier == r.qualifier { 17 | return l.ver.LessThan(r.ver) 18 | } 19 | return l.qualifier > r.qualifier 20 | } 21 | 22 | func (l *Version) Equals(r *Version) bool { 23 | return l.raw == r.raw 24 | } 25 | 26 | func (t *Version) String() string { 27 | return t.raw 28 | } 29 | 30 | func (t *Version) TrimTo(part VersionPart) string { 31 | prefix := t.qualifier 32 | if prefix != "" { 33 | prefix += "@" 34 | } 35 | switch part { 36 | case VPMajor: 37 | return fmt.Sprintf("%v%v", prefix, t.ver.Major()) 38 | case VPMinor: 39 | return fmt.Sprintf("%v%v.%v", prefix, t.ver.Major(), t.ver.Minor()) 40 | case VPPatch: 41 | return fmt.Sprintf("%v%v.%v.%v", prefix, t.ver.Major(), t.ver.Minor(), t.ver.Patch()) 42 | } 43 | return t.raw 44 | } 45 | 46 | func (t *Version) Major() int64 { 47 | return t.ver.Major() 48 | } 49 | 50 | func (t *Version) Minor() int64 { 51 | return t.ver.Minor() 52 | } 53 | 54 | func (t *Version) Patch() int64 { 55 | return t.ver.Patch() 56 | } 57 | 58 | func (t *Version) Prerelease() string { 59 | return t.ver.Prerelease() 60 | } 61 | 62 | func ParseVersion(raw string) (*Version, error) { 63 | p := new(Version) 64 | p.raw = raw 65 | // selector can be either or @ 66 | if strings.Contains(raw, "@") { 67 | p.qualifier = raw[0:strings.Index(raw, "@")] 68 | raw = raw[strings.Index(raw, "@")+1:] 69 | } 70 | parsed, err := semver.NewVersion(raw) 71 | if err != nil { 72 | return nil, fmt.Errorf("%s is not a valid version", raw) 73 | } 74 | p.ver = parsed 75 | return p, nil 76 | } 77 | 78 | type VersionSlice []*Version 79 | 80 | // impl sort.Interface: 81 | 82 | func (c VersionSlice) Len() int { 83 | return len(c) 84 | } 85 | func (c VersionSlice) Swap(i, j int) { 86 | c[i], c[j] = c[j], c[i] 87 | } 88 | func (c VersionSlice) Less(i, j int) bool { 89 | return c[i].LessThan(c[j]) 90 | } 91 | 92 | type VersionPart int 93 | 94 | const ( 95 | VPMajor VersionPart = iota 96 | VPMinor 97 | VPPatch 98 | ) 99 | 100 | func (c VersionSlice) TrimTo(part VersionPart) VersionSlice { 101 | var r []*Version 102 | var pQualifier string 103 | var pMajor, pMinor, pPatch int64 104 | for _, v := range c { 105 | switch part { 106 | case VPMajor: 107 | if pQualifier == v.qualifier && pMajor == v.Major() { 108 | continue 109 | } 110 | case VPMinor: 111 | if pQualifier == v.qualifier && pMajor == v.Major() && pMinor == v.Minor() { 112 | continue 113 | } 114 | case VPPatch: 115 | if pQualifier == v.qualifier && pMajor == v.Major() && pMinor == v.Minor() && pPatch == v.Patch() { 116 | continue 117 | } 118 | } 119 | pQualifier = v.qualifier 120 | pMajor = v.Major() 121 | pMinor = v.Minor() 122 | pPatch = v.Patch() 123 | r = append(r, v) 124 | } 125 | return r 126 | } 127 | -------------------------------------------------------------------------------- /w32/w32_windows.go: -------------------------------------------------------------------------------- 1 | package w32 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | modshell32 = syscall.NewLazyDLL("shell32.dll") 13 | // https://msdn.microsoft.com/en-us/library/windows/desktop/bb762154(v=vs.85).aspx 14 | procShellExecuteEx = modshell32.NewProc("ShellExecuteExW") 15 | ) 16 | 17 | // some of the code below was borrowed from 18 | // https://github.com/AllenDang/w32/blob/65507298e138d537445133ed145a1f2685782b34/shell32.go 19 | 20 | func ShellExecuteAndWait(hwnd HWND, lpOperation, lpFile, lpParameters, lpDirectory string, nShowCmd int) error { 21 | var lpctstrVerb, lpctstrParameters, lpctstrDirectory LPCTSTR 22 | if len(lpOperation) != 0 { 23 | lpctstrVerb = LPCTSTR(unsafe.Pointer(syscall.StringToUTF16Ptr(lpOperation))) 24 | } 25 | if len(lpParameters) != 0 { 26 | lpctstrParameters = LPCTSTR(unsafe.Pointer(syscall.StringToUTF16Ptr(lpParameters))) 27 | } 28 | if len(lpDirectory) != 0 { 29 | lpctstrDirectory = LPCTSTR(unsafe.Pointer(syscall.StringToUTF16Ptr(lpDirectory))) 30 | } 31 | i := &SHELLEXECUTEINFO{ 32 | fMask: SEE_MASK_NOCLOSEPROCESS, 33 | hwnd: hwnd, 34 | lpVerb: lpctstrVerb, 35 | lpFile: LPCTSTR(unsafe.Pointer(syscall.StringToUTF16Ptr(lpFile))), 36 | lpParameters: lpctstrParameters, 37 | lpDirectory: lpctstrDirectory, 38 | nShow: nShowCmd, 39 | } 40 | i.cbSize = DWORD(unsafe.Sizeof(*i)) 41 | return ShellExecuteEx(i) 42 | } 43 | 44 | func ShellExecuteEx(pExecInfo *SHELLEXECUTEINFO) error { 45 | ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(pExecInfo))) 46 | if ret == 1 && pExecInfo.fMask&SEE_MASK_NOCLOSEPROCESS != 0 { 47 | s, e := syscall.WaitForSingleObject(syscall.Handle(pExecInfo.hProcess), syscall.INFINITE) 48 | switch s { 49 | case syscall.WAIT_OBJECT_0: 50 | break 51 | case syscall.WAIT_FAILED: 52 | return os.NewSyscallError("WaitForSingleObject", e) 53 | default: 54 | return errors.New("Unexpected result from WaitForSingleObject") 55 | } 56 | } 57 | errorMsg := "" 58 | if pExecInfo.hInstApp != 0 && pExecInfo.hInstApp <= 32 { 59 | switch int(pExecInfo.hInstApp) { 60 | case SE_ERR_FNF: 61 | errorMsg = "The specified file was not found" 62 | case SE_ERR_PNF: 63 | errorMsg = "The specified path was not found" 64 | case ERROR_BAD_FORMAT: 65 | errorMsg = "The .exe file is invalid (non-Win32 .exe or error in .exe image)" 66 | case SE_ERR_ACCESSDENIED: 67 | errorMsg = "The operating system denied access to the specified file" 68 | case SE_ERR_ASSOCINCOMPLETE: 69 | errorMsg = "The file name association is incomplete or invalid" 70 | case SE_ERR_DDEBUSY: 71 | errorMsg = "The DDE transaction could not be completed because other DDE transactions were being processed" 72 | case SE_ERR_DDEFAIL: 73 | errorMsg = "The DDE transaction failed" 74 | case SE_ERR_DDETIMEOUT: 75 | errorMsg = "The DDE transaction could not be completed because the request timed out" 76 | case SE_ERR_DLLNOTFOUND: 77 | errorMsg = "The specified DLL was not found" 78 | case SE_ERR_NOASSOC: 79 | errorMsg = "There is no application associated with the given file name extension" 80 | case SE_ERR_OOM: 81 | errorMsg = "There was not enough memory to complete the operation" 82 | case SE_ERR_SHARE: 83 | errorMsg = "A sharing violation occurred" 84 | default: 85 | errorMsg = fmt.Sprintf("Unknown error occurred with error code %v", pExecInfo.hInstApp) 86 | } 87 | } else { 88 | return nil 89 | } 90 | return errors.New(errorMsg) 91 | } 92 | -------------------------------------------------------------------------------- /command/link.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/Jabba-Team/jabba/cfg" 11 | "github.com/Jabba-Team/jabba/semver" 12 | log "github.com/Sirupsen/logrus" 13 | ) 14 | 15 | func Link(selector string, dir string) error { 16 | if !strings.HasPrefix(selector, "system@") { 17 | return errors.New("Name must begin with 'system@' (e.g. 'system@1.8.73')") 18 | } 19 | // has to be valid per semver 20 | if _, err := semver.ParseVersion(selector); err != nil { 21 | return err 22 | } 23 | if dir == "" { 24 | ver, err := LsBestMatch(selector) 25 | if err != nil { 26 | return err 27 | } 28 | return os.Remove(filepath.Join(cfg.Dir(), "jdk", ver)) 29 | } else { 30 | if err := assertJavaDistribution(dir, runtime.GOOS); err != nil { 31 | return err 32 | } 33 | if err := os.MkdirAll(filepath.Join(cfg.Dir(), "jdk"), 0755); err != nil { 34 | return err 35 | } 36 | return os.Symlink(dir, filepath.Join(cfg.Dir(), "jdk", selector)) 37 | } 38 | } 39 | 40 | func LinkLatest() error { 41 | files, _ := readDir(filepath.Join(cfg.Dir(), "jdk")) 42 | var vs, err = Ls() 43 | if err != nil { 44 | return err 45 | } 46 | cache := make(map[string]string) 47 | for _, f := range files { 48 | if f.IsDir() || f.Mode()&os.ModeSymlink == os.ModeSymlink { 49 | sourceVersion := f.Name() 50 | if strings.Count(sourceVersion, ".") == 1 && !strings.HasPrefix(sourceVersion, "system@") { 51 | target := GetLink(sourceVersion) 52 | _, err := LsBestMatchWithVersionSlice(vs, sourceVersion) 53 | if err != nil { 54 | err := os.Remove(filepath.Join(cfg.Dir(), "jdk", sourceVersion)) 55 | if err == nil { 56 | log.Info(sourceVersion + " -/> " + target) 57 | } 58 | if !os.IsNotExist(err) { 59 | return err 60 | } 61 | } else { 62 | cache[sourceVersion] = target 63 | } 64 | } 65 | } 66 | } 67 | for _, v := range semver.VersionSlice(vs).TrimTo(semver.VPMinor) { 68 | sourceVersion := v.TrimTo(semver.VPMinor) 69 | target := filepath.Join(cfg.Dir(), "jdk", v.String()) 70 | if v.Prerelease() == "" && cache[sourceVersion] != target && !strings.HasPrefix(sourceVersion, "system@") { 71 | source := filepath.Join(cfg.Dir(), "jdk", sourceVersion) 72 | log.Info(sourceVersion + " -> " + target) 73 | os.Remove(source) 74 | if err := os.Symlink(target, source); err != nil { 75 | return err 76 | } 77 | } 78 | } 79 | return linkAlias("default", vs) 80 | } 81 | 82 | func LinkAlias(name string) error { 83 | var vs, err = Ls() 84 | if err != nil { 85 | return err 86 | } 87 | return linkAlias(name, vs) 88 | } 89 | 90 | func linkAlias(name string, vs []*semver.Version) error { 91 | defaultAlias := GetAlias(name) 92 | if defaultAlias != "" { 93 | defaultAlias, _ = LsBestMatchWithVersionSlice(vs, defaultAlias) 94 | } 95 | sourceRef := /*"alias@" + */ name 96 | source := filepath.Join(cfg.Dir(), "jdk", sourceRef) 97 | sourceTarget := GetLink(sourceRef) 98 | if defaultAlias != "" { 99 | target := filepath.Join(cfg.Dir(), "jdk", defaultAlias) 100 | if sourceTarget != target { 101 | log.Info(sourceRef + " -> " + target) 102 | os.Remove(source) 103 | if err := os.Symlink(target, source); err != nil { 104 | return err 105 | } 106 | } 107 | } else { 108 | err := os.Remove(source) 109 | if err == nil { 110 | log.Info(sourceRef + " -/> " + sourceTarget) 111 | } 112 | if !os.IsNotExist(err) { 113 | return err 114 | } 115 | } 116 | return nil 117 | } 118 | 119 | func GetLink(name string) string { 120 | res, err := filepath.EvalSymlinks(filepath.Join(cfg.Dir(), "jdk", name)) 121 | if err != nil { 122 | return "" 123 | } 124 | return res 125 | } 126 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $jabbaHome = if ($env:JABBA_HOME) { $env:JABBA_HOME } else { if ($env:JABBA_DIR) { $env:JABBA_DIR } else { "$env:USERPROFILE\.jabba" } } 4 | $jabbaVersion = if ($env:JABBA_VERSION) { $env:JABBA_VERSION } else { "latest" } 5 | $jabbaArch = if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { "amd64" } else { "386" } 6 | 7 | if ($jabbaVersion -eq "latest") 8 | { 9 | # resolving "latest" to an actual tag 10 | $jabbaVersion = (Invoke-RestMethod https://api.github.com/repos/Jabba-Team/jabba/releases/latest).body 11 | } 12 | 13 | if ($jabbaVersion -notmatch '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.+-]+)?$') 14 | { 15 | Write-Host "'$jabbaVersion' is not a valid version." 16 | exit 1 17 | } 18 | 19 | Write-Host "Installing v$jabbaVersion...`n" 20 | 21 | New-Item -Type Directory -Force $jabbaHome/bin | Out-Null 22 | 23 | if ($env:JABBA_MAKE_INSTALL -eq "true") 24 | { 25 | Copy-Item jabba.exe $jabbaHome/bin 26 | } 27 | else 28 | { 29 | Invoke-WebRequest https://github.com/Jabba-Team/jabba/releases/download/$jabbaVersion/jabba-$jabbaVersion-windows-$jabbaArch.exe -UseBasicParsing -OutFile $jabbaHome/bin/jabba.exe 30 | } 31 | 32 | $ErrorActionPreference="SilentlyContinue" 33 | & $jabbaHome\bin\jabba.exe --version | Out-Null 34 | $binaryValid = $? 35 | $ErrorActionPreference="Continue" 36 | if (-not $binaryValid) 37 | { 38 | Write-Host @" 39 | $jabbaHome\bin\jabba does not appear to be a valid binary. 40 | 41 | Check your Internet connection / proxy settings and try again. 42 | if the problem persists - please create a ticket at https://github.com/Jabba-Team/jabba/issues. 43 | "@ 44 | exit 1 45 | } 46 | 47 | @" 48 | `$env:JABBA_HOME="$jabbaHome" 49 | 50 | function jabba 51 | { 52 | `$fd3=`$([System.IO.Path]::GetTempFileName()) 53 | `$command="& '$jabbaHome\bin\jabba.exe' `$args --fd3 ```"`$fd3```"" 54 | & { `$env:JABBA_SHELL_INTEGRATION="ON"; Invoke-Expression `$command } 55 | `$fd3content=`$(Get-Content `$fd3) 56 | if (`$fd3content) { 57 | `$expression=`$fd3content.replace("export ","```$env:").replace("unset ","Remove-Item env:") -join "``n" 58 | if (-not `$expression -eq "") { Invoke-Expression `$expression } 59 | } 60 | Remove-Item -Force `$fd3 61 | } 62 | "@ | Out-File $jabbaHome/jabba.ps1 63 | 64 | $sourceJabba="if (Test-Path `"$jabbaHome\jabba.ps1`") { . `"$jabbaHome\jabba.ps1`" }" 65 | 66 | if (-not $(Test-Path $profile)) 67 | { 68 | New-Item -Path $profile -Type File -Force | Out-Null 69 | } 70 | 71 | if ("$(Get-Content $profile | Select-String "\\jabba.ps1")" -eq "") 72 | { 73 | Write-Host "Adding source string to $profile" 74 | "`n$sourceJabba`n" | Out-File -Append -Encoding ASCII $profile 75 | } 76 | else 77 | { 78 | Write-Host "Skipped update of $profile (source string already present)" 79 | } 80 | 81 | @" 82 | # https://github.com/Jabba-Team/jabba 83 | # This file is intended to be "sourced" (i.e. "source ~/.jabba/jabba.nu") 84 | 85 | def --env jabba [...params:string] { 86 | `$env.JABBA_HOME = '$jabbaHome' 87 | let `$jabbaExe = '$jabbaHome/bin/jabba' | str replace --all '\' '/' 88 | let fd3 = mktemp -t jabba-fd3.XXXXXX.env 89 | nu -c `$"\`$env.JABBA_SHELL_INTEGRATION = 'ON' 90 | (`$jabbaExe) ...(`$params) --fd3 (`$fd3)" 91 | let exit_code = `$env.LAST_EXIT_CODE 92 | if ( ls `$fd3 | where size > 0B | is-not-empty ) { 93 | ( 94 | open `$fd3 95 | | str trim 96 | | lines 97 | | parse 'export {name}="{value}"' 98 | | transpose --header-row --as-record)| load-env 99 | } 100 | if `$exit_code != 0 { 101 | return `$exit_code 102 | } 103 | } 104 | "@ | Out-String | New-Item -Force $jabbaHome/jabba.nu 105 | # the above Out-String | New-Item lets us avoid the BOM in powershell 5.1. nushell chokes on the BOM 106 | $sourceNushell="source `'$jabbaHome\jabba.nu`'" 107 | $nuConfig="$Env:APPDATA\nushell\config.nu" 108 | 109 | if ($(Test-Path $nuConfig)){ 110 | if("$(Get-Content $nuConfig | Select-String "\\jabba.nu")" -eq "") 111 | { 112 | Write-Host "Adding source string to $nuConfig" 113 | "`n$sourceNushell`n" | Out-File -Append -Encoding ASCII $nuconfig 114 | } 115 | else 116 | { 117 | Write-Host "Skipped update of $nuConfig (source string already present)" 118 | } 119 | } 120 | . $jabbaHome\jabba.ps1 121 | 122 | Write-Host @" 123 | 124 | Installation completed 125 | (if you have any problems please report them at https://github.com/Jabba-Team/jabba/issues) 126 | "@ 127 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.3.1 h1:4CEBDLZtuloRJFiIzzlR/VcQOCiFzhaaa7hE4DEB97Y= 2 | github.com/Masterminds/semver v1.3.1/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 3 | github.com/Sirupsen/logrus v0.10.0 h1:I5b9VTLOttchcwWCzzNfRDAW2EFGlEN49hyoyq6d2ZI= 4 | github.com/Sirupsen/logrus v0.10.0/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 5 | github.com/aktau/github-release v0.10.0 h1:4U+9iRM7n094ZCROdnoah084FDmdQ01hwQsz0f0hwIw= 6 | github.com/aktau/github-release v0.10.0/go.mod h1:cPkP83iRnV8pAJyQlQ4vjLJoC+JE+aT5sOrYz3sTsX0= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 10 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 11 | github.com/github-release/github-release v0.10.0 h1:nJ3oEV2JrC0brYi6B8CsXumn/ORFeiAEOB2fwN9epOw= 12 | github.com/github-release/github-release v0.10.0/go.mod h1:CcaWgA5VoBGz94mOHYIXavqUA8kADNZxU+5/oDQxF6o= 13 | github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:VBj0QYQ0u2MCJzBfeYXGexnAl17GsH1yidnoxCqqD9E= 14 | github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= 15 | github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= 16 | github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 17 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 18 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 19 | github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c h1:hnbwWED5rIu+UaMkLR3JtnscMVGqp35lfzQwLuZAAUY= 20 | github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c/go.mod h1:pD+iEcdAGVXld5foVN4e24zb/6fnb60tgZPZ3P/3T/I= 21 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 22 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 25 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 26 | github.com/mitchellh/go-homedir v0.0.0-20160301183130-981ab348d865 h1:bCuq5mC5XKQognwAQG20DK+aKXCl9qbQr253imrLfMY= 27 | github.com/mitchellh/go-homedir v0.0.0-20160301183130-981ab348d865/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 28 | github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= 29 | github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= 30 | github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= 31 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 32 | github.com/mitchellh/ioprogress v0.0.0-20150521211556-816395526456 h1:GPLl+eMs5PMtqSsc405es5pDH2k06xJilq8xrg2A1NI= 33 | github.com/mitchellh/ioprogress v0.0.0-20150521211556-816395526456/go.mod h1:waEya8ee1Ro/lgxpVhkJI4BVASzkm3UZqkx/cFJiYHM= 34 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= 35 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/spf13/cobra v0.0.0-20160322171042-c678ff029ee2 h1:ZUpMwiqoxlvb/9AuwEkR5ISR/oUsgztJiMykpL5lU2w= 39 | github.com/spf13/cobra v0.0.0-20160322171042-c678ff029ee2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 40 | github.com/spf13/pflag v0.0.0-20151218134703-7f60f83a2c81 h1:e8OMOPK+iXlzdnq5GOtSZDnw9HJi1faEKhCoEIxVUrY= 41 | github.com/spf13/pflag v0.0.0-20151218134703-7f60f83a2c81/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 44 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= 46 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= 47 | github.com/voxelbrain/goptions v0.0.0-20180630082107-58cddc247ea2 h1:txplJASvd6b/hrE0s/Ixfpp2cuwH9IO9oZBAN9iYa4A= 48 | github.com/voxelbrain/goptions v0.0.0-20180630082107-58cddc247ea2/go.mod h1:DGCIhurYgnLz8J9ga1fMV/fbLDyUvTyrWXVWUIyJon4= 49 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 50 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 51 | golang.org/x/sys v0.0.0-20160322232243-afce3de5756c h1:rvKEj3qecbotxLr4L4QmX+cKYj+t0LEVfTG5eSvNqMk= 52 | golang.org/x/sys v0.0.0-20160322232243-afce3de5756c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 56 | gopkg.in/yaml.v2 v2.0.0-20160928153709-a5b47d31c556 h1:hKXbLW5oaJoQgs8KrzTLdF4PoHi+0oQPgea9TNtvE3E= 57 | gopkg.in/yaml.v2 v2.0.0-20160928153709-a5b47d31c556/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 58 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 59 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [UNRELEASED] - 2021-11-20 6 | 7 | ### Changed 8 | - Old default Java version in README.md 9 | - Update dependencies in go.mod and go.sum 10 | 11 | ### Added 12 | - Homebrew package is broken note in README.md 13 | 14 | ## [0.11.2](https://github.com/shyiko/jabba/compare/0.11.1...0.11.2) - 2019-01-06 15 | 16 | - Oracle JDK installation on macOS ([#350](https://github.com/shyiko/jabba/issues/350)). 17 | 18 | ## [0.11.1](https://github.com/shyiko/jabba/compare/0.11.0...0.11.1) - 2018-12-18 19 | 20 | ### Fixed 21 | - Oracle JDK installation on macOS ([#350](https://github.com/shyiko/jabba/issues/350)). 22 | - Broken symlinks (`tar.*`) ([#356](https://github.com/shyiko/jabba/issues/356)). 23 | 24 | ## [0.11.0](https://github.com/shyiko/jabba/compare/0.10.1...0.11.0) - 2018-10-11 25 | 26 | ### Added 27 | - Flags to filter remote versions by `--os` and `--arch` (e.g. `jabba ls-remote --os=windows`) ([#240](https://github.com/shyiko/jabba/issues/240)) 28 | 29 | ### Fixed 30 | - File tree normalization 31 | (Adopt OpenJDK 11 distributions contain meta file at the root level causing untgz --strip to fail) ([#311](https://github.com/shyiko/jabba/issues/311)). 32 | 33 | ## [0.10.1](https://github.com/shyiko/jabba/compare/0.10.0...0.10.1) - 2018-05-07 34 | 35 | ### Fixed 36 | - `jabba install ` not checking whether JDK is already installed. 37 | 38 | ## [0.10.0](https://github.com/shyiko/jabba/compare/0.9.6...0.10.0) - 2018-05-06 39 | 40 | - [OpenJDK with Shenandoah GC](https://wiki.openjdk.java.net/display/shenandoah/Main) support ([#191](https://github.com/shyiko/jabba/issues/191)) 41 | (e.g. `jabba install openjdk-shenandoah@1.9`). 42 | - Ability to install JDK from `tar.xz` archives 43 | (e.g. `jabba install openjdk-shenandoah@1.9.0-220=tgx+file://$PWD/local-copy-of-openjdk-shenandoah-jdk9-b220-x86-release.tar.xz`). 44 | 45 | ## [0.9.6](https://github.com/shyiko/jabba/compare/0.9.5...0.9.6) - 2018-05-05 46 | 47 | ### Fixed 48 | - Sporadic "open: permission denied" when installing from tgz/zip's ([#190](https://github.com/shyiko/jabba/issues/190)). 49 | Fix applied in 0.9.5 proved to be incomplete. 50 | 51 | ## [0.9.5](https://github.com/shyiko/jabba/compare/0.9.4...0.9.5) - 2018-05-04 52 | 53 | ### Fixed 54 | - Sporadic "open: permission denied" when installing from tgz/zip's ([#190](https://github.com/shyiko/jabba/issues/190)). 55 | 56 | ## [0.9.4](https://github.com/shyiko/jabba/compare/0.9.3...0.9.4) - 2018-05-01 57 | 58 | ### Fixed 59 | - Installation from sources containing `Contents/Home` (macOS) (regression introduced in 0.9.3). 60 | 61 | ## [0.9.3](https://github.com/shyiko/jabba/compare/0.9.2...0.9.3) - 2018-04-30 62 | 63 | ### Fixed 64 | - `Contents/Home` handling (macOS) ([#187](https://github.com/shyiko/jabba/issues/187)). 65 | 66 | ## [0.9.2](https://github.com/shyiko/jabba/compare/0.9.1...0.9.2) - 2017-11-18 67 | 68 | ### Fixed 69 | - `zip` & `tgz` stripping on Windows ([#116](https://github.com/shyiko/jabba/issues/116)). 70 | 71 | ## [0.9.1](https://github.com/shyiko/jabba/compare/0.9.0...0.9.1) - 2017-10-12 72 | 73 | ### Fixed 74 | - `tgz is not supported` when trying to install JDK from `tar.gz` on macOS & Windows. 75 | 76 | ## [0.9.0](https://github.com/shyiko/jabba/compare/0.8.0...0.9.0) - 2017-09-19 77 | 78 | ### Added 79 | - Latest JDK / `default` alias (automatic) linking ([#6](https://github.com/shyiko/jabba/issues/6)) 80 | 81 | ```sh 82 | $ ll ~/.jabba/jdk/ 83 | lrwxrwxrwx 1 shyiko shyiko 30 Sep 19 2017 1.8 -> /home/shyiko/.jabba/jdk/1.8.144/ 84 | drwxr-xr-x 8 shyiko shyiko 4096 Sep 19 2017 1.8.144/ 85 | drwxr-xr-x 8 shyiko shyiko 4096 Sep 19 2017 1.8.141/ 86 | lrwxrwxrwx 8 shyiko shyiko 30 Sep 19 2017 zulu@1.6 -> /home/shyiko/.jabba/jdk/zulu@1.6.97/ 87 | drwxr-xr-x 8 shyiko shyiko 4096 Sep 19 2017 zulu@1.6.97/ 88 | lrwxrwxrwx 1 shyiko shyiko 30 Sep 19 2017 default -> /home/shyiko/.jabba/jdk/1.8.144/ 89 | ``` 90 | 91 | ## [0.8.0](https://github.com/shyiko/jabba/compare/0.7.0...0.8.0) - 2017-09-19 92 | 93 | ### Added 94 | - [Adopt OpenJDK](https://adoptopenjdk.net/) support. 95 | - `jabba ls ` & `jabba ls-remote `. 96 | - `jabba ls --latest=` & `jabba ls-remote --latest=`. 97 | 98 | ```sh 99 | $ jabba ls-remote "zulu@<1.9" --latest=minor 100 | zulu@1.8.144 101 | zulu@1.7.154 102 | zulu@1.6.97 103 | ``` 104 | 105 | - Ability to install JDK in a custom location (`jabba install -o /jdk/destination`) 106 | NOTE: any JDK installed in this way is considered to be unmanaged, i.e. not available to `jabba ls`, `jabba use`, etc. (unless `jabba link`ed). 107 | 108 | ### Changed 109 | - semver library to [masterminds/semver](https://github.com/Masterminds/semver) 110 | (previously used library proved unreliable when given certain input (e.g. `>=1.6`)). 111 | 112 | ## [0.7.0](https://github.com/shyiko/jabba/compare/0.6.1...0.7.0) - 2017-05-12 113 | 114 | ### Added 115 | * Ability to change the location of `~/.jabba` with `JABBA_HOME` env variable (e.g. 116 | `curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | JABBA_HOME=/opt/jabba bash && . /opt/jabba/jabba.sh`) 117 | * `--home` flag for `jabba which` (`jabba which --home ` returns `$JABBA_DIR/jdk//Contents/Home` on macOS and 118 | `$JABBA_DIR/jdk/` everywhere else) 119 | 120 | ### Changed 121 | * `~` directory referencing inside shell integration scripts (path to home directory is now determined by `$PATH`). 122 | 123 | ### Fixed 124 | * `jabba deactivate` escaping. 125 | * `JAVA_HOME` propagation in Fish shell on CentOS 7. 126 | 127 | ## [0.6.1](https://github.com/shyiko/jabba/compare/0.6.0...0.6.1) - 2017-02-27 128 | 129 | ### Fixed 130 | * `x509: certificate signed by unknown authority` while executing `jabba ls-remote` (macOS) ([#56](https://github.com/shyiko/jabba/issues/56)). 131 | 132 | ## [0.6.0](https://github.com/shyiko/jabba/compare/0.5.1...0.6.0) - 2016-12-10 133 | 134 | ### Added 135 | * IBM SDK, Java Technology Edition support (e.g. `jabba install ibm@`). 136 | 137 | ## [0.5.1](https://github.com/shyiko/jabba/compare/0.5.0...0.5.1) - 2016-11-28 138 | 139 | ### Fixed 140 | * `link` command (it was missing `mkdir -p ...` call). 141 | 142 | ## [0.5.0](https://github.com/shyiko/jabba/compare/0.4.0...0.5.0) - 2016-11-11 143 | 144 | ### Added 145 | * `.jabbarc` support. 146 | 147 | ## [0.4.0](https://github.com/shyiko/jabba/compare/0.3.3...0.4.0) - 2016-07-22 148 | 149 | ### Added 150 | * Windows 10 support. 151 | * `link`/`unlink` commands. 152 | 153 | ### Fixed 154 | * `bin+file://` handling (original file is now copied instead of being moved). 155 | 156 | ## [0.3.3](https://github.com/shyiko/jabba/compare/0.3.2...0.3.3) - 2016-03-30 157 | 158 | ### Fixed 159 | * `dmg` handling when GNU tar is installed. 160 | 161 | ## [0.3.2](https://github.com/shyiko/jabba/compare/0.3.1...0.3.2) - 2016-03-26 162 | 163 | ### Fixed 164 | * Zulu OpenJDK installation. 165 | 166 | ## [0.3.1](https://github.com/shyiko/jabba/compare/0.3.0...0.3.1) - 2016-03-26 167 | 168 | ### Fixed 169 | * `current` (previously output was written to stderr instead of stdout). 170 | 171 | ## [0.3.0](https://github.com/shyiko/jabba/compare/0.2.0...0.3.0) - 2016-03-26 172 | 173 | ### Added 174 | * Zulu OpenJDK support (e.g. `jabba install zulu@`). 175 | * Ability to install JDK from `zip` archives (in addition to already implemented `dmg`/`tar.gz`/`bin`). 176 | * Support for custom registries (e.g. `JABBA_INDEX=https://github.com/shyiko/jabba/raw/master/index.json jabba install ...`). 177 | 178 | ### Fixed 179 | * `which `. 180 | 181 | ## [0.2.0](https://github.com/shyiko/jabba/compare/0.1.0...0.2.0) - 2016-03-24 182 | 183 | ### Added 184 | * `alias default`/`unalias default`, `which`, `deactivate` commands. 185 | 186 | ## 0.1.0 - 2016-03-23 187 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | { # this ensures the entire script is downloaded 3 | 4 | JABBA_HOME=${JABBA_HOME:-$JABBA_DIR} # JABBA_DIR is here for backward-compatibility 5 | JABBA_VERSION=${JABBA_VERSION:-latest} 6 | 7 | if [ "$JABBA_HOME" == "" ] || [ "$JABBA_HOME" == "$HOME/.jabba" ]; then 8 | JABBA_HOME=$HOME/.jabba 9 | JABBA_HOME_TO_EXPORT=\${HOME}/.jabba 10 | JABBA_HOME_TO_EXPORT_NUSHELL='~/.jabba' 11 | else 12 | JABBA_HOME_TO_EXPORT=$JABBA_HOME 13 | fi 14 | 15 | # Install location customizations 16 | JABBA_BIN=${JABBA_BIN:-"$JABBA_HOME/bin"} 17 | JABBA_BIN_TO_EXPORT=${JABBA_BIN_TO_EXPORT:-"$JABBA_HOME_TO_EXPORT/bin"} 18 | JABBA_SHARE=${JABBA_SHARE:-$JABBA_HOME} 19 | 20 | has_command() { 21 | if ! command -v "$1" > /dev/null 2>&1 22 | then echo 1; 23 | else echo 0; 24 | fi 25 | } 26 | 27 | SKIP_RC= 28 | while :; do 29 | case "$1" in 30 | --skip-rc) # skip rc file scripts 31 | SKIP_RC="1" 32 | ;; 33 | *) 34 | break 35 | ;; 36 | esac 37 | shift 38 | done 39 | 40 | # curl looks for HTTPS_PROXY while wget for https_proxy 41 | https_proxy=${https_proxy:-$HTTPS_PROXY} 42 | HTTPS_PROXY=${HTTPS_PROXY:-$https_proxy} 43 | 44 | if [ "$JABBA_GET" == "" ]; then 45 | if [ 0 -eq "$(has_command curl)" ]; then 46 | JABBA_GET="curl -sL" 47 | elif [ 0 -eq "$(has_command wget)" ]; then 48 | JABBA_GET="wget -qO-" 49 | else 50 | echo "[ERROR] This script needs wget or curl to be installed." 51 | exit 1 52 | fi 53 | fi 54 | 55 | if [ "$JABBA_VERSION" == "latest" ]; then 56 | # resolving "latest" to an actual tag 57 | JABBA_VERSION=$($JABBA_GET https://Jabba-Team.github.io/jabba/latest) 58 | fi 59 | 60 | # http://semver.org/spec/v2.0.0.html 61 | if [[ ! "$JABBA_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.+-]+)?$ ]]; then 62 | echo "'$JABBA_VERSION' is not a valid version." 63 | exit 1 64 | fi 65 | 66 | case "$OSTYPE" in 67 | darwin*) 68 | case "$(uname -m)" in 69 | x86_64*) 70 | OSARCH=amd64 71 | ;; 72 | arm64*) 73 | OSARCH=arm64 74 | ;; 75 | esac 76 | printf "Downloading for mac for arch %s" "${OSARCH}" 77 | BINARY_URL=https://github.com/Jabba-Team/jabba/releases/download/${JABBA_VERSION}/jabba-${JABBA_VERSION}-darwin-${OSARCH} 78 | ;; 79 | linux*) 80 | case "$(uname -m)" in 81 | arm*|aarch*) 82 | if [ "$(getconf LONG_BIT)" = "64" ]; then OSARCH=arm64; else OSARCH=arm; fi 83 | ;; 84 | s390x) 85 | OS_ARCH=s390x 86 | echo "OS_ARCH='$OS_ARCH' is not a valid architecture at this point." 87 | exit 1 88 | ;; 89 | s390*) 90 | if [ "$(getconf LONG_BIT)" = "64" ]; then OSARCH=s390x; else OSARCH=s390; fi 91 | echo "OS_ARCH='$OS_ARCH' is not a valid architecture at this point." 92 | exit 1 93 | ;; 94 | powerpc*) 95 | OS_ARCH=powerpc 96 | echo "OS_ARCH='$OS_ARCH' is not a valid architecture at this point." 97 | exit 1 98 | ;; 99 | ppc64*) 100 | OS_ARCH=ppc64le 101 | echo "OS_ARCH='$OS_ARCH' is not a valid architecture at this point." 102 | exit 1 103 | ;; 104 | *) 105 | if [ "$(getconf LONG_BIT)" = "64" ]; then OSARCH=amd64; else OSARCH=386; fi 106 | ;; 107 | esac 108 | BINARY_URL="https://github.com/Jabba-Team/jabba/releases/download/${JABBA_VERSION}/jabba-${JABBA_VERSION}-linux-${OSARCH}" 109 | ;; 110 | cygwin*|msys*) 111 | OS_ARCH=$(echo 'echo %PROCESSOR_ARCHITECTURE% & exit' | cmd | tail -n 1 | xargs) # xargs used to trim whitespace 112 | if [ "$OS_ARCH" == "AMD64" ]; then 113 | BINARY_URL="https://github.com/Jabba-Team/jabba/releases/download/${JABBA_VERSION}/jabba-${JABBA_VERSION}-windows-amd64.exe" 114 | elif [ "$OS_ARCH" == "x86" ]; then 115 | BINARY_URL="https://github.com/Jabba-Team/jabba/releases/download/${JABBA_VERSION}/jabba-${JABBA_VERSION}-windows-386.exe" 116 | else 117 | echo "OS_ARCH='$OS_ARCH' is not a valid architecture at this point." 118 | exit 1 119 | fi 120 | ;; 121 | *) 122 | echo "Unsupported OS $OSTYPE. If you believe this is an error - 123 | please create a ticket at https://github.com/Jabba-Team/jabba/issues." 124 | exit 1 125 | ;; 126 | esac 127 | 128 | echo "Installing v$JABBA_VERSION..." 129 | echo 130 | 131 | if [ ! -f "${JABBA_BIN}/jabba" ]; then 132 | JABBA_SELF_DESTRUCT_AFTER_COMMAND="true" 133 | fi 134 | 135 | mkdir -p "${JABBA_SHARE}" 136 | mkdir -p "${JABBA_BIN}" 137 | 138 | if [ "$JABBA_MAKE_INSTALL" == "true" ]; then 139 | cp jabba "${JABBA_BIN}" 140 | else 141 | $JABBA_GET "${BINARY_URL}" > "${JABBA_BIN}/jabba" && chmod a+x "${JABBA_BIN}/jabba" 142 | fi 143 | 144 | if ! "${JABBA_BIN}/jabba" --version &>/dev/null; then 145 | echo "${JABBA_BIN}/jabba does not appear to be a valid binary. 146 | 147 | Check your Internet connection / proxy settings and try again. 148 | If the problem persists - please create a ticket at https://github.com/Jabba-Team/jabba/issues." 149 | exit 1 150 | fi 151 | 152 | if [ "$JABBA_COMMAND" != "" ]; then 153 | "${JABBA_BIN}/jabba" "$JABBA_COMMAND" 154 | if [ "$JABBA_SELF_DESTRUCT_AFTER_COMMAND" == "true" ]; then 155 | rm -f "${JABBA_BIN}/jabba" 156 | rmdir "${JABBA_BIN}" 157 | exit 0 158 | fi 159 | fi 160 | 161 | { 162 | echo "# https://github.com/Jabba-Team/jabba" 163 | echo "# This file is intended to be \"sourced\" (i.e. \". ~/.jabba/jabba.sh\")" 164 | echo "" 165 | echo "export JABBA_HOME=\"$JABBA_HOME_TO_EXPORT\"" 166 | echo "" 167 | echo "jabba() {" 168 | echo " local fd3=\$(mktemp /tmp/jabba-fd3.XXXXXX)" 169 | echo " (JABBA_SHELL_INTEGRATION=ON $JABBA_HOME_TO_EXPORT/bin/jabba \"\$@\" 3>| \${fd3})" 170 | echo " local exit_code=\$?" 171 | echo " eval \$(cat \${fd3})" 172 | echo " rm -f \${fd3}" 173 | echo " return \${exit_code}" 174 | echo "}" 175 | echo "" 176 | echo "if [ ! -z \"\$(jabba alias default)\" ]; then" 177 | echo " jabba use default" 178 | echo "fi" 179 | } > "${JABBA_HOME}/jabba.sh" 180 | 181 | SOURCE_JABBA="[ -s \"$JABBA_SHARE/jabba.sh\" ] && source \"$JABBA_SHARE/jabba.sh\"" 182 | 183 | if [ ! "$SKIP_RC" ]; then 184 | files=("$HOME/.bashrc") 185 | 186 | if [ -f "$HOME/.bash_profile" ]; then 187 | files+=("$HOME/.bash_profile") 188 | elif [ -f "$HOME/.bash_login" ]; then 189 | files+=("$HOME/.bash_login") 190 | elif [ -f "$HOME/.profile" ]; then 191 | files+=("$HOME/.profile") 192 | else 193 | files+=("$HOME/.bash_profile") 194 | fi 195 | 196 | for file in "${files[@]}" 197 | do 198 | touch "${file}" 199 | if ! grep -qc '/jabba.sh' "${file}"; then 200 | echo "Adding source string to ${file}" 201 | printf "%s\n" "$SOURCE_JABBA" >> "${file}" 202 | else 203 | echo "Skipped update of ${file} (source string already present)" 204 | fi 205 | done 206 | 207 | if [ -f "$(which zsh 2>/dev/null)" ]; then 208 | file="$HOME/.zshrc" 209 | touch "${file}" 210 | if ! grep -qc '/jabba.sh' "${file}"; then 211 | echo "Adding source string to ${file}" 212 | printf "%s\n" "$SOURCE_JABBA" >> "${file}" 213 | else 214 | echo "Skipped update of ${file} (source string already present)" 215 | fi 216 | fi 217 | fi 218 | 219 | { 220 | echo "# https://github.com/Jabba-Team/jabba" 221 | echo "# This file is intended to be \"sourced\" (i.e. \". ~/.jabba/jabba.fish\")" 222 | echo "" 223 | echo "set -xg JABBA_HOME \"$JABBA_HOME_TO_EXPORT\"" 224 | echo "" 225 | echo "function jabba" 226 | echo " set fd3 (mktemp /tmp/jabba-fd3.XXXXXX)" 227 | echo " env JABBA_SHELL_INTEGRATION=ON $JABBA_HOME_TO_EXPORT/bin/jabba \$argv 3> \$fd3" 228 | echo " set exit_code \$status" 229 | echo " eval (cat \$fd3 | sed \"s/^export/set -xg/g\" | sed \"s/^unset/set -e/g\" | tr '=' ' ' | sed \"s/:/\\\" \\\"/g\" | tr '\\\\n' ';')" 230 | echo " rm -f \$fd3" 231 | echo " return \$exit_code" 232 | echo "end" 233 | echo "" 234 | echo "[ ! -z (echo (jabba alias default)) ]; and jabba use default" 235 | } > "${JABBA_HOME}/jabba.fish" 236 | 237 | 238 | FISH_SOURCE_JABBA="\n[ -s \"$JABBA_SHARE/jabba.fish\" ]; and source \"$JABBA_SHARE/jabba.fish\"" 239 | 240 | if [ -f "$(which fish 2>/dev/null)" ]; then 241 | file="$HOME/.config/fish/config.fish" 242 | mkdir -p "$(dirname "${file}")" 243 | touch "${file}" 244 | if ! grep -qc '/jabba.fish' "${file}"; then 245 | echo "Adding source string to ${file}" 246 | echo -e "$FISH_SOURCE_JABBA" >>"$file" 247 | else 248 | echo "Skipped update of ${file} (source string already present)" 249 | fi 250 | fi 251 | { 252 | echo "# https://github.com/Jabba-Team/jabba" 253 | echo "# This file is intended to be \"sourced\" (i.e. \"source ~/.jabba/jabba.nu\")" 254 | echo "" 255 | echo "def --env jabba [...params:string] {" 256 | echo " \$env.JABBA_HOME = ('$JABBA_HOME_TO_EXPORT_NUSHELL' | path expand)" 257 | echo " let fd3 = mktemp -t jabba-fd3.XXXXXX.env" 258 | echo " nu -c \$\"\\\$env.JABBA_SHELL_INTEGRATION = 'ON'" 259 | echo " $JABBA_HOME_TO_EXPORT_NUSHELL/bin/jabba ...(\$params) --fd3 (\$fd3)\"" 260 | echo " let exit_code = \$env.LAST_EXIT_CODE" 261 | echo " if ( ls \$fd3 | where size > 0B | is-not-empty ) {" 262 | echo "" 263 | echo " (" 264 | echo " open \$fd3" 265 | echo " | str trim" 266 | echo " | lines" 267 | echo " | parse 'export {name}=\"{value}\"'" 268 | echo " | transpose --header-row --as-record)| load-env" 269 | echo " }" 270 | echo " if \$exit_code != 0 {" 271 | echo " return \$exit_code" 272 | echo " }" 273 | echo "}" 274 | } > "${JABBA_HOME}/jabba.nu" 275 | 276 | #nushell doesn't support sourcing files only if they exist 277 | NUSHELL_SOURCE_JABBA="\nsource \"$JABBA_SHARE/jabba.nu\"" 278 | 279 | if [ -f "$(which nu 2>/dev/null)" ]; then 280 | file="$HOME/.config/nushell/config.nu" 281 | mkdir -p "$(dirname "${file}")" 282 | touch "${file}" 283 | if ! grep -qc '/jabba.nu' "${file}"; then 284 | echo "Adding source string to ${file}" 285 | echo -e "$NUSHELL_SOURCE_JABBA" >>"$file" 286 | else 287 | echo "Skipped update of ${file} (source string already present)" 288 | fi 289 | fi 290 | 291 | echo "" 292 | echo "Installation completed 293 | (if you have any problems please report them at https://github.com/Jabba-Team/jabba/issues)" 294 | 295 | } # this ensures the entire script is downloaded 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jabba ![Latest Version](https://img.shields.io/badge/latest-0.14.0-blue.svg) [![Actions Status](https://github.com/Jabba-Team/jabba/workflows/make%20Jabba/badge.svg)](https://github.com/Jabba-Team/jabba/actions) 2 | 3 | Java Version Manager inspired by [nvm](https://github.com/creationix/nvm) (Node.js). Written in Go. 4 | 5 | The goal is to provide unified pain-free experience of **installing** (and **switching** between different versions of) JDK regardless of 6 | the OS (macOS, Linux x86/x86_64/ARMv7+, Windows x86_64). 7 | 8 | This is a community fork of the [original project](https://github.com/shyiko/jabba) and is currently a work in progress. There may be some out of date instructions in the README, please bear with us while we get things going. 9 | 10 | `jabba install` 11 | 12 | The JDK index is powered by [DiscoAPI](https://github.com/foojayio/discoapi) as such some of the JDK's may be named slightly differently from the old index 13 | The index will give you the list of jdks for your OS and architecture 14 | 15 | - [Oracle JDK](http://www.oracle.com/technetwork/java/javase/archive-139210.html) (latest-version only) 16 | - [Oracle Server JRE](http://www.oracle.com/technetwork/java/javase/downloads/server-jre8-downloads-2133154.html) (latest-version only), 17 | - [Adopt OpenJDK](https://adoptopenjdk.net/) (jabba >=0.8.0 is required) 18 | - Hotspot 19 | - [Eclipse OpenJ9](https://www.eclipse.org/openj9/oj9_faq.html) 20 | - [Zulu OpenJDK](http://zulu.org/) (jabba >=0.3.0 is required) 21 | - [IBM SDK, Java Technology Edition](https://developer.ibm.com/javasdk/) (jabba >=0.6.0 is required) 22 | - [GraalVM CE](https://www.graalvm.org/) 23 | - [OpenJDK](http://openjdk.java.net/) 24 | - [OpenJDK Reference Implementation](http://openjdk.java.net/) 25 | - [OpenJDK with Shenandoah GC](https://wiki.openjdk.java.net/display/shenandoah/Main) (jabba >=0.10.0 is required) 26 | - [Liberica JDK](https://bell-sw.com/) 27 | - [Amazon Corretto](https://aws.amazon.com/corretto/) 28 | 29 | 30 | ... and from custom URLs. 31 | 32 | ## Install-less 33 | 34 | See [jabba-wrapper](jabbaw.md) 35 | 36 | ## Installation 37 | 38 | #### macOS homebrew 39 | 40 | ```sh 41 | brew install jabba 42 | ``` 43 | 44 | Add the following to your .zshrc 45 | 46 | ```sh 47 | [ -s "$HOMEBREW_PREFIX/opt/jabba/jabba.sh" ] && . "$HOMEBREW_PREFIX/opt/jabba/jabba.sh" 48 | ``` 49 | If `HOMEBREW_PREFIX` isn't already defined for you run: 50 | 51 | ```sh 52 | brew --prefix 53 | ``` 54 | to get the value 55 | 56 | #### macOS / Linux 57 | 58 | > (in bash/zsh/...) 59 | 60 | ```sh 61 | export JABBA_VERSION=... 62 | curl -sL https://github.com/Jabba-Team/jabba/raw/main/install.sh | bash && . ~/.jabba/jabba.sh 63 | ``` 64 | 65 | > Use the same command to upgrade, you can also upgrade from shyiko's 0.11.2 by running this command 66 | 67 | The script modifies common shell rc files by default. To skip these provide the `--skip-rc` flag to `install.sh` like so: 68 | 69 | ```sh 70 | export JABBA_VERSION=... 71 | curl -sL https://github.com/Jabba-Team/jabba/raw/main/install.sh | bash -s -- --skip-rc && . ~/.jabba/jabba.sh 72 | ``` 73 | 74 | Make sure to source `jabba.sh` in your environment if you skip it: 75 | 76 | ```sh 77 | export JABBA_VERSION=... 78 | [ -s "$JABBA_HOME/jabba.sh" ] && source "$JABBA_HOME/jabba.sh" 79 | ``` 80 | 81 | > In [fish](https://fishshell.com/) command looks a little bit different - 82 | > export JABBA_VERSION=... 83 | `curl -sL https://github.com/Jabba-Team/jabba/raw/main/install.sh | bash; and . ~/.jabba/jabba.fish` 84 | 85 | > If you don't have `curl` installed - replace `curl -sL` with `wget -qO-`. 86 | 87 | > If you are behind a proxy see - 88 | [curl](https://curl.haxx.se/docs/manpage.html#ENVIRONMENT) / 89 | [wget](https://www.gnu.org/software/wget/manual/wget.html#Proxies) manpage. 90 | Usually simple `http_proxy=http://proxy-server:port https_proxy=http://proxy-server:port curl -sL ...` is enough. 91 | 92 | #### Docker 93 | 94 | While you can use the same snippet as above, chances are you don't want jabba binary & shell 95 | integration script(s) to be included in the final Docker image, all you want is a JDK. Here is the `Dockerfile` showing how this can be done: 96 | 97 | ```dockerfile 98 | FROM buildpack-deps:jessie-curl 99 | 100 | RUN curl -sL https://github.com/Jabba-Team/jabba/raw/main/install.sh | \ 101 | JABBA_COMMAND="install 1.15.0 -o /jdk" bash 102 | 103 | ENV JAVA_HOME /jdk 104 | ENV PATH $JAVA_HOME/bin:$PATH 105 | ``` 106 | 107 | > (when `JABBA_COMMAND` env variable is set `install.sh` downloads `jabba` binary, executes specified command and then deletes the binary) 108 | 109 | ```sh 110 | $ docker build -t : . 111 | $ docker run -it --rm : java -version 112 | 113 | java version "1.15.0.... 114 | ``` 115 | 116 | #### Windows 10 117 | 118 | > (in powershell) 119 | 120 | ```powershell 121 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 122 | Invoke-Expression ( 123 | Invoke-WebRequest https://github.com/Jabba-Team/jabba/raw/main/install.ps1 -UseBasicParsing 124 | ).Content 125 | ``` 126 | 127 | > Use the same command to upgrade, you can also upgrade from shyiko's 0.11.2 by running this command 128 | 129 | > Scoop 130 | 131 | Whilst jabba is listed in the Scoop package manager, scoop only install the binary, it doesn't add the shell integration, it is recommended you install from the script above or try adding the shell integration manually while we investigate the proper fix, see the (issue here)[https://github.com/Jabba-Team/jabba/issues/48] for details. 132 | 133 | ## Usage 134 | 135 | ```sh 136 | # list available JDK's 137 | jabba ls-remote 138 | # you can use any valid semver range to narrow down the list 139 | jabba ls-remote zulu@~1.8.60 140 | jabba ls-remote "*@>=1.6.45 <1.9" --latest=minor 141 | 142 | # install Oracle JDK 143 | jabba install 1.15.0 144 | # install Oracle Server JRE 145 | jabba install sjre@1.8 146 | # install Eclipse Temurin / Adoptium (Hotspot) 147 | jabba install temurin@1.8-0 148 | # install Adopt OpenJDK (Hotspot) 149 | jabba install aoj@1.8-0 150 | # install Adopt OpenJDK (Eclipse OpenJ9) 151 | jabba install aoj_openj9@1.9-0 152 | # install Zulu OpenJDK 153 | jabba install zulu@1.8 154 | jabba install zulu@~1.8.144 # same as "zulu@>=1.8.144 <1.9" 155 | # install IBM SDK, Java Technology Edition 156 | jabba install semeru@1.8 157 | # install GraalVM CE 158 | jabba install graalvm@1.0-0 159 | # install Oracle OpenJDK 160 | jabba install oracle_open_jdk@1.10-0 161 | # install Microsoft OpenJDK 162 | jabba install microsoft@11.0.16 163 | # install OpenJDK with Shenandoah GC 164 | jabba install openjdk-shenandoah@1.10-0 165 | 166 | # install from custom URL 167 | # (supported qualifiers: zip (since 0.3.0), tgz, tgx (since 0.10.0), dmg, bin, exe) 168 | jabba install 1.8.0-custom=tgz+http://example.com/distribution.tar.gz 169 | jabba install 1.8.0-custom=tgx+http://example.com/distribution.tar.xz 170 | jabba install 1.8.0-custom=zip+file:///opt/distribution.zip 171 | 172 | # uninstall JDK 173 | jabba uninstall zulu@1.6.77 174 | 175 | # link system JDK 176 | jabba link system@1.8.72 /Library/Java/JavaVirtualMachines/jdk1.8.0_72.jdk 177 | 178 | # list all installed JDK's 179 | jabba ls 180 | 181 | # switch to a different version of JDK (it must be already `install`ed) 182 | jabba use aoj@1.8 183 | jabba use zulu@~1.6.97 184 | 185 | echo "1.8" > .jabbarc 186 | # switch to the JDK specified in .jabbarc (since 0.5.0) 187 | jabba use 188 | 189 | # set default java version on shell (since 0.2.0) 190 | # this version will automatically be "jabba use"d every time you open up a new terminal 191 | jabba alias default 1.8 192 | ``` 193 | 194 | > `.jabbarc` has to be a valid YAML file. JDK version can be specified as `jdk: 1.8` or simply as `1.8` 195 | (same as `~1.8`, `1.8.x` `">=1.8.0 <1.9.0"` (mind the quotes)). 196 | 197 | > jsyk: **jabba** keeps everything under `~/.jabba` (on Linux/Mac OS X) / `%USERPROFILE%/.jabba` (on Windows). If at any point of time you decide to uninstall **jabba** - just remove this directory. 198 | 199 | For more information see `jabba --help`. 200 | 201 | ## Development 202 | 203 | > PREREQUISITE: [go1.8](https://github.com/moovweb/gvm) 204 | 205 | ```sh 206 | git clone https://github.com/Jabba-Team/jabba $GOPATH/src/github.com/Jabba-Team/jabba 207 | cd $GOPATH/src/github.com/Jabba-Team/jabba 208 | make fetch 209 | 210 | go run jabba.go 211 | 212 | # to test a change 213 | make test # or "test-coverage" if you want to get a coverage breakdown 214 | 215 | # to make a build 216 | make build # or "build-release" (latter is cross-compiling jabba to different OSs/ARCHs) 217 | ``` 218 | 219 | ## FAQ 220 | 221 | **Q**: What if I already have `java` installed? 222 | 223 | A: It's fine. You can switch between system JDK and `jabba`-provided one whenever you feel like it (`jabba use ...` / `jabba deactivate`). 224 | They are not gonna conflict with each other. 225 | 226 | **Q**: How do I switch `java` globally? 227 | 228 | A: **jabba** doesn't have this functionality built-in because the exact way varies greatly between the operation systems and usually 229 | involves elevated permissions. But. Here are the snippets that should work: 230 | 231 | * Windows 232 | 233 | > (in powershell as administrator) 234 | 235 | ``` 236 | # select jdk 237 | jabba use ... 238 | 239 | # modify global PATH & JAVA_HOME 240 | $envRegKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment', $true) 241 | $envPath=$envRegKey.GetValue('Path', $null, "DoNotExpandEnvironmentNames").replace('%JAVA_HOME%\bin;', '') 242 | [Environment]::SetEnvironmentVariable('JAVA_HOME', "$(jabba which $(jabba current))", 'Machine') 243 | [Environment]::SetEnvironmentVariable('PATH', "%JAVA_HOME%\bin;$envPath", 'Machine') 244 | ``` 245 | 246 | * Linux 247 | 248 | > (tested on Debian/Ubuntu) 249 | 250 | ``` 251 | # select jdk 252 | jabba use ... 253 | 254 | sudo update-alternatives --install /usr/bin/java java ${JAVA_HOME%*/}/bin/java 20000 255 | sudo update-alternatives --install /usr/bin/javac javac ${JAVA_HOME%*/}/bin/javac 20000 256 | ``` 257 | 258 | > To switch between multiple GLOBAL alternatives use `sudo update-alternatives --config java`. 259 | 260 | ## License 261 | 262 | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 263 | 264 | By using this software you agree to 265 | - [Oracle Binary Code License Agreement for the Java SE Platform Products and JavaFX](http://www.oracle.com/technetwork/java/javase/terms/license/index.html) 266 | - [Oracle Technology Network Early Adopter Development License Agreement](http://www.oracle.com/technetwork/licenses/ea-license-noexhibits-1938914.html) in case of EA releases 267 | - Apple's Software License Agreement in case of "Java for OS X" 268 | - [International License Agreement for Non-Warranted Programs](http://www14.software.ibm.com/cgi-bin/weblap/lap.pl?la_formnum=&li_formnum=L-PMAA-A3Z8P2&l=en) in case of IBM SDK, Java Technology Edition. 269 | 270 | This software is for educational purposes only. 271 | Use it at your own risk. 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /jabba.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "runtime" 11 | "sort" 12 | "strings" 13 | 14 | yaml "gopkg.in/yaml.v2" 15 | 16 | "github.com/Jabba-Team/jabba/command" 17 | "github.com/Jabba-Team/jabba/semver" 18 | log "github.com/Sirupsen/logrus" 19 | rootcerts "github.com/hashicorp/go-rootcerts" 20 | "github.com/spf13/cobra" 21 | "github.com/spf13/pflag" 22 | ) 23 | 24 | var version string 25 | var rootCmd *cobra.Command 26 | 27 | func init() { 28 | log.SetFormatter(&simpleFormatter{}) 29 | // todo: make it configurable through the command line 30 | log.SetLevel(log.InfoLevel) 31 | 32 | tlsConfig := &tls.Config{} 33 | err := rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{ 34 | CAFile: os.Getenv("JABBA_CAFILE"), 35 | CAPath: os.Getenv("JABBA_CAPATH"), 36 | }) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | defTransport := http.DefaultTransport.(*http.Transport) 41 | defTransport.TLSClientConfig = tlsConfig 42 | } 43 | 44 | type simpleFormatter struct{} 45 | 46 | func (f *simpleFormatter) Format(entry *log.Entry) ([]byte, error) { 47 | b := &bytes.Buffer{} 48 | fmt.Fprintf(b, "%s ", entry.Message) 49 | for k, v := range entry.Data { 50 | fmt.Fprintf(b, "%s=%+v ", k, v) 51 | } 52 | b.WriteByte('\n') 53 | return b.Bytes(), nil 54 | } 55 | 56 | func main() { 57 | rootCmd = &cobra.Command{ 58 | Use: "jabba", 59 | Long: "Java Version Manager (https://github.com/Jabba-Team/jabba).", 60 | RunE: func(cmd *cobra.Command, args []string) error { 61 | if showVersion, _ := cmd.Flags().GetBool("version"); !showVersion { 62 | return pflag.ErrHelp 63 | } 64 | fmt.Println(version) 65 | return nil 66 | }, 67 | } 68 | var whichHome bool 69 | whichCmd := &cobra.Command{ 70 | Use: "which [version]", 71 | Short: "Display path to installed JDK", 72 | RunE: func(cmd *cobra.Command, args []string) error { 73 | var ver string 74 | if len(args) == 0 { 75 | ver = rc().JDK 76 | if ver == "" { 77 | return pflag.ErrHelp 78 | } 79 | } else { 80 | ver = args[0] 81 | } 82 | dir, _ := command.Which(ver, whichHome) 83 | if dir != "" { 84 | fmt.Println(dir) 85 | } 86 | return nil 87 | }, 88 | } 89 | whichCmd.Flags().BoolVar(&whichHome, "home", false, 90 | "Account for platform differences so that value could be used as JAVA_HOME (e.g. append \"/Contents/Home\" on macOS)") 91 | var customInstallDestination string 92 | installCmd := &cobra.Command{ 93 | Use: "install [version to install]", 94 | Short: "Download and install JDK", 95 | RunE: func(cmd *cobra.Command, args []string) error { 96 | var ver string 97 | if len(args) == 0 { 98 | ver = rc().JDK 99 | if ver == "" { 100 | return pflag.ErrHelp 101 | } 102 | } else { 103 | ver = args[0] 104 | } 105 | ver, err := command.Install(ver, customInstallDestination) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | if customInstallDestination == "" { 110 | if err := command.LinkLatest(); err != nil { 111 | log.Fatal(err) 112 | } 113 | return use(ver) 114 | } else { 115 | return nil 116 | } 117 | }, 118 | Example: " jabba install 1.8\n" + 119 | " jabba install ~1.8.73 # same as \">=1.8.73 <1.9.0\"\n" + 120 | " jabba install 1.8.73=dmg+http://.../jdk-9-ea+110_osx-x64_bin.dmg", 121 | } 122 | installCmd.Flags().StringVarP(&customInstallDestination, "output", "o", "", 123 | "Custom destination (any JDK outside of $JABBA_HOME/jdk is considered to be unmanaged, i.e. not available to jabba ls, use, etc. (unless `jabba link`ed))") 124 | var trimTo string 125 | lsCmd := &cobra.Command{ 126 | Use: "ls", 127 | Short: "List installed versions", 128 | RunE: func(cmd *cobra.Command, args []string) error { 129 | var r *semver.Range 130 | if len(args) > 0 { 131 | var err error 132 | r, err = semver.ParseRange(args[0]) 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | } 137 | vs, err := command.Ls() 138 | current := command.Current() 139 | if err != nil { 140 | log.Fatal(err) 141 | } 142 | if trimTo != "" { 143 | vs = semver.VersionSlice(vs).TrimTo(parseTrimTo(trimTo)) 144 | } 145 | for _, v := range vs { 146 | if r != nil && !r.Contains(v) { 147 | continue 148 | } 149 | if current == v.String() { 150 | fmt.Print("\033[32m") 151 | fmt.Println(v.String() + "*") 152 | fmt.Print("\033[0m") 153 | } else { 154 | fmt.Println(v) 155 | } 156 | } 157 | return nil 158 | }, 159 | } 160 | lsAliasCmd := &cobra.Command{ 161 | Use: "ls-alias", 162 | Short: "List configured aliases", 163 | RunE: func(cmd *cobra.Command, args []string) error { 164 | aliases, err := command.LsAlias() 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | index := 1 170 | for name, content := range aliases { 171 | fmt.Printf("%d. Alias Name: %s Content: %s\n", index, name, content) 172 | index += 1 173 | } 174 | 175 | return nil 176 | }, 177 | } 178 | lsRemoteCmd := &cobra.Command{ 179 | Use: "ls-remote", 180 | Short: "List remote versions available for install", 181 | RunE: func(cmd *cobra.Command, args []string) error { 182 | var r *semver.Range 183 | if len(args) > 0 { 184 | var err error 185 | r, err = semver.ParseRange(args[0]) 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | } 190 | os, _ := cmd.Flags().GetString("os") 191 | arch, _ := cmd.Flags().GetString("arch") 192 | releaseMap, err := command.LsRemote(os, arch) 193 | if err != nil { 194 | log.Fatal(err) 195 | } 196 | var vs = make([]*semver.Version, len(releaseMap)) 197 | var i = 0 198 | for k := range releaseMap { 199 | vs[i] = k 200 | i++ 201 | } 202 | sort.Sort(sort.Reverse(semver.VersionSlice(vs))) 203 | if trimTo != "" { 204 | vs = semver.VersionSlice(vs).TrimTo(parseTrimTo(trimTo)) 205 | } 206 | for _, v := range vs { 207 | if r != nil && !r.Contains(v) { 208 | continue 209 | } 210 | fmt.Println(v) 211 | } 212 | return nil 213 | }, 214 | } 215 | lsRemoteCmd.Flags().String("os", runtime.GOOS, "Operating System (darwin, linux, windows)") 216 | lsRemoteCmd.Flags().String("arch", runtime.GOARCH, "Architecture (amd64, 386)") 217 | for _, cmd := range []*cobra.Command{lsCmd, lsRemoteCmd} { 218 | cmd.Flags().StringVar(&trimTo, "latest", "", 219 | "Part of the version to trim to (\"major\", \"minor\" or \"patch\")") 220 | } 221 | rootCmd.AddCommand( 222 | installCmd, 223 | &cobra.Command{ 224 | Use: "uninstall [version to uninstall]", 225 | Short: "Uninstall JDK", 226 | RunE: func(cmd *cobra.Command, args []string) error { 227 | if len(args) == 0 { 228 | return pflag.ErrHelp 229 | } 230 | if strings.HasPrefix(args[0], "system@") { 231 | log.Fatal("Link to system JDK can only be removed with 'unlink'" + 232 | " (e.g. 'jabba unlink " + args[0] + "')") 233 | } 234 | err := command.Uninstall(args[0]) 235 | if err != nil { 236 | log.Fatal(err) 237 | } 238 | if err := command.LinkLatest(); err != nil { 239 | log.Fatal(err) 240 | } 241 | return nil 242 | }, 243 | Example: " jabba uninstall 1.8", 244 | }, 245 | &cobra.Command{ 246 | Use: "link [name] [path]", 247 | Short: "Resolve or update a link", 248 | RunE: func(cmd *cobra.Command, args []string) error { 249 | if len(args) == 0 { 250 | if err := command.LinkLatest(); err != nil { 251 | log.Fatal(err) 252 | } 253 | return nil 254 | } 255 | if len(args) == 1 { 256 | if value := command.GetLink(args[0]); value != "" { 257 | fmt.Println(value) 258 | } 259 | } else if err := command.Link(args[0], args[1]); err != nil { 260 | log.Fatal(err) 261 | } 262 | return nil 263 | }, 264 | Example: " jabba link system@1.8.20 /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk\n" + 265 | " jabba link system@1.8.20 # show link target", 266 | }, 267 | &cobra.Command{ 268 | Use: "unlink [name]", 269 | Short: "Delete a link", 270 | RunE: func(cmd *cobra.Command, args []string) error { 271 | if len(args) == 0 { 272 | return pflag.ErrHelp 273 | } 274 | if err := command.Link(args[0], ""); err != nil { 275 | log.Fatal(err) 276 | } 277 | return nil 278 | }, 279 | Example: " jabba unlink system@1.8.20", 280 | }, 281 | &cobra.Command{ 282 | Use: "use [version to use]", 283 | Short: "Modify PATH & JAVA_HOME to use specific JDK", 284 | RunE: func(cmd *cobra.Command, args []string) error { 285 | var ver string 286 | if len(args) == 0 { 287 | ver = rc().JDK 288 | if ver == "" { 289 | return pflag.ErrHelp 290 | } 291 | } else { 292 | ver = args[0] 293 | } 294 | return use(ver) 295 | }, 296 | Example: " jabba use 1.8\n" + 297 | " jabba use ~1.8.73 # same as \">=1.8.73 <1.9.0\"", 298 | }, 299 | &cobra.Command{ 300 | Use: "current", 301 | Short: "Display currently 'use'ed version", 302 | Run: func(cmd *cobra.Command, args []string) { 303 | ver := command.Current() 304 | if ver != "" { 305 | fmt.Println(ver) 306 | } 307 | }, 308 | }, 309 | lsCmd, 310 | lsRemoteCmd, 311 | lsAliasCmd, 312 | &cobra.Command{ 313 | Use: "deactivate", 314 | Short: "Undo effects of `jabba` on current shell", 315 | RunE: func(cmd *cobra.Command, args []string) error { 316 | out, err := command.Deactivate() 317 | if err != nil { 318 | log.Fatal(err) 319 | } 320 | printForShellToEval(out) 321 | return nil 322 | }, 323 | }, 324 | &cobra.Command{ 325 | Use: "alias [name] [version]", 326 | Short: "Resolve or update an alias", 327 | RunE: func(cmd *cobra.Command, args []string) error { 328 | if len(args) == 0 { 329 | return pflag.ErrHelp 330 | } 331 | name := args[0] 332 | if len(args) == 1 { 333 | if value := command.GetAlias(name); value != "" { 334 | fmt.Println(value) 335 | } 336 | return nil 337 | } 338 | if err := command.SetAlias(name, args[1]); err != nil { 339 | log.Fatal(err) 340 | } 341 | if err := command.LinkAlias(name); err != nil { 342 | log.Fatal(err) 343 | } 344 | return nil 345 | }, 346 | Example: " jabba alias default 1.8\n" + 347 | " jabba alias default # show value bound to an alias", 348 | }, 349 | &cobra.Command{ 350 | Use: "unalias [name]", 351 | Short: "Delete an alias", 352 | RunE: func(cmd *cobra.Command, args []string) error { 353 | if len(args) == 0 { 354 | return pflag.ErrHelp 355 | } 356 | if err := command.SetAlias(args[0], ""); err != nil { 357 | log.Fatal(err) 358 | } 359 | return nil 360 | }, 361 | }, 362 | whichCmd, 363 | ) 364 | rootCmd.Flags().Bool("version", false, "version of jabba") 365 | rootCmd.PersistentFlags().String("fd3", "", "") 366 | rootCmd.PersistentFlags().MarkHidden("fd3") 367 | if err := rootCmd.Execute(); err != nil { 368 | os.Exit(-1) 369 | } 370 | } 371 | 372 | func parseTrimTo(value string) semver.VersionPart { 373 | switch strings.ToLower(value) { 374 | case "major": 375 | return semver.VPMajor 376 | case "minor": 377 | return semver.VPMinor 378 | case "patch": 379 | return semver.VPPatch 380 | default: 381 | log.Fatal("Unexpected value of --latest (must be either \"major\", \"minor\" or \"patch\")") 382 | return -1 383 | } 384 | } 385 | 386 | type jabbarc struct { 387 | JDK string 388 | } 389 | 390 | func rc() (rc jabbarc) { 391 | b, err := ioutil.ReadFile(".jabbarc") 392 | if err != nil { 393 | return 394 | } 395 | // content can be a string (jdk version) 396 | err = yaml.Unmarshal(b, &rc.JDK) 397 | if err != nil { 398 | // or a struct 399 | err = yaml.Unmarshal(b, &rc) 400 | if err != nil { 401 | log.Fatal(".jabbarc is not valid") 402 | } 403 | } 404 | return 405 | } 406 | 407 | func use(ver string) error { 408 | out, err := command.Use(ver) 409 | if err != nil { 410 | log.Fatal(err) 411 | } 412 | printForShellToEval(out) 413 | return nil 414 | } 415 | 416 | func printForShellToEval(out []string) { 417 | fd3, _ := rootCmd.Flags().GetString("fd3") 418 | if fd3 != "" { 419 | ioutil.WriteFile(fd3, []byte(strings.Join(out, "\n")), 0666) 420 | } else { 421 | fd3 := os.NewFile(3, "fd3") 422 | for _, line := range out { 423 | fmt.Fprintln(fd3, line) 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /command/install.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "archive/tar" 5 | "archive/zip" 6 | "compress/gzip" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "regexp" 16 | "runtime" 17 | "sort" 18 | "strings" 19 | 20 | "github.com/Jabba-Team/jabba/cfg" 21 | "github.com/Jabba-Team/jabba/command/fileiter" 22 | "github.com/Jabba-Team/jabba/semver" 23 | "github.com/Jabba-Team/jabba/w32" 24 | log "github.com/Sirupsen/logrus" 25 | "github.com/mitchellh/ioprogress" 26 | "github.com/xi2/xz" 27 | ) 28 | 29 | func Install(selector string, dst string) (string, error) { 30 | var releaseMap map[*semver.Version]string 31 | var ver *semver.Version 32 | var err error 33 | // selector can be in form of = 34 | if strings.Contains(selector, "=") && strings.Contains(selector, "://") { 35 | split := strings.SplitN(selector, "=", 2) 36 | selector = split[0] 37 | // has to be valid per semver 38 | ver, err = semver.ParseVersion(selector) 39 | if err != nil { 40 | return "", err 41 | } 42 | releaseMap = map[*semver.Version]string{ver: split[1]} 43 | } else { 44 | // ... or a version (range will be tried over remote targets) 45 | ver, _ = semver.ParseVersion(selector) 46 | } 47 | // ... apparently it's not 48 | if releaseMap == nil { 49 | ver = nil 50 | rng, err := semver.ParseRange(selector) 51 | if err != nil { 52 | return "", err 53 | } 54 | releaseMap, err = LsRemote(runtime.GOOS, runtime.GOARCH) 55 | if err != nil { 56 | return "", err 57 | } 58 | var vs = make([]*semver.Version, len(releaseMap)) 59 | var i = 0 60 | for k := range releaseMap { 61 | vs[i] = k 62 | i++ 63 | } 64 | sort.Sort(sort.Reverse(semver.VersionSlice(vs))) 65 | for _, v := range vs { 66 | if rng.Contains(v) { 67 | ver = v 68 | break 69 | } 70 | } 71 | if ver == nil { 72 | tt := make([]string, len(vs)) 73 | for i, v := range vs { 74 | tt[i] = v.String() 75 | } 76 | return "", errors.New("No compatible version found for " + selector + 77 | "\nValid install targets: " + strings.Join(tt, ", ")) 78 | } 79 | } 80 | // check whether requested version is already installed 81 | if ver != nil && dst == "" { 82 | local, err := Ls() 83 | if err != nil { 84 | return "", err 85 | } 86 | for _, v := range local { 87 | if ver.Equals(v) { 88 | return ver.String(), nil 89 | } 90 | } 91 | } 92 | url := releaseMap[ver] 93 | if matched, _ := regexp.MatchString("^\\w+[+]\\w+://", url); !matched { 94 | return "", errors.New("URL must contain qualifier, e.g. tgz+http://...") 95 | } 96 | if dst == "" { 97 | dst = filepath.Join(cfg.Dir(), "jdk", ver.String()) 98 | } else { 99 | if _, err := os.Stat(dst); !os.IsNotExist(err) { 100 | if err == nil { // dst exists 101 | if empty, _ := isEmptyDir(dst); !empty { 102 | err = fmt.Errorf("\"%s\" is not empty", dst) 103 | } 104 | } // or is inaccessible 105 | if err != nil { 106 | return "", err 107 | } 108 | } 109 | } 110 | var fileType = url[0:strings.Index(url, "+")] 111 | url = url[strings.Index(url, "+")+1:] 112 | var file string 113 | var deleteFileWhenFinnished bool 114 | if strings.HasPrefix(url, "file://") { 115 | file = strings.TrimPrefix(url, "file://") 116 | if runtime.GOOS == "windows" { 117 | // file:///C:/path/... 118 | file = strings.Replace(strings.TrimPrefix(file, "/"), "/", "\\", -1) 119 | } 120 | } else { 121 | log.Info("Downloading ", ver, " (", url, ")") 122 | file, err = download(url, fileType) 123 | if err != nil { 124 | return "", err 125 | } 126 | deleteFileWhenFinnished = true 127 | } 128 | switch runtime.GOOS { 129 | case "darwin": 130 | err = installOnDarwin(file, fileType, dst) 131 | case "linux": 132 | err = installOnLinux(file, fileType, dst) 133 | case "windows": 134 | err = installOnWindows(file, fileType, dst) 135 | default: 136 | err = errors.New(runtime.GOOS + " OS is not supported") 137 | } 138 | if err == nil && deleteFileWhenFinnished { 139 | os.Remove(file) 140 | } 141 | return ver.String(), err 142 | } 143 | 144 | func isEmptyDir(name string) (bool, error) { 145 | entries, err := ioutil.ReadDir(name) 146 | if err != nil { 147 | return false, err 148 | } 149 | return len(entries) == 0, nil 150 | } 151 | 152 | type RedirectTracer struct { 153 | Transport http.RoundTripper 154 | } 155 | 156 | func (self RedirectTracer) RoundTrip(req *http.Request) (resp *http.Response, err error) { 157 | transport := self.Transport 158 | if transport == nil { 159 | transport = http.DefaultTransport 160 | } 161 | resp, err = transport.RoundTrip(req) 162 | if err != nil { 163 | return 164 | } 165 | switch resp.StatusCode { 166 | case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: 167 | log.Debug("Following ", resp.StatusCode, " redirect to ", resp.Header.Get("Location")) 168 | } 169 | return 170 | } 171 | 172 | func download(url string, fileType string) (file string, err error) { 173 | tmp, err := ioutil.TempFile("", "jabba-d-") 174 | if err != nil { 175 | return 176 | } 177 | if fileType == "exe" { 178 | err = tmp.Close() 179 | if err != nil { 180 | return 181 | } 182 | err = os.Rename(tmp.Name(), tmp.Name()+".exe") 183 | if err != nil { 184 | return 185 | } 186 | tmp, err = os.OpenFile(tmp.Name()+".exe", os.O_RDWR, 0600) 187 | if err != nil { 188 | return 189 | } 190 | } 191 | defer tmp.Close() 192 | file = tmp.Name() 193 | log.Debug("Saving ", url, " to ", file) 194 | // todo: timeout 195 | client := http.Client{Transport: RedirectTracer{}} 196 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 197 | if len(via) >= 10 { 198 | return fmt.Errorf("too many redirects") 199 | } 200 | if len(via) != 0 { 201 | // https://github.com/golang/go/issues/4800 202 | for attr, val := range via[0].Header { 203 | if _, ok := req.Header[attr]; !ok { 204 | req.Header[attr] = val 205 | } 206 | } 207 | } 208 | return nil 209 | } 210 | req, err := http.NewRequest("GET", url, nil) 211 | if strings.Contains(url, "zulu") { 212 | req.Header.Set("Referer", "http://www.azul.com/downloads/zulu/") 213 | } 214 | req.Header.Set("Cookie", "oraclelicense=accept-securebackup-cookie") 215 | res, err := client.Do(req) 216 | if err != nil { 217 | return 218 | } 219 | defer res.Body.Close() 220 | progressTracker := &ioprogress.Reader{ 221 | Reader: res.Body, 222 | Size: res.ContentLength, 223 | } 224 | _, err = io.Copy(tmp, progressTracker) 225 | if err != nil { 226 | return 227 | } 228 | return 229 | } 230 | 231 | func installOnDarwin(file string, fileType string, dst string) (err error) { 232 | switch fileType { 233 | case "dmg": 234 | err = installFromDmg(file, dst) 235 | case "tgz": 236 | err = installFromTgz(file, dst) 237 | case "tgx": 238 | err = installFromTgx(file, dst) 239 | case "zip": 240 | err = installFromZip(file, dst) 241 | default: 242 | return errors.New(fileType + " is not supported") 243 | } 244 | if err == nil { 245 | err = normalizePathToBinJava(dst, runtime.GOOS) 246 | } 247 | if err != nil { 248 | os.RemoveAll(dst) 249 | } 250 | return 251 | } 252 | 253 | // **/{Contents/Home,Home,}bin/java -> /Contents/Home/bin/java 254 | func normalizePathToBinJava(dir string, goos string) error { 255 | dir = filepath.Clean(dir) 256 | if _, err := os.Stat(expectedJavaPath(dir, goos)); os.IsNotExist(err) { 257 | java := "java" 258 | if goos == "windows" { 259 | java = "java.exe" 260 | } 261 | var javaPath string 262 | for it := fileiter.New(dir, fileiter.BreadthFirst()); it.Next(); { 263 | if err := it.Err(); err != nil { 264 | return err 265 | } 266 | if !it.IsDir() && filepath.Base(it.Dir()) == "bin" && it.Name() == java { 267 | javaPath = filepath.Join(it.Dir(), it.Name()) 268 | break 269 | } 270 | } 271 | if javaPath != "" { 272 | log.Debugf("Found %s", javaPath) 273 | tmp := dir + "~" 274 | javaPath = strings.Replace(javaPath, dir, tmp, 1) 275 | log.Debugf("Moving %s to %s", dir, tmp) 276 | if err := os.Rename(dir, tmp); err != nil { 277 | return err 278 | } 279 | defer func() { 280 | log.Debugf("Removing %s", tmp) 281 | os.RemoveAll(tmp) 282 | }() 283 | homeDir := filepath.Dir(filepath.Dir(javaPath)) 284 | var src, dst string 285 | if goos == "darwin" { 286 | if filepath.Base(homeDir) == "Home" { 287 | src = filepath.Dir(homeDir) 288 | dst = filepath.Join(dir, "Contents") 289 | } else { 290 | src = homeDir 291 | dst = filepath.Join(dir, "Contents", "Home") 292 | } 293 | } else { 294 | src = homeDir 295 | dst = dir 296 | } 297 | log.Debugf("Moving %s to %s", src, dst) 298 | if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 299 | return err 300 | } 301 | if err = os.Rename(src, dst); err != nil { 302 | return err 303 | } 304 | } 305 | return assertJavaDistribution(dir, goos) 306 | } 307 | return nil 308 | } 309 | 310 | func expectedJavaPath(dir string, goos string) string { 311 | var osSpecificSubDir = "" 312 | if goos == "darwin" { 313 | osSpecificSubDir = filepath.Join("Contents", "Home") 314 | } 315 | java := "java" 316 | if goos == "windows" { 317 | java = "java.exe" 318 | } 319 | return filepath.Join(dir, osSpecificSubDir, "bin", java) 320 | } 321 | 322 | func assertJavaDistribution(dir string, goos string) error { 323 | var path = expectedJavaPath(dir, goos) 324 | var err error 325 | if _, err = os.Stat(path); os.IsNotExist(err) { 326 | err = errors.New(path + " wasn't found. " + 327 | "If you believe this is an error - please create a ticket at https://github.com/Jabba-Team/jabba/issues " + 328 | "(specify OS and command that was used)") 329 | } 330 | return err 331 | } 332 | 333 | func installFromDmg(src string, dst string) error { 334 | tmp, err := ioutil.TempDir("", "jabba-i-") 335 | if err != nil { 336 | return err 337 | } 338 | defer os.RemoveAll(tmp) 339 | srcName := filepath.Base(src) 340 | pkgdir := tmp + "/" + srcName + "-pkg" 341 | mountpoint := tmp + "/" + srcName 342 | log.Info("Mounting " + src) 343 | err = sh(fmt.Sprintf(`hdiutil mount -mountpoint "%s" "%s"`, mountpoint, src)) 344 | if err != nil { 345 | return err 346 | } 347 | defer func() { 348 | log.Info("Unmounting " + mountpoint) 349 | sh(fmt.Sprintf(`hdiutil unmount "%s"`, mountpoint)) 350 | }() 351 | log.Info("Extracting " + mountpoint + "/*.pkg") 352 | err = sh(fmt.Sprintf(`pkgutil --expand "%s"/*.pkg "%s"`, mountpoint, pkgdir)) 353 | if err == nil { 354 | pathToPayload := "" 355 | // fixme: find a better way 356 | var payloadSize int64 357 | for it := fileiter.New(pkgdir, fileiter.BreadthFirst()); it.Next(); { 358 | if !it.IsDir() && it.Name() == "Payload" { 359 | path := filepath.Join(it.Dir(), it.Name()) 360 | stat, err := os.Stat(path) 361 | if err != nil { 362 | return err 363 | } 364 | if payloadSize < stat.Size() { // pick largest (e.g. among javaappletplugin.pkg & jdk180191.pkg) 365 | pathToPayload = path 366 | payloadSize = stat.Size() 367 | } 368 | } 369 | } 370 | if pathToPayload != "" { 371 | log.Info("Extracting " + pathToPayload) 372 | err = sh(fmt.Sprintf(`mkdir -p "%s" && cd "%s" && gzip -dc "%s" | cpio -i`, dst, dst, pathToPayload)) 373 | } 374 | } 375 | return err 376 | } 377 | 378 | func installOnLinux(file string, fileType string, dst string) (err error) { 379 | switch fileType { 380 | case "bin": 381 | err = installFromBin(file, dst) 382 | case "ia": 383 | err = installFromIa(file, dst) 384 | case "tgz": 385 | err = installFromTgz(file, dst) 386 | case "tgx": 387 | err = installFromTgx(file, dst) 388 | case "zip": 389 | err = installFromZip(file, dst) 390 | default: 391 | return errors.New(fileType + " is not supported") 392 | } 393 | if err == nil { 394 | err = normalizePathToBinJava(dst, runtime.GOOS) 395 | } 396 | if err != nil { 397 | os.RemoveAll(dst) 398 | } 399 | return 400 | } 401 | 402 | func installOnWindows(file string, fileType string, dst string) (err error) { 403 | switch fileType { 404 | case "exe": 405 | err = installFromExe(file, dst) 406 | case "tgz": 407 | err = installFromTgz(file, dst) 408 | case "tgx": 409 | err = installFromTgx(file, dst) 410 | case "zip": 411 | err = installFromZip(file, dst) 412 | default: 413 | return errors.New(fileType + " is not supported") 414 | } 415 | if err == nil { 416 | err = normalizePathToBinJava(dst, runtime.GOOS) 417 | } 418 | if err != nil { 419 | os.RemoveAll(dst) 420 | } 421 | return 422 | } 423 | 424 | func installFromBin(src string, dst string) (err error) { 425 | tmp, err := ioutil.TempDir("", "jabba-i-") 426 | if err != nil { 427 | return 428 | } 429 | defer os.RemoveAll(tmp) 430 | err = sh("cp " + src + " " + tmp) 431 | if err == nil { 432 | log.Info("Extracting " + filepath.Join(tmp, filepath.Base(src)) + " to " + dst) 433 | err = sh("cd " + tmp + " && echo | sh " + filepath.Base(src) + " && mv jdk*/ " + dst) 434 | } 435 | return 436 | } 437 | 438 | func installFromIa(src string, dst string) error { 439 | tmp, err := ioutil.TempDir("", "jabba-i-") 440 | if err != nil { 441 | return err 442 | } 443 | defer os.RemoveAll(tmp) 444 | err = sh("printf 'LICENSE_ACCEPTED=TRUE\\nUSER_INSTALL_DIR=" + dst + "' > " + 445 | filepath.Join(tmp, "installer.properties")) 446 | if err == nil { 447 | log.Info("Extracting " + src + " to " + dst) 448 | err = sh("echo | sh " + src + " -i silent -f " + filepath.Join(tmp, "installer.properties")) 449 | } 450 | return err 451 | } 452 | 453 | func installFromExe(src string, dst string) error { 454 | log.Info("Unpacking " + src + " to " + dst) 455 | // using ShellExecute instead of exec.Command so user could decide whether to trust the installer when UAC is active 456 | return w32.ShellExecuteAndWait(w32.HWND(0), "open", src, "/s INSTALLDIR=\""+dst+ 457 | "\" STATIC=1 AUTO_UPDATE=0 WEB_JAVA=0 WEB_ANALYTICS=0 REBOOT=0", "", 3) 458 | } 459 | 460 | func installFromTgz(src string, dst string) error { 461 | log.Info("Extracting " + src + " to " + dst) 462 | return untgz(src, dst, true) 463 | } 464 | 465 | func untgz(src string, dst string, strip bool) error { 466 | gzFile, err := os.Open(src) 467 | if err != nil { 468 | return err 469 | } 470 | defer gzFile.Close() 471 | var prefixToStrip string 472 | if strip { 473 | gzr, err := gzip.NewReader(gzFile) 474 | if err != nil { 475 | return err 476 | } 477 | defer gzr.Close() 478 | r := tar.NewReader(gzr) 479 | var prefix []string 480 | for { 481 | header, err := r.Next() 482 | if err == io.EOF { 483 | break 484 | } 485 | if err != nil { 486 | return err 487 | } 488 | var dir string 489 | if header.Typeflag != tar.TypeDir { 490 | dir = filepath.Dir(header.Name) 491 | } else { 492 | continue 493 | } 494 | if prefix != nil { 495 | dirSplit := strings.Split(dir, string(filepath.Separator)) 496 | i, e, dse := 0, len(prefix), len(dirSplit) 497 | if dse < e { 498 | e = dse 499 | } 500 | for i < e { 501 | if prefix[i] != dirSplit[i] { 502 | prefix = prefix[0:i] 503 | break 504 | } 505 | i++ 506 | } 507 | } else { 508 | prefix = strings.Split(dir, string(filepath.Separator)) 509 | } 510 | } 511 | prefixToStrip = strings.Join(prefix, string(filepath.Separator)) 512 | } 513 | gzFile.Seek(0, 0) 514 | gzr, err := gzip.NewReader(gzFile) 515 | if err != nil { 516 | return err 517 | } 518 | defer gzr.Close() 519 | r := tar.NewReader(gzr) 520 | dirCache := make(map[string]bool) // todo: radix tree would perform better here 521 | if err := os.MkdirAll(dst, 0755); err != nil { 522 | return err 523 | } 524 | for { 525 | header, err := r.Next() 526 | if err == io.EOF { 527 | break 528 | } 529 | if err != nil { 530 | return err 531 | } 532 | var dir string 533 | if header.Typeflag != tar.TypeDir { 534 | dir = filepath.Dir(header.Name) 535 | } else { 536 | dir = filepath.Clean(header.Name) 537 | if !strings.HasPrefix(dir, prefixToStrip) { 538 | continue 539 | } 540 | } 541 | dir = strings.TrimPrefix(dir, prefixToStrip) 542 | if dir != "" && dir != "." { 543 | cached := dirCache[dir] 544 | if !cached { 545 | if err := os.MkdirAll(filepath.Join(dst, dir), 0755); err != nil { 546 | return err 547 | } 548 | dirCache[dir] = true 549 | } 550 | } 551 | target := filepath.Join(dst, dir, filepath.Base(header.Name)) 552 | switch header.Typeflag { 553 | case tar.TypeReg: 554 | d, err := os.OpenFile(target, 555 | os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode|0600)&0777) 556 | if err != nil { 557 | return err 558 | } 559 | _, err = io.Copy(d, r) 560 | d.Close() 561 | if err != nil { 562 | return err 563 | } 564 | case tar.TypeSymlink: 565 | if err = os.Symlink(header.Linkname, target); err != nil { 566 | return err 567 | } 568 | } 569 | } 570 | return nil 571 | } 572 | 573 | func installFromTgx(src string, dst string) error { 574 | log.Info("Extracting " + src + " to " + dst) 575 | return untgx(src, dst, true) 576 | } 577 | 578 | func untgx(src string, dst string, strip bool) error { 579 | xzFile, err := os.Open(src) 580 | if err != nil { 581 | return err 582 | } 583 | defer xzFile.Close() 584 | var prefixToStrip string 585 | if strip { 586 | xzr, err := xz.NewReader(xzFile, 0) 587 | if err != nil { 588 | return err 589 | } 590 | r := tar.NewReader(xzr) 591 | var prefix []string 592 | for { 593 | header, err := r.Next() 594 | if err == io.EOF { 595 | break 596 | } 597 | if err != nil { 598 | return err 599 | } 600 | var dir string 601 | if header.Typeflag != tar.TypeDir { 602 | dir = filepath.Dir(header.Name) 603 | } else { 604 | continue 605 | } 606 | if prefix != nil { 607 | dirSplit := strings.Split(dir, string(filepath.Separator)) 608 | i, e, dse := 0, len(prefix), len(dirSplit) 609 | if dse < e { 610 | e = dse 611 | } 612 | for i < e { 613 | if prefix[i] != dirSplit[i] { 614 | prefix = prefix[0:i] 615 | break 616 | } 617 | i++ 618 | } 619 | } else { 620 | prefix = strings.Split(dir, string(filepath.Separator)) 621 | } 622 | } 623 | prefixToStrip = strings.Join(prefix, string(filepath.Separator)) 624 | } 625 | xzFile.Seek(0, 0) 626 | xzr, err := xz.NewReader(xzFile, 0) 627 | if err != nil { 628 | return err 629 | } 630 | r := tar.NewReader(xzr) 631 | dirCache := make(map[string]bool) // todo: radix tree would perform better here 632 | if err := os.MkdirAll(dst, 0755); err != nil { 633 | return err 634 | } 635 | for { 636 | header, err := r.Next() 637 | if err == io.EOF { 638 | break 639 | } 640 | if err != nil { 641 | return err 642 | } 643 | var dir string 644 | if header.Typeflag != tar.TypeDir { 645 | dir = filepath.Dir(header.Name) 646 | } else { 647 | dir = filepath.Clean(header.Name) 648 | if !strings.HasPrefix(dir, prefixToStrip) { 649 | continue 650 | } 651 | } 652 | dir = strings.TrimPrefix(dir, prefixToStrip) 653 | if dir != "" && dir != "." { 654 | cached := dirCache[dir] 655 | if !cached { 656 | if err := os.MkdirAll(filepath.Join(dst, dir), 0755); err != nil { 657 | return err 658 | } 659 | dirCache[dir] = true 660 | } 661 | } 662 | target := filepath.Join(dst, dir, filepath.Base(header.Name)) 663 | switch header.Typeflag { 664 | case tar.TypeReg: 665 | d, err := os.OpenFile(target, 666 | os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode|0600)&0777) 667 | if err != nil { 668 | return err 669 | } 670 | _, err = io.Copy(d, r) 671 | d.Close() 672 | if err != nil { 673 | return err 674 | } 675 | case tar.TypeSymlink: 676 | if err = os.Symlink(header.Linkname, target); err != nil { 677 | return err 678 | } 679 | } 680 | } 681 | return nil 682 | } 683 | 684 | func installFromZip(src string, dst string) error { 685 | log.Info("Extracting " + src + " to " + dst) 686 | return unzip(src, dst, true) 687 | } 688 | 689 | func unzip(src string, dst string, strip bool) error { 690 | r, err := zip.OpenReader(src) 691 | if err != nil { 692 | return err 693 | } 694 | defer r.Close() 695 | var prefixToStrip string 696 | if strip { 697 | var prefix []string 698 | for _, f := range r.File { 699 | var dir string 700 | if !f.Mode().IsDir() { 701 | dir = filepath.Dir(f.Name) 702 | } else { 703 | continue 704 | } 705 | if prefix != nil { 706 | dirSplit := strings.Split(dir, string(filepath.Separator)) 707 | i, e, dse := 0, len(prefix), len(dirSplit) 708 | if dse < e { 709 | e = dse 710 | } 711 | for i < e { 712 | if prefix[i] != dirSplit[i] { 713 | prefix = prefix[0:i] 714 | break 715 | } 716 | i++ 717 | } 718 | } else { 719 | prefix = strings.Split(dir, string(filepath.Separator)) 720 | } 721 | } 722 | prefixToStrip = strings.Join(prefix, string(filepath.Separator)) 723 | } 724 | dirCache := make(map[string]bool) // todo: radix tree would perform better here 725 | if err := os.MkdirAll(dst, 0755); err != nil { 726 | return err 727 | } 728 | for _, f := range r.File { 729 | var dir string 730 | if !f.Mode().IsDir() { 731 | dir = filepath.Dir(f.Name) 732 | } else { 733 | dir = filepath.Clean(f.Name) 734 | if !strings.HasPrefix(dir, prefixToStrip) { 735 | continue 736 | } 737 | } 738 | dir = strings.TrimPrefix(dir, prefixToStrip) 739 | if dir != "" && dir != "." { 740 | cached := dirCache[dir] 741 | if !cached { 742 | if err := os.MkdirAll(filepath.Join(dst, dir), 0755); err != nil { 743 | return err 744 | } 745 | dirCache[dir] = true 746 | } 747 | } 748 | if !f.Mode().IsDir() { 749 | name := filepath.Base(f.Name) 750 | fr, err := f.Open() 751 | if err != nil { 752 | return err 753 | } 754 | d, err := os.OpenFile(filepath.Join(dst, dir, name), 755 | os.O_WRONLY|os.O_CREATE|os.O_TRUNC, (f.Mode()|0600)&0777) 756 | if err != nil { 757 | return err 758 | } 759 | _, err = io.Copy(d, fr) 760 | d.Close() 761 | if err != nil { 762 | return err 763 | } 764 | } 765 | } 766 | return nil 767 | } 768 | 769 | func sh(cmd string) error { 770 | var execArg []string 771 | if runtime.GOOS == "windows" { 772 | execArg = []string{"cmd", "/C"} 773 | } else { 774 | execArg = []string{"sh", "-c"} 775 | } 776 | out, err := exec.Command(execArg[0], execArg[1], cmd).CombinedOutput() 777 | if err != nil { 778 | log.Error(string(out)) 779 | return errors.New("'" + cmd + "' failed: " + err.Error()) 780 | } 781 | return nil 782 | } 783 | --------------------------------------------------------------------------------