Skip to content

Commit 719d02f

Browse files
dmjioclaude
andauthored
Add darwin build and dev support. (#69)
* Add darwin build and dev support. * Add ArrayFire build * Update flake.nix * Use CPU in CI * drop `info` * Conditional setBackend * Disable CI GPU/OpenCL backend setup Comment out code related to CI GPU/OpenCL backend handling. * CI: build via `nix build -L`, drop brew and cabal flow Replace the cabal-in-devShell test flow with `nix build -L`, which builds the package derivation and runs the hspec suite in its checkPhase. Drop the redundant `brew install arrayfire` (ArrayFire is provided by the flake's .pkg-based darwin build) and pin macOS to macos-13 since ArrayFire only ships an x86_64 macOS binary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Use Rosetta, `macos-latest` --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8914928 commit 719d02f

2 files changed

Lines changed: 170 additions & 35 deletions

File tree

.github/workflows/ci.yml

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@ env:
1010

1111
jobs:
1212
build:
13-
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
os: [ubuntu-latest, macos-latest]
16+
runs-on: ${{ matrix.os }}
1417
steps:
1518
- uses: actions/checkout@v4
1619

20+
# macos-latest is Apple Silicon, but ArrayFire only ships an x86_64 macOS
21+
# binary, so the flake's darwin output is x86_64-darwin. Build it under
22+
# Rosetta 2: ensure Rosetta is present and let Nix build x86_64-darwin
23+
# derivations via `extra-platforms` below.
24+
- name: Install Rosetta 2
25+
if: runner.os == 'macOS'
26+
run: softwareupdate --install-rosetta --agree-to-license
27+
1728
- uses: cachix/install-nix-action@v31
1829
with:
1930
nix_path: nixpkgs=channel:nixpkgs-unstable
31+
extra_nix_config: |
32+
extra-platforms = x86_64-darwin
2033
21-
- name: Nix channel --update
22-
run: nix-channel --update
23-
24-
- name: Cabal update
25-
run: nix develop --command bash -c 'cabal update'
34+
- name: Build and run tests (Linux)
35+
if: runner.os == 'Linux'
36+
run: nix build -L
2637

27-
- name: Build and run tests
28-
run: nix develop --command bash -c 'cabal install hspec-discover && cabal test'
38+
- name: Build and run tests (macOS, x86_64 via Rosetta)
39+
if: runner.os == 'macOS'
40+
run: nix build -L .#packages.x86_64-darwin.default

flake.nix

Lines changed: 150 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
];
2626
};
2727

