Skip to content

Instantly share code, notes, and snippets.

@scivision
Last active March 30, 2026 04:02
Show Gist options
  • Select an option

  • Save scivision/44a293da0ead6694eaf1257186f7a923 to your computer and use it in GitHub Desktop.

Select an option

Save scivision/44a293da0ead6694eaf1257186f7a923 to your computer and use it in GitHub Desktop.
macOS DYLD_* environment variable wrapper script example
.buildtool/
bin/
lib/

macOS DYLD_LIBRARY_PATH wrapper for MATLAB system()

This repository demonstrates a specific macOS behavior:

  1. A small executable is linked against a local .dylib.
  2. Running that executable directly fails because the library is not in the default dyld search path and no rpath is embedded.
  3. Running the same executable through a shell wrapper succeeds because the wrapper restores DYLD_LIBRARY_PATH from a non-DYLD_ environment variable.

On Linux or Windows, steps 1 and 2 are the same, except that the environment variable LD_LIBRARY_PATH or PATH respectively isn't blocked by the OS, but if it isn't present, the run would fail due to missing dynamic library.

The runtime concept is compiler-agnostic. The build now supports either Apple Clang or a Homebrew GCC toolchain on macOS so the same dyld behavior can be demonstrated with either compiler.

Environment variables that start with DYLD_ are restricted by macOS Hardened Runtime. In hardened contexts, those variables can be stripped before a child process sees them. This demo passes dummy_LIBRARY_PATH from MATLAB and remaps it to DYLD_LIBRARY_PATH inside macos_env_wrapper.sh.

