Skip to content

Commit 845fe07

Browse files
committed
Add python runtime
1 parent 3607792 commit 845fe07

File tree

4 files changed

+125
-55
lines changed

4 files changed

+125
-55
lines changed

cmd/init.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func configFileTemplate(tools []tools.Tool) string {
8383

8484
return fmt.Sprintf(`runtimes:
8585
86+
8687
tools:
8788
- eslint@%s
8889
`, eslintVersion)

plugins/runtime-utils.go

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ type binary struct {
2424

2525
// pluginConfig holds the structure of the plugin.yaml file
2626
type pluginConfig struct {
27-
Name string `yaml:"name"`
28-
Description string `yaml:"description"`
29-
Download downloadConfig `yaml:"download"`
30-
Binaries []binary `yaml:"binaries"`
27+
Name string `yaml:"name"`
28+
Description string `yaml:"description"`
29+
Download downloadConfig `yaml:"download"`
30+
Binaries []binary `yaml:"binaries"`
3131
}
3232

3333
// downloadConfig holds the download configuration from the plugin.yaml
@@ -36,6 +36,8 @@ type downloadConfig struct {
3636
FileNameTemplate string `yaml:"file_name_template"`
3737
Extension extensionConfig `yaml:"extension"`
3838
ArchMapping map[string]string `yaml:"arch_mapping"`
39+
OSMapping map[string]string `yaml:"os_mapping"`
40+
ReleaseVersion string `yaml:"release_version"`
3941
}
4042

4143
// extensionConfig defines the file extension based on OS
@@ -46,11 +48,12 @@ type extensionConfig struct {
4648

4749
// templateData holds the data to be used in template substitution
4850
type templateData struct {
49-
Version string
50-
FileName string
51-
OS string
52-
Arch string
53-
Extension string
51+
Version string
52+
FileName string
53+
OS string
54+
Arch string
55+
Extension string
56+
ReleaseVersion string
5457
}
5558

5659
// runtimePlugin represents a runtime plugin with methods to interact with it
@@ -79,31 +82,30 @@ type RuntimeInfo struct {
7982
// ProcessRuntimes processes a list of runtime configurations and returns a map of runtime information
8083
func ProcessRuntimes(configs []RuntimeConfig, runtimesDir string) (map[string]*RuntimeInfo, error) {
8184
result := make(map[string]*RuntimeInfo)
82-
85+
8386
for _, config := range configs {
8487
runtimeInfo, err := processRuntime(config, runtimesDir)
8588
if err != nil {
8689
return nil, err
8790
}
88-
91+
8992
result[config.Name] = runtimeInfo
9093
}
91-
94+
9295
return result, nil
9396
}
9497

95-
9698
// ProcessRuntime processes a single runtime configuration and returns detailed runtime info
9799
func processRuntime(config RuntimeConfig, runtimesDir string) (*RuntimeInfo, error) {
98100
plugin, err := loadPlugin(config.Name)
99101
if err != nil {
100102
return nil, fmt.Errorf("failed to load plugin for runtime %s: %w", config.Name, err)
101103
}
102-
104+
103105
fileName := plugin.getFileName(config.Version)
104106
extension := plugin.getExtension(runtime.GOOS)
105107
installDir := plugin.getInstallationDirectoryPath(runtimesDir, config.Version)
106-
108+
107109
// Create RuntimeInfo with essential information
108110
info := &RuntimeInfo{
109111
Name: config.Name,
@@ -114,39 +116,38 @@ func processRuntime(config RuntimeConfig, runtimesDir string) (*RuntimeInfo, err
114116
Extension: extension,
115117
Binaries: make(map[string]string),
116118
}
117-
119+
118120
// Process binary paths
119121
for _, binary := range plugin.Config.Binaries {
120122
binaryPath := path.Join(installDir, binary.Path)
121-
123+
122124
// Add file extension for Windows executables
123125
if runtime.GOOS == "windows" && !strings.HasSuffix(binaryPath, ".exe") {
124126
binaryPath += ".exe"
125127
}
126-
128+
127129
info.Binaries[binary.Name] = binaryPath
128130
}
129-
131+
130132
return info, nil
131133
}
132134

133-
134135
// LoadPlugin loads a plugin configuration from the specified plugin directory
135136
func loadPlugin(runtimeName string) (*runtimePlugin, error) {
136137
pluginPath := filepath.Join("runtimes", runtimeName, "plugin.yaml")
137-
138+
138139
// Read from embedded filesystem
139140
data, err := pluginsFS.ReadFile(pluginPath)
140141
if err != nil {
141142
return nil, fmt.Errorf("error reading plugin.yaml: %w", err)
142143
}
143-
144+
144145
var config pluginConfig
145146
err = yaml.Unmarshal(data, &config)
146147
if err != nil {
147148
return nil, fmt.Errorf("error parsing plugin.yaml: %w", err)
148149
}
149-
150+
150151
return &runtimePlugin{
151152
Config: config,
152153
ConfigPath: pluginPath,
@@ -175,67 +176,79 @@ func (p *runtimePlugin) getExtension(goos string) string {
175176
func (p *runtimePlugin) getFileName(version string) string {
176177
goos := runtime.GOOS
177178
goarch := runtime.GOARCH
178-
179-
// Map Go architecture to runtime-specific architecture
179+
180+
// Map Go architecture and OS to runtime-specific values
180181
mappedArch := p.getMappedArch(goarch)
181-
182+
mappedOS := p.getMappedOS(goos)
183+
releaseVersion := p.getReleaseVersion()
184+
182185
// Prepare template data
183186
data := templateData{
184-
Version: version,
185-
OS: goos,
186-
Arch: mappedArch,
187+
Version: version,
188+
OS: mappedOS,
189+
Arch: mappedArch,
190+
ReleaseVersion: releaseVersion,
187191
}
188-
192+
189193
// Execute template substitution for filename
190194
tmpl, err := template.New("filename").Parse(p.Config.Download.FileNameTemplate)
191195
if err != nil {
192196
return ""
193197
}
194-
198+
195199
var buf bytes.Buffer
196200
err = tmpl.Execute(&buf, data)
197201
if err != nil {
198202
return ""
199203
}
200-
204+
201205
return buf.String()
202206
}
203207

208+
// GetReleaseVersion returns the release version from the plugin configuration
209+
func (p *runtimePlugin) getReleaseVersion() string {
210+
return p.Config.Download.ReleaseVersion
211+
}
212+
204213
// GetDownloadURL generates the download URL based on the template in plugin.yaml
205214
func (p *runtimePlugin) getDownloadURL(version string) string {
206215
goos := runtime.GOOS
207216
goarch := runtime.GOARCH
208-
209-
// Map Go architecture to runtime-specific architecture
217+
218+
// Map Go architecture and OS to runtime-specific values
210219
mappedArch := p.getMappedArch(goarch)
211-
220+
mappedOS := p.getMappedOS(goos)
221+
212222
// Get the appropriate extension
213223
extension := p.getExtension(goos)
214-
224+
215225
// Get the filename
216226
fileName := p.getFileName(version)
217-
227+
228+
releaseVersion := p.getReleaseVersion()
229+
218230
// Prepare template data
219231
data := templateData{
220-
Version: version,
221-
FileName: fileName,
222-
OS: goos,
223-
Arch: mappedArch,
224-
Extension: extension,
232+
Version: version,
233+
FileName: fileName,
234+
OS: mappedOS,
235+
Arch: mappedArch,
236+
Extension: extension,
237+
ReleaseVersion: releaseVersion,
225238
}
226-
239+
227240
// Execute template substitution for URL
228241
tmpl, err := template.New("url").Parse(p.Config.Download.URLTemplate)
229242
if err != nil {
230243
return ""
231244
}
232-
245+
233246
var buf bytes.Buffer
234247
err = tmpl.Execute(&buf, data)
235248
if err != nil {
236249
return ""
237250
}
238-
251+
239252
return buf.String()
240253
}
241254

@@ -244,3 +257,13 @@ func (p *runtimePlugin) getInstallationDirectoryPath(runtimesDir string, version
244257
fileName := p.getFileName(version)
245258
return path.Join(runtimesDir, fileName)
246259
}
260+
261+
// GetMappedOS returns the OS mapping for the current system
262+
func (p *runtimePlugin) getMappedOS(goos string) string {
263+
// Check if there's a mapping for this OS
264+
if mappedOS, ok := p.Config.Download.OSMapping[goos]; ok {
265+
return mappedOS
266+
}
267+
// Return the original OS if no mapping exists
268+
return goos
269+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: python
2+
description: Python runtime
3+
download:
4+
url_template: "https://github.com/astral-sh/python-build-standalone/releases/download/{{.ReleaseVersion}}/cpython-{{.Version}}+{{.ReleaseVersion}}-{{.Arch}}-{{.OS}}-install_only.{{.Extension}}"
5+
file_name_template: "cpython-{{.Version}}+{{.ReleaseVersion}}-{{.Arch}}-{{.OS}}"
6+
release_version: "20250317"
7+
extension:
8+
windows: "tar.gz"
9+
default: "tar.gz"
10+
arch_mapping:
11+
"386": "x86"
12+
"amd64": "x86_64"
13+
"arm": "armv7l"
14+
"arm64": "aarch64"
15+
os_mapping:
16+
"darwin": "apple-darwin"
17+
"linux": "unknown-linux-gnu"
18+
"windows": "pc-windows-msvc"
19+
binaries:
20+
- name: python3
21+
path: "python/bin/python3"
22+
- name: pip
23+
path: "python/bin/pip"

utils/extract.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package utils
22

33
import (
44
"context"
5-
"github.com/mholt/archiver/v4"
5+
"fmt"
66
"io"
77
"os"
88
"path/filepath"
9+
10+
"github.com/mholt/archiver/v4"
911
)
1012

1113
func ExtractTarGz(archive *os.File, targetDir string) error {
@@ -14,31 +16,33 @@ func ExtractTarGz(archive *os.File, targetDir string) error {
1416
Archival: archiver.Tar{},
1517
}
1618

19+
// Create a map to store symlinks for later creation
20+
symlinks := make(map[string]string)
21+
1722
handler := func(ctx context.Context, f archiver.File) error {
1823
path := filepath.Join(targetDir, f.NameInArchive)
1924

2025
switch f.IsDir() {
2126
case true:
2227
// create a directory
23-
//fmt.Println("creating: " + f.NameInArchive)
2428
err := os.MkdirAll(path, 0777)
2529
if err != nil {
2630
return err
2731
}
2832

2933
case false:
30-
//log.Print("extracting: " + f.NameInArchive)
31-
32-
// if is a symlink
34+
// if is a symlink, store it for later
3335
if f.LinkTarget != "" {
34-
os.Remove(path)
35-
err := os.Symlink(f.LinkTarget, path)
36-
if err != nil {
37-
return err
38-
}
36+
symlinks[path] = f.LinkTarget
3937
return nil
4038
}
4139

40+
// ensure parent directory exists
41+
err := os.MkdirAll(filepath.Dir(path), 0777)
42+
if err != nil {
43+
return err
44+
}
45+
4246
// write a file
4347
w, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.Mode())
4448
if err != nil {
@@ -62,6 +66,25 @@ func ExtractTarGz(archive *os.File, targetDir string) error {
6266
if err != nil {
6367
return err
6468
}
69+
70+
// Create symlinks after all files have been extracted
71+
for path, target := range symlinks {
72+
// Remove any existing file/symlink
73+
os.Remove(path)
74+
75+
// Ensure parent directory exists
76+
err := os.MkdirAll(filepath.Dir(path), 0777)
77+
if err != nil {
78+
return err
79+
}
80+
81+
// Create the symlink
82+
err = os.Symlink(target, path)
83+
if err != nil {
84+
return fmt.Errorf("failed to create symlink %s -> %s: %w", path, target, err)
85+
}
86+
}
87+
6588
return nil
6689
}
6790

0 commit comments

Comments
 (0)