-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrake_builder.go
More file actions
343 lines (285 loc) · 9.58 KB
/
rake_builder.go
File metadata and controls
343 lines (285 loc) · 9.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
package rubyext
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
var execLookPath = exec.LookPath
var execCommandContext = exec.CommandContext
// Ruby command constant
const (
rubyCommand = "ruby"
)
// RakeBuilder handles Ruby-based builds using Rakefile or mkrf_conf
type RakeBuilder struct{}
// Name returns the builder name
func (b *RakeBuilder) Name() string {
return "Rake"
}
// RequiredTools returns the tools needed for Rake builds
func (b *RakeBuilder) RequiredTools() []ToolRequirement {
return []ToolRequirement{
{
Name: "ruby",
Purpose: "Ruby interpreter",
},
{
Name: "rake",
Optional: true,
Purpose: "Ruby build tool (usually bundled with Ruby)",
},
}
}
// CheckTools verifies that Ruby and Rake are available
func (b *RakeBuilder) CheckTools() error {
return CheckRequiredTools(b.RequiredTools())
}
// CanBuild checks if this builder can handle the extension file
func (b *RakeBuilder) CanBuild(extensionFile string) bool {
filename := strings.ToLower(filepath.Base(extensionFile))
return MatchesPattern(filename, `rakefile$`) ||
MatchesPattern(filename, `rakefile\.rb$`) ||
MatchesPattern(filename, `mkrf_conf$`) ||
MatchesPattern(filename, `mkrf_conf\.rb$`)
}
// Build compiles the extension using rake
func (b *RakeBuilder) Build(ctx context.Context, config *BuildConfig, extensionFile string) (*BuildResult, error) {
result := &BuildResult{
Success: false,
Output: []string{},
MissingDependencies: nil,
}
extensionPath := filepath.Join(config.GemDir, extensionFile)
extensionDir := filepath.Dir(extensionPath)
// Handle mkrf_conf files differently - they generate Rakefiles
if b.isMkrfConf(extensionFile) {
if err := b.runMkrfConf(ctx, config, extensionDir, extensionFile, result); err != nil {
result.Error = err
return result, err
}
}
if missingDeps, err := b.ensureRakeAvailable(ctx, config); err != nil {
result.MissingDependencies = missingDeps
result.Error = err
return result, err
}
// Run rake to build the extension
if err := b.runRake(ctx, config, extensionDir, result); err != nil {
result.Error = err
return result, err
}
// Find built extensions
extensions, err := b.findBuiltExtensions(extensionDir)
if err != nil {
result.Error = err
return result, err
}
finalized, err := finalizeNativeExtensions(config, extensionFile, extensionDir, extensions)
if err != nil {
result.Error = err
return result, err
}
result.Extensions = finalized
result.Success = true
return result, nil
}
// Clean removes build artifacts
func (b *RakeBuilder) Clean(ctx context.Context, config *BuildConfig, extensionFile string) error {
extensionPath := filepath.Join(config.GemDir, extensionFile)
extensionDir := filepath.Dir(extensionPath)
// Try rake clean task
cmdName, cmdArgs := b.determineRakeCommand(config, []string{"clean"})
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
cmd.Dir = extensionDir
// Set environment for Ruby/rake
cmd.Env = os.Environ()
if config.RubyPath != "" {
// Ensure rake uses the correct Ruby
rubyDir := filepath.Dir(config.RubyPath)
cmd.Env = append(cmd.Env, fmt.Sprintf("PATH=%s:%s", rubyDir, os.Getenv("PATH")))
}
return cmd.Run() // Ignore errors, clean is best-effort
}
// isMkrfConf checks if this is an mkrf_conf file
func (b *RakeBuilder) isMkrfConf(extensionFile string) bool {
filename := strings.ToLower(filepath.Base(extensionFile))
return MatchesPattern(filename, `mkrf_conf`) || MatchesPattern(filename, `mkrf_conf\.rb`)
}
// runMkrfConf executes mkrf_conf.rb to generate a Rakefile
func (b *RakeBuilder) runMkrfConf(ctx context.Context, config *BuildConfig, extensionDir, extensionFile string, result *BuildResult) error {
rubyPath := config.RubyPath
if rubyPath == "" {
rubyPath = rubyCommand
}
mkrfPath := filepath.Join(extensionDir, filepath.Base(extensionFile))
cmd := exec.CommandContext(ctx, rubyPath, mkrfPath)
cmd.Dir = extensionDir
// Set environment variables
cmd.Env = os.Environ()
for key, value := range config.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
output, err := cmd.CombinedOutput()
outputLines := strings.Split(string(output), "\n")
result.Output = append(result.Output, outputLines...)
if config.Verbose {
result.Output = append(result.Output,
fmt.Sprintf("Running: %s %s", rubyPath, mkrfPath),
fmt.Sprintf("Working directory: %s", extensionDir))
}
if err != nil {
return BuildError("mkrf_conf", result.Output, err)
}
// Verify Rakefile was created
rakefilePath := filepath.Join(extensionDir, "Rakefile")
if _, err := os.Stat(rakefilePath); os.IsNotExist(err) {
return BuildError("mkrf_conf", result.Output, fmt.Errorf("rakefile not generated by mkrf_conf"))
}
return nil
}
// runRake executes rake to build the extension
func (b *RakeBuilder) runRake(ctx context.Context, config *BuildConfig, extensionDir string, result *BuildResult) error {
// Build rake arguments
args := []string{}
// Add parallel jobs if specified and rake supports it
if config.Parallel > 0 {
args = append(args, fmt.Sprintf("--jobs=%d", config.Parallel))
}
// Clean first if requested
if config.CleanFirst {
cleanCmdName, cleanCmdArgs := b.determineRakeCommand(config, []string{"clean"})
cleanCmd := exec.CommandContext(ctx, cleanCmdName, cleanCmdArgs...)
cleanCmd.Dir = extensionDir
cleanOutput, _ := cleanCmd.CombinedOutput()
result.Output = append(result.Output, strings.Split(string(cleanOutput), "\n")...)
}
// Add any custom build args
args = append(args, config.BuildArgs...)
cmdName, cmdArgs := b.determineRakeCommand(config, args)
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
cmd.Dir = extensionDir
// Set environment variables
cmd.Env = os.Environ()
for key, value := range config.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
// Ensure rake uses the correct Ruby
if config.RubyPath != "" {
rubyDir := filepath.Dir(config.RubyPath)
// Prepend Ruby's bin directory to PATH
currentPath := os.Getenv("PATH")
newPath := fmt.Sprintf("%s:%s", rubyDir, currentPath)
cmd.Env = append(cmd.Env,
fmt.Sprintf("PATH=%s", newPath),
fmt.Sprintf("RUBY=%s", config.RubyPath))
}
// Set other Ruby-related environment variables
if config.RubyEngine != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("RUBY_ENGINE=%s", config.RubyEngine))
}
if config.RubyVersion != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("RUBY_VERSION=%s", config.RubyVersion))
}
if config.Verbose {
result.Output = append(result.Output,
fmt.Sprintf("Running: rake %s", strings.Join(args, " ")),
fmt.Sprintf("Working directory: %s", extensionDir))
}
output, err := cmd.CombinedOutput()
outputLines := strings.Split(string(output), "\n")
result.Output = append(result.Output, outputLines...)
if err != nil {
return BuildError("Rake", result.Output, err)
}
return nil
}
func (b *RakeBuilder) determineRakeCommand(config *BuildConfig, args []string) (cmd string, resolvedArgs []string) {
if rakePath, err := execLookPath("rake"); err == nil {
return rakePath, append([]string{}, args...)
}
rubyPath := config.RubyPath
if rubyPath == "" {
rubyPath = rubyCommand
}
rubyArgs := []string{
"-rrubygems",
"-e", `load Gem.bin_path("rake", "rake")`,
"--",
}
rubyArgs = append(rubyArgs, args...)
return rubyPath, rubyArgs
}
// findBuiltExtensions locates the compiled extension files
func (b *RakeBuilder) findBuiltExtensions(extensionDir string) ([]string, error) {
var extensions []string
// Common extension file patterns
patterns := []string{
"*.so", // Linux/Unix shared libraries
"*.bundle", // macOS bundles
"lib/*.so", // Extensions might be in lib subdirectory
"lib/*.bundle", // macOS bundles in lib
"ext/*.so", // Or in ext subdirectory
"ext/*.bundle", // macOS bundles in ext
}
for _, pattern := range patterns {
matches, err := filepath.Glob(filepath.Join(extensionDir, pattern))
if err != nil {
return nil, fmt.Errorf("failed to glob pattern %s in %s: %v", pattern, extensionDir, err)
}
for _, match := range matches {
// Convert to relative path
relPath, err := filepath.Rel(extensionDir, match)
if err == nil {
extensions = append(extensions, relPath)
}
}
}
return extensions, nil
}
// ensureRakeAvailable verifies that rake can be executed either directly or via RubyGems.
func (b *RakeBuilder) ensureRakeAvailable(ctx context.Context, config *BuildConfig) ([]MissingDependency, error) {
if _, err := execLookPath("rake"); err == nil {
return nil, nil
}
rubyPath := config.RubyPath
if rubyPath == "" {
rubyPath = rubyCommand
}
if _, err := execLookPath(rubyPath); err != nil {
return nil, fmt.Errorf("ruby executable %q not found while checking for rake: %w", rubyPath, err)
}
script := strings.Join([]string{
"begin",
" Gem.bin_path(\"rake\", \"rake\")",
" exit 0",
"rescue Gem::LoadError, Gem::GemNotFoundException, Gem::Exception",
" exit 1",
"end",
}, "\n")
cmd := execCommandContext(ctx, rubyPath, "-rrubygems", "-e", script)
env := append([]string{}, cmd.Env...)
if len(env) == 0 {
env = os.Environ()
}
for key, value := range config.Env {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
if config.RubyPath != "" {
rubyDir := filepath.Dir(config.RubyPath)
currentPath := os.Getenv("PATH")
env = append(env, fmt.Sprintf("PATH=%s:%s", rubyDir, currentPath))
}
cmd.Env = env
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return []MissingDependency{{Name: "rake"}}, fmt.Errorf("rake not found")
}
return nil, fmt.Errorf("failed to verify rake availability: %w", err)
}
return nil, nil
}