fix(build): improve macOS packaging reliability and clean up Rust FFI

- Implement 'inside-out' signing strategy in package-flutter-mac-app.sh to fix nested code validity errors
- Fix install_name of embedded FFI library to use @rpath for portability
- Remove manual 'cargo build' triggers from Makefile and integration scripts (externalize management)
- Clean up unused types and structs in Rust source (lib.rs and types.rs)
- Update architecture docs to reflect AcpBridgeServerModeConfig priority logic
This commit is contained in:
Haitao Pan 2026-04-19 15:00:34 +08:00
parent 1f13ce7514
commit 1f6d8bd4e0
7 changed files with 105 additions and 70 deletions

View File

@ -129,11 +129,10 @@ check-export-compliance: ## Verify source and built Apple plist export-complianc
rust-build: rust-build-release ## Build Rust FFI library (release mode)
rust-build-release: ## Build Rust FFI library for macOS (release)
cd rust && cargo build --release
@echo "Rust FFI library built successfully"
@echo "Skip cargo build (external management)"
rust-build-debug: ## Build Rust FFI library in debug mode
cd rust && cargo build
@echo "Skip cargo build (external management)"
rust-test: ## Run Rust tests
cd rust && cargo test
@ -144,7 +143,7 @@ ffi-copy: ## Copy FFI library to macOS Frameworks
ffi-generate: ## Generate FFI bindings using flutter_rust_bridge
bash scripts/generate_ffi_bindings.sh
ffi-integrate: rust-build-release ffi-copy ## Build and copy FFI library (full integration)
ffi-integrate: ffi-copy ## Copy FFI library (full integration)
# Build with FFI integration
build-macos-ffi: rust-build-release ffi-copy build-macos ## Build macOS app with FFI integration
build-macos-ffi: ffi-copy build-macos ## Build macOS app with FFI integration

View File

@ -1,14 +1,18 @@
# Settings Integration Configuration Model
Last Updated: 2026-04-14
Last Updated: 2026-04-19
本文件记录当前 `Settings -> Integrations` 在主链中的职责边界。
本文件记录当前 `Settings -> Integrations` 在主链中的职责边界,以及
`acpBridgeServerModeConfig` 的有效配置仲裁规则。
## Current Rule
- Settings 只管理 bridge connection 参数与 account sync 元数据
- Settings 只管理 Bridge 连接参数、account sync 元数据和本地编辑态
- `AcpBridgeServerModeConfig.effective` 是运行时实际生效配置
- `selfHosted` 优先级高于 `cloudSynced`
- `cloudSynced` 只在 manual Bridge 未配置时作为有效回退来源
- app 不从本地 endpoint preset、旧 module 配置、历史 fallback 恢复 provider catalog
- `xworkmate-bridge` 是 provider catalog、gateway capability、routing resolve 的唯一真源
- `xworkmate-bridge` 仍然是 provider catalog、gateway capability、routing resolve 的唯一真源
## Bridge-Owned Source Of Truth
@ -63,6 +67,9 @@ flowchart TD
- bridge host / transport / auth input
- account-linked bridge configuration metadata
- `acpBridgeServerModeConfig.cloudSynced`
- `acpBridgeServerModeConfig.selfHosted`
- `acpBridgeServerModeConfig.effective`
- secure secret references
- gateway connection test / connect / disconnect affordance
@ -75,6 +82,10 @@ flowchart TD
## Notes
- `AcpBridgeServerModeConfig` 的实际仲裁顺序是 `selfHosted -> cloudSynced -> default`
- `selfHosted.isConfigured == true` 时,`effective.source == 'bridge'`
- `selfHosted` 未配置且 `accountSyncState` 提供了可用云端桥接信息时,`effective.source == 'cloud'`
- 两者都不可用时,`effective.source == 'default'`
- 当前任务对话框 provider 选择主链固定为 `providerCatalogForExecutionTarget() -> resolveProviderForExecutionTarget() -> setAssistantProvider()`
- `agent` catalog 只对应 bridge 广告的 ACP server bridges
- `gateway` catalog 只对应 bridge 返回的 gateway provider 列表;当前为 `openclaw`,未来可扩展 `hermes` 等项
@ -84,6 +95,37 @@ flowchart TD
- bridge 若未返回 catalogprovider 菜单为空或禁用app 不伪造 `codex / opencode / gemini / openclaw`
- production provider / gateway 选择继续由 bridge 拥有app 只保留消费与展示
## Effective Config Mermaid
```mermaid
stateDiagram-v2
[*] --> EvaluateEffective
EvaluateEffective --> BridgeEffective: selfHosted.isConfigured == true
EvaluateEffective --> CloudEffective: selfHosted 未配置 且 cloudSynced 可用
EvaluateEffective --> DefaultEffective: 两者都不可用
BridgeEffective --> CloudEffective: 关闭 manual Bridge
CloudEffective --> BridgeEffective: manual Bridge 配置生效
CloudEffective --> DefaultEffective: cloud sync 失效
DefaultEffective --> CloudEffective: cloud sync 恢复
note right of BridgeEffective
source = bridge
effective.endpoint = selfHosted.serverUrl
end note
note right of CloudEffective
source = cloud
effective.endpoint = accountSyncState.syncedDefaults.bridgeServerUrl
end note
note right of DefaultEffective
source = default
effective.endpoint = kManagedBridgeServerUrl
end note
```
## See Also
- [Task Dialog Provider Selection Mainline](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-dialog-provider-selection-mainline.md)

