internal/platform/product/product_info.go (402 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 product import ( "encoding/json" "fmt" "os" "path/filepath" "runtime" "strconv" "strings" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" "github.com/JetBrains/qodana-cli/internal/platform/utils" log "github.com/sirupsen/logrus" ) type Product struct { Analyzer Analyzer Name string IdeCode string Code string Version string BaseScriptName string IdeScript string Build string Home string IsEap bool } var ( // base script name Idea = "idea" PhpStorm = "phpstorm" WebStorm = "webstorm" Rider = "rider" PyCharm = "pycharm" RubyMine = "rubymine" GoLand = "goland" RustRover = "rustrover" Clion = "clion" ) var supportedIdes = [...]string{ Idea, PhpStorm, WebStorm, Rider, PyCharm, RubyMine, GoLand, RustRover, Clion, } func (p Product) IdeBin() string { return ideBin(p.Home) } func ideBin(home string) string { return filepath.Join(home, "bin") } func (p Product) CustomPluginsPath() string { if qdenv.IsContainer() || runtime.GOOS != "darwin" { return filepath.Join(p.Home, "custom-plugins") } base, err := os.UserConfigDir() if err != nil { log.Fatal(err) } return filepath.Join(base, "JetBrains", "Qodana", p.Code, p.GetVersionBranch(), "custom-plugins") } func (p Product) DisabledPluginsFilePath() string { if qdenv.IsContainer() || runtime.GOOS != "darwin" { return filepath.Join(p.Home, "disabled_plugins.txt") } base, err := os.UserConfigDir() if err != nil { log.Fatal(err) } return filepath.Join(base, "JetBrains", "Qodana", p.Code, p.GetVersionBranch(), "disabled_plugins.txt") } func (p Product) javaHome() string { return filepath.Join(p.Home, "jbr") } func (p Product) JbrJava() string { if p.Home != "" { switch runtime.GOOS { case "darwin": return filepath.Join(p.javaHome(), "Contents", "Home", "bin", "java") case "windows": return filepath.Join(p.javaHome(), "bin", "java.exe") default: return filepath.Join(p.javaHome(), "bin", "java") } } else if utils.IsInstalled("java") { return "java" } log.Warn("Java is not installed") return "" } func (p Product) VmOptionsEnv() string { switch p.BaseScriptName { case Idea: return "IDEA_VM_OPTIONS" case PhpStorm: return "PHPSTORM_VM_OPTIONS" case WebStorm: return "WEBIDE_VM_OPTIONS" case Rider: return "RIDER_VM_OPTIONS" case PyCharm: return "PYCHARM_VM_OPTIONS" case RubyMine: return "RUBYMINE_VM_OPTIONS" case GoLand: return "GOLAND_VM_OPTIONS" case RustRover: return "RUSTROVER_VM_OPTIONS" case Clion: return "CLION_VM_OPTIONS" default: log.Fatalf("Usupported base script name for vmoptions file: %s", p.BaseScriptName) return "" } } func (p Product) ParentPrefix() string { switch p.Code { case QDPHP: return "PhpStorm" case QDJS: return "WebStorm" case QDNET: return "Rider" case QDPY: return "Python" case QDPYC: return "PyCharmCore" case QDGO: return "GoLand" case QDRUBY: return "Ruby" case QDRST: return "RustRover" case QDCPP: return "CLion" default: return "Idea" } } func (p Product) GetProductNameFromCode() string { return GetProductNameFromCode(p.Code) } func GetProductNameFromCode(code string) string { switch code { case QDJVMC: return "Qodana Community for JVM" case QDPYC: return "Qodana Community for Python" case QDANDC: return "Qodana Community for Android" case QDAND: return "Qodana for Android" case QDJVM: return "Qodana for JVM" case QDPHP: return "Qodana for PHP" case QDJS: return "Qodana for JS" case QDNET: return "Qodana for .NET" case QDNETC: return "Qodana Community for .NET" case QDCLC: return "Qodana for C/C++" case QDPY: return "Qodana for Python" case QDGO: return "Qodana for Go" case QDRST: return "Qodana for Rust" case QDRUBY: return "Qodana for Ruby" case QDCPP: return "Qodana for C/C++" default: return "Qodana" } } // GetVersionBranch returns the version branch of the current Product, e.g. 2020.3 -> 203, 2021.1 -> 211, 2022.3 -> 223. func (p Product) GetVersionBranch() string { versions := strings.Split(p.Version, ".") if len(versions) < 2 { return "master" } return fmt.Sprintf("%s%s", versions[0][2:], versions[1]) } // Is233orNewer returns true if the current Product is 233 or newer. func (p Product) Is233orNewer() bool { return p.isNotOlderThan(233) } func (p Product) Is242orNewer() bool { return p.isNotOlderThan(242) } func (p Product) Is251orNewer() bool { return p.isNotOlderThan(251) } func (p Product) isNotOlderThan(version int) bool { number, err := strconv.Atoi(p.GetVersionBranch()) if err != nil { msg.WarningMessage("Invalid version: %s", err) return false } return number >= version } func (p Product) isRuby() bool { return p.Code == QDRUBY } type Launch struct { CustomCommands []struct { Commands []string AdditionalJvmArguments []string } `json:"customCommands"` } type InfoJson struct { Version string `json:"version"` BuildNumber string `json:"buildNumber"` ProductCode string `json:"productCode"` VersionSuffix string `json:"versionSuffix"` Launch []Launch `json:"launch"` } func GuessProduct(idePath string, analyzer Analyzer) Product { homePath := idePath if //goland:noinspection GoBoolExpressions runtime.GOOS == "darwin" { contentsDir := filepath.Join(homePath, "Contents") if _, err := os.Stat(contentsDir); err == nil { homePath = contentsDir } } if homePath == "" { if home, ok := os.LookupEnv(qdenv.QodanaDistEnv); ok { homePath = home } else if qdenv.IsContainer() { homePath = "/opt/idea" } else { // guess from the executable location ex, err := os.Executable() if err != nil { log.Fatal(err) } homePath = filepath.Dir(filepath.Dir(ex)) } } ideBinPath := ideBin(homePath) var baseScriptName string if //goland:noinspection GoBoolExpressions runtime.GOOS == "darwin" { baseScriptName = findIde(filepath.Join(homePath, "MacOS")) } else { baseScriptName = findIde(ideBinPath) } if baseScriptName == "" { msg.WarningMessage( "Supported IDE not found in %s, you can declare the path to IDE home via %s variable", homePath, qdenv.QodanaDistEnv, ) log.Fatal("IDE to run is not found") } var ideScript string if //goland:noinspection ALL runtime.GOOS == "darwin" { ideScript = filepath.Join(homePath, "MacOS", baseScriptName) } else { ideScript = filepath.Join(ideBinPath, fmt.Sprintf("%s%s", baseScriptName, getScriptSuffix())) } productInfo, err := ReadIdeProductInfo(homePath) if err != nil { log.Fatalf("Can't read product-info.json: %v ", err) } flavourProductCode := ReadDistFlavour(homePath) version := productInfo.Version ideCode := productInfo.ProductCode var code string if flavourProductCode != "" { code = flavourProductCode } else { code = toQodanaCode(ideCode) } linter := FindLinterByProductCode(code) if linter == UnknownLinter { log.Fatalf("Unknown product code %s", code) } name := linter.PresentableName build := productInfo.BuildNumber eap := IsEap(productInfo) prod := Product{ Analyzer: analyzer, Name: name, IdeCode: ideCode, Code: code, Version: version, BaseScriptName: baseScriptName, IdeScript: ideScript, Build: build, Home: homePath, IsEap: eap, } log.Debug(prod) qdenv.SetEnv(qdenv.QodanaDistEnv, prod.Home) if prod.isRuby() { qdenv.UnsetRubyVariables() } return prod } func toQodanaCode(baseProduct string) string { switch baseProduct { case "IC": return QDJVMC case "PC": return QDPYC case "IU": return QDJVM case "PS": return QDPHP case "WS": return QDJS case "RD": return QDNET case "PY": return QDPY case "GO": return QDGO case "RM": return QDRUBY case "RR": return QDRST case "CL": return QDCPP default: return "QD" } } func IsEap(info *InfoJson) bool { treatAsRelease := os.Getenv(qdenv.QodanaTreatAsRelease) if treatAsRelease == "true" { return true } for _, launch := range info.Launch { for _, command := range launch.CustomCommands { for _, cmd := range command.Commands { if cmd == "qodana" { for _, arg := range command.AdditionalJvmArguments { if arg == "-Dqodana.eap=true" { return true } } } } } } return false } // ReadIdeProductInfo returns IDE info from the given path. func ReadIdeProductInfo(ideDir string) (*InfoJson, error) { if //goland:noinspection ALL runtime.GOOS == "darwin" { ideDir = filepath.Join(ideDir, "Resources") } productInfo := filepath.Join(ideDir, "product-info.json") if _, err := os.Stat(productInfo); err != nil { return nil, err } productInfoFile, err := os.ReadFile(productInfo) if err != nil { return nil, err } var productInfoJson InfoJson err = json.Unmarshal(productInfoFile, &productInfoJson) if err != nil { return nil, err } return &productInfoJson, nil } // ReadDistFlavour returns more specific product code. // QDJVM could be packed as QDJVM, QDJVMC, QDAND, QDANDC func ReadDistFlavour(ideDir string) string { if //goland:noinspection ALL runtime.GOOS == "darwin" { ideDir = filepath.Join(ideDir, "Resources") } productFlavourFile := filepath.Join(ideDir, "dist.flavour.txt") if _, err := os.Stat(productFlavourFile); err != nil { return "" } productFlavour, err := os.ReadFile(productFlavourFile) if err != nil { return "" } return strings.TrimSpace(string(productFlavour)) } func findIde(dir string) string { for _, element := range supportedIdes { if _, err := os.Stat(filepath.Join(dir, fmt.Sprintf("%s%s", element, getScriptSuffix()))); err == nil { return element } } return "" } //goland:noinspection GoBoolExpressions func getScriptSuffix() string { if runtime.GOOS == "windows" { return "64.exe" } return "" }