Skip to content

Commit

Permalink
feat: add lib installer to CLI command
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed Feb 22, 2021
1 parent e226d6d commit bcb3e9a
Show file tree
Hide file tree
Showing 7 changed files with 596 additions and 104 deletions.
13 changes: 8 additions & 5 deletions command/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"log"
"os"

"github.com/pact-foundation/pact-go/install"
"github.com/pact-foundation/pact-go/v3/installer"

"github.com/spf13/cobra"
)
Expand All @@ -18,16 +18,19 @@ var installCmd = &cobra.Command{
setLogLevel(verbose, logLevel)

// Run the installer
i := install.NewInstaller()
var err error
i, err := installer.NewInstaller()
if err != nil {
log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err)
os.Exit(1)
}

if err = i.CheckInstallation(); err != nil {
log.Println("[ERROR] Your Pact CLI installation is out of date, please update to the latest version. Error:", err)
log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err)
os.Exit(1)
}
},
}

func init() {
installCmd.Flags().StringVarP(&path, "path", "p", "/opt/pact", "Location to install the Pact CLI tools")
RootCmd.AddCommand(installCmd)
}
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ module github.com/pact-foundation/pact-go
go 1.12

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07
github.com/golang/protobuf v1.3.2 // indirect
github.com/hashicorp/go-version v1.0.0
github.com/hashicorp/go-getter v1.5.2
github.com/hashicorp/go-version v1.1.0
github.com/hashicorp/logutils v1.0.0
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/spf13/afero v1.4.0
github.com/spf13/cobra v0.0.0-20160604044732-f447048345b6
github.com/spf13/pflag v1.0.3 // indirect
Expand Down
145 changes: 141 additions & 4 deletions go.sum

Large diffs are not rendered by default.

263 changes: 263 additions & 0 deletions v3/installer/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
// Package installer is responsible for finding, acquiring and addressing
// runtime dependencies for this package (e.g. Ruby standalone, Rust bindings etc.)
package installer

import (
"fmt"
"log"
"os"
"path"
"runtime"
"strings"

getter "github.com/hashicorp/go-getter"
goversion "github.com/hashicorp/go-version"

// mockserver "github.com/pact-foundation/pact-go/v3/internal/native/mock_server"
// verifier "github.com/pact-foundation/pact-go/v3/internal/native/verifier"
"github.com/spf13/afero"
)

// Installer manages the underlying Ruby installation
// (eventual) implementation requirements
// 1. Download OS specific artifacts if not pre-installed - DONE
// 1. Check the semver range of pre-installed artifacts - DONE
// 1. Enable global configuration (environment vars, config files, code options e.g. (`PACT_GO_SHARED_LIBRARY_PATH`))
// 1. Allow users to specify where they pre-install their artifacts (e.g. /usr/local/pact/libs)

// Installer is used to check the Pact Go installation is setup correctly, and can automatically install
// packages if required
type Installer struct {
downloader downloader
os string
arch string
fs afero.Fs
libDir string
}

type installerConfig func(*Installer) error

// NewInstaller creates a new initialised Installer
func NewInstaller(opts ...installerConfig) (*Installer, error) {
i := &Installer{downloader: &defaultDownloader{}, fs: afero.NewOsFs()}

for _, opt := range opts {
opt(i)
}

if _, ok := supportedOSes[runtime.GOOS]; !ok {
return nil, fmt.Errorf("%s is not a supported OS", runtime.GOOS)
}
i.os = supportedOSes[runtime.GOOS]

if !strings.Contains(runtime.GOARCH, "64") {
return nil, fmt.Errorf("%s is not a supported architecture, only 64 bit architectures are supported", runtime.GOARCH)
}

i.arch = x86_64
if runtime.GOARCH != "amd64" {
log.Println("[WARN] amd64 architecture not detected, behaviour may be undefined")
}

return i, nil
}

// CheckInstallation checks installation of all of the required libraries
// and downloads if they aren't present
func (i *Installer) CheckInstallation() error {

// Check if files exist
// --> Check versions of existing installed files
if err := i.checkPackageInstall(); err == nil {
return nil
}

// Check if override package path exists
// -> if it does, copy files from existing location
// --> Check versions of these files
// --> copy files to lib dir

// Download dependencies
if err := i.downloadDependencies(); err != nil {
return err
}

if err := i.checkPackageInstall(); err != nil {
return fmt.Errorf("unable to verify downloaded/installed dependencies: %s", err)
}

// --> Check if download is disabled (return error if downloads are disabled)
// --> download files to lib dir

return nil
}

