Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions pkg/sandbox/runx/impl/registry/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ import (
)

func findArtifactForPlatform(artifacts []types.ArtifactMetadata, platform types.Platform) *types.ArtifactMetadata {
var artifactForPlatform types.ArtifactMetadata
for _, artifact := range artifacts {
if isArtifactForPlatform(artifact, platform) && isKnownArchive(artifact.Name) {
// We only consider known artchives because sometimes releases contain multiple files
// for the same platform. Some times those files are alternative installation methods
// like `.dmg`, `.msi`, or `.deb`, and sometimes they are metadata files like `.sha256`
// or a `.sig` file. We don't want to install those.
return &artifact
if isArtifactForPlatform(artifact, platform) {
artifactForPlatform = artifact
if isKnownArchive(artifact.Name) {
// We only consider known archives because sometimes releases contain multiple files
// for the same platform. Some times those files are alternative installation methods
// like `.dmg`, `.msi`, or `.deb`, and sometimes they are metadata files like `.sha256`
// or a `.sig` file. We don't want to install those.
return &artifact
}
}
}
return nil
// Best attempt:
return &artifactForPlatform
}

func isArtifactForPlatform(artifact types.ArtifactMetadata, platform types.Platform) bool {
Expand Down
23 changes: 23 additions & 0 deletions pkg/sandbox/runx/impl/registry/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package registry

import (
"context"
"errors"
"os"
"path/filepath"
"strings"

"github.com/codeclysm/extract"
"go.jetpack.io/pkg/sandbox/runx/impl/fileutil"
Expand Down Expand Up @@ -53,3 +55,24 @@ func contentDir(path string) string {
}
return filepath.Join(path, contents[0].Name())
}

func createSymbolicLink(src, dst, repoName string) error {
if err := os.MkdirAll(dst, 0700); err != nil {
return err
}
if err := os.Chmod(src, 0755); err != nil {
return err
}
binaryName := filepath.Base(src)
// This is a good guess for the binary name. In the future we could allow
// user to customize.
if strings.Contains(binaryName, repoName) {
binaryName = repoName
}
err := os.Symlink(src, filepath.Join(dst, binaryName))
if errors.Is(err, os.ErrExist) {
// TODO: verify symlink points to the right place
return nil
}
return err
}
41 changes: 40 additions & 1 deletion pkg/sandbox/runx/impl/registry/registry.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package registry

import (
"bytes"
"context"
"os"
"path/filepath"
Expand Down Expand Up @@ -118,7 +119,11 @@ func (r *Registry) GetPackage(ctx context.Context, ref types.PkgRef, platform ty
return "", err
}

err = Extract(ctx, artifactPath, installPath.String())
if isKnownArchive(filepath.Base(artifactPath)) {
err = Extract(ctx, artifactPath, installPath.String())
} else if isExecutableBinary(artifactPath) {
err = createSymbolicLink(artifactPath, installPath.String(), resolvedRef.Repo)
}
if err != nil {
return "", err
}
Expand Down Expand Up @@ -160,3 +165,37 @@ func (r *Registry) ResolveVersion(ref types.PkgRef) (types.PkgRef, error) {
Version: latestVersion,
}, nil
}

// Best effort heuristic to determine if the artifact is an executable binary.
func isExecutableBinary(path string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer file.Close()

header := make([]byte, 4)
_, err = file.Read(header)
if err != nil {
return false
}

switch {
case bytes.HasPrefix(header, []byte("#!")): // Shebang
return true
case bytes.HasPrefix(header, []byte{0x7f, 0x45}): // ELF
return true
case bytes.Equal(header, []byte{0xfe, 0xed, 0xfa, 0xce}): // MachO32 BE
return true
case bytes.Equal(header, []byte{0xfe, 0xed, 0xfa, 0xcf}): // MachO64 BE
return true
case bytes.Equal(header, []byte{0xca, 0xfe, 0xba, 0xbe}): // Java class
return true
case bytes.Equal(header, []byte{0xCF, 0xFA, 0xED, 0xFE}): // Little-endian mac 64-bit
return true
case bytes.Equal(header, []byte{0xCE, 0xFA, 0xED, 0xFE}): // Little-endian mac 32-bit
return true
default:
return false
}
}
44 changes: 44 additions & 0 deletions pkg/sandbox/runx/impl/registry/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package registry

import (
"os"
"testing"
)

func TestIsBinary(t *testing.T) {
tests := []struct {
name string
header []byte
want bool
}{
{"Shebang", []byte("#!/bin/bash\n"), true},
{"ELF", []byte{0x7f, 0x45, 0x4c, 0x46}, true},
{"MachO32 BE", []byte{0xfe, 0xed, 0xfa, 0xce}, true},
{"MachO64 BE", []byte{0xfe, 0xed, 0xfa, 0xcf}, true},
{"Java Class", []byte{0xca, 0xfe, 0xba, 0xbe}, true},
{"MachO64 LE", []byte{0xcf, 0xfa, 0xed, 0xfe}, true},
{"MachO32 LE", []byte{0xce, 0xfa, 0xed, 0xfe}, true},
{"Unknown", []byte{0xaa, 0xbb, 0xcc, 0xdd}, false},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
file, err := os.CreateTemp("", "testfile")
if err != nil {
t.Fatalf("Could not create temp file: %v", err)
}
defer os.Remove(file.Name())

_, err = file.Write(test.header)
if err != nil {
t.Fatalf("Could not write to temp file: %v", err)
}
file.Close()

got := isExecutableBinary(file.Name())
if got != test.want {
t.Errorf("isBinary() = %v, want %v", got, test.want)
}
})
}
}