Skip to content

Conversation

nskobelevs
Copy link

@nskobelevs nskobelevs commented Sep 23, 2025

This is a fix for #78 which updates run.sh to use arch instead of exec when on Apple Silicon to allow the executable to run natively when the run script is invoked from a process running on Rosetta.

I've tested the script itself somewhat thoroughly with a custom exec and custom dylib for injection to confirm that both the executable is ran natively and there's no impact to environment variables. This was done with x86_64, arm64, and universal versions of the exec as well as laughing the script directly from a terminal running natively & via Rosetta and having another process run the script via exec'ing sh. In all cases the executable and dylib were loaded natively as arm64.

e.g. running a small x86_64 summon executable via Rosetta that just uses execvp to run the script.

❯ ./summon
[summon] running as x86_64
; original process is run via Rosetta ^

[inject.dylib] dylib injected in /Users/nskobelevs/Projects/dylib/main.app/Contents/MacOS/main, architecture = arm64
; the dylib gets loaded as arm64
[inject.dylib] DOORSTOP_ENABLED = 1
[inject.dylib] DOORSTOP_TARGET_ASSEMBLY = /Users/nskobelevs/Projects/dylib/Doorstop.dll
[inject.dylib] DOORSTOP_BOOT_CONFIG_OVERRIDE =
[inject.dylib] DOORSTOP_IGNORE_DISABLED_ENV = 0
[inject.dylib] DOORSTOP_MONO_DLL_SEARCH_PATH_OVERRIDE =
[inject.dylib] DOORSTOP_MONO_DEBUG_ENABLED = 0
[inject.dylib] DOORSTOP_MONO_DEBUG_ADDRESS = 127.0.0.1:10000
[inject.dylib] DOORSTOP_MONO_DEBUG_SUSPEND = 0
[inject.dylib] DOORSTOP_CLR_RUNTIME_CORECLR_PATH = .dylib
[inject.dylib] DOORSTOP_CLR_CORLIB_DIR =
[inject.dylib] LD_PRELOAD = inject.dylib
[inject.dylib] DYLD_INSERT_LIBRARIES = inject.dylib
; environment variables in the dylib are intact

[main] running as arm64
; executable itself ran natively
[main] DOORSTOP_ENABLED = 1
[main] DOORSTOP_TARGET_ASSEMBLY = /Users/nskobelevs/Projects/dylib/Doorstop.dll
[main] DOORSTOP_BOOT_CONFIG_OVERRIDE =
[main] DOORSTOP_IGNORE_DISABLED_ENV = 0
[main] DOORSTOP_MONO_DLL_SEARCH_PATH_OVERRIDE =
[main] DOORSTOP_MONO_DEBUG_ENABLED = 0
[main] DOORSTOP_MONO_DEBUG_ADDRESS = 127.0.0.1:10000
[main] DOORSTOP_MONO_DEBUG_SUSPEND = 0
[main] DOORSTOP_CLR_RUNTIME_CORECLR_PATH = .dylib
[main] DOORSTOP_CLR_CORLIB_DIR =
[main] LD_PRELOAD = inject.dylib
[main] DYLD_INSERT_LIBRARIES = inject.dylib
; environment variables for the main exec itself remain intact

Also tested practically with doorstop and BepInEx 5 running Silksong through Steam - the game launches natively with BepInEx loading all expected plugins meaning doorstop was successful.

The main thing I'm not sure about if any special handling is needed for the Special case: program is launched via Steam case - I've been laughing the game via Steam with launch options but I don't think it applied here, not sure if that might be only for Linux?

Comment on lines +152 to +158
# CPUs for Apple Silicon are in the format "Apple M.."
cpu_type="$(sysctl -n machdep.cpu.brand_string)"
case "${cpu_type}" in
Apple*)
is_apple_silicon=1
;;
esac
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that on Intel Macs this would be something like Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz.