View File

@ -2,7 +2,7 @@
Status: Implementing V1
Date: 2026-04-11
Date: 2026-04-19
Scope:
- `xworkmate-app`
@ -10,9 +10,12 @@ Scope:
## V1 Decision
Production cloud mode is bridge-only:
Production cloud mode is bridge-owned, with explicit effective-config priority:
- app-facing cloud endpoint is fixed to `https://xworkmate-bridge.svc.plus`
- `selfHosted` manual Bridge configuration has highest priority
- `cloudSynced` svc.plus configuration is the fallback when manual Bridge is not configured
- `default` managed server is the last-resort fallback
- app-facing cloud entry remains fixed to `https://xworkmate-bridge.svc.plus`
- production provider catalog is bridge-owned
- production gateway upstream is bridge-owned
- account sync is metadata-only for session state, status, and managed secret references
@ -71,6 +74,12 @@ The app only talks to:
- may retain `openclawUrl` / `apisixUrl` as account profile metadata
- does not overwrite executable cloud routing targets
`acpBridgeServerModeConfig.effective`
- represents the actual runtime source of truth
- resolves to `selfHosted` first, then `cloudSynced`, then `default`
- is what UI and runtime should read when deciding the active Bridge endpoint
`acpBridgeServerModeConfig.cloudSynced.remoteServerSummary.endpoint`
- represents bridge cloud entry only
@ -78,6 +87,12 @@ The app only talks to:
- is not an upstream provider URL
- is not a gateway upstream URL
`acpBridgeServerModeConfig.selfHosted.serverUrl`
- represents the manual Bridge endpoint
- overrides cloud-synced bridge endpoint when configured
- is the first priority in effective-config resolution
## Workflow
```mermaid
@ -99,6 +114,11 @@ flowchart TD
APPLY --> SKIP1["do not overwrite gateway executable endpoint"]
APPLY --> SKIP2["do not overwrite ACP executable endpoint"]
LOAD --> RESOLVE["resolveAcpBridgeServerEffectiveConfigInternal()"]
RESOLVE --> BRIDGE["selfHosted.isConfigured == true"]
RESOLVE --> CLOUD["cloudSynced usable"]
RESOLVE --> DEFAULT["managed default fallback"]
UI --> BRIDGE_CAPS["acp.capabilities via bridge"]
UI --> BRIDGE_ROUTE["xworkmate.routing.resolve via bridge"]
UI --> BRIDGE_RUN["session.* via bridge"]
@ -111,3 +131,5 @@ flowchart TD
- account sync may update metadata, but not production execution targets.
- gateway runtime status shown in the app must come from bridge runtime results.
- bridge capability/provider availability shown in the app must come from `acp.capabilities`.
- `effective` must never be inferred from stale local fallback state when `selfHosted` is configured.
- cloud sync is allowed to coexist with manual Bridge config, but it does not outrank it.

View File

@ -9,7 +9,7 @@ mod types;
pub use error::CodexError;
pub use runtime::{CodexRuntime, CodexConfig, CodexConfigRust, ThreadHandle, RuntimeState};
pub use types::{CodexResult, CodexMessage, CodexEvent};
pub use types::{CodexResult, CodexEvent};
use std::ffi::CStr;
use std::os::raw::c_char;

View File

@ -33,19 +33,6 @@ impl CodexResult {
}
}
/// FFI-safe message type.
#[repr(C)]
pub struct CodexMessage {
/// Message type (text, code, tool_call, etc.).
pub message_type: *const c_char,
/// Message content.
pub content: *const c_char,
/// Thread ID.
pub thread_id: *const c_char,
/// Turn ID.
pub turn_id: *const c_char,
}
/// FFI-safe event type.
#[repr(C)]
pub struct CodexEvent {
@ -61,34 +48,6 @@ pub struct CodexEvent {
pub timestamp: i64,
}
/// FFI-safe model info.
#[repr(C)]
pub struct CodexModelInfo {
/// Model ID.
pub id: *const c_char,
/// Model name.
pub name: *const c_char,
/// Provider name.
pub provider: *const c_char,
/// Is online.
pub is_online: bool,
}
/// FFI-safe account info.
#[repr(C)]
pub struct CodexAccountInfo {
/// Email.
pub email: *const c_char,
/// Plan type.
pub plan: *const c_char,
/// Has credits.
pub has_credits: bool,
/// Credits balance.
pub credits_balance: f64,
/// Rate limits JSON.
pub rate_limits: *const c_char,
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -12,9 +12,7 @@ echo "Integrating Rust FFI with Flutter..."
# Build Rust library if not exists
RUST_LIB="$PROJECT_ROOT/rust/target/universal/libcodex_ffi.dylib"
if [[ ! -f "$RUST_LIB" ]]; then
echo "Rust library not found, building..."
# Attempt to build using Makefile target if available
(cd "$PROJECT_ROOT" && make rust-build-release)
echo "Rust library not found. Please build it manually or ensure it exists in target/."
fi
# Ensure Frameworks directory exists
@ -31,9 +29,6 @@ else
if [[ -f "$ARM_LIB" ]]; then
cp "$ARM_LIB" "$FRAMEWORKS_DIR/"
echo "Copied arm64 library to $FRAMEWORKS_DIR/"
else
echo "Error: No Rust library found. Please run 'make rust-build-release' first."
exit 1
fi
fi

View File

@ -109,6 +109,8 @@ if [[ -f "$SOURCE_FFI_LIB" ]]; then
echo "Embedding FFI library into app bundle..."
mkdir -p "$(dirname "$TARGET_FFI_LIB")"
cp "$SOURCE_FFI_LIB" "$TARGET_FFI_LIB"
# Fix install name to be @rpath-based so it is portable within the bundle
install_name_tool -id "@rpath/$(basename "$TARGET_FFI_LIB")" "$TARGET_FFI_LIB"
fi
# Embed xworkmate-go-core for local/non-App-Store builds if available
@ -140,18 +142,34 @@ validate_bundle_dependencies "$BUILD_APP_PATH"
rm -rf "$DIST_APP_PATH" "$DIST_DMG_PATH"
ditto "$BUILD_APP_PATH" "$DIST_APP_PATH"
if [[ -n "$SIGN_IDENTITY" ]]; then
echo "Re-signing app bundle with explicit identity..."
codesign --force --deep --sign "$SIGN_IDENTITY" \
--preserve-metadata=entitlements,requirements,flags,runtime \
--timestamp=none "$DIST_APP_PATH"
else
echo "Ad-hoc re-signing app bundle to account for manual additions..."
codesign --force --deep --sign - \
--preserve-metadata=entitlements,requirements,flags,runtime \
--timestamp=none "$DIST_APP_PATH"
echo "Re-signing app bundle to account for manual additions..."
# Components must be signed from inside out.
# 1. Sign all dylibs and frameworks
find "$DIST_APP_PATH/Contents/Frameworks" -name "*.dylib" -type f | while read -r dylib; do
echo "Signing nested dylib: $dylib"
codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$dylib"
done
find "$DIST_APP_PATH/Contents/Frameworks" -name "*.framework" -type d | while read -r framework; do
echo "Signing nested framework: $framework"
codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$framework"
done
# 2. Sign our manually added binaries if any
if [[ -f "$DIST_APP_PATH/Contents/MacOS/build/bin/xworkmate-go-core" ]]; then
echo "Signing embedded helper: xworkmate-go-core"
codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$DIST_APP_PATH/Contents/MacOS/build/bin/xworkmate-go-core"
fi
# 3. Sign the main executable
echo "Signing main executable..."
codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$DIST_APP_PATH/Contents/MacOS/$APP_NAME"
# 4. Finally sign the app bundle itself
echo "Signing app bundle..."
codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$DIST_APP_PATH"
verify_bundle_signature "$DIST_APP_PATH"
validate_bundle_dependencies "$DIST_APP_PATH"