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