I don't have an Intel Mac to test this but I've reached out to a mate with one to check the output as a sanity check

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe uname -m would work? Is there a world where this matches on Linux?

Copy link
Author

@nskobelevs nskobelevs Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uname shipped with MacOS is also a universal binary so it falls into the same pitfalls where it'll just run it via Rosetta and return x86_64.

Could run it via arch and ARCHPREFERENCES like the main executable but I'm not actually sure if the arch command shipped on Intel Macs behaves the same without throwing a fit. Will have someone who has an Intel Mac check this.

Is there a world where this matches on Linux?

My understanding is that machdep is an Apple only namespace, plus this is in the *Darwin*) branch so linux wouldn't reach here.


exec "$executable_path" "$@"
if [ -n "${is_apple_silicon}" ]; then
export ARCHPREFERENCE="arm64,x86_64"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From man arch:

ENVIRONMENT
     The environment variable ARCHPREFERENCE can be used to provide architecture order preferences.  It is checked before looking for the
     corresponding property list file.

...

ARCHPREFERENCE Values
  i386,x86_64,x86_64h,arm64,arm64e
    A specifier that matches any name.

This tells arch to prefer arm64 and only fallback to x86_64 if arm64 is not suitable for this executable.
This works as expected regardless of whether the executable is universal, arm64 or solely x86_64.

# the executable is universal, supporting both x86_64 and arm64, MacOs will still run it as x86_64
# if the parent process is running as x86.
# arch also strips the DYLD_INSERT_LIBRARIES env var so we have to pass that in manually
arch -e DYLD_INSERT_LIBRARIES=$DYLD_INSERT_LIBRARIES "$executable_path" "$@"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to find concrete docs on DYLD_INSERT_LIBRARIES being stripped here but my guess it's done so that you don't try to run an arm64 process with a x86_64 preload so the setting has to be explicit.

Without this the env var is null in the main exec and the dylib doesn't get loaded at all.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't $DYLD_INSERT_LIBRARIES be in quotes, or is there no chance of it having a space? Also I couldn't find docs on the -e switch from a quick search, is this macos-only?

Copy link
Author

@nskobelevs nskobelevs Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed: d4b21af

And yeah looks like -e is macOS only - the macOS arch version seems to have a bunch of other functionality. Here's the full dump of the man page.

From man arch:

     -e envname=value	     Assigns the given value to the named environment
			     variable in the environment that will be passed
			     to the command to be run.	Any existing
			     environment variable with the same name will be
			     replaced.

Copy link
Collaborator

@ManlyMarco ManlyMarco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine, but I have no way of testing it so multiple people will have to test this on Linux and MacOS before it can be merged.

Comment on lines +152 to +158
# CPUs for Apple Silicon are in the format "Apple M.."
cpu_type="$(sysctl -n machdep.cpu.brand_string)"
case "${cpu_type}" in
Apple*)
is_apple_silicon=1
;;
esac
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe uname -m would work? Is there a world where this matches on Linux?

# the executable is universal, supporting both x86_64 and arm64, MacOs will still run it as x86_64
# if the parent process is running as x86.
# arch also strips the DYLD_INSERT_LIBRARIES env var so we have to pass that in manually
arch -e DYLD_INSERT_LIBRARIES=$DYLD_INSERT_LIBRARIES "$executable_path" "$@"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't $DYLD_INSERT_LIBRARIES be in quotes, or is there no chance of it having a space? Also I couldn't find docs on the -e switch from a quick search, is this macos-only?

@nskobelevs
Copy link
Author

eb008b9 updates to calling arch via exec to entirely replace the process so the sh process isn't left around until the game exists.

Tested with a small summon binary which calls execvp with "/bin/sh", "./run.sh" So we have summon -> exec sh run.sh -> (exec) arch main

Without exec summon and main have different PIDs, switching to exec results in both having the same PID so process got entirely replaces. main remains running in native mode as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Executable isn't always run natively on Apple Silicon
2 participants