feat(workout): add mobile release kit command
Add one canonical Workout mobile release command that builds the production server and writes Android/iOS shell metadata, explicit production policy, and honest external signing/toolchain blockers. req: examples/001 req: examples/006 req: host/002 req: local/001
This commit is contained in:
@@ -87,8 +87,10 @@ and integrate at explicit boundaries. req: laws/002 req: auth/001
|
||||
cookies, and normal SameSite/browser semantics. hemx preserves submitted form
|
||||
fields and credentials semantics. See `docs/recipes/auth-session-csrf.md`. req: auth/004 req: auth/005
|
||||
- **Observability, feature flags, killswitches, deploy:** use explicit platform
|
||||
integrations around handlers, routes, and runtime assets. Core hemx must not
|
||||
vendor providers or add framework-specific magic. See `docs/recipes/observability-flags.md` and `docs/recipes/deploy-versioning.md`.
|
||||
integrations around handlers, routes, runtime assets, and mobile shells. Core
|
||||
hemx must not vendor providers or add framework-specific magic. See
|
||||
`docs/recipes/observability-flags.md`, `docs/recipes/deploy-versioning.md`,
|
||||
and `docs/recipes/mobile-release.md`. req: examples/006
|
||||
- **PWA/offline/sync:** optional adapters may reuse generated targets/effects,
|
||||
but core hemx must not gain a mandatory client state graph or local app
|
||||
runtime. Local truth is commands/events/projections, not stored DOM patches or
|
||||
|
||||
@@ -809,6 +809,9 @@ what a valid business email is.
|
||||
### req: examples/005
|
||||
005 Canonical examples must not contain user-authored browser JavaScript. They may load the shared hemx runtime (`/hemx.js`) and may use declarative `data-hemx-*` attributes; inline `<script>`, `on*=` event handlers, and `javascript:` URLs are forbidden outside opaque leaf-widget examples.
|
||||
|
||||
### req: examples/006
|
||||
006 The Workout exemplar must have one boring mobile release command that builds the production server binary and writes Android and iOS shell metadata. The command must make app identity, version, production origin, runtime asset policy, cache/offline state policy, environment/secrets boundary, rollback expectation, and external store-signing blockers explicit without adding a broad `hemx-mobile` framework.
|
||||
|
||||
---
|
||||
|
||||
## milestone
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Recipe: Workout mobile release
|
||||
|
||||
The Workout exemplar is the production-shaped mobile path for hemx. It stays
|
||||
boring on purpose: hemx builds the server app and writes mobile shell metadata;
|
||||
Android/iOS SDKs, store signing, provisioning, and submission remain external
|
||||
vendor work. req: examples/006
|
||||
|
||||
## Command surface
|
||||
|
||||
```sh
|
||||
cargo run -p hemx-xtask -- workout-mobile release
|
||||
cargo run -p hemx-xtask -- workout-mobile doctor
|
||||
```
|
||||
|
||||
`release` builds `target/release/hemx-workout-example` and writes a release kit
|
||||
under `target/hemx-mobile/workout` by default:
|
||||
|
||||
```text
|
||||
target/hemx-mobile/workout/
|
||||
release-manifest.json
|
||||
BLOCKERS.md
|
||||
android/twa-release.json
|
||||
android/README.md
|
||||
ios/webview-release.json
|
||||
ios/README.md
|
||||
```
|
||||
|
||||
Use `doctor` when you only want to see missing external inputs.
|
||||
|
||||
## Production configuration
|
||||
|
||||
Set these explicitly for a real app release:
|
||||
|
||||
```sh
|
||||
HEMX_WORKOUT_APP_ID=com.example.workout
|
||||
HEMX_WORKOUT_APP_NAME="Workout Copilot"
|
||||
HEMX_WORKOUT_VERSION=1.0.0
|
||||
HEMX_WORKOUT_ORIGIN=https://workout.example.com
|
||||
HEMX_WORKOUT_ANDROID_PACKAGE=com.example.workout
|
||||
HEMX_WORKOUT_IOS_BUNDLE_ID=com.example.workout
|
||||
HEMX_WORKOUT_MOBILE_OUT=target/hemx-mobile/workout
|
||||
```
|
||||
|
||||
The generated manifest records:
|
||||
|
||||
- app identity and version;
|
||||
- the production HTTPS origin used by Android and iOS shells;
|
||||
- `target/release/hemx-workout-example` as the server artifact;
|
||||
- runtime assets served by the same release through `hemx_axum::runtime_js_path()`;
|
||||
- cache policy: release-scoped HTML/CSS/runtime assets only;
|
||||
- offline truth policy: app-owned command/event/projection records, never DOM
|
||||
patches or UI effect payloads;
|
||||
- environment/secrets boundary: public shell config in the kit, signing secrets
|
||||
outside the repo;
|
||||
- rollback: redeploy the previous server binary and rebuild store artifacts from
|
||||
the previous shell metadata/signing inputs. req: local/001 req: host/002
|
||||
|
||||
## Android and iOS artifacts
|
||||
|
||||
The command writes release-ready metadata, not store-signed binaries. That is the
|
||||
honest boundary: producing `.aab`/`.apk` and `.ipa` files requires vendor SDKs,
|
||||
signing credentials, and store accounts on the release machine.
|
||||
|
||||
Android blockers are reported when the Android SDK/JDK/signing key are not
|
||||
visible. iOS blockers are reported when Xcode or the Apple signing team is not
|
||||
visible. These blockers are copied into `BLOCKERS.md` so the release kit can be
|
||||
reviewed without guessing what is still external. req: examples/006
|
||||
|
||||
## What this does not add
|
||||
|
||||
This is not a `hemx-mobile` framework, sync layer, client database, or native UI
|
||||
runtime. The mobile shells load the production Workout web app and route host
|
||||
capabilities such as share/haptics through the typed host boundary before UI
|
||||
effects are produced. req: host/002 req: local/002
|
||||
@@ -98,6 +98,7 @@ Evidence:
|
||||
- observability/metrics + feature flags/killswitches:
|
||||
`docs/recipes/observability-flags.md`
|
||||
- deploy/versioning: `docs/recipes/deploy-versioning.md`
|
||||
- mobile release: `docs/recipes/mobile-release.md`
|
||||
- optional PWA/offline: `docs/recipes/pwa-offline.md`
|
||||
|
||||
### Public examples
|
||||
|
||||
@@ -57,6 +57,23 @@ serves the shared hemx runtime through `hemx-axum`; there is no frontend build,
|
||||
Node runtime, Expo/Ionic/Tauri shell, or handwritten selector UI JavaScript to
|
||||
ship. req: axum_integration/005 req: examples/005
|
||||
|
||||
## Mobile release kit
|
||||
|
||||
Use the canonical release command to build the production server binary and
|
||||
write Android/iOS shell metadata:
|
||||
|
||||
```sh
|
||||
HEMX_WORKOUT_ORIGIN=https://workout.example.com \
|
||||
cargo run -p hemx-xtask -- workout-mobile release
|
||||
```
|
||||
|
||||
The kit lands in `target/hemx-mobile/workout` unless
|
||||
`HEMX_WORKOUT_MOBILE_OUT` is set. It records app identity, version, production
|
||||
origin, runtime asset policy, cache/offline state policy, secrets/signing
|
||||
boundaries, rollback expectations, Android TWA metadata, iOS WebView metadata,
|
||||
and any external blocker such as missing Android SDK, Xcode, or store signing
|
||||
credentials. See `docs/recipes/mobile-release.md`. req: examples/006
|
||||
|
||||
## Boundaries proven
|
||||
|
||||
- Core workout logging works without network availability once the page/runtime
|
||||
|
||||
+304
-3
@@ -1,5 +1,6 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitCode};
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -8,6 +9,7 @@ fn main() -> ExitCode {
|
||||
match args.next().as_deref() {
|
||||
Some("test") | None => run_test_plan(),
|
||||
Some("bench") => run_bench_plan(),
|
||||
Some("workout-mobile") => run_workout_mobile(args.next().as_deref()),
|
||||
Some("help") | Some("--help") | Some("-h") => {
|
||||
print_help();
|
||||
ExitCode::SUCCESS
|
||||
@@ -22,10 +24,257 @@ fn main() -> ExitCode {
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- bench\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E"
|
||||
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- bench\n cargo run -p hemx-xtask -- workout-mobile release\n cargo run -p hemx-xtask -- workout-mobile doctor\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E\n HEMX_WORKOUT_ORIGIN=https://app.example.com\n HEMX_WORKOUT_MOBILE_OUT=target/hemx-mobile/workout"
|
||||
);
|
||||
}
|
||||
|
||||
fn run_workout_mobile(command: Option<&str>) -> ExitCode {
|
||||
match command.unwrap_or("release") {
|
||||
"release" => run_workout_mobile_release(),
|
||||
"doctor" => {
|
||||
let config = WorkoutMobileConfig::from_env();
|
||||
let blockers = mobile_external_blockers(&config);
|
||||
print_mobile_doctor(&config, &blockers);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
"help" | "--help" | "-h" => {
|
||||
print_help();
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
other => {
|
||||
eprintln!("unknown workout-mobile command `{other}`\n");
|
||||
print_help();
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_workout_mobile_release() -> ExitCode {
|
||||
// req: examples/001 req: host/002 req: local/001
|
||||
let budget = Budget::detect();
|
||||
budget.report();
|
||||
if let Err(code) = Step::new(
|
||||
"workout-mobile-server-release",
|
||||
["build", "--release", "--bin", "hemx-workout-example"],
|
||||
)
|
||||
.run(&budget)
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
let config = WorkoutMobileConfig::from_env();
|
||||
let blockers = mobile_external_blockers(&config);
|
||||
match write_workout_mobile_release(&config, &blockers) {
|
||||
Ok(()) => {
|
||||
println!(
|
||||
"workout-mobile\tout={}\tblockers={}",
|
||||
config.out_dir.display(),
|
||||
blockers.len()
|
||||
);
|
||||
for blocker in &blockers {
|
||||
println!("workout-mobile-blocker\t{}", blocker);
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to write Workout mobile release kit: {err}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct WorkoutMobileConfig {
|
||||
app_id: String,
|
||||
app_name: String,
|
||||
version: String,
|
||||
origin: String,
|
||||
android_package: String,
|
||||
ios_bundle_id: String,
|
||||
out_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl WorkoutMobileConfig {
|
||||
fn from_env() -> Self {
|
||||
let app_id = env::var("HEMX_WORKOUT_APP_ID").unwrap_or_else(|_| "com.hemx.workout".into());
|
||||
Self {
|
||||
android_package: env::var("HEMX_WORKOUT_ANDROID_PACKAGE")
|
||||
.unwrap_or_else(|_| app_id.clone()),
|
||||
ios_bundle_id: env::var("HEMX_WORKOUT_IOS_BUNDLE_ID")
|
||||
.unwrap_or_else(|_| app_id.clone()),
|
||||
app_id,
|
||||
app_name: env::var("HEMX_WORKOUT_APP_NAME")
|
||||
.unwrap_or_else(|_| "hemx Workout Copilot".into()),
|
||||
version: env::var("HEMX_WORKOUT_VERSION")
|
||||
.unwrap_or_else(|_| env!("CARGO_PKG_VERSION").into()),
|
||||
origin: env::var("HEMX_WORKOUT_ORIGIN")
|
||||
.unwrap_or_else(|_| "https://workout.example.invalid".into()),
|
||||
out_dir: env::var_os("HEMX_WORKOUT_MOBILE_OUT")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("target/hemx-mobile/workout")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_external_blockers(config: &WorkoutMobileConfig) -> Vec<String> {
|
||||
let mut blockers = Vec::new();
|
||||
if !config.origin.starts_with("https://") {
|
||||
blockers.push("HEMX_WORKOUT_ORIGIN must be the production HTTPS origin used by Android and iOS shells".into());
|
||||
}
|
||||
if env::var_os("ANDROID_HOME").is_none() && env::var_os("ANDROID_SDK_ROOT").is_none() {
|
||||
blockers.push("Android SDK not found: set ANDROID_HOME or ANDROID_SDK_ROOT before producing signed Android artifacts".into());
|
||||
}
|
||||
if !has_command("java") {
|
||||
blockers.push("Java runtime not found: Android packaging requires a JDK".into());
|
||||
}
|
||||
if env::var_os("HEMX_WORKOUT_ANDROID_KEYSTORE").is_none() {
|
||||
blockers.push("Android signing key not configured: set HEMX_WORKOUT_ANDROID_KEYSTORE for store-ready signing".into());
|
||||
}
|
||||
if !has_command("xcodebuild") {
|
||||
blockers.push(
|
||||
"Xcode command line tools not found: iOS archive/export requires xcodebuild on macOS"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if env::var_os("HEMX_WORKOUT_IOS_TEAM_ID").is_none() {
|
||||
blockers.push("iOS signing team not configured: set HEMX_WORKOUT_IOS_TEAM_ID for App Store/TestFlight export".into());
|
||||
}
|
||||
blockers
|
||||
}
|
||||
|
||||
fn print_mobile_doctor(config: &WorkoutMobileConfig, blockers: &[String]) {
|
||||
println!("Workout mobile release doctor");
|
||||
println!(
|
||||
"app_id={} version={} origin={}",
|
||||
config.app_id, config.version, config.origin
|
||||
);
|
||||
if blockers.is_empty() {
|
||||
println!("ready: Android SDK/signing and iOS Xcode/signing inputs are visible");
|
||||
} else {
|
||||
println!("blocked external steps:");
|
||||
for blocker in blockers {
|
||||
println!("- {blocker}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_workout_mobile_release(
|
||||
config: &WorkoutMobileConfig,
|
||||
blockers: &[String],
|
||||
) -> std::io::Result<()> {
|
||||
let android_dir = config.out_dir.join("android");
|
||||
let ios_dir = config.out_dir.join("ios");
|
||||
fs::create_dir_all(&android_dir)?;
|
||||
fs::create_dir_all(&ios_dir)?;
|
||||
fs::write(
|
||||
config.out_dir.join("release-manifest.json"),
|
||||
workout_mobile_manifest(config, blockers),
|
||||
)?;
|
||||
fs::write(
|
||||
config.out_dir.join("BLOCKERS.md"),
|
||||
workout_mobile_blockers_md(blockers),
|
||||
)?;
|
||||
fs::write(
|
||||
android_dir.join("twa-release.json"),
|
||||
android_twa_release_json(config),
|
||||
)?;
|
||||
fs::write(
|
||||
android_dir.join("README.md"),
|
||||
android_release_readme(config),
|
||||
)?;
|
||||
fs::write(
|
||||
ios_dir.join("webview-release.json"),
|
||||
ios_webview_release_json(config),
|
||||
)?;
|
||||
fs::write(ios_dir.join("README.md"), ios_release_readme(config))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) -> String {
|
||||
format!(
|
||||
"{{\n \"app_id\": \"{}\",\n \"name\": \"{}\",\n \"version\": \"{}\",\n \"origin\": \"{}\",\n \"server_binary\": \"target/release/hemx-workout-example\",\n \"runtime_asset_path\": \"served by hemx_axum::runtime_js_path() from the same release\",\n \"cache_policy\": \"cache only release-scoped HTML/CSS/runtime assets; never store DOM patches or UI effects as truth\",\n \"state_policy\": \"app-owned command/event/projection records\",\n \"environment_boundary\": \"public mobile shell config lives here; secrets and signing credentials stay outside the repo\",\n \"rollback\": \"redeploy the previous server binary and matching mobile shell metadata; rebuild store artifacts with the previous version/signing inputs\",\n \"android\": \"android/twa-release.json\",\n \"ios\": \"ios/webview-release.json\",\n \"external_blockers\": [{}]\n}}\n",
|
||||
json_escape(&config.app_id),
|
||||
json_escape(&config.app_name),
|
||||
json_escape(&config.version),
|
||||
json_escape(&config.origin),
|
||||
json_string_list(blockers),
|
||||
)
|
||||
}
|
||||
|
||||
fn android_twa_release_json(config: &WorkoutMobileConfig) -> String {
|
||||
format!(
|
||||
"{{\n \"package\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"host\": \"{}\",\n \"version\": \"{}\",\n \"signing\": \"external Android keystore; never commit credentials\"\n}}\n",
|
||||
json_escape(&config.android_package),
|
||||
json_escape(&config.app_name),
|
||||
json_escape(config.origin.trim_end_matches('/')),
|
||||
json_escape(origin_host(&config.origin)),
|
||||
json_escape(&config.version),
|
||||
)
|
||||
}
|
||||
|
||||
fn ios_webview_release_json(config: &WorkoutMobileConfig) -> String {
|
||||
format!(
|
||||
"{{\n \"bundle_id\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"signing\": \"external Apple team/provisioning profile; never commit credentials\"\n}}\n",
|
||||
json_escape(&config.ios_bundle_id),
|
||||
json_escape(&config.app_name),
|
||||
json_escape(config.origin.trim_end_matches('/')),
|
||||
json_escape(&config.version),
|
||||
)
|
||||
}
|
||||
|
||||
fn workout_mobile_blockers_md(blockers: &[String]) -> String {
|
||||
if blockers.is_empty() {
|
||||
"# Workout mobile external blockers\n\nNo external blocker was detected locally. Store submission still remains a human/vendor step.\n".into()
|
||||
} else {
|
||||
let mut out = String::from("# Workout mobile external blockers\n\nThe hemx release kit is generated, but these external inputs are still required for signed store artifacts:\n\n");
|
||||
for blocker in blockers {
|
||||
out.push_str("- ");
|
||||
out.push_str(blocker);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn android_release_readme(config: &WorkoutMobileConfig) -> String {
|
||||
format!(
|
||||
"# Workout Android release\n\nUse `twa-release.json` as the Android shell authority for `{}`. Build the hemx server with the same release and serve `{}/` over HTTPS. Android SDK, Java, and signing credentials are external inputs; this repository does not own them.\n",
|
||||
config.android_package, config.origin
|
||||
)
|
||||
}
|
||||
|
||||
fn ios_release_readme(config: &WorkoutMobileConfig) -> String {
|
||||
format!(
|
||||
"# Workout iOS release\n\nUse `webview-release.json` as the iOS shell authority for `{}`. Archive with Xcode against `{}/` and route share/haptics through the typed host adapter. Apple team IDs, provisioning profiles, and App Store submission are external inputs; this repository does not own them.\n",
|
||||
config.ios_bundle_id, config.origin
|
||||
)
|
||||
}
|
||||
|
||||
fn json_string_list(values: &[String]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| format!("\"{}\"", json_escape(value)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn json_escape(value: &str) -> String {
|
||||
value
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('\n', "\\n")
|
||||
}
|
||||
|
||||
fn origin_host(origin: &str) -> &str {
|
||||
origin
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| origin.strip_prefix("http://"))
|
||||
.unwrap_or(origin)
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or(origin)
|
||||
}
|
||||
|
||||
fn run_test_plan() -> ExitCode {
|
||||
// req: test/004
|
||||
let budget = Budget::detect();
|
||||
@@ -313,7 +562,11 @@ fn is_executable(path: impl AsRef<Path>) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Budget;
|
||||
use super::{
|
||||
android_twa_release_json, mobile_external_blockers, origin_host, workout_mobile_manifest,
|
||||
Budget, WorkoutMobileConfig,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn budget_is_capped_by_available_memory() {
|
||||
@@ -345,4 +598,52 @@ mod tests {
|
||||
assert_eq!(super::bench_values(1), vec![1]);
|
||||
assert_eq!(super::bench_values(6), vec![1, 2, 4, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workout_mobile_manifest_names_production_boundaries() {
|
||||
// req: examples/001 req: local/001 req: host/002
|
||||
let config = workout_mobile_config("https://workout.example.com");
|
||||
let blockers = vec!["Android signing key not configured".to_string()];
|
||||
let manifest = workout_mobile_manifest(&config, &blockers);
|
||||
|
||||
assert!(manifest.contains("target/release/hemx-workout-example"));
|
||||
assert!(manifest.contains("hemx_axum::runtime_js_path()"));
|
||||
assert!(manifest.contains("app-owned command/event/projection records"));
|
||||
assert!(manifest.contains("secrets and signing credentials stay outside the repo"));
|
||||
assert!(manifest.contains("Android signing key not configured"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workout_mobile_release_uses_https_origin_and_host_metadata() {
|
||||
// req: examples/001 req: host/002
|
||||
let config = workout_mobile_config("https://workout.example.com/app");
|
||||
let android = android_twa_release_json(&config);
|
||||
|
||||
assert_eq!(origin_host(&config.origin), "workout.example.com");
|
||||
assert!(android.contains("\"start_url\": \"https://workout.example.com/app/\""));
|
||||
assert!(android.contains("\"host\": \"workout.example.com\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workout_mobile_doctor_rejects_non_https_production_origin() {
|
||||
// req: examples/001
|
||||
let config = workout_mobile_config("http://workout.example.com");
|
||||
let blockers = mobile_external_blockers(&config);
|
||||
|
||||
assert!(blockers
|
||||
.iter()
|
||||
.any(|blocker| blocker.contains("production HTTPS origin")));
|
||||
}
|
||||
|
||||
fn workout_mobile_config(origin: &str) -> WorkoutMobileConfig {
|
||||
WorkoutMobileConfig {
|
||||
app_id: "com.hemx.workout".into(),
|
||||
app_name: "hemx Workout Copilot".into(),
|
||||
version: "1.2.3".into(),
|
||||
origin: origin.into(),
|
||||
android_package: "com.hemx.workout".into(),
|
||||
ios_bundle_id: "com.hemx.workout".into(),
|
||||
out_dir: PathBuf::from("target/test-workout-mobile"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user