Scripts for building Wine from source on macOS as an x86_64 binary and bundling it into a self-contained, relocatable distribution with WoW64 (i386+x86_64). Sources come from athei/wine, CrossOver Wine with custom patches on top. Direct3D does not go through wined3d by default: the bundle ships Apple's D3DMetal, DXMT and mtld3d as per-process selectable implementations, plus the small library that selects them.
./build-wine.sh # Step 1: compile (arch -x86_64)
./bundle-wine.sh --dest /path/to/out # Step 2: relocatable wine/ treebuild-wine.sh configures and compiles Wine into the build directory. It is
incremental; --clean wipes the build directory first, which is also the only
way to pick up changed configure flags. Before and after every build it makes
sure the sonames in config.h point at @loader_path/../../external/, since
config.status silently regenerates the file after a source update. It also
builds the two d3d9_test.exe binaries the bundle carries, and a second,
arm64ec+aarch64 configured tree that yields only libwinecrt0.a and
libntdll.a under dist/wine-arm64ec/lib/wine/aarch64-windows/: link
libraries for arm64ec builtins that CrossOver's arm64 Wine will load, nothing
runnable.
bundle-wine.sh turns the build output into a distributable wine/ tree:
staged make install, prefix flattened, dylibs copied into lib/external/
with @loader_path install names, the Direct3D implementations and
compatdb.so installed, then a verification pass that ends by booting a
throwaway prefix. --runtime-only skips the SDK files and the test binaries;
that is the flavor that goes into an application.
The d3d9_test.exe binaries land in lib/wine/tests/{i386,x86_64}-windows/,
outside the directories the loader searches, and are plain PEs. Wine's
dlls/d3d9/tests is the de-facto D3D9 conformance suite, so a consumer
(mtld3d's CI) can gate its own d3d9.dll against it with nothing but this
bundle.
| Variable | Default | Used by |
|---|---|---|
WINE_SRC |
../src |
build |
BUILD_DIR |
../build |
build, bundle |
MINGW_DIR |
/opt/llvm-mingw |
build, bundle |
CACHE_DIR |
../cache |
bundle |
- Apple Silicon with Rosetta, or an Intel Mac. Everything compiles under
arch -x86_64. - x86_64 Homebrew in
/usr/local/(not the ARM one in/opt/homebrew/):freetype gnutls sdl2-compat sdl3, plusbison3.0 or newer andpkgconf. - llvm-mingw at
MINGW_DIRfor the PE modules. Itsbin/is appended toPATH, never prepended: it carries an unprefixedclangtargeting MinGW that would break configure's host compiler detection. - A Rust toolchain with the
x86_64-apple-darwintarget, forcompatdb.so.
Configure uses a strict allowlist: every optional dependency is --without-*
and only re-enabled with --with-*, so a missing package fails configure
loudly instead of silently changing the feature set. Never pass
--with-opengl on macOS; it triggers an EGL probe that always fails there,
and the Mac driver links -framework OpenGL on its own. Vulkan is off
(--without-vulkan): nothing in the bundle needs it, and wined3d serves D3D8
and DDraw through its GL backend.
build-wine.sh patches the SONAME_LIB* defines in config.h to
@loader_path/../../external/<lib> before compiling, so Wine's .so modules
in lib/wine/x86_64-unix/ find the bundled dylibs in lib/external/ through
dyld alone. The bundle step then copies each dylib and its /usr/local
closure into lib/external/, rewrites their install names to @loader_path/,
and verifies that no /usr/local/ reference survives. There is no launcher
binary and no DYLD_FALLBACK_LIBRARY_PATH; bin/ is exactly what
make install creates, bin/wine being the preloader and the convenience
programs symlinks to it.
.github/workflows/release.yml builds on a
macos-26 runner (Apple Silicon, Rosetta, x86_64 Homebrew, pinned llvm-mingw)
whenever a cx-* tag is pushed and attaches
wine-<tag>-macos-x86_64.tar.xz to a draft release. Tags are named after the
CrossOver version of the sources plus a build revision, so cx-26.2.0-0 is
the first build from CrossOver 26.2.0. wine-src.ref pins the
branch, tag or commit of athei/wine that gets built.
- Push the desired source state to athei/wine.
- Point
wine-src.refat it and push tomain. git tag cx-26.2.0-0 && git push origin cx-26.2.0-0.- Review and publish the draft release.
build-wine.sh exports MACOSX_DEPLOYMENT_TARGET=15.0. Without it clang takes
the deployment target from whatever host is building, which made the tarball's
macOS floor an accident of the runner image: cx-26.3.0-3 shipped minos 15.0
off the macos-15 runner while a local build on macOS 27 produced minos 26.0.
The SDK is still whatever the build host has; only the minimum is fixed. Check
this pin still holds whenever the runner image is bumped.
The workflow does not install Homebrew. Homebrew has stopped shipping x86_64
macOS bottles (gmp, sdl2-compat and sdl3 have none at all, the rest stop at the
sonoma tag), so a runner would have to compile them from source: that took 48
minutes on cx-26.3.0-3 and then stopped working altogether, because the
runners resolve neither ftpmirror.gnu.org nor gmplib.org and gmp's source
cannot be fetched there at all.
Instead package-deps.sh builds the x86_64 prefix once on a
machine that has an Intel Homebrew, and the result is mirrored as a release
asset that the workflow unpacks into /usr/local. It ships freetype, gnutls,
sdl2-compat, sdl3 and bison plus their runtime closure, mostly taken from
Homebrew's sonoma bottles so the libraries carry a minos of 14.0.
gmp is the exception. It has no x86_64 bottle at all, so Homebrew compiles it
on whatever machine runs package-deps.sh and stamps it with that machine's
macOS: cx-26.3.0-8 shipped a libgmp at minos 26.0, which would not have
loaded on anything older and would have taken gnutls down with it. The script
rebuilds gmp against the same floor build-wine.sh pins, and then refuses to
write the archive if any Mach-O in the tree is newer than that floor. If
another formula loses its bottle, that check is what will catch it.
To refresh it: arch -x86_64 /usr/local/bin/brew upgrade the formulae, run
./package-deps.sh, attach dist/wine-deps-macos-x86_64.tar.xz to a new
deps-<date> release, and point DEPS_URL and DEPS_SHA256 in the workflow
at it. The one thing the prefix does not provide is pkg-config, which comes
from the runner image; the workflow fails loudly if it is missing rather than
building a Wine with the libraries silently absent.
Wine turned data execution prevention off for a whole process as soon as any
loaded module lacked the NX_COMPAT flag, a DLL included. Turning it off
makes every readable mapping executable for the rest of the process, and
under Rosetta each fresh writable and executable page costs a Mach round trip
on first touch, so a 32-bit program with one 2000s-era DLL turns into a fault
storm on every allocation
(#3). The patched tree
decides from the main executable alone, as Windows does: a DLL without the
flag changes nothing. WINE_DISABLE_NX_COMPAT=1 keeps no-execute on even
when the executable itself lacks the flag; a compatdb env rule sets it per
game.
Every Direct3D implementation lives in its own tree under lib/wine, and the
default <arch>-windows directories hold only fake-module markers for the
DLLs involved. compatdb.so prepends one tree per API family to the builtin
search path of every process, so a process sees exactly one coherent set of
modules, nothing is ever mixed, and switching is purely additive.
There are two families because D3D10, D3D10.1, D3D11 and D3D12 all create
their device through one dxgi.dll and only work with their own
implementation's copy, while neither D3D9 implementation touches DXGI at all.
D3D8 and DDraw have no alternative to wined3d and stay in the default dirs.
| Family | Tree | Contents | Default |
|---|---|---|---|
dxgi |
lib/wine/dxgi/gptk/ |
Apple D3DMetal: dxgi, d3d10, d3d11, d3d12, nvapi64, nvngx; plus Wine's d3d10core and d3d10_1, which return E_FAIL on its dxgi but keep D3D10 probes from failing to load. x86_64 only. |
x86_64 |
dxgi |
lib/wine/dxgi/dxmt/ |
DXMT: dxgi, d3d10core, d3d11, winemetal for both arches; plus Wine's d3d10 and d3d10_1. | i386 |
dxgi |
lib/wine/dxgi/wined3d/ |
Wine's dxgi, d3d10, d3d10core, d3d10_1, d3d11 for both arches. No d3d12, which needs Vulkan. | |
d3d9 |
lib/wine/d3d9/mtld3d/ |
mtld3d: d3d9 and mtld3d for both arches, one x86_64 mtld3d.so. |
both |
d3d9 |
lib/wine/d3d9/wined3d/ |
Wine's d3d9 for both arches. Needs only wined3d.dll, which is in the default dir. |
Each tree has <arch>-windows/ and, where the implementation has a unix half,
x86_64-unix/. D3DMetal's unix modules are symlinks to
lib/external/libd3dshared.dylib; DXMT's and mtld3d's single x86_64 .so
also serves the i386 PE modules through wow64 entry points. winemetal.so
stays in lib/wine/x86_64-unix/ as the shared DXMT backend. nvngx is
Apple's nvngx-on-metalfx, renamed because that is the name games load when
they probe for DLSS.
The bundle step deletes what a --without-vulkan build can no longer serve:
vulkan-1 and winevulkan on both arches, d3d12/d3d12core for i386 and
d3d12core for x86_64.
lib/wine/x86_64-unix/compatdb.so is built from the compatdb/
crate in this repository (a Cargo workspace: compatdb-table holds the pure
rule logic, compatdb the cdylib around it). Wine's ntdll dlopens
<ntdll_dir>/compatdb.so in every process before any PE code runs, the slot
CrossOver's own compat database uses. The library reads the process's image
name and version resource, resolves the rules that match, and applies them:
dxgiandd3d9: which tree to prepend. Unset means the defaults in the table above. An implementation that does not exist for the process's architecture (gptkon i386) degrades to that architecture's default with a log line, sodxgi = gptkin a rule reads as "GPTK where it exists".dll_overrides:WINEDLLOVERRIDES-stylenames=orderentries, added through ntdll's load-order hook.arguments: text appended to the command line unless already present, which leaves a Chromium child that inherited its parent's switches alone.env:NAME=valueentries written into the process's environment block and its unix environment; an empty value removes the variable.
Rules match on the image basename (case-insensitive, or * for every
process) plus optional case-insensitive substrings of the version resource's
CompanyName, ProductName and OriginalFilename. Every matching rule
applies, folded least specific first (wildcard, then basename, then
fingerprinted), so a scalar ends up with the most specific rule's value and
the lists accumulate. The built-in rules, all pinned by version resource:
rockstar-launcher(Launcher.exe, Rockstar Games):dxgi = wined3d, because the launcher needs a real D3D10.1 device and D3DMetal has none.rockstar-social-club-ui(SocialClubHelper.exe, Take-Two):--in-process-gpu, since a CEF GPU process cannot paint into another process's window under winemac.steam-web-helper(steamwebhelper.exe, Valve):--in-process-gpu --disable-gpu --disable-software-rasterizer, the same problem plus GPU rendering off.gta-iv(GTAIV.exe, Rockstar Games):-availablevidmem 2048.0, which overrides the game's broken video-memory detection.
Whatever starts the process tree can add or change rules through the
WINE_COMPATDB environment variable. The value is a v=3 header line
followed by one rule per line; each rule is key=value fields joined by ;:
v=3
name=my-game;exe=Game.exe;company=Some Vendor;d3d9=wined3d;env=MTLD3D_CONFIG=adapter.spoof=amd
name=rockstar-launcher;dxgi=dxmt
name=steam-web-helper;enabled=false
Keys: name (required), exe, company, product, original_filename,
dxgi, d3d9, dll_overrides, arguments, env (repeatable, value
NAME=value) and enabled. Inside a value, %, ;, CR, LF and other
control characters are percent-encoded (%3B for ;); everything else,
including =, passes through. A rule naming a built-in rule is merged into it
(a set scalar wins, lists append), so an override needs only the fields it
changes; enabled=false drops the rule of that name; a new rule needs an
exe. A header other than v=3 makes the library ignore the whole value with
a diagnostic, which is what keeps a format change safe for long-lived
processes. A malformed line is skipped, never fatal.
The library writes compatdb: lines to wine's stderr: one block per process
with the image name, its version fingerprint, the rules that matched, the
trees it prepended, the overrides it added, and any parse diagnostics. To
confirm a tree took effect, look at the running process's mapped files
(lsof -p <pid> | grep -i dxgi.dll): a path under lib/wine/dxgi/<impl>/ or
lib/wine/d3d9/<impl>/ proves it.
redist.env pins one URL and one SHA-256 per artifact.
bundle-wine.sh downloads each into CACHE_DIR and reuses the cached copy as
long as its checksum still matches the pin; a mismatch is an error, never a
silent re-download, so a bump changes both lines. The release workflow caches
the same directory keyed on the pin file's hash.
Before verification, the bundle step removes inherited com.apple.quarantine
attributes from the assembled wine/ tree so macOS does not block a copied
library such as winemetal.so. Other attributes and code signatures are
preserved, and symlink targets outside the bundle are not touched. Failure to
remove quarantine stops the bundle step. Downloading the finished distribution
can apply quarantine again; this step only removes metadata inherited from
the build inputs.
DXMT and mtld3d come from their GitHub releases. Apple's Game Porting Toolkit
download needs an Apple ID session, so the unmodified dmg is attached to a
gptk-<version> release on this repository (those tags do not trigger the
release workflow) and the pin points there; upgrading means downloading the
new image by hand and creating a new gptk-* release. Apple's license
(shipped as lib/external/D3DMetal-License.rtf) allows distributing the
Redistributables unmodified for non-commercial purposes, which is why the
files are copied byte for byte: no install_name_tool, no re-signing, and the
dylib closure walk never touches them, which is why the Direct3D step runs
after the dylib step.
The patched tree at athei/wine carries the glue:
dlls/winemac.drv/d3dmetal.cexportsmacdrv_functions, the window, Metal and monitor helpers D3DMetal and DXMTdlsymout ofwinemac.so.dlls/ntdll/ntdll.specexports__wine_unix_call, which D3DMetal's PE halves look up at runtime.dlls/ntdll/unix/loader.cinit_non_native_support()dlopenslibd3dshared.dyliband records its__TEXTrange, which selects the ms-ABI unix call entry for calls arriving from Apple code; without it they take the sysv entry and crash. It defaults to<ntdll_dir>/../../external/libd3dshared.dylib, withCX_APPLEGPTK_LIBD3DSHARED_PATHas an override, and runs on the first native PE load.dlls/ntdll/unix/loader.cdlopenscompatdb.soand exportsprepend_dll_pathandadd_load_order_overridefor it.dlls/ntdll/loader.cdecides no-execute from the main executable alone instead of turning it off for the process as soon as any module lacksNX_COMPAT; see No-execute.loader/wine.inf.inregistersatidxx64.dll,nvapi64.dllandnvngx.dllas fake DLLs.
macdrv_functionsis a private contract with no version field.d3dmetal.cassertssizeof(struct macdrv_functions_t) == 192andsizeof(struct d3dmetal_macdrv_win_data) == 120; a mismatch with a newer GPTK or DXMT drop is a crash inside their code, not an error message. Check it on every GPTK, DXMT or CrossOver bump.init_non_native_support()is gated on Sonoma or later, and GPTK 4.0 wants macOS 15.- D3DMetal's client surface goes through
get_win_data(hwnd), so it is same-process only. Cross-process presentation (Steam's CEF GPU process drawing into the browser window) still needs--in-process-gpu. - A builtin only loads if its placeholder exists in the prefix, and wineboot
stamps placeholders for what is in
lib/wine/<arch>-windowswhen the prefix is created. The markers in the default dirs cover every DLL the trees supply, so a new prefix is complete; a builtin name added later (a development install of mtld3d intolib/wine/d3d9/mtld3d/, or a mod) needs awineboot -uonly if its name is new. A bundle redeploy wipeslib/wine, so such additions have to be reinstalled after one.