Skip to content

Commit 08f0601

Browse files
committed
Add Apple XCFramework artifact workflow
1 parent a5ba117 commit 08f0601

21 files changed

Lines changed: 1508 additions & 41 deletions

File tree

.github/workflows/run_unix.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,53 @@ jobs:
116116
do
117117
BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift run "$example"
118118
done
119+
- name: Build Apple XCFramework Artifacts
120+
if: (matrix.os == 'macos-14')
121+
run: |
122+
$GITHUB_WORKSPACE/tools/apple/build_xcframeworks.sh \
123+
--output $GITHUB_WORKSPACE/build/apple_xcframeworks
124+
env:
125+
BRAINFLOW_VERSION: ${{ steps.version.outputs.version }}
126+
- name: Verify Apple XCFramework Artifacts
127+
if: (matrix.os == 'macos-14')
128+
run: |
129+
$GITHUB_WORKSPACE/tools/apple/verify_xcframeworks.sh $GITHUB_WORKSPACE/build/apple_xcframeworks
130+
- name: Build Generated Swift Binary Package MacOS
131+
if: (matrix.os == 'macos-14')
132+
run: |
133+
cd $GITHUB_WORKSPACE/build/apple_xcframeworks/BrainFlowSwiftBinaryPackage
134+
swift build
135+
- name: Test Generated Swift Binary Package MacOS
136+
if: (matrix.os == 'macos-14')
137+
run: |
138+
$GITHUB_WORKSPACE/tools/apple/test_swift_binary_package.sh $GITHUB_WORKSPACE/build/apple_xcframeworks
139+
- name: Build iOS Demo With Generated XCFrameworks
140+
if: (matrix.os == 'macos-14')
141+
run: |
142+
BRAINFLOW_APPLE_XCFRAMEWORKS_DIR=$GITHUB_WORKSPACE/build/apple_xcframeworks/XCFrameworks \
143+
xcodebuild -quiet \
144+
-project $GITHUB_WORKSPACE/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj \
145+
-scheme BrainFlowiOSDemo \
146+
-configuration Debug \
147+
-destination 'generic/platform=iOS Simulator' \
148+
-derivedDataPath $GITHUB_WORKSPACE/build/ios-demo-derived \
149+
CODE_SIGNING_ALLOWED=NO \
150+
build
151+
- name: Package macOS Demo With Generated XCFrameworks
152+
if: (matrix.os == 'macos-14')
153+
run: |
154+
BRAINFLOW_APPLE_XCFRAMEWORKS_DIR=$GITHUB_WORKSPACE/build/apple_xcframeworks/XCFrameworks \
155+
$GITHUB_WORKSPACE/tools/apple/package_macos_demo_app.sh $GITHUB_WORKSPACE/build/apple_xcframeworks/BrainFlowMacDemo.app
156+
- name: Upload Apple XCFramework Artifacts
157+
if: (matrix.os == 'macos-14')
158+
uses: actions/upload-artifact@v4
159+
with:
160+
name: brainflow-apple-xcframeworks
161+
path: |
162+
${{ github.workspace }}/build/apple_xcframeworks/BrainFlowAppleXCFrameworks.zip
163+
${{ github.workspace }}/build/apple_xcframeworks/BrainFlowAppleXCFrameworks.zip.sha256
164+
${{ github.workspace }}/build/apple_xcframeworks/checksums.sha256
165+
${{ github.workspace }}/build/apple_xcframeworks/manifest.json
119166
- name: Compile BrainFlow Ubuntu
120167
if: (matrix.os == 'ubuntu-latest')
121168
run: |

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ BenchmarkDotNet.Artifacts/
5353
project.lock.json
5454
project.fragment.lock.json
5555
artifacts/
56+
swift_package/Artifacts/Apple/
5657