Files

  • buildfile.m: Matlab buildtool plan (Matlab's built-in build system)
  • libhello.c: tiny dynamic library
  • main.c: executable linked against libhello.dylib
  • macos_env_wrapper.sh: maps dummy_LIBRARY_PATH -> DYLD_LIBRARY_PATH and execs child
  • main.m: MATLAB demo that runs with and without wrapper

Tasks

The buildfile exposes explicit compiler-specific tasks.

  • build:clang builds with Clang
  • build:gcc builds with GCC
  • run:wrapper:clang runs the Clang-built executable through the macOS wrapper
  • run:wrapper:gcc runs the GCC-built executable through the macOS wrapper
  • run:exe:clang runs the Clang-built executable directly (expected to fail on macOS)
  • run:exe:gcc runs the GCC-built executable directly (expected to fail on macOS)

Run

From MATLAB in this project directory:

main

or with Matlab's build system

buildtool run

Expected behavior:

  • First execution of bin/main_dylib fails because dyld cannot find lib/libhello.dylib
  • Second execution through macos_env_wrapper.sh succeeds because the wrapper reconstructs DYLD_LIBRARY_PATH

Notes

  • The build intentionally does not embed an Rpath into the executable.
  • The library is built with an install name of lib/libhello.dylib.
  • The interesting part of the demo is runtime library discovery, not compiler-specific code generation.
% a minimal buildfile for macOS to demo dynamic library loading
function plan = buildfile
plan = buildplan(localfunctions);
plan('clean') = matlab.buildtool.tasks.CleanTask();
cwd = fileparts(mfilename('fullpath'));
libdir = fullfile(cwd, 'lib');
bindir = fullfile(cwd, 'bin');
if ismac()
shared_suffix = ".dylib";
exe_suffix = "";
elseif ispc()
% here we assume MinGW
assert(isenv('MW_MINGW64_LOC'), 'set environment variable MW_MINGW64_LOC to MinGW path')
shared_suffix = ".dll";
exe_suffix = ".exe";
else
shared_suffix = ".so";
exe_suffix = "";
end
plan('build:clang') = matlab.buildtool.Task(Actions=@(context) buildWithCompiler(context, "clang"), ...
Inputs = fullfile(cwd, ["libhello.c", "main.c"]), ...
Outputs = [fullfile(libdir, "libhello_clang" + shared_suffix), fullfile(bindir, "clang_dylib" + exe_suffix)], ...
Description="Builds the dynamic library and executable with Clang");
plan('build:gcc') = matlab.buildtool.Task(Actions=@(context) buildWithCompiler(context, "gcc"), ...
Inputs = plan("build:clang").Inputs, ...
Outputs = [fullfile(libdir, "libhello_gcc" + shared_suffix), fullfile(bindir, "gcc_dylib" + exe_suffix)], ...
Description="Builds the dynamic library and executable with GCC");
plan('run:exe:clang') = matlab.buildtool.Task(Inputs = plan('build:clang').Outputs, ...
Dependencies = "build:clang", Actions = @runExe, DisableIncremental=true, ...
Description = "Runs the exe built with Clang.");
plan('run:exe:gcc') = matlab.buildtool.Task(Inputs = plan('build:gcc').Outputs, ...
Dependencies = "build:gcc", Actions = @runExe, DisableIncremental=true, ...
Description = "Runs the exe built with GCC.");
if ismac()
plan('run:wrapper:clang') = matlab.buildtool.Task(Inputs = plan('build:clang').Outputs, ...
Dependencies = "build:clang", Actions = @runWrapper, DisableIncremental=true, ...
Description = "Runs the wrapper using exe built with Clang.");
plan('run:wrapper:gcc') = matlab.buildtool.Task(Inputs = plan('build:gcc').Outputs, ...
Dependencies = "build:gcc", Actions = @runWrapper, DisableIncremental=true, ...
Description = "Runs the wrapper using exe built with GCC.");
end
end
function buildWithCompiler(context, compiler_mode)
lib_src = context.Task.Inputs(1).Path;
exe_src = context.Task.Inputs(2).Path;
exe = context.Task.Outputs(2).Path;
bindir = fileparts(exe);
incdir = fileparts(bindir);
if ~isfolder(bindir), mkdir(bindir), end
lib = context.Task.Outputs(1).Path;
[libdir, libname, ext] = fileparts(lib);
if ~isfolder(libdir), mkdir(libdir), end
shared_prefix = "lib";
libstem = extractAfter(libname, shared_prefix);
if ismac()
shared_flag = "-dynamiclib";
shared_name_flag = "-install_name";
pic = "-fPIC";
shell = "";
elseif ispc()
% here we assume MinGW
shared_flag = "-shared";
shared_name_flag = "--out-implib";
pic = "";
libname = libname + ".a"; % MinGW needs an import library for linking
shell = "set PATH=" + fullfile(getenv('MW_MINGW64_LOC'), "bin") + pathsep + getenv("PATH") + " && ";
else
shared_flag = "-shared";
shared_name_flag = "-soname";
pic = "-fPIC";
shell = "";
end
exe_suffix = "";
compiler = resolveCompiler(compiler_mode);
fprintf("Building demo with %s (%s)\n", compiler_mode, compiler)
lib_cmd = sprintf('%s%s %s %s -I%s -o %s %s "-Wl,%s,%s%s"', ...
shell, compiler, shared_flag, pic, incdir, lib, lib_src, shared_name_flag, libname, ext);
exe_cmd = sprintf('%s%s -o %s %s -L%s -l%s', ...
shell, compiler, exe + exe_suffix, exe_src, libdir, libstem);
disp(lib_cmd)
[s, m] = system(lib_cmd);
assert(s == 0, "Failed to build library error %d with %s:\n%s\n%s", s, compiler, m, lib_cmd);
disp(exe_cmd)
[s, m] = system(exe_cmd);
assert(s == 0, "Failed to build executable with %s:\n%s\n%s", compiler, m, exe_cmd);
end
function runExe(context)
exe = context.Task.Inputs(2).Path;
if ismac()
envn = "DYLD_LIBRARY_PATH";
elseif ispc()
envn = "PATH";
else
envn = "LD_LIBRARY_PATH";
end
v = getenv(envn);
% commenting next line causes any OS to fail on run because the library won't be found.
v = fileparts(context.Task.Inputs(1).Path) + pathsep + v;
env = {envn, v};
[s, m] = system(exe, env{:});
if ismac()
assert(s ~= 0, "ERROR: " + m)
% must fail on macOS because DYLD_LIBRARY_PATH is blocked
% and RPATH is not set in the executable
else
assert(s == 0, "ERROR: " + m)
end
disp(m)
end
function runWrapper(context)
exe = context.Task.Inputs(2).Path;
dummy_name = "dummy_LIBRARY_PATH";
libdir = fileparts(context.Task.Inputs(1).Path);
wrapper = fullfile(fileparts(libdir), "macos_env_wrapper.sh");
env = {dummy_name, libdir};
run_cmd = sprintf('%s %s', wrapper, exe);
disp(run_cmd)
[s, m] = system(run_cmd, env{:});
assert(s == 0, sprintf("Failed to run executable:\n%s\n%s", m, run_cmd))
assert(strip(m) == "hello from dynamic-linked libhello", m)
disp(m)
end
function compiler = resolveCompiler(requested_mode)
switch lower(requested_mode)
case "clang"
compiler = 'clang';
case "gcc"
compiler = findGcc();
assert(strlength(compiler) ~= 0, "GCC executable not found.")
otherwise
compiler = requested_mode;
end
assert(commandExists(compiler), "Compiler not found: %s", compiler)
end
function compiler = findGcc()
compiler = '';
if ismac()
[stat, prefix] = system('brew --prefix gcc');
if stat == 0
prefix = strip(prefix);
gcc_path = fullfile(prefix, 'bin');
cs = dir(fullfile(gcc_path, 'gcc-*'));
for c = cs.'
if matches(c.name, "gcc-" + digitsPattern)
compiler = fullfile(gcc_path, c.name);
if commandExists(compiler)
return
else
compiler = '';
end
end
end
end
elseif ispc()
mp = getenv('MW_MINGW64_LOC');
gp = fullfile(mp, "bin/gcc.exe");
if isfile(gp)
compiler = gp;
else
compiler = "gcc.exe";
end
else
compiler = 'gcc';
end
end
function tf = commandExists(command)
if contains(command, filesep)
tf = isfile(command);
return
end
[status, ~] = system(sprintf('command -v %s >/dev/null 2>&1', command));
tf = status == 0;
end
# compare Matlab buildtool build with CMake build
#
# cmake -B build -G Ninja
#
# cmake --build build --verbose -- -t compdb-targets main
cmake_minimum_required(VERSION 3.20)
project(cmake_build_comparison LANGUAGES C)
add_library(hello SHARED libhello.c)
target_include_directories(hello PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(main main.c)
target_link_libraries(main PRIVATE hello)
file(GENERATE OUTPUT .gitignore CONTENT "*")
#include "libhello.h"
const char *hello(void) {
return "hello from dynamic-linked libhello";
}
const char *hello(void);
#!/bin/sh
# this wrapper is needed to workaround the censoring of DYLD_LIBRARY_PATH by SIP hardened runtime on macOS.
# Any environment variable starting with the name "DYLD" will be blocked. This boilerplate shell script
# technique can be used on any Unix-like shell where an environment variable is blocked by the system,
# but you want to pass it through to a child process
if [ -z "$dummy_LIBRARY_PATH" ]; then
echo "Error: dummy_LIBRARY_PATH is not set." >&2
exit 1
fi
DYLD_LIBRARY_PATH="$dummy_LIBRARY_PATH:$DYLD_LIBRARY_PATH" exec "$@"
#include <stdio.h>
#include <stdlib.h>
#include "libhello.h"
int main(void) {
printf("%s\n", hello());
return EXIT_SUCCESS;
}
% first we build the ./bin/ executable
buildtool("build:clang")
% now show the example
env = {'dummy_LIBRARY_PATH', './lib'};
exe = "./bin/clang_dylib";
wrapper = "./macos_env_wrapper.sh";
cmd = wrapper + " " + exe;
disp(cmd)
[stat, msg] = system(cmd, env{:});
assert(stat==0, msg)
fprintf('ok: %s', msg);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment