Skip to content

feat(perry/system): #675 — App Group / cross-process shared storage#974

Merged
proggeramlug merged 1 commit into
mainfrom
feat/675-app-group
May 18, 2026
Merged

feat(perry/system): #675 — App Group / cross-process shared storage#974
proggeramlug merged 1 commit into
mainfrom
feat/675-app-group

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #675 (MVP — see follow-up matrix).

Summary

Three new perry/system entry points so widgets, share extensions, watchOS targets, etc. can share key/value data with the host app:

import { appGroupSet, appGroupGet, appGroupDelete } from "perry/system";

appGroupSet("sessionToken", "abc123");
const t = appGroupGet("sessionToken");  // "abc123" — "" if absent
appGroupDelete("sessionToken");

Without this, the only official way to wire a Perry app to a Perry-built widget extension was to hand-write a Rust+Swift bridge per project. The issue calls this out as a steep onboarding cliff that creates a forest of subtly-different forks across the ecosystem.

Cross-platform coverage

Platform Status
macOS Real implNSUserDefaults(suiteName:), default suite name "group.perryapp" overridable via PERRY_APP_GROUP env var
iOS In-process HashMap MVP (API exercisable in dev/tests) + #675 follow-up warning
tvOS Stub (tvOS user apps rarely share data cross-process)
watchOS Stub + #675 follow-up
visionOS Stub + #675 follow-up
Android Stub + #675 follow-up (SharedPreferences via JNI)
Windows Stub + #675 follow-up (%LOCALAPPDATA%-backed kv)
Linux/GTK4 Stub + #675 follow-up (XDG-backed kv)
WASM Stub via dispatch table fallback
HarmonyOS Stub auto-generated by perry-runtime/build.rs

Every stub funnels through perry_runtime::stub_diag::perry_stub_warn so first call per process per symbol prints a [perry] warning line naming the platform and the #675 tracker.

macOS implementation detail

NSUserDefaults initWithSuiteName:setObject:forKey: / stringForKey: / removeObjectForKey:synchronize. The suite name is read from the PERRY_APP_GROUP environment variable; defaults to "group.perryapp".

For dev builds without the application-groups entitlement, the suite still works — it just stores under ~/Library/Preferences/group.perryapp.plist instead of inside the shared App Group container. That makes the API usable in local iteration without setting up code-signing.

