Compare commits
5
Commits
v0.7.10
...
v0.7.10-sw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
275fc84a4a | ||
|
|
657e6f2125 | ||
|
|
48baafc3ff | ||
|
|
07dddd569e | ||
|
|
6adf7ba465 |
No files matched your search
@@ -27,7 +27,7 @@ dioxus-cli-telemetry = { workspace = true }
|
||||
dioxus-component-manifest = { workspace = true }
|
||||
|
||||
bytes = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "cargo"] }
|
||||
clap = { workspace = true, features = ["derive", "cargo", "env"] }
|
||||
convert_case = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4", "v5", "v7"] }
|
||||
|
||||
@@ -13,7 +13,7 @@ android {
|
||||
applicationId = "{{ application_id }}"
|
||||
minSdk = {{ min_sdk }}
|
||||
targetSdk = {{ target_sdk }}
|
||||
versionCode = 1
|
||||
versionCode = {{ version_code }}
|
||||
versionName = "{{ version }}"
|
||||
}
|
||||
{{#if android_bundle}}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{{{ raw_manifest }}}
|
||||
|
||||
<application android:hasCode="true" android:icon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="true"
|
||||
android:extractNativeLibs="false"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="{{#if supports_rtl}}{{ supports_rtl }}{{else}}true{{/if}}"
|
||||
|
||||
@@ -4,7 +4,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.7.0")
|
||||
classpath("com.android.tools.build:gradle:8.13.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<string>{{ bundle_name }}</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{ version }}</string>
|
||||
<string>{{ bundle_version }}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{ version }}</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<string>{{ version }}</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{ version }}</string>
|
||||
<string>{{ bundle_version }}</string>
|
||||
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>{{ minimum_system_version }}</string>
|
||||
|
||||
@@ -267,6 +267,15 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version_code": {
|
||||
"description": "The version code used for `versionCode` in the generated Gradle project.\n\nMust be between 1 and 2100000000 (the Google Play Store maximum).\n\nCan be overridden per-build with `--version-code` or the `DX_ANDROID_VERSION_CODE`\nenvironment variable. If unset, it is derived from the crate version as\n`major * 1000000 + minor * 1000 + patch`.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -964,6 +973,13 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"bundle_version": {
|
||||
"description": "The bundle version string (CFBundleVersion).\n\nThis is the machine-readable build number that must increase with every upload\nto App Store Connect. Can be overridden per-build with `--bundle-version` or the\n`DX_BUNDLE_VERSION` environment variable. Defaults to the crate version.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
"description": "App category. Overrides `bundle.category` for iOS builds.",
|
||||
"type": [
|
||||
|
||||
@@ -120,6 +120,7 @@ impl BuildRequest {
|
||||
application_id: String,
|
||||
app_name: String,
|
||||
version: String,
|
||||
version_code: u32,
|
||||
android_bundle: Option<crate::AndroidSettings>,
|
||||
/// Android SDK version settings
|
||||
min_sdk: u32,
|
||||
@@ -180,6 +181,7 @@ impl BuildRequest {
|
||||
application_id: self.bundle_identifier(),
|
||||
app_name: self.bundled_app_name(),
|
||||
version: self.crate_version(),
|
||||
version_code: self.android_version_code()?,
|
||||
android_bundle: self.config.bundle.android.clone(),
|
||||
min_sdk: self.config.android.min_sdk.unwrap_or(24),
|
||||
target_sdk: self.config.android.target_sdk.unwrap_or(34),
|
||||
|
||||
@@ -160,6 +160,8 @@ impl BuildRequest {
|
||||
pub executable_name: String,
|
||||
/// App version string (from Cargo.toml)
|
||||
pub version: String,
|
||||
/// Bundle version string (CFBundleVersion)
|
||||
pub bundle_version: String,
|
||||
/// Permission usage descriptions
|
||||
pub permissions: Vec<PlistPermission>,
|
||||
/// Additional plist entries as raw XML
|
||||
@@ -231,6 +233,7 @@ impl BuildRequest {
|
||||
executable_name: self.platform_exe_name(),
|
||||
bundle_identifier: self.bundle_identifier(),
|
||||
version: self.crate_version(),
|
||||
bundle_version: self.apple_bundle_version(),
|
||||
permissions,
|
||||
plist_entries,
|
||||
raw_plist,
|
||||
@@ -265,6 +268,7 @@ impl BuildRequest {
|
||||
executable_name: self.platform_exe_name(),
|
||||
bundle_identifier: self.bundle_identifier(),
|
||||
version: self.crate_version(),
|
||||
bundle_version: self.apple_bundle_version(),
|
||||
permissions,
|
||||
plist_entries,
|
||||
raw_plist,
|
||||
|
||||
@@ -269,6 +269,8 @@ pub(crate) struct BuildRequest {
|
||||
pub(crate) using_dioxus_explicitly: bool,
|
||||
pub(crate) apple_entitlements: Option<PathBuf>,
|
||||
pub(crate) apple_team_id: Option<String>,
|
||||
pub(crate) version_code: Option<u32>,
|
||||
pub(crate) bundle_version: Option<String>,
|
||||
pub(crate) session_cache_dir: PathBuf,
|
||||
pub(crate) raw_json_diagnostics: bool,
|
||||
pub(crate) windows_subsystem: Option<String>,
|
||||
@@ -905,6 +907,8 @@ impl BuildRequest {
|
||||
inject_loading_scripts: args.inject_loading_scripts,
|
||||
apple_entitlements: args.apple_entitlements.clone(),
|
||||
apple_team_id: args.apple_team_id.clone(),
|
||||
version_code: args.version_code,
|
||||
bundle_version: args.bundle_version.clone(),
|
||||
raw_json_diagnostics: args.raw_json_diagnostics,
|
||||
windows_subsystem: args.windows_subsystem.clone(),
|
||||
})
|
||||
@@ -2685,6 +2689,64 @@ impl BuildRequest {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Get the Android `versionCode` for the generated Gradle project.
|
||||
///
|
||||
/// Resolution order: `--version-code` / `DX_ANDROID_VERSION_CODE`, then
|
||||
/// `[android] version_code` in Dioxus.toml, then a value derived from the crate
|
||||
/// version as `major * 1000000 + minor * 1000 + patch`.
|
||||
pub(crate) fn android_version_code(&self) -> Result<u32> {
|
||||
/// The maximum value accepted by the Google Play Store.
|
||||
const MAX_VERSION_CODE: u32 = 2_100_000_000;
|
||||
|
||||
let code = match self.version_code.or(self.config.android.version_code) {
|
||||
Some(code) => code,
|
||||
None => {
|
||||
let version = &self.workspace.krates[self.crate_package].version;
|
||||
if version.major > 2099 || version.minor > 999 || version.patch > 999 {
|
||||
bail!(
|
||||
"Cannot derive an Android versionCode from crate version `{version}`: each component must fit `major <= 2099`, `minor <= 999`, `patch <= 999`. Set an explicit version code via `[android] version_code` in Dioxus.toml or the `--version-code` flag."
|
||||
);
|
||||
}
|
||||
if !version.pre.is_empty() {
|
||||
tracing::warn!(
|
||||
"Pre-release identifier of crate version `{version}` is ignored when deriving the Android versionCode"
|
||||
);
|
||||
}
|
||||
let code =
|
||||
(version.major * 1_000_000 + version.minor * 1_000 + version.patch) as u32;
|
||||
|
||||
// Google Play requires versionCode >= 1, so bump 0.0.0 up to 1.
|
||||
code.max(1)
|
||||
}
|
||||
};
|
||||
|
||||
if !(1..=MAX_VERSION_CODE).contains(&code) {
|
||||
bail!(
|
||||
"Android versionCode must be between 1 and {MAX_VERSION_CODE}, got {code}. See https://developer.android.com/studio/publish/versioning"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
/// The explicitly-configured `CFBundleVersion` for iOS/macOS bundles, if any.
|
||||
///
|
||||
/// Resolution order: `--bundle-version` / `DX_BUNDLE_VERSION`, then the platform's
|
||||
/// `bundle_version` in Dioxus.toml (`[ios]` or `[macos]`).
|
||||
pub(crate) fn apple_bundle_version_override(&self) -> Option<String> {
|
||||
self.bundle_version.clone().or_else(|| match self.bundle {
|
||||
BundleFormat::Ios => self.config.ios.bundle_version.clone(),
|
||||
BundleFormat::MacOS => self.config.macos.bundle_version.clone(),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The `CFBundleVersion` for iOS/macOS bundles, falling back to the crate version.
|
||||
pub(crate) fn apple_bundle_version(&self) -> String {
|
||||
self.apple_bundle_version_override()
|
||||
.unwrap_or_else(|| self.crate_version())
|
||||
}
|
||||
|
||||
pub(crate) fn bundle_identifier(&self) -> String {
|
||||
use crate::config::BundlePlatform;
|
||||
|
||||
|
||||
@@ -482,7 +482,11 @@ impl BundleContext<'_> {
|
||||
);
|
||||
|
||||
let version = self.version_string();
|
||||
let bundle_version = macos_settings.bundle_version.as_deref().unwrap_or(&version);
|
||||
let bundle_version = self
|
||||
.build
|
||||
.apple_bundle_version_override()
|
||||
.or_else(|| macos_settings.bundle_version.clone())
|
||||
.unwrap_or_else(|| version.clone());
|
||||
|
||||
dict.insert(
|
||||
"CFBundleShortVersionString".into(),
|
||||
|
||||
@@ -162,6 +162,22 @@ pub(crate) struct TargetArgs {
|
||||
#[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)]
|
||||
pub(crate) wasm_js_cfg: bool,
|
||||
|
||||
/// The Android `versionCode` to stamp into the generated Gradle project.
|
||||
///
|
||||
/// Must be between 1 and 2100000000 (the Google Play Store maximum). Overrides
|
||||
/// `[android] version_code` in Dioxus.toml. If neither is set, the version code is
|
||||
/// derived from the crate version as `major * 1000000 + minor * 1000 + patch`.
|
||||
#[clap(long, env = "DX_ANDROID_VERSION_CODE", help_heading = HELP_HEADING)]
|
||||
pub(crate) version_code: Option<u32>,
|
||||
|
||||
/// The bundle version (CFBundleVersion) to stamp into the Info.plist of iOS and macOS bundles.
|
||||
///
|
||||
/// This is the machine-readable build number that must increase with every upload to
|
||||
/// App Store Connect. Overrides `[ios] bundle_version` / `[macos] bundle_version` in
|
||||
/// Dioxus.toml. Defaults to the crate version.
|
||||
#[clap(long, env = "DX_BUNDLE_VERSION", help_heading = HELP_HEADING)]
|
||||
pub(crate) bundle_version: Option<String>,
|
||||
|
||||
/// The Windows subsystem to use when building for Windows targets. This can be either `CONSOLE` or `WINDOWS`.
|
||||
///
|
||||
/// By default, DX uses `WINDOWS` since it assumes a GUI application, but you can override this behavior with this flag.
|
||||
@@ -200,6 +216,8 @@ impl Anonymized for TargetArgs {
|
||||
"debug_symbols": self.debug_symbols,
|
||||
"keep_names": self.keep_names,
|
||||
"device": self.device,
|
||||
"version_code": self.version_code,
|
||||
"bundle_version": self.bundle_version.is_some(),
|
||||
"base_path": self.base_path.is_some(),
|
||||
"cargo_args": self.cargo_args.is_some(),
|
||||
"rustc_args": self.rustc_args.is_some(),
|
||||
|
||||
@@ -326,6 +326,14 @@ pub struct IosConfig {
|
||||
pub long_description: Option<String>,
|
||||
|
||||
// === iOS-specific settings ===
|
||||
/// The bundle version string (CFBundleVersion).
|
||||
///
|
||||
/// This is the machine-readable build number that must increase with every upload
|
||||
/// to App Store Connect. Can be overridden per-build with `--bundle-version` or the
|
||||
/// `DX_BUNDLE_VERSION` environment variable. Defaults to the crate version.
|
||||
#[serde(default)]
|
||||
pub bundle_version: Option<String>,
|
||||
|
||||
/// Minimum iOS deployment target (e.g., "15.0").
|
||||
#[serde(default)]
|
||||
pub deployment_target: Option<String>,
|
||||
@@ -590,6 +598,16 @@ pub struct AndroidConfig {
|
||||
pub signing: Option<AndroidSigningConfig>,
|
||||
|
||||
// === Android-specific settings ===
|
||||
/// The version code used for `versionCode` in the generated Gradle project.
|
||||
///
|
||||
/// Must be between 1 and 2100000000 (the Google Play Store maximum).
|
||||
///
|
||||
/// Can be overridden per-build with `--version-code` or the `DX_ANDROID_VERSION_CODE`
|
||||
/// environment variable. If unset, it is derived from the crate version as
|
||||
/// `major * 1000000 + minor * 1000 + patch`.
|
||||
#[serde(default)]
|
||||
pub version_code: Option<u32>,
|
||||
|
||||
/// Minimum SDK version.
|
||||
#[serde(default)]
|
||||
pub min_sdk: Option<u32>,
|
||||
|
||||
@@ -398,7 +398,7 @@ impl FfiBridgeParser {
|
||||
// Use with_activity which returns Option<R>, wrapping our Result in the Option
|
||||
let inner_result: Option<Result<Self, String>> = manganis::android::with_activity(|mut env, activity| {
|
||||
// Find the class
|
||||
let class_result = env.find_class(#class_name_lit);
|
||||
let class_result = manganis::android::load_class_from_classloader(env, activity, #class_name_lit);
|
||||
let class = match class_result {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Some(Err(format!("Failed to find class {}: {:?}", #class_name_lit, e))),
|
||||
@@ -724,10 +724,10 @@ impl FfiBridgeParser {
|
||||
let class_name_bytes = format!("{}\0", name);
|
||||
|
||||
let type_def = quote! {
|
||||
/// Opaque wrapper around an Objective-C object pointer
|
||||
/// Opaque wrapper around a retained Objective-C object
|
||||
/// The actual Swift class is looked up dynamically at runtime after dx links everything
|
||||
pub struct #name {
|
||||
inner: *mut manganis::objc2::runtime::AnyObject,
|
||||
inner: manganis::objc2::rc::Retained<manganis::objc2::runtime::AnyObject>,
|
||||
}
|
||||
|
||||
unsafe impl Send for #name {}
|
||||
@@ -827,17 +827,26 @@ impl FfiBridgeParser {
|
||||
let instance: *mut manganis::objc2::runtime::AnyObject = manganis::objc2::msg_send![class, alloc];
|
||||
let instance: *mut manganis::objc2::runtime::AnyObject = manganis::objc2::msg_send![instance, init];
|
||||
|
||||
if instance.is_null() {
|
||||
return Err("Failed to initialize instance");
|
||||
}
|
||||
// `alloc` + `init` returns a +1 retained object; `Retained::from_raw`
|
||||
// takes over that reference and sends `release` on drop.
|
||||
let inner = manganis::objc2::rc::Retained::from_raw(instance)
|
||||
.ok_or("Failed to initialize instance")?;
|
||||
|
||||
Ok(Self { inner: instance })
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from an existing object pointer
|
||||
/// Create from an existing object pointer.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `ptr` must be a valid, non-null pointer to an Objective-C object,
|
||||
/// and the caller must transfer ownership of one retain count (+1)
|
||||
/// to the returned wrapper, which sends `release` on drop.
|
||||
pub unsafe fn from_raw(ptr: *mut manganis::objc2::runtime::AnyObject) -> Self {
|
||||
Self { inner: ptr }
|
||||
let inner = manganis::objc2::rc::Retained::from_raw(ptr)
|
||||
.expect("from_raw called with a null pointer");
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -960,7 +969,7 @@ impl FfiBridgeParser {
|
||||
|
||||
// Build the msg_send call
|
||||
let this_expr = if func.receiver.is_some() {
|
||||
quote! { this.inner }
|
||||
quote! { manganis::objc2::rc::Retained::as_ptr(&this.inner) as *mut manganis::objc2::runtime::AnyObject }
|
||||
} else {
|
||||
// For static methods, we'd need the class
|
||||
quote! { class }
|
||||
|
||||
@@ -111,17 +111,56 @@ pub fn request_permissions_via_helper(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a Java class from the APK's classloader
|
||||
/// Load a Java class from the APK's classloader.
|
||||
///
|
||||
/// `JNIEnv::find_class` resolves against the *system* classloader on a thread that
|
||||
/// was attached from native code (`JavaVM::attach_current_thread`), which cannot see
|
||||
/// classes packaged in the APK, so it raises `ClassNotFoundException`. The activity's
|
||||
/// own classloader can, so we ask the activity for it and call `loadClass` on it.
|
||||
///
|
||||
/// `class_name` may be given in either JNI form (`com/example/Foo`) or binary form
|
||||
/// (`com.example.Foo`); `loadClass` requires the binary form, so slashes are converted.
|
||||
pub fn load_class_from_classloader<'env>(
|
||||
env: &mut JNIEnv<'env>,
|
||||
activity: &JObject<'_>,
|
||||
class_name: &str,
|
||||
) -> Result<JClass<'env>> {
|
||||
let class_name_jstring = new_string(env, class_name)?;
|
||||
let class = env.call_static_method(
|
||||
"java/lang/Class",
|
||||
"forName",
|
||||
let binary_name = class_name.replace('/', ".");
|
||||
|
||||
// Ask the activity INSTANCE for its classloader. `Activity` is a `Context`, and
|
||||
// `Context.getClassLoader()` is documented to return a loader for the application
|
||||
// package unconditionally. Do not route this through `get_object_class(activity)`:
|
||||
// that calls `Class.getClassLoader()`, which asks which loader loaded the Activity's
|
||||
// own class and is specified to return null for anything the bootstrap loader
|
||||
// loaded -- a null receiver that then fails obscurely at the `loadClass` below.
|
||||
let loader = env.call_method(activity, "getClassLoader", "()Ljava/lang/ClassLoader;", &[]);
|
||||
let loader = describe_and_clear(env, loader)?.l()?;
|
||||
|
||||
let class_name_jstring = new_string(env, &binary_name);
|
||||
let class_name_jstring = describe_and_clear(env, class_name_jstring)?;
|
||||
|
||||
let class = env.call_method(
|
||||
&loader,
|
||||
"loadClass",
|
||||
"(Ljava/lang/String;)Ljava/lang/Class;",
|
||||
&[JValue::Object(&class_name_jstring)],
|
||||
)?;
|
||||
);
|
||||
let class = describe_and_clear(env, class)?;
|
||||
Ok(class.l()?.into())
|
||||
}
|
||||
|
||||
/// Log and clear a pending Java exception before returning an `Err`.
|
||||
///
|
||||
/// jni 0.21 does NOT clear the pending exception when a call returns `Err`
|
||||
/// (see its `JNIEnv` docs), and any later JNI call made while one is pending
|
||||
/// aborts the process with "JNI DETECTED ERROR IN APPLICATION: ... called with
|
||||
/// pending exception". That is what turns a recoverable `ClassNotFoundException`
|
||||
/// into an unreadable ART abort. Describe it to logcat so the cause is visible,
|
||||
/// then clear it so the `Err` can travel normally.
|
||||
fn describe_and_clear<T>(env: &mut JNIEnv<'_>, result: Result<T>) -> Result<T> {
|
||||
if result.is_err() && env.exception_check().unwrap_or(false) {
|
||||
let _ = env.exception_describe();
|
||||
let _ = env.exception_clear();
|
||||
}
|
||||
result
|
||||
}
|
||||
Reference in new issue
Block a user