Skip to content

Commit 8a36461

Browse files
committed
Handle native installs in finalize step
1 parent f8e6b7e commit 8a36461

File tree

7 files changed

+439
-3
lines changed

7 files changed

+439
-3
lines changed

cargo_builder.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ func (b *CargoBuilder) Build(ctx context.Context, config *BuildConfig, extension
7070
return result, err
7171
}
7272

73+
finalized, err := finalizeNativeExtensions(config, extensionFile, extensionDir, result.Extensions)
74+
if err != nil {
75+
result.Error = err
76+
return result, err
77+
}
78+
79+
result.Extensions = finalized
7380
result.Success = true
7481
return result, nil
7582
}

common.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,14 @@ func runCommonBuild(ctx context.Context, config *BuildConfig, extensionFile stri
114114
return result, err
115115
}
116116

117+
finalized, err := finalizeNativeExtensions(config, extensionFile, extensionDir, extensions)
118+
if err != nil {
119+
result.Error = err
120+
return result, err
121+
}
122+
117123
// Success!
118-
result.Extensions = extensions
124+
result.Extensions = finalized
119125
result.Success = true
120126
return result, nil
121127
}

configure_builder.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ func (b *ConfigureBuilder) Build(ctx context.Context, config *BuildConfig, exten
7878
return result, err
7979
}
8080

81-
result.Extensions = extensions
81+
finalized, err := finalizeNativeExtensions(config, extensionFile, extensionDir, extensions)
82+
if err != nil {
83+
result.Error = err
84+
return result, err
85+
}
86+
87+
result.Extensions = finalized
8288
result.Success = true
8389
return result, nil
8490
}

extconf_builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ func (b *ExtConfBuilder) runExtConf(ctx context.Context, config *BuildConfig, ex
117117
}
118118

