Skip to content
Merged
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
8 changes: 7 additions & 1 deletion .github/workflows/auto_upstream_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,20 @@ jobs:
fi

- name: Dispatch native release
if: steps.existing.outputs.exists == 'false' || steps.complete.outputs.complete == 'false'
if: steps.existing.outputs.exists == 'false'
env:
GH_TOKEN: ${{ secrets.LITERT_LM_RELEASE_TOKEN || github.token }}
run: |
gh workflow run native_release.yml \
--repo "${{ github.repository }}" \
-f upstream_tag="${{ steps.upstream.outputs.tag }}"

- name: Existing release is incomplete
if: steps.existing.outputs.exists == 'true' && steps.complete.outputs.complete == 'false'
run: |
echo "Release ${{ steps.upstream.outputs.tag }} exists but does not match current validation."
echo "Skipping automatic rebuild to avoid mutating an existing release tag."

- name: No-op when release is complete
if: steps.existing.outputs.exists == 'true' && steps.complete.outputs.complete == 'true'
run: echo "Release ${{ steps.upstream.outputs.tag }} already exists and is complete."
71 changes: 56 additions & 15 deletions .github/workflows/native_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
description: "google-ai-edge/LiteRT-LM tag to package"
required: true
type: string
release_tag:
description: "Native release tag to publish; defaults to upstream_tag"
required: false
default: ""
type: string
prerelease:
description: "Publish as a prerelease"
required: false
Expand Down Expand Up @@ -136,6 +141,16 @@ jobs:
git fetch --depth=1 origin "${{ github.sha }}"
git checkout --detach FETCH_HEAD

- name: Resolve native release tag
id: release
shell: bash
run: |
release_tag="${{ inputs.release_tag }}"
if [ -z "$release_tag" ]; then
release_tag="${{ inputs.upstream_tag }}"
fi
echo "tag=${release_tag}" >> "$GITHUB_OUTPUT"

- name: Package upstream prebuilt libraries
run: |
python3 tools/package_upstream_prebuilts.py \
Expand Down Expand Up @@ -169,36 +184,44 @@ jobs:
--upstream-tag "${{ inputs.upstream_tag }}" \
--clean

- name: Package official macOS runtime
run: |
python3 tools/package_macos_runtime.py \
--upstream-tag "${{ inputs.upstream_tag }}" \
--clean

- name: Package Apple SPM XCFrameworks
run: |
python3 tools/package_apple_xcframeworks.py \
--upstream-tag "${{ inputs.upstream_tag }}" \
--release-tag "${{ steps.release.outputs.tag }}" \
--clean

- name: Generate manifest and checksums
run: |
python3 tools/validate_runtime_artifacts.py --upstream-tag "${{ inputs.upstream_tag }}"
python3 tools/validate_runtime_dependencies.py
python3 tools/package_release.py --upstream-tag "${{ inputs.upstream_tag }}"
python3 tools/package_release.py \
--upstream-tag "${{ inputs.upstream_tag }}" \
--release-tag "${{ steps.release.outputs.tag }}"
python3 tools/validate_artifacts.py