appGroupGet returns the empty string when the key is absent (per the issue's contract). NSString → UTF-8 bytes via UTF8String + lengthOfBytesUsingEncoding:NSUTF8StringEncoding (= 4).

Plumbing (mirrors #918 / #917 pattern)

  • perry-api-manifest/src/entries.rs — three method rows.
  • perry-dispatch/src/lib.rs — three MethodRow rows (set: [Str, Str] → Void, get: [Str] → Str, delete: [Str] → Void). WASM falls through to ui_method_to_runtime() — symbol mapping for free.
  • Per-platform exports across all UI crates.
  • perry-ui-test/src/lib.rs — three Feature rows.
  • HarmonyOS auto-stubbed via perry-runtime/build.rs.

Smoke test

App({body}) + appGroupSet + appGroupGet + appGroupDelete against release perry compiles + links cleanly; symbol resolution verified end-to-end.

What's deferred (#675 follow-ups)

  • writeBlob/readBlob — file-backed shared data the issue calls out (FileManager.containerURL on iOS, XDG on Linux, etc.).
  • Real iOS / Android / Windows / Linux / visionOS impls.
  • perry.toml [ios].app_group + [android].shared_prefs_name configuration plumbing — currently hardcoded to the PERRY_APP_GROUP env var on macOS.
  • Entitlement merge into Perry's signing pipeline for App Group capability.

Notes

No Cargo.toml version bump, no CLAUDE.md touch, no CHANGELOG.md entry — maintainer folds those in at merge time.

Exposes three perry/system entry points so widgets, share
extensions, watchOS targets, etc. can share data with the host app:

  import { appGroupSet, appGroupGet, appGroupDelete } from "perry/system";

  appGroupSet("sessionToken", "abc123");
  const t = appGroupGet("sessionToken");  // "abc123" — "" if absent
  appGroupDelete("sessionToken");

Without this, the only official way to wire a Perry app to a
Perry-built widget extension was to hand-write a Rust+Swift bridge
per project. Per the issue, this is a steep onboarding cliff that
creates a forest of subtly-different forks across the ecosystem.

| Platform   | Status                                                       |
|------------|--------------------------------------------------------------|
| **macOS**  | Real impl: NSUserDefaults(suiteName:), default suite name    |
|            | "group.perryapp" overridable via PERRY_APP_GROUP env var.  |
| iOS        | In-process HashMap MVP + #675 follow-up warning              |
| tvOS       | Stub (tvOS user apps rarely share data cross-process)       |
| watchOS    | Stub + #675 follow-up (UserDefaults(suiteName:) via         |
|            | iOS host's App Group entitlement)                            |
| visionOS   | Stub + #675 follow-up (UIKit UserDefaults path)              |
| Android    | Stub + #675 follow-up (SharedPreferences via JNI)            |
| Windows    | Stub + #675 follow-up (%LOCALAPPDATA%-backed kv)             |
| Linux/GTK4 | Stub + #675 follow-up (XDG-backed kv)                        |
| WASM       | Stub via dispatch table fallback                             |
| HarmonyOS  | Stub auto-generated by perry-runtime/build.rs               |

Every stub funnels through perry_runtime::stub_diag::perry_stub_warn
so first call per process per symbol prints a [perry] warning naming
the platform and the #675 tracker.

NSUserDefaults initWithSuiteName: → setObject:forKey: / stringForKey:
/ removeObjectForKey: → synchronize. The suite name is read from
the PERRY_APP_GROUP environment variable (so an app's signing
identity can override the default without a recompile); when unset,
falls back to "group.perryapp".

For dev builds without the application-groups entitlement, the suite
still works — it just stores under
~/Library/Preferences/group.perryapp.plist instead of inside the
shared App Group container. That makes the API usable in local
iteration without setting up code-signing.

Get returns the empty string when the key is absent (per the issue's
contract). NSString -> UTF-8 bytes via UTF8String +
lengthOfBytesUsingEncoding:NSUTF8StringEncoding (= 4).

- perry-api-manifest/src/entries.rs — three method rows.
- perry-dispatch/src/lib.rs — three MethodRows (set: [Str, Str]
  → Void, get: [Str] → Str, delete: [Str] → Void). WASM falls
  through to ui_method_to_runtime() in perry-codegen-wasm/src/emit.rs
  — symbol mapping for free.
- Per-platform exports: perry-ui-macos (real),
  perry-ui-ios (in-process HashMap MVP), and stubs in tvos,
  watchos, visionos, android, gtk4, windows.
- perry-ui-test/src/lib.rs — three Feature rows.
- HarmonyOS auto-stubbed by perry-runtime/build.rs from the dispatch
  table.

Compiled App({body}) + appGroupSet + appGroupGet + appGroupDelete
against release perry; symbols resolve end-to-end, link succeeds,
binary writes cleanly.

- writeBlob/readBlob — for file-backed shared data the issue calls
  out (FileManager containerURL on iOS, XDG on Linux, etc.).
- Real iOS / Android / Windows / Linux / visionOS impls.
- perry.toml [ios].app_group + [android].shared_prefs_name
  configuration plumbing — currently hardcoded to the
  PERRY_APP_GROUP env var on macOS.
- Entitlement merge into Perry's signing pipeline for App Group
  capability.
@proggeramlug proggeramlug merged commit 5978d02 into main May 18, 2026
@proggeramlug proggeramlug deleted the feat/675-app-group branch May 18, 2026 10:13
proggeramlug added a commit that referenced this pull request May 18, 2026
…Rs (#1019)

#981 (PERRY_SANDBOX_BUILDRS), #988 (--emit-attest), and #969
(perry.permissions) each landed via admin-bypass with their
SUMMARY.md entries intact but without the actual .md content files
(or, for #969, without any docs entry at all). docs/src/cli/lockdown.md
and docs/src/cli/emit-sandbox.md did make it into main and are
fine; the others left dead links.

Separately, #976 / #972 / #974 added perry/system runtime methods
(getOSVersion, shareText, shareUrl, appGroupSet/Get/Delete) but
never updated the hand-maintained types/perry/system/index.d.ts.
TypeScript users importing those APIs from `perry/system` get a
type error today.

This PR:
- Creates docs/src/cli/sandbox-buildrs.md (#505)
- Creates docs/src/cli/emit-attest.md (#504)
- Creates docs/src/cli/capabilities.md (#501) and adds the SUMMARY.md entry
- Adds the six new perry/system signatures to types/perry/system/index.d.ts

The auto-generated docs/api/perry.d.ts + docs/src/api/reference.md
were regenerated during the original PRs and are already current.

Pure docs-only diff. No code changes.
proggeramlug added a commit that referenced this pull request May 18, 2026
…from #974 rebase merge)

The merge of #974 (#675 app-group) into main left several files with
unclosed delimiters: every platform-stub crate's perry_system_share_url
function was missing its closing );} and perry-ui-test's FEATURES list
had two Feature entries collapsed together.

Without this fix, cargo fmt --check --all errors out on every PR,
blocking every PR's lint gate.

- perry-ui-android/gtk4/ios/tvos/visionos/watchos/windows: close the
  stub function bodies that lost their delimiters in the merge.
- perry-ui-test: reconstruct the share_text + share_url Feature
  entries (which had been merged with app_group_set and app_group_get
  respectively) and split out a proper app_group_get entry.
proggeramlug added a commit that referenced this pull request May 18, 2026
…from #974 rebase merge)

The merge of #974 (#675 app-group) into main left several files with
unclosed delimiters: every platform-stub crate's perry_system_share_url
function was missing its closing );} and perry-ui-test's FEATURES list
had two Feature entries collapsed together.

Without this fix, cargo fmt --check --all errors out on every PR,
blocking every PR's lint gate.

- perry-ui-android/gtk4/ios/tvos/visionos/watchos/windows: close the
  stub function bodies that lost their delimiters in the merge.
- perry-ui-test: reconstruct the share_text + share_url Feature
  entries (which had been merged with app_group_set and app_group_get
  respectively) and split out a proper app_group_get entry.
proggeramlug added a commit that referenced this pull request May 18, 2026
…from #974 rebase merge) (#1020)

The merge of #974 (#675 app-group) into main left several files with
unclosed delimiters: every platform-stub crate's perry_system_share_url
function was missing its closing );} and perry-ui-test's FEATURES list
had two Feature entries collapsed together.

Without this fix, cargo fmt --check --all errors out on every PR,
blocking every PR's lint gate.

- perry-ui-android/gtk4/ios/tvos/visionos/watchos/windows: close the
  stub function bodies that lost their delimiters in the merge.
- perry-ui-test: reconstruct the share_text + share_url Feature
  entries (which had been merged with app_group_set and app_group_get
  respectively) and split out a proper app_group_get entry.
proggeramlug added a commit that referenced this pull request May 18, 2026
…rry widget init + iOS .appex embed) (#1018)

* feat(widget): #676 — WidgetKit build glue (perry.toml [[widget]] + perry widget init + iOS .appex embed)

Adds the `[[widget]]` schema to `perry.toml`, a `perry widget init <name>`
scaffolder for SwiftUI WidgetKit boilerplate, and a build hook that
invokes `swiftc` per declared widget during `perry compile --target ios`
and embeds the produced `.appex` into `<output>.app/Frameworks/`.

Scope (iOS-only v1):
- `[[widget]]` parser (`widget_build::WidgetEntry`) supports `name`,
  `swift_source`, `watchos_source`, `glance_source`, `display_name`,
  `description`, `intents`.
- `perry widget init <Name>` writes `<Name>Widget.swift` (TimelineProvider
  + WidgetEntryView + `@main` Widget) and `<Name>Intent.swift`
  (WidgetConfigurationIntent stub), then appends the matching
  `[[widget]]` block to the nearest `perry.toml`.
- iOS `.app` assembly invokes swiftc per widget with `iphoneos`/
  `iphonesimulator` SDK, embeds the resulting binary at
  `Frameworks/<Name>.appex/<Name>` with a generated Info.plist that
  carries `NSExtensionPointIdentifier = com.apple.widgetkit-extension`.

Deferred to follow-ups (warn-and-skip in v1):
- watchOS build path (`watchos_source` slot accepted but ignored).
- Android Glance build path (`glance_source` slot accepted but ignored).
- Data-sharing wiring (App Group plumbing lives under #675).
- Separate `@perryts/widgets` npm package skeleton.

Tests cover schema parsing (minimal + full entries), Info.plist key
emission, swift-file discovery filtering, and the scaffolder humanize
helper.

Closes #676 (iOS slice; watchOS / Android Glance / data-sharing tracked
as explicit non-goals for this PR per the issue scope).

* style: apply cargo fmt across touched files and stale main violations

* fix(ui): repair broken share_url stubs + perry-ui-test Feature list (from #974 rebase merge)

The merge of #974 (#675 app-group) into main left several files with
unclosed delimiters: every platform-stub crate's perry_system_share_url
function was missing its closing );} and perry-ui-test's FEATURES list
had two Feature entries collapsed together.

Without this fix, cargo fmt --check --all errors out on every PR,
blocking every PR's lint gate.

- perry-ui-android/gtk4/ios/tvos/visionos/watchos/windows: close the
  stub function bodies that lost their delimiters in the merge.
- perry-ui-test: reconstruct the share_text + share_url Feature
  entries (which had been merged with app_group_set and app_group_get
  respectively) and split out a proper app_group_get entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: official @perryts/app-group for cross-process shared storage

1 participant