├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cache └── cache.go ├── cmd ├── install_cmd.go ├── root_cmd.go └── version_cmd.go ├── go.mod ├── go.sum ├── main.go ├── statik └── statik.go ├── templates └── templates.toml └── tools └── tools.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @netlify/core-pod-build 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 21 | 22 | **- Do you want to request a *feature* or report a *bug*?** 23 | 24 | **- What is the current behavior?** 25 | 26 | **- If the current behavior is a bug, please provide the steps to reproduce.** 27 | 28 | **- What is the expected behavior?** 29 | 30 | **- Please mention your Go version, and operating system version.** 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | **- Summary** 15 | 16 | 20 | 21 | **- Test plan** 22 | 23 | 27 | 28 | **- Description for the changelog** 29 | 30 | 34 | 35 | **- A picture of a cute animal (not mandatory but encouraged)** 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | binrc 2 | vendor 3 | builds 4 | releases 5 | _vendor* 6 | 7 | .gobincache 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | sudo: false 4 | 5 | go: 6 | - 1.12 7 | env: 8 | global: 9 | - GO111MODULE=on 10 | - GOPROXY=https://microsoftgoproxy.azurewebsites.net/ 11 | 12 | install: make deps 13 | script: make all 14 | notifications: 15 | email: false 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at david@netlify.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are always welcome, no matter how large or small. Before contributing, 4 | please read the [code of conduct](CODE_OF_CONDUCT.md). 5 | 6 | ## Setup 7 | 8 | > Install Go and Dep https://github.com/golang/dep 9 | 10 | ```sh 11 | $ git clone https://github.com/netlify/binrc 12 | $ cd binrc 13 | $ make deps 14 | ``` 15 | 16 | ## Building 17 | 18 | ```sh 19 | $ make build 20 | ``` 21 | 22 | ## Testing 23 | 24 | ```sh 25 | $ make test 26 | ``` 27 | 28 | ## Pull Requests 29 | 30 | We actively welcome your pull requests. 31 | 32 | 1. Fork the repo and create your branch from `master`. 33 | 2. If you've added code that should be tested, add tests. 34 | 3. If you've changed APIs, update the documentation. 35 | 4. Ensure the test suite passes. 36 | 5. Make sure your code lints. 37 | 38 | ## License 39 | 40 | By contributing to Binrc, you agree that your contributions will be licensed 41 | under its [MIT license](LICENSE). 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Netlify 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build deps image lint release generate test 2 | 3 | TAG = $(shell git describe --tags --abbrev=0 | cut -c 2-) 4 | 5 | help: ## Show this help. 6 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 7 | 8 | 9 | ## $(1) binary-name 10 | ## $(2) entry file, path to main.go 11 | ## $(3) goos 12 | ## $(4) goarch 13 | ## $(5) extension (like ".exe") 14 | define build_binary 15 | @echo "Building $(1) for $(3)/$(4) $(2)" 16 | @GOOS=darwin GOARCH=$(arch) go build \ 17 | -ldflags " \ 18 | -X github.com/netlify/binrc/info.sha=`git rev-parse HEAD` \ 19 | -X github.com/netlify/binrc/info.distro=$(3) \ 20 | -X github.com/netlify/binrc/info.arch=$(4) \ 21 | -X github.com/netlify/binrc/cmd.Version=`git rev-parse HEAD`" \ 22 | -o builds/$(3)-$(4)-${TAG}/$(1)$(5) $(2) 23 | 24 | @echo "Built: builds/$(3)-$(4)-${TAG}/$(1)$(5)" 25 | endef 26 | 27 | ## $(1) binary-name 28 | ## $(2) goos 29 | ## $(3) goarch 30 | ## $(4) artifact ending (like ".zip" or ".tar.gz") 31 | ## $(5) extension (like ".exe") 32 | define package 33 | @cp LICENSE releases/${TAG}/ 34 | @tar -czf releases/${TAG}/$(1)-$(2)-$(3)$(4) -C builds/$(2)-$(3)-${TAG} $(1)$(5) 35 | 36 | @sha256sum releases/${TAG}/$(1)-$(2)-$(3)$(4) >> releases/${TAG}/checksums.txt 37 | endef 38 | 39 | all: deps generate test lint build ## Run the tests and build the binary. 40 | 41 | generate: 42 | @go generate 43 | 44 | deps: ## Install dependencies. 45 | @echo "Installing dependencies" 46 | @go get -u github.com/myitcv/gobin 47 | @go get -u golang.org/x/lint/golint 48 | @go mod verify 49 | @go mod tidy 50 | @go mod download 51 | 52 | build: ## Build the client binary 53 | @rm -rf build 54 | $(call build_binary,binrc_$(TAG),main.go,linux,386) 55 | $(call build_binary,binrc_$(TAG),main.go,linux,amd64) 56 | $(call build_binary,binrc_$(TAG),main.go,linux,arm) 57 | $(call build_binary,binrc_$(TAG),main.go,linux,arm64) 58 | $(call build_binary,binrc_$(TAG),main.go,darwin,amd64) 59 | $(call build_binary,binrc_$(TAG),main.go,darwin,arm64) 60 | 61 | lint: ## Run golint to ensure the code follows Go styleguide. 62 | golint -set_exit_status ./... 63 | 64 | test: lint ## Run tests. 65 | @go test -v ./... 66 | 67 | clean: ## Clean the build artifacts. 68 | @mkdir -p releases/${TAG} 69 | @rm -f releases/${TAG}/* 70 | @rm -rf pkg-build 71 | 72 | package: clean build ## Build a release package for Linux. 73 | $(call package,binrc_$(TAG),linux,386,.tar.gz) 74 | $(call package,binrc_$(TAG),linux,amd64,.tar.gz) 75 | $(call package,binrc_$(TAG),linux,arm,.tar.gz) 76 | $(call package,binrc_$(TAG),linux,arm64,.tar.gz) 77 | $(call package,binrc_$(TAG),darwin,amd64,.tar.gz) 78 | $(call package,binrc_$(TAG),darwin,arm64,.tar.gz) 79 | 80 | release_upload: package 81 | @echo "Uploading release" 82 | 83 | @hub release create \ 84 | -a releases/${TAG}/binrc_${TAG}-darwin-amd64.tar.gz \ 85 | -a releases/${TAG}/binrc_${TAG}-darwin-arm64.tar.gz \ 86 | -a releases/${TAG}/binrc_${TAG}-linux-386.tar.gz \ 87 | -a releases/${TAG}/binrc_${TAG}-linux-amd64.tar.gz \ 88 | -a releases/${TAG}/binrc_${TAG}-linux-arm.tar.gz \ 89 | -a releases/${TAG}/binrc_${TAG}-linux-arm64.tar.gz \ 90 | -a releases/${TAG}/LICENSE \ 91 | -a releases/${TAG}/checksums.txt v${TAG} \ 92 | -m "Release v${TAG}" 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binrc 2 | 3 | Binrc is a command line application to manage different versions of binaries on GitHub releases. 4 | 5 | Binrc doesn't try to be smart about version schemas and it only search for exact version matches. 6 | 7 | Binrc is released under the [MIT License](LICENSE). 8 | Please make sure you understand its [implications and guarantees](https://writing.kemitchell.com/2016/09/21/MIT-License-Line-by-Line.html). 9 | 10 | ## Installation 11 | 12 | You can download prebuilt binaries from the [releases](https://github.com/netlify/binrc/releases). 13 | 14 | You can also install binrc to install binrc: 15 | 16 | ``` 17 | binrc install netlify/binrc 0.2.0 18 | ``` 19 | 20 | Or you can download the cutting edge version with `go get`: 21 | 22 | ``` 23 | go get github.com/netlify/binrc 24 | ``` 25 | 26 | ## Usage 27 | 28 | There is a main subcommand, `install`. 29 | 30 | ### install subcommand 31 | 32 | The `install` subcommand Installs a new binary. If that version is not already in the host's 33 | cache, Binrc will try to fetch it from GitHub's releases and keep it in its cache. 34 | 35 | ``` 36 | binrc install spf13/hugo 0.19 37 | ``` 38 | 39 | ### Versions as environment variables 40 | 41 | Binrc supports settings binary version numbers as environment variables. It will search for the binary name followed by `_VERSION` 42 | in the environment to configure the right version. 43 | 44 | ``` 45 | HUGO_VERSION=0.19 binrc install spf13/hugo 46 | ``` 47 | 48 | ### Aliases 49 | 50 | Binrc keeps a list of aliases to make installing binaries more easy. If a project name is not in `OWNER/NAME` for, Binrc will 51 | check the list of aliases to try to resolve the project. 52 | 53 | This is the current known list: 54 | 55 | - hugo: spf13/hugo 56 | - gutenberg: keats/gutenberg 57 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/BurntSushi/toml" 17 | version "github.com/hashicorp/go-version" 18 | _ "github.com/netlify/binrc/statik" // this is required 19 | "github.com/pkg/errors" 20 | "github.com/rakyll/statik/fs" 21 | ) 22 | 23 | const ( 24 | // DefaultStorePath is the name 25 | // of the default directory where 26 | // binaries and bundles are stored. 27 | DefaultStorePath = ".binrc" 28 | 29 | tarballTemplate = "https://github.com/%s/releases/download/%s/%s" 30 | ) 31 | 32 | // aliases is a map of known project aliases 33 | // to make finding project more easy. 34 | var aliases = map[string]string{ 35 | "hugo": "gohugoio/hugo", 36 | "gutenberg": "keats/gutenberg", 37 | "zola": "getzola/zola", 38 | } 39 | 40 | type template struct { 41 | Range string 42 | Tarball string 43 | Bin string 44 | } 45 | 46 | type templates map[string][]template 47 | 48 | var ( 49 | defaultTemplate = &template{ 50 | Tarball: "%s_v%s_Linux-64bit.tar.gz", 51 | Bin: "%s_%s_linux_amd64/%s_%s_linux_amd64", 52 | } 53 | ) 54 | 55 | // Cache stores and retrieves projects 56 | // from the system's cache and GitHub releases 57 | type Cache struct { 58 | templates templates 59 | storePath string 60 | } 61 | 62 | // Project represents a project managed with Binrc. 63 | // It holds information about its version, cache and binary paths. 64 | type Project struct { 65 | FullName string 66 | Version string 67 | Owner string 68 | Name string 69 | FullPath string 70 | cleanVersion string 71 | template *template 72 | } 73 | 74 | // URL returns the URL to download the tarball from. 75 | func (p *Project) URL() string { 76 | tarballName := fmt.Sprintf(p.template.Tarball, p.Name, p.cleanVersion) 77 | return fmt.Sprintf(tarballTemplate, p.FullName, p.Version, tarballName) 78 | } 79 | 80 | // BinaryName returns the name of the binary to look for 81 | // after the tarball is extracted. 82 | func (p *Project) BinaryName() string { 83 | if strings.Contains(p.template.Bin, "%") { 84 | return fmt.Sprintf(p.template.Bin, p.Name, p.cleanVersion, p.Name, p.cleanVersion) 85 | } 86 | return p.template.Bin 87 | } 88 | 89 | func (c *Cache) newProject(name, versionString string) (*Project, error) { 90 | name = strings.Trim(name, "/") 91 | if !strings.Contains(name, "/") { 92 | p, exist := aliases[name] 93 | if !exist { 94 | return nil, errors.Errorf("invalid project name %s. it should have a format like `netlify/binrc`.", name) 95 | } 96 | 97 | name = p 98 | } 99 | 100 | nwo := strings.SplitN(name, "/", 2) 101 | if versionString == "" { 102 | ev := os.Getenv(fmt.Sprintf("%s_VERSION", strings.ToUpper(nwo[1]))) 103 | if ev == "" { 104 | return nil, errors.Errorf("unknown project version for %s", name) 105 | } 106 | versionString = ev 107 | } 108 | 109 | if !strings.HasPrefix(versionString, "v") { 110 | versionString = "v" + versionString 111 | } 112 | 113 | cleanVersionString := strings.TrimLeft(versionString, "v") 114 | cleanVersion, err := version.NewVersion(cleanVersionString) 115 | if err != nil { 116 | return nil, errors.Wrapf(err, "invalid version %s", versionString) 117 | } 118 | 119 | var t *template 120 | projectTemplates, ok := c.templates[nwo[1]] 121 | if ok { 122 | for _, tmpl := range projectTemplates { 123 | constraint, err := version.NewConstraint(tmpl.Range) 124 | if err != nil { 125 | return nil, errors.Wrapf(err, "invalid constraint %s", tmpl.Range) 126 | } 127 | if constraint.Check(cleanVersion) { 128 | t = &tmpl 129 | break 130 | } 131 | } 132 | 133 | if t == nil { 134 | return nil, errors.Errorf("%s's version %s doesn't match any known constraint, binrc cannot install it", nwo[1], versionString) 135 | } 136 | } else { 137 | t = defaultTemplate 138 | } 139 | 140 | return &Project{ 141 | FullName: name, 142 | Version: versionString, 143 | Owner: nwo[0], 144 | Name: nwo[1], 145 | cleanVersion: cleanVersionString, 146 | template: t, 147 | }, nil 148 | } 149 | 150 | // New initializes the cache with 151 | // a given store path. 152 | func New(storePath string) (*Cache, error) { 153 | t, err := loadVersionTemplates() 154 | if err != nil { 155 | return nil, err 156 | } 157 | return &Cache{ 158 | templates: *t, 159 | storePath: storePath, 160 | }, nil 161 | } 162 | 163 | // GetOrSet fetches a project from the cache. 164 | // If the project is not in the cache, it tries 165 | // to donwload it from GitHub releases. 166 | func (c *Cache) GetOrSet(name, version string) (*Project, error) { 167 | project, err := c.newProject(name, version) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | p := c.binaryPath(project) 173 | _, err = exec.LookPath(p) 174 | if err != nil { 175 | if err := download(project, p); err != nil { 176 | return nil, err 177 | } 178 | 179 | project.FullPath = p 180 | } else { 181 | project.FullPath = p 182 | } 183 | 184 | return project, nil 185 | } 186 | 187 | func (c *Cache) binaryPath(project *Project) string { 188 | p := []string{c.storePath, "binaries", project.FullName, project.Version, project.Name} 189 | return path.Join(p...) 190 | } 191 | 192 | func download(project *Project, destination string) error { 193 | parent := filepath.Dir(destination) 194 | if err := os.MkdirAll(parent, 0755); err != nil { 195 | return errors.Wrapf(err, "error preparing cache directory for %s", project.FullName) 196 | } 197 | 198 | tmp, err := ioutil.TempDir(parent, "binrc-") 199 | if err != nil { 200 | os.RemoveAll(parent) 201 | return errors.Wrapf(err, "error preparing directory to download %s", project.FullName) 202 | } 203 | defer os.RemoveAll(tmp) 204 | 205 | url := project.URL() 206 | res, err := http.Get(url) 207 | if err != nil { 208 | os.RemoveAll(parent) 209 | return errors.Wrapf(err, "error downloading project %s, from %s:", project.FullName, url) 210 | } 211 | defer res.Body.Close() 212 | 213 | if res.StatusCode != 200 { 214 | os.RemoveAll(parent) 215 | return errors.Errorf("error downloading project %s, from %s - binary doesn't seem to exist: %v", project.FullName, url, res.StatusCode) 216 | } 217 | 218 | if err := untar(res.Body, tmp); err != nil { 219 | os.RemoveAll(parent) 220 | return errors.Wrapf(err, "error unpacking file for %s, from %s", project.FullName, url) 221 | } 222 | 223 | bin := project.BinaryName() 224 | fp := path.Join(tmp, bin) 225 | if err := os.Rename(fp, destination); err != nil { 226 | os.RemoveAll(parent) 227 | return errors.Wrapf(err, "error renaming %s to %s", fp, destination) 228 | } 229 | 230 | return nil 231 | } 232 | 233 | func untar(reader io.Reader, destination string) error { 234 | gzr, err := gzip.NewReader(reader) 235 | if err != nil { 236 | return err 237 | } 238 | 239 | tr := tar.NewReader(gzr) 240 | for { 241 | header, err := tr.Next() 242 | if err == io.EOF { 243 | break 244 | } else if err != nil { 245 | return err 246 | } 247 | 248 | path := filepath.Join(destination, header.Name) 249 | info := header.FileInfo() 250 | if info.IsDir() { 251 | if err = os.MkdirAll(path, info.Mode()); err != nil { 252 | return errors.Wrapf(err, "error creating directory tree: %s", path) 253 | } 254 | continue 255 | } 256 | 257 | if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil { 258 | return errors.Wrapf(err, "error creating directory tree: %s", path) 259 | } 260 | 261 | file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) 262 | if err != nil { 263 | return errors.Wrapf(err, "error creating file: %s", path) 264 | } 265 | defer file.Close() 266 | 267 | _, err = io.Copy(file, tr) 268 | if err != nil { 269 | return errors.Wrapf(err, "error creating file: %+v", file) 270 | } 271 | } 272 | 273 | return nil 274 | } 275 | 276 | func loadVersionTemplates() (*templates, error) { 277 | var r io.Reader 278 | if fromEnv := os.Getenv("BINRC_TEMPLATES"); fromEnv != "" { 279 | f, err := os.Open(fromEnv) 280 | if err != nil { 281 | return nil, err 282 | } 283 | r = f 284 | } else { 285 | statikFS, err := fs.New() 286 | f, err := statikFS.Open("/templates.toml") 287 | if err != nil { 288 | return nil, errors.Wrapf(err, "unable to load templates files") 289 | } 290 | r = f 291 | } 292 | 293 | t := &templates{} 294 | _, err := toml.DecodeReader(r, t) 295 | if err != nil { 296 | return nil, err 297 | } 298 | 299 | return t, nil 300 | } 301 | -------------------------------------------------------------------------------- /cmd/install_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strings" 7 | 8 | homedir "github.com/mitchellh/go-homedir" 9 | "github.com/netlify/binrc/cache" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var installCmd = &cobra.Command{ 14 | Use: "install [PROJECT_PATH] [VERSION]", 15 | Long: "This subcommand installs the binary in your system.", 16 | Run: execInstallCmd, 17 | } 18 | 19 | func execInstallCmd(cmd *cobra.Command, args []string) { 20 | sp := cmd.Flag("-cache-store-path").Value.String() 21 | 22 | if !strings.HasPrefix(sp, "/") { 23 | h, err := homedir.Dir() 24 | if err != nil { 25 | displayError(err) 26 | return 27 | } 28 | 29 | sp = path.Clean(path.Join(h, sp)) 30 | } 31 | c, err := cache.New(sp) 32 | if err != nil { 33 | displayError(err) 34 | return 35 | } 36 | 37 | var version string 38 | if len(args) > 1 { 39 | version = args[1] 40 | } 41 | 42 | pc, err := c.GetOrSet(args[0], version) 43 | if err != nil { 44 | displayError(err) 45 | return 46 | } 47 | 48 | fmt.Println(pc.FullPath) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/root_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/netlify/binrc/cache" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // rootCmd is the main Binrc command. 12 | // It runs `use` when no subcommand specified. 13 | var rootCmd = &cobra.Command{ 14 | Use: "binrc", 15 | Long: "A command line application to manage different versions of binaries on GitHub releases", 16 | Run: execInstallCmd, 17 | } 18 | 19 | // RootCmd adds flags and subcommands to the root command. 20 | func RootCmd() *cobra.Command { 21 | rootCmd.PersistentFlags().StringP("-cache-store-path", "c", cache.DefaultStorePath, "The path to binrc's cache directory, $HOME/.binrc by default") 22 | rootCmd.AddCommand(installCmd, versionCmd) 23 | return rootCmd 24 | } 25 | 26 | func displayError(err error) { 27 | if os.Getenv("DEBUG") != "" { 28 | fmt.Printf("%+v\n", err) 29 | } else { 30 | fmt.Println(err) 31 | } 32 | 33 | os.Exit(1) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/version_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // Version is the Git SHA specified when Binrc was build. 10 | var Version string 11 | 12 | var versionCmd = &cobra.Command{ 13 | Use: "version", 14 | Long: "This subcommand displays Binrc's current version.", 15 | Run: showVersion, 16 | } 17 | 18 | func showVersion(cmd *cobra.Command, args []string) { 19 | if Version == "" { 20 | fmt.Println("Unknown version, this binary was probably built by `go get`") 21 | } else { 22 | fmt.Println(Version) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/netlify/binrc 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.0 7 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3 8 | github.com/hashicorp/go-version v0.0.0-20170914154128-fc61389e27c7 9 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 10 | github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 11 | github.com/pkg/errors v0.8.0 12 | github.com/rakyll/statik v0.1.1 13 | github.com/spf13/cobra v0.0.1 14 | github.com/spf13/pflag v1.0.0 // indirect 15 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect 16 | golang.org/x/tools v0.1.12 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 2 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3 h1:I4BOK3PBMjhWfQM2zPJKK7lOBGsrsvOB7kBELP33hiE= 4 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 5 | github.com/hashicorp/go-version v0.0.0-20170914154128-fc61389e27c7 h1:Tijq+ZHupzK8WfomfH2s5dpKkpZd2TcN2i1LDbzWbwk= 6 | github.com/hashicorp/go-version v0.0.0-20170914154128-fc61389e27c7/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 7 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 8 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 9 | github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk= 10 | github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 11 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 12 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/rakyll/statik v0.1.1 h1:fCLHsIMajHqD5RKigbFXpvX3dN7c80Pm12+NCrI3kvg= 14 | github.com/rakyll/statik v0.1.1/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= 15 | github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8= 16 | github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 17 | github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI= 18 | github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 19 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 20 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 21 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 22 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 23 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= 24 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 25 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 26 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 27 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 28 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 29 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 30 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 31 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 32 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 40 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 43 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 44 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 46 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 47 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 48 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 49 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 50 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 51 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:generate rm -rf ./statik 2 | //go:generate gobin -m -run github.com/rakyll/statik -src=./templates 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/netlify/binrc/cmd" 11 | ) 12 | 13 | func main() { 14 | if err := cmd.RootCmd().Execute(); err != nil { 15 | fmt.Fprintf(os.Stderr, "Failed to run command: %v\n", err) 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /statik/statik.go: -------------------------------------------------------------------------------- 1 | // Code generated by statik. DO NOT EDIT. 2 | 3 | package statik 4 | 5 | import ( 6 | "github.com/rakyll/statik/fs" 7 | ) 8 | 9 | func init() { 10 | data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\xaekN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00templates.tomlUT\x05\x00\x01\xbf\xd7\x86\\\xac\x92\xd1N\x830\x14@\xdf\xf9\x8a\x9b&\xbc\xb5\xd7\xceu\xd5\x07\xf1\x0b\xfc\x03cH M%\xe2%\x81v\x92\x99\xfd\xbb\x01\x0c\x99i\xe7\x96(\x8f@\xce\xe9\xe9\xbd\xaf\xc1uP\xc0s\x06\xf0 \xbd!g\xa1\x00\xf6XH\xdcl9\x88\xf1^\x97Z\x89@o\xd4}\x90X&\xee(D\xd3Y5\x0c\x8e\x93\xf6\xd0\xb5&\xbd\xc8;\x94\x7fSM\xe8\xc5\xf2\x15\x00\x00\xff\xffPK\x07\x08L\xa8}0\xec\x00\x00\x00.\x03\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\xaekNL\xa8}0\xec\x00\x00\x00.\x03\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00templates.tomlUT\x05\x00\x01\xbf\xd7\x86\\PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00E\x00\x00\x001\x01\x00\x00\x00\x00" 11 | fs.Register(data) 12 | } 13 | -------------------------------------------------------------------------------- /templates/templates.toml: -------------------------------------------------------------------------------- 1 | hugo = [ 2 | { range = ">=0.13, <0.16", tarball = "%s_%s_linux_amd64.tar.gz", bin = "%s_%s_linux_amd64/%s_%s_linux_amd64" }, 3 | { range = "0.16", tarball = "%s_%s_linux-64bit.tgz", bin = "hugo" }, 4 | { range = ">=0.17, <0.20.3", tarball = "%s_%s_Linux-64bit.tar.gz", bin = "%s_%s_linux_amd64/%s_%s_linux_amd64" }, 5 | { range = "0.20.3", tarball = "%s_v%s_Linux-64bit.tar.gz", bin = "hugo" }, 6 | { range = ">0.20.3, <0.43", tarball = "%s_%s_Linux-64bit.tar.gz", bin = "hugo" }, 7 | { range = ">=0.43", tarball = "%s_extended_%s_Linux-64bit.tar.gz", bin = "hugo" }, 8 | ] 9 | 10 | gutenberg = [ 11 | { range = ">0.0.5, <0.4.3", tarball = "%s-v%s-x86_64-unknown-linux-gnu.tar.gz", bin = "gutenberg" } 12 | ] 13 | 14 | zola = [ 15 | { range = ">=0.5.0", tarball = "%s-v%s-x86_64-unknown-linux-gnu.tar.gz", bin = "zola" } 16 | ] 17 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | // See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 6 | // for more details 7 | 8 | import ( 9 | _ "github.com/rakyll/statik" 10 | _ "github.com/golang/lint/golint" 11 | ) 12 | --------------------------------------------------------------------------------