Commit 76b6a637 authored by Amos Wenger's avatar Amos Wenger

Add naked executable support on macOS, cf. https://github.com/itchio/itch/issues/2131

parent 3341da9a
Pipeline #12679 failed with stage
in 33 seconds
......@@ -12,33 +12,57 @@ import (
)
type appRunner struct {
params *RunnerParams
params RunnerParams
target *MacLaunchTarget
simpleRunner Runner
}
var _ Runner = (*appRunner)(nil)
func newAppRunner(params *RunnerParams) (Runner, error) {
func newAppRunner(params RunnerParams) (Runner, error) {
target, err := PrepareMacLaunchTarget(params)
if err != nil {
return nil, errors.WithStack(err)
}
params.FullTargetPath = target.Path
ar := &appRunner{
params: params,
target: target,
}
if !target.IsAppBundle {
ar.simpleRunner, err = newSimpleRunner(params)
if err != nil {
return nil, errors.WithStack(err)
}
}
return ar, nil
}
func (ar *appRunner) Prepare() error {
if ar.simpleRunner {
return ar.simpleRunner.Prepare()
}
// nothing to prepare
return nil
}
func (ar *appRunner) Run() error {
params := ar.params
if ar.simpleRunner {
params.Consumer.Infof("Mac app runner here, delegating run to simple runner")
return ar.simpleRunner.Run()
}
return RunAppBundle(
params,
params.FullTargetPath,
ar.target.Path,
)
}
func RunAppBundle(params *RunnerParams, bundlePath string) error {
func RunAppBundle(params RunnerParams, bundlePath string) error {
consumer := params.Consumer
var args = []string{
......
......@@ -7,6 +7,6 @@ import (
"runtime"
)
func newAppRunner(params *RunnerParams) (Runner, error) {
func newAppRunner(params RunnerParams) (Runner, error) {
return nil, fmt.Errorf("app runner: not supported on %s", runtime.GOOS)
}
......@@ -2,6 +2,6 @@
package runner
func getAttachRunner(params *RunnerParams) (Runner, error) {
func getAttachRunner(params RunnerParams) (Runner, error) {
return nil, nil
}
......@@ -12,7 +12,7 @@ import (
"github.com/pkg/errors"
)
func getAttachRunner(params *RunnerParams) (Runner, error) {
func getAttachRunner(params RunnerParams) (Runner, error) {
consumer := params.Consumer
snapshot, err := syscallex.CreateToolhelp32Snapshot(
......@@ -84,7 +84,7 @@ func getAttachRunner(params *RunnerParams) (Runner, error) {
}
type attachRunner struct {
params *RunnerParams
params RunnerParams
pid uint32
}
......
......@@ -14,12 +14,12 @@ import (
)
type firejailRunner struct {
params *RunnerParams
params RunnerParams
}
var _ Runner = (*firejailRunner)(nil)
func newFirejailRunner(params *RunnerParams) (Runner, error) {
func newFirejailRunner(params RunnerParams) (Runner, error) {
if params.FirejailParams.BinaryPath == "" {
return nil, errors.Errorf("FirejailParams.BinaryPath must be set")
}
......
......@@ -8,6 +8,6 @@ import (
"github.com/pkg/errors"
)
func newFirejailRunner(params *RunnerParams) (Runner, error) {
func newFirejailRunner(params RunnerParams) (Runner, error) {
return nil, errors.Errorf("firejail runner is not implemented on %s", runtime.GOOS)
}
......@@ -7,6 +7,6 @@ import (
"runtime"
)
func newFujiRunner(params *RunnerParams) (Runner, error) {
func newFujiRunner(params RunnerParams) (Runner, error) {
return nil, fmt.Errorf("fuji runner: not supported on %s", runtime.GOOS)
}
......@@ -16,14 +16,14 @@ import (
)
type fujiRunner struct {
params *RunnerParams
params RunnerParams
fi fuji.Instance
credentials *fuji.Credentials
}
var _ Runner = (*fujiRunner)(nil)
func newFujiRunner(params *RunnerParams) (Runner, error) {
func newFujiRunner(params RunnerParams) (Runner, error) {
if params.FujiParams.Settings == nil {
return nil, errors.Errorf("FujiParams.Instance should be set")
}
......
package runner
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
type MacLaunchTarget struct {
Path string
IsAppBundle bool
}
func (t *MacLaunchTarget) String() string {
kind := "naked executable"
if t.IsAppBundle {
kind = "app bundle"
}
return fmt.Sprintf("(%s) [%s]", t.Path, kind)
}
// PrepareMacLaunchTarget looks at a path and tries to figure out if
// it's a mac app bundle, an executable inside of a mac app bundle,
// or just as naked executable.
func PrepareMacLaunchTarget(params RunnerParams) (*MacLaunchTarget, error) {
consumer := params.Consumer
target := &MacLaunchTarget{
Path: params.FullTargetPath,
}
stats, err := os.Stat(params.FullTargetPath)
if err != nil {
return nil, errors.WithMessage(err, "while preparing mac launch target")
}
if stats.IsDir() {
if PathLooksLikeAppBundle(target.Path) {
consumer.Infof("(%s) is a directory and ends with .app - looks like an app bundle alright.", target.Path)
target.IsAppBundle = true
return target, nil
}
return nil, errors.New("(%s) is a directory but does not in .app - doesn't look like an app bundle")
}
{
currentPath := target.Path
for currentPath != params.InstallFolder {
nextPath := filepath.Dir(currentPath)
if nextPath == currentPath {
break
}
if PathLooksLikeAppBundle(nextPath) {
target.Path = nextPath
target.IsAppBundle = true
consumer.Infof("(%s) looks like the real bundle, using that.", target.Path)
return target, nil
}
currentPath = nextPath
}
}
consumer.Infof("(%s) assumed naked executable (not an app bundle and not contained in an app bundle)", target.Path)
return target, nil
}
func PathLooksLikeAppBundle(dir string) bool {
return strings.HasSuffix(strings.ToLower(dir), ".app")
}
package runner_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/itchio/smaug/runner"
"github.com/itchio/wharf/state"
"github.com/itchio/wharf/wtest"
"github.com/stretchr/testify/assert"
)
func Test_PrepareMacLaunchTarget(t *testing.T) {
assert := assert.New(t)
installFolder, err := ioutil.TempDir("", "install-folder")
wtest.Must(t, err)
defer os.RemoveAll(installFolder)
t.Logf("Regular app bundle")
bundlePath := filepath.Join(installFolder, "Foobar.app")
wtest.Must(t, os.MkdirAll(bundlePath, 0755))
consumer := &state.Consumer{
OnMessage: func(lvl string, msg string) {
t.Logf("[%s] %s", lvl, msg)
},
}
params := runner.RunnerParams{
Consumer: consumer,
FullTargetPath: bundlePath,
InstallFolder: installFolder,
}
target, err := runner.PrepareMacLaunchTarget(params)
assert.NoError(err)
assert.EqualValues(bundlePath, target.Path)
assert.True(target.IsAppBundle)
machoHeader := []byte{0xfe, 0xed, 0xfa, 0xcf}
t.Logf("Naked executable (not in bundle)")
nakedExecPath := filepath.Join(installFolder, "utilities", "x86_64", "bin", "jtool")
wtest.Must(t, os.MkdirAll(filepath.Dir(nakedExecPath), 0755))
wtest.Must(t, ioutil.WriteFile(nakedExecPath, machoHeader, 0755))
params.FullTargetPath = nakedExecPath
target, err = runner.PrepareMacLaunchTarget(params)
assert.NoError(err)
assert.EqualValues(nakedExecPath, target.Path)
assert.False(target.IsAppBundle)
t.Logf("Nested executable (in bundle)")
nestedExecPath := filepath.Join(bundlePath, "Contents", "MacOS", "crabapple-launcher")
wtest.Must(t, os.MkdirAll(filepath.Dir(nestedExecPath), 0755))
wtest.Must(t, ioutil.WriteFile(nestedExecPath, machoHeader, 0755))
params.FullTargetPath = nestedExecPath
target, err = runner.PrepareMacLaunchTarget(params)
assert.NoError(err)
assert.EqualValues(bundlePath, target.Path)
assert.True(target.IsAppBundle)
}
......@@ -55,7 +55,7 @@ type Runner interface {
Run() error
}
func GetRunner(params *RunnerParams) (Runner, error) {
func GetRunner(params RunnerParams) (Runner, error) {
consumer := params.Consumer
attachRunner, err := getAttachRunner(params)
......
......@@ -19,15 +19,24 @@ import (
var investigateSandbox = os.Getenv("INVESTIGATE_SANDBOX") == "1"
type sandboxExecRunner struct {
params *RunnerParams
params RunnerParams
target *MacLaunchTarget
}
var _ Runner = (*sandboxExecRunner)(nil)
func newSandboxExecRunner(params *RunnerParams) (Runner, error) {
func newSandboxExecRunner(params RunnerParams) (Runner, error) {
target, err := PrepareMacLaunchTarget(params)
if err != nil {
return nil, errors.WithStack(err)
}
params.FullTargetPath = target.Path
ser := &sandboxExecRunner{
params: params,
target: target,
}
return ser, nil
}
......@@ -47,20 +56,13 @@ func (ser *sandboxExecRunner) Prepare() error {
return nil
}
func (ser *sandboxExecRunner) Run() error {
params := ser.params
consumer := params.Consumer
func (ser *sandboxExecRunner) SandboxProfilePath() string {
return filepath.Join(params.InstallFolder, ".itch", "isolate-app.sb")
}
consumer.Infof("Creating shim app bundle to enable sandboxing")
realBundlePath := params.FullTargetPath
func (ser *sandboxExecRunner) WriteSandboxProfile() error {
sandboxProfilePath := ser.SandboxProfilePath()
binaryPath, err := macox.GetExecutablePath(realBundlePath)
if err != nil {
return errors.WithStack(err)
}
binaryName := filepath.Base(binaryPath)
sandboxProfilePath := filepath.Join(params.InstallFolder, ".itch", "isolate-app.sb")
consumer.Opf("Writing sandbox profile to (%s)", sandboxProfilePath)
err = os.MkdirAll(filepath.Dir(sandboxProfilePath), 0755)
if err != nil {
......@@ -90,6 +92,51 @@ func (ser *sandboxExecRunner) Run() error {
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (ser *sandboxExecRunner) Run() error {
params := ser.params
consumer := params.Consumer
err = ser.WriteSandboxProfile()
if err != nil {
return errors.WithStack(err)
}
if !ser.target.IsAppBundle {
consumer.Infof("Dealing with naked executable, launching via sandbox-exec directly")
args := []string{
"sandbox-exec"
"-f"
ser.SandboxProfilePath(),
params.FullTargetPath,
}
args = append(args, params.Args...)
simpleParams := params
simpleParams.Args = args
simpleRunner, err := newSimpleRunner(simpleParams)
if err != nil {
return errors.WithStack(err)
}
err = simpleRunner.Prepare()
if err != nil {
return errors.WithStack(err)
}
return simpleRunner.Run()
}
consumer.Infof("Creating shim app bundle to enable sandboxing")
realBundlePath := params.FullTargetPath
binaryPath, err := macox.GetExecutablePath(realBundlePath)
if err != nil {
return errors.WithStack(err)
}
binaryName := filepath.Base(binaryPath)
workDir, err := ioutil.TempDir("", "butler-shim-bundle")
if err != nil {
......@@ -119,7 +166,7 @@ func (ser *sandboxExecRunner) Run() error {
sandbox-exec -f "%s" "%s" "$@"
`,
params.Dir,
sandboxProfilePath,
ser.SandboxProfilePath(),
binaryPath,
)
......
......@@ -7,6 +7,6 @@ import (
"runtime"
)
func newSandboxExecRunner(params *RunnerParams) (Runner, error) {
func newSandboxExecRunner(params RunnerParams) (Runner, error) {
return nil, fmt.Errorf("sandbox-exec runner: not supported on %s", runtime.GOOS)
}
......@@ -9,12 +9,12 @@ import (
)
type simpleRunner struct {
params *RunnerParams
params RunnerParams
}
var _ Runner = (*simpleRunner)(nil)
func newSimpleRunner(params *RunnerParams) (Runner, error) {
func newSimpleRunner(params RunnerParams) (Runner, error) {
sr := &simpleRunner{
params: params,
}
......
......@@ -9,12 +9,12 @@ import (
)
type simpleRunner struct {
params *RunnerParams
params RunnerParams
}
var _ Runner = (*simpleRunner)(nil)
func newSimpleRunner(params *RunnerParams) (Runner, error) {
func newSimpleRunner(params RunnerParams) (Runner, error) {
sr := &simpleRunner{
params: params,
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment