Skip to content

Commit c7b1dbe

Browse files
authored
feat: add support for previously unsupported error (#11)
1 parent e404a54 commit c7b1dbe

File tree

4 files changed

+74
-8
lines changed

4 files changed

+74
-8
lines changed

extconf_builder.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path/filepath"
9+
"regexp"
910
"runtime"
1011
"strings"
1112
)
@@ -104,12 +105,16 @@ func (b *ExtConfBuilder) runExtConf(ctx context.Context, config *BuildConfig, ex
104105
}
105106

106107
if err != nil {
108+
// Parse output for missing dependencies
109+
result.MissingDependencies = b.parseLoadErrors(result.Output)
107110
return BuildError("ExtConf", result.Output, err)
108111
}
109112

110113
// Verify Makefile was created
111114
makefilePath := filepath.Join(extensionDir, "Makefile")
112115
if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
116+
// Parse output for missing dependencies even when Makefile wasn't generated
117+
result.MissingDependencies = b.parseLoadErrors(result.Output)
113118
return BuildError("ExtConf", result.Output, fmt.Errorf("makefile not generated"))
114119
}
115120

@@ -229,3 +234,58 @@ func (b *ExtConfBuilder) getMakeProgram() string {
229234
return "make"
230235
}
231236
}
237+
238+
// parseLoadErrors parses build output for missing dependencies.
239+
// It recognizes common Ruby error patterns:
240+
// - "cannot load such file -- gem_name"
241+
// - "Could not find 'gem_name' (~> 1.0)"
242+
// - "Gem::MissingSpecVersionError: gem_name requires version ~> 1.0"
243+
// - "LoadError: cannot load such file -- gem_name/subpath"
244+
func (b *ExtConfBuilder) parseLoadErrors(output []string) []MissingDependency {
245+
var deps []MissingDependency
246+
seen := make(map[string]bool)
247+
248+
// Regex patterns for different error formats
249+
patterns := []*regexp.Regexp{
250+
// "cannot load such file -- mini_portile2" or "cannot load such file -- mini_portile2/version"
251+
regexp.MustCompile(`cannot load such file -- ([a-zA-Z0-9_-]+)`),
252+
// "Could not find 'mini_portile2' (~> 2.8.2)"
253+
regexp.MustCompile(`Could not find '([^']+)'(?: \(([^)]+)\))?`),
254+
// "Gem::MissingSpecVersionError: mini_portile2 requires ~> 2.8.2"
255+
regexp.MustCompile(`Gem::MissingSpec(?:Version)?Error:?\s*([a-zA-Z0-9_-]+)(?:\s+requires?\s+(.+))?`),
256+
// "Bundler could not find compatible versions for gem \"mini_portile2\":"
257+
regexp.MustCompile(`compatible versions for gem "([^"]+)"`),
258+
// "mini_portile2 (~> 2.8.2) was resolved to 2.8.2"
259+
regexp.MustCompile(`^([a-zA-Z0-9_-]+) \(([^)]+)\) was resolved`),
260+
}
261+
262+
for _, line := range output {
263+
for i, re := range patterns {
264+
matches := re.FindStringSubmatch(line)
265+
if matches == nil {
266+
continue
267+
}
268+
269+
name := matches[1]
270+
if seen[name] {
271+
continue
272+
}
273+
seen[name] = true
274+
275+
dep := MissingDependency{Name: name}
276+
277+
// Extract version constraint if present
278+
if i == 1 && len(matches) > 2 && matches[2] != "" {
279+
dep.Constraint = matches[2]
280+
} else if i == 2 && len(matches) > 2 && matches[2] != "" {
281+
dep.Constraint = strings.TrimSpace(matches[2])
282+
} else if i == 4 && len(matches) > 2 {
283+
dep.Constraint = matches[2]
284+
}
285+
286+
deps = append(deps, dep)
287+
}
288+
}
289+
290+
return deps
291+
}

rake_builder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func (b *RakeBuilder) findBuiltExtensions(extensionDir string) ([]string, error)
294294
}
295295

296296
// ensureRakeAvailable verifies that rake can be executed either directly or via RubyGems.
297-
func (b *RakeBuilder) ensureRakeAvailable(ctx context.Context, config *BuildConfig) ([]string, error) {
297+
func (b *RakeBuilder) ensureRakeAvailable(ctx context.Context, config *BuildConfig) ([]MissingDependency, error) {
298298
if _, err := execLookPath("rake"); err == nil {
299299
return nil, nil
300300
}
@@ -336,7 +336,7 @@ func (b *RakeBuilder) ensureRakeAvailable(ctx context.Context, config *BuildConf
336336
if err := cmd.Run(); err != nil {
337337
var exitErr *exec.ExitError
338338
if errors.As(err, &exitErr) {
339-
return []string{"rake"}, fmt.Errorf("rake not found")
339+
return []MissingDependency{{Name: "rake"}}, fmt.Errorf("rake not found")
340340
}
341341
return nil, fmt.Errorf("failed to verify rake availability: %w", err)
342342
}

rake_builder_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func TestEnsureRakeAvailableMissingRake(t *testing.T) {
109109
t.Fatalf("expected rake not found error, got %v", err)
110110
}
111111

112-
expectedMissing := []string{"rake"}
112+
expectedMissing := []MissingDependency{{Name: "rake"}}
113113
if !reflect.DeepEqual(missing, expectedMissing) {
114114
t.Fatalf("expected missing dependencies %v, got %v", expectedMissing, missing)
115115
}

types.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ package rubyext
22

33
import "context"
44

5+
// MissingDependency represents a build-time dependency that was not found.
6+
type MissingDependency struct {
7+
Name string // Gem name (e.g., "mini_portile2")
8+
Constraint string // Version constraint if known (e.g., "~> 2.8.2"), empty if unknown
9+
}
10+
511
// BuildResult contains the output and status of a build operation.
612
//
713
// After a build completes, this structure provides:
@@ -10,11 +16,11 @@ import "context"
1016
// - Extensions list of compiled extension files (.so/.bundle/.dll)
1117
// - Error information if the build failed
1218
type BuildResult struct {
13-
Success bool // True if build completed successfully
14-
Output []string // Lines of output from the build process
15-
Extensions []string // Paths to built extension files
16-
Error error // Error if build failed, nil otherwise
17-
MissingDependencies []string // Names of build-time dependencies that were missing
19+
Success bool // True if build completed successfully
20+
Output []string // Lines of output from the build process
21+
Extensions []string // Paths to built extension files
22+
Error error // Error if build failed, nil otherwise
23+
MissingDependencies []MissingDependency // Build-time dependencies that were missing
1824
}
1925

2026
// BuildConfig contains configuration for the build process.

0 commit comments

Comments
 (0)