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