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: 2 additions & 6 deletions .claude/skills/ohos-build/scripts/sign-and-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

OHOS_PROJECT="$PROJECT_ROOT/examples/api/src-tauri/gen/ohos"
if [ "$OHOS_DEVICE_TYPE" = "desktop" ]; then
ENTRY_DIR="entry_desktop"
else
ENTRY_DIR="entry_mobile"
fi
SIGNED_HAP="$OHOS_PROJECT/$ENTRY_DIR/build/default/outputs/default/$ENTRY_DIR-default-signed.hap"
ENTRY_MODULE="entry_${OHOS_DEVICE_TYPE:-desktop}"
SIGNED_HAP="$OHOS_PROJECT/${ENTRY_MODULE}/build/default/outputs/default/${ENTRY_MODULE}-default-signed.hap"

# ─── 检查已签名 HAP ───
if [ ! -f "$SIGNED_HAP" ]; then
Expand Down
2 changes: 2 additions & 0 deletions crates/tauri-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ build = [
"dep:glob",
"dep:plist",
"dep:walkdir",
"dep:json5",
]
runtime = []

Expand All @@ -35,6 +36,7 @@ glob = { version = "0.3", optional = true }
# Our code requires at least 0.8.21 so don't simplify this to 0.8
schemars = { version = "0.8.21", features = ["preserve_order"] }
walkdir = { version = "2", optional = true }
json5 = { version = "0.4", optional = true }

[target."cfg(target_os = \"macos\")".dependencies]
plist = { version = "1", optional = true }
70 changes: 68 additions & 2 deletions crates/tauri-plugin/src/build/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,73 @@ pub fn update_android_manifest(block_identifier: &str, parent: &str, insert: Str
tauri_utils::build::update_android_manifest(block_identifier, parent, insert)
}

/// Updates the OHOS module.json5 by appending deep-link skill objects to abilities[0].skills.
///
/// Reads `TAURI_OHOS_PROJECT_PATH` to locate the OHOS project directory (set by tauri-cli).
/// Self-gating is via `CARGO_CFG_TARGET_ENV == "ohos"` (cross-compilation safe).
/// Locates `entry_{OHOS_DEVICE_TYPE}/src/main/module.json5`. Uses json5 parse/serialize.
/// Idempotent: removes existing deep-link skills (by `ohos.want.action.viewData` signature)
/// before re-injecting, so repeated builds don't accumulate. Home entry skill is preserved.
///
/// Limitations:
/// - Only `abilities[0]` is injected (single-ability Tauri OHOS apps; multi-ability projects
/// would need to target the entry ability by name).
/// - Output is serialized as strict JSON (`serde_json::to_string_pretty`); JSON5-only features
/// in the template (comments, trailing commas, unquoted keys) are lost on round-trip.
pub fn update_ohos_module_json(skills: serde_json::Value) -> Result<()> {
// Gate 1: only run on OHOS builds. CARGO_CFG_TARGET_ENV is set by Cargo for build scripts,
// reflecting the cross-compilation target (not the host).
if std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() != "ohos" {
return Ok(());
}
// Gate 2: TAURI_OHOS_PROJECT_PATH is set by tauri-cli (mod.rs:191) for OHOS builds.
// If unset (e.g. build-ohos.sh without tauri-cli), no-op gracefully.
let Some(project_path) = std::env::var_os("TAURI_OHOS_PROJECT_PATH") else {
return Ok(());
};
println!("cargo:rerun-if-env-changed=TAURI_OHOS_PROJECT_PATH");
let device_type = std::env::var("OHOS_DEVICE_TYPE").unwrap_or_else(|_| "mobile".to_string());
let module_json = PathBuf::from(project_path)
.join(format!("entry_{device_type}"))
.join("src/main/module.json5");
if !module_json.exists() {
return Ok(());
}
let content = std::fs::read_to_string(&module_json)?;
let mut json: serde_json::Value = json5::from_str(&content)?;
if let Some(abilities) = json
.get_mut("module")
.and_then(|m| m.get_mut("abilities"))
.and_then(|a| a.as_array_mut())
{
if let Some(first_ability) = abilities.get_mut(0) {
// ensure the ability has a `skills` array; initialize an empty one if missing
// so deep-link skills are always injected (avoid silent skip on custom templates)
if let Some(obj) = first_ability.as_object_mut() {
let skills_arr = obj
.entry("skills")
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
if let Some(skills_arr) = skills_arr.as_array_mut() {
// idempotent: remove existing deep-link skills (actions contains ohos.want.action.viewData)
skills_arr.retain(|s| {
!s.get("actions")
.and_then(|a| a.as_array())
.map(|a| a.iter().any(|v| v == "ohos.want.action.viewData"))
.unwrap_or(false)
});
// append new skills
if let Some(new_skills) = skills.as_array() {
skills_arr.extend(new_skills.iter().cloned());
}
}
}
}
}
let serialized = serde_json::to_string_pretty(&json)?;
std::fs::write(&module_json, serialized)?;
Ok(())
}

