Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codacy/codacy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ runtimes:
- node@22.2.0
- python@3.11.11
- dart@3.7.2
- java@17.0.10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need remove this one from here, and add it only on the specific tool test. Does it make sense @zhamborova with the new strucuture? Then, if you don't have it here, the integration-test will fail if you don't put the java there

Copy link
Contributor Author

@andrzej-janczak andrzej-janczak May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we should stop using this for developing maybe
But do we need to start in my PR 😄

tools:
- eslint@8.57.0
- trivy@0.59.1
Expand Down
8 changes: 5 additions & 3 deletions .cursor/rules/cursor.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ alwaysApply: true
# Your rule content

## Key Rules
- use full names like e.g. feature, instead of feat
- avoid code copy pasting and duplication, refactor to function eagerly when possible
- use fulnames like e.g. feature, instaed of feat
- run go build after each code modification to see if app compiles
- remove dead unused code
- remove dead unused code
- look for constants like file permissons in `constants` folder

## Code Style Guidelines
- **Imports**: Standard lib first, external packages second, internal last
Expand All @@ -24,4 +26,4 @@ alwaysApply: true
- `cmd/`: CLI command implementations
- `config/`: Configuration handling
- `tools/`: Tool-specific implementations
- `utils/`: Utility functions and static - look for static like default file permisson here
- `utils/`: Utility functions
45 changes: 40 additions & 5 deletions config/runtimes-installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func InstallRuntimes(config *ConfigType) error {

// InstallRuntime installs a specific runtime
func InstallRuntime(name string, runtimeInfo *plugins.RuntimeInfo) error {

// Check if the runtime is already installed
if isRuntimeInstalled(runtimeInfo) {
logger.Info("Runtime already installed", logrus.Fields{
Expand All @@ -63,6 +64,15 @@ func InstallRuntime(name string, runtimeInfo *plugins.RuntimeInfo) error {
return fmt.Errorf("failed to download and extract runtime %s: %w", name, err)
}

// Verify that the runtime binaries are available
if !isRuntimeInstalled(runtimeInfo) {
logger.Error("Runtime binaries not found after extraction", logrus.Fields{
"runtime": name,
"version": runtimeInfo.Version,
})
return fmt.Errorf("runtime %s was extracted but binaries are not available", name)
}

return nil
}

Expand All @@ -71,18 +81,30 @@ func isRuntimeInstalled(runtimeInfo *plugins.RuntimeInfo) bool {
// If there are no binaries, check the install directory
if len(runtimeInfo.Binaries) == 0 {
_, err := os.Stat(runtimeInfo.InstallDir)
return err == nil
if err != nil {
logger.Debug("Runtime install directory not found", logrus.Fields{
"directory": runtimeInfo.InstallDir,
"error": err,
})
return false
}
return true
}

// Check if at least one binary exists
for _, binaryPath := range runtimeInfo.Binaries {
for binaryName, binaryPath := range runtimeInfo.Binaries {
_, err := os.Stat(binaryPath)
if err == nil {
return true
if err != nil {
logger.Debug("Runtime binary not found", logrus.Fields{
"binary": binaryName,
"path": binaryPath,
"error": err,
})
return false
}
}

return false
return true
}

// downloadAndExtractRuntime downloads and extracts a runtime
Expand Down Expand Up @@ -140,6 +162,19 @@ func downloadAndExtractRuntime(runtimeInfo *plugins.RuntimeInfo) error {
return fmt.Errorf("failed to extract runtime: %w", err)
}

// Ensure binaries have executable permissions
for _, binaryPath := range runtimeInfo.Binaries {
fullPath := filepath.Join(Config.RuntimesDirectory(), filepath.Base(runtimeInfo.InstallDir), binaryPath)
if err := os.Chmod(fullPath, utils.DefaultDirPerms); err != nil {
logger.Debug("Failed to set binary permissions", logrus.Fields{
"binary": binaryPath,
"path": fullPath,
"error": err,
})
return fmt.Errorf("failed to set binary permissions for %s: %w", binaryPath, err)
}
}

logger.Debug("Runtime extraction completed", logrus.Fields{
"runtime": runtimeInfo.Name,
"version": runtimeInfo.Version,
Expand Down
24 changes: 24 additions & 0 deletions constants/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package constants

const (
// FilePermission represents the default file permission (rw-r--r--)

Check notice on line 4 in constants/permissions.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

constants/permissions.go#L4

comment on exported const DefaultFilePerms should be of the form "DefaultFilePerms ..."
// This permission gives:
// - read/write (rw-) permissions to the owner
// - read-only (r--) permissions to the group
// - read-only (r--) permissions to others
DefaultFilePerms = 0644

// DefaultDirPerms represents the default directory permission (rwxr-xr-x)
// This permission gives:
// - read/write/execute (rwx) permissions to the owner
// - read/execute (r-x) permissions to the group
// - read/execute (r-x) permissions to others
//
// Execute permission on directories is required to:
// - List directory contents (ls)
// - Access files within the directory (cd)
// - Create/delete files in the directory
// Without execute permission, users cannot traverse into or use the directory,
// even if they have read/write permissions on files inside it
DefaultDirPerms = 0755 // For directories
)
46 changes: 40 additions & 6 deletions plugins/runtime-utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@

// binary represents a binary executable provided by the runtime
type binary struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
Name string `yaml:"name"`
Path interface{} `yaml:"path"` // Can be either string or map[string]string
}

// binaryPath represents OS-specific paths for a binary
type binaryPath struct {
Darwin string `yaml:"darwin"`
Linux string `yaml:"linux"`
}

// pluginConfig holds the structure of the plugin.yaml file
Expand Down Expand Up @@ -48,6 +54,7 @@
// templateData holds the data to be used in template substitution
type templateData struct {
Version string
MajorVersion string
FileName string
OS string
Arch string
Expand Down Expand Up @@ -118,14 +125,31 @@

// Process binary paths
for _, binary := range plugin.Config.Binaries {
binaryPath := path.Join(installDir, binary.Path)
var binaryPath string

switch path := binary.Path.(type) {
case string:
// If path is a simple string, use it directly
binaryPath = path
case map[string]interface{}:
// If path is a map, get the OS-specific path
if osPath, ok := path[runtime.GOOS]; ok {
binaryPath = osPath.(string)
} else {
return nil, fmt.Errorf("no binary path specified for OS %s", runtime.GOOS)
}
default:
return nil, fmt.Errorf("invalid path format for binary %s", binary.Name)
}

fullPath := path.Join(installDir, binaryPath)

Check warning on line 145 in plugins/runtime-utils.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/runtime-utils.go#L145

`path.Join(...)` always joins using a forward slash.

// Add file extension for Windows executables
if runtime.GOOS == "windows" && !strings.HasSuffix(binaryPath, ".exe") {
binaryPath += ".exe"
if runtime.GOOS == "windows" && !strings.HasSuffix(fullPath, ".exe") {
fullPath += ".exe"
}

info.Binaries[binary.Name] = binaryPath
info.Binaries[binary.Name] = fullPath
}

return info, nil
Expand Down Expand Up @@ -172,6 +196,14 @@
return p.Config.Download.Extension.Default
}

// getMajorVersion extracts the major version from a version string (e.g. "17.0.10" -> "17")
func (p *runtimePlugin) getMajorVersion(version string) string {
if idx := strings.Index(version, "."); idx != -1 {
return version[:idx]
}
return version
}

// GetFileName generates the filename based on the template in plugin.yaml
func (p *runtimePlugin) getFileName(version string) string {
goos := runtime.GOOS
Expand All @@ -185,6 +217,7 @@
// Prepare template data
data := templateData{
Version: version,
MajorVersion: p.getMajorVersion(version),
OS: mappedOS,
Arch: mappedArch,
ReleaseVersion: releaseVersion,
Expand Down Expand Up @@ -230,6 +263,7 @@
// Prepare template data
data := templateData{
Version: version,
MajorVersion: p.getMajorVersion(version),
FileName: fileName,
OS: mappedOS,
Arch: mappedArch,
Expand Down
20 changes: 20 additions & 0 deletions plugins/runtimes/java/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: java
description: Java Runtime Environment
download:
url_template: "https://github.com/adoptium/temurin{{.MajorVersion}}-binaries/releases/download/jdk-{{.Version}}%2B7/OpenJDK{{.MajorVersion}}U-jdk_{{.Arch}}_{{.OS}}_hotspot_{{.Version}}_7.{{.Extension}}"
file_name_template: "jdk-{{.Version}}+7"
extension:
default: "tar.gz"
arch_mapping:
"386": "x86-32"
"amd64": "x64"
"arm": "arm"
"arm64": "aarch64"
os_mapping:
"darwin": "mac"
"linux": "linux"
binaries:
- name: java
path:
darwin: "Contents/Home/bin/java"
linux: "bin/java"
74 changes: 59 additions & 15 deletions plugins/tool-utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"bytes"
"embed"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -50,9 +52,8 @@ type RuntimeBinaries struct {
Execution string `yaml:"execution"`
}

// ExtensionConfig defines the file extension based on OS
// ExtensionConfig defines the file extension
type ExtensionConfig struct {
Windows string `yaml:"windows"`
Default string `yaml:"default"`
}

Expand All @@ -73,6 +74,7 @@ type ToolPluginConfig struct {
RuntimeBinaries RuntimeBinaries `yaml:"runtime_binaries"`
Installation InstallationConfig `yaml:"installation"`
Download DownloadConfig `yaml:"download"`
Environment map[string]string `yaml:"environment"`
Binaries []ToolBinary `yaml:"binaries"`
Formatters []Formatter `yaml:"formatters"`
OutputOptions OutputOptions `yaml:"output_options"`
Expand Down Expand Up @@ -107,6 +109,8 @@ type ToolInfo struct {
DownloadURL string
FileName string
Extension string
// Environment variables
Environment map[string]string
}

// ProcessTools processes a list of tool configurations and returns a map of tool information
Expand Down Expand Up @@ -158,6 +162,8 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
// Store raw command templates (processing will happen later)
InstallCommand: pluginConfig.Installation.Command,
RegistryCommand: pluginConfig.Installation.RegistryTemplate,
// Store environment variables
Environment: make(map[string]string),
}

// Handle download configuration for directly downloaded tools
Expand Down Expand Up @@ -208,6 +214,39 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
info.Formatters[formatter.Name] = formatter.Flag
}

// Process environment variables
if len(pluginConfig.Environment) > 0 {
// Get runtime install directory if needed
var runtimeInstallDir string
if info.Runtime != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this block of code be inside the len(pluginConfig.Environment)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think so, this should only run when we have Environment declared in yaml

if runtime, ok := runtimes[info.Runtime]; ok {
runtimeInstallDir = runtime.InstallDir
}
}

// Process each environment variable
for key, tmplStr := range pluginConfig.Environment {
tmpl, err := template.New("env").Parse(tmplStr)
if err != nil {
return nil, fmt.Errorf("error parsing environment template for %s: %w", config.Name, err)
}

var buf bytes.Buffer
err = tmpl.Execute(&buf, struct {
RuntimeInstallDir string
Path string
}{
RuntimeInstallDir: runtimeInstallDir,
Path: os.Getenv("PATH"),
})
if err != nil {
return nil, fmt.Errorf("error executing environment template for %s: %w", config.Name, err)
}

info.Environment[key] = buf.String()
}
}

result[config.Name] = info
}

Expand Down Expand Up @@ -238,9 +277,6 @@ func getMappedOS(osMapping map[string]string, goos string) string {

// getExtension returns the appropriate file extension based on the OS
func getExtension(extensionConfig ExtensionConfig, goos string) string {
if goos == "windows" {
return extensionConfig.Windows
}
return extensionConfig.Default
}

Expand Down Expand Up @@ -274,19 +310,27 @@ func getFileName(fileNameTemplate string, version string, mappedArch string, goo

// getDownloadURL generates the download URL based on the template
func getDownloadURL(urlTemplate string, fileName string, version string, mappedArch string, mappedOS string, extension string) string {
// Extract major version from version string (e.g. "17.0.10" -> "17")
majorVersion := version
if idx := strings.Index(version, "."); idx != -1 {
majorVersion = version[:idx]
}

// Prepare template data
data := struct {
Version string
FileName string
OS string
Arch string
Extension string
Version string
MajorVersion string
FileName string
OS string
Arch string
Extension string
}{
Version: version,
FileName: fileName,
OS: mappedOS,
Arch: mappedArch,
Extension: extension,
Version: version,
MajorVersion: majorVersion,
FileName: fileName,
OS: mappedOS,
Arch: mappedArch,
Extension: extension,
}

// Execute template substitution for URL
Expand Down
Loading
Loading