28-
# Build ArrayFire from the official binary installer; avoids freeimage entirely.
29-
mkArrayfire = pkgs: pkgs.stdenv.mkDerivation rec {
28+
# Build ArrayFire from the official Linux binary installer; avoids freeimage entirely.
29+
mkArrayfireLinux = pkgs: pkgs.stdenv.mkDerivation rec {
3030
pname = "arrayfire";
3131
version = "3.10.0";
3232
src = pkgs.fetchurl {
@@ -50,12 +50,98 @@
5050
mkdir -p $out
5151
bash $src --exclude-subdir --prefix=$out
5252
'';
53+
# autoPatchelfIgnoreMissingDeps silences missing-dep errors at build time,
54+
# but a genuinely-absent dep of libafcpu.so would still make its runtime
55+
# dlopen fail with LoadLibError. Fail the build loudly if the CPU backend
56+
# has any unresolved (=> not just intentionally-ignored GPU) dependencies.
57+
doInstallCheck = true;
58+
installCheckPhase = ''
59+
libdir=$out/lib64
60+
[ -d "$libdir" ] || libdir=$out/lib
61+
cpu=$(echo "$libdir"/libafcpu.so* | tr ' ' '\n' | head -n1)
62+
echo "Checking runtime deps of $cpu"
63+
if ldd "$cpu" | grep -i 'not found'; then
64+
echo "ERROR: libafcpu.so has unresolved dependencies" >&2
65+
exit 1
66+
fi
67+
'';
5368
meta = {
5469
description = "A general-purpose library for parallel and massively-parallel architectures";
5570
platforms = [ "x86_64-linux" ];
5671
};
5772
};
5873

74+
# Build ArrayFire on macOS from the official .pkg installer. ArrayFire has not
75+
# shipped a macOS binary since 3.8.2 (x86_64 only), so darwin pins that version.
76+
# The .pkg is a xar archive of component sub-packages, each carrying a
77+
# gzip+cpio Payload that installs under opt/arrayfire/{include,lib}.
78+
mkArrayfireDarwin = pkgs: pkgs.stdenv.mkDerivation rec {
79+
pname = "arrayfire";
80+
version = "3.8.2";
81+
src = pkgs.fetchurl {
82+
url = "https://arrayfire.s3.amazonaws.com/${version}/ArrayFire-${version}_OSX_x86_64.pkg";
83+
hash = "sha256-MDqpDONbzl+PNu2VS1UTaYL10fpzpt0pv10oxNwgm+k=";
84+
};
85+
nativeBuildInputs = with pkgs; [ xar cpio fixDarwinDylibNames ];
86+
# Never strip the prebuilt vendor dylibs: the default strip phase corrupts
87+
# them (it silently truncated libmkl_core.dylib to 0 bytes, which then made
88+
# MKL fail to load its computational layer at runtime).
89+
dontStrip = true;
90+
unpackPhase = ''
91+
runHook preUnpack
92+
xar -xf $src
93+
runHook postUnpack
94+
'';
95+
# Extract every component Payload (except the heavy CUDA/OpenCL/examples ones
96+
# we don't ship) into a staging tree, then install only the unified + CPU
97+
# backends and their bundled runtime deps (MKL, TBB, forge).
98+
installPhase = ''
99+
runHook preInstall
100+
mkdir -p stage
101+
for comp in ArrayFire-${version}-Darwin-*.pkg; do
102+
case "$comp" in
103+
*cuda*|*opencl*|*examples*|*documentation*) continue ;;
104+
esac
105+
[ -f "$comp/Payload" ] || continue
106+
( cd stage && gzip -dc "../$comp/Payload" | cpio -id --quiet )
107+
done
108+
109+
mkdir -p $out/lib
110+
cp -R stage/opt/arrayfire/include $out/include
111+
for pat in 'libaf.*' 'libafcpu.*' 'libforge.*' 'libmkl_*.dylib' \
112+
'libtbb*.dylib' 'libiomp*.dylib'; do
113+
cp -P stage/opt/arrayfire/lib/$pat $out/lib/ 2>/dev/null || true
114+
done
115+
runHook postInstall
116+
'';
117+
# fixDarwinDylibNames (run in fixupPhase) rewrites the @rpath install ids
118+
# and matching inter-library references to absolute store paths. It only
119+
# rewrites references whose leaf matches a sibling's *original* id, so it
120+
# misses cases where the ids differ, e.g. libafcpu -> @rpath/libmkl_rt and
121+
# libmkl_tbb_thread -> @rpath/libtbb (the latter is dlopen'd by MKL's
122+
# libmkl_rt and would otherwise fail to load at runtime). Re-point any
123+
# remaining @rpath/<leaf> dep at $out/lib/<leaf> so everything is hermetic.
124+
postFixup = ''
125+
for dylib in $out/lib/*.dylib; do
126+
for dep in $(otool -L "$dylib" | awk 'NR>1{print $1}' | grep '^@rpath/' || true); do
127+
leaf=''${dep#@rpath/}
128+
if [ -e "$out/lib/$leaf" ]; then
129+
install_name_tool -change "$dep" "$out/lib/$leaf" "$dylib"
130+
fi
131+
done
132+
done
133+
'';
134+
meta = {
135+
description = "A general-purpose library for parallel and massively-parallel architectures";
136+
platforms = [ "x86_64-darwin" ];
137+
};
138+
};
139+
140+
mkArrayfire = pkgs:
141+
if pkgs.stdenv.isDarwin
142+
then mkArrayfireDarwin pkgs
143+
else mkArrayfireLinux pkgs;
144+
59145
arrayfire-overlay = self: super: {
60146
arrayfire = mkArrayfire self;
61147
};
@@ -65,37 +151,69 @@
65151
haskell = super.haskell // {
66152
packageOverrides = inputs.nixpkgs.lib.composeExtensions super.haskell.packageOverrides
67153
(hself: hsuper: {
68-
arrayfire = self.haskell.lib.appendConfigureFlags
69-
(hself.callCabal2nix "arrayfire" src {
70-
af = self.arrayfire;
71-
})
72-
[ "-f disable-default-paths" ];
154+
arrayfire =
155+
let
156+
pkg = self.haskell.lib.appendConfigureFlags
157+
(hself.callCabal2nix "arrayfire" src {
158+
af = self.arrayfire;
159+
})
160+
[ "-f disable-default-paths" ];
161+
in
162+
# On macOS ArrayFire's bundled MKL dlopens its threading layer
163+
# (libmkl_tbb_thread.dylib) by bare leaf name, which dyld only
164+
# resolves via DYLD_LIBRARY_PATH. Point it at the arrayfire libs
165+
# so the test suite (and doctests) can run. Runtime consumers of
166+
# this package need the same DYLD_LIBRARY_PATH.
167+
if self.stdenv.isDarwin
168+
then pkg.overrideAttrs (old: {
169+
preCheck = (old.preCheck or "") + ''
170+
export DYLD_LIBRARY_PATH="${self.arrayfire}/lib''${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
171+
'';
172+
})
173+
# On Linux we link against the unified backend (libaf), which is
174+
# just a dispatcher that dlopens the real backend impl
175+
# (libafcpu.so) at runtime. The sandboxed check phase has no
176+
# LD_LIBRARY_PATH/AF_PATH, so that dlopen finds nothing and every
177+
# test throws AFException LoadLibError (501). Point the loader at
178+
# the arrayfire libs so the backend can be found.
179+
else pkg.overrideAttrs (old: {
180+
preCheck = (old.preCheck or "") + ''
181+
export AF_PATH="${self.arrayfire}"
182+
export LD_LIBRARY_PATH="${self.arrayfire}/lib:${self.arrayfire}/lib64''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
183+
'';
184+
});
73185
});
74186
};
75187
};
76188

77189
devShell-for = pkgs:
78190
let
79191
ps = pkgs.haskellPackages;
192+
isLinux = pkgs.stdenv.isLinux;
193+
isDarwin = pkgs.stdenv.isDarwin;
194+
# ArrayFire only ships an x86_64 macOS binary, so it's unavailable on
195+
# Apple Silicon; fall back to a plain shell there.
196+
hasArrayfire = isLinux || pkgs.stdenv.hostPlatform.system == "x86_64-darwin";
80197
in
81-
ps.shellFor {
82-
packages = ps: with ps; [ arrayfire ];
83-
withHoogle = true;
84-
buildInputs = with pkgs; [ ocl-icd ];
85-
nativeBuildInputs = with pkgs; with ps; [
86-
# Building and testing
87-
cabal-install
88-
doctest
89-
hsc2hs
90-
# hspec-discover
91-
nil
92-
# Formatters
93-
nixpkgs-fmt
94-
];
95-
shellHook = ''
96-
export LD_LIBRARY_PATH="${pkgs.arrayfire}/lib:$LD_LIBRARY_PATH"
97-
'';
98-
};
198+
ps.shellFor {
199+
packages = ps: if hasArrayfire then [ ps.arrayfire ] else [ ];
200+
withHoogle = true;
201+
buildInputs = with pkgs; (if isLinux then [ ocl-icd ] else [ darwin.apple_sdk.frameworks.Security ]);
202+
nativeBuildInputs = with pkgs; with ps; [
203+
# Building and testing
204+
cabal-install
205+
doctest
206+
hsc2hs
207+
# hspec-discover
208+
nil
209+
# Formatters
210+
nixpkgs-fmt
211+
];
212+
shellHook =
213+
if isLinux then ''export LD_LIBRARY_PATH="${pkgs.arrayfire}/lib:$LD_LIBRARY_PATH"''
214+
else if hasArrayfire then ''export DYLD_LIBRARY_PATH="${pkgs.arrayfire}/lib:$DYLD_LIBRARY_PATH"''
215+
else "";
216+
};
99217

100218
pkgs-for = system: import inputs.nixpkgs {
101219
inherit system;
@@ -107,8 +225,13 @@
107225
in
108226
{
109227
packages = inputs.flake-utils.lib.eachDefaultSystemMap (system:
110-
with (pkgs-for system); {
111-
default = haskellPackages.arrayfire;
228+
let
229+
pkgs = pkgs-for system;
230+
# ArrayFire only provides binaries for x86_64-linux and x86_64-darwin
231+
# (no Apple Silicon / aarch64), so only expose the package there.
232+
hasArrayfire = pkgs.stdenv.isLinux || system == "x86_64-darwin";
233+
in inputs.nixpkgs.lib.optionalAttrs hasArrayfire {
234+
default = pkgs.haskellPackages.arrayfire;
112235
});
113236

114237
devShells = inputs.flake-utils.lib.eachDefaultSystemMap (system: {

0 commit comments

Comments
 (0)