chore(mobile): rust native core (#31254)

This commit is contained in:
Santo Shakil
2026-10-08 02:42:15 +06:00
committed by GitHub
parent 7e7a1de3fd
commit 8a02a91ee9
32 changed files with 886 additions and 4 deletions
+55
View File
@@ -74,6 +74,7 @@ jobs:
- 'mobile/**'
- 'open-api/**'
- 'i18n/**'
- 'native/**'
force-filters: |
- '.github/workflows/build-mobile.yml'
force-events: 'workflow_call,workflow_dispatch'
@@ -102,6 +103,20 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Restore Rust Cache
id: cache-rust-restore
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
key: build-mobile-rust-${{ runner.os }}-${{ hashFiles('native/crates/immich_core_ffi/rust-toolchain.toml', 'native/Cargo.lock') }}
restore-keys: |
build-mobile-rust-${{ runner.os }}-
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
@@ -197,6 +212,19 @@ jobs:
mobile/android/.gradle
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
- name: Save Rust Cache
id: cache-rust-save
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: github.ref == 'refs/heads/main'
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
key: ${{ steps.cache-rust-restore.outputs.cache-primary-key }}
build-sign-ios:
name: Build and sign iOS
needs: pre-job
@@ -223,6 +251,20 @@ jobs:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Restore Rust Cache
id: cache-rust-restore
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
key: build-mobile-rust-${{ runner.os }}-${{ hashFiles('native/crates/immich_core_ffi/rust-toolchain.toml', 'native/Cargo.lock') }}
restore-keys: |
build-mobile-rust-${{ runner.os }}-
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
@@ -314,3 +356,16 @@ jobs:
with:
name: ios-release-ipa
path: mobile/ios/Runner.ipa
- name: Save Rust Cache
id: cache-rust-save
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: github.ref == 'refs/heads/main'
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
key: ${{ steps.cache-rust-restore.outputs.cache-primary-key }}
+1
View File
@@ -36,6 +36,7 @@ jobs:
- 'mobile/**'
- 'open-api/**'
- 'i18n/en.json'
- 'native/**'
force-filters: |
- '.github/workflows/static_analysis.yml'
force-events: 'workflow_dispatch,release'
+96
View File
@@ -61,6 +61,10 @@ jobs:
- 'mobile/**'
- 'open-api/**'
- 'i18n/**'
- 'native/**'
- 'mise.toml'
native:
- 'native/**'
- 'mise.toml'
machine-learning:
- 'machine-learning/**'
@@ -71,6 +75,71 @@ jobs:
- '.github/workflows/test.yml'
force-events: 'workflow_dispatch'
native-tests:
name: Test & Lint Native Core
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).native == true }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@df46d635b905e618b88fc4c95baa920aeb30b309 # create-workflow-token-action-v3.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Restore Rust Cache
id: cache-rust-restore
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
native/target
key: native-rust-${{ runner.os }}-${{ hashFiles('native/crates/immich_core_ffi/rust-toolchain.toml', 'native/mise.lock', 'native/Cargo.lock') }}
restore-keys: |
native-rust-${{ runner.os }}-
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./native
- name: Check formatting
run: mise //native:fmt
- name: Run clippy
run: mise //native:lint
- name: Run tests
run: mise //native:test
- name: Save Rust Cache
id: cache-rust-save
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: github.ref == 'refs/heads/main'
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
native/target
key: ${{ steps.cache-rust-restore.outputs.cache-primary-key }}
script-unit-tests:
name: Scripts unit tests
needs: pre-job
@@ -607,6 +676,20 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Restore Rust Cache
id: cache-rust-restore
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
key: mobile-rust-${{ runner.os }}-${{ hashFiles('native/crates/immich_core_ffi/rust-toolchain.toml', 'mobile/mise.lock', 'native/Cargo.lock') }}
restore-keys: |
mobile-rust-${{ runner.os }}-
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
@@ -622,6 +705,19 @@ jobs:
- name: Run tests
run: mise //mobile:test
- name: Save Rust Cache
id: cache-rust-save
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: github.ref == 'refs/heads/main'
with:
path: |
~/.rustup/toolchains
~/.rustup/update-hashes
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
key: ${{ steps.cache-rust-restore.outputs.cache-primary-key }}
ml-unit-tests:
name: Unit Test ML
needs: pre-job
+1
View File
@@ -5,3 +5,4 @@
/machine-learning/ @mertalev
/e2e/ @danieldietzler
/mobile/ @shenlong-tanwen @santoshakil @agg23
/native/ @santoshakil
+2
View File
@@ -104,6 +104,8 @@ To see local changes to `@immich/ui` in Immich, do the following:
Always run `mise //mobile:checkout` after switching branches.
:::
The app includes a small Rust core (`native/`) that is compiled from source during the build. With mise the Rust toolchain is installed for you; with your own Flutter install, install [rustup](https://rustup.rs) once and the build fetches the pinned toolchain itself. The first build takes a little longer because of this.
##### iOS Code Signing
The Immich Apple Team ID and bundle IDs are specified in `mobile/ios/Signing.xcconfig`. For local development, we provide an override mechanism.
+1
View File
@@ -12,6 +12,7 @@ config_roots = [
"docs",
".github",
"machine-learning",
"native",
]
[tools]
@@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_native_core/immich_native_core.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
test('native core loads on device', () {
expect(version(), isNotEmpty);
});
}
+13
View File
@@ -33,6 +33,13 @@ platform :ios do
in_house: false
)
end
# Xcode build phases need mise trust saved to disk.
def trust_mise_configs
return unless system("command -v mise > /dev/null 2>&1")
sh("mise trust ../../../mise.toml")
sh("mise trust ../../mise.toml")
end
# Helper method to assemble xcargs with optional CUSTOM_GROUP_ID override
def build_xcargs(group_id: nil)
@@ -102,6 +109,8 @@ end
)
app_identifier = base_bundle_id
trust_mise_configs
# Set version number if provided
if version_number
increment_version_number(version_number: version_number)
@@ -122,6 +131,7 @@ end
workspace: "Runner.xcworkspace",
configuration: configuration,
export_method: "app-store",
xcodebuild_formatter: "", # raw xcodebuild output so script-phase errors show in CI logs
xcargs: build_xcargs(group_id: group_id),
export_options: {
provisioningProfiles: {
@@ -258,6 +268,8 @@ end
api_key = get_api_key
trust_mise_configs
# Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true)
@@ -284,6 +296,7 @@ end
configuration: "Release",
export_method: "app-store",
skip_package_ipa: true,
xcodebuild_formatter: "", # raw xcodebuild output so script-phase errors show in CI logs
xcargs: build_xcargs(group_id: DEV_GROUP_ID),
export_options: {
provisioningProfiles: {
+7
View File
@@ -126,3 +126,10 @@ url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c55847602
[[tools."npm:@openapitools/openapi-generator-cli"]]
version = "2.40.1"
backend = "npm:@openapitools/openapi-generator-cli"
[[tools.rust]]
version = "1.98.0"
backend = "core:rust"
[tools.rust.options]
targets = "aarch64-apple-ios,aarch64-apple-ios-sim,aarch64-linux-android,armv7-linux-androideabi,x86_64-apple-ios,x86_64-linux-android"
+12
View File
@@ -1,5 +1,7 @@
[tools]
java = "21.0.2"
# RUSTUP_TOOLCHAIN makes rustup ignore the targets in rust-toolchain.toml, so the cross targets are listed here too.
rust = { version = "1.98.0", targets = "aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios" }
[tools."aqua:flutter/flutter"]
version = "3.47.2"
@@ -34,6 +36,7 @@ depends = [
"codegen:dart",
"codegen:drift:migration",
"codegen:drift:schema",
"codegen:native",
"codegen:pigeon",
"codegen:translation",
]
@@ -80,6 +83,15 @@ alias = "watch"
description = "Watch and auto-generate dart code"
run = "dart run build_runner watch"
[tasks."codegen:native"]
description = "Generate the C header and the ffigen bindings for the native core"
dir = "../native/immich_native_core"
run = [
"cargo build -p immich_core_ffi",
"flutter pub get",
"dart run ffigen --config ffigen.yaml",
]
[tasks."codegen:pigeon"]
alias = "pigeon"
description = "Generate pigeon platform code"
+27 -4
View File
@@ -796,10 +796,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.2.0"
hooks_riverpod:
dependency: "direct main"
description:
@@ -928,6 +928,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2"
immich_native_core:
dependency: "direct main"
description:
path: "../native/immich_native_core"
relative: true
source: path
version: "0.0.0"
immich_ui:
dependency: "direct main"
description:
@@ -1140,6 +1147,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.1"
native_toolchain_rust:
dependency: transitive
description:
name: native_toolchain_rust
sha256: "8469a0e32a47a5c84b7fd300de9fd0e8082aac76c37922986324f5082a58350d"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
native_video_player:
dependency: "direct main"
description:
@@ -1458,10 +1473,10 @@ packages:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "1.1.1"
riverpod:
dependency: transitive
description:
@@ -1772,6 +1787,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.4"
toml:
dependency: transitive
description:
name: toml
sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e"
url: "https://pub.dev"
source: hosted
version: "0.18.0"
typed_data:
dependency: transitive
description:
+2
View File
@@ -39,6 +39,8 @@ dependencies:
hooks_riverpod: ^2.6.1
http: ^1.6.0
image_picker: ^1.2.3
immich_native_core:
path: ../native/immich_native_core
immich_ui:
path: './packages/ui'
intl: ^0.20.2
+3
View File
@@ -0,0 +1,3 @@
/target
/crates/immich_core_ffi/include/immich_core.h
/immich_native_core/lib/src/bindings.g.dart
+338
View File
@@ -0,0 +1,338 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "cbindgen"
version = "0.29.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20"
dependencies = [
"heck",
"indexmap",
"log",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 2.0.119",
"tempfile",
"toml",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "immich_core"
version = "0.1.0"
[[package]]
name = "immich_core_ffi"
version = "0.1.0"
dependencies = [
"cbindgen",
"immich_core",
]
[[package]]
name = "indexmap"
version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "toml"
version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow 0.7.15",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow 1.0.4",
]
[[package]]
name = "toml_writer"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+20
View File
@@ -0,0 +1,20 @@
[workspace]
resolver = "3"
members = ["crates/immich_core", "crates/immich_core_ffi"]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "AGPL-3.0-only"
[workspace.dependencies]
cbindgen = { version = "0.29", default-features = false }
[workspace.lints.clippy]
undocumented_unsafe_blocks = "deny"
# catch_unwind at the FFI boundary needs unwinding, so no panic = "abort" here.
[profile.release]
lto = true
codegen-units = 1
strip = true
+12
View File
@@ -0,0 +1,12 @@
# Native core
Rust code that ships inside the mobile app. Flutter compiles it from source through the
`immich_native_core` build hook, so building the app needs a Rust toolchain: `mise install`
in `mobile/` sets one up, otherwise install [rustup](https://rustup.rs) once and the build
fetches the pinned toolchain itself.
- `crates/immich_core`: the shared logic
- `crates/immich_core_ffi`: the C ABI, cbindgen writes `include/immich_core.h`
- `immich_native_core`: Flutter package with the build hook and the ffigen bindings
`mise run build`, `test`, `lint`, and `fmt`. The header and the Dart bindings come from `mise run //mobile:codegen:native`.
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "immich_core"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
+3
View File
@@ -0,0 +1,3 @@
pub fn core_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "immich_core_ffi"
version.workspace = true
edition.workspace = true
license.workspace = true
# native_toolchain_rust wants the explicit name and both crate types.
[lib]
name = "immich_core_ffi"
crate-type = ["cdylib", "staticlib"]
[dependencies]
immich_core = { path = "../immich_core" }
[build-dependencies]
cbindgen = { workspace = true }
[lints]
workspace = true
+11
View File
@@ -0,0 +1,11 @@
use std::path::Path;
fn main() {
// Files that define exported items. A directory here would make Flutter rerun the hook every build.
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=cbindgen.toml");
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::generate(&crate_dir)
.unwrap_or_else(|e| panic!("cbindgen failed: {e}"))
.write_to_file(Path::new(&crate_dir).join("include/immich_core.h"));
}
@@ -0,0 +1,3 @@
language = "C"
pragma_once = true
autogen_warning = "// Generated by cbindgen. Do not edit."
@@ -0,0 +1,17 @@
# Keep the channel in sync with the rust entries in native/mise.toml and mobile/mise.toml.
[toolchain]
channel = "1.98.0"
targets = [
"aarch64-linux-android",
"armv7-linux-androideabi",
"x86_64-linux-android",
"aarch64-apple-ios",
"aarch64-apple-ios-sim",
"x86_64-apple-ios",
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",
"aarch64-pc-windows-msvc",
"x86_64-pc-windows-msvc",
]
+56
View File
@@ -0,0 +1,56 @@
use std::ffi::{CString, c_char};
use std::panic::{self, UnwindSafe};
use std::ptr;
/// Returns the core version as a C string. Free it with `immich_core_free_string`.
#[unsafe(no_mangle)]
pub extern "C" fn immich_core_version() -> *mut c_char {
guard(ptr::null_mut(), || {
match CString::new(immich_core::core_version()) {
Ok(s) => s.into_raw(),
Err(_) => ptr::null_mut(),
}
})
}
/// Releases a string returned by this library. Null is a no-op.
///
/// # Safety
/// `ptr` must come from this library and must not be freed twice.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn immich_core_free_string(ptr: *mut c_char) {
if ptr.is_null() {
return;
}
guard((), || {
// SAFETY: the caller guarantees `ptr` came from `CString::into_raw` in this library.
drop(unsafe { CString::from_raw(ptr) });
});
}
fn guard<T>(fallback: T, f: impl FnOnce() -> T + UnwindSafe) -> T {
panic::catch_unwind(f).unwrap_or(fallback)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
#[test]
fn version_roundtrips() {
let p = immich_core_version();
assert!(!p.is_null());
// SAFETY: `p` is a C string from this library.
let s = unsafe { CStr::from_ptr(p) }.to_str().unwrap();
assert_eq!(s, immich_core::core_version());
// SAFETY: `p` is freed exactly once.
unsafe { immich_core_free_string(p) };
}
#[test]
fn free_null_is_noop() {
// SAFETY: null is allowed by the contract.
unsafe { immich_core_free_string(ptr::null_mut()) };
}
}
+3
View File
@@ -0,0 +1,3 @@
.dart_tool/
build/
pubspec.lock
@@ -0,0 +1,14 @@
include: package:flutter_lints/flutter.yaml
formatter:
page_width: 120
analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
+9
View File
@@ -0,0 +1,9 @@
# The asset id must match assetName in hook/build.dart.
ffi-native:
asset-id: 'package:immich_native_core/src/bindings.g.dart'
output: 'lib/src/bindings.g.dart'
headers:
entry-points:
- '../crates/immich_core_ffi/include/immich_core.h'
include-directives:
- '**/immich_core.h'
+77
View File
@@ -0,0 +1,77 @@
import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_rust/native_toolchain_rust.dart';
const _crate = '../crates/immich_core_ffi';
// Cargo's dep-info only lists source files, so these have to rerun the hook too.
const _manifests = [
'../Cargo.toml',
'../Cargo.lock',
'../crates/immich_core/Cargo.toml',
'$_crate/Cargo.toml',
'$_crate/rust-toolchain.toml',
];
void main(List<String> args) async {
await build(args, (input, output) async {
if (!input.config.buildCodeAssets) return;
final code = input.config.code;
output.dependencies.addAll(_manifests.map(input.packageRoot.resolve));
final target = _crossTarget(code);
if (target != null) {
await _rustup(['target', 'add', '--toolchain', await _channel(input), target]);
}
await RustBuilder(
assetName: 'src/bindings.g.dart',
cratePath: _crate,
extraCargoEnvironmentVariables: {if (code.targetOS == OS.android) 'RUSTFLAGS': _androidRustFlags(code)},
).run(input: input, output: output);
});
}
// mise sets RUSTUP_TOOLCHAIN, which makes rustup ignore rust-toolchain.toml and never install
// its targets, so add the one this build needs.
String? _crossTarget(CodeConfig code) => switch ((code.targetOS, code.targetArchitecture)) {
(OS.android, Architecture.arm) => 'armv7-linux-androideabi',
(OS.android, Architecture.arm64) => 'aarch64-linux-android',
(OS.android, Architecture.x64) => 'x86_64-linux-android',
(OS.iOS, Architecture.arm64) when code.iOS.targetSdk == IOSSdk.iPhoneSimulator => 'aarch64-apple-ios-sim',
(OS.iOS, Architecture.arm64) => 'aarch64-apple-ios',
(OS.iOS, Architecture.x64) => 'x86_64-apple-ios',
_ => null,
};
Future<String> _channel(BuildInput input) async {
final toml = await File.fromUri(input.packageRoot.resolve('$_crate/rust-toolchain.toml')).readAsString();
final match = RegExp(r'^channel = "([^"]+)"', multiLine: true).firstMatch(toml);
if (match == null) throw StateError('channel missing in rust-toolchain.toml');
return match.group(1)!;
}
Future<void> _rustup(List<String> args) async {
final home = Platform.environment[Platform.isWindows ? 'USERPROFILE' : 'HOME'];
ProcessResult result;
try {
result = await Process.run('rustup', args);
} on ProcessException {
if (home == null) rethrow;
result = await Process.run('$home/.cargo/bin/rustup', args);
}
if (result.exitCode != 0) {
throw ProcessException('rustup', args, '${result.stderr}', result.exitCode);
}
}
// Link against minSdk instead of the API 35 that native_toolchain_rust picks.
String _androidRustFlags(CodeConfig code) {
final triple = switch (code.targetArchitecture) {
Architecture.arm => 'armv7a-linux-androideabi',
Architecture.arm64 => 'aarch64-linux-android',
Architecture.x64 => 'x86_64-linux-android',
_ => throw UnsupportedError('Unsupported Android architecture: ${code.targetArchitecture}'),
};
return '-C link-arg=--target=$triple${code.android.targetNdkApi}';
}
@@ -0,0 +1,12 @@
import 'package:ffi/ffi.dart';
import 'src/bindings.g.dart';
String version() {
final ptr = immich_core_version();
try {
return ptr.cast<Utf8>().toDartString();
} finally {
immich_core_free_string(ptr);
}
}
+19
View File
@@ -0,0 +1,19 @@
name: immich_native_core
publish_to: none
environment:
sdk: '>=3.13.0 <4.0.0'
dependencies:
code_assets: ^1.2.1
ffi: ^2.2.0
flutter:
sdk: flutter
hooks: ^2.2.0
native_toolchain_rust: ^1.0.6
dev_dependencies:
ffigen: ^21.0.0
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
@@ -0,0 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_native_core/immich_native_core.dart';
void main() {
test('loads the core and returns its version', () {
expect(version(), isNotEmpty);
});
}
+8
View File
@@ -0,0 +1,8 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.rust]]
version = "1.98.0"
backend = "core:rust"
[tools.rust.options]
components = "clippy,rustfmt"
+18
View File
@@ -0,0 +1,18 @@
[tools]
rust = { version = "1.98.0", components = "rustfmt,clippy" } # keep in sync with crates/immich_core_ffi/rust-toolchain.toml
[tasks.build]
description = "Build the native crates for the host"
run = "cargo build --workspace"
[tasks.test]
description = "Run the Rust tests"
run = "cargo test --workspace --locked"
[tasks.fmt]
description = "Check Rust formatting"
run = "cargo fmt --all --check"
[tasks.lint]
description = "Run clippy with warnings as errors"
run = "cargo clippy --workspace --all-targets --locked -- -D warnings"