Skip to content

Commit 7a41667

Browse files
committed
clean: Update runtime installation to be generic relying on specification file
1 parent 36db748 commit 7a41667

File tree

6 files changed

+296
-10
lines changed

6 files changed

+296
-10
lines changed

cmd/install.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
cfg "codacy/cli-v2/config"
5+
"codacy/cli-v2/plugins"
56
"fmt"
67
"log"
78

@@ -28,16 +29,9 @@ var installCmd = &cobra.Command{
2829
}
2930

3031
func fetchRuntimes(config *cfg.ConfigType) {
31-
for _, r := range config.Runtimes() {
32-
switch r.Name() {
33-
case "node":
34-
err := cfg.InstallNode(r)
35-
if err != nil {
36-
log.Fatal(err)
37-
}
38-
default:
39-
log.Fatal("Unknown runtime:", r.Name())
40-
}
32+
err := plugins.InstallRuntimes(config.Runtimes())
33+
if err != nil {
34+
log.Fatal(err)
4135
}
4236
}
4337

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.22.3
55
require (
66
github.com/google/uuid v1.6.0
77
github.com/spf13/cobra v1.8.0
8+
gopkg.in/yaml.v2 v2.2.2
89
)
910

1011
require (

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
268268
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
269269
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
270270
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
271+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
271272
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
272273
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
273274
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

plugins/runtime-utils.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package plugins
2+
3+
import (
4+
"codacy/cli-v2/config"
5+
"codacy/cli-v2/utils"
6+
"embed"
7+
"fmt"
8+
"log"
9+
"os"
10+
"path"
11+
"path/filepath"
12+
"runtime"
13+
"strings"
14+
"text/template"
15+
16+
"gopkg.in/yaml.v2"
17+
)
18+
19+
//go:embed runtimes/*/plugin.yaml
20+
var runtimePlugins embed.FS
21+
22+
type PluginSpec struct {
23+
Name string `yaml:"name"`
24+
Description string `yaml:"description"`
25+
Download struct {
26+
URLTemplate string `yaml:"url_template"`
27+
FileNameTemplate string `yaml:"file_name_template"`
28+
Extension map[string]string `yaml:"extension"`
29+
ArchMapping map[string]string `yaml:"arch_mapping"`
30+
} `yaml:"download"`
31+
Binaries []struct {
32+
Name string `yaml:"name"`
33+
Path string `yaml:"path"`
34+
} `yaml:"binaries"`
35+
}
36+
37+
// getPluginSpec loads a runtime plugin specification by name
38+
func getPluginSpec(runtimeName string) (*PluginSpec, error) {
39+
pluginPath := fmt.Sprintf("runtimes/%s/plugin.yaml", runtimeName)
40+
41+
data, err := runtimePlugins.ReadFile(pluginPath)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to read plugin specification for runtime %s: %v", runtimeName, err)
44+
}
45+
46+
var spec PluginSpec
47+
if err := yaml.Unmarshal(data, &spec); err != nil {
48+
return nil, fmt.Errorf("failed to parse plugin specification for runtime %s: %v", runtimeName, err)
49+
}
50+
51+
return &spec, nil
52+
}
53+
54+
// getExtension returns the appropriate file extension based on OS
55+
func getExtension(spec *PluginSpec, goos string) string {
56+
if ext, ok := spec.Download.Extension[goos]; ok {
57+
return ext
58+
}
59+
return spec.Download.Extension["default"]
60+
}
61+
62+
// getArchMapping maps Go architecture to runtime architecture
63+
func getArchMapping(spec *PluginSpec, goarch string) string {
64+
if arch, ok := spec.Download.ArchMapping[goarch]; ok {
65+
return arch
66+
}
67+
return goarch
68+
}
69+
70+
// executeTemplate processes a template string with provided data
71+
func executeTemplate(tmplString string, data map[string]string) (string, error) {
72+
tmpl, err := template.New("template").Parse(tmplString)
73+
if err != nil {
74+
return "", err
75+
}
76+
77+
var result strings.Builder
78+
if err := tmpl.Execute(&result, data); err != nil {
79+
return "", err
80+
}
81+
82+
return result.String(), nil
83+
}
84+
85+
// generateRuntimeInfo creates runtime info based on the plugin specification
86+
func generateRuntimeInfo(r *config.Runtime, spec *PluginSpec, goos, goarch string) (map[string]string, error) {
87+
// Prepare template data
88+
data := map[string]string{
89+
"Version": r.Version(),
90+
"OS": goos,
91+
"Arch": getArchMapping(spec, goarch),
92+
}
93+
94+
// Process file name template
95+
fileName, err := executeTemplate(spec.Download.FileNameTemplate, data)
96+
if err != nil {
97+
return nil, fmt.Errorf("failed to generate file name: %v", err)
98+
}
99+
100+
// Add fileName to template data for URL generation
101+
data["FileName"] = fileName
102+
data["Extension"] = getExtension(spec, goos)
103+
104+
info := map[string]string{
105+
"fileName": fileName,
106+
"installDir": path.Join(config.Config.RuntimesDirectory(), fileName),
107+
}
108+
109+
// Add binary paths
110+
for _, binary := range spec.Binaries {
111+
info[binary.Name] = path.Join(config.Config.RuntimesDirectory(), fileName, binary.Path)
112+
}
113+
114+
return info, nil
115+
}
116+
117+
// getDownloadURL generates the download URL for a runtime
118+
func getDownloadURL(r *config.Runtime, spec *PluginSpec, goos, goarch string) (string, error) {
119+
// Generate file name and prepare template data
120+
info, err := generateRuntimeInfo(r, spec, goos, goarch)
121+
if err != nil {
122+
return "", err
123+
}
124+
125+
data := map[string]string{
126+
"Version": r.Version(),
127+
"OS": goos,
128+
"Arch": getArchMapping(spec, goarch),
129+
"FileName": info["fileName"],
130+
"Extension": getExtension(spec, goos),
131+
}
132+
133+
// Process URL template
134+
url, err := executeTemplate(spec.Download.URLTemplate, data)
135+
if err != nil {
136+
return "", fmt.Errorf("failed to generate download URL: %v", err)
137+
}
138+
139+
return url, nil
140+
}
141+
142+
// InstallRuntime installs a single runtime using its plugin specification
143+
func InstallRuntime(r *config.Runtime) error {
144+
goos := runtime.GOOS
145+
goarch := runtime.GOARCH
146+
147+
spec, err := getPluginSpec(r.Name())
148+
if err != nil {
149+
return err
150+
}
151+
152+
downloadURL, err := getDownloadURL(r, spec, goos, goarch)
153+
if err != nil {
154+
return err
155+
}
156+
157+
fileName := filepath.Base(downloadURL)
158+
filePath := filepath.Join(config.Config.RuntimesDirectory(), fileName)
159+
160+
// Check if runtime archive already exists
161+
t, err := os.Open(filePath)
162+
if err == nil {
163+
defer t.Close()
164+
log.Printf("%s is already downloaded, skipping download...", r.Name())
165+
} else {
166+
// Download the runtime archive
167+
log.Printf("Downloading %s version %s...", r.Name(), r.Version())
168+
downloadedFile, err := utils.DownloadFile(downloadURL, config.Config.RuntimesDirectory())
169+
if err != nil {
170+
return fmt.Errorf("failed to download %s: %v", r.Name(), err)
171+
}
172+
173+
t, err = os.Open(downloadedFile)
174+
if err != nil {
175+
return fmt.Errorf("failed to open downloaded file: %v", err)
176+
}
177+
defer t.Close()
178+
}
179+
180+
log.Printf("Extracting %s...", r.Name())
181+
182+
// Extract based on file extension
183+
extension := getExtension(spec, goos)
184+
if extension == "zip" {
185+
err = utils.ExtractZip(filePath, config.Config.RuntimesDirectory())
186+
} else {
187+
err = utils.ExtractTarGz(t, config.Config.RuntimesDirectory())
188+
}
189+
190+
if err != nil {
191+
return fmt.Errorf("failed to extract %s: %v", r.Name(), err)
192+
}
193+
194+
log.Printf("Successfully installed %s version %s", r.Name(), r.Version())
195+
return nil
196+
}
197+
198+
// InstallRuntimes installs multiple runtimes based on the provided map
199+
func InstallRuntimes(runtimes map[string]*config.Runtime) error {
200+
for _, runtime := range runtimes {
201+
if err := InstallRuntime(runtime); err != nil {
202+
return fmt.Errorf("failed to install runtime %s: %v", runtime.Name(), err)
203+
}
204+
}
205+
return nil
206+
}

plugins/runtimes/node/plugin.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: node
2+
description: Node.js JavaScript runtime
3+
download:
4+
url_template: "https://nodejs.org/dist/v{{.Version}}/{{.FileName}}.{{.Extension}}"
5+
file_name_template: "node-v{{.Version}}-{{.OS}}-{{.Arch}}"
6+
extension:
7+
windows: "zip"
8+
default: "tar.gz"
9+
arch_mapping:
10+
"386": "x86"
11+
"amd64": "x64"
12+
"arm": "armv7l"
13+
"arm64": "arm64"
14+
binaries:
15+
- name: node
16+
path: "bin/node"
17+
- name: npm
18+
path: "bin/npm"

utils/extract.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,69 @@ func ExtractTarGz(archive *os.File, targetDir string) error {
6464
}
6565
return nil
6666
}
67+
68+
func ExtractZip(filePath string, targetDir string) error {
69+
archive, err := os.Open(filePath)
70+
if err != nil {
71+
return err
72+
}
73+
defer archive.Close()
74+
75+
format := archiver.Zip{}
76+
77+
handler := func(ctx context.Context, f archiver.File) error {
78+
path := filepath.Join(targetDir, f.NameInArchive)
79+
80+
switch f.IsDir() {
81+
case true:
82+
// create a directory
83+
err := os.MkdirAll(path, 0777)
84+
if err != nil {
85+
return err
86+
}
87+
88+
case false:
89+
// if is a symlink
90+
if f.LinkTarget != "" {
91+
os.Remove(path)
92+
err := os.Symlink(f.LinkTarget, path)
93+
if err != nil {
94+
return err
95+
}
96+
return nil
97+
}
98+
99+
// ensure parent directory exists
100+
parentDir := filepath.Dir(path)
101+
if err := os.MkdirAll(parentDir, 0777); err != nil {
102+
return err
103+
}
104+
105+
// write a file
106+
w, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.Mode())
107+
if err != nil {
108+
return err
109+
}
110+
defer w.Close()
111+
112+
stream, err := f.Open()
113+
if err != nil {
114+
return err
115+
}
116+
defer stream.Close()
117+
118+
_, err = io.Copy(w, stream)
119+
if err != nil {
120+
return err
121+
}
122+
}
123+
124+
return nil
125+
}
126+
127+
err = format.Extract(context.Background(), archive, nil, handler)
128+
if err != nil {
129+
return err
130+
}
131+
return nil
132+
}

0 commit comments

Comments
 (0)