Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: lots of refactoring for release #24

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ linters:
- dogsled
- dupl
- errcheck
- exportloopref
- copyloopvar
- funlen
- goconst
- gocritic
Expand Down
85 changes: 66 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ might change, but I like `dist` for the binary name.

## Overview

Without a doubt, [homebrew](https://brew.sh) has had a major impact on the macOS ecosystem. It has made it easy to
install software and keep it up to date. It has been around for 15 years and while it has evolved over time, its core
technology hasn't changed, and 15 year is an eternity in the tech world. I love homebrew, but I think there's room for
another tool.
Without a doubt, [homebrew](https://brew.sh) has had a major impact on the macOS and even the linux ecosystem. It has made it easy
to install software and keep it up to date. It has been around for 15+ years and while it has evolved over time, its core
technology hasn't changed, and 15+ years is an eternity in the tech world.

I love homebrew, but I think there's room for another tool.

Distillery is a tool that is designed to make it easy to install binaries on your system from multiple sources. It is
designed to be simple and easy to use. It is **NOT** designed to be a package manager or handle complex dependencies,
that's where homebrew shines.

The goal of this project is to leverage the collective power of all the developers out there that are using tools like
[goreleaser](https://goreleaser.com/) and [cargo-dist](https://github.com/axodotdev/cargo-dist) and many others to
pre-compile their software and put their binaries up on GitHub or GitLab and install the binaries.
[goreleaser](https://goreleaser.com/) and [cargo-dist](https://github.com/axodotdev/cargo-dist) and many others to pre-compile their software and put their binaries up on
GitHub or GitLab and install the binaries.

## Features

- Make it simple to install binaries on your system from multiple sources
- Do not rely on a centralized repository of metadata like package managers
- Support binary verifications and signatures if they exist
- Support multiple platforms and architectures
- Support private repositories (this was a feature removed from homebrew)

## Needed Before 1.0

Expand All @@ -25,18 +38,46 @@ pre-compile their software and put their binaries up on GitHub or GitLab and ins

## Install

## MacOS/Linux

1. Set your path `export PATH=$HOME/.distillery/bin:$PATH`
2. Download the latest release from the [releases page](https://github.com/ekristen/distillery/releases)
3. Extract and Run `./dist install ekristen/distillery`
4. Delete `./dist` and the .tar.gz, now use `dist` normally
5. Run `dist install owner/repo` to install a binary from GitHub Repository

## Windows

1. [Set Your Path](#set-your-path)
2. Download the latest release from the [releases page](https://github.com/ekristen/distillery/releases)
3. Extract and Run `.\dist.exe install ekristen/distillery`
4. Delete `.\dist.exe` and the .zip, now use `dist` normally
5. Run `dist install owner/repo` to install a binary from GitHub Repository

### Set Your Path

#### For Current Session

```powershell
$env:Path = "C:\Users\<username>\.distillery\bin;" + $env:Path
```

#### For Current User

```powershell
[Environment]::SetEnvironmentVariable("Path", "C:\Users\<username>\.distillery\bin;" + $env:Path, [EnvironmentVariableTarget]::User)
```

### For System

```powershell
[Environment]::SetEnvironmentVariable("Path", "C:\Users\<username>\.distillery\bin;" + $env:Path, [EnvironmentVariableTarget]::Machine)
```

### Uninstall

1. Simply remove `$HOME/.distillery/bin` from your path
2. Remove `$HOME/.distillery` directory
3. Optionally remove cache directory (varies by OS, viewable by the `info` command)
4. Done
1. Run `dist info`
2. Remove the directories listed under the cleanup section

### Examples

Expand All @@ -62,25 +103,24 @@ dist install gitlab/gitlab-org/gitlab-runner

Often times installing from GitHub or GitLab is sufficient, but if you are on a MacOS system and Homebrew
has the binary you want, you can install it using the `homebrew` scope. I would generally still recommend just
installing from GitHub.
installing from GitHub or GitLab directly.

```console
dist install homebrew/opentofu
```

## Goals

- Make it simple to install binaries on your system from multiple sources
- Do not rely on a centralized repository of metadata like package managers
- Support binary verifications and signatures if they exist
- Support multiple platforms and architectures

## Supported Platforms

- GitHub
- GitLab
- Homebrew (binaries only, if anything has a dependency, it will not work at this time)
- Hashicorp
- Hashicorp (special handling for their releases, pointing to github repos will automatically pass through)

### Authentication

Distillery supports authentication for GitHub and GitLab. There are CLI options to pass in a token, but the preferred
method is to set the `DISTILLERY_GITHUB_TOKEN` or `DISTILLERY_GITLAB_TOKEN` environment variables using a tool like
(direnv)[https://direnv.net/].

## Behaviors

Expand All @@ -101,3 +141,10 @@ dist install homebrew/opentofu
- MacOS `$HOME/Library/Caches/distillery`
- Linux `$HOME/.cache/distillery`
- Windows `$HOME/AppData/Local/distillery`

### Caching

At the moment there are two discrete caches. One for HTTP requests and one for downloads. The HTTP cache is used to
store the ETag and Last-Modified headers from the server to determine if the file has changed. The download cache is
used to store the downloaded file. The download cache is not used to determine if the file has changed, that is done
by the HTTP cache.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/nicksnyder/go-i18n v1.10.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/nicksnyder/go-i18n v1.10.3 h1:0U60fnLBNrLBVt8vb8Q67yKNs+gykbQuLsIkiesJL+w=
github.com/nicksnyder/go-i18n v1.10.3/go.mod h1:hvLG5HTlZ4UfSuVLSRuX7JRUomIaoKQM19hm6f+no7o=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
Expand Down
73 changes: 63 additions & 10 deletions pkg/asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"compress/bzip2"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"runtime"
Expand All @@ -22,6 +24,7 @@ import (
"github.com/xi2/xz"

"github.com/ekristen/distillery/pkg/common"
"github.com/ekristen/distillery/pkg/osconfig"
)

var (
Expand Down Expand Up @@ -55,6 +58,10 @@ var (
// Type is the type of asset
type Type int

func (t Type) String() string {
return [...]string{"unknown", "archive", "binary", "installer", "checksum", "signature", "key", "sbom", "data"}[t]
}

const (
Unknown Type = iota
Archive
Expand Down Expand Up @@ -111,6 +118,7 @@ type Asset struct {
func (a *Asset) ID() string {
return "not-implemented"
}
func (a *Asset) Path() string { return "not-implemented" }

func (a *Asset) GetName() string {
return a.Name
Expand Down Expand Up @@ -218,11 +226,8 @@ func (a *Asset) copyFile(srcFile, dstFile string) error {
return nil
}

// Install installs the asset
// TODO(ek): simplify this function
func (a *Asset) Install(id, binDir string) error { //nolint:funlen
found := false

// determineInstallable determines if the file is installable or not based on the mimetype
func (a *Asset) determineInstallable() {
logrus.Tracef("files to process: %d", len(a.Files))
for _, file := range a.Files {
// Actual path to the downloaded/extracted file
Expand All @@ -231,7 +236,7 @@ func (a *Asset) Install(id, binDir string) error { //nolint:funlen
logrus.Debug("checking file for installable: ", file.Name)
m, err := mimetype.DetectFile(fullPath)
if err != nil {
return err
logrus.WithError(err).Warn("unable to determine mimetype")
}

logrus.Debug("found mimetype: ", m.String())
Expand All @@ -246,6 +251,18 @@ func (a *Asset) Install(id, binDir string) error { //nolint:funlen
file.Installable = true
}
}
}

// Install installs the asset
// TODO(ek): simplify this function
func (a *Asset) Install(id, binDir, optDir string) error {
found := false

if err := os.MkdirAll(optDir, 0755); err != nil {
return err
}

a.determineInstallable()

for _, file := range a.Files {
if !file.Installable {
Expand All @@ -272,14 +289,23 @@ func (a *Asset) Install(id, binDir string) error { //nolint:funlen
dstFilename = strings.ReplaceAll(dstFilename, fmt.Sprintf("v%s", a.Version), "")
dstFilename = strings.ReplaceAll(dstFilename, a.Version, "")

if a.OS == osconfig.Windows || strings.HasSuffix(dstFilename, ".exe") {
dstFilename = strings.TrimSuffix(dstFilename, ".exe")
}

dstFilename = strings.TrimSpace(dstFilename)
dstFilename = strings.TrimRight(dstFilename, "-")
dstFilename = strings.TrimRight(dstFilename, "_")

if a.OS == osconfig.Windows {
dstFilename = fmt.Sprintf("%s.exe", dstFilename)
}

logrus.Tracef("post-dstFilename: %s", dstFilename)

destBinaryName := fmt.Sprintf("%s-%s", id, dstFilename)
destBinFilename := filepath.Join(binDir, destBinaryName)
destBinaryName := fmt.Sprintf("%s-%s", dstFilename, id)
// Note: copy to the opt dir for organization
destBinFilename := filepath.Join(optDir, destBinaryName)
defaultBinFilename := filepath.Join(binDir, dstFilename)

versionedBinFilename := fmt.Sprintf("%s@%s", defaultBinFilename, strings.TrimLeft(a.Version, "v"))
Expand Down Expand Up @@ -465,7 +491,11 @@ func (a *Asset) processTar(in io.Reader) (io.Reader, error) {

// TODO(ek): do we need to somehow check the location in the tar file?

target := filepath.Join(a.TempDir, header.Name) //nolint:gosec
target, err := sanitizeArchivePath(a.TempDir, header.Name)
if err != nil {
return nil, err
}

logrus.Tracef("tar > target %s", target)

switch header.Typeflag {
Expand All @@ -487,7 +517,12 @@ func (a *Asset) processTar(in io.Reader) (io.Reader, error) {
logrus.Tracef("tar > create directory %s", baseDir)
}

f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
convertedMode, err := int64ToUint32(header.Mode)
if err != nil {
return nil, err
}

f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(convertedMode))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -535,3 +570,21 @@ func (a *Asset) processBz2(in io.Reader) (io.Reader, error) {
br := bzip2.NewReader(in)
return br, nil
}

func int64ToUint32(value int64) (uint32, error) {
if value < 0 || value > math.MaxUint32 {
return 0, errors.New("value out of range for uint32")
}
return uint32(value), nil
}

// sanitizeArchivePath ensures that the path is not tainted
// thanks https://github.com/securego/gosec/issues/324#issuecomment-935927967
func sanitizeArchivePath(d, t string) (v string, err error) {
v = filepath.Join(d, t)
if strings.HasPrefix(v, filepath.Clean(d)) {
return v, nil
}

return "", fmt.Errorf("%s: %s", "content filepath is tainted", t)
}
6 changes: 5 additions & 1 deletion pkg/asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ func TestAssetInstall(t *testing.T) {
assert.NoError(t, err)
defer os.RemoveAll(binDir)

optDir, err := os.MkdirTemp("", "opt")
assert.NoError(t, err)
defer os.RemoveAll(optDir)

version := c.version
if version == "" {
version = "1.0.0"
Expand All @@ -483,7 +487,7 @@ func TestAssetInstall(t *testing.T) {
err = asset.Extract()
assert.NoError(t, err)

err = asset.Install("test-id", binDir)
err = asset.Install("test-id", binDir, optDir)
assert.NoError(t, err)

for _, fileName := range c.expectedFiles {
Expand Down
3 changes: 2 additions & 1 deletion pkg/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ type IAsset interface {
GetFilePath() string
Download(context.Context) error
Extract() error
Install(string, string) error
Install(string, string, string) error
Cleanup() error
ID() string
Path() string
}
10 changes: 9 additions & 1 deletion pkg/checksum/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"hash"
"io"
"os"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
)

func ComputeFileHash(filePath string, hashFunc func() hash.Hash) (string, error) {
Expand All @@ -26,6 +29,8 @@ func ComputeFileHash(filePath string, hashFunc func() hash.Hash) (string, error)

// CompareHashWithChecksumFile compares the computed hash of a file with the hashes in a checksum file.
func CompareHashWithChecksumFile(fileName, filePath, checksumFilePath string, hashFunc func() hash.Hash) (bool, error) {
log := logrus.WithField("handler", "compare-hash-with-checksum-file")

// Compute the hash of the file
computedHash, err := ComputeFileHash(filePath, hashFunc)
if err != nil {
Expand All @@ -45,14 +50,17 @@ func CompareHashWithChecksumFile(fileName, filePath, checksumFilePath string, ha
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) < 2 {
log.Trace("skipping line: ", line)
continue
}
fileHash := parts[0]
filename := parts[1]
log.Trace("fileHash: ", fileHash)
log.Trace("filename: ", filename)
// Rust does *(binary) for the binary name
filename = strings.TrimPrefix(filename, "*")

if filename == fileName && fileHash == computedHash {
if (filename == fileName || filepath.Base(filename) == fileName) && fileHash == computedHash {
return true, nil
}
}
Expand Down
Loading