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,
)
}
}
}