internal/core/startup/prepare.go (294 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 ( "fmt" "os" "path/filepath" "runtime" "strings" "github.com/JetBrains/qodana-cli/internal/cloud" "github.com/JetBrains/qodana-cli/internal/platform/commoncontext" "github.com/JetBrains/qodana-cli/internal/platform/git" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/nuget" "github.com/JetBrains/qodana-cli/internal/platform/product" "github.com/JetBrains/qodana-cli/internal/platform/qdcontainer" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" "github.com/JetBrains/qodana-cli/internal/platform/tokenloader" cp "github.com/otiai10/copy" "github.com/pterm/pterm" log "github.com/sirupsen/logrus" ) const ( m2 = ".m2" nugetDir = "nuget" ) type PreparedHost struct { IdeDir string QodanaUploadToken string Prod product.Product } // PrepareHost gets the current user, creates the necessary folders for the analysis. func PrepareHost(commonCtx commoncontext.Context) PreparedHost { prod := product.Product{} cloudUploadToken := commonCtx.QodanaToken ideDir := "" if commonCtx.IsClearCache { err := os.RemoveAll(commonCtx.CacheDir) if err != nil { log.Errorf("Could not clear local Qodana cache: %s", err) } } nuget.WarnIfPrivateFeedDetected(commonCtx.Analyzer, commonCtx.ProjectDir) if nuget.IsNugetConfigNeeded() { nuget.PrepareNugetConfig(os.Getenv("HOME")) } if err := os.MkdirAll(commonCtx.CacheDir, os.ModePerm); err != nil { log.Fatal("couldn't create a directory ", err.Error()) } if err := os.MkdirAll(commonCtx.ResultsDir, os.ModePerm); err != nil { log.Fatal("couldn't create a directory ", err.Error()) } if err := os.MkdirAll(commonCtx.ReportDir, os.ModePerm); err != nil { log.Fatal("couldn't create a directory ", err.Error()) } if commonCtx.Analyzer.DownloadDist() { linter := commonCtx.Analyzer.GetLinter() msg.PrintProcess( func(spinner *pterm.SpinnerPrinter) { if spinner != nil { spinner.ShowTimer = false // We will update interactive spinner } ideDir = downloadAndInstallIDE(commonCtx.Analyzer, commonCtx.QodanaSystemDir, spinner) fixWindowsPlugins(ideDir) }, fmt.Sprintf("Downloading %s", linter.Name), fmt.Sprintf("downloading IDE distribution to %s", commonCtx.QodanaSystemDir), ) } if commonCtx.Analyzer.IsContainer() { qdcontainer.PrepareContainerEnvSettings() } else { prod, cloudUploadToken = prepareLocalIdeSettingsAndGetQodanaCloudUploadToken(commonCtx, ideDir) // in case of container run the token passed directly (ref - core/container.go#getDockerOptions) prepareQodanaTokenForNative(cloudUploadToken) } if tokenloader.IsCloudTokenRequired(commonCtx) { cloudUploadToken = tokenloader.ValidateCloudToken(commonCtx, false) } checkVcsSameAsRepositoryRoot(commonCtx) result := PreparedHost{ IdeDir: ideDir, QodanaUploadToken: cloudUploadToken, Prod: prod, } return result } func prepareLocalIdeSettingsAndGetQodanaCloudUploadToken( commonCtx commoncontext.Context, ideDir string, ) (product.Product, string) { prod := product.GuessProduct(ideDir, commonCtx.Analyzer) qdenv.ExtractQodanaEnvironment(qdenv.SetEnv) isTokenRequired := tokenloader.IsCloudTokenRequired(commonCtx) token := tokenloader.LoadCloudUploadToken(commonCtx, false, isTokenRequired, true) cloud.SetupLicenseToken(token) SetupLicenseAndProjectHash(prod, cloud.GetCloudApiEndpoints(), cloud.Token.Token) prepareDirectories(commonCtx.CacheDir, commonCtx.LogDir(), commonCtx.ConfDirPath()) if qdenv.IsContainer() { prepareContainerSpecificDirectories(prod, commonCtx.CacheDir, commonCtx.ConfDirPath()) CreateUser("/etc/passwd") } SyncCache(commonCtx, prod) prepareCustomPlugins(prod) return prod, token } func SyncCache(commonCtx commoncontext.Context, prod product.Product) { err := SyncIdeaCache(commonCtx.CacheDir, commonCtx.ProjectDir, false) if err != nil { log.Warnf("failed to sync .idea directory: %v", err) } SyncConfigCache(prod, commonCtx.ConfDirPath(), commonCtx.CacheDir, true) } func prepareQodanaTokenForNative(token string) { _, isSet := os.LookupEnv(qdenv.QodanaToken) if !isSet { err := os.Setenv(qdenv.QodanaToken, token) if err != nil { log.Fatal("Cannot set QODANA_TOKEN environment variable. The result may not be uploaded to the Qodana cloud.") } } } func prepareCustomPlugins(prod product.Product) { if runtime.GOOS == "darwin" && !prod.Analyzer.IsContainer() { if info := getIde(prod.Analyzer); info != nil { err := downloadCustomPlugins(info.Link, filepath.Dir(prod.CustomPluginsPath()), nil) if err != nil { log.Warning("Error while downloading custom plugins: " + err.Error()) } } } } func prepareContainerSpecificDirectories(prod product.Product, cacheDir string, confDir string) { homeDir, err := os.UserHomeDir() if err != nil { log.Fatal(err) } userPrefsDir := filepath.Join(homeDir, ".java", ".userPrefs") MakeDirAll(userPrefsDir) switch prod.BaseScriptName { case product.Rider: nugetDir := filepath.Join(cacheDir, nugetDir) if err := os.Setenv("NUGET_PACKAGES", nugetDir); err != nil { log.Fatal(err) } MakeDirAll(nugetDir) case product.Idea: MakeDirAll(filepath.Join(cacheDir, m2)) if err = os.Setenv("GRADLE_USER_HOME", filepath.Join(cacheDir, "gradle")); err != nil { log.Fatal(err) } } writeFileIfNew(filepath.Join(userPrefsDir, "prefs.xml"), userPrefsXml) ideaOptions := filepath.Join(confDir, "options") if prod.BaseScriptName == product.Idea { mavenRootDir := filepath.Join(homeDir, ".m2") if _, err = os.Stat(mavenRootDir); os.IsNotExist(err) { if err = os.MkdirAll(mavenRootDir, 0o755); err != nil { log.Fatal(err) } } writeFileIfNew(filepath.Join(mavenRootDir, "settings.xml"), mavenSettingsXml) writeFileIfNew(filepath.Join(ideaOptions, "path.macros.xml"), mavenPathMacroxXml) androidSdk := os.Getenv(qdenv.AndroidSdkRoot) if androidSdk != "" { writeFileIfNew(filepath.Join(ideaOptions, "project.default.xml"), androidProjectDefaultXml(androidSdk)) corettoSdk := os.Getenv(qdenv.QodanaCorettoSdk) if corettoSdk != "" { writeFileIfNew(filepath.Join(ideaOptions, "jdk.table.xml"), jdkTableXml(corettoSdk)) } } } } func prepareDirectories(cacheDir string, logDir string, confDir string) { MakeDirAll(cacheDir) MakeDirAll(logDir) ideaOptions := filepath.Join(confDir, "options") MakeDirAll(ideaOptions) addKeepassIDEConfig(ideaOptions) } func MakeDirAll(dir string) { if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0o755); err != nil { log.Fatal(err) } } } func addKeepassIDEConfig(ideaOptions string) { if //goland:noinspection ALL runtime.GOOS != "darwin" && runtime.GOOS != "windows" { writeFileIfNew(filepath.Join(ideaOptions, "security.xml"), securityXml) } } func SyncConfigCache(prod product.Product, confDirPath string, cacheDir string, fromCache bool) { isIdea := prod.BaseScriptName == product.Idea isPyCharm := prod.BaseScriptName == product.PyCharm if !isIdea && !isPyCharm { return } jdkTableFile := filepath.Join(confDirPath, "options", "jdk.table.xml") cacheFile := filepath.Join(cacheDir, "config", prod.GetVersionBranch(), "jdk.table.xml") if fromCache { if _, err := os.Stat(cacheFile); os.IsNotExist(err) { return } if _, err := os.Stat(jdkTableFile); os.IsNotExist(err) { if err := cp.Copy(cacheFile, jdkTableFile); err != nil { log.Fatal(err) } log.Debugf("SDK table is synced from cache") } } else { if _, err := os.Stat(jdkTableFile); os.IsNotExist(err) { log.Debugf("SDK table isnt't stored to cache, file doesn't exist") } else { if err := cp.Copy(jdkTableFile, cacheFile); err != nil { log.Fatal(err) } log.Debugf("SDK table is stored to cache") } } } // SyncIdeaCache sync .idea/ content from cache and back. func SyncIdeaCache(from string, to string, overwrite bool) error { copyOptions := cp.Options{ OnDirExists: func(src, dest string) cp.DirExistsAction { if overwrite { return cp.Merge } return cp.Untouchable }, OnSymlink: func(src string) cp.SymlinkAction { return cp.Skip }, } src := filepath.Join(from, ".idea") if _, err := os.Stat(src); os.IsNotExist(err) { return fmt.Errorf("source .idea directory does not exist: %s", src) } dst := filepath.Join(to, ".idea") log.Printf("Sync IDE cache from: %s to: %s", src, dst) if err := cp.Copy(src, dst, copyOptions); err != nil { return fmt.Errorf("failed to sync .idea directory: %w", err) } return nil } func writeFileIfNew(filepath string, content string) { if _, err := os.Stat(filepath); os.IsNotExist(err) { if err := os.WriteFile(filepath, []byte(content), 0o755); err != nil { log.Fatal(err) } } } // CreateUser will make dynamic uid as a valid user `idea`, needed for gradle cache. func CreateUser(fn string) { if //goland:noinspection ALL os.Getuid() == 0 { return } idea := fmt.Sprintf("idea:x:%d:%d:idea:/root:/bin/bash", os.Getuid(), os.Getgid()) data, err := os.ReadFile(fn) if err != nil { log.Fatal(err) } lines := strings.Split(strings.TrimSpace(string(data)), "\n") for _, line := range lines { if line == idea { return } } if err = os.WriteFile(fn, []byte(strings.Join(append(lines, idea), "\n")), 0o777); err != nil { log.Fatal(err) } } // fixWindowsPlugins quick-fix for Windows 241 distributions func fixWindowsPlugins(ideDir string) { if runtime.GOOS == "windows" && strings.Contains(ideDir, "241") { pluginsClasspath := filepath.Join(ideDir, "plugins", "plugin-classpath.txt") if _, err := os.Stat(pluginsClasspath); err == nil { err = os.Remove(pluginsClasspath) if err != nil { log.Warnf("Failed to remove plugin-classpath.txt: %v", err) } } } } func checkVcsSameAsRepositoryRoot(ctx commoncontext.Context) { if vcsRoot, err := git.Root(ctx.RepositoryRoot, ctx.LogDir()); err == nil { vcsRootAbs, err1 := filepath.Abs(vcsRoot) repositoryRootAbs, err2 := filepath.Abs(ctx.RepositoryRoot) if err1 != nil || err2 != nil { log.Warnf("Failed to resolve absolute paths for git root check: vcs=%v, proj=%v", err1, err2) } else if vcsRootAbs != repositoryRootAbs { log.Warnf( "The git root directory is different from the repository root directory. This may lead to incorrect results. VCS root: %s, repository root: %s", vcsRootAbs, repositoryRootAbs, ) } } }