internal/core/startup/installers.go (337 lines of code) (raw):

/* * Copyright 2021-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package startup import ( "crypto/sha256" "encoding/hex" "fmt" "io" "math/rand" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "github.com/JetBrains/qodana-cli/internal/platform" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/product" "github.com/JetBrains/qodana-cli/internal/platform/strutil" "github.com/JetBrains/qodana-cli/internal/platform/utils" cp "github.com/otiai10/copy" "github.com/pterm/pterm" log "github.com/sirupsen/logrus" ) func downloadAndInstallIDE( analyser product.Analyzer, baseDir string, spinner *pterm.SpinnerPrinter, ) string { var ideUrl string checkSumUrl := "" releaseDownloadInfo := getIde(analyser) if releaseDownloadInfo == nil { log.Fatalf("Error while obtaining the URL for the supplied IDE, exiting") } else { ideUrl = releaseDownloadInfo.Link checkSumUrl = releaseDownloadInfo.ChecksumLink } fileName := filepath.Base(ideUrl) fileExt := filepath.Ext(fileName) installDir := filepath.Join(baseDir, strings.TrimSuffix(fileName, fileExt)) if _, err := os.Stat(installDir); err == nil { switch runtime.GOOS { case "windows": if dirs, err := filepath.Glob(filepath.Join(installDir, "*")); err == nil && len(dirs) == 1 { installDir = dirs[0] } case "darwin": if dirs, err := filepath.Glob(filepath.Join(installDir, "*.app")); err == nil && len(dirs) == 1 { installDir = filepath.Join(dirs[0], "Contents") } } log.Debugf("IDE already installed to %s, skipping download", installDir) return installDir } downloadedIdePath := filepath.Join(baseDir, fileName) err := utils.DownloadFile(downloadedIdePath, ideUrl, getInternalAuth(), spinner) if err != nil { log.Fatalf("Error while downloading linter: %v", err) } defer func(filePath string) { err = os.Remove(filePath) if err != nil { log.Warning("Error while removing temporary file: " + err.Error()) } }(downloadedIdePath) if checkSumUrl != "" { checksumFilePath := filepath.Join(baseDir, strings.TrimSuffix(fileName, fileExt)+".sha256") verifySha256(checksumFilePath, checkSumUrl, downloadedIdePath) } switch fileExt { case ".sit": err = installIdeFromZip(downloadedIdePath, installDir) case ".zip": err = installIdeFromZip(downloadedIdePath, installDir) case ".exe": err = installIdeWindowsExe(downloadedIdePath, installDir) case ".gz": err = installIdeFromTar(downloadedIdePath, installDir) case ".dmg": err = installIdeMacOS(downloadedIdePath, installDir) default: log.Fatalf("Unsupported file extension: %s", fileExt) } if err != nil { log.Fatalf("Error while unpacking: %v", err) } switch runtime.GOOS { case "windows": if dirs, err := filepath.Glob(filepath.Join(installDir, "*")); err == nil && len(dirs) == 1 { installDir = dirs[0] } case "darwin": if dirs, err := filepath.Glob(filepath.Join(installDir, "*.app")); err == nil && len(dirs) == 1 { installDir = filepath.Join(dirs[0], "Contents") } } return installDir } //goland:noinspection GoBoolExpressions func getIde(analyzer product.Analyzer) *ReleaseDownloadInfo { dist := product.ReleaseVer if analyzer.IsEAP() { dist = product.EapVer } name := analyzer.GetLinter().Name if !analyzer.GetLinter().SupportNative { msg.ErrorMessage("Native mode for linter %s is not supported", name) return nil } linterProperties := product.FindLinterProperties(analyzer.GetLinter()) if linterProperties == nil { msg.ErrorMessage("Native mode for linter %s is not supported", name) return nil } feedProductCode := linterProperties.FeedProductCode prod, err := GetProductByCode(feedProductCode) if err != nil { msg.ErrorMessage("Error while obtaining the product info: %s", err) return nil } if prod == nil { msg.ErrorMessage("Product info is not found for code: %s", feedProductCode) return nil } release := SelectLatestCompatibleRelease(prod, dist) if release == nil { msg.ErrorMessage("Could not find a %s version for '%s'", dist, linterProperties.PresentableName) return nil } var downloadType string switch runtime.GOOS { case "darwin": downloadType = "macSit" _, ok := (*release.Downloads)[downloadType] if !ok { downloadType = "mac" } if runtime.GOARCH == "arm64" { downloadType = "macSitM1" _, ok := (*release.Downloads)[downloadType] if !ok { downloadType = "macM1" } } case "windows": downloadType = "windowsZip" _, ok := (*release.Downloads)[downloadType] if !ok { downloadType = "windows" } if runtime.GOARCH == "arm64" { downloadType = "windowsZipARM64" _, ok := (*release.Downloads)[downloadType] if !ok { downloadType = "windowsARM64" } } default: downloadType = "linux" if runtime.GOARCH == "arm64" { downloadType = "linuxARM64" } } res, ok := (*release.Downloads)[downloadType] if !ok { msg.ErrorMessage( "%s %s (%s) is not available or not supported for the current platform", feedProductCode, *release.Version, dist, ) return nil } log.Debug(fmt.Sprintf("%s %s %s %s URL: %s", feedProductCode, dist, *release.Version, downloadType, res.Link)) return &res } // installIdeWindowsExe is used as a fallback, since it needs installation privileges and alters the registry func installIdeWindowsExe(archivePath string, targetDir string) error { stdout, stderr, _, err := utils.RunCmdRedirectOutput( "", strutil.QuoteForWindows(archivePath), "/S", fmt.Sprintf("/D=%s", strutil.QuoteForWindows(targetDir)), ) if err != nil { return fmt.Errorf("%s: %s. Stdout: %s. Stderr: %s", archivePath, err, stdout, stderr) } return nil } func extractArchive(archivePath string, targetDir string, stripComponents int) error { targetBasename := filepath.Base(targetDir) if targetBasename == "." || targetBasename == "/" { // filepath.Base returns either "." or "/" when the path has no basename. return fmt.Errorf("invalid target directory") } tempDir, err := os.MkdirTemp("", fmt.Sprintf("%s-*.partial", targetBasename)) if err != nil { return fmt.Errorf("failed to create temporary directory for extraction: %w", err) } defer func() { if err := os.RemoveAll(tempDir); err != nil { log.Warningf("failed to remove temporary directory: %s", err.Error()) } }() tarExe, err := exec.LookPath("tar") if err != nil { return fmt.Errorf("could not find 'tar': %w", err) } tarArgv := []string{tarExe, "-xf", strutil.GetQuotedPath(archivePath), "-C", strutil.GetQuotedPath(tempDir)} if stripComponents > 0 { tarArgv = append(tarArgv, "--strip-components", strconv.Itoa(stripComponents)) } stdout, stderr, _, err := utils.RunCmdRedirectOutput("", tarArgv...) if err != nil { return fmt.Errorf("failed to extract: %w. Stdout: %s. Stderr: %s", err, stdout, stderr) } if err := os.RemoveAll(targetDir); err != nil { return fmt.Errorf("failed to remove existing target directory: %w", err) } err = os.Rename(tempDir, targetDir) if err != nil { // Trying to fallback if rename failed https://youtrack.jetbrains.com/issue/QD-12252 log.Warningf("Error moving linter files from temp to target: %s. Trying to copy instead.", err) err = cp.Copy(tempDir, targetDir) if err != nil { return fmt.Errorf("error copying linter files from temp to target: %w", err) } } return nil } func installIdeFromZip(archivePath string, targetDir string) error { err := extractArchive(archivePath, targetDir, 0) if err != nil { return err } if runtime.GOOS != "windows" { err = platform.ChangePermissionsRecursivelyUnix(targetDir, 0755) if err != nil { return fmt.Errorf("error moving linter files from temp to target: %w", err) } } return nil } func installIdeFromTar(archivePath string, targetDir string) error { return extractArchive(archivePath, targetDir, 1) } func installIdeMacOS(archivePath string, targetDir string) error { mountDir := fmt.Sprintf("/Volumes/MyTempMount%d", rand.Intn(10000)) _, err := exec.Command("hdiutil", "attach", "-nobrowse", "-mountpoint", mountDir, archivePath).Output() if err != nil { return fmt.Errorf("hdiutil attach: %s", err) } defer func(command *exec.Cmd) { err := command.Run() if err != nil { log.Fatal(fmt.Errorf("hdiutil eject: %s", err)) } }(exec.Command("hdiutil", "eject", mountDir, "-force")) matches, err := filepath.Glob(mountDir + "/*.app") if err != nil { return fmt.Errorf("filepath.Glob: %s", err) } if len(matches) == 0 { return fmt.Errorf("no .app found in dmg") } err = cp.Copy(matches[0], targetDir) if err != nil { return fmt.Errorf("cp.Copy: %s", err) } return nil } func verifySha256(checksumFile string, checkSumUrl string, filePath string) { err := utils.DownloadFile(checksumFile, checkSumUrl, getInternalAuth(), nil) if err != nil { log.Fatalf("Error while downloading checksum for IDE: %v", err) } defer func(filePath string) { err = os.Remove(filePath) if err != nil { log.Warning("Error while removing temporary file: " + err.Error()) } }(checksumFile) checksum, err := os.ReadFile(checksumFile) if err != nil { log.Fatalf("Error occurred during reading checksum file: %v", err) } file, err := os.Open(filePath) if err != nil { log.Fatalf("Error while opening IDE archive: %v", err) } defer func(file *os.File) { err := file.Close() if err != nil { log.Fatalf("Error while closing IDE archive: %v", err) } }(file) h := sha256.New() if _, err := io.Copy(h, file); err != nil { log.Fatalf("Error while computing checksum of IDE archive: %v", err) } actual := hex.EncodeToString(h.Sum(nil)) expected := strings.SplitN(string(checksum), " ", 2)[0] if actual != expected { err = os.Remove(filePath) if err != nil { log.Warning("Error while removing temporary file: " + err.Error()) } log.Fatalf("Checksums doesn't match. Expected: %s, Actual: %s", expected, actual) } log.Info("Checksum of downloaded IDE was verified") } func downloadCustomPlugins(ideUrl string, targetDir string, spinner *pterm.SpinnerPrinter) error { pluginsUrl := getPluginsURL(ideUrl) log.Debugf("Downloading custom plugins from %s to %s", pluginsUrl, targetDir) if err := os.MkdirAll(targetDir, os.ModePerm); err != nil { return fmt.Errorf("couldn't create a directory %s: %v", targetDir, err) } archivePath := filepath.Join(targetDir, "custom-plugins.zip") err := utils.DownloadFile(archivePath, pluginsUrl, getInternalAuth(), spinner) if err != nil { return fmt.Errorf("error while downloading plugins: %v", err) } _, err = utils.RunCmd("", "tar", "-xf", strutil.GetQuotedPath(archivePath), "-C", strutil.GetQuotedPath(targetDir)) if err != nil { return fmt.Errorf("tar: %s", err) } disabledPluginsPath := filepath.Join(targetDir, "custom-plugins", "disabled_plugins.txt") err = cp.Copy(disabledPluginsPath, filepath.Join(targetDir, "disabled_plugins.txt")) if err != nil { return fmt.Errorf("error while copying plugins: %s", err) } return nil } func getPluginsURL(ideUrl string) string { pluginsUrl := strings.Replace(ideUrl, "-aarch64", "", 1) if strings.Contains(pluginsUrl, ".sit") { return strings.Replace(pluginsUrl, ".sit", "-custom-plugins.zip", 1) } else if strings.Contains(pluginsUrl, ".win.zip") { return strings.Replace(pluginsUrl, ".win.zip", "-custom-plugins.zip", 1) } return strings.Replace(pluginsUrl, ".tar.gz", "-custom-plugins.zip", 1) }