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 ""
}