diff --git a/pkg/sandbox/runx/impl/registry/artifact.go b/pkg/sandbox/runx/impl/registry/artifact.go index 59ed0b4f..647eea7a 100644 --- a/pkg/sandbox/runx/impl/registry/artifact.go +++ b/pkg/sandbox/runx/impl/registry/artifact.go @@ -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 { diff --git a/pkg/sandbox/runx/impl/registry/extract.go b/pkg/sandbox/runx/impl/registry/extract.go index d0537f47..143cfb71 100644 --- a/pkg/sandbox/runx/impl/registry/extract.go +++ b/pkg/sandbox/runx/impl/registry/extract.go @@ -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" @@ -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 +} diff --git a/pkg/sandbox/runx/impl/registry/registry.go b/pkg/sandbox/runx/impl/registry/registry.go index 19a341d2..61c92495 100644 --- a/pkg/sandbox/runx/impl/registry/registry.go +++ b/pkg/sandbox/runx/impl/registry/registry.go @@ -1,6 +1,7 @@ package registry import ( + "bytes" "context" "os" "path/filepath" @@ -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 } @@ -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 + } +} diff --git a/pkg/sandbox/runx/impl/registry/registry_test.go b/pkg/sandbox/runx/impl/registry/registry_test.go new file mode 100644 index 00000000..2dac7457 --- /dev/null +++ b/pkg/sandbox/runx/impl/registry/registry_test.go @@ -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) + } + }) + } +}