func (i *Installer) getLibDir() string {
if i.libDir == "" {

dir, err := os.Getwd()

if err != nil {
return os.TempDir()
}

i.libDir = path.Join(dir, "libs")
}

return i.libDir
}

// checkPackageInstall discovers any existing packages, and checks installation of a given binary using semver-compatible checks
func (i *Installer) checkPackageInstall() error {
for pkg, info := range packages {

log.Println("[DEBUG] checking version for lib", info.libName, "semver range", info.semverRange)
dst, _ := i.getLibDstForPackage(pkg)

if _, err := i.fs.Stat(dst); err != nil {
log.Println("[DEBUG] package", info.libName, "not found")
return err
}

// if err := checkVersion(info.testCommand(), info.libName, info.semverRange); err != nil {
// return err
// }
}

return nil
}

// getVersionForBinary gets the version of a given Ruby binary
func (i *Installer) getVersionForBinary(binary string) (version string, err error) {
log.Println("[DEBUG] running binary", binary)

return "", nil
}

// TODO: checksums (they don't currently exist)
func (i *Installer) downloadDependencies() error {
for pkg := range packages {
src, err := i.getDownloadURLForPackage(pkg)

if err != nil {
return err

}
dst, err := i.getLibDstForPackage(pkg)

if err != nil {
return err
}

err = i.downloader.download(src, dst)

if err != nil {
return err
}
}

return nil
}

// returns src
func (i *Installer) getDownloadURLForPackage(pkg string) (string, error) {
pkgInfo, ok := packages[pkg]
if !ok {
return "", fmt.Errorf("unable to find package details for package: %s", pkg)
}

return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, pkgInfo.libName, i.os, i.arch, osToExtension[i.os]), nil
}

func (i *Installer) getLibDstForPackage(pkg string) (string, error) {
pkgInfo, ok := packages[pkg]
if !ok {
return "", fmt.Errorf("unable to find package details for package: %s", pkg)
}

return path.Join(i.getLibDir(), pkgInfo.libName) + "." + osToExtension[i.os], nil
}

// download template structure: "https://github.com/pact-foundation/pact-reference/releases/download/PACKAGE-vVERSION/LIBNAME-OS-ARCH.EXTENSION.gz"
var downloadTemplate = "https://github.com/pact-foundation/pact-reference/releases/download/%s-v%s/%s-%s-%s.%s.gz"

var supportedOSes = map[string]string{
"darwin": osx,
windows: windows,
linux: linux,
}

var osToExtension = map[string]string{
windows: "dll",
linux: "so",
osx: "dylib",
}

type packageInfo struct {
packageName string
libName string
version string
semverRange string
testCommand func() string
}

const (
verifierPackage = "pact_verifier_ffi"
mockServerPackage = "libpact_mock_server_ffi"
linux = "linux"
windows = "windows"
osx = "osx"
x86_64 = "x86_64"
)

var packages = map[string]packageInfo{
verifierPackage: {
libName: "libpact_verifier_ffi",
version: "0.0.2",
semverRange: ">= 0.8.3, < 1.0.0",
// testCommand: func() string {
// return (&verifier.Verifier{}).Version()
// },
},
mockServerPackage: {
libName: "libpact_mock_server_ffi",
version: "0.0.14",
semverRange: ">= 0.0.14, < 1.0.0",
// testCommand: func() string {
// return mockserver.Version()
// },
},
}

func checkVersion(lib, version, versionRange string) error {
log.Println("[DEBUG] checking version", version, "of", lib, "against semver constraint", versionRange)

v, err := goversion.NewVersion(version)
if err != nil {
return err
}

constraints, err := goversion.NewConstraint(versionRange)
if err != nil {
return err
}

if constraints.Check(v) {
log.Println("[DEBUG]", v, "satisfies constraints", v, constraints)
return nil
}

return fmt.Errorf("version %s of %s does not match constraint %s", version, lib, versionRange)
}

type downloader interface {
download(src string, dst string) error
}

type defaultDownloader struct{}

func (d *defaultDownloader) download(src string, dst string) error {
log.Println("[DEBUG] downloading library from", src, "to", dst)

return getter.GetFile(dst, src)
}

0 comments on commit bcb3e9a

Please sign in to comment.