pub(crate) fn setup(
android_path: Option<PathBuf>,
#[allow(unused_variables)] ios_path: Option<PathBuf>,
Expand All @@ -61,8 +128,7 @@ pub(crate) fn setup(
let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let mobile = if target_env == "ohos" {
println!("cargo:rerun-if-env-changed=OHOS_DEVICE_TYPE");
let device_type =
std::env::var("OHOS_DEVICE_TYPE").unwrap_or_else(|_| "mobile".to_string());
let device_type = std::env::var("OHOS_DEVICE_TYPE").unwrap_or_else(|_| "mobile".to_string());
device_type != "desktop"
} else {
target_os == "ios" || target_os == "android"
Expand Down
16 changes: 10 additions & 6 deletions crates/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,9 @@ impl<R: Runtime> AppHandle<R> {
/// but accepts a boxed trait object instead of a generic type.
#[cfg_attr(feature = "tracing", tracing::instrument(name = "app::plugin::register", skip(plugin), fields(name = plugin.name())))]
pub fn plugin_boxed(&self, mut plugin: Box<dyn Plugin<R>>) -> crate::Result<()> {
// initialize outside lock to avoid blocking on_event_loop_event (appfreeze fix)
crate::plugin::initialize(&mut plugin, self, &self.config().plugins)?;
let mut store = self.manager().plugins.lock().unwrap();
store.initialize(&mut plugin, self, &self.config().plugins)?;
store.register(plugin);

Ok(())
Expand Down Expand Up @@ -2686,11 +2687,14 @@ fn on_event_loop_event<R: Runtime>(
_ => unimplemented!(),
};

manager
.plugins
.lock()
.expect("poisoned plugin store")
.on_event(app_handle, &event);
// try_lock to avoid blocking main thread when plugins lock is held by register (appfreeze fix)
if let Ok(mut store) = manager.plugins.try_lock() {
store.on_event(app_handle, &event);
} else {
// lock contended (e.g. during plugin register); skip on_event to avoid blocking the main
// thread. Log so a dropped event (e.g. a deep-link RunEvent::Opened) is traceable.
log::warn!("[tauri] plugin store lock busy, skipping on_event (appfreeze try_lock)");
}

event
}
Expand Down
12 changes: 1 addition & 11 deletions crates/tauri/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,16 +892,6 @@ impl<R: Runtime> PluginStore<R> {
len != self.store.len()
}

/// Initializes the given plugin.
pub(crate) fn initialize(
&self,
plugin: &mut Box<dyn Plugin<R>>,
app: &AppHandle<R>,
config: &PluginConfig,
) -> crate::Result<()> {
initialize(plugin, app, config)
}

/// Initializes all plugins in the store.
pub(crate) fn initialize_all(
&mut self,
Expand Down Expand Up @@ -996,7 +986,7 @@ impl<R: Runtime> PluginStore<R> {
}

#[cfg_attr(feature = "tracing", tracing::instrument(name = "plugin::hooks::initialize", skip(plugin, app), fields(name = plugin.name())))]
fn initialize<R: Runtime>(
pub(crate) fn initialize<R: Runtime>(
plugin: &mut Box<dyn Plugin<R>>,
app: &AppHandle<R>,
config: &PluginConfig,
Expand Down
11 changes: 10 additions & 1 deletion doc/manual_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@

---

## 十九、Deep-Link 手动用例

| 一级场景 | 二级场景 | 三级场景 | 用例名称 | 用例级别 | 预置条件 | 测试步骤 | 预期结果 | 备注 |
|---------|---------|---------|---------|---------|---------|---------|---------|------|
| core | deep-link | onOpenUrl | onOpenUrl 事件触发 — 运行中收到外部链接 | **T0** | app 已运行 | 1. 在 TestRunner UI manual 区点击 "onOpenUrl (trigger with hdc)" 按钮注册监听 2. 执行 `hdc shell "aa start -U taurideeplink://manualtest"` | UI 消息区显示 `[deep-link] onOpenUrl received: ["taurideeplink://manualtest"]` | RunEvent::Opened urls 非空时触发 |
| core | deep-link | getCurrent | getCurrent 冷启动 — 首启动链接拉起 | **T0** | app 未运行 | 1. `hdc shell "aa force-stop com.tauri.api"` 2. `hdc shell "aa start -U taurideeplink://coldstart"` 3. 等 app 冷启动后在 TestRunner UI manual 区点击 "getCurrent" 按钮 | UI 消息区显示 `[deep-link] getCurrent → ["taurideeplink://coldstart"]` | 冷启动 onCreate want.uri 经 lazy take 注入 |
| core | deep-link | 外部唤起 | 外部链接唤起 app — 跨 app 跳转 | **T0** | app 已安装 | 1. `hdc shell "aa force-stop com.tauri.api"` 2. `hdc shell "aa start -U taurideeplink://foreground-test"` | app 唤起到前台(onCreate 冷启动或 onNewWant 运行中) | aa start -U 与浏览器点击 `<a href>` 走相同系统 Want 路由(module.json5 skills 匹配);浏览器地址栏直接输入 scheme 会被当搜索词 |

## 二十、用例统计

| 模块 | T0 | T1 | 合计 |
Expand Down Expand Up @@ -425,5 +433,6 @@
| Global Shortcut(全局快捷键) | 2 | 0 | **2** |
| 窗口聚焦与热键缩放 | 1 | 1 | **2** |
| Vibrancy(窗口模糊) | 3 | 2 | **5** |
| **合计** | **59** | **57** | **116** |
| Deep-Link(深度链接) | 3 | 0 | **3** |
| **合计** | **62** | **57** | **119** |

1 change: 1 addition & 0 deletions examples/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@tauri-apps/api": "../../packages/api/dist",
"@tauri-apps/plugin-autostart": "file:../../../plugins-workspace/plugins/autostart",
"@tauri-apps/plugin-clipboard-manager": "file:../../../plugins-workspace/plugins/clipboard-manager",
"@tauri-apps/plugin-deep-link": "file:../../../plugins-workspace/plugins/deep-link",
"@tauri-apps/plugin-dialog": "file:../../../plugins-workspace/plugins/dialog",
"@tauri-apps/plugin-fs": "file:../../../plugins-workspace/plugins/fs",
"@tauri-apps/plugin-global-shortcut": "file:../../../plugins-workspace/plugins/global-shortcut",
Expand Down
1 change: 1 addition & 0 deletions examples/api/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ hilog = "*"
tauri-plugin-dialog = { path = "../../../../plugins-workspace/plugins/dialog" }
tauri-plugin-single-instance = { path = "../../../../plugins-workspace/plugins/single-instance" }
tauri-plugin-global-shortcut = { path = "../../../../plugins-workspace/plugins/global-shortcut" }
tauri-plugin-deep-link = { path = "../../../../plugins-workspace/plugins/deep-link" }

[dependencies.tauri]
path = "../../../crates/tauri"
Expand Down
4 changes: 4 additions & 0 deletions examples/api/src-tauri/capabilities/run-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@
"dialog:allow-save",
"dialog:allow-message",
"notification:default",
"deep-link:default",
"deep-link:allow-register",
"deep-link:allow-unregister",
"deep-link:allow-is-registered",
"sentry:default",
"allow-sentry-test-breadcrumb",
"global-shortcut:allow-register",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "test-windows-is-decorated",
"description": "Allow the read-only is_decorated query from any test popup window (e.g. borderless/transparent child windows) so the STATUS_SCRIPT badge can display the child window's decoration state. These windows are created with arbitrary labels that do not all match the run-app capability's window targeting.",
"windows": ["*"],
"permissions": [
"core:window:allow-is-decorated"
]
}
59 changes: 49 additions & 10 deletions examples/api/src-tauri/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,14 +672,35 @@ const STATUS_SCRIPT: &str = r##"
statusDiv.style.cssText = 'position:fixed;bottom:10px;left:10px;background:rgba(0,0,0,0.8);color:#0f0;padding:8px 14px;border-radius:8px;font-size:13px;font-family:monospace;z-index:9999;';
statusDiv.textContent = 'isDecorated: checking...';
document.body.appendChild(statusDiv);
// Tauri v2 exposes the public invoke at `window.__TAURI__.core.invoke` (not the
// v1 top-level `window.__TAURI__.invoke`). The low-level bridge
// `window.__TAURI_INTERNALS__.invoke` is always present and is what the bundled
// @tauri-apps/api uses (proven to work on OHOS). Resolve whichever is available,
// and degrade gracefully instead of leaving the badge stuck on "checking...".
function resolveInvoke() {
var i = window.__TAURI_INTERNALS__;
if (i && typeof i.invoke === 'function') return i.invoke.bind(i);
var t = window.__TAURI__;
if (t && t.core && typeof t.core.invoke === 'function') return t.core.invoke.bind(t.core);
return null;
}
function setStatus(text, color) {
var el = document.getElementById('state-status');
if (el) { el.textContent = text; el.style.color = color; }
}
setInterval(function() {
window.__TAURI__.invoke('plugin:window|is_decorated').then(function(v) {
var el = document.getElementById('state-status');
if (el) {
el.textContent = 'isDecorated: ' + v;
el.style.color = v ? '#0f0' : '#f80';
}
}).catch(function() {});
var inv = resolveInvoke();
if (!inv) { setStatus('isDecorated: (n/a)', '#888'); return; }
try {
// No label arg: get_window() resolves to the current (this child) window.
inv('plugin:window|is_decorated').then(function(v) {
setStatus('isDecorated: ' + v, v ? '#0f0' : '#f80');
}).catch(function() {
setStatus('isDecorated: (err)', '#f00');
});
} catch (e) {
setStatus('isDecorated: (err)', '#f00');
}
}, 500);
"##;

Expand All @@ -694,7 +715,13 @@ pub fn create_transparent_window<R: tauri::Runtime>(
log::info!("Creating transparent window: {} (effect={:?}, radius={:?})", window_id, effect, radius);

let close_link = CLOSE_LINK_HTML;
let status_script = STATUS_SCRIPT;
// Autotest-created windows (label prefix "test-") are created and closed
// programmatically; on OHOS programmatic close doesn't destroy the Float window
// (tao OHOS Window::close is unimplemented), so a lingering closed popup would
// poll is_decorated on an unregistered webview → "failed to acquire webview
// reference". Skip the live isDecorated badge for autotest windows to avoid that
// noisy error; manual test windows keep the badge (they stay open and work).
let status_script = if window_id.starts_with("test-") { "" } else { STATUS_SCRIPT };
let init_script = format!(
r#"
document.addEventListener('DOMContentLoaded', function() {{
Expand Down Expand Up @@ -762,7 +789,13 @@ pub fn create_borderless_window<R: tauri::Runtime>(
log::info!("Creating borderless window: {}", window_id);

let close_link = CLOSE_LINK_HTML;
let status_script = STATUS_SCRIPT;
// Autotest-created windows (label prefix "test-") are created and closed
// programmatically; on OHOS programmatic close doesn't destroy the Float window
// (tao OHOS Window::close is unimplemented), so a lingering closed popup would
// poll is_decorated on an unregistered webview → "failed to acquire webview
// reference". Skip the live isDecorated badge for autotest windows to avoid that
// noisy error; manual test windows keep the badge (they stay open and work).
let status_script = if window_id.starts_with("test-") { "" } else { STATUS_SCRIPT };
let init_script = format!(
r#"
document.addEventListener('DOMContentLoaded', function() {{
Expand Down Expand Up @@ -806,7 +839,13 @@ pub fn create_transparent_borderless_window<R: tauri::Runtime>(
log::info!("Creating transparent borderless window: {}", window_id);

let close_link = CLOSE_LINK_HTML;
let status_script = STATUS_SCRIPT;
// Autotest-created windows (label prefix "test-") are created and closed
// programmatically; on OHOS programmatic close doesn't destroy the Float window
// (tao OHOS Window::close is unimplemented), so a lingering closed popup would
// poll is_decorated on an unregistered webview → "failed to acquire webview
// reference". Skip the live isDecorated badge for autotest windows to avoid that
// noisy error; manual test windows keep the badge (they stay open and work).
let status_script = if window_id.starts_with("test-") { "" } else { STATUS_SCRIPT };
let init_script = format!(
r#"
document.addEventListener('DOMContentLoaded', function() {{
Expand Down
8 changes: 7 additions & 1 deletion examples/api/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
));
))
.plugin(tauri_plugin_deep_link::init());
if let Some(ref client) = sentry_client {
builder = builder.plugin(tauri_plugin_sentry::init(client));
}
Expand All @@ -122,6 +123,11 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
}));
}

#[cfg(target_env = "ohos")]
{
builder = builder.plugin(tauri_plugin_deep_link::init());
}

#[cfg(target_env = "ohos")]
{
builder = builder
Expand Down
5 changes: 5 additions & 0 deletions examples/api/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
}
},
"plugins": {
"deep-link": {
"mobile": [
{ "scheme": ["taurideeplink"] }
]
},
"cli": {
"description": "Tauri API example",
"args": [
Expand Down
Loading
Loading