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 }