Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 28 additions & 36 deletions .github/workflows/ios-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,36 +72,7 @@ jobs:
node -e "require('fs').writeFileSync('tauri-version-override.json', JSON.stringify({ version: process.env.VERSION_OVERRIDE }))"

- name: Prepare ONNX Runtime iOS libs
env:
IOS_ORT_LIBS_URL: ${{ secrets.IOS_ORT_LIBS_URL }}
IOS_ORT_LIBS_TGZ_BASE64: ${{ secrets.IOS_ORT_LIBS_TGZ_BASE64 }}
run: |
set -euo pipefail

ARCHIVE="$RUNNER_TEMP/ort-ios.tgz"
EXTRACT_ROOT="$RUNNER_TEMP/ort-ios"
mkdir -p "$EXTRACT_ROOT"

if [ -n "${IOS_ORT_LIBS_URL:-}" ]; then
curl -fsSL "$IOS_ORT_LIBS_URL" -o "$ARCHIVE"
elif [ -n "${IOS_ORT_LIBS_TGZ_BASE64:-}" ]; then
printf '%s' "$IOS_ORT_LIBS_TGZ_BASE64" | base64 --decode > "$ARCHIVE"
else
echo "Missing iOS ONNX Runtime source. Set IOS_ORT_LIBS_URL or IOS_ORT_LIBS_TGZ_BASE64."
exit 1
fi

tar -xzf "$ARCHIVE" -C "$EXTRACT_ROOT"

CHILD_COUNT=$(find "$EXTRACT_ROOT" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')
if [ "$CHILD_COUNT" -eq 1 ] && [ -d "$(find "$EXTRACT_ROOT" -mindepth 1 -maxdepth 1 | head -1)" ]; then
ORT_LIB_LOCATION="$(find "$EXTRACT_ROOT" -mindepth 1 -maxdepth 1 | head -1)"
else
ORT_LIB_LOCATION="$EXTRACT_ROOT"
fi

echo "ORT_LIB_LOCATION=$ORT_LIB_LOCATION" >> "$GITHUB_ENV"
echo "Using ORT_LIB_LOCATION=$ORT_LIB_LOCATION"
run: bun run tauri:ios:prepare-onnxruntime

- name: Init iOS project
run: |
Expand Down Expand Up @@ -173,13 +144,13 @@ jobs:
import json, os
path = os.path.join(os.environ["RUNNER_TEMP"], "xcode-list.json")
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
schemes = data.get("workspace", {}).get("schemes", [])
if not schemes:
data = json.load(f)
schemes = data.get("workspace", {}).get("schemes", [])
if not schemes:
raise SystemExit("No schemes found in workspace.")
print(schemes[0])
PY
)
print(schemes[0])
PY
)

DERIVED_DATA="$RUNNER_TEMP/ios-derived-data"
ARCHIVE_PATH="$RUNNER_TEMP/LettuceAI.xcarchive"
Expand Down Expand Up @@ -233,9 +204,30 @@ jobs:
echo "IOS_IPA_PATH=$IPA_PATH" >> "$GITHUB_ENV"
echo "Built IPA at: $IPA_PATH"

- name: Generate AltStore metadata
run: |
set -euo pipefail

if [ -z "${IOS_IPA_PATH:-}" ]; then
echo "IOS_IPA_PATH is empty."
exit 1
fi

ALTSTORE_METADATA_PATH="$RUNNER_TEMP/altstore-metadata.json"
node scripts/generate-altstore-metadata.mjs "$IOS_IPA_PATH" "$ALTSTORE_METADATA_PATH"
echo "ALTSTORE_METADATA_PATH=$ALTSTORE_METADATA_PATH" >> "$GITHUB_ENV"
echo "Generated AltStore metadata at: $ALTSTORE_METADATA_PATH"

- name: Upload iOS IPA
uses: actions/upload-artifact@v4
with:
name: ios-ipa
if-no-files-found: error
path: ${{ env.IOS_IPA_PATH }}