119119
// runMake executes make to compile the extension
120+
//
121+
//nolint:dupl // Similar to makefile builder runMake but tailored for extconf
120122
func (b *ExtConfBuilder) runMake(ctx context.Context, config *BuildConfig, extensionDir string, result *BuildResult) error {
121123
makeProgram := b.getMakeProgram()
122124

install.go

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
package rubyext
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
var nativeLibraryExtensions = map[string]struct{}{
14+
".so": {},
15+
".bundle": {},
16+
".dll": {},
17+
".dylib": {},
18+
}
19+
20+
// finalizeNativeExtensions copies compiled native libraries into the gem's lib directory structure
21+
// and returns their paths relative to the gem root. If no native libraries are present, the original
22+
// build outputs are returned relative to the gem root.
23+
func finalizeNativeExtensions(config *BuildConfig, extensionFile, extensionDir string, built []string) ([]string, error) {
24+
if len(built) == 0 {
25+
return nil, nil
26+
}
27+
28+
var hasNative bool
29+
for _, rel := range built {
30+
if isNativeLibrary(rel) {
31+
hasNative = true
32+
break
33+
}
34+
}
35+
36+
if !hasNative {
37+
return makeGemRelative(config.GemDir, extensionFile, built), nil
38+
}
39+
40+
primaryDest, extraDests := installTargets(config)
41+
if primaryDest == "" {
42+
return makeGemRelative(config.GemDir, extensionFile, built), nil
43+
}
44+
45+
var installed []string
46+
47+
for _, rel := range built {
48+
if !isNativeLibrary(rel) {
49+
continue
50+
}
51+
52+
srcPath := filepath.Join(extensionDir, rel)
53+
if info, err := os.Stat(srcPath); err != nil || !info.Mode().IsRegular() {
54+
continue
55+
}
56+
57+
relDest := determineInstallRelativePath(config.GemDir, extensionFile, rel)
58+
if relDest == "" {
59+
relDest = filepath.Base(rel)
60+
}
61+
62+
if err := copyFile(srcPath, filepath.Join(primaryDest, relDest)); err != nil {
63+
return nil, err
64+
}
65+
66+
for _, dest := range extraDests {
67+
if err := copyFile(srcPath, filepath.Join(dest, relDest)); err != nil {
68+
return nil, err
69+
}
70+
}
71+
72+
if relPath, err := filepath.Rel(config.GemDir, filepath.Join(primaryDest, relDest)); err == nil {
73+
installed = append(installed, filepath.ToSlash(relPath))
74+
} else {
75+
installed = append(installed, filepath.ToSlash(filepath.Join(primaryDest, relDest)))
76+
}
77+
}
78+
79+
return installed, nil
80+
}
81+
82+
func makeGemRelative(gemDir, extensionFile string, built []string) []string {
83+
var relPaths []string
84+
baseDir := filepath.Dir(extensionFile)
85+
86+
for _, rel := range built {
87+
full := filepath.Join(baseDir, rel)
88+
if gemDir != "" {
89+
if cleaned, err := filepath.Rel(gemDir, filepath.Join(gemDir, full)); err == nil {
90+
relPaths = append(relPaths, filepath.ToSlash(cleaned))
91+
continue
92+
}
93+
}
94+
relPaths = append(relPaths, filepath.ToSlash(full))
95+
}
96+
97+
return relPaths
98+
}
99+
100+
func isNativeLibrary(path string) bool {
101+
ext := strings.ToLower(filepath.Ext(path))
102+
_, ok := nativeLibraryExtensions[ext]
103+
return ok
104+
}
105+
106+
func installTargets(config *BuildConfig) (primary string, additional []string) {
107+
baseDirs := gatherBaseDirectories(config)
108+
if len(baseDirs) == 0 {
109+
return "", nil
110+
}
111+
112+
versionDir, useVersion := rubyVersionDirectory(config.RubyVersion)
113+
114+
for i, base := range baseDirs {
115+
target := base
116+
if useVersion {
117+
target = filepath.Join(base, versionDir)
118+
}
119+
120+
if i == 0 {
121+
primary = target
122+
} else {
123+
additional = append(additional, target)
124+
}
125+
126+
// Also copy to unversioned base for compatibility
127+
if useVersion {
128+
additional = append(additional, base)
129+
}
130+
}
131+
132+
additional = uniqueStrings(additional)
133+
return primary, additional
134+
}
135+
136+
func gatherBaseDirectories(config *BuildConfig) []string {
137+
var dirs []string
138+
139+
add := func(dir string) {
140+
if dir == "" {
141+
return
142+
}
143+
if !filepath.IsAbs(dir) && config.GemDir != "" {
144+
dir = filepath.Join(config.GemDir, dir)
145+
}
146+
dirs = append(dirs, filepath.Clean(dir))
147+
}
148+
149+
add(config.DestPath)
150+
add(config.LibDir)
151+
152+
if len(dirs) == 0 && config.GemDir != "" {
153+
add(filepath.Join(config.GemDir, "lib"))
154+
}
155+
156+
return uniqueStrings(dirs)
157+
}
158+
159+
func rubyVersionDirectory(version string) (string, bool) {
160+
major, minor, ok := parseRubyVersion(version)
161+
if !ok {
162+
return "", false
163+
}
164+
165+
if major > 3 || (major == 3 && minor >= 4) {
166+
return fmt.Sprintf("%d.%d", major, minor), true
167+
}
168+
169+
return "", false
170+
}
171+
172+
func determineInstallRelativePath(gemDir, extensionFile, builtRel string) string {
173+
suffix := filepath.Ext(builtRel)
174+
baseName := strings.TrimSuffix(filepath.Base(builtRel), suffix)
175+
176+
if module := moduleFromCreateMakefile(gemDir, extensionFile); module != "" {
177+
modulePath := filepath.FromSlash(module)
178+
if suffix != "" && !strings.HasSuffix(modulePath, suffix) {
179+
modulePath += suffix
180+
}
181+
return safeRelativePath(modulePath)
182+
}
183+
184+
if strings.HasSuffix(extensionFile, "extconf.rb") {
185+
relPath := strings.TrimPrefix(extensionFile, "ext/")
186+
relPath = strings.TrimSuffix(relPath, "/extconf.rb")
187+
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath))
188+
relPath = strings.Trim(relPath, "/\\")
189+
190+
if relPath != "" && !strings.HasSuffix(relPath, baseName) {
191+
relPath = filepath.Join(relPath, baseName)
192+
}
193+
194+
if relPath == "" {
195+
relPath = baseName
196+
}
197+
198+
if suffix != "" && !strings.HasSuffix(relPath, suffix) {
199+
relPath += suffix
200+
}
201+
202+
return safeRelativePath(relPath)
203+
}
204+
205+
relDir := strings.TrimPrefix(filepath.Dir(extensionFile), "ext/")
206+
if relDir == "" {
207+
relDir = baseName
208+
} else if !strings.HasSuffix(relDir, baseName) {
209+
relDir = filepath.Join(relDir, baseName)
210+
}
211+
212+
if suffix != "" && !strings.HasSuffix(relDir, suffix) {
213+
relDir += suffix
214+
}
215+
216+
return safeRelativePath(relDir)
217+
}
218+
219+
func moduleFromCreateMakefile(gemDir, extensionFile string) string {
220+
if !strings.HasSuffix(extensionFile, "extconf.rb") {
221+
return ""
222+
}
223+
224+
extconfPath := filepath.Join(gemDir, extensionFile)
225+
content, err := os.ReadFile(extconfPath)
226+
if err != nil {
227+
return ""
228+
}
229+
230+
patterns := []string{
231+
`create_makefile\s*\(\s*['"]([^'"]+)['"]`,
232+
`create_makefile\s+['"]([^'"]+)['"]`,
233+
}
234+
235+
for _, pattern := range patterns {
236+
re := regexp.MustCompile(pattern)
237+
if matches := re.FindStringSubmatch(string(content)); len(matches) > 1 {
238+
return matches[1]
239+
}
240+
}
241+
242+
return ""
243+
}
244+
245+
func copyFile(srcPath, destPath string) error {
246+
info, err := os.Stat(srcPath)
247+
if err != nil {
248+
return err
249+
}
250+
251+
dir := filepath.Dir(destPath)
252+
if mkErr := os.MkdirAll(dir, 0o755); mkErr != nil {
253+
return mkErr
254+
}
255+
256+
in, err := os.Open(srcPath)
257+
if err != nil {
258+
return err
259+
}
260+
defer in.Close()
261+
262+
out, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
263+
if err != nil {
264+
return err
265+
}
266+
267+
if _, err = io.Copy(out, in); err != nil {
268+
out.Close()
269+
return err
270+
}
271+
272+
return out.Close()
273+
}
274+
275+
func safeRelativePath(path string) string {
276+
clean := filepath.Clean(path)
277+
if clean == "." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
278+
return filepath.Base(path)
279+
}
280+
return clean
281+
}
282+
283+
func parseRubyVersion(version string) (major, minor int, ok bool) {
284+
parts := strings.Split(version, ".")
285+
if len(parts) < 2 {
286+
return 0, 0, false
287+
}
288+
289+
var err error
290+
major, err = strconv.Atoi(parts[0])
291+
if err != nil {
292+
return 0, 0, false
293+
}
294+
295+
minor, err = strconv.Atoi(parts[1])
296+
if err != nil {
297+
return 0, 0, false
298+
}
299+
300+
return major, minor, true
301+
}
302+
303+
func uniqueStrings(values []string) []string {
304+
seen := make(map[string]struct{})
305+
var result []string
306+
307+
for _, value := range values {
308+
if value == "" {
309+
continue
310+
}
311+
if _, ok := seen[value]; ok {
312+
continue
313+
}
314+
seen[value] = struct{}{}
315+
result = append(result, value)
316+
}
317+
318+
return result
319+
}

0 commit comments

Comments
 (0)