github.com/mattdotmatt/gauge@v0.3.2-0.20160421115137-425a4cdccb62/plugin/install/install.go (about)

     1  // Copyright 2015 ThoughtWorks, Inc.
     2  
     3  // This file is part of Gauge.
     4  
     5  // Gauge is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  
    10  // Gauge is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU General Public License for more details.
    14  
    15  // You should have received a copy of the GNU General Public License
    16  // along with Gauge.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package install
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"regexp"
    28  	"runtime"
    29  	"sort"
    30  	"strings"
    31  
    32  	"github.com/getgauge/common"
    33  	"github.com/getgauge/gauge/config"
    34  	"github.com/getgauge/gauge/logger"
    35  	"github.com/getgauge/gauge/manifest"
    36  	"github.com/getgauge/gauge/plugin"
    37  	"github.com/getgauge/gauge/runner"
    38  	"github.com/getgauge/gauge/util"
    39  	"github.com/getgauge/gauge/version"
    40  )
    41  
    42  const (
    43  	pluginJSON = "plugin.json"
    44  	jsonExt    = ".json"
    45  )
    46  
    47  type installDescription struct {
    48  	Name        string
    49  	Description string
    50  	Versions    []versionInstallDescription
    51  }
    52  
    53  type versionInstallDescription struct {
    54  	Version             string
    55  	GaugeVersionSupport version.VersionSupport
    56  	Install             platformSpecificCommand
    57  	DownloadUrls        downloadUrls
    58  }
    59  
    60  type downloadUrls struct {
    61  	X86 platformSpecificURL
    62  	X64 platformSpecificURL
    63  }
    64  
    65  type platformSpecificCommand struct {
    66  	Windows []string
    67  	Linux   []string
    68  	Darwin  []string
    69  }
    70  
    71  type platformSpecificURL struct {
    72  	Windows string
    73  	Linux   string
    74  	Darwin  string
    75  }
    76  
    77  // InstallResult represents the result of plugin installation
    78  type InstallResult struct {
    79  	Error   error
    80  	Warning string
    81  	Success bool
    82  	Skipped bool
    83  }
    84  
    85  func (installResult *InstallResult) getMessage() string {
    86  	return installResult.Error.Error()
    87  }
    88  
    89  func installError(err error) InstallResult {
    90  	return InstallResult{Error: err, Success: false}
    91  }
    92  
    93  func installSuccess(warning string) InstallResult {
    94  	return InstallResult{Warning: warning, Success: true}
    95  }
    96  
    97  func installSkipped(warning string) InstallResult {
    98  	return InstallResult{Warning: warning, Skipped: true}
    99  }
   100  
   101  // GaugePlugin represents any plugin to Gauge. It can be an language runner or any other plugin.
   102  type GaugePlugin struct {
   103  	ID          string
   104  	Version     string
   105  	Description string
   106  	PreInstall  struct {
   107  		Windows []string
   108  		Linux   []string
   109  		Darwin  []string
   110  	}
   111  	PostInstall struct {
   112  		Windows []string
   113  		Linux   []string
   114  		Darwin  []string
   115  	}
   116  	PreUnInstall struct {
   117  		Windows []string
   118  		Linux   []string
   119  		Darwin  []string
   120  	}
   121  	PostUnInstall struct {
   122  		Windows []string
   123  		Linux   []string
   124  		Darwin  []string
   125  	}
   126  	GaugeVersionSupport version.VersionSupport
   127  }
   128  
   129  // InstallPluginFromZipFile installs plugin from given zip file
   130  func InstallPluginFromZipFile(zipFile string, pluginName string) InstallResult {
   131  	tempDir := common.GetTempDir()
   132  	defer common.Remove(tempDir)
   133  	unzippedPluginDir, err := common.UnzipArchive(zipFile, tempDir)
   134  	if err != nil {
   135  		return installError(err)
   136  	}
   137  
   138  	gp, err := parsePluginJSON(unzippedPluginDir, pluginName)
   139  	if err != nil {
   140  		return installError(err)
   141  	}
   142  
   143  	if err = runPlatformCommands(gp.PreInstall, unzippedPluginDir); err != nil {
   144  		return installError(err)
   145  	}
   146  
   147  	pluginInstallDir, err := getPluginInstallDir(gp.ID, getVersionedPluginDirName(zipFile))
   148  	if err != nil {
   149  		return installError(err)
   150  	}
   151  
   152  	// copy files to gauge plugin install location
   153  	logger.Info("Installing plugin %s %s", gp.ID, filepath.Base(pluginInstallDir))
   154  	if _, err = common.MirrorDir(unzippedPluginDir, pluginInstallDir); err != nil {
   155  		return installError(err)
   156  	}
   157  
   158  	if err = runPlatformCommands(gp.PostInstall, pluginInstallDir); err != nil {
   159  		return installError(err)
   160  	}
   161  	return installSuccess("")
   162  }
   163  
   164  func getPluginInstallDir(pluginID, pluginDirName string) (string, error) {
   165  	pluginsDir, err := common.GetPrimaryPluginsInstallDir()
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	pluginDirPath := filepath.Join(pluginsDir, pluginID, pluginDirName)
   170  	if common.DirExists(pluginDirPath) {
   171  		return "", fmt.Errorf("Plugin %s %s already installed at %s", pluginID, pluginDirName, pluginDirPath)
   172  	}
   173  	return pluginDirPath, nil
   174  }
   175  
   176  func parsePluginJSON(pluginDir, pluginName string) (*GaugePlugin, error) {
   177  	var file string
   178  	if common.FileExists(filepath.Join(pluginDir, pluginName+jsonExt)) {
   179  		file = filepath.Join(pluginDir, pluginName+jsonExt)
   180  	} else {
   181  		file = filepath.Join(pluginDir, pluginJSON)
   182  	}
   183  
   184  	var gp GaugePlugin
   185  	contents, err := common.ReadFileContents(file)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	if err = json.Unmarshal([]byte(contents), &gp); err != nil {
   190  		return nil, err
   191  	}
   192  	return &gp, nil
   193  }
   194  
   195  // InstallPlugin download and install the latest plugin(if version not specified) of given plugin name
   196  func InstallPlugin(pluginName, version string) InstallResult {
   197  	logger.Info("Gathering metadata for %s", pluginName)
   198  	installDescription, result := getInstallDescription(pluginName, false)
   199  	defer util.RemoveTempDir()
   200  	if !result.Success {
   201  		return result
   202  	}
   203  	return installPluginWithDescription(installDescription, version)
   204  }
   205  
   206  func installPluginWithDescription(installDescription *installDescription, currentVersion string) InstallResult {
   207  	var versionInstallDescription *versionInstallDescription
   208  	var err error
   209  	if currentVersion != "" {
   210  		versionInstallDescription, err = installDescription.getVersion(currentVersion)
   211  		if err != nil {
   212  			return installError(err)
   213  		}
   214  		if compatibilityError := version.CheckCompatibility(version.CurrentGaugeVersion, &versionInstallDescription.GaugeVersionSupport); compatibilityError != nil {
   215  			return installError(fmt.Errorf("Plugin Version %s-%s is not supported for gauge %s : %s", installDescription.Name, versionInstallDescription.Version, version.CurrentGaugeVersion.String(), compatibilityError.Error()))
   216  		}
   217  	} else {
   218  		versionInstallDescription, err = installDescription.getLatestCompatibleVersionTo(version.CurrentGaugeVersion)
   219  		if err != nil {
   220  			return installError(fmt.Errorf("Could not find compatible version for plugin %s. : %s", installDescription.Name, err))
   221  		}
   222  	}
   223  	return installPluginVersion(installDescription, versionInstallDescription)
   224  }
   225  
   226  func installPluginVersion(installDesc *installDescription, versionInstallDescription *versionInstallDescription) InstallResult {
   227  	if common.IsPluginInstalled(installDesc.Name, versionInstallDescription.Version) {
   228  		return installSkipped(fmt.Sprintf("Plugin %s %s is already installed.", installDesc.Name, versionInstallDescription.Version))
   229  	}
   230  
   231  	downloadLink, err := getDownloadLink(versionInstallDescription.DownloadUrls)
   232  	if err != nil {
   233  		return installError(fmt.Errorf("Could not get download link: %s", err.Error()))
   234  	}
   235  
   236  	tempDir := common.GetTempDir()
   237  	defer common.Remove(tempDir)
   238  	logger.Info("Downloading %s", filepath.Base(downloadLink))
   239  	pluginZip, err := util.Download(downloadLink, tempDir, "", false)
   240  	if err != nil {
   241  		return installError(fmt.Errorf("Failed to download the plugin. %s", err.Error()))
   242  	}
   243  	return InstallPluginFromZipFile(pluginZip, installDesc.Name)
   244  }
   245  
   246  func runPlatformCommands(commands platformSpecificCommand, workingDir string) error {
   247  	command := []string{}
   248  	switch runtime.GOOS {
   249  	case "windows":
   250  		command = commands.Windows
   251  		break
   252  	case "darwin":
   253  		command = commands.Darwin
   254  		break
   255  	default:
   256  		command = commands.Linux
   257  		break
   258  	}
   259  
   260  	if len(command) == 0 {
   261  		return nil
   262  	}
   263  
   264  	logger.Info("Running plugin hook command => %s", command)
   265  	cmd, err := common.ExecuteSystemCommand(command, workingDir, os.Stdout, os.Stderr)
   266  
   267  	if err != nil {
   268  		return err
   269  	}
   270  
   271  	return cmd.Wait()
   272  }
   273  
   274  // UninstallPlugin uninstall the given plugin of the given version
   275  // If version is not specified, it uninstalls all the versions of given plugin
   276  func UninstallPlugin(pluginName string, version string) {
   277  	pluginsHome, err := common.GetPrimaryPluginsInstallDir()
   278  	if err != nil {
   279  		logger.Fatalf("Failed to uninstall plugin %s. %s", pluginName, err.Error())
   280  	}
   281  	if !common.DirExists(filepath.Join(pluginsHome, pluginName, version)) {
   282  		logger.Errorf("Plugin %s not found.", strings.TrimSpace(pluginName+" "+version))
   283  		os.Exit(0)
   284  	}
   285  	var failed bool
   286  	pluginsDir := filepath.Join(pluginsHome, pluginName)
   287  	filepath.Walk(pluginsDir, func(dir string, info os.FileInfo, err error) error {
   288  		if err == nil && info.IsDir() && dir != pluginsDir && strings.HasPrefix(filepath.Base(dir), version) {
   289  			if err := uninstallVersionOfPlugin(dir, pluginName, filepath.Base(dir)); err != nil {
   290  				logger.Errorf("Failed to uninstall plugin %s %s. %s", pluginName, version, err.Error())
   291  				failed = true
   292  			}
   293  		}
   294  		return nil
   295  	})
   296  	if failed {
   297  		os.Exit(1)
   298  	}
   299  	if version == "" {
   300  		if err := os.RemoveAll(pluginsDir); err != nil {
   301  			logger.Fatalf("Failed to remove directory %s. %s", pluginsDir, err.Error())
   302  		}
   303  	}
   304  }
   305  
   306  func uninstallVersionOfPlugin(pluginDir, pluginName, version string) error {
   307  	gp, err := parsePluginJSON(pluginDir, pluginName)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	if err := runPlatformCommands(gp.PreUnInstall, pluginDir); err != nil {
   312  		return err
   313  	}
   314  
   315  	if err := os.RemoveAll(pluginDir); err != nil {
   316  		return err
   317  	}
   318  	if err := runPlatformCommands(gp.PostUnInstall, path.Dir(pluginDir)); err != nil {
   319  		return err
   320  	}
   321  	logger.Info("Successfully uninstalled plugin %s %s.", pluginName, version)
   322  	return nil
   323  }
   324  
   325  func getDownloadLink(downloadUrls downloadUrls) (string, error) {
   326  	var platformLinks *platformSpecificURL
   327  	if strings.Contains(runtime.GOARCH, "64") {
   328  		platformLinks = &downloadUrls.X64
   329  	} else {
   330  		platformLinks = &downloadUrls.X86
   331  	}
   332  
   333  	var downloadLink string
   334  	switch runtime.GOOS {
   335  	case "windows":
   336  		downloadLink = platformLinks.Windows
   337  		break
   338  	case "darwin":
   339  		downloadLink = platformLinks.Darwin
   340  		break
   341  	default:
   342  		downloadLink = platformLinks.Linux
   343  		break
   344  	}
   345  	if downloadLink == "" {
   346  		return "", fmt.Errorf("Platform not supported for %s. Download URL not specified.", runtime.GOOS)
   347  	}
   348  	return downloadLink, nil
   349  }
   350  
   351  func getInstallDescription(plugin string, silent bool) (*installDescription, InstallResult) {
   352  	versionInstallDescriptionJSONFile := plugin + "-install.json"
   353  	versionInstallDescriptionJSONUrl, result := constructPluginInstallJSONURL(plugin)
   354  	if !result.Success {
   355  		return nil, installError(fmt.Errorf("Could not construct plugin install json file URL. %s", result.Error))
   356  	}
   357  	tempDir := common.GetTempDir()
   358  	defer common.Remove(tempDir)
   359  
   360  	downloadedFile, downloadErr := util.Download(versionInstallDescriptionJSONUrl, tempDir, versionInstallDescriptionJSONFile, silent)
   361  	if downloadErr != nil {
   362  		logger.Debug("Failed to download %s file: %s", versionInstallDescriptionJSONFile, downloadErr)
   363  		return nil, installError(fmt.Errorf("Invalid plugin. Could not download %s file.", versionInstallDescriptionJSONFile))
   364  	}
   365  
   366  	return getInstallDescriptionFromJSON(downloadedFile)
   367  }
   368  
   369  func getInstallDescriptionFromJSON(installJSON string) (*installDescription, InstallResult) {
   370  	InstallJSONContents, readErr := common.ReadFileContents(installJSON)
   371  	if readErr != nil {
   372  		return nil, installError(readErr)
   373  	}
   374  	installDescription := &installDescription{}
   375  	if err := json.Unmarshal([]byte(InstallJSONContents), installDescription); err != nil {
   376  		return nil, installError(err)
   377  	}
   378  	return installDescription, installSuccess("")
   379  }
   380  
   381  func constructPluginInstallJSONURL(p string) (string, InstallResult) {
   382  	repoURL := config.GaugeRepositoryUrl()
   383  	if repoURL == "" {
   384  		return "", installError(fmt.Errorf("Could not find gauge repository url from configuration."))
   385  	}
   386  	JSONURL := fmt.Sprintf("%s/%s", repoURL, p)
   387  	if qp := plugin.QueryParams(); qp != "" {
   388  		JSONURL += qp
   389  	}
   390  	return JSONURL, installSuccess("")
   391  }
   392  
   393  func (installDesc *installDescription) getVersion(version string) (*versionInstallDescription, error) {
   394  	for _, versionInstallDescription := range installDesc.Versions {
   395  		if versionInstallDescription.Version == version {
   396  			return &versionInstallDescription, nil
   397  		}
   398  	}
   399  	return nil, errors.New("Could not find install description for Version " + version)
   400  }
   401  
   402  func (installDesc *installDescription) getLatestCompatibleVersionTo(currentVersion *version.Version) (*versionInstallDescription, error) {
   403  	installDesc.sortVersionInstallDescriptions()
   404  	for _, versionInstallDesc := range installDesc.Versions {
   405  		if err := version.CheckCompatibility(currentVersion, &versionInstallDesc.GaugeVersionSupport); err == nil {
   406  			return &versionInstallDesc, nil
   407  		}
   408  	}
   409  	return nil, fmt.Errorf("Compatible version to %s not found", currentVersion)
   410  }
   411  
   412  func (installDesc *installDescription) sortVersionInstallDescriptions() {
   413  	sort.Sort(byDecreasingVersion(installDesc.Versions))
   414  }
   415  
   416  func getVersionedPluginDirName(pluginZip string) string {
   417  	zipFileName := filepath.Base(pluginZip)
   418  	if !strings.Contains(zipFileName, "nightly") {
   419  		re, _ := regexp.Compile("[0-9]+\\.[0-9]+\\.[0-9]+")
   420  		return re.FindString(zipFileName)
   421  	}
   422  	re, _ := regexp.Compile("[0-9]+\\.[0-9]+\\.[0-9]+\\.nightly-[0-9]+-[0-9]+-[0-9]+")
   423  	return re.FindString(zipFileName)
   424  }
   425  
   426  func getRunnerJSONContents(file string) (*runner.Runner, error) {
   427  	var r runner.Runner
   428  	contents, err := common.ReadFileContents(file)
   429  	if err != nil {
   430  		return nil, err
   431  	}
   432  	err = json.Unmarshal([]byte(contents), &r)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	return &r, nil
   437  }
   438  
   439  // InstallAllPlugins install the latest version of all plugins specified in Gauge project manifest file
   440  func InstallAllPlugins() {
   441  	manifest, err := manifest.ProjectManifest()
   442  	if err != nil {
   443  		logger.Fatalf(err.Error())
   444  	}
   445  	installPluginsFromManifest(manifest)
   446  }
   447  
   448  // UpdatePlugins updates all the currently installed plugins to its latest version
   449  func UpdatePlugins() {
   450  	var failedPlugin []string
   451  	for _, pluginInfo := range plugin.GetPluginsInfo() {
   452  		logger.Info("Updating plugin '%s'", pluginInfo.Name)
   453  		passed := HandleUpdateResult(InstallPlugin(pluginInfo.Name, ""), pluginInfo.Name, false)
   454  		if !passed {
   455  			failedPlugin = append(failedPlugin, pluginInfo.Name)
   456  		}
   457  		fmt.Println()
   458  	}
   459  	if len(failedPlugin) > 0 {
   460  		logger.Fatalf("Failed to update '%s' plugins.", strings.Join(failedPlugin, ", "))
   461  	}
   462  	logger.Info("Successfully updated all the plugins.")
   463  }
   464  
   465  // HandleInstallResult handles the result of plugin Installation
   466  // TODO: Merge both HandleInstallResult and HandleUpdateResult, eliminate boolean exitIfFailure
   467  func HandleInstallResult(result InstallResult, pluginName string, exitIfFailure bool) bool {
   468  	if result.Warning != "" {
   469  		logger.Warning(result.Warning)
   470  	}
   471  	if result.Skipped {
   472  		return true
   473  	}
   474  	if !result.Success {
   475  		logger.Errorf("Failed to install plugin '%s'.\nReason: %s", pluginName, result.getMessage())
   476  		if exitIfFailure {
   477  			os.Exit(1)
   478  		}
   479  		return false
   480  	}
   481  
   482  	logger.Info("Successfully installed plugin '%s'.", pluginName)
   483  	return true
   484  }
   485  
   486  // HandleUpdateResult handles the result of plugin Installation
   487  func HandleUpdateResult(result InstallResult, pluginName string, exitIfFailure bool) bool {
   488  	if result.Warning != "" {
   489  		logger.Warning(result.Warning)
   490  	}
   491  	if result.Skipped {
   492  		return true
   493  	}
   494  	if !result.Success {
   495  		logger.Errorf("Failed to update plugin '%s'.\nReason: %s", pluginName, result.getMessage())
   496  		if exitIfFailure {
   497  			os.Exit(1)
   498  		}
   499  		return false
   500  	}
   501  	logger.Info("Successfully updated plugin '%s'.", pluginName)
   502  	return true
   503  }
   504  
   505  func installPluginsFromManifest(manifest *manifest.Manifest) {
   506  	pluginsMap := make(map[string]bool, 0)
   507  	pluginsMap[manifest.Language] = true
   508  	for _, plugin := range manifest.Plugins {
   509  		pluginsMap[plugin] = false
   510  	}
   511  
   512  	for pluginName, isRunner := range pluginsMap {
   513  		if !IsCompatiblePluginInstalled(pluginName, isRunner) {
   514  			logger.Info("Compatible version of plugin %s not found. Installing plugin %s...", pluginName, pluginName)
   515  			HandleInstallResult(InstallPlugin(pluginName, ""), pluginName, true)
   516  		} else {
   517  			logger.Info("Plugin %s is already installed.", pluginName)
   518  		}
   519  	}
   520  }
   521  
   522  // IsCompatiblePluginInstalled checks if a plugin compatible to gauge is installed
   523  // TODO: This always checks if latest installed version of a given plugin is compatible. This should also check for older versions.
   524  func IsCompatiblePluginInstalled(pluginName string, isRunner bool) bool {
   525  	if isRunner {
   526  		return isCompatibleLanguagePluginInstalled(pluginName)
   527  	}
   528  	pd, err := plugin.GetPluginDescriptor(pluginName, "")
   529  	if err != nil {
   530  		return false
   531  	}
   532  	return version.CheckCompatibility(version.CurrentGaugeVersion, &pd.GaugeVersionSupport) == nil
   533  }
   534  
   535  func isCompatibleLanguagePluginInstalled(name string) bool {
   536  	jsonFilePath, err := plugin.GetLanguageJSONFilePath(name)
   537  	if err != nil {
   538  		return false
   539  	}
   540  
   541  	r, err := getRunnerJSONContents(jsonFilePath)
   542  	if err != nil {
   543  		return false
   544  	}
   545  	return version.CheckCompatibility(version.CurrentGaugeVersion, &r.GaugeVersionSupport) == nil
   546  }
   547  
   548  type byDecreasingVersion []versionInstallDescription
   549  
   550  func (a byDecreasingVersion) Len() int      { return len(a) }
   551  func (a byDecreasingVersion) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   552  func (a byDecreasingVersion) Less(i, j int) bool {
   553  	version1, _ := version.ParseVersion(a[i].Version)
   554  	version2, _ := version.ParseVersion(a[j].Version)
   555  	return version1.IsGreaterThan(version2)
   556  }
   557  
   558  // AddPluginToProject adds the given plugin to current Gauge project. It installs the plugin if not installed.
   559  func AddPluginToProject(pluginName string, pluginArgs string) {
   560  	additionalArgs := make(map[string]string)
   561  	if pluginArgs != "" {
   562  		// plugin args will be comma separated values
   563  		// eg: version=1.0, foo_version = 2.41
   564  		args := strings.Split(pluginArgs, ",")
   565  		for _, arg := range args {
   566  			keyValuePair := strings.Split(arg, "=")
   567  			if len(keyValuePair) == 2 {
   568  				additionalArgs[strings.TrimSpace(keyValuePair[0])] = strings.TrimSpace(keyValuePair[1])
   569  			}
   570  		}
   571  	}
   572  	manifest, err := manifest.ProjectManifest()
   573  	if err != nil {
   574  		logger.Fatalf(err.Error())
   575  	}
   576  	if err := addPluginToTheProject(pluginName, additionalArgs, manifest); err != nil {
   577  		logger.Fatalf(fmt.Sprintf("Failed to add plugin %s to project : %s\n", pluginName, err.Error()))
   578  	} else {
   579  		logger.Info("Plugin %s was successfully added to the project\n", pluginName)
   580  	}
   581  }
   582  
   583  func addPluginToTheProject(pluginName string, pluginArgs map[string]string, manifest *manifest.Manifest) error {
   584  	if !plugin.IsPluginInstalled(pluginName, pluginArgs["version"]) {
   585  		logger.Info("Plugin %s %s is not installed. Downloading the plugin.... \n", pluginName, pluginArgs["version"])
   586  		result := InstallPlugin(pluginName, pluginArgs["version"])
   587  		if !result.Success {
   588  			logger.Errorf(result.getMessage())
   589  		}
   590  	}
   591  	pd, err := plugin.GetPluginDescriptor(pluginName, pluginArgs["version"])
   592  	if err != nil {
   593  		return err
   594  	}
   595  	if plugin.IsPluginAdded(manifest, pd) {
   596  		return fmt.Errorf("Plugin %s is already added.", pd.Name)
   597  	}
   598  	manifest.Plugins = append(manifest.Plugins, pd.ID)
   599  	return manifest.Save()
   600  }