- name: Create release archives
run: |
mkdir -p release
tar -czf "release/litert-lm-native-prebuilts-${{ inputs.upstream_tag }}.tar.gz" \
tar -czf "release/litert-lm-native-prebuilts-${{ steps.release.outputs.tag }}.tar.gz" \
bin manifest.json SHA256SUMS
while IFS= read -r runtime_dir; do
platform="$(basename "$(dirname "$runtime_dir")")"
arch="$(basename "$runtime_dir")"
tar -czf "release/litert-lm-native-runtime-${platform}-${arch}-${{ inputs.upstream_tag }}.tar.gz" \
tar -czf "release/litert-lm-native-runtime-${platform}-${arch}-${{ steps.release.outputs.tag }}.tar.gz" \
-C bin "${platform}/${arch}"
done < <(find bin -mindepth 2 -maxdepth 2 -type d | sort)
if [ -d dist ]; then
tar -czf "release/litert-lm-native-official-assets-${{ inputs.upstream_tag }}.tar.gz" \
tar -czf "release/litert-lm-native-official-assets-${{ steps.release.outputs.tag }}.tar.gz" \
dist
fi
if [ -d "dist/spm/${{ inputs.upstream_tag }}" ]; then
cp dist/spm/${{ inputs.upstream_tag }}/*.zip release/
if [ -d "dist/spm/${{ steps.release.outputs.tag }}" ]; then
cp dist/spm/${{ steps.release.outputs.tag }}/*.zip release/
fi

- name: Publish GitHub release
Expand All @@ -209,24 +232,41 @@ jobs:
if [ "${{ inputs.prerelease }}" = "true" ]; then
args+=(--prerelease)
fi
notes="Packaged LiteRT-LM upstream runtime libraries, upstream prebuilts, Apple SPM XCFrameworks, and official release assets for ${{ inputs.upstream_tag }}."
if gh release view "${{ inputs.upstream_tag }}" \
notes="Packaged LiteRT-LM upstream ${{ inputs.upstream_tag }} runtime libraries, upstream prebuilts, Apple SPM XCFrameworks, and official release assets as ${{ steps.release.outputs.tag }}."
if gh release view "${{ steps.release.outputs.tag }}" \
--repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release edit "${{ inputs.upstream_tag }}" \
gh release edit "${{ steps.release.outputs.tag }}" \
--repo "${{ github.repository }}" \
--title "LiteRT-LM ${{ inputs.upstream_tag }}" \
--title "LiteRT-LM ${{ steps.release.outputs.tag }}" \
--notes "$notes" \
"${args[@]}"
gh release upload "${{ inputs.upstream_tag }}" \
intended_assets="$(mktemp)"
{
find release -maxdepth 1 -type f -exec basename {} \;
printf '%s\n' manifest.json SHA256SUMS
} | sort > "$intended_assets"
while IFS= read -r asset; do
if ! grep -Fxq "$asset" "$intended_assets"; then
gh release delete-asset "${{ steps.release.outputs.tag }}" "$asset" \
--repo "${{ github.repository }}" \
--yes
fi
done < <(
gh release view "${{ steps.release.outputs.tag }}" \
--repo "${{ github.repository }}" \
--json assets \
--jq '.assets[].name'
)
gh release upload "${{ steps.release.outputs.tag }}" \
--repo "${{ github.repository }}" \
--clobber \
release/* \
manifest.json \
SHA256SUMS
else
gh release create "${{ inputs.upstream_tag }}" \
gh release create "${{ steps.release.outputs.tag }}" \
--repo "${{ github.repository }}" \
--title "LiteRT-LM ${{ inputs.upstream_tag }}" \
--title "LiteRT-LM ${{ steps.release.outputs.tag }}" \
--notes "$notes" \
"${args[@]}" \
release/* \
Expand All @@ -244,7 +284,7 @@ jobs:
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ inputs.upstream_tag }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ steps.release.outputs.tag }}" \
-o published-release-check/release.json
manifest_url="$(python3 - <<'PY'
import json
Expand All @@ -260,4 +300,5 @@ jobs:
python3 tools/validate_release_manifest.py \
published-release-check/manifest.json \
--upstream-tag "${{ inputs.upstream_tag }}" \
--release-tag "${{ steps.release.outputs.tag }}" \
--release-metadata published-release-check/release.json
62 changes: 43 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ GPU/NPU validation; web should use JavaScript interop instead of FFI.
`CLiteRTLM.xcframework` slices and stages `LiteRtLm.framework` plus
`CLiteRTLM.framework`, with the wrapper embedding LiteRtLmBridge symbols and
re-exporting `CLiteRTLM`.
- `tools/package_apple_xcframeworks.py`: packages iOS framework wrappers, a
macOS `LiteRtLm.framework` wrapper around the source-built runtime, and macOS
companion dylibs as SPM-compatible XCFramework zip assets.
- `tools/package_macos_runtime.py`: extracts official upstream
`CLiteRTLM_mac.xcframework` slices and stages bridge-enabled
`libLiteRtLm.dylib` wrappers that re-export `libCLiteRTLM_mac.dylib`.
- `tools/package_apple_xcframeworks.py`: packages iOS framework wrappers and
macOS bridge wrappers as SPM-compatible XCFramework zip assets.
- `tools/package_release.py`: builds local manifest and checksums.
- `tools/validate_artifacts.py`: validates manifest, checksums, and layout.
- `docs/platform_strategy.md`: platform and distribution strategy.
Expand Down Expand Up @@ -99,27 +101,51 @@ python3 tools/validate_artifacts.py
Android arm64/x64, macOS arm64/x64, Linux x64/arm64, and Windows x64, copies
upstream `prebuilt/` companion libraries for Android, Apple, Linux, and
Windows, converts official upstream `CLiteRTLM.xcframework` slices into iOS
framework runtime archives with an embedded LiteRtLmBridge wrapper, packages
Apple SPM XCFramework zips from the same runtime payloads, includes the
official upstream release assets, then publishes a GitHub release with
`manifest.json` and `SHA256SUMS`.
framework runtime archives with an embedded LiteRtLmBridge wrapper, converts
official upstream `CLiteRTLM_mac.xcframework` slices into macOS runtime
archives with an embedded LiteRtLmBridge wrapper, packages Apple SPM
XCFramework zips from the same runtime payloads, includes the official
upstream release assets, then publishes a GitHub release with `manifest.json`
and `SHA256SUMS`. The workflow accepts a separate `release_tag`; use it when
repackaging the same upstream tag without mutating an existing native release.
- `Auto Upstream Release`: runs daily and dispatches `Native Build & Release`
when `google-ai-edge/LiteRT-LM` has a latest release tag that this repo has
not published yet.
not published yet. Existing releases are treated as immutable; if validation
rules change and an existing release no longer matches, the scheduled workflow
reports it but does not overwrite the tag automatically.

## Native Version Management

The published upstream tag is the native version contract consumed by downstream
package hooks and Swift Package manifests. When moving to a new LiteRT-LM tag:
The published native release tag is the version contract consumed by downstream
package hooks and Swift Package manifests. For the first package of an upstream
LiteRT-LM tag, the native release tag normally matches the upstream tag. If a
packaging fix is needed for the same upstream sources, publish a new native
release tag such as `v0.13.1-native.1` instead of overwriting `v0.13.1`.

1. Run `Native Build & Release` for `upstream_tag`, or let
`Auto Upstream Release` dispatch it for the latest upstream release.
When moving to a new LiteRT-LM tag:

1. Run `Native Build & Release` for `upstream_tag`, or let `Auto Upstream
Release` dispatch it for the latest upstream release.
2. Verify the release contains runtime archives, official upstream assets,
Apple SPM XCFramework zips, `manifest.json`, and `SHA256SUMS`.
3. Update downstream `llamadart` hook pins, SPM URLs, and SPM checksums
together so native-assets and SPM consumers use the same bridge-enabled
runtime build.

To publish a corrected package for existing upstream sources without breaking
downstream checksum pins, dispatch the workflow with both tags:

```bash
gh workflow run native_release.yml \
--repo leehack/litert-lm-native \
--ref main \
-f upstream_tag=v0.13.1 \
-f release_tag=v0.13.1-native.1 \
-f prerelease=false \
-f target_platform=all \
-f target_arch=all
```

The release workflow uses upstream's public C API (`c/engine.h`) as the
production FFI boundary. Downstream loaders should bind directly to the runtime
library for the selected platform. Source-built native runtimes are assembled
Expand All @@ -133,13 +159,11 @@ asynchronous callback loaders.
Apple SPM consumers should depend on the release's direct
`litert-lm-native-apple-*-xcframework-<tag>.zip` assets. The `LiteRtLm`
XCFramework contains the primary iOS wrapper and a macOS framework wrapper
around the source-built runtime. `CLiteRTLM` is iOS-only and is re-exported by
the iOS wrapper. macOS companion dylibs are published as separate XCFramework
targets when the native release payload contains them. Downstream macOS SPM
integration must account for architecture coverage and deployment targets; for
example, upstream `v0.13.1` provides extra macOS companion dylibs for arm64,
while x64 only requires `libLiteRtLm.dylib` plus `libLiteRt.dylib`, and the
macOS dylibs are built for macOS 14.
around the official `CLiteRTLM_mac` runtime. `CLiteRTLM` is re-exported by the
iOS wrapper, and `CLiteRTLMMac` is re-exported by the macOS wrapper. Downstream
macOS SPM integration must account for architecture coverage and deployment
targets; upstream `v0.13.1` provides a universal `CLiteRTLM_mac.xcframework`,
and the packaged macOS wrapper targets macOS 14.

## Consumer Contract

Expand Down
30 changes: 19 additions & 11 deletions docs/platform_strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ The release automation publishes these runtime artifact groups:
- iOS framework-style runtime wrappers derived from `CLiteRTLM.xcframework`,
with LiteRtLmBridge symbols embedded in `LiteRtLm.framework/LiteRtLm` and
upstream symbols re-exported from `CLiteRTLM.framework/CLiteRTLM`
- macOS dylib runtime wrappers derived from `CLiteRTLM_mac.xcframework`, with
LiteRtLmBridge symbols embedded in `libLiteRtLm.dylib` and upstream symbols
re-exported from `libCLiteRTLM_mac.dylib`
- Apple Swift Package Manager XCFramework zips produced from the same iOS
wrappers, macOS source-built runtime, and macOS companion dylibs used by the
native-assets payloads
wrappers and official macOS wrappers used by the native-assets payloads

Native release tags are immutable consumer contracts. Use a separate native
release tag when repackaging the same upstream source tag, for example
`upstream_tag=v0.13.1` with `release_tag=v0.13.1-native.1`, so downstream
packages with pinned checksums keep resolving the original artifacts.

The upstream C runtime is the production FFI target for downstream packages.
LiteRtLmBridge is limited to narrow FFI helpers around that runtime surface. It
Expand All @@ -29,17 +36,17 @@ build the repo-owned bridge package alongside upstream LiteRT-LM, without
patching upstream source files.

SPM artifacts are intentionally split by binary target. `LiteRtLm` carries the
primary iOS runtime/wrapper and a macOS framework wrapper around the
source-built runtime. `CLiteRTLM` is published for iOS re-export support. macOS
companion dylibs are published as separate XCFramework targets when the native
release payload contains them.
primary iOS runtime/wrapper and a macOS framework wrapper around the official
macOS runtime. `CLiteRTLM` is published for iOS re-export support, and
`CLiteRTLMMac` is published for macOS re-export support.

The macOS LiteRT-LM SPM path must account for the architecture coverage of the
native payload. Upstream `v0.13.1` publishes arm64 macOS companion dylibs, while
the x64 source-built runtime links only to `libLiteRt.dylib`; those macOS dylibs
are built for macOS 14. Keep native-assets runtime archives as the source of
truth, and only wire macOS SPM dependencies in downstream packages when the
required binary targets cover the selected architecture and deployment target.
native payload. Upstream `v0.13.1` publishes a universal
`CLiteRTLM_mac.xcframework`; the packaged macOS wrapper is universal across
arm64 and x64 and targets macOS 14. Keep native-assets runtime archives as the
source of truth, and only wire macOS SPM dependencies in downstream packages
when the required binary targets cover the selected architecture and deployment
target.

Initial native targets:

Expand Down Expand Up @@ -75,6 +82,7 @@ Each artifact entry records:

- runtime: `native` or `web`
- platform and architecture
- native release tag
- upstream LiteRT-LM tag
- file name and SHA-256
- library names required by loaders
Expand Down
26 changes: 18 additions & 8 deletions tools/package_apple_xcframeworks.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,8 @@ def package_macos_companions(
return packaged


def package_all(upstream_tag: str, clean: bool) -> list[Path]:
output_dir = DIST_DIR / upstream_tag
def package_all(release_tag: str, clean: bool) -> list[Path]:
output_dir = DIST_DIR / release_tag
if clean and WORK_DIR.exists():
shutil.rmtree(WORK_DIR)
if clean and output_dir.exists():
Expand All @@ -325,7 +325,7 @@ def package_all(upstream_tag: str, clean: bool) -> list[Path]:
PRIMARY_MODULE,
WORK_DIR,
output_dir,
upstream_tag,
release_tag,
extra_args=primary_macos_args,
)
if primary is None and primary_macos_args:
Expand All @@ -334,7 +334,7 @@ def package_all(upstream_tag: str, clean: bool) -> list[Path]:
primary_macos_args,
WORK_DIR,
output_dir,
upstream_tag,
release_tag,
)
if primary is None:
raise RuntimeError("Could not package LiteRtLm: no iOS or macOS runtime found")
Expand All @@ -344,24 +344,34 @@ def package_all(upstream_tag: str, clean: bool) -> list[Path]:
IOS_REEXPORT_MODULE,
WORK_DIR,
output_dir,
upstream_tag,
release_tag,
)
if clitertlm is not None:
packaged.append(clitertlm)

packaged.extend(package_macos_companions(WORK_DIR, output_dir, upstream_tag))
packaged.extend(package_macos_companions(WORK_DIR, output_dir, release_tag))
return packaged


def main() -> int:
parser = argparse.ArgumentParser(
description="Package Apple LiteRT-LM runtimes as SPM-compatible XCFramework zips."
)
parser.add_argument("--upstream-tag", required=True)
parser.add_argument(
"--release-tag",
help="Native release tag to use for output directory and asset names.",
)
parser.add_argument(
"--upstream-tag",
help="Deprecated alias for --release-tag.",
)
parser.add_argument("--clean", action="store_true")
args = parser.parse_args()

packaged = package_all(args.upstream_tag, clean=args.clean)
release_tag = args.release_tag or args.upstream_tag
if not release_tag:
parser.error("--release-tag is required")
packaged = package_all(release_tag, clean=args.clean)
if not packaged:
raise RuntimeError("No Apple XCFramework zips were produced")
for path in packaged:
Expand Down
Loading