Skip to content

Commit 090d910

Browse files
authored
[runx] Allow non-archived binaries (#172)
## Summary Some repos (e.g. https://github.com/mvdan/gofumpt) release files as binaries instead of archives. This expands runx to treat unknown files (that still match the platform and architecture name structure) as binaries. It only does so if it finds a compatible artifact but the type is unknown. We could additionally test the mime type of the file, but it doesn't seem like binaries use consistent mime types. ## How was it tested? `./dist/runx +mvdan/gofumpt gofumpt --help`
1 parent 50620e2 commit 090d910

File tree

4 files changed

+119
-8
lines changed

4 files changed

+119
-8
lines changed

pkg/sandbox/runx/impl/registry/artifact.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ import (
99
)
1010

1111
func findArtifactForPlatform(artifacts []types.ArtifactMetadata, platform types.Platform) *types.ArtifactMetadata {
12+
var artifactForPlatform types.ArtifactMetadata
1213
for _, artifact := range artifacts {
13-
if isArtifactForPlatform(artifact, platform) && isKnownArchive(artifact.Name) {
14-
// We only consider known artchives because sometimes releases contain multiple files
15-
// for the same platform. Some times those files are alternative installation methods
16-
// like `.dmg`, `.msi`, or `.deb`, and sometimes they are metadata files like `.sha256`
17-
// or a `.sig` file. We don't want to install those.
18-
return &artifact
14+
if isArtifactForPlatform(artifact, platform) {
15+
artifactForPlatform = artifact
16+
if isKnownArchive(artifact.Name) {
17+
// We only consider known archives because sometimes releases contain multiple files
18+
// for the same platform. Some times those files are alternative installation methods
19+
// like `.dmg`, `.msi`, or `.deb`, and sometimes they are metadata files like `.sha256`
20+
// or a `.sig` file. We don't want to install those.
21+
return &artifact
22+
}
1923
}
2024
}
21-
return nil
25+
// Best attempt:
26+
return &artifactForPlatform
2227
}
2328

2429
func isArtifactForPlatform(artifact types.ArtifactMetadata, platform types.Platform) bool {

pkg/sandbox/runx/impl/registry/extract.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package registry
22

33
import (
44
"context"
5+
"errors"
56
"os"
67
"path/filepath"
8+
"strings"
79

810
"github.com/codeclysm/extract"
911
"go.jetpack.io/pkg/sandbox/runx/impl/fileutil"
@@ -53,3 +55,24 @@ func contentDir(path string) string {
5355
}
5456
return filepath.Join(path, contents[0].Name())
5557
}
58+
59+
func createSymbolicLink(src, dst, repoName string) error {
60+
if err := os.MkdirAll(dst, 0700); err != nil {
61+
return err
62+
}
63+
if err := os.Chmod(src, 0755); err != nil {
64+
return err
65+
}
66+
binaryName := filepath.Base(src)
67+
// This is a good guess for the binary name. In the future we could allow
68+
// user to customize.
69+
if strings.Contains(binaryName, repoName) {
70+
binaryName = repoName
71+
}
72+
err := os.Symlink(src, filepath.Join(dst, binaryName))
73+
if errors.Is(err, os.ErrExist) {
74+
// TODO: verify symlink points to the right place
75+
return nil
76+
}
77+
return err
78+
}

pkg/sandbox/runx/impl/registry/registry.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package registry
22

33
import (
4+
"bytes"
45
"context"
56
"os"
67
"path/filepath"
@@ -118,7 +119,11 @@ func (r *Registry) GetPackage(ctx context.Context, ref types.PkgRef, platform ty
118119
return "", err
119120
}
120121

121-
err = Extract(ctx, artifactPath, installPath.String())
122+
if isKnownArchive(filepath.Base(artifactPath)) {
123+
err = Extract(ctx, artifactPath, installPath.String())
124+
} else if isExecutableBinary(artifactPath) {
125+
err = createSymbolicLink(artifactPath, installPath.String(), resolvedRef.Repo)
126+
}
122127
if err != nil {
123128
return "", err
124129
}
@@ -160,3 +165,37 @@ func (r *Registry) ResolveVersion(ref types.PkgRef) (types.PkgRef, error) {
160165
Version: latestVersion,
161166
}, nil
162167
}
168+
169+
// Best effort heuristic to determine if the artifact is an executable binary.
170+
func isExecutableBinary(path string) bool {
171+
file, err := os.Open(path)
172+
if err != nil {
173+
return false
174+
}
175+
defer file.Close()
176+
177+
header := make([]byte, 4)
178+
_, err = file.Read(header)
179+
if err != nil {
180+
return false
181+
}
182+
183+
switch {
184+
case bytes.HasPrefix(header, []byte("#!")): // Shebang
185+
return true
186+
case bytes.HasPrefix(header, []byte{0x7f, 0x45}): // ELF
187+
return true
188+
case bytes.Equal(header, []byte{0xfe, 0xed, 0xfa, 0xce}): // MachO32 BE
189+
return true
190+
case bytes.Equal(header, []byte{0xfe, 0xed, 0xfa, 0xcf}): // MachO64 BE
191+
return true
192+
case bytes.Equal(header, []byte{0xca, 0xfe, 0xba, 0xbe}): // Java class
193+
return true
194+
case bytes.Equal(header, []byte{0xCF, 0xFA, 0xED, 0xFE}): // Little-endian mac 64-bit
195+
return true
196+
case bytes.Equal(header, []byte{0xCE, 0xFA, 0xED, 0xFE}): // Little-endian mac 32-bit
197+
return true
198+
default:
199+
return false
200+
}
201+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package registry
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestIsBinary(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
header []byte
12+
want bool
13+
}{
14+
{"Shebang", []byte("#!/bin/bash\n"), true},
15+
{"ELF", []byte{0x7f, 0x45, 0x4c, 0x46}, true},
16+
{"MachO32 BE", []byte{0xfe, 0xed, 0xfa, 0xce}, true},
17+
{"MachO64 BE", []byte{0xfe, 0xed, 0xfa, 0xcf}, true},
18+
{"Java Class", []byte{0xca, 0xfe, 0xba, 0xbe}, true},
19+
{"MachO64 LE", []byte{0xcf, 0xfa, 0xed, 0xfe}, true},
20+
{"MachO32 LE", []byte{0xce, 0xfa, 0xed, 0xfe}, true},
21+
{"Unknown", []byte{0xaa, 0xbb, 0xcc, 0xdd}, false},
22+
}
23+
24+
for _, test := range tests {
25+
t.Run(test.name, func(t *testing.T) {
26+
file, err := os.CreateTemp("", "testfile")
27+
if err != nil {
28+
t.Fatalf("Could not create temp file: %v", err)
29+
}
30+
defer os.Remove(file.Name())
31+
32+
_, err = file.Write(test.header)
33+
if err != nil {
34+
t.Fatalf("Could not write to temp file: %v", err)
35+
}
36+
file.Close()
37+
38+
got := isExecutableBinary(file.Name())
39+
if got != test.want {
40+
t.Errorf("isBinary() = %v, want %v", got, test.want)
41+
}
42+
})
43+
}
44+
}

0 commit comments

Comments
 (0)