- name: Upload AltStore metadata
uses: actions/upload-artifact@v4
with:
name: ios-altstore-metadata
if-no-files-found: error
path: ${{ env.ALTSTORE_METADATA_PATH }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
build

# Editor directories and files
.vscode/*
Expand All @@ -37,6 +38,7 @@ src-tauri/gen/android/app/src/main/assets/feedback_sounds/success.mp3
.act/
.tmp/
src-tauri/onnxruntime-ios/
src-tauri/scripts
src-tauri/gen/android/app/src/main/assets/onnxruntime/libonnxruntime.so
docs/memory-cycle-workflow.svg
docs/chatpkg-format.md
Expand Down
66 changes: 66 additions & 0 deletions scripts/generate-altstore-metadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env node

import { createHash } from "node:crypto";
import { readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

function fail(message) {
console.error(`[altstore] ${message}`);
process.exit(1);
}

function requireEnv(name) {
const value = process.env[name];
if (!value || !value.trim()) {
fail(`Missing required environment variable: ${name}`);
}
return value.trim();
}

async function main() {
const ipaPath = process.argv[2];
const outputPath = process.argv[3];
if (!ipaPath || !outputPath) {
fail("Usage: node scripts/generate-altstore-metadata.mjs <ipaPath> <outputPath>");
}

const artifactUrl = process.env.ALTSTORE_ARTIFACT_URL || "";
const scriptPath = fileURLToPath(import.meta.url);
const packageJsonPath = path.resolve(path.dirname(scriptPath), "..", "package.json");
const tauriConfigPath = path.resolve(path.dirname(scriptPath), "..", "src-tauri", "tauri.conf.json");
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
const tauriConfig = JSON.parse(await readFile(tauriConfigPath, "utf8"));
const version = process.env.ALTSTORE_VERSION || process.env.GITHUB_REF_NAME || packageJson.version || "0.0.0";
const buildVersion = process.env.ALTSTORE_BUILD_VERSION || String(process.env.GITHUB_RUN_NUMBER || "1");
const bundleIdentifier =
process.env.ALTSTORE_BUNDLE_ID || process.env.IOS_BUNDLE_IDENTIFIER || tauriConfig.identifier || "";
if (!bundleIdentifier) {
fail("Missing bundle identifier. Set ALTSTORE_BUNDLE_ID or IOS_BUNDLE_IDENTIFIER.");
}
const appName = process.env.ALTSTORE_APP_NAME || "LettuceAI";

const ipaBytes = await readFile(ipaPath);
const sha256 = createHash("sha256").update(ipaBytes).digest("hex");
const fileInfo = await stat(ipaPath);
const fileName = path.basename(ipaPath);

const metadata = {
name: appName,
bundleIdentifier,
version,
buildVersion,
fileName,
size: fileInfo.size,
sha256,
downloadURL: artifactUrl,
generatedAt: new Date().toISOString(),
};

await writeFile(outputPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
console.log(`[altstore] Metadata generated: ${outputPath}`);
}

main().catch((error) => {
fail(error instanceof Error ? error.message : String(error));
});
34 changes: 32 additions & 2 deletions scripts/install-onnxruntime-ios.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ async function exists(pathname) {
}
}

async function hasArArchiveMagic(pathname) {
if (!(await exists(pathname))) {
return false;
}
const header = await readFile(pathname);
if (header.length < 8) {
return false;
}
return header.subarray(0, 8).toString("utf8") === "!<arch>\n";
}

async function ensureArchive() {
if (await exists(archivePath)) {
return;
Expand All @@ -66,7 +77,7 @@ async function installSlice(slice) {

if ((await exists(libPath)) && (await exists(versionFile))) {
const currentVersion = (await readFile(versionFile, "utf8")).trim();
if (currentVersion === ORT_VERSION) {
if (currentVersion === ORT_VERSION && (await hasArArchiveMagic(libPath))) {
console.log(`[ios-ort] Using existing ${slice} install at ${sliceRoot}`);
return sliceRoot;
}
Expand All @@ -92,7 +103,26 @@ async function installSlice(slice) {
fail(`Expected ONNX Runtime binary is missing from archive for slice ${slice}`);
}

await copyFile(frameworkBinary, libPath);
const thinArchCandidates = slice === "ios-arm64" ? ["arm64"] : ["arm64", "x86_64"];
let thinned = false;
for (const arch of thinArchCandidates) {
const result = spawnSync("lipo", ["-thin", arch, frameworkBinary, "-output", libPath], {
stdio: "inherit",
cwd: repoRoot,
});
if (!result.error && result.status === 0) {
thinned = true;
break;
}
}
if (!thinned) {
await copyFile(frameworkBinary, libPath);
}

if (!(await hasArArchiveMagic(libPath))) {
fail(`Installed library for ${slice} is not an ar archive: ${libPath}`);
}

await writeFile(versionFile, `${ORT_VERSION}\n`, "utf8");
console.log(`[ios-ort] Installed ${slice} to ${sliceRoot}`);
return sliceRoot;
Expand Down
42 changes: 38 additions & 4 deletions scripts/run-tauri-ios-xcode-onnxruntime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
REPO_ROOT="${SCRIPT_DIR}"
while [ "${REPO_ROOT}" != "/" ] && [ ! -f "${REPO_ROOT}/package.json" ]; do
REPO_ROOT="$(dirname "${REPO_ROOT}")"
done
if [ ! -f "${REPO_ROOT}/package.json" ]; then
echo "[ios-ort] Failed to locate repository root from ${SCRIPT_DIR}" >&2
exit 1
fi
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.bun/bin:${PATH}"

SDKROOT_LOWER="$(printf '%s' "${SDKROOT:-}" | tr '[:upper:]' '[:lower:]')"

if [[ "${SDKROOT:-}" == *"iphonesimulator"* ]]; then
if [[ "${SDKROOT_LOWER}" == *"iphonesimulator"* ]]; then
ORT_SLICE="ios-arm64_x86_64-simulator"
elif [[ "${SDKROOT:-}" == *"iphoneos"* ]]; then
elif [[ "${SDKROOT_LOWER}" == *"iphoneos"* ]]; then
ORT_SLICE="ios-arm64"
else
echo "[ios-ort] Unsupported SDKROOT='${SDKROOT:-}'" >&2
Expand All @@ -15,8 +25,19 @@ fi

if command -v node >/dev/null 2>&1; then
JS_RUNNER="node"
NODE_BIN="$(command -v node)"
elif [ -x "/opt/homebrew/bin/node" ]; then
JS_RUNNER="/opt/homebrew/bin/node"
NODE_BIN="/opt/homebrew/bin/node"
elif [ -x "/usr/local/bin/node" ]; then
JS_RUNNER="/usr/local/bin/node"
NODE_BIN="/usr/local/bin/node"
elif command -v bun >/dev/null 2>&1; then
JS_RUNNER="bun"
NODE_BIN=""
elif [ -x "${HOME}/.bun/bin/bun" ]; then
JS_RUNNER="${HOME}/.bun/bin/bun"
NODE_BIN=""
else
echo "[ios-ort] Neither node nor bun is available in PATH." >&2
exit 1
Expand All @@ -29,4 +50,17 @@ export ORT_PREFER_DYNAMIC_LINK=0

echo "[ios-ort] ORT_LIB_LOCATION=${ORT_LIB_LOCATION}"

exec bun tauri ios xcode-script "$@"
if command -v bun >/dev/null 2>&1; then
exec bun tauri ios xcode-script "$@"
elif [ -x "${HOME}/.bun/bin/bun" ]; then
exec "${HOME}/.bun/bin/bun" tauri ios xcode-script "$@"
elif [ -n "${NODE_BIN}" ] && [ -f "${REPO_ROOT}/node_modules/@tauri-apps/cli/tauri.js" ]; then
exec "${NODE_BIN}" "${REPO_ROOT}/node_modules/@tauri-apps/cli/tauri.js" ios xcode-script "$@"
elif [ -x "${REPO_ROOT}/node_modules/.bin/tauri" ]; then
exec "${REPO_ROOT}/node_modules/.bin/tauri" ios xcode-script "$@"
elif command -v npx >/dev/null 2>&1; then
exec npx tauri ios xcode-script "$@"
else
echo "[ios-ort] Cannot run tauri CLI: bun, local tauri bin, and npx are all unavailable." >&2
exit 1
fi
2 changes: 2 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ fn macos_archive_candidates(target_arch: &str) -> Vec<(String, String)> {
}

fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=tauri.conf.json");
println!("cargo:rerun-if-env-changed=ORT_LIB_LOCATION");

let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
Expand Down
Loading