5758
# StyleCop
5859
StyleCopReport.xml
@@ -374,6 +375,7 @@ src/ml/train/data/
374375
src/ml/train/data/*.onnx
375376
tools/brainflow-android.aar
376377
build_android_aar/
378+
build_apple/
377379
tools/simpleble-bridge.jar
378380
tools/simpleble-bridge-classes/
379381
tools/simpleble-bridge-sources.txt

AGENTS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Agent Notes
2+
3+
## Apple XCFramework Artifacts
4+
5+
Apple binary artifacts are generated build outputs. They contain framework-wrapped XCFrameworks
6+
for iOS device, iOS simulator, and macOS app integration, plus a generated
7+
`BrainFlowSwiftBinaryPackage` for app developers.
8+
9+
Regenerate the artifacts from the repository root:
10+
11+
```bash
12+
tools/apple/regenerate_artifacts.sh
13+
```
14+
15+
Verify an existing artifact tree:
16+
17+
```bash
18+
tools/apple/verify_xcframeworks.sh build/apple_xcframeworks
19+
```
20+
21+
The regeneration script builds native BrainFlow Apple slices, packages the XCFrameworks, verifies
22+
the required core frameworks, and creates `BrainFlowAppleXCFrameworks.zip` plus checksum files.
23+
24+
By default, `tools/apple/regenerate_artifacts.sh` and `tools/apple/build_xcframeworks.sh` write to
25+
`build/apple_xcframeworks`. Do not commit `build/`, `build_apple/`, `swift_package/Artifacts/Apple`,
26+
or local `installed/` outputs.
27+
28+
The iOS demo and macOS packaging script default to `build/apple_xcframeworks/XCFrameworks`.
29+
CI may override artifact paths with
30+
`BRAINFLOW_APPLE_XCFRAMEWORKS_DIR`.
31+
32+
When changing Apple artifact generation, regenerate locally, run verification, and let CI upload
33+
`BrainFlowAppleXCFrameworks.zip` plus `BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`,
34+
and `manifest.json` as the distributable artifact set. Generated framework headers and binaries
35+
should come from the scripts, not from files copied into the repository.
36+
37+
For a BrainFlow release or Apple-library refresh, build from the release commit/tag with
38+
`BRAINFLOW_VERSION` set, regenerate artifacts, verify the generated tree, smoke-test the generated
39+
Swift binary package, and validate the iOS/macOS sample apps against the regenerated XCFrameworks.
40+
Do not manually patch release frameworks after generation; update source and rerun the script.

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ option (BUILD_ONNX "BUILD_ONNX" OFF)
2828
option (BUILD_TESTS "BUILD_TESTS" OFF)
2929
option (BUILD_PERIPHERY "BUILD_PERIPHERY" OFF)
3030
option (BRAINFLOW_COPY_TO_PACKAGE_DIRS "Copy built artifacts into language package folders" ON)
31+
option (BRAINFLOW_APPLE_DYNAMIC_FRAMEWORKS "Build Apple iOS native libraries as dynamic libraries for framework/XCFramework packaging" OFF)
3132

3233
set (BRAINFLOW_IOS OFF)
3334
if (CMAKE_SYSTEM_NAME STREQUAL "iOS")
@@ -38,6 +39,11 @@ set (BRAINFLOW_CORE_LIBRARY_TYPE SHARED)
3839
if (BRAINFLOW_IOS)
3940
set (BRAINFLOW_CORE_LIBRARY_TYPE STATIC)
4041

42+
if (BRAINFLOW_APPLE_DYNAMIC_FRAMEWORKS)
43+
message (STATUS "Building iOS BrainFlow native libraries as dynamic libraries for XCFramework packaging.")
44+
set (BRAINFLOW_CORE_LIBRARY_TYPE SHARED)
45+
endif ()
46+
4147
if (BRAINFLOW_COPY_TO_PACKAGE_DIRS)
4248
message (STATUS "Disabling BRAINFLOW_COPY_TO_PACKAGE_DIRS for iOS builds.")
4349
set (BRAINFLOW_COPY_TO_PACKAGE_DIRS OFF CACHE BOOL "Copy built artifacts into language package folders" FORCE)

docs/BuildBrainFlow.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,25 @@ Local build example:
167167
168168
The Swift package intentionally does not vendor BrainFlow native binaries. Like source builds for other bindings, it dynamically loads native libraries built from this repository. The loader searches :code:`BRAINFLOW_LIB_DIR`, system library paths, :code:`installed/lib`, and app bundle resource/framework directories for :code:`libBoardController`, :code:`libDataHandler`, and :code:`libMLModule`.
169169

170+
For production iOS and macOS applications, use Apple XCFramework artifacts and the generated Swift binary package. Regenerate Apple artifacts with:
171+
172+
.. code-block:: bash
173+
174+
tools/apple/regenerate_artifacts.sh
175+
tools/apple/verify_xcframeworks.sh build/apple_xcframeworks
176+
177+
The default generated artifact directory is :code:`build/apple_xcframeworks`. It contains :code:`XCFrameworks`, :code:`BrainFlowSwiftBinaryPackage`, :code:`BrainFlowAppleXCFrameworks.zip`, and checksum files. These generated headers and binaries are release/CI artifacts, not source files committed to the repository.
178+
179+
The generated :code:`BrainFlowSwiftBinaryPackage` contains the Swift API and binary targets for :code:`BoardController.xcframework`, :code:`DataHandler.xcframework`, and :code:`MLModule.xcframework`. Add this package to an app through Xcode or Swift Package Manager so embedded frameworks are handled by standard Apple build, embed, and signing flows.
180+
170181
The macOS demo can be built with:
171182

172183
.. code-block:: bash
173184
174185
cd swift_package
175186
BRAINFLOW_LIB_DIR=../installed/lib swift run BrainFlowMacDemo
176187
177-
iOS and Mac App Store sample source and release-preparation notes are available in :code:`swift_package/examples/apps` and :code:`swift_package/Docs/AppStoreReadiness.md`. iOS runtime support requires BrainFlow native libraries compiled for the target iOS architectures and embedded as signed app-bundle libraries or XCFrameworks.
188+
iOS and Mac App Store sample source and release-preparation notes are available in :code:`swift_package/examples/apps`, :code:`swift_package/Docs/AppleBinaryDistribution.md`, and :code:`swift_package/Docs/AppStoreReadiness.md`. App runtime support requires matching BrainFlow native frameworks embedded and signed inside the app bundle; App Store builds should not depend on :code:`BRAINFLOW_LIB_DIR` or local development directories.
178189

179190
Docker Image
180191
--------------

src/utils/inc/runtime_dll_loader.h

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#include <windows.h>
66
#else
77
#include <dlfcn.h>
8+
#include <string>
9+
#include <vector>
810
#endif
911

1012

@@ -19,7 +21,8 @@ class DLLLoader
1921
public:
2022
DLLLoader (const char *dll_path)
2123
{
22-
strcpy (this->dll_path, dll_path);
24+
strncpy (this->dll_path, dll_path, sizeof (this->dll_path) - 1);
25+
this->dll_path[sizeof (this->dll_path) - 1] = '\0';
2326
this->lib_instance = NULL;
2427
}
2528

@@ -68,11 +71,15 @@ class DLLLoader
6871
{
6972
// RTLD_DEEPBIND will search for symbols in loaded lib first and after that in global
7073
// scope
71-
lib_instance = dlopen (this->dll_path, RTLD_LAZY | RTLD_DEEPBIND);
72-
if (!lib_instance)
74+
for (const std::string &candidate : get_dlopen_candidates ())
7375
{
74-
return false;
76+
lib_instance = dlopen (candidate.c_str (), RTLD_LAZY | RTLD_DEEPBIND);
77+
if (lib_instance)
78+
{
79+
return true;
80+
}
7581
}
82+
return false;
7683
}
7784
return true;
7885
}
@@ -97,6 +104,115 @@ class DLLLoader
97104
#endif
98105

99106
private:
107+
#ifndef _WIN32
108+
std::vector<std::string> get_dlopen_candidates () const
109+
{
110+
std::vector<std::string> candidates;
111+
append_unique (candidates, this->dll_path);
112+
113+
#ifdef __APPLE__
114+
std::string original_path = this->dll_path;
115+
std::string directory = parent_directory (original_path);
116+
std::string file_name = last_path_component (original_path);
117+
std::string framework_name = apple_framework_name (file_name);
118+
119+
if (!framework_name.empty ())
120+
{
121+
append_unique (candidates, framework_name + ".framework/" + framework_name);
122+
append_unique (candidates, "@rpath/" + framework_name + ".framework/" + framework_name);
123+
if (!directory.empty ())
124+
{
125+
append_unique (
126+
candidates, directory + framework_name + ".framework/" + framework_name);
127+
std::string framework_parent = parent_frameworks_directory (directory);
128+
if (!framework_parent.empty ())
129+
{
130+
append_unique (candidates,
131+
framework_parent + framework_name + ".framework/" + framework_name);
132+
}
133+
}
134+
}
135+
#endif
136+
return candidates;
137+
}
138+
139+
static void append_unique (std::vector<std::string> &values, const std::string &value)
140+
{
141+
if (value.empty ())
142+
{
143+
return;
144+
}
145+
for (const std::string &existing : values)
146+
{
147+
if (existing == value)
148+
{
149+
return;
150+
}
151+
}
152+
values.push_back (value);
153+
}
154+
155+
#ifdef __APPLE__
156+
static std::string last_path_component (const std::string &path)
157+
{
158+
size_t slash = path.find_last_of ('/');
159+
if (slash == std::string::npos)
160+
{
161+
return path;
162+
}
163+
return path.substr (slash + 1);
164+
}
165+
166+
static std::string parent_directory (const std::string &path)
167+
{
168+
size_t slash = path.find_last_of ('/');
169+
if (slash == std::string::npos)
170+
{
171+
return "";
172+
}
173+
return path.substr (0, slash + 1);
174+
}
175+
176+
static std::string parent_frameworks_directory (const std::string &directory)
177+
{
178+
std::string normalized = directory;
179+
if (!normalized.empty () && normalized[normalized.size () - 1] == '/')
180+
{
181+
normalized.resize (normalized.size () - 1);
182+
}
183+
184+
size_t framework_suffix = normalized.rfind (".framework");
185+
if (framework_suffix == std::string::npos)
186+
{
187+
return "";
188+
}
189+
190+
size_t framework_dir_start = normalized.find_last_of ('/', framework_suffix);
191+
if (framework_dir_start == std::string::npos)
192+
{
193+
return "";
194+
}
195+
return normalized.substr (0, framework_dir_start + 1);
196+
}
197+
198+
static std::string apple_framework_name (const std::string &file_name)
199+
{
200+
std::string name = file_name;
201+
const std::string dylib_suffix = ".dylib";
202+
if (name.size () > dylib_suffix.size () &&
203+
name.substr (name.size () - dylib_suffix.size ()) == dylib_suffix)
204+
{
205+
name.resize (name.size () - dylib_suffix.size ());
206+
}
207+
if (name.size () > 3 && name.substr (0, 3) == "lib")
208+
{
209+
name = name.substr (3);
210+
}
211+
return name;
212+
}
213+
#endif
214+
#endif
215+
100216
char dll_path[1024];
101217
#ifdef _WIN32
102218
HINSTANCE lib_instance;

swift_package/Docs/AppStoreReadiness.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,42 @@ This checklist is intentionally separate from the sample source because final Ap
88
- Replace placeholder bundle IDs.
99
- Add production app icons and screenshots.
1010
- Keep the synthetic-board demo path available so App Review can exercise the app without external hardware.
11-
- Embed BrainFlow native libraries or XCFrameworks in the app bundle and sign them with the app.
11+
- Embed BrainFlow XCFramework products in the app bundle and sign them with the app.
12+
- Use the generated `BrainFlowSwiftBinaryPackage` for production app integration. Do not rely on `BRAINFLOW_LIB_DIR`, local `installed/lib` folders, or loose development dylibs in App Store builds.
1213
- Confirm final privacy answers reflect real-board connectivity, Bluetooth, networking, files, and any third-party native dependencies actually shipped.
1314
- Run an archive build and install it on a physical device or clean Mac before upload.
15+
- Verify the archive contains only device slices for iOS, with embedded framework install names in the form `@rpath/<Framework>.framework/<Framework>`.
1416

1517
## iOS
1618

1719
- Use `examples/apps/ios/BrainFlowiOSDemo` as the Xcode app project.
18-
- Keep `swift_package` as the local package dependency.
19-
- Provide iOS-compatible BrainFlow native binaries. The current high-level Swift package compiles for iOS, but the app can only run BrainFlow calls when matching native libraries are embedded.
20+
- Use the generated Swift binary package or embed framework slices from `build/apple_xcframeworks/XCFrameworks`.
21+
- Provide iOS-compatible BrainFlow native binaries through XCFrameworks. The high-level Swift package compiles for iOS, but BrainFlow calls can only run when matching native frameworks are embedded and signed.
2022
- For Muse native BLE boards, build BrainFlow native libraries with BLE support enabled for the target platform and keep the Bluetooth privacy string in the app plist.
2123
- Keep permissions minimal. The synthetic-board demo needs no network or file permissions.
2224
- Test via TestFlight before App Store submission.
2325

2426
## macOS
2527

26-
- Use `swift_package` product `BrainFlowMacDemo` for local development, or create an Xcode app target for App Store archiving.
28+
- Use `swift_package` product `BrainFlowMacDemo` for local development, or package it with `tools/apple/package_macos_demo_app.sh` for app-bundle smoke testing.
2729
- Add the files from `examples/apps/macos/BrainFlowMacDemo`.
2830
- Enable App Sandbox.
29-
- Embed and sign BrainFlow dylibs/XCFrameworks.
30-
- Verify dynamic loading works inside the archived app bundle, not only with `BRAINFLOW_LIB_DIR`.
31+
- Embed and sign BrainFlow XCFramework products.
32+
- Verify dynamic loading works inside the app bundle, not only with `BRAINFLOW_LIB_DIR`.
3133

3234
## Production Gate
3335

3436
- Swift package builds.
3537
- Swift tests pass with native libraries present.
3638
- CLI smoke test succeeds with the synthetic board.
37-
- iOS and macOS app targets launch, handle missing native libraries gracefully, and run the synthetic-board workflow when libraries are embedded.
39+
- `tools/apple/build_xcframeworks.sh` and `tools/apple/verify_xcframeworks.sh` pass.
40+
- `tools/apple/regenerate_artifacts.sh` refreshes `build/apple_xcframeworks`, and `tools/apple/verify_xcframeworks.sh build/apple_xcframeworks` passes.
41+
- The Apple release artifact set includes `BrainFlowAppleXCFrameworks.zip`,
42+
`BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`, and `manifest.json` from the same
43+
build.
44+
- `manifest.json` records the BrainFlow version, source revision, toolchain versions, deployment
45+
targets, and optional native feature flags.
46+
- A clean app consumes `BrainFlowSwiftBinaryPackage` without building native BrainFlow locally.
47+
- iOS and macOS app targets launch, handle missing native frameworks gracefully, and run the synthetic-board workflow when frameworks are embedded.
3848
- Accessibility labels and dynamic text behavior are reviewed in the sample apps.
3949
- Crash logs are clean after repeated start, stop, read, and release cycles.

0 commit comments

Comments
 (0)