Compare commits

...

85 Commits

Author SHA1 Message Date
Lucas Nogueira
5b8443ffa3 feat(ci): add iOS test 2022-11-21 12:28:34 -03:00
Lucas Fernandes Nogueira
be808a9f5c feat(ci): add Android test workflow (#5661) 2022-11-21 11:47:12 -03:00
Lucas Fernandes Nogueira
6dcb7fbb81 fix(core): manage mobile logs, fix Android logcat filtering (#5659) 2022-11-20 10:55:38 -03:00
Lucas Fernandes Nogueira
f6f9192aa5 fix(core): Android compilation on Windows (#5658) 2022-11-20 09:49:23 -03:00
Lucas Fernandes Nogueira
03d6c6a68f refactor(cli): use jsonrpsee for mobile CLI options communication (#5657) 2022-11-20 09:48:02 -03:00
Lucas Nogueira
538e21e2e7 fix(cli): iOS development team is required on init 2022-11-19 14:25:23 -03:00
Lucas Fernandes Nogueira
658bb1165e feat(cli): automatically enable native-tls-vendored feature on mobile (#5651) 2022-11-18 13:10:20 -03:00
Lucas Nogueira
634c6b832c feat(cli): update cargo-mobile including Android env var cleanup 2022-11-14 19:59:46 -03:00
Lucas Fernandes Nogueira
8c576222ba feat(cli): find development teams for iOS development (#5627) 2022-11-14 18:31:51 -03:00
Lucas Nogueira
169d2434ff Merge remote-tracking branch 'origin/dev' into next 2022-11-11 11:46:15 -03:00
Lucas Nogueira
f9b529a96e feat(cli): update project.yml syntax 2022-11-11 11:43:25 -03:00
Lucas Nogueira
7c26514340 fix(cli): kill beforeDevCommand process when mobile fails to compile 2022-11-11 11:41:20 -03:00
Lucas Nogueira
69414a487f chore(cli): lint 2022-11-09 11:50:41 -03:00
Lucas Nogueira
602d3ed3f6 Merge branch 'dev' into next 2022-11-09 11:04:54 -03:00
Lucas Nogueira
348b26ffed fix(ci): minimum rustc is now 1.59 2022-10-31 13:27:22 -03:00
Lucas Nogueira
d0ad0d2f48 Merge remote-tracking branch 'origin/dev' into next 2022-10-31 10:13:57 -03:00
Amr Bashir
10ab3e2f5e feat(cli/mobile): allow checking gen folder into source control (#5146)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
2022-09-12 16:46:09 -03:00
Jeffrey Hutchins
8ea87e9c9c feat(android): with_webview access for jni execution (#5148) 2022-09-08 10:57:10 -03:00
Amr Bashir
c9ad2a73af fix(cli/mobile): use one write_all call (#5155)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
2022-09-07 12:05:11 -03:00
Lucas Fernandes Nogueira
bc1622c5ab feat(core): use native window dialogs on Android (#5137) 2022-09-05 16:16:59 -03:00
Lucas Nogueira
e1b8ee2b7a refactor(cli): move generated Kotlin files to the generated folder 2022-09-04 22:06:56 -03:00
Lucas Nogueira
f333656876 fix(core): set response mimetype on dev 2022-09-04 14:16:52 -03:00
Lucas Nogueira
4c1c78ebf0 chore(cli): update lockfile 2022-09-04 13:49:49 -03:00
Lucas Nogueira
610aab7045 Merge branch 'dev' into next 2022-09-04 13:45:59 -03:00
Amr Bashir
30d0e190bb fix(cli/templates): fix desktop entrypoint template (#5127) 2022-09-02 12:21:15 -03:00
Amr Bashir
5643ece77c fix(cli/mobile): strip \n before parsing json (#5126) 2022-09-02 10:30:42 -03:00
Lucas Nogueira
aae91a9b53 refactor(tauri-codegen): panic if local IP address cannot be resolved 2022-09-01 16:58:41 -03:00
Lucas Nogueira
349895b296 refactor(cli): remove mobile assets folder symlink 2022-09-01 12:25:08 -03:00
Lucas Nogueira
5d82357166 feat(cli): add --reinstall-deps option to ios init 2022-09-01 12:07:06 -03:00
Lucas Fernandes Nogueira
8f3a9c5cf6 feat(cli): improve device/simulator prompt logic (#5114) 2022-09-01 12:00:17 -03:00
Lucas Fernandes Nogueira
6593f267b3 feat(cli): iOS simulator support on Intel based devices (#5112) 2022-08-31 21:40:51 -03:00
Lucas Nogueira
5d3242c496 fix(examples): keep the target fallback 2022-08-31 20:53:21 -03:00
Lucas Fernandes Nogueira
68e80ffaa9 feat(cli): add option to run on specific iOS simulator/device (#5098) 2022-08-30 16:09:06 -03:00
Lucas Nogueira
9f9e3ae54d fix: improve target check on context codegen 2022-08-30 13:22:26 -03:00
Lucas Fernandes Nogueira
82e8751ae8 feat(cli): add option to run on specific Android emulator/device (#5093) 2022-08-30 10:27:53 -03:00
Lucas Fernandes Nogueira
e22d21beaf fix(cli): add timeout on interprocess communication (#5090) 2022-08-29 14:46:37 -03:00
Lucas Fernandes Nogueira
b36ccb7e99 feat(ci): test mobile targets (#5078) 2022-08-28 23:02:32 -03:00
Lucas Nogueira
65e7085d2f fix(ci): checkout repo in udeps.yml 2022-08-28 17:57:36 -03:00
Lucas Nogueira
c4d323b70f fix(ci): set target on test-core.yml 2022-08-28 17:57:31 -03:00
Lucas Nogueira
914c5f299f Merge remote-tracking branch 'origin/dev' into next 2022-08-28 16:27:01 -03:00
Lucas Fernandes Nogueira
1d9226b28c refactor:move iOS configuration to the bundle object (#5072) 2022-08-28 15:34:41 -03:00
Lucas Fernandes Nogueira
8cf9af93eb refactor(cli): remove mobile Error enum, use anyhow instead (#5076) 2022-08-28 15:32:50 -03:00
Jeffrey Hutchins
0a203a13ae fix(cli): config issues on macos (#5075)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
2022-08-28 14:48:15 -03:00
Lucas Fernandes Nogueira
dee19784f9 fix(cli): only require development team when running iOS commands (#5071) 2022-08-27 16:35:57 -03:00
Lucas Fernandes Nogueira
53a3398beb feat(cli): improve DX on android dev and ios dev commands (#5059) 2022-08-26 16:08:44 -03:00
Lucas Fernandes Nogueira
69aaff5507 feat(cli): persist verbosity for the IDE scripts (#5047) 2022-08-26 09:24:23 -03:00
Lucas Fernandes Nogueira
80a301ea63 feat(cli): add mobile support to the app template (#5046) 2022-08-25 16:43:29 -03:00
Lucas Fernandes Nogueira
badad2b9a1 feat(cli): implement verbosity on mobile commands (#5044) 2022-08-25 16:06:00 -03:00
Lucas Fernandes Nogueira
927ccc465d refactor(cli): improve security of android dev/build (#5043) 2022-08-25 15:30:05 -03:00
Lucas Fernandes Nogueira
4a5f2ec1ae feat(android): enable dev HMR in both HTTP and HTTPS dev servers (#5033) 2022-08-25 00:19:47 -03:00
Lucas Fernandes Nogueira
752ad3b203 feat(cli): use templates from wry (#5030) 2022-08-24 16:06:14 -03:00
Lucas Fernandes Nogueira
ff4cb56b2e fix(tauri-macros): escape _ in mobile entry point's app name (#5029) 2022-08-24 14:02:08 -03:00
Lucas Fernandes Nogueira
641d56dcb3 feat(android): improve initialization scripts (#5028) 2022-08-24 13:06:45 -03:00
Lucas Fernandes Nogueira
0500d3b4b1 fix(core): mobile app name must match the crate name (#5027) 2022-08-24 12:41:57 -03:00
Amr Bashir
1f84385e8a chore: update cargo-mobile to latest (#5025)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
2022-08-24 10:01:32 -03:00
Lucas Fernandes Nogueira
3668a1fdc8 fix(cli): resolve absolute tauri binary path for the android template (#5015) 2022-08-23 13:54:41 -03:00
Amr Bashir
2b846f413c feat(examples/api): use strict port (#5013) 2022-08-23 12:40:02 -03:00
Amr Bashir
4e26b05d20 fix(cli/template/android): use raw string for executable (#5012) 2022-08-23 12:39:26 -03:00
Lucas Nogueira
a70f7b26bc feat(cli): improve error messages in mobile commands 2022-08-23 11:34:16 -03:00
Lucas Fernandes Nogueira
e56a9dd729 refactor(cli): move mobile commands to their own module (#5005) 2022-08-22 21:59:17 -03:00
Lucas Fernandes Nogueira
403859d47e feat(cli): add ios build command (#5002) 2022-08-22 17:47:52 -03:00
Lucas Fernandes Nogueira
4c9ea450c3 feat(cli): add android build command (#4999) 2022-08-22 12:49:58 -03:00
Lucas Fernandes Nogueira
b3a3afc7de feat(core): detect android and ios platform configuration files (#4997) 2022-08-22 10:48:06 -03:00
Lucas Nogueira
3d992a8899 Merge branch 'dev' into next 2022-08-21 17:48:56 -03:00
Lucas Fernandes Nogueira
9bb81bc328 refactor(android): rename BuildTask.kt command to android-studio-script (#4991) 2022-08-21 17:48:38 -03:00
Lucas Fernandes Nogueira
9890486321 feat(core): add mobile_entry_point macro (#4983) 2022-08-21 10:35:34 -03:00
Lucas Nogueira
16360aed33 chore(cli): update cargo-mobile to fix cli.js mobile script running 2022-08-21 10:35:11 -03:00
Lucas Fernandes Nogueira
e55d96acc1 feat(cli): enhance iOS build task arguments (#4987) 2022-08-21 10:34:38 -03:00
Lucas Fernandes Nogueira
8aad710064 feat(cli): enhance android build task arguments (#4986) 2022-08-21 09:58:27 -03:00
Lucas Fernandes Nogueira
6f0615044d feat(cli): add android dev and ios dev commands (#4982) 2022-08-20 16:53:07 -03:00
Lucas Nogueira
f445f374a3 feat(android): update project dependencies 2022-08-18 11:31:07 -03:00
Lucas Nogueira
a3680ef2bc feat(cli): skip dev tools installation on mobile init 2022-08-17 16:40:04 -03:00
Lucas Nogueira
e20145cccc fix(examples): change API example domain, export mobile mod 2022-08-16 11:48:01 -03:00
Lucas Nogueira
c2b120be51 Merge branch 'dev' into next 2022-08-16 10:13:55 -03:00
Lucas Fernandes Nogueira
3f655d6280 refactor: pull mobile config from tauri config instead of mobile.toml (#4948) 2022-08-16 09:44:55 -03:00
Lucas Fernandes Nogueira
a9c8e565c6 feat: add android open and ios open commands (#4946) 2022-08-15 13:38:17 -03:00
Lucas Nogueira
a9f8ac7f96 fix(examples): set API lib crate-type 2022-08-15 13:24:26 -03:00
Lucas Fernandes Nogueira
d44f67f7af feat: add android init and ios init commands (#4942) 2022-08-15 12:43:50 -03:00
Lucas Nogueira
d3179b84b5 chore(examples): readd gitignore rules for mobile api example 2022-08-14 20:54:14 -03:00
Lucas Nogueira
4c9c64c429 Merge branch 'dev' into next 2022-08-14 18:40:06 -03:00
Lucas Nogueira
d14322de68 chore(deps): update to wry 0.20.2 refactor 2022-08-14 17:52:20 -03:00
Lucas Nogueira
899f9b917a Merge remote-tracking branch 'origin/dev' into next 2022-08-12 20:34:51 -03:00
Lucas Nogueira
11d50e8474 Merge remote-tracking branch 'origin/dev' into next 2022-08-12 20:09:23 -03:00
Lucas Nogueira
6aee91a181 feat(core): prepare for Android 2022-08-12 14:11:14 -03:00
Lucas Fernandes Nogueira
b4622ea4d3 refactor(app): run setup and window creation when event loop is ready (#4914) 2022-08-11 10:30:55 -03:00
126 changed files with 6547 additions and 783 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri-build": patch
---
Set environment variables used by `tauri::mobile_entry_point`.

View File

@@ -0,0 +1,6 @@
---
"cli.rs": minor
"cli.js": minor
---
Added `android build` command.

View File

@@ -0,0 +1,6 @@
---
"cli.rs": patch
"cli.js": patch
---
Added `ios build` command.

View File

@@ -0,0 +1,6 @@
---
"cli.rs": minor
"cli.js": minor
---
Added `android dev` and `ios dev` commands.

View File

@@ -0,0 +1,5 @@
---
"tauri-codegen": patch
---
Change `devPath` URL to use the local IP address on iOS and Android.

View File

@@ -3,7 +3,6 @@
"timeout": 3600000,
"pkgManagers": {
"rust": {
"errorOnVersionRange": "^2.0.0-0",
"version": true,
"getPublishedVersion": "cargo search ${ pkgFile.pkg.package.name } --limit 1 | sed -nE \"s/^[^\\\"]*\\\"//; s/\\\".*//1p\"",
"prepublish": [
@@ -68,7 +67,6 @@
]
},
"javascript": {
"errorOnVersionRange": "^2.0.0-0",
"version": true,
"getPublishedVersion": "npm view ${ pkgFile.pkg.name } version",
"prepublish": [

View File

@@ -0,0 +1,5 @@
---
"tauri": major
---
Added the `default-tls` and `reqwest-default-tls` Cargo features for enabling TLS suppport to connect over HTTPS.

5
.changes/dev-proxy.md Normal file
View File

@@ -0,0 +1,5 @@
---
"tauri": major
---
**Breaking change:** Use the custom protocol as a proxy to the development server on all platforms except Linux.

View File

@@ -0,0 +1,5 @@
---
"tauri-utils": minor
---
Parse `android` and `ios` Tauri configuration files.

View File

@@ -0,0 +1,5 @@
---
"tauri-macros": minor
---
Added the `mobile_entry_point` macro.

6
.changes/mobile-init.md Normal file
View File

@@ -0,0 +1,6 @@
---
"cli.rs": minor
"cli.js": minor
---
Added `android init` and `ios init` commands.

6
.changes/mobile-open.md Normal file
View File

@@ -0,0 +1,6 @@
---
"cli.rs": minor
"cli.js": minor
---
Added `android open` and `ios open` commands.

View File

@@ -0,0 +1,6 @@
---
"tauri-runtime-wry": minor
"tauri": minor
---
Support `with_webview` for Android platform alowing execution of JNI code in context.

View File

@@ -0,0 +1,5 @@
---
"tauri": major
---
**Breaking change:** The window creation and setup hook are now called when the event loop is ready.

View File

@@ -0,0 +1,5 @@
---
"tauri": minor
---
Export types required by the `mobile_entry_point` macro.

View File

@@ -34,29 +34,49 @@ jobs:
- {
target: x86_64-pc-windows-msvc,
os: windows-latest,
toolchain: '1.61.0'
toolchain: '1.61.0',
cross: false,
command: 'test'
}
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-latest,
toolchain: '1.59.0'
toolchain: '1.59.0',
cross: false,
command: 'test'
}
- {
target: x86_64-apple-darwin,
os: macos-latest,
toolchain: '1.59.0'
toolchain: '1.59.0',
cross: false,
command: 'test'
}
- {
target: aarch64-apple-ios,
os: macos-latest,
toolchain: '1.59.0',
cross: false,
command: 'build'
}
- {
target: aarch64-linux-android,
os: ubuntu-latest,
toolchain: '1.59.0',
cross: true,
command: 'build'
}
features:
- {
args: --no-default-features,
args: --no-default-features --features native-tls-vendored,
key: no-default
}
- {
args: --features api-all,
args: --features native-tls-vendored,api-all,
key: api-all
}
- {
args: --features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray,windows7-compat,http-multipart,
args: --features native-tls-vendored,compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray,windows7-compat,http-multipart,
key: all
}
@@ -83,4 +103,8 @@ jobs:
save-if: ${{ matrix.features.key == 'all' }}
- name: test
run: cargo test --target ${{ matrix.platform.target }} ${{ matrix.features.args }}
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.platform.cross }}
command: ${{ matrix.platform.command }}
args: --target ${{ matrix.platform.target }} ${{ matrix.features.args }}

106
.github/workflows/test-mobile.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: test mobile
on:
pull_request:
paths:
- '.github/workflows/test-mobile.yml'
- 'tooling/cli/templates/mobile/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: install Linux dependencies
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
cache-dependency-path: |
tooling/api/yarn.lock
examples/api/yarn.lock
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: gradle
- name: Setup NDK
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r25b
local-cache: true
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
tooling/cli
examples/api/src-tauri
- name: build CLI
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path ./tooling/cli/Cargo.toml
- name: build Tauri API
working-directory: ./tooling/api
run: yarn && yarn build
- name: install API example dependencies
working-directory: ./examples/api
run: yarn
- name: init Android Studio project
working-directory: ./examples/api
run: ../../tooling/cli/target/debug/cargo-tauri android init
env:
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: build APK
working-directory: ./examples/api
run: ../../tooling/cli/target/debug/cargo-tauri android build
env:
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: setup iOS code signing certificate
if: matrix.platform == 'macos-latest'
run: ./tooling/cli/target/debug/cargo-tauri ios certificate setup
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: init XCode project
if: matrix.platform == 'macos-latest'
working-directory: ./examples/api
run: ../../tooling/cli/target/debug/cargo-tauri ios init
- name: build IPA
if: matrix.platform == 'macos-latest'
run: ../../tooling/cli/target/debug/cargo-tauri ios build

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Config",
"description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler, enable the app updater, define a system tray, enable APIs via the allowlist and more.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, and `tauri.macos.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml` and `Tauri.macos.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"allowlist\": { \"all\": true }, \"bundle\": {}, \"security\": { \"csp\": null }, \"updater\": { \"active\": false }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
"description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler, enable the app updater, define a system tray, enable APIs via the allowlist and more.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"allowlist\": { \"all\": true }, \"bundle\": {}, \"security\": { \"csp\": null }, \"updater\": { \"active\": false }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
"type": "object",
"properties": {
"$schema": {
@@ -135,6 +135,7 @@
"deb": {
"files": {}
},
"iOS": {},
"icon": [],
"identifier": "",
"macOS": {
@@ -271,6 +272,7 @@
"deb": {
"files": {}
},
"iOS": {},
"icon": [],
"identifier": "",
"macOS": {
@@ -1132,6 +1134,15 @@
"$ref": "#/definitions/WindowsConfig"
}
]
},
"iOS": {
"description": "iOS configuration.",
"default": {},
"allOf": [
{
"$ref": "#/definitions/IosConfig"
}
]
}
},
"additionalProperties": false
@@ -1618,6 +1629,20 @@
},
"additionalProperties": false
},
"IosConfig": {
"description": "General configuration for the iOS target.",
"type": "object",
"properties": {
"developmentTeam": {
"description": "The development team. This value is required for iOS development because code signing is enforced. The `TAURI_APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"AllowlistConfig": {
"description": "Allowlist configuration.",
"type": "object",

View File

@@ -219,6 +219,18 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
}
let config: Config = serde_json::from_value(config)?;
let s = config.tauri.bundle.identifier.split('.');
let last = s.clone().count() - 1;
let mut domain = String::new();
for (i, w) in s.enumerate() {
if i != last {
domain.push_str(w);
domain.push('_');
}
}
domain.pop();
println!("cargo:rustc-env=TAURI_ANDROID_DOMAIN={}", domain);
cfg_alias("dev", !has_feature("custom-protocol"));
let mut manifest = Manifest::from_path("Cargo.toml")?;
@@ -275,6 +287,9 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
}
let target_triple = std::env::var("TARGET").unwrap();
println!("cargo:rustc-env=TAURI_TARGET_TRIPLE={}", target_triple);
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
// TODO: far from ideal, but there's no other way to get the target dir, see <https://github.com/rust-lang/cargo/issues/5457>
let target_dir = out_dir
@@ -333,11 +348,12 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
.window_icon_path
.unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico"));
if window_icon_path.exists() {
let mut res = WindowsResource::new();
if target_triple.contains("windows") {
if window_icon_path.exists() {
let mut res = WindowsResource::new();
res.set_manifest(
r#"
res.set_manifest(
r#"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
@@ -353,42 +369,43 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
</dependency>
</assembly>
"#,
);
);
if let Some(sdk_dir) = &attributes.windows_attributes.sdk_dir {
if let Some(sdk_dir_str) = sdk_dir.to_str() {
res.set_toolkit_path(sdk_dir_str);
} else {
return Err(anyhow!(
"sdk_dir path is not valid; only UTF-8 characters are allowed"
));
if let Some(sdk_dir) = &attributes.windows_attributes.sdk_dir {
if let Some(sdk_dir_str) = sdk_dir.to_str() {
res.set_toolkit_path(sdk_dir_str);
} else {
return Err(anyhow!(
"sdk_dir path is not valid; only UTF-8 characters are allowed"
));
}
}
}
if let Some(version) = &config.package.version {
if let Ok(v) = Version::parse(version) {
let version = v.major << 48 | v.minor << 32 | v.patch << 16;
res.set_version_info(VersionInfo::FILEVERSION, version);
res.set_version_info(VersionInfo::PRODUCTVERSION, version);
if let Some(version) = &config.package.version {
if let Ok(v) = Version::parse(version) {
let version = v.major << 48 | v.minor << 32 | v.patch << 16;
res.set_version_info(VersionInfo::FILEVERSION, version);
res.set_version_info(VersionInfo::PRODUCTVERSION, version);
}
res.set("FileVersion", version);
res.set("ProductVersion", version);
}
res.set("FileVersion", version);
res.set("ProductVersion", version);
}
if let Some(product_name) = &config.package.product_name {
res.set("ProductName", product_name);
res.set("FileDescription", product_name);
}
res.set_icon_with_id(&window_icon_path.display().to_string(), "32512");
res.compile().with_context(|| {
format!(
"failed to compile `{}` into a Windows Resource file during tauri-build",
if let Some(product_name) = &config.package.product_name {
res.set("ProductName", product_name);
res.set("FileDescription", product_name);
}
res.set_icon_with_id(&window_icon_path.display().to_string(), "32512");
res.compile().with_context(|| {
format!(
"failed to compile `{}` into a Windows Resource file during tauri-build",
window_icon_path.display()
)
})?;
} else {
return Err(anyhow!(format!(
"`{}` not found; required for generating a Windows Resource file during tauri-build",
window_icon_path.display()
)
})?;
} else {
return Err(anyhow!(format!(
"`{}` not found; required for generating a Windows Resource file during tauri-build",
window_icon_path.display()
)));
)));
}
}
let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();

View File

@@ -29,6 +29,10 @@ semver = "1"
ico = "0.1"
png = "0.17"
json-patch = "0.2"
url = "2"
[target."cfg(any(windows, target_os = \"linux\", target_os = \"macos\"))".dependencies]
local-ip-address = "0.4"
[target."cfg(target_os = \"macos\")".dependencies]
plist = "1"

View File

@@ -124,38 +124,56 @@ enum Target {
pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsError> {
let ContextData {
dev,
config,
mut config,
config_parent,
root,
} = data;
let target = if let Ok(target) = std::env::var("TARGET") {
if target.contains("unknown-linux") {
let target =
if let Ok(target) = std::env::var("TARGET").or_else(|_| std::env::var("TAURI_TARGET_TRIPLE")) {
if target.contains("unknown-linux") {
Target::Linux
} else if target.contains("pc-windows") {
Target::Windows
} else if target.contains("apple-darwin") {
Target::Darwin
} else if target.contains("android") {
Target::Android
} else if target.contains("apple-ios") {
Target::Ios
} else {
panic!("unknown codegen target {}", target);
}
} else if cfg!(target_os = "linux") {
Target::Linux
} else if target.contains("pc-windows") {
} else if cfg!(windows) {
Target::Windows
} else if target.contains("apple-darwin") {
} else if cfg!(target_os = "macos") {
Target::Darwin
} else if target.contains("android") {
} else if cfg!(target_os = "android") {
Target::Android
} else if target.contains("apple-ios") {
} else if cfg!(target_os = "ios") {
Target::Ios
} else {
panic!("unknown codegen target {}", target);
panic!("unknown codegen target")
};
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
if dev && (target == Target::Ios || target == Target::Android) {
if let AppUrl::Url(WindowUrl::External(url)) = &mut config.build.dev_path {
let localhost = match url.host() {
Some(url::Host::Domain(d)) => d == "localhost",
Some(url::Host::Ipv4(i)) => {
i == std::net::Ipv4Addr::LOCALHOST || i == std::net::Ipv4Addr::UNSPECIFIED
}
_ => false,
};
if localhost {
let ip = local_ip_address::local_ip().expect("failed to resolve local IP address");
url.set_host(Some(&ip.to_string())).unwrap();
}
}
} else if cfg!(target_os = "linux") {
Target::Linux
} else if cfg!(windows) {
Target::Windows
} else if cfg!(target_os = "macos") {
Target::Darwin
} else if cfg!(target_os = "android") {
Target::Android
} else if cfg!(target_os = "ios") {
Target::Ios
} else {
panic!("unknown codegen target");
};
}
let mut options = AssetOptions::new(config.tauri.pattern.clone())
.freeze_prototype(config.tauri.security.freeze_prototype)

View File

@@ -431,6 +431,7 @@ impl ToTokens for EmbeddedAssets {
// we expect phf related items to be in path when generating the path code
tokens.append_all(quote! {{
#[allow(unused_imports)]
use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map};
EmbeddedAssets::new(phf_map! { #assets }, &[#global_hashes], phf_map! { #html_hashes })
}});

View File

@@ -8,6 +8,7 @@ use syn::{parse_macro_input, DeriveInput, ItemFn};
mod command;
mod command_module;
mod mobile;
mod runtime;
#[macro_use]
@@ -24,6 +25,11 @@ pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream {
command::wrapper(attributes, item)
}
#[proc_macro_attribute]
pub fn mobile_entry_point(attributes: TokenStream, item: TokenStream) -> TokenStream {
mobile::entry_point(attributes, item)
}
/// Accepts a list of commands functions. Creates a handler that allows commands to be called from JS with invoke().
///
/// # Examples

View File

@@ -0,0 +1,83 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use std::env::var;
use syn::{parse_macro_input, spanned::Spanned, ItemFn};
fn get_env_var<R: FnOnce(String) -> String>(
name: &str,
replacer: R,
error: &mut Option<TokenStream2>,
function: &ItemFn,
) -> TokenStream2 {
match var(name) {
Ok(value) => {
let ident = format_ident!("{}", replacer(value));
quote!(#ident)
}
Err(_) => {
error.replace(
syn::Error::new(
function.span(),
format!(
"`{}` env var not set, do you have a build script with tauri-build?",
name,
),
)
.into_compile_error(),
);
quote!()
}
}
}
pub fn entry_point(_attributes: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn);
let function_name = function.sig.ident.clone();
let mut error = None;
let domain = get_env_var("TAURI_ANDROID_DOMAIN", |r| r, &mut error, &function);
let app_name = get_env_var(
"CARGO_PKG_NAME",
|r| r.replace('_', "_1"),
&mut error,
&function,
);
let app_name_str = var("CARGO_PKG_NAME").unwrap();
if let Some(e) = error {
quote!(#e).into()
} else {
quote!(
fn stop_unwind<F: FnOnce() -> T, T>(f: F) -> T {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(t) => t,
Err(err) => {
eprintln!("attempt to unwind out of `rust` with err: {:?}", err);
std::process::abort()
}
}
}
#function
fn _start_app() {
::tauri::init_logging(#app_name_str);
#[cfg(target_os = "android")]
{
use ::tauri::paste;
::tauri::wry_android_binding!(#domain, #app_name, _start_app, ::tauri::wry);
}
stop_unwind(#function_name);
}
#[cfg(not(target_os = "android"))]
#[no_mangle]
#[inline(never)]
pub extern "C" fn start_app() {
_start_app()
}
)
.into()
}
}

View File

@@ -100,9 +100,9 @@ type FileDropHandler = dyn Fn(&Window, WryFileDropEvent) -> bool + 'static;
#[cfg(all(desktop, feature = "system-tray"))]
pub use tauri_runtime::TrayId;
#[cfg(desktop)]
#[cfg(any(desktop, target_os = "android"))]
mod webview;
#[cfg(desktop)]
#[cfg(any(desktop, target_os = "android"))]
pub use webview::Webview;
#[cfg(all(desktop, feature = "system-tray"))]
@@ -1005,7 +1005,7 @@ pub enum ApplicationMessage {
}
pub enum WindowMessage {
#[cfg(desktop)]
#[cfg(any(desktop, target_os = "android"))]
WithWebview(Box<dyn FnOnce(Webview) + Send>),
AddEventListener(Uuid, Box<dyn Fn(&WindowEvent) + Send>),
AddMenuEventListener(Uuid, Box<dyn Fn(&MenuEvent) + Send>),
@@ -1156,7 +1156,7 @@ pub struct WryDispatcher<T: UserEvent> {
unsafe impl<T: UserEvent> Sync for WryDispatcher<T> {}
impl<T: UserEvent> WryDispatcher<T> {
#[cfg(desktop)]
#[cfg(any(desktop, target_os = "android"))]
pub fn with_webview<F: FnOnce(Webview) + Send + 'static>(&self, f: F) -> Result<()> {
send_user_message(
&self.context,
@@ -2253,7 +2253,7 @@ fn handle_user_message<T: UserEvent>(
});
if let Some((Some(window), window_event_listeners, menu_event_listeners)) = w {
match window_message {
#[cfg(desktop)]
#[cfg(any(target_os = "android", desktop))]
WindowMessage::WithWebview(f) => {
if let WindowHandle::Webview { inner: w, .. } = &window {
#[cfg(any(
@@ -2276,13 +2276,17 @@ fn handle_user_message<T: UserEvent>(
ns_window: w.ns_window(),
});
}
#[cfg(windows)]
{
f(Webview {
controller: w.controller(),
});
}
#[cfg(target_os = "android")]
{
use wry::webview::WebviewExtAndroid;
f(w.handle())
}
}
}

View File

@@ -34,4 +34,10 @@ mod imp {
}
}
#[cfg(target_os = "android")]
mod imp {
use wry::webview::JniHandle;
pub type Webview = JniHandle;
}
pub use imp::*;

View File

@@ -599,6 +599,9 @@ pub struct BundleConfig {
/// Configuration for the Windows bundle.
#[serde(default)]
pub windows: WindowsConfig,
/// iOS configuration.
#[serde(rename = "iOS", default)]
pub ios: IosConfig,
}
/// A CLI argument definition.
@@ -2475,6 +2478,18 @@ fn default_dialog() -> bool {
true
}
/// General configuration for the iOS target.
#[skip_serializing_none]
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct IosConfig {
/// The development team. This value is required for iOS development because code signing is enforced.
/// The `TAURI_APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it.
#[serde(alias = "development-team")]
pub development_team: Option<String>,
}
/// Defines the URL or assets to embed in the application.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
@@ -2723,8 +2738,8 @@ impl PackageConfig {
///
/// In addition to the default configuration file, Tauri can
/// read a platform-specific configuration from `tauri.linux.conf.json`,
/// `tauri.windows.conf.json`, and `tauri.macos.conf.json`
/// (or `Tauri.linux.toml`, `Tauri.windows.toml` and `Tauri.macos.toml` if the `Tauri.toml` format is used),
/// `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`
/// (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),
/// which gets merged with the main configuration object.
///
/// ## Configuration Structure
@@ -3252,6 +3267,7 @@ mod build {
let macos = quote!(Default::default());
let external_bin = opt_vec_str_lit(self.external_bin.as_ref());
let windows = &self.windows;
let ios = quote!(Default::default());
literal_struct!(
tokens,
@@ -3270,7 +3286,8 @@ mod build {
deb,
macos,
external_bin,
windows
windows,
ios
);
}
}
@@ -3686,6 +3703,7 @@ mod test {
macos: Default::default(),
external_bin: None,
windows: Default::default(),
ios: Default::default(),
},
cli: None,
updater: UpdaterConfig {

View File

@@ -54,6 +54,10 @@ impl ConfigFormat {
"tauri.macos.conf.json"
} else if cfg!(windows) {
"tauri.windows.conf.json"
} else if cfg!(target_os = "android") {
"tauri.android.conf.json"
} else if cfg!(target_os = "ios") {
"tauri.ios.conf.json"
} else {
"tauri.linux.conf.json"
}
@@ -63,6 +67,10 @@ impl ConfigFormat {
"tauri.macos.conf.json5"
} else if cfg!(windows) {
"tauri.windows.conf.json5"
} else if cfg!(target_os = "android") {
"tauri.android.conf.json"
} else if cfg!(target_os = "ios") {
"tauri.ios.conf.json"
} else {
"tauri.linux.conf.json5"
}
@@ -72,6 +80,10 @@ impl ConfigFormat {
"Tauri.macos.toml"
} else if cfg!(windows) {
"Tauri.windows.toml"
} else if cfg!(target_os = "android") {
"tauri.android.toml"
} else if cfg!(target_os = "ios") {
"tauri.ios.toml"
} else {
"Tauri.linux.toml"
}
@@ -170,11 +182,13 @@ pub fn is_configuration_file(path: &Path) -> bool {
/// Reads the configuration from the given root directory.
///
/// It first looks for a `tauri.conf.json[5]` file on the given directory. The file must exist.
/// It first looks for a `tauri.conf.json[5]` or `Tauri.toml` file on the given directory. The file must exist.
/// Then it looks for a platform-specific configuration file:
/// - `tauri.macos.conf.json[5]` on macOS
/// - `tauri.linux.conf.json[5]` on Linux
/// - `tauri.windows.conf.json[5]` on Windows
/// - `tauri.macos.conf.json[5]` or `Tauri.macos.toml` on macOS
/// - `tauri.linux.conf.json[5]` or `Tauri.linux.toml` on Linux
/// - `tauri.windows.conf.json[5]` or `Tauri.windows.toml` on Windows
/// - `tauri.android.conf.json[5]` or `Tauri.android.toml` on Android
/// - `tauri.ios.conf.json[5]` or `Tauri.ios.toml` on iOS
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
///
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.

View File

@@ -67,9 +67,9 @@ dirs-next = "2.0"
percent-encoding = "2.2"
base64 = { version = "0.13", optional = true }
clap = { version = "3", optional = true }
reqwest = { version = "0.11", features = [ "json", "stream" ], optional = true }
reqwest = { version = "0.11", default-features = false, features = [ "json", "stream" ], optional = true }
bytes = { version = "1", features = [ "serde" ], optional = true }
attohttpc = { version = "0.22", features = [ "compress", "json", "form" ], optional = true }
attohttpc = { version = "0.24", default-features = false, features = [ "compress", "json", "form" ] }
open = { version = "3.0", optional = true }
shared_child = { version = "1.0", optional = true }
os_pipe = { version = "1.0", optional = true }
@@ -104,9 +104,17 @@ objc = "0.2"
webview2-com = "0.19.1"
win7-notifications = { version = "0.3.1", optional = true }
[target."cfg(windows)".dependencies.windows]
version = "0.39.0"
features = [ "Win32_Foundation" ]
[target.'cfg(target_os = "android")'.dependencies]
paste = "1.0"
android_logger = "0.9"
log = "0.4"
[target.'cfg(target_os = "ios")'.dependencies]
env_logger = "0.9.0"
[target."cfg(windows)".dependencies.windows]
version = "0.39.0"
features = [ "Win32_Foundation" ]
[build-dependencies]
heck = "0.4"
@@ -139,11 +147,13 @@ updater = [
"dialog-ask",
"fs-extract-api"
]
http-api = [ "attohttpc" ]
http-api = [ ]
http-multipart = [ "attohttpc/multipart-form", "reqwest/multipart" ]
shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ]
fs-extract-api = [ "zip" ]
reqwest-client = [ "reqwest", "bytes" ]
reqwest-default-tls = [ "reqwest-client", "reqwest/default-tls" ]
default-tls = [ "attohttpc/tls-native" ]
reqwest-native-tls-vendored = [ "reqwest-client", "reqwest/native-tls-vendored" ]
native-tls-vendored = [ "attohttpc/tls-vendored" ]
process-command-api = [ "shared_child", "os_pipe" ]

View File

@@ -245,41 +245,4 @@
setNotificationPermission(response ? 'granted' : 'denied')
}
})
window.alert = function (message) {
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Dialog',
message: {
cmd: 'messageDialog',
message: message.toString()
}
})
}
window.confirm = function (message) {
return window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Dialog',
message: {
cmd: 'confirmDialog',
message: message.toString()
}
})
}
// window.print works on Linux/Windows; need to use the API on macOS
if (navigator.userAgent.includes('Mac')) {
window.print = function () {
return window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Window',
message: {
cmd: 'manage',
data: {
cmd: {
type: 'print'
}
}
}
})
}
}
})()

View File

@@ -17,6 +17,10 @@
__RAW_core_script__
__RAW_window_dialogs_script__
__RAW_window_print_script__
__RAW_event_initialization_script__
if (window.ipc) {

View File

@@ -0,0 +1,19 @@
window.alert = function (message) {
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Dialog',
message: {
cmd: 'messageDialog',
message: message.toString()
}
})
}
window.confirm = function (message) {
return window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Dialog',
message: {
cmd: 'confirmDialog',
message: message.toString()
}
})
}

View File

@@ -0,0 +1,13 @@
window.print = function () {
return window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Window',
message: {
cmd: 'manage',
data: {
cmd: {
type: 'print'
}
}
}
})
}

View File

@@ -40,6 +40,7 @@ use tauri_utils::PackageInfo;
use std::{
collections::HashMap,
fmt,
path::{Path, PathBuf},
sync::{mpsc::Sender, Arc, Weak},
};
@@ -559,9 +560,10 @@ impl<R: Runtime> ManagerBase<R> for AppHandle<R> {
///
/// This type implements [`Manager`] which allows for manipulation of global application items.
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct App<R: Runtime> {
runtime: Option<R>,
pending_windows: Option<Vec<PendingWindow<EventLoopMessage, R>>>,
setup: Option<SetupHook<R>>,
manager: WindowManager<R>,
#[cfg(all(desktop, feature = "global-shortcut"))]
global_shortcut_manager: R::GlobalShortcutManager,
@@ -570,6 +572,22 @@ pub struct App<R: Runtime> {
handle: AppHandle<R>,
}
impl<R: Runtime> fmt::Debug for App<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = f.debug_struct("App");
d.field("runtime", &self.runtime)
.field("manager", &self.manager)
.field("handle", &self.handle);
#[cfg(all(desktop, feature = "global-shortcut"))]
d.field("global_shortcut_manager", &self.global_shortcut_manager);
#[cfg(feature = "clipboard")]
d.field("clipboard_manager", &self.clipboard_manager);
d.finish()
}
}
impl<R: Runtime> Manager<R> for App<R> {}
impl<R: Runtime> ManagerBase<R> for App<R> {
fn manager(&self) -> &WindowManager<R> {
@@ -577,7 +595,11 @@ impl<R: Runtime> ManagerBase<R> for App<R> {
}
fn runtime(&self) -> RuntimeOrDispatch<'_, R> {
RuntimeOrDispatch::Runtime(self.runtime.as_ref().unwrap())
if let Some(runtime) = self.runtime.as_ref() {
RuntimeOrDispatch::Runtime(runtime)
} else {
self.handle.runtime()
}
}
fn managed_app_handle(&self) -> AppHandle<R> {
@@ -830,6 +852,17 @@ impl<R: Runtime> App<R> {
let app_handle = self.handle();
let manager = self.manager.clone();
self.runtime.take().unwrap().run(move |event| match event {
RuntimeRunEvent::Ready => {
if let Err(e) = setup(&mut self) {
panic!("Failed to setup app: {}", e);
}
on_event_loop_event(
&app_handle,
RuntimeRunEvent::Ready,
&manager,
Some(&mut callback),
);
}
RuntimeRunEvent::Exit => {
on_event_loop_event(
&app_handle,
@@ -1518,8 +1551,11 @@ impl<R: Runtime> Builder<R> {
#[cfg(feature = "clipboard")]
let clipboard_manager = runtime.clipboard_manager();
#[allow(unused_mut)]
let mut app = App {
runtime: Some(runtime),
pending_windows: Some(self.pending_windows),
setup: Some(self.setup),
manager: manager.clone(),
#[cfg(all(desktop, feature = "global-shortcut"))]
global_shortcut_manager: global_shortcut_manager.clone(),
@@ -1609,26 +1645,6 @@ impl<R: Runtime> Builder<R> {
app.manager.initialize_plugins(&app.handle())?;
let window_labels = self
.pending_windows
.iter()
.map(|p| p.label.clone())
.collect::<Vec<_>>();
for pending in self.pending_windows {
let pending =
app
.manager
.prepare_window(app.handle.clone(), pending, &window_labels, None)?;
let detached = app.runtime.as_ref().unwrap().create_window(pending)?;
let _window = app.manager.attach_window(app.handle(), detached);
}
(self.setup)(&mut app).map_err(|e| crate::Error::Setup(e.into()))?;
#[cfg(updater)]
app.run_updater();
Ok(app)
}
@@ -1651,6 +1667,38 @@ unsafe impl HasRawDisplayHandle for App {
}
}
fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
let pending_windows = app.pending_windows.take();
if let Some(pending_windows) = pending_windows {
let window_labels = pending_windows
.iter()
.map(|p| p.label.clone())
.collect::<Vec<_>>();
for pending in pending_windows {
let pending =
app
.manager
.prepare_window(app.handle.clone(), pending, &window_labels, None)?;
let detached = if let RuntimeOrDispatch::RuntimeHandle(runtime) = app.handle().runtime() {
runtime.create_window(pending)?
} else {
// the AppHandle's runtime is always RuntimeOrDispatch::RuntimeHandle
unreachable!()
};
let _window = app.manager.attach_window(app.handle(), detached);
}
}
if let Some(setup) = app.setup.take() {
(setup)(app).map_err(|e| crate::Error::Setup(e.into()))?;
}
#[cfg(updater)]
app.run_updater();
Ok(())
}
fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
app_handle: &AppHandle<R>,
event: RuntimeRunEvent<EventLoopMessage>,

View File

@@ -78,6 +78,8 @@ fn os_type() -> &'static str {
return "Darwin";
#[cfg(target_os = "ios")]
return "iOS";
#[cfg(target_os = "android")]
return "Android";
}
#[cfg(os_all)]

View File

@@ -22,6 +22,8 @@
//! - **http-api**: Enables the [`api::http`] module.
//! - **http-multipart**: Adds support to `multipart/form-data` requests.
//! - **reqwest-client**: Uses `reqwest` as HTTP client on the `http` APIs. Improves performance, but increases the bundle size.
//! - **default-tls**: Provides TLS support to connect over HTTPS (applies to the default HTTP client).
//! - **reqwest-default-tls**: Provides TLS support to connect over HTTPS (applies to the `reqwest` HTTP client).
//! - **native-tls-vendored**: Compile and statically link to a vendored copy of OpenSSL (applies to the default HTTP client).
//! - **reqwest-native-tls-vendored**: Compile and statically link to a vendored copy of OpenSSL (applies to the `reqwest` HTTP client).
//! - **process-command-api**: Enables the [`api::process::Command`] APIs.
@@ -166,6 +168,8 @@ pub use error::Error;
#[cfg(shell_scope)]
#[doc(hidden)]
pub use regex;
#[cfg(mobile)]
pub use tauri_macros::mobile_entry_point;
pub use tauri_macros::{command, generate_handler};
pub mod api;
@@ -196,6 +200,17 @@ pub use tauri_utils as utils;
#[cfg_attr(doc_cfg, doc(cfg(feature = "wry")))]
pub type Wry = tauri_runtime_wry::Wry<EventLoopMessage>;
#[cfg(all(feature = "wry", target_os = "android"))]
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "wry", target_os = "android"))))]
pub use tauri_runtime_wry::wry::android_binding as wry_android_binding;
#[cfg(all(feature = "wry", target_os = "android"))]
#[doc(hidden)]
pub use paste;
#[cfg(all(feature = "wry", target_os = "android"))]
#[doc(hidden)]
pub use tauri_runtime_wry::wry;
/// `Result<T, ::tauri::Error>`
pub type Result<T> = std::result::Result<T, Error>;
@@ -262,6 +277,22 @@ pub use self::runtime::ClipboardManager;
#[cfg_attr(doc_cfg, doc(cfg(feature = "global-shortcut")))]
pub use self::runtime::GlobalShortcutManager;
#[cfg(target_os = "android")]
#[doc(hidden)]
pub fn init_logging(tag: &str) {
android_logger::init_once(
android_logger::Config::default()
.with_min_level(log::Level::Trace)
.with_tag(tag),
);
}
#[cfg(target_os = "ios")]
#[doc(hidden)]
pub fn init_logging(_tag: &str) {
env_logger::init();
}
/// Updater events.
#[cfg(updater)]
#[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))]

View File

@@ -145,11 +145,6 @@ fn set_csp<R: Runtime>(
Csp::DirectiveMap(csp).to_string()
}
#[cfg(target_os = "linux")]
fn set_html_csp(html: &str, csp: &str) -> String {
html.replacen(tauri_utils::html::CSP_TOKEN, csp, 1)
}
// inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
fn replace_with_callback<F: FnMut() -> String>(
original: &str,
@@ -377,7 +372,13 @@ impl<R: Runtime> WindowManager<R> {
/// Get the origin as it will be seen in the webview.
fn get_browser_origin(&self) -> String {
match self.base_path() {
AppUrl::Url(WindowUrl::External(url)) => url.origin().ascii_serialization(),
AppUrl::Url(WindowUrl::External(url)) => {
if cfg!(dev) && !cfg!(target_os = "linux") {
format_real_schema("tauri")
} else {
url.origin().ascii_serialization()
}
}
_ => format_real_schema("tauri"),
}
}
@@ -884,8 +885,12 @@ impl<R: Runtime> WindowManager<R> {
>,
) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
{
#[cfg(dev)]
let url = self.get_url().into_owned();
#[cfg(not(dev))]
let manager = self.clone();
let window_origin = window_origin.to_string();
Box::new(move |request| {
let path = request
.uri()
@@ -898,32 +903,53 @@ impl<R: Runtime> WindowManager<R> {
// the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
// where `$P` is not `localhost/*`
.unwrap_or_else(|| "".to_string());
let asset = manager.get_asset(path)?;
let mut builder = HttpResponseBuilder::new()
.header("Access-Control-Allow-Origin", &window_origin)
.mimetype(&asset.mime_type);
if let Some(csp) = &asset.csp_header {
builder = builder.header("Content-Security-Policy", csp);
}
let mut response = builder.body(asset.bytes)?;
if let Some(handler) = &web_resource_request_handler {
handler(request, &mut response);
// if it's an HTML file, we need to set the CSP meta tag on Linux
#[cfg(target_os = "linux")]
if let Some(response_csp) = response.headers().get("Content-Security-Policy") {
let response_csp = String::from_utf8_lossy(response_csp.as_bytes());
let body = set_html_csp(&String::from_utf8_lossy(response.body()), &response_csp);
*response.body_mut() = body.as_bytes().to_vec();
}
} else {
#[cfg(target_os = "linux")]
let mut builder =
HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin);
#[cfg(dev)]
let mut response = {
let mut url = url.clone();
url.set_path(&path);
match attohttpc::get(url.as_str())
.danger_accept_invalid_certs(true)
.send()
{
if let Some(csp) = &asset.csp_header {
let body = set_html_csp(&String::from_utf8_lossy(response.body()), csp);
*response.body_mut() = body.as_bytes().to_vec();
Ok(r) => {
for (name, value) in r.headers() {
if name == "Content-Type" {
builder = builder.mimetype(value.to_str().unwrap());
}
builder = builder.header(name, value);
}
builder.status(r.status()).body(r.bytes()?)?
}
Err(e) => {
debug_eprintln!("Failed to request {}: {}", url.as_str(), e);
return Err(Box::new(e));
}
}
};
#[cfg(not(dev))]
let mut response = {
let asset = manager.get_asset(path)?;
builder = builder.mimetype(&asset.mime_type);
if let Some(csp) = &asset.csp_header {
builder = builder.header("Content-Security-Policy", csp);
}
builder.body(asset.bytes)?
};
if let Some(handler) = &web_resource_request_handler {
handler(request, &mut response);
}
// if it's an HTML file, we need to set the CSP meta tag on Linux
#[cfg(all(not(dev), target_os = "linux"))]
if let Some(response_csp) = response.headers().get("Content-Security-Policy") {
let response_csp = String::from_utf8_lossy(response_csp.as_bytes());
let html = String::from_utf8_lossy(response.body());
let body = html.replacen(tauri_utils::html::CSP_TOKEN, &response_csp, 1);
*response.body_mut() = body.as_bytes().to_vec();
}
Ok(response)
})
@@ -952,6 +978,10 @@ impl<R: Runtime> WindowManager<R> {
#[raw]
core_script: &'a str,
#[raw]
window_dialogs_script: &'a str,
#[raw]
window_print_script: &'a str,
#[raw]
event_initialization_script: &'a str,
#[raw]
plugin_initialization_script: &'a str,
@@ -1017,6 +1047,19 @@ impl<R: Runtime> WindowManager<R> {
)
),
core_script: include_str!("../scripts/core.js"),
// window.print works on Linux/Windows; need to use the API on macOS
#[cfg(any(target_os = "macos", target_os = "ios"))]
window_print_script: include_str!("../scripts/window_print.js"),
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
window_print_script: "",
// dialogs are implemented natively on Android
#[cfg(not(target_os = "android"))]
window_dialogs_script: include_str!("../scripts/window_dialogs.js"),
#[cfg(target_os = "android")]
window_dialogs_script: "",
event_initialization_script: &self.event_initialization_script(),
plugin_initialization_script,
freeze_prototype,
@@ -1125,7 +1168,10 @@ impl<R: Runtime> WindowManager<R> {
#[allow(unused_mut)] // mut url only for the data-url parsing
let (is_local, mut url) = match &pending.webview_attributes.url {
WindowUrl::App(path) => {
#[cfg(target_os = "linux")]
let url = self.get_url();
#[cfg(not(target_os = "linux"))]
let url: Cow<'_, Url> = Cow::Owned(Url::parse("tauri://localhost").unwrap());
(
true,
// ignore "index.html" just to simplify the url
@@ -1142,7 +1188,13 @@ impl<R: Runtime> WindowManager<R> {
}
WindowUrl::External(url) => {
let config_url = self.get_url();
(config_url.make_relative(url).is_some(), url.clone())
let is_local = config_url.make_relative(url).is_some();
let mut url = url.clone();
if is_local && !cfg!(target_os = "linux") {
url.set_scheme("tauri").unwrap();
url.set_host(Some("localhost")).unwrap();
}
(is_local, url)
}
_ => unimplemented!(),
};

View File

@@ -86,7 +86,7 @@ pub(crate) struct PatternJavascript {
#[allow(dead_code)]
pub(crate) fn format_real_schema(schema: &str) -> String {
if cfg!(windows) {
if cfg!(windows) || cfg!(target_os = "android") {
format!("https://{}.localhost", schema)
} else {
format!("{}://localhost", schema)

View File

@@ -13,10 +13,10 @@ use std::{borrow::Cow, sync::Arc};
#[cfg(shell_scope)]
use crate::ShellScopeConfig;
use crate::{Manager, Pattern};
use crate::{Manager, Pattern, WindowBuilder};
use tauri_utils::{
assets::{AssetKey, Assets, CspHash},
config::{CliConfig, Config, PatternKind, TauriConfig},
config::{CliConfig, Config, PatternKind, TauriConfig, WindowUrl},
};
pub struct NoopAsset {
@@ -46,7 +46,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
package: Default::default(),
tauri: TauriConfig {
pattern: PatternKind::Brownfield,
windows: vec![Default::default()],
windows: Vec::new(),
cli: Some(CliConfig {
description: None,
long_description: None,
@@ -86,9 +86,15 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
}
pub fn mock_app() -> crate::App<MockRuntime> {
crate::Builder::<MockRuntime>::new()
let app = crate::Builder::<MockRuntime>::new()
.build(mock_context(noop_assets()))
.unwrap()
.unwrap();
WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into()))
.build()
.unwrap();
app
}
pub(crate) fn mock_invoke_context() -> crate::endpoints::InvokeContext<MockRuntime> {

View File

@@ -643,11 +643,14 @@ impl<'de, R: Runtime> CommandArg<'de, R> for Window<R> {
}
/// The platform webview handle. Accessed with [`Window#method.with_webview`];
#[cfg(all(desktop, feature = "wry"))]
#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "wry"))))]
#[cfg(all(any(desktop, target_os = "android"), feature = "wry"))]
#[cfg_attr(
doc_cfg,
doc(cfg(all(any(desktop, target_os = "android"), feature = "wry")))
)]
pub struct PlatformWebview(tauri_runtime_wry::Webview);
#[cfg(all(desktop, feature = "wry"))]
#[cfg(all(any(desktop, target_os = "android"), feature = "wry"))]
impl PlatformWebview {
/// Returns [`webkit2gtk::WebView`] handle.
#[cfg(any(
@@ -706,6 +709,12 @@ impl PlatformWebview {
pub fn ns_window(&self) -> cocoa::base::id {
self.0.ns_window
}
/// Returns handle for JNI execution.
#[cfg(target_os = "android")]
pub fn jni_handle(&self) -> tauri_runtime_wry::wry::webview::JniHandle {
self.0
}
}
/// APIs specific to the wry runtime.
@@ -749,13 +758,24 @@ impl Window<crate::Wry> {
/// let bg_color: cocoa::base::id = msg_send![class!(NSColor), colorWithDeviceRed:0.5 green:0.2 blue:0.4 alpha:1.];
/// let () = msg_send![webview.ns_window(), setBackgroundColor: bg_color];
/// }
///
/// #[cfg(target_os = "android")]
/// {
/// use jni::objects::JValue;
/// webview.jni_handle().exec(|env, _, webview| {
/// env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap();
/// })
/// }
/// });
/// Ok(())
/// });
/// }
/// ```
#[cfg(desktop)]
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "wry", desktop))))]
#[cfg(any(desktop, target_os = "android"))]
#[cfg_attr(
doc_cfg,
doc(cfg(all(feature = "wry", any(desktop, target_os = "android"))))
)]
pub fn with_webview<F: FnOnce(PlatformWebview) + Send + 'static>(
&self,
f: F,

View File

@@ -100,7 +100,12 @@ fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
#[cfg(target_os = "ios")]
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
root_dir.join(format!("target/debug/bundle/ios/app-updater.app"))
root_dir.join(format!("target/debug/bundle/ios/app-updater.ipa"))
}
#[cfg(target_os = "android")]
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
root_dir.join(format!("target/debug/bundle/android/app-updater.apk"))
}
#[cfg(windows)]

View File

@@ -3,7 +3,7 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false --port 5173",
"dev": "vite --clearScreen false",
"build": "vite build",
"serve": "vite preview",
"tauri": "node ../../tooling/cli/node/tauri.js"
@@ -15,9 +15,10 @@
"devDependencies": {
"@iconify-json/codicon": "^1.1.10",
"@iconify-json/ph": "^1.1.1",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"internal-ip": "^7.0.0",
"svelte": "^3.49.0",
"unocss": "^0.39.3",
"vite": "^2.9.13"
"vite": "^3.0.9"
}
}

View File

@@ -1,3 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# cargo-mobile
.cargo/
/gen

View File

@@ -101,17 +101,11 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
name = "api"
version = "0.1.0"
dependencies = [
"android_logger",
"env_logger 0.9.3",
"jni 0.19.0",
"log",
"mobile-entry-point",
"paste",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-runtime-wry",
"tiny_http",
"window-shadows",
"window-vibrancy",
@@ -149,16 +143,15 @@ dependencies = [
[[package]]
name = "attohttpc"
version = "0.22.0"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
checksum = "b85f766c20e6ae766956f7a2fcc4e0931e79a7e1f48b29132b5d647021114914"
dependencies = [
"flate2",
"http",
"log",
"mime",
"multipart",
"native-tls",
"serde",
"serde_json",
"serde_urlencoded",
@@ -1333,19 +1326,6 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "ico"
version = "0.1.0"
@@ -1490,20 +1470,6 @@ dependencies = [
"system-deps 5.0.0",
]
[[package]]
name = "jni"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni"
version = "0.20.0"
@@ -1620,6 +1586,18 @@ dependencies = [
"safemem",
]
[[package]]
name = "local-ip-address"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d6f43d42d775afa8073bfa6aba569b340a3635efa8c9f7702c9c6ed209692f"
dependencies = [
"libc",
"neli",
"thiserror",
"windows-sys 0.36.1",
]
[[package]]
name = "lock_api"
version = "0.4.9"
@@ -1778,17 +1756,6 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "mobile-entry-point"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bef5a90018326583471cccca10424d7b3e770397b02f03276543cbb9b6a1a6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "multipart"
version = "0.18.0"
@@ -1802,24 +1769,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.6.0"
@@ -1848,6 +1797,16 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "neli"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9053554eb5dcb7e10d9cdab1206965bde870eed5d0d341532ca035e3ba221508"
dependencies = [
"byteorder",
"libc",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
@@ -2014,51 +1973,6 @@ dependencies = [
"windows-sys 0.36.1",
]
[[package]]
name = "openssl"
version = "0.10.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "os_info"
version = "3.5.1"
@@ -2574,13 +2488,11 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
@@ -2588,7 +2500,6 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower-service",
"url",
@@ -2667,16 +2578,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys 0.36.1",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -2689,29 +2590,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.22.0"
@@ -3092,7 +2970,7 @@ dependencies = [
"gtk",
"image",
"instant",
"jni 0.20.0",
"jni",
"lazy_static",
"libappindicator",
"libc",
@@ -3130,6 +3008,7 @@ dependencies = [
name = "tauri"
version = "1.2.0"
dependencies = [
"android_logger",
"anyhow",
"attohttpc",
"base64",
@@ -3139,6 +3018,7 @@ dependencies = [
"dirs-next",
"embed_plist",
"encoding_rs",
"env_logger 0.9.3",
"flate2",
"futures-util",
"glib",
@@ -3149,6 +3029,7 @@ dependencies = [
"ico",
"ignore",
"infer 0.9.0",
"log",
"minisign-verify",
"notify-rust",
"objc",
@@ -3156,6 +3037,7 @@ dependencies = [
"open",
"os_info",
"os_pipe",
"paste",
"percent-encoding",
"png 0.17.7",
"rand 0.8.5",
@@ -3212,6 +3094,7 @@ dependencies = [
"brotli",
"ico",
"json-patch",
"local-ip-address",
"plist",
"png 0.17.7",
"proc-macro2",
@@ -3224,6 +3107,7 @@ dependencies = [
"tauri-utils",
"thiserror",
"time",
"url",
"uuid 1.2.1",
"walkdir",
]
@@ -3463,16 +3347,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.4"
@@ -3676,12 +3550,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.0.11"

View File

@@ -6,6 +6,9 @@ edition = "2021"
rust-version = "1.59"
license = "Apache-2.0 OR MIT"
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { path = "../../../core/tauri-build", features = ["codegen", "isolation"] }
@@ -35,19 +38,6 @@ features = [
window-vibrancy = "0.2"
window-shadows= "0.2"
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
log = "0.4"
tauri-runtime-wry = { path = "../../../core/tauri-runtime-wry/" }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.9.0"
jni = "0.19.0"
paste = "1.0"
[target.'cfg(target_os = "ios")'.dependencies]
mobile-entry-point = "0.1.0"
env_logger = "0.9.0"
[features]
default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]

View File

@@ -1,8 +0,0 @@
[app]
name = "api"
stylized-name = "Tauri API"
domain = "com.tauri"
template-pack = "tauri"
[apple]
development-team = "0"

View File

@@ -9,6 +9,11 @@
mod cmd;
#[cfg(mobile)]
mod mobile;
#[cfg(mobile)]
pub use mobile::*;
use serde::Serialize;
use tauri::{window::WindowBuilder, App, AppHandle, RunEvent, WindowUrl};

View File

@@ -0,0 +1,4 @@
#[tauri::mobile_entry_point]
fn main() {
super::AppBuilder::new().run();
}

View File

@@ -3,7 +3,7 @@
"build": {
"distDir": "../dist",
"devPath": "http://localhost:5173",
"beforeDevCommand": "yarn dev",
"beforeDevCommand": "yarn dev --host",
"beforeBuildCommand": "yarn build"
},
"package": {

View File

@@ -1,22 +1,33 @@
import { defineConfig } from 'vite'
import Unocss from 'unocss/vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { internalIpV4 } from 'internal-ip'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [Unocss(), svelte()],
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
export default defineConfig(async ({ command, mode }) => {
const host = await internalIpV4()
return {
plugins: [Unocss(), svelte()],
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
},
server: {
port: 5173,
strictPort: true,
hmr: {
protocol: 'ws',
host,
port: 5183
},
fs: {
allow: ['.', '../../tooling/api/dist']
}
}
},
server: {
fs: {
allow: ['.', '../../tooling/api/dist']
}
}
})

View File

@@ -15,6 +15,11 @@
resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.5.2.tgz#8c2d931ff927be0ebe740169874a3d4004ab414b"
integrity sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==
"@esbuild/linux-loong64@0.14.54":
version "0.14.54"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
"@iconify-json/codicon@^1.1.10":
version "1.1.10"
resolved "https://registry.yarnpkg.com/@iconify-json/codicon/-/codicon-1.1.10.tgz#22fee909be51afebfbcc6cd57209064b5363f202"
@@ -80,15 +85,15 @@
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@sveltejs/vite-plugin-svelte@^1.0.0-next.49":
version "1.0.0-next.49"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.0-next.49.tgz#44cc00a19c6c23002516b66c5ab90ee66720df57"
integrity sha512-AKh0Ka8EDgidnxWUs8Hh2iZLZovkETkefO99XxZ4sW4WGJ7VFeBx5kH/NIIGlaNHLcrIvK3CK0HkZwC3Cici0A==
"@sveltejs/vite-plugin-svelte@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.1.tgz#7f468f03c933fcdfc60d4773671c73f33b9ef4d6"
integrity sha512-PorCgUounn0VXcpeJu+hOweZODKmGuLHsLomwqSj+p26IwjjGffmYQfVHtiTWq+NqaUuuHWWG7vPge6UFw4Aeg==
dependencies:
"@rollup/pluginutils" "^4.2.1"
debug "^4.3.4"
deepmerge "^4.2.2"
kleur "^4.1.4"
kleur "^4.1.5"
magic-string "^0.26.2"
svelte-hmr "^0.14.12"
@@ -323,6 +328,13 @@ deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
default-gateway@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71"
integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==
dependencies:
execa "^5.0.0"
defu@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.0.0.tgz#b397a6709a2f3202747a3d9daf9446e41ad0c5fc"
@@ -338,138 +350,139 @@ duplexer@^0.1.2:
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
esbuild-android-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz#ef95b42c67bcf4268c869153fa3ad1466c4cea6b"
integrity sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==
esbuild-android-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
esbuild-android-arm64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz#4ebd7ce9fb250b4695faa3ee46fd3b0754ecd9e6"
integrity sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==
esbuild-android-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
esbuild-darwin-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz#e0da6c244f497192f951807f003f6a423ed23188"
integrity sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==
esbuild-darwin-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
esbuild-darwin-arm64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz#cd40fd49a672fca581ed202834239dfe540a9028"
integrity sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==
esbuild-darwin-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
esbuild-freebsd-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz#8da6a14c095b29c01fc8087a16cb7906debc2d67"
integrity sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==
esbuild-freebsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
esbuild-freebsd-arm64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz#ad31f9c92817ff8f33fd253af7ab5122dc1b83f6"
integrity sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==
esbuild-freebsd-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
esbuild-linux-32@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz#de085e4db2e692ea30c71208ccc23fdcf5196c58"
integrity sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==
esbuild-linux-32@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
esbuild-linux-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz#2a9321bbccb01f01b04cebfcfccbabeba3658ba1"
integrity sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==
esbuild-linux-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
esbuild-linux-arm64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz#b9da7b6fc4b0ca7a13363a0c5b7bb927e4bc535a"
integrity sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==
esbuild-linux-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
esbuild-linux-arm@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz#56fec2a09b9561c337059d4af53625142aded853"
integrity sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==
esbuild-linux-arm@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
esbuild-linux-mips64le@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz#9db21561f8f22ed79ef2aedb7bbef082b46cf823"
integrity sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==
esbuild-linux-mips64le@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
esbuild-linux-ppc64le@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz#dc3a3da321222b11e96e50efafec9d2de408198b"
integrity sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==
esbuild-linux-ppc64le@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
esbuild-linux-riscv64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz#9bd6dcd3dca6c0357084ecd06e1d2d4bf105335f"
integrity sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==
esbuild-linux-riscv64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
esbuild-linux-s390x@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz#a458af939b52f2cd32fc561410d441a51f69d41f"
integrity sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==
esbuild-linux-s390x@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
esbuild-netbsd-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz#6388e785d7e7e4420cb01348d7483ab511b16aa8"
integrity sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==
esbuild-netbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
esbuild-openbsd-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz#309af806db561aa886c445344d1aacab850dbdc5"
integrity sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==
esbuild-openbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
esbuild-sunos-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz#3f19612dcdb89ba6c65283a7ff6e16f8afbf8aaa"
integrity sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==
esbuild-sunos-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
esbuild-windows-32@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz#a92d279c8458d5dc319abcfeb30aa49e8f2e6f7f"
integrity sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==
esbuild-windows-32@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
esbuild-windows-64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz#2564c3fcf0c23d701edb71af8c52d3be4cec5f8a"
integrity sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==
esbuild-windows-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
esbuild-windows-arm64@0.14.47:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz#86d9db1a22d83360f726ac5fba41c2f625db6878"
integrity sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==
esbuild-windows-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
esbuild@^0.14.27:
version "0.14.47"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.47.tgz#0d6415f6bd8eb9e73a58f7f9ae04c5276cda0e4d"
integrity sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==
esbuild@^0.14.47:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
optionalDependencies:
esbuild-android-64 "0.14.47"
esbuild-android-arm64 "0.14.47"
esbuild-darwin-64 "0.14.47"
esbuild-darwin-arm64 "0.14.47"
esbuild-freebsd-64 "0.14.47"
esbuild-freebsd-arm64 "0.14.47"
esbuild-linux-32 "0.14.47"
esbuild-linux-64 "0.14.47"
esbuild-linux-arm "0.14.47"
esbuild-linux-arm64 "0.14.47"
esbuild-linux-mips64le "0.14.47"
esbuild-linux-ppc64le "0.14.47"
esbuild-linux-riscv64 "0.14.47"
esbuild-linux-s390x "0.14.47"
esbuild-netbsd-64 "0.14.47"
esbuild-openbsd-64 "0.14.47"
esbuild-sunos-64 "0.14.47"
esbuild-windows-32 "0.14.47"
esbuild-windows-64 "0.14.47"
esbuild-windows-arm64 "0.14.47"
"@esbuild/linux-loong64" "0.14.54"
esbuild-android-64 "0.14.54"
esbuild-android-arm64 "0.14.54"
esbuild-darwin-64 "0.14.54"
esbuild-darwin-arm64 "0.14.54"
esbuild-freebsd-64 "0.14.54"
esbuild-freebsd-arm64 "0.14.54"
esbuild-linux-32 "0.14.54"
esbuild-linux-64 "0.14.54"
esbuild-linux-arm "0.14.54"
esbuild-linux-arm64 "0.14.54"
esbuild-linux-mips64le "0.14.54"
esbuild-linux-ppc64le "0.14.54"
esbuild-linux-riscv64 "0.14.54"
esbuild-linux-s390x "0.14.54"
esbuild-netbsd-64 "0.14.54"
esbuild-openbsd-64 "0.14.54"
esbuild-sunos-64 "0.14.54"
esbuild-windows-32 "0.14.54"
esbuild-windows-64 "0.14.54"
esbuild-windows-arm64 "0.14.54"
estree-walker@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
execa@^5.1.1:
execa@^5.0.0, execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
@@ -558,6 +571,26 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
internal-ip@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-7.0.0.tgz#5b1c6a9d7e188aa73a1b69717daf50c8d8ed774f"
integrity sha512-qE4TeD4brqC45Vq/+VASeMiS1KRyfBkR6HT2sh9pZVVCzSjPkaCEfKFU+dL0PRv7NHJtvoKN2r82G6wTfzorkw==
dependencies:
default-gateway "^6.0.3"
ipaddr.js "^2.0.1"
is-ip "^3.1.0"
p-event "^4.2.0"
ip-regex@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
ipaddr.js@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -584,6 +617,13 @@ is-glob@^4.0.1, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-ip@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
dependencies:
ip-regex "^4.0.0"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -604,10 +644,10 @@ jiti@^1.13.0:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.14.0.tgz#5350fff532a4d891ca4bcd700c47c1f40e6ee326"
integrity sha512-4IwstlaKQc9vCTC+qUXLM1hajy2ImiL9KnLvVYiaHOtS/v3wRjhLlGl121AmgDgx/O43uKmxownJghS5XMya2A==
kleur@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
kleur@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
kolorist@^1.5.1:
version "1.5.1"
@@ -710,6 +750,18 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
p-event@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
dependencies:
p-timeout "^3.1.0"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
p-limit@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
@@ -724,6 +776,13 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
p-timeout@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
dependencies:
p-finally "^1.0.0"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -764,10 +823,10 @@ picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
postcss@^8.4.13:
version "8.4.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
postcss@^8.4.16:
version "8.4.16"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c"
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==
dependencies:
nanoid "^3.3.4"
picocolors "^1.0.0"
@@ -785,7 +844,7 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
resolve@^1.22.0:
resolve@^1.22.1:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
@@ -799,10 +858,10 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rollup@^2.59.0:
version "2.75.7"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.75.7.tgz#221ff11887ae271e37dcc649ba32ce1590aaa0b9"
integrity sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==
"rollup@>=2.75.6 <2.77.0 || ~2.77.0":
version "2.77.3"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12"
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==
optionalDependencies:
fsevents "~2.3.2"
@@ -926,15 +985,15 @@ unocss@^0.39.3:
"@unocss/transformer-variant-group" "0.39.3"
"@unocss/vite" "0.39.3"
vite@^2.9.13:
version "2.9.13"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.13.tgz#859cb5d4c316c0d8c6ec9866045c0f7858ca6abc"
integrity sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==
vite@^3.0.9:
version "3.0.9"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.9.tgz#45fac22c2a5290a970f23d66c1aef56a04be8a30"
integrity sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==
dependencies:
esbuild "^0.14.27"
postcss "^8.4.13"
resolve "^1.22.0"
rollup "^2.59.0"
esbuild "^0.14.47"
postcss "^8.4.16"
resolve "^1.22.1"
rollup ">=2.75.6 <2.77.0 || ~2.77.0"
optionalDependencies:
fsevents "~2.3.2"

View File

@@ -16,6 +16,9 @@ mod updater_bundle;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "macos")]
pub use macos::sign as macos_sign;
pub use self::{
category::AppCategory,
settings::{

View File

@@ -7,4 +7,5 @@ pub mod app;
pub mod dmg;
pub mod icon;
pub mod ios;
/// Code signing and notarization utilities.
pub mod sign;

View File

@@ -14,12 +14,12 @@ use regex::Regex;
const KEYCHAIN_ID: &str = "tauri-build.keychain";
const KEYCHAIN_PWD: &str = "tauri-build";
// Import certificate from ENV variables.
// APPLE_CERTIFICATE is the p12 certificate base64 encoded.
// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt
// Then use the value of the base64 in APPLE_CERTIFICATE env variable.
// You need to set APPLE_CERTIFICATE_PASSWORD to the password you set when you exported your certificate.
// https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate`
/// Import certificate from ENV variables.
/// APPLE_CERTIFICATE is the p12 certificate base64 encoded.
/// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt
/// Then use the value of the base64 in APPLE_CERTIFICATE env variable.
/// You need to set APPLE_CERTIFICATE_PASSWORD to the password you set when you exported your certificate.
/// https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate`
pub fn setup_keychain(
certificate_encoded: OsString,
certificate_password: OsString,
@@ -130,6 +130,7 @@ pub fn setup_keychain(
Ok(())
}
/// Deletes the keychain we setup from the environment variables.
pub fn delete_keychain() {
// delete keychain if needed and skip any error
let _ = Command::new("security")
@@ -138,6 +139,7 @@ pub fn delete_keychain() {
.output_ok();
}
/// Sign the application.
pub fn sign(
path_to_sign: PathBuf,
identity: &str,
@@ -212,6 +214,7 @@ fn try_sign(
Ok(())
}
/// Notarize the application.
pub fn notarize(
app_bundle_path: PathBuf,
auth_args: Vec<String>,
@@ -373,6 +376,7 @@ fn get_notarization_status(
}
}
/// Authorization arguments for the notarization process. Read from environment variables.
pub fn notarize_auth_args() -> crate::Result<Vec<String>> {
match (
std::env::var_os("APPLE_ID"),

845
tooling/cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,12 @@ name = "cargo-tauri"
path = "src/main.rs"
[dependencies]
# cargo-mobile = { path = "../../../cargo-mobile/", default-features = false }
cargo-mobile = { git = "https://github.com/tauri-apps/cargo-mobile", branch = "dev", default-features = false }
textwrap = { version = "0.11.0", features = ["term_size"] }
jsonrpsee = { version = "0.16", features = [ "client", "server" ]}
thiserror = "1"
sublime_fuzzy = "0.7"
clap = { version = "4.0", features = [ "derive" ] }
anyhow = "1.0"
tauri-bundler = { version = "1.1.0", path = "../bundler" }

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Config",
"description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler, enable the app updater, define a system tray, enable APIs via the allowlist and more.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, and `tauri.macos.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml` and `Tauri.macos.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"allowlist\": { \"all\": true }, \"bundle\": {}, \"security\": { \"csp\": null }, \"updater\": { \"active\": false }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
"description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler, enable the app updater, define a system tray, enable APIs via the allowlist and more.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"allowlist\": { \"all\": true }, \"bundle\": {}, \"security\": { \"csp\": null }, \"updater\": { \"active\": false }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
"type": "object",
"properties": {
"$schema": {
@@ -135,6 +135,7 @@
"deb": {
"files": {}
},
"iOS": {},
"icon": [],
"identifier": "",
"macOS": {
@@ -271,6 +272,7 @@
"deb": {
"files": {}
},
"iOS": {},
"icon": [],
"identifier": "",
"macOS": {
@@ -1132,6 +1134,15 @@
"$ref": "#/definitions/WindowsConfig"
}
]
},
"iOS": {
"description": "iOS configuration.",
"default": {},
"allOf": [
{
"$ref": "#/definitions/IosConfig"
}
]
}
},
"additionalProperties": false
@@ -1618,6 +1629,20 @@
},
"additionalProperties": false
},
"IosConfig": {
"description": "General configuration for the iOS target.",
"type": "object",
"properties": {
"developmentTeam": {
"description": "The development team. This value is required for iOS development because code signing is enforced. The `TAURI_APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"AllowlistConfig": {
"description": "Allowlist configuration.",
"type": "object",

View File

@@ -57,110 +57,15 @@ pub struct Options {
}
pub fn command(mut options: Options) -> Result<()> {
let (merge_config, merge_config_path) = if let Some(config) = &options.config {
if config.starts_with('{') {
(Some(config.to_string()), None)
} else {
(
Some(
std::fs::read_to_string(config).with_context(|| "failed to read custom configuration")?,
),
Some(config.clone()),
)
}
} else {
(None, None)
};
options.config = merge_config;
let tauri_path = tauri_dir();
set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
let mut interface = setup(&mut options, false)?;
let config = get_config(options.config.as_deref())?;
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
Some(source) => source,
None => "tauri.conf.json".into(),
};
if config_.tauri.bundle.identifier == "com.tauri.dev" {
error!(
"You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
bundle_identifier_source
);
std::process::exit(1);
}
if config_
.tauri
.bundle
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
error!(
"The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (AZ, az, and 09), hyphens (-), and periods (.).",
config_.tauri.bundle.identifier,
bundle_identifier_source
);
std::process::exit(1);
}
let mut interface = AppInterface::new(config_, options.target.clone())?;
let app_settings = interface.app_settings();
let interface_options = options.clone().into();
if let Some(before_build) = config_.build.before_build_command.clone() {
run_hook(
"beforeBuildCommand",
before_build,
&interface,
options.debug,
)?;
}
if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
if !web_asset_path.exists() {
return Err(anyhow::anyhow!(
"Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
web_asset_path
));
}
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
return Err(anyhow::anyhow!(
"The configured distDir is the `src-tauri` folder.
Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
));
}
let mut out_folders = Vec::new();
for folder in &["node_modules", "src-tauri", "target"] {
if web_asset_path.join(folder).is_dir() {
out_folders.push(folder.to_string());
}
}
if !out_folders.is_empty() {
return Err(anyhow::anyhow!(
"The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
)
);
}
}
if options.runner.is_none() {
options.runner = config_.build.runner.clone();
}
if let Some(list) = options.features.as_mut() {
list.extend(config_.build.features.clone().unwrap_or_default());
}
let bin_path = app_settings.app_binary_path(&interface_options)?;
let out_dir = bin_path.parent().unwrap();
@@ -317,6 +222,114 @@ pub fn command(mut options: Options) -> Result<()> {
Ok(())
}
pub fn setup(options: &mut Options, mobile: bool) -> Result<AppInterface> {
let (merge_config, merge_config_path) = if let Some(config) = &options.config {
if config.starts_with('{') {
(Some(config.to_string()), None)
} else {
(
Some(
std::fs::read_to_string(config).with_context(|| "failed to read custom configuration")?,
),
Some(config.clone()),
)
}
} else {
(None, None)
};
options.config = merge_config;
let tauri_path = tauri_dir();
set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
let config = get_config(options.config.as_deref())?;
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
let interface = AppInterface::new(config_, options.target.clone())?;
let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
Some(source) => source,
None => "tauri.conf.json".into(),
};
if config_.tauri.bundle.identifier == "com.tauri.dev" {
error!(
"You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
bundle_identifier_source
);
std::process::exit(1);
}
if config_
.tauri
.bundle
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
error!(
"The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (AZ, az, and 09), hyphens (-), and periods (.).",
config_.tauri.bundle.identifier,
bundle_identifier_source
);
std::process::exit(1);
}
if let Some(before_build) = config_.build.before_build_command.clone() {
run_hook(
"beforeBuildCommand",
before_build,
&interface,
options.debug,
)?;
}
if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
if !web_asset_path.exists() {
return Err(anyhow::anyhow!(
"Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
web_asset_path
));
}
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
return Err(anyhow::anyhow!(
"The configured distDir is the `src-tauri` folder.
Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
));
}
let mut out_folders = Vec::new();
for folder in &["node_modules", "src-tauri", "target"] {
if web_asset_path.join(folder).is_dir() {
out_folders.push(folder.to_string());
}
}
if !out_folders.is_empty() {
return Err(anyhow::anyhow!(
"The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
)
);
}
}
if options.runner.is_none() {
options.runner = config_.build.runner.clone();
}
options
.features
.get_or_insert(Vec::new())
.extend(config_.build.features.clone().unwrap_or_default());
interface.build_options(&mut options.features, mobile);
Ok(interface)
}
fn run_hook(name: &str, hook: HookCommand, interface: &AppInterface, debug: bool) -> Result<()> {
let (script, script_cwd) = match hook {
HookCommand::Script(s) if s.is_empty() => (None, None),

View File

@@ -8,7 +8,7 @@ use crate::{
command_env,
config::{get as get_config, reload as reload_config, AppUrl, BeforeDevCommand, WindowUrl},
},
interface::{AppInterface, ExitReason, Interface},
interface::{AppInterface, DevProcess, ExitReason, Interface},
CommandExt, Result,
};
use clap::{ArgAction, Parser};
@@ -49,7 +49,7 @@ pub struct Options {
pub features: Option<Vec<String>>,
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,
pub exit_on_panic: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
@@ -74,6 +74,15 @@ pub fn command(options: Options) -> Result<()> {
}
fn command_internal(mut options: Options) -> Result<()> {
let mut interface = setup(&mut options)?;
let exit_on_panic = options.exit_on_panic;
let no_watch = options.no_watch;
interface.dev(options.into(), move |status, reason| {
on_app_exit(status, reason, exit_on_panic, no_watch)
})
}
pub fn setup(options: &mut Options) -> Result<AppInterface> {
let tauri_path = tauri_dir();
options.config = if let Some(config) = &options.config {
Some(if config.starts_with('{') {
@@ -89,7 +98,7 @@ fn command_internal(mut options: Options) -> Result<()> {
let config = get_config(options.config.as_deref())?;
let mut interface = AppInterface::new(
let interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),
options.target.clone(),
)?;
@@ -229,8 +238,8 @@ fn command_internal(mut options: Options) -> Result<()> {
// or better separate the config passed from the cli internally and
// config passed by the user in `--config` into to separate env vars
// and the context merges, the user first, then the internal cli config
if let Some(c) = options.config {
let mut c: tauri_utils::config::Config = serde_json::from_str(&c)?;
if let Some(c) = &options.config {
let mut c: tauri_utils::config::Config = serde_json::from_str(c)?;
c.build.dev_path = dev_path.clone();
options.config = Some(serde_json::to_string(&c).unwrap());
} else {
@@ -295,14 +304,30 @@ fn command_internal(mut options: Options) -> Result<()> {
}
}
let exit_on_panic = options.exit_on_panic;
let no_watch = options.no_watch;
interface.dev(options.into(), move |status, reason| {
on_dev_exit(status, reason, exit_on_panic, no_watch)
})
Ok(interface)
}
fn on_dev_exit(status: ExitStatus, reason: ExitReason, exit_on_panic: bool, no_watch: bool) {
pub fn wait_dev_process<
C: DevProcess + Send + 'static,
F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static,
>(
child: C,
on_exit: F,
) {
std::thread::spawn(move || {
let status = child.wait().expect("failed to wait on app");
on_exit(
status,
if child.manually_killed_process() {
ExitReason::TriggeredKill
} else {
ExitReason::NormalExit
},
);
});
}
pub fn on_app_exit(status: ExitStatus, reason: ExitReason, exit_on_panic: bool, no_watch: bool) {
if no_watch
|| (!matches!(reason, ExitReason::TriggeredKill)
&& (exit_on_panic || matches!(reason, ExitReason::NormalExit)))
@@ -332,7 +357,7 @@ fn check_for_updates() -> Result<()> {
Ok(())
}
fn kill_before_dev_process() {
pub fn kill_before_dev_process() {
if let Some(child) = BEFORE_DEV.get() {
let child = child.lock().unwrap();
KILL_BEFORE_DEV_FLAG

View File

@@ -0,0 +1,363 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// taken from https://github.com/rust-lang/cargo/blob/b0c9586f4cbf426914df47c65de38ea323772c74/src/cargo/util/flock.rs
#![allow(dead_code)]
use std::fs::{create_dir_all, File, OpenOptions};
use std::io;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::Result;
use anyhow::Context as _;
use sys::*;
#[derive(Debug)]
pub struct FileLock {
f: Option<File>,
path: PathBuf,
state: State,
}
#[derive(PartialEq, Debug)]
enum State {
Unlocked,
Shared,
Exclusive,
}
impl FileLock {
/// Returns the underlying file handle of this lock.
pub fn file(&self) -> &File {
self.f.as_ref().unwrap()
}
/// Returns the underlying path that this lock points to.
///
/// Note that special care must be taken to ensure that the path is not
/// referenced outside the lifetime of this lock.
pub fn path(&self) -> &Path {
assert_ne!(self.state, State::Unlocked);
&self.path
}
/// Returns the parent path containing this file
pub fn parent(&self) -> &Path {
assert_ne!(self.state, State::Unlocked);
self.path.parent().unwrap()
}
}
impl Read for FileLock {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file().read(buf)
}
}
impl Seek for FileLock {
fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
self.file().seek(to)
}
}
impl Write for FileLock {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file().flush()
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if self.state != State::Unlocked {
if let Some(f) = self.f.take() {
let _ = unlock(&f);
}
}
}
}
/// Opens exclusive access to a file, returning the locked version of a
/// file.
///
/// This function will create a file at `path` if it doesn't already exist
/// (including intermediate directories), and then it will acquire an
/// exclusive lock on `path`. If the process must block waiting for the
/// lock, the `msg` is logged.
///
/// The returned file can be accessed to look at the path and also has
/// read/write access to the underlying file.
pub fn open_rw<P>(path: P, msg: &str) -> Result<FileLock>
where
P: AsRef<Path>,
{
open(
path.as_ref(),
OpenOptions::new().read(true).write(true).create(true),
State::Exclusive,
msg,
)
}
/// Opens shared access to a file, returning the locked version of a file.
///
/// This function will fail if `path` doesn't already exist, but if it does
/// then it will acquire a shared lock on `path`. If the process must block
/// waiting for the lock, the `msg` is logged.
///
/// The returned file can be accessed to look at the path and also has read
/// access to the underlying file. Any writes to the file will return an
/// error.
pub fn open_ro<P>(path: P, msg: &str) -> Result<FileLock>
where
P: AsRef<Path>,
{
open(
path.as_ref(),
OpenOptions::new().read(true),
State::Shared,
msg,
)
}
fn open(path: &Path, opts: &OpenOptions, state: State, msg: &str) -> Result<FileLock> {
// If we want an exclusive lock then if we fail because of NotFound it's
// likely because an intermediate directory didn't exist, so try to
// create the directory and then continue.
let f = opts
.open(path)
.or_else(|e| {
if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
create_dir_all(path.parent().unwrap())?;
Ok(opts.open(path)?)
} else {
Err(anyhow::Error::from(e))
}
})
.with_context(|| format!("failed to open: {}", path.display()))?;
match state {
State::Exclusive => {
acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
lock_exclusive(&f)
})?;
}
State::Shared => {
acquire(msg, path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
}
State::Unlocked => {}
}
Ok(FileLock {
f: Some(f),
path: path.to_path_buf(),
state,
})
}
/// Acquires a lock on a file in a "nice" manner.
///
/// Almost all long-running blocking actions in Cargo have a status message
/// associated with them as we're not sure how long they'll take. Whenever a
/// conflicted file lock happens, this is the case (we're not sure when the lock
/// will be released).
///
/// This function will acquire the lock on a `path`, printing out a nice message
/// to the console if we have to wait for it. It will first attempt to use `try`
/// to acquire a lock on the crate, and in the case of contention it will emit a
/// status message based on `msg` to `config`'s shell, and then use `block` to
/// block waiting to acquire a lock.
///
/// Returns an error if the lock could not be acquired or if any error other
/// than a contention error happens.
fn acquire(
msg: &str,
path: &Path,
lock_try: &dyn Fn() -> io::Result<()>,
lock_block: &dyn Fn() -> io::Result<()>,
) -> Result<()> {
// File locking on Unix is currently implemented via `flock`, which is known
// to be broken on NFS. We could in theory just ignore errors that happen on
// NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
// forever**, even if the "non-blocking" flag is passed!
//
// As a result, we just skip all file locks entirely on NFS mounts. That
// should avoid calling any `flock` functions at all, and it wouldn't work
// there anyway.
//
// [1]: https://github.com/rust-lang/cargo/issues/2615
if is_on_nfs_mount(path) {
return Ok(());
}
match lock_try() {
Ok(()) => return Ok(()),
// In addition to ignoring NFS which is commonly not working we also
// just ignore locking on filesystems that look like they don't
// implement file locking.
Err(e) if error_unsupported(&e) => return Ok(()),
Err(e) => {
if !error_contended(&e) {
let e = anyhow::Error::from(e);
let cx = format!("failed to lock file: {}", path.display());
return Err(e.context(cx));
}
}
}
let msg = format!("waiting for file lock on {}", msg);
log::info!(action = "Blocking"; "{}", &msg);
lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
return Ok(());
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
fn is_on_nfs_mount(path: &Path) -> bool {
use std::ffi::CString;
use std::mem;
use std::os::unix::prelude::*;
let path = match CString::new(path.as_os_str().as_bytes()) {
Ok(path) => path,
Err(_) => return false,
};
unsafe {
let mut buf: libc::statfs = mem::zeroed();
let r = libc::statfs(path.as_ptr(), &mut buf);
r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
}
}
#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
fn is_on_nfs_mount(_path: &Path) -> bool {
false
}
}
#[cfg(unix)]
mod sys {
use std::fs::File;
use std::io::{Error, Result};
use std::os::unix::io::AsRawFd;
pub(super) fn lock_shared(file: &File) -> Result<()> {
flock(file, libc::LOCK_SH)
}
pub(super) fn lock_exclusive(file: &File) -> Result<()> {
flock(file, libc::LOCK_EX)
}
pub(super) fn try_lock_shared(file: &File) -> Result<()> {
flock(file, libc::LOCK_SH | libc::LOCK_NB)
}
pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
flock(file, libc::LOCK_EX | libc::LOCK_NB)
}
pub(super) fn unlock(file: &File) -> Result<()> {
flock(file, libc::LOCK_UN)
}
pub(super) fn error_contended(err: &Error) -> bool {
err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
}
pub(super) fn error_unsupported(err: &Error) -> bool {
match err.raw_os_error() {
// Unfortunately, depending on the target, these may or may not be the same.
// For targets in which they are the same, the duplicate pattern causes a warning.
#[allow(unreachable_patterns)]
Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
Some(libc::ENOSYS) => true,
_ => false,
}
}
#[cfg(not(target_os = "solaris"))]
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
if ret < 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(target_os = "solaris")]
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
// Solaris lacks flock(), so simply succeed with a no-op
Ok(())
}
}
#[cfg(windows)]
mod sys {
use std::fs::File;
use std::io::{Error, Result};
use std::mem;
use std::os::windows::io::AsRawHandle;
use winapi::shared::minwindef::DWORD;
use winapi::shared::winerror::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
use winapi::um::fileapi::{LockFileEx, UnlockFile};
use winapi::um::minwinbase::{LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY};
pub(super) fn lock_shared(file: &File) -> Result<()> {
lock_file(file, 0)
}
pub(super) fn lock_exclusive(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
}
pub(super) fn try_lock_shared(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
}
pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
}
pub(super) fn error_contended(err: &Error) -> bool {
err
.raw_os_error()
.map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
}
pub(super) fn error_unsupported(err: &Error) -> bool {
err
.raw_os_error()
.map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
}
pub(super) fn unlock(file: &File) -> Result<()> {
unsafe {
let ret = UnlockFile(file.as_raw_handle(), 0, 0, !0, !0);
if ret == 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
}
fn lock_file(file: &File, flags: DWORD) -> Result<()> {
unsafe {
let mut overlapped = mem::zeroed();
let ret = LockFileEx(file.as_raw_handle(), flags, 0, !0, !0, &mut overlapped);
if ret == 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
}
}

View File

@@ -4,6 +4,7 @@
pub mod app_paths;
pub mod config;
pub mod flock;
pub mod framework;
pub mod template;
pub mod updater_signature;

View File

@@ -3,22 +3,74 @@
// SPDX-License-Identifier: MIT
use std::{
collections::BTreeMap,
fs::{create_dir_all, File},
io::Write,
path::Path,
path::{Path, PathBuf},
};
use handlebars::Handlebars;
use handlebars::{to_json, Handlebars};
use include_dir::Dir;
use serde::Serialize;
use serde_json::value::{Map, Value as JsonValue};
pub fn render<P: AsRef<Path>>(
/// Map of template variable names and values.
#[derive(Clone, Debug)]
#[repr(transparent)]
pub struct JsonMap(Map<String, JsonValue>);
impl Default for JsonMap {
fn default() -> Self {
Self(Map::new())
}
}
impl JsonMap {
pub fn insert(&mut self, name: &str, value: impl Serialize) {
self.0.insert(name.to_owned(), to_json(value));
}
pub fn inner(&self) -> &Map<String, JsonValue> {
&self.0
}
}
pub fn render<P: AsRef<Path>, D: Serialize>(
handlebars: &Handlebars<'_>,
data: &BTreeMap<&str, serde_json::Value>,
data: &D,
dir: &Dir<'_>,
out_dir: P,
) -> crate::Result<()> {
create_dir_all(out_dir.as_ref().join(dir.path()))?;
let out_dir = out_dir.as_ref();
let mut created_dirs = Vec::new();
render_with_generator(
handlebars,
data,
dir,
out_dir,
&mut |file_path: &PathBuf| {
let path = out_dir.join(file_path);
let parent = path.parent().unwrap().to_path_buf();
if !created_dirs.contains(&parent) {
create_dir_all(&parent)?;
created_dirs.push(parent);
}
File::create(path).map(Some)
},
)
}
pub fn render_with_generator<
P: AsRef<Path>,
D: Serialize,
F: FnMut(&PathBuf) -> std::io::Result<Option<File>>,
>(
handlebars: &Handlebars<'_>,
data: &D,
dir: &Dir<'_>,
out_dir: P,
out_file_generator: &mut F,
) -> crate::Result<()> {
let out_dir = out_dir.as_ref();
for file in dir.files() {
let mut file_path = file.path().to_path_buf();
// cargo for some reason ignores the /templates folder packaging when it has a Cargo.toml file inside
@@ -28,17 +80,18 @@ pub fn render<P: AsRef<Path>>(
file_path.set_extension("toml");
}
}
let mut output_file = File::create(out_dir.as_ref().join(file_path))?;
if let Some(utf8) = file.contents_utf8() {
handlebars
.render_template_to_write(utf8, &data, &mut output_file)
.expect("Failed to render template");
} else {
output_file.write_all(file.contents())?;
if let Some(mut output_file) = out_file_generator(&file_path)? {
if let Some(utf8) = file.contents_utf8() {
handlebars
.render_template_to_write(utf8, &data, &mut output_file)
.expect("Failed to render template");
} else {
output_file.write_all(file.contents())?;
}
}
}
for dir in dir.dirs() {
render(handlebars, data, dir, out_dir.as_ref())?;
render_with_generator(handlebars, data, dir, out_dir, out_file_generator)?;
}
Ok(())
}

View File

@@ -636,7 +636,7 @@ pub fn command(_options: Options) -> Result<()> {
InfoBlock::new("MSVC", "").display();
for i in build_tools {
indent(6);
println!("{}", format!("{} {}", "-".cyan(), i));
println!("{} {}", "-".cyan(), i);
}
}
}
@@ -885,6 +885,29 @@ pub fn command(_options: Options) -> Result<()> {
}
}
#[cfg(target_os = "macos")]
if tauri_dir.is_some() {
let p = tauri_dir.as_ref().unwrap();
if p.join("gen/apple").exists() {
let teams = cargo_mobile::apple::teams::find_development_teams().unwrap_or_default();
Section("iOS").display();
InfoBlock::new(
"Teams",
if teams.is_empty() {
"None".red().to_string()
} else {
teams
.iter()
.map(|t| format!("{} (ID: {})", t.name, t.id))
.collect::<Vec<String>>()
.join(", ")
.to_string()
},
)
.display();
}
}
Ok(())
}

View File

@@ -13,7 +13,15 @@ use std::{
use crate::helpers::config::Config;
use tauri_bundler::bundle::{PackageType, Settings, SettingsBuilder};
pub use rust::{Options, Rust as AppInterface};
pub use rust::{manifest, MobileOptions, Options, Rust as AppInterface};
pub trait DevProcess {
fn kill(&self) -> std::io::Result<()>;
fn try_wait(&self) -> std::io::Result<Option<ExitStatus>>;
fn wait(&self) -> std::io::Result<ExitStatus>;
fn manually_killed_process(&self) -> bool;
fn is_building_app(&self) -> bool;
}
pub trait AppSettings {
fn get_package_settings(&self) -> tauri_bundler::PackageSettings;
@@ -85,4 +93,9 @@ pub trait Interface: Sized {
options: Options,
on_exit: F,
) -> crate::Result<()>;
fn mobile_dev<R: Fn(MobileOptions) -> crate::Result<Box<dyn DevProcess>>>(
&mut self,
options: MobileOptions,
runner: R,
) -> crate::Result<()>;
}

View File

@@ -10,11 +10,7 @@ use std::{
path::{Path, PathBuf},
process::{Command, ExitStatus},
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
mpsc::sync_channel,
Arc, Mutex,
},
sync::{mpsc::sync_channel, Arc, Mutex},
time::{Duration, Instant},
};
@@ -26,14 +22,13 @@ use log::{debug, error, info};
use notify::RecursiveMode;
use notify_debouncer_mini::new_debouncer;
use serde::Deserialize;
use shared_child::SharedChild;
use tauri_bundler::{
AppCategory, BundleBinary, BundleSettings, DebianSettings, MacOsSettings, PackageSettings,
UpdaterSettings, WindowsSettings,
};
use tauri_utils::config::parse::is_configuration_file;
use super::{AppSettings, ExitReason, Interface};
use super::{AppSettings, DevProcess, ExitReason, Interface};
use crate::helpers::{
app_paths::{app_dir, tauri_dir},
config::{reload as reload_config, wix_settings, Config},
@@ -41,11 +36,11 @@ use crate::helpers::{
mod cargo_config;
mod desktop;
mod manifest;
pub mod manifest;
use cargo_config::Config as CargoConfig;
use manifest::{rewrite_manifest, Manifest};
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Options {
pub runner: Option<String>,
pub debug: bool,
@@ -84,30 +79,13 @@ impl From<crate::dev::Options> for Options {
}
}
pub struct DevChild {
manually_killed_app: Arc<AtomicBool>,
build_child: Arc<SharedChild>,
app_child: Arc<Mutex<Option<Arc<SharedChild>>>>,
}
impl DevChild {
fn kill(&self) -> std::io::Result<()> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.kill()?;
} else {
self.build_child.kill()?;
}
self.manually_killed_app.store(true, Ordering::Relaxed);
Ok(())
}
fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.try_wait()
} else {
self.build_child.try_wait()
}
}
#[derive(Debug, Clone)]
pub struct MobileOptions {
pub debug: bool,
pub features: Option<Vec<String>>,
pub args: Vec<String>,
pub config: Option<String>,
pub no_watch: bool,
}
#[derive(Debug)]
@@ -156,8 +134,10 @@ impl Interface for Rust {
std::env::set_var("MACOSX_DEPLOYMENT_TARGET", minimum_system_version);
}
let app_settings = RustAppSettings::new(config, manifest, target)?;
Ok(Self {
app_settings: RustAppSettings::new(config, manifest, target)?,
app_settings,
config_features: config.build.features.clone().unwrap_or_default(),
product_name: config.package.product_name.clone(),
available_targets: None,
@@ -168,11 +148,7 @@ impl Interface for Rust {
&self.app_settings
}
fn build(&mut self, mut options: Options) -> crate::Result<()> {
options
.features
.get_or_insert(Vec::new())
.push("custom-protocol".into());
fn build(&mut self, options: Options) -> crate::Result<()> {
desktop::build(
options,
&self.app_settings,
@@ -185,28 +161,62 @@ impl Interface for Rust {
fn dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
&mut self,
options: Options,
mut options: Options,
on_exit: F,
) -> crate::Result<()> {
let on_exit = Arc::new(on_exit);
let on_exit_ = on_exit.clone();
let mut run_args = Vec::new();
dev_options(
false,
&mut options.args,
&mut run_args,
&mut options.features,
&self.app_settings,
);
if options.no_watch {
let (tx, rx) = sync_channel(1);
self.run_dev(options, move |status, reason| {
self.run_dev(options, run_args, move |status, reason| {
tx.send(()).unwrap();
on_exit_(status, reason)
on_exit(status, reason)
})?;
rx.recv().unwrap();
Ok(())
} else {
let child = self.run_dev(options.clone(), move |status, reason| {
on_exit_(status, reason)
})?;
let config = options.config.clone();
let run = Arc::new(|rust: &mut Rust| {
let on_exit = on_exit.clone();
rust.run_dev(options.clone(), run_args.clone(), move |status, reason| {
on_exit(status, reason)
})
});
self.run_dev_watcher(config, run)
}
}
self.run_dev_watcher(child, options, on_exit)
fn mobile_dev<R: Fn(MobileOptions) -> crate::Result<Box<dyn DevProcess>>>(
&mut self,
mut options: MobileOptions,
runner: R,
) -> crate::Result<()> {
let mut run_args = Vec::new();
dev_options(
true,
&mut options.args,
&mut run_args,
&mut options.features,
&self.app_settings,
);
if options.no_watch {
runner(options)?;
Ok(())
} else {
let config = options.config.clone();
let run = Arc::new(|_rust: &mut Rust| runner(options.clone()));
self.run_dev_watcher(config, run)
}
}
@@ -334,51 +344,89 @@ fn lookup<F: FnMut(FileType, PathBuf)>(dir: &Path, mut f: F) {
}
}
fn shared_options(
mobile: bool,
features: &mut Option<Vec<String>>,
app_settings: &RustAppSettings,
) {
if mobile {
let all_features = app_settings
.manifest
.all_enabled_features(if let Some(f) = features { f } else { &[] });
if all_features.contains(&"tauri/default-tls".into())
|| all_features.contains(&"tauri/reqwest-default-tls".into())
{
if all_features.contains(&"tauri/reqwest-client".into()) {
features
.get_or_insert(Vec::new())
.push("tauri/reqwest-native-tls-vendored".into());
} else {
features
.get_or_insert(Vec::new())
.push("tauri/native-tls-vendored".into());
}
}
}
}
fn dev_options(
mobile: bool,
args: &mut Vec<String>,
run_args: &mut Vec<String>,
features: &mut Option<Vec<String>>,
app_settings: &RustAppSettings,
) {
let mut dev_args = Vec::new();
let mut reached_run_args = false;
for arg in args.clone() {
if reached_run_args {
run_args.push(arg);
} else if arg == "--" {
reached_run_args = true;
} else {
dev_args.push(arg);
}
}
*args = dev_args;
shared_options(mobile, features, app_settings);
if !args.contains(&"--no-default-features".into()) {
let manifest_features = app_settings.manifest.features();
let enable_features: Vec<String> = manifest_features
.get("default")
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|feature| {
if let Some(manifest_feature) = manifest_features.get(feature) {
!manifest_feature.contains(&"tauri/custom-protocol".into())
} else {
feature != "tauri/custom-protocol"
}
})
.collect();
args.push("--no-default-features".into());
if !enable_features.is_empty() {
features.get_or_insert(Vec::new()).extend(enable_features);
}
}
}
impl Rust {
pub fn build_options(&self, features: &mut Option<Vec<String>>, mobile: bool) {
features
.get_or_insert(Vec::new())
.push("custom-protocol".into());
shared_options(mobile, features, &self.app_settings);
}
fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
&mut self,
mut options: Options,
options: Options,
run_args: Vec<String>,
on_exit: F,
) -> crate::Result<DevChild> {
let mut args = Vec::new();
let mut run_args = Vec::new();
let mut reached_run_args = false;
for arg in options.args.clone() {
if reached_run_args {
run_args.push(arg);
} else if arg == "--" {
reached_run_args = true;
} else {
args.push(arg);
}
}
if !args.contains(&"--no-default-features".into()) {
let manifest_features = self.app_settings.manifest.features();
let enable_features: Vec<String> = manifest_features
.get("default")
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|feature| {
if let Some(manifest_feature) = manifest_features.get(feature) {
!manifest_feature.contains(&"tauri/custom-protocol".into())
} else {
feature != "tauri/custom-protocol"
}
})
.collect();
args.push("--no-default-features".into());
if !enable_features.is_empty() {
options
.features
.get_or_insert(Vec::new())
.extend(enable_features);
}
}
options.args = args;
) -> crate::Result<Box<dyn DevProcess>> {
desktop::run_dev(
options,
run_args,
@@ -388,14 +436,16 @@ impl Rust {
self.product_name.clone(),
on_exit,
)
.map(|c| Box::new(c) as Box<dyn DevProcess>)
}
fn run_dev_watcher<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
fn run_dev_watcher<F: Fn(&mut Rust) -> crate::Result<Box<dyn DevProcess>>>(
&mut self,
child: DevChild,
options: Options,
on_exit: Arc<F>,
config: Option<String>,
run: Arc<F>,
) -> crate::Result<()> {
let child = run(self)?;
let process = Arc::new(Mutex::new(child));
let (tx, rx) = sync_channel(1);
let app_path = app_dir();
@@ -452,12 +502,11 @@ impl Rust {
loop {
if let Ok(events) = rx.recv() {
for event in events {
let on_exit = on_exit.clone();
let event_path = event.path;
if !ignore_matcher.is_ignore(&event_path, event_path.is_dir()) {
if is_configuration_file(&event_path) {
match reload_config(options.config.as_deref()) {
match reload_config(config.as_deref()) {
Ok(config) => {
info!("Tauri configuration changed. Rewriting manifest...");
self.app_settings.manifest =
@@ -465,8 +514,7 @@ impl Rust {
}
Err(err) => {
let p = process.lock().unwrap();
let is_building_app = p.app_child.lock().unwrap().is_none();
if is_building_app {
if p.is_building_app() {
p.kill().with_context(|| "failed to kill app process")?;
}
error!("{}", err);
@@ -491,9 +539,7 @@ impl Rust {
break;
}
}
*p = self.run_dev(options.clone(), move |status, reason| {
on_exit(status, reason)
})?;
*p = run(self)?;
}
}
}

View File

@@ -1,4 +1,4 @@
use super::{AppSettings, DevChild, ExitReason, Options, RustAppSettings, Target};
use super::{AppSettings, DevProcess, ExitReason, Options, RustAppSettings, Target};
use crate::CommandExt;
use anyhow::Context;
@@ -16,6 +16,52 @@ use std::{
},
};
pub struct DevChild {
manually_killed_app: Arc<AtomicBool>,
build_child: Option<Arc<SharedChild>>,
app_child: Arc<Mutex<Option<Arc<SharedChild>>>>,
}
impl DevProcess for DevChild {
fn kill(&self) -> std::io::Result<()> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.kill()?;
} else if let Some(child) = &self.build_child {
child.kill()?;
}
self.manually_killed_app.store(true, Ordering::Relaxed);
Ok(())
}
fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.try_wait()
} else if let Some(child) = &self.build_child {
child.try_wait()
} else {
unreachable!()
}
}
fn wait(&self) -> std::io::Result<ExitStatus> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.wait()
} else if let Some(child) = &self.build_child {
child.wait()
} else {
unreachable!()
}
}
fn manually_killed_process(&self) -> bool {
self.manually_killed_app.load(Ordering::Relaxed)
}
fn is_building_app(&self) -> bool {
self.app_child.lock().unwrap().is_none()
}
}
pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
options: Options,
run_args: Vec<String>,
@@ -24,7 +70,7 @@ pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
app_settings: &RustAppSettings,
product_name: Option<String>,
on_exit: F,
) -> crate::Result<DevChild> {
) -> crate::Result<impl DevProcess> {
let bin_path = app_settings.app_binary_path(&options)?;
let manually_killed_app = Arc::new(AtomicBool::default());
@@ -45,18 +91,14 @@ pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
app.stderr(os_pipe::dup_stderr().unwrap());
app.args(run_args);
let app_child = Arc::new(SharedChild::spawn(&mut app).unwrap());
let app_child_t = app_child.clone();
std::thread::spawn(move || {
let status = app_child_t.wait().expect("failed to wait on app");
on_exit(
status,
if manually_killed_app_.load(Ordering::Relaxed) {
ExitReason::TriggeredKill
} else {
ExitReason::NormalExit
},
);
});
crate::dev::wait_dev_process(
DevChild {
manually_killed_app: manually_killed_app_,
build_child: None,
app_child: Arc::new(Mutex::new(Some(app_child.clone()))),
},
on_exit,
);
app_child_.lock().unwrap().replace(app_child);
} else {
@@ -74,7 +116,7 @@ pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
Ok(DevChild {
manually_killed_app,
build_child,
build_child: Some(build_child),
app_child,
})
}
@@ -341,7 +383,7 @@ fn rename_app(bin_path: &Path, product_name: Option<&str>) -> crate::Result<Path
let product_path = bin_path
.parent()
.unwrap()
.join(&product_name)
.join(product_name)
.with_extension(bin_path.extension().unwrap_or_default());
rename(bin_path, &product_path).with_context(|| {

View File

@@ -82,7 +82,7 @@ fn get_enabled_features(list: &HashMap<String, Vec<String>>, feature: &str) -> V
f
}
fn read_manifest(manifest_path: &Path) -> crate::Result<Document> {
pub fn read_manifest(manifest_path: &Path) -> crate::Result<Document> {
let mut manifest_str = String::new();
let mut manifest_file = File::open(manifest_path)

View File

@@ -11,10 +11,11 @@ mod icon;
mod info;
mod init;
mod interface;
mod mobile;
mod plugin;
mod signer;
use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand};
use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use env_logger::fmt::Color;
use env_logger::Builder;
use log::{debug, log_enabled, Level};
@@ -23,9 +24,33 @@ use std::io::{BufReader, Write};
use std::process::{exit, Command, ExitStatus, Output, Stdio};
use std::{
ffi::OsString,
fmt::Display,
sync::{Arc, Mutex},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum RunMode {
Desktop,
#[cfg(target_os = "macos")]
Ios,
Android,
}
impl Display for RunMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Desktop => "desktop",
#[cfg(target_os = "macos")]
Self::Ios => "iOS",
Self::Android => "android",
}
)
}
}
#[derive(Deserialize)]
pub struct VersionMetadata {
tauri: String,
@@ -68,6 +93,9 @@ enum Commands {
Init(init::Options),
Plugin(plugin::Cli),
Signer(signer::Cli),
Android(mobile::android::Cli),
#[cfg(target_os = "macos")]
Ios(mobile::ios::Cli),
}
fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
@@ -167,6 +195,9 @@ where
Commands::Init(options) => init::command(options)?,
Commands::Plugin(cli) => plugin::command(cli)?,
Commands::Signer(cli) => signer::command(cli)?,
Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
#[cfg(target_os = "macos")]
Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
}
Ok(())

View File

@@ -0,0 +1,274 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use cargo_mobile::{
android::{
adb,
config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig},
device::Device,
emulator,
env::Env,
target::Target,
},
config::app::App,
opts::NoiseLevel,
os,
util::prompt,
};
use clap::{Parser, Subcommand};
use std::{
env::set_var,
thread::{sleep, spawn},
time::Duration,
};
use sublime_fuzzy::best_match;
use super::{
ensure_init, get_app,
init::{command as init_command, init_dot_cargo},
log_finished, read_options, CliOptions, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE,
};
use crate::{
helpers::config::{get as get_tauri_config, Config as TauriConfig},
Result,
};
mod android_studio_script;
mod build;
mod dev;
mod open;
pub(crate) mod project;
#[derive(Parser)]
#[clap(
author,
version,
about = "Android commands",
subcommand_required(true),
arg_required_else_help(true)
)]
pub struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Debug, Parser)]
#[clap(about = "Initializes a Tauri Android project")]
pub struct InitOptions {
/// Skip prompting for values
#[clap(long)]
ci: bool,
}
#[derive(Subcommand)]
enum Commands {
Init(InitOptions),
/// Open project in Android Studio
Open,
Dev(dev::Options),
Build(build::Options),
#[clap(hide(true))]
AndroidStudioScript(android_studio_script::Options),
}
pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
let noise_level = NoiseLevel::from_occurrences(verbosity as u64);
match cli.command {
Commands::Init(options) => init_command(MobileTarget::Android, options.ci, false)?,
Commands::Open => open::command()?,
Commands::Dev(options) => dev::command(options, noise_level)?,
Commands::Build(options) => build::command(options, noise_level)?,
Commands::AndroidStudioScript(options) => android_studio_script::command(options)?,
}
Ok(())
}
pub fn get_config(
app: Option<App>,
config: &TauriConfig,
cli_options: &CliOptions,
) -> (App, AndroidConfig, AndroidMetadata) {
let app = app.unwrap_or_else(|| get_app(config));
let android_options = cli_options.clone();
let raw = RawAndroidConfig {
features: android_options.features.clone(),
logcat_filter_specs: vec!["RustStdoutStderr".into()],
..Default::default()
};
let config = AndroidConfig::from_raw(app.clone(), Some(raw)).unwrap();
let metadata = AndroidMetadata {
supported: true,
cargo_args: Some(android_options.args),
features: android_options.features,
..Default::default()
};
set_var("WRY_ANDROID_REVERSED_DOMAIN", app.reverse_domain());
set_var("WRY_ANDROID_APP_NAME_SNAKE_CASE", app.name());
set_var(
"WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
config
.project_dir()
.join("app/src/main")
.join(format!(
"java/{}/{}",
app.reverse_domain().replace('.', "/"),
app.name()
))
.join("generated"),
);
(app, config, metadata)
}
fn with_config<T>(
cli_options: Option<CliOptions>,
f: impl FnOnce(&App, &AndroidConfig, &AndroidMetadata, CliOptions) -> Result<T>,
) -> Result<T> {
let (app, config, metadata, cli_options) = {
let tauri_config = get_tauri_config(None)?;
let tauri_config_guard = tauri_config.lock().unwrap();
let tauri_config_ = tauri_config_guard.as_ref().unwrap();
let cli_options = cli_options.unwrap_or_else(read_options);
let (app, config, metadata) = get_config(None, tauri_config_, &cli_options);
(app, config, metadata, cli_options)
};
f(&app, &config, &metadata, cli_options)
}
fn env() -> Result<Env> {
let env = super::env()?;
cargo_mobile::android::env::Env::from_env(env).map_err(Into::into)
}
fn delete_codegen_vars() {
for (k, _) in std::env::vars() {
if k.starts_with("WRY_") && (k.ends_with("CLASS_EXTENSION") || k.ends_with("CLASS_INIT")) {
std::env::remove_var(k);
}
}
}
fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
let device_list = adb::device_list(env)
.map_err(|cause| anyhow::anyhow!("Failed to detect connected Android devices: {cause}"))?;
if !device_list.is_empty() {
let device = if let Some(t) = target {
let (device, score) = device_list
.into_iter()
.rev()
.map(|d| {
let score = best_match(t, d.name()).map_or(0, |m| m.score());
(d, score)
})
.max_by_key(|(_, score)| *score)
// we already checked the list is not empty
.unwrap();
if score > MIN_DEVICE_MATCH_SCORE {
device
} else {
anyhow::bail!("Could not find an Android device matching {t}")
}
} else if device_list.len() > 1 {
let index = prompt::list(
concat!("Detected ", "Android", " devices"),
device_list.iter(),
"device",
None,
"Device",
)
.map_err(|cause| anyhow::anyhow!("Failed to prompt for Android device: {cause}"))?;
device_list.into_iter().nth(index).unwrap()
} else {
device_list.into_iter().next().unwrap()
};
println!(
"Detected connected device: {} with target {:?}",
device,
device.target().triple,
);
Ok(device)
} else {
Err(anyhow::anyhow!("No connected Android devices detected"))
}
}
fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<emulator::Emulator> {
let emulator_list = emulator::avd_list(env).unwrap_or_default();
if !emulator_list.is_empty() {
let emulator = if let Some(t) = target {
let (device, score) = emulator_list
.into_iter()
.rev()
.map(|d| {
let score = best_match(t, d.name()).map_or(0, |m| m.score());
(d, score)
})
.max_by_key(|(_, score)| *score)
// we already checked the list is not empty
.unwrap();
if score > MIN_DEVICE_MATCH_SCORE {
device
} else {
anyhow::bail!("Could not find an Android Emulator matching {t}")
}
} else if emulator_list.len() > 1 {
let index = prompt::list(
concat!("Detected ", "Android", " emulators"),
emulator_list.iter(),
"emulator",
None,
"Emulator",
)
.map_err(|cause| anyhow::anyhow!("Failed to prompt for Android Emulator device: {cause}"))?;
emulator_list.into_iter().nth(index).unwrap()
} else {
emulator_list.into_iter().next().unwrap()
};
let handle = emulator.start(env)?;
spawn(move || {
let _ = handle.wait();
});
Ok(emulator)
} else {
Err(anyhow::anyhow!("No available Android Emulator detected"))
}
}
fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
if let Ok(device) = adb_device_prompt(env, target) {
Ok(device)
} else {
let emulator = emulator_prompt(env, target)?;
let handle = emulator.start(env)?;
spawn(move || {
let _ = handle.wait();
});
loop {
sleep(Duration::from_secs(2));
if let Ok(device) = adb_device_prompt(env, Some(emulator.name())) {
return Ok(device);
}
}
}
}
fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
device_prompt(env, None).map(|device| device.target()).ok()
}
fn open_and_wait(config: &AndroidConfig, env: &Env) -> ! {
log::info!("Opening Android Studio");
if let Err(e) = os::open_file_with("Android Studio", config.project_dir(), &env.base) {
log::error!("{}", e);
}
loop {
sleep(Duration::from_secs(24 * 60 * 60));
}
}

View File

@@ -0,0 +1,59 @@
use super::{detect_target_ok, ensure_init, env, with_config, MobileTarget};
use crate::Result;
use clap::{ArgAction, Parser};
use cargo_mobile::{
android::target::Target,
opts::Profile,
target::{call_for_targets_with_fallback, TargetTrait},
};
#[derive(Debug, Parser)]
pub struct Options {
/// Targets to build.
#[clap(
short,
long = "target",
action = ArgAction::Append,
num_args(0..),
default_value = Target::DEFAULT_KEY,
value_parser(clap::builder::PossibleValuesParser::new(Target::name_list()))
)]
targets: Option<Vec<String>>,
/// Builds with the release flag
#[clap(short, long)]
release: bool,
}
pub fn command(options: Options) -> Result<()> {
let profile = if options.release {
Profile::Release
} else {
Profile::Debug
};
with_config(None, |_app, config, metadata, cli_options| {
ensure_init(config.project_dir(), MobileTarget::Android)?;
let env = env()?;
call_for_targets_with_fallback(
options.targets.unwrap_or_default().iter(),
&detect_target_ok,
&env,
|target: &Target| {
target
.build(
config,
metadata,
&env,
cli_options.noise_level,
true,
profile,
)
.map_err(Into::into)
},
)
.map_err(|e| anyhow::anyhow!(e.to_string()))?
})
}

View File

@@ -0,0 +1,194 @@
use super::{
delete_codegen_vars, ensure_init, env, init_dot_cargo, log_finished, open_and_wait, with_config,
MobileTarget,
};
use crate::{
helpers::flock,
interface::{AppSettings, Interface, Options as InterfaceOptions},
mobile::{write_options, CliOptions},
Result,
};
use clap::{ArgAction, Parser};
use cargo_mobile::{
android::{aab, apk, config::Config as AndroidConfig, env::Env, target::Target},
opts::{NoiseLevel, Profile},
target::TargetTrait,
};
use std::env::set_var;
#[derive(Debug, Clone, Parser)]
#[clap(about = "Android build")]
pub struct Options {
/// Builds with the debug flag
#[clap(short, long)]
pub debug: bool,
/// Which targets to build (all by default).
#[clap(
short,
long = "target",
action = ArgAction::Append,
num_args(0..),
value_parser(clap::builder::PossibleValuesParser::new(Target::name_list()))
)]
pub targets: Option<Vec<String>>,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
/// Whether to split the APKs and AABs per ABIs.
#[clap(long)]
pub split_per_abi: bool,
/// Build APKs.
#[clap(long)]
pub apk: bool,
/// Build AABs.
#[clap(long)]
pub aab: bool,
/// Open Android Studio
#[clap(short, long)]
pub open: bool,
}
impl From<Options> for crate::build::Options {
fn from(options: Options) -> Self {
Self {
runner: None,
debug: options.debug,
target: None,
features: options.features,
bundles: None,
config: options.config,
args: Vec::new(),
}
}
}
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
delete_codegen_vars();
with_config(
Some(Default::default()),
|app, config, _metadata, _cli_options| {
set_var("WRY_RUSTWEBVIEWCLIENT_CLASS_EXTENSION", "");
set_var("WRY_RUSTWEBVIEW_CLASS_INIT", "");
ensure_init(config.project_dir(), MobileTarget::Android)?;
let mut env = env()?;
init_dot_cargo(app, Some((&env, config)))?;
let open = options.open;
run_build(options, config, &mut env, noise_level)?;
if open {
open_and_wait(config, &env);
}
Ok(())
},
)
.map_err(Into::into)
}
fn run_build(
mut options: Options,
config: &AndroidConfig,
env: &mut Env,
noise_level: NoiseLevel,
) -> Result<()> {
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
if !(options.apk || options.aab) {
// if the user didn't specify the format to build, we'll do both
options.apk = true;
options.aab = true;
}
let mut build_options = options.clone().into();
let interface = crate::build::setup(&mut build_options, true)?;
let app_settings = interface.app_settings();
let bin_path = app_settings.app_binary_path(&InterfaceOptions {
debug: build_options.debug,
..Default::default()
})?;
let out_dir = bin_path.parent().unwrap();
let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?;
let cli_options = CliOptions {
features: build_options.features.clone(),
args: build_options.args.clone(),
noise_level,
vars: Default::default(),
};
let _handle = write_options(cli_options, &mut env.base)?;
options
.features
.get_or_insert(Vec::new())
.push("custom-protocol".into());
let apk_outputs = if options.apk {
apk::build(
config,
env,
noise_level,
profile,
get_targets_or_all(Vec::new())?,
options.split_per_abi,
)?
} else {
Vec::new()
};
let aab_outputs = if options.aab {
aab::build(
config,
env,
noise_level,
profile,
get_targets_or_all(Vec::new())?,
options.split_per_abi,
)?
} else {
Vec::new()
};
log_finished(apk_outputs, "APK");
log_finished(aab_outputs, "AAB");
Ok(())
}
fn get_targets_or_all<'a>(targets: Vec<String>) -> Result<Vec<&'a Target<'a>>> {
if targets.is_empty() {
Ok(Target::all().iter().map(|t| t.1).collect())
} else {
let mut outs = Vec::new();
let possible_targets = Target::all()
.keys()
.map(|key| key.to_string())
.collect::<Vec<String>>()
.join(",");
for t in targets {
let target = Target::for_name(&t).ok_or_else(|| {
anyhow::anyhow!(
"Target {} is invalid; the possible targets are {}",
t,
possible_targets
)
})?;
outs.push(target);
}
Ok(outs)
}
}

View File

@@ -0,0 +1,203 @@
use super::{
delete_codegen_vars, device_prompt, ensure_init, env, init_dot_cargo, open_and_wait, with_config,
MobileTarget,
};
use crate::{
helpers::flock,
interface::{AppSettings, Interface, MobileOptions, Options as InterfaceOptions},
mobile::{write_options, CliOptions, DevChild, DevProcess},
Result,
};
use clap::{ArgAction, Parser};
use cargo_mobile::{
android::{
config::{Config as AndroidConfig, Metadata as AndroidMetadata},
env::Env,
},
config::app::App,
opts::{FilterLevel, NoiseLevel, Profile},
};
use std::env::set_var;
const WEBVIEW_CLIENT_CLASS_EXTENSION: &str = "
@android.annotation.SuppressLint(\"WebViewClientOnReceivedSslError\")
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler, error: android.net.http.SslError) {
handler.proceed()
}
";
const WEBVIEW_CLASS_INIT: &str =
"this.settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW";
#[derive(Debug, Clone, Parser)]
#[clap(about = "Android dev")]
pub struct Options {
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
/// Disable the file watcher
#[clap(long)]
pub no_watch: bool,
/// Open Android Studio instead of trying to run on a connected device
#[clap(short, long)]
pub open: bool,
/// Runs on the given device name
pub device: Option<String>,
}
impl From<Options> for crate::dev::Options {
fn from(options: Options) -> Self {
Self {
runner: None,
target: None,
features: options.features,
exit_on_panic: options.exit_on_panic,
config: options.config,
release_mode: false,
args: Vec::new(),
no_watch: options.no_watch,
}
}
}
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
delete_codegen_vars();
with_config(
Some(Default::default()),
|app, config, metadata, _cli_options| {
set_var(
"WRY_RUSTWEBVIEWCLIENT_CLASS_EXTENSION",
WEBVIEW_CLIENT_CLASS_EXTENSION,
);
set_var("WRY_RUSTWEBVIEW_CLASS_INIT", WEBVIEW_CLASS_INIT);
ensure_init(config.project_dir(), MobileTarget::Android)?;
run_dev(options, app, config, metadata, noise_level).map_err(Into::into)
},
)
.map_err(Into::into)
}
fn run_dev(
options: Options,
app: &App,
config: &AndroidConfig,
metadata: &AndroidMetadata,
noise_level: NoiseLevel,
) -> Result<()> {
let mut dev_options = options.clone().into();
let mut interface = crate::dev::setup(&mut dev_options)?;
let app_settings = interface.app_settings();
let bin_path = app_settings.app_binary_path(&InterfaceOptions {
debug: !dev_options.release_mode,
..Default::default()
})?;
let out_dir = bin_path.parent().unwrap();
let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?;
let env = env()?;
init_dot_cargo(app, Some((&env, config)))?;
let open = options.open;
let exit_on_panic = options.exit_on_panic;
let no_watch = options.no_watch;
let device = options.device;
interface.mobile_dev(
MobileOptions {
debug: true,
features: options.features,
args: Vec::new(),
config: options.config,
no_watch: options.no_watch,
},
|options| {
let mut env = env.clone();
let cli_options = CliOptions {
features: options.features.clone(),
args: options.args.clone(),
noise_level,
vars: Default::default(),
};
let _handle = write_options(cli_options, &mut env.base)?;
if open {
open_and_wait(config, &env)
} else {
match run(
device.as_deref(),
options,
config,
&env,
metadata,
noise_level,
) {
Ok(c) => {
crate::dev::wait_dev_process(c.clone(), move |status, reason| {
crate::dev::on_app_exit(status, reason, exit_on_panic, no_watch)
});
Ok(Box::new(c) as Box<dyn DevProcess>)
}
Err(RunError::FailedToPromptForDevice(e)) => {
log::error!("{}", e);
open_and_wait(config, &env)
}
Err(e) => {
crate::dev::kill_before_dev_process();
Err(e.into())
}
}
}
},
)
}
#[derive(Debug, thiserror::Error)]
enum RunError {
#[error("{0}")]
FailedToPromptForDevice(String),
#[error("{0}")]
RunFailed(String),
}
fn run(
device: Option<&str>,
options: MobileOptions,
config: &AndroidConfig,
env: &Env,
metadata: &AndroidMetadata,
noise_level: NoiseLevel,
) -> Result<DevChild, RunError> {
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
let build_app_bundle = metadata.asset_packs().is_some();
device_prompt(env, device)
.map_err(|e| RunError::FailedToPromptForDevice(e.to_string()))?
.run(
config,
env,
noise_level,
profile,
Some(match noise_level {
NoiseLevel::Polite => FilterLevel::Info,
NoiseLevel::LoudAndProud => FilterLevel::Debug,
NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose,
}),
build_app_bundle,
false,
".MainActivity".into(),
)
.map(DevChild::new)
.map_err(|e| RunError::RunFailed(e.to_string()))
}

View File

@@ -0,0 +1,14 @@
use super::{ensure_init, env, with_config, MobileTarget};
use crate::Result;
use cargo_mobile::os;
pub fn command() -> Result<()> {
with_config(
Some(Default::default()),
|_root_conf, config, _metadata, _cli_options| {
ensure_init(config.project_dir(), MobileTarget::Android)?;
let env = env()?;
os::open_file_with("Android Studio", config.project_dir(), &env.base).map_err(Into::into)
},
)
}

View File

@@ -0,0 +1,185 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{helpers::template, Result};
use anyhow::Context;
use cargo_mobile::{
android::{
config::{Config, Metadata},
target::Target,
},
config::app::DEFAULT_ASSET_DIR,
os,
target::TargetTrait as _,
util::{
self,
cli::{Report, TextWrapper},
prefix_path,
},
};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use std::{ffi::OsStr, fs, path::Path};
const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/mobile/android");
pub fn gen(
config: &Config,
metadata: &Metadata,
(handlebars, mut map): (Handlebars, template::JsonMap),
wrapper: &TextWrapper,
) -> Result<()> {
println!("Installing Android toolchains...");
Target::install_all().with_context(|| "failed to run rustup")?;
println!("Generating Android Studio project...");
let dest = config.project_dir();
let asset_packs = metadata.asset_packs().unwrap_or_default();
map.insert(
"root-dir-rel",
Path::new(&os::replace_path_separator(
util::relativize_path(
config.app().root_dir(),
config.project_dir().join(config.app().name()),
)
.into_os_string(),
)),
);
map.insert("root-dir", config.app().root_dir());
map.insert("targets", Target::all().values().collect::<Vec<_>>());
map.insert("target-names", Target::all().keys().collect::<Vec<_>>());
map.insert(
"arches",
Target::all()
.values()
.map(|target| target.arch)
.collect::<Vec<_>>(),
);
map.insert("android-app-plugins", metadata.app_plugins());
map.insert(
"android-project-dependencies",
metadata.project_dependencies(),
);
map.insert("android-app-dependencies", metadata.app_dependencies());
map.insert(
"android-app-dependencies-platform",
metadata.app_dependencies_platform(),
);
map.insert(
"has-code",
metadata.project_dependencies().is_some()
|| metadata.app_dependencies().is_some()
|| metadata.app_dependencies_platform().is_some(),
);
map.insert(
"asset-packs",
asset_packs
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
);
map.insert("windows", cfg!(windows));
let domain = config.app().reverse_domain().replace('.', "/");
let package_path = format!("java/{}/{}", domain, config.app().name());
map.insert("package-path", &package_path);
let mut created_dirs = Vec::new();
template::render_with_generator(
&handlebars,
map.inner(),
&TEMPLATE_DIR,
&dest,
&mut |path| {
let mut iter = path.iter();
let root = iter.next().unwrap().to_str().unwrap();
let path_without_root: std::path::PathBuf = iter.collect();
let path = match (
root,
path.extension().and_then(|o| o.to_str()),
path_without_root.strip_prefix("src/main"),
) {
("app" | "buildSrc", Some("kt"), Ok(path)) => {
let parent = path.parent().unwrap();
let file_name = path.file_name().unwrap();
let out_dir = dest
.join(root)
.join("src/main")
.join(&package_path)
.join(parent);
out_dir.join(file_name)
}
_ => dest.join(path),
};
let parent = path.parent().unwrap().to_path_buf();
if !created_dirs.contains(&parent) {
fs::create_dir_all(&parent)?;
created_dirs.push(parent);
}
let mut options = fs::OpenOptions::new();
options.write(true);
#[cfg(unix)]
if path.file_name().unwrap() == OsStr::new("gradlew") {
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o755);
}
if path.file_name().unwrap() == OsStr::new("BuildTask.kt") || !path.exists() {
options.create(true).open(path).map(Some)
} else {
Ok(None)
}
},
)
.with_context(|| "failed to process template")?;
if !asset_packs.is_empty() {
Report::action_request(
"When running from Android Studio, you must first set your deployment option to \"APK from app bundle\".",
"Android Studio will not be able to find your asset packs otherwise. The option can be found under \"Run > Edit Configurations > Deploy\"."
).print(wrapper);
}
let source_dest = dest.join("app");
for source in metadata.app_sources() {
let source_src = config.app().root_dir().join(source);
let source_file = source_src
.file_name()
.ok_or_else(|| anyhow::anyhow!("asset source {} is invalid", source_src.display()))?;
fs::copy(&source_src, source_dest.join(source_file)).map_err(|cause| {
anyhow::anyhow!(
"failed to copy {} to {}: {}",
source_src.display(),
source_dest.display(),
cause
)
})?;
}
let dest = prefix_path(dest, "app/src/main/");
fs::create_dir_all(&dest).map_err(|cause| {
anyhow::anyhow!(
"failed to create directory at {}: {}",
dest.display(),
cause
)
})?;
let asset_dir = dest.join(DEFAULT_ASSET_DIR);
if !asset_dir.is_dir() {
fs::create_dir_all(&asset_dir).map_err(|cause| {
anyhow::anyhow!(
"failed to create asset dir {path}: {cause}",
path = asset_dir.display()
)
})?;
}
Ok(())
}

View File

@@ -0,0 +1,368 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{get_app, Target};
use crate::helpers::{config::get as get_tauri_config, template::JsonMap};
use crate::Result;
use cargo_mobile::{
android::{
config::Config as AndroidConfig, env::Env as AndroidEnv, target::Target as AndroidTarget,
},
config::app::App,
dot_cargo,
target::TargetTrait as _,
util::{
self,
cli::{Report, TextWrapper},
},
};
use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError};
use std::{env::current_dir, path::PathBuf};
pub fn command(target: Target, ci: bool, reinstall_deps: bool) -> Result<()> {
let wrapper = TextWrapper::with_splitter(textwrap::termwidth(), textwrap::NoHyphenation);
exec(
target,
&wrapper,
ci || std::env::var("CI").is_ok(),
reinstall_deps,
)
.map_err(|e| anyhow::anyhow!("{:#}", e))?;
Ok(())
}
pub fn init_dot_cargo(app: &App, android: Option<(&AndroidEnv, &AndroidConfig)>) -> Result<()> {
let mut dot_cargo = dot_cargo::DotCargo::load(app)?;
// Mysteriously, builds that don't specify `--target` seem to fight over
// the build cache with builds that use `--target`! This means that
// alternating between i.e. `cargo run` and `cargo apple run` would
// result in clean builds being made each time you switched... which is
// pretty nightmarish. Specifying `build.target` in `.cargo/config`
// fortunately has the same effect as specifying `--target`, so now we can
// `cargo run` with peace of mind!
//
// This behavior could be explained here:
// https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags
dot_cargo.set_default_target(util::host_target_triple()?);
if let Some((env, config)) = android {
for target in AndroidTarget::all().values() {
dot_cargo.insert_target(
target.triple.to_owned(),
target.generate_cargo_config(config, env)?,
);
}
}
dot_cargo.write(app).map_err(Into::into)
}
pub fn exec(
target: Target,
wrapper: &TextWrapper,
#[allow(unused_variables)] non_interactive: bool,
#[allow(unused_variables)] reinstall_deps: bool,
) -> Result<App> {
let tauri_config = get_tauri_config(None)?;
let tauri_config_guard = tauri_config.lock().unwrap();
let tauri_config_ = tauri_config_guard.as_ref().unwrap();
let app = get_app(tauri_config_);
let (handlebars, mut map) = handlebars(&app);
let mut args = std::env::args_os();
let tauri_binary = args
.next()
.map(|bin| {
let path = PathBuf::from(&bin);
if path.exists() {
if let Ok(dir) = current_dir() {
let absolute_path = util::prefix_path(dir, path);
return absolute_path.into();
}
}
bin
})
.unwrap_or_else(|| std::ffi::OsString::from("cargo"));
let mut build_args = Vec::new();
for arg in args {
let path = PathBuf::from(&arg);
if path.exists() {
if let Ok(dir) = current_dir() {
let absolute_path = util::prefix_path(dir, path);
build_args.push(absolute_path.to_string_lossy().into_owned());
continue;
}
}
build_args.push(arg.to_string_lossy().into_owned());
if arg == "android" || arg == "ios" {
break;
}
}
build_args.push(target.ide_build_script_name().into());
map.insert("tauri-binary", tauri_binary.to_string_lossy());
map.insert("tauri-binary-args", &build_args);
map.insert("tauri-binary-args-str", build_args.join(" "));
let app = match target {
// Generate Android Studio project
Target::Android => match AndroidEnv::new() {
Ok(env) => {
let (app, config, metadata) =
super::android::get_config(Some(app), tauri_config_, &Default::default());
map.insert("android", &config);
super::android::project::gen(&config, &metadata, (handlebars, map), wrapper)?;
init_dot_cargo(&app, Some((&env, &config)))?;
app
}
Err(err) => {
if err.sdk_or_ndk_issue() {
Report::action_request(
" to initialize Android environment; Android support won't be usable until you fix the issue below and re-run `tauri android init`!",
err,
)
.print(wrapper);
init_dot_cargo(&app, None)?;
app
} else {
return Err(err.into());
}
}
},
#[cfg(target_os = "macos")]
// Generate Xcode project
Target::Ios => {
let (app, config, metadata) =
super::ios::get_config(Some(app), tauri_config_, &Default::default());
map.insert("apple", &config);
super::ios::project::gen(
&config,
&metadata,
(handlebars, map),
wrapper,
non_interactive,
reinstall_deps,
)?;
init_dot_cargo(&app, None)?;
app
}
};
Report::victory(
"Project generated successfully!",
"Make cool apps! 🌻 🐕 🎉",
)
.print(wrapper);
Ok(app)
}
fn handlebars(app: &App) -> (Handlebars<'static>, JsonMap) {
let mut h = Handlebars::new();
h.register_escape_fn(handlebars::no_escape);
h.register_helper("html-escape", Box::new(html_escape));
h.register_helper("join", Box::new(join));
h.register_helper("quote-and-join", Box::new(quote_and_join));
h.register_helper(
"quote-and-join-colon-prefix",
Box::new(quote_and_join_colon_prefix),
);
h.register_helper("snake-case", Box::new(snake_case));
h.register_helper("reverse-domain", Box::new(reverse_domain));
h.register_helper(
"reverse-domain-snake-case",
Box::new(reverse_domain_snake_case),
);
// don't mix these up or very bad things will happen to all of us
h.register_helper("prefix-path", Box::new(prefix_path));
h.register_helper("unprefix-path", Box::new(unprefix_path));
let mut map = JsonMap::default();
map.insert("app", app);
(h, map)
}
fn get_str<'a>(helper: &'a Helper) -> &'a str {
helper
.param(0)
.and_then(|v| v.value().as_str())
.unwrap_or("")
}
fn get_str_array<'a>(
helper: &'a Helper,
formatter: impl Fn(&str) -> String,
) -> Option<Vec<String>> {
helper.param(0).and_then(|v| {
v.value().as_array().and_then(|arr| {
arr
.iter()
.map(|val| {
val.as_str().map(
#[allow(clippy::redundant_closure)]
|s| formatter(s),
)
})
.collect()
})
})
}
fn html_escape(
helper: &Helper,
_: &Handlebars,
_ctx: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(&handlebars::html_escape(get_str(helper)))
.map_err(Into::into)
}
fn join(
helper: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(
&get_str_array(helper, |s| s.to_string())
.ok_or_else(|| RenderError::new("`join` helper wasn't given an array"))?
.join(", "),
)
.map_err(Into::into)
}
fn quote_and_join(
helper: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(
&get_str_array(helper, |s| format!("{:?}", s))
.ok_or_else(|| RenderError::new("`quote-and-join` helper wasn't given an array"))?
.join(", "),
)
.map_err(Into::into)
}
fn quote_and_join_colon_prefix(
helper: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(
&get_str_array(helper, |s| format!("{:?}", format!(":{}", s)))
.ok_or_else(|| {
RenderError::new("`quote-and-join-colon-prefix` helper wasn't given an array")
})?
.join(", "),
)
.map_err(Into::into)
}
fn snake_case(
helper: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
use heck::ToSnekCase as _;
out
.write(&get_str(helper).to_snek_case())
.map_err(Into::into)
}
fn reverse_domain(
helper: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(&util::reverse_domain(get_str(helper)))
.map_err(Into::into)
}
fn reverse_domain_snake_case(
helper: &Helper,
_: &Handlebars,
_: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
use heck::ToSnekCase as _;
out
.write(&util::reverse_domain(get_str(helper)).to_snek_case())
.map_err(Into::into)
}
fn app_root(ctx: &Context) -> Result<&str, RenderError> {
let app_root = ctx
.data()
.get("app")
.ok_or_else(|| RenderError::new("`app` missing from template data."))?
.get("root-dir")
.ok_or_else(|| RenderError::new("`app.root-dir` missing from template data."))?;
app_root
.as_str()
.ok_or_else(|| RenderError::new("`app.root-dir` contained invalid UTF-8."))
}
fn prefix_path(
helper: &Helper,
_: &Handlebars,
ctx: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(
util::prefix_path(app_root(ctx)?, get_str(helper))
.to_str()
.ok_or_else(|| {
RenderError::new(
"Either the `app.root-dir` or the specified path contained invalid UTF-8.",
)
})?,
)
.map_err(Into::into)
}
fn unprefix_path(
helper: &Helper,
_: &Handlebars,
ctx: &Context,
_: &mut RenderContext,
out: &mut dyn Output,
) -> HelperResult {
out
.write(
util::unprefix_path(app_root(ctx)?, get_str(helper))
.map_err(|_| {
RenderError::new("Attempted to unprefix a path that wasn't in the app root dir.")
})?
.to_str()
.ok_or_else(|| {
RenderError::new(
"Either the `app.root-dir` or the specified path contained invalid UTF-8.",
)
})?,
)
.map_err(Into::into)
}

View File

@@ -0,0 +1,286 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use cargo_mobile::{
apple::{
config::{
Config as AppleConfig, Metadata as AppleMetadata, Platform as ApplePlatform,
Raw as RawAppleConfig,
},
device::Device,
ios_deploy, simctl,
target::Target,
teams::find_development_teams,
},
config::app::App,
env::Env,
opts::NoiseLevel,
os,
util::prompt,
};
use clap::{Parser, Subcommand};
use sublime_fuzzy::best_match;
use super::{
ensure_init, env, get_app,
init::{command as init_command, init_dot_cargo},
log_finished, read_options, CliOptions, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE,
};
use crate::{
helpers::config::{get as get_tauri_config, Config as TauriConfig},
Result,
};
use std::{
process::exit,
thread::{sleep, spawn},
time::Duration,
};
mod build;
mod certificate;
mod dev;
mod open;
pub(crate) mod project;
mod xcode_script;
pub const APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME: &str = "TAURI_APPLE_DEVELOPMENT_TEAM";
#[derive(Parser)]
#[clap(
author,
version,
about = "iOS commands",
subcommand_required(true),
arg_required_else_help(true)
)]
pub struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Debug, Parser)]
#[clap(about = "Initializes a Tauri iOS project")]
pub struct InitOptions {
/// Skip prompting for values
#[clap(long)]
ci: bool,
/// Reinstall dependencies
#[clap(short, long)]
reinstall_deps: bool,
}
#[derive(Subcommand)]
enum Commands {
Init(InitOptions),
Open,
Dev(dev::Options),
Build(build::Options),
#[clap(hide(true))]
XcodeScript(xcode_script::Options),
#[clap(subcommand)]
Certificate(CertificateCommands),
}
#[derive(Subcommand)]
enum CertificateCommands {
Setup,
Delete,
}
pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
let noise_level = NoiseLevel::from_occurrences(verbosity as u64);
match cli.command {
Commands::Init(options) => init_command(MobileTarget::Ios, options.ci, options.reinstall_deps)?,
Commands::Open => open::command()?,
Commands::Dev(options) => dev::command(options, noise_level)?,
Commands::Build(options) => build::command(options, noise_level)?,
Commands::XcodeScript(options) => xcode_script::command(options)?,
Commands::Certificate(CertificateCommands::Setup) => certificate::setup()?,
Commands::Certificate(CertificateCommands::Delete) => certificate::delete(),
}
Ok(())
}
pub fn get_config(
app: Option<App>,
config: &TauriConfig,
cli_options: &CliOptions,
) -> (App, AppleConfig, AppleMetadata) {
let app = app.unwrap_or_else(|| get_app(config));
let ios_options = cli_options.clone();
let raw = RawAppleConfig {
development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME)
.ok()
.or_else(|| config.tauri.bundle.ios.development_team.clone())
.unwrap_or_else(|| {
let teams = find_development_teams().unwrap_or_default();
match teams.len() {
0 => {
log::error!("No code signing certificates found. You must add one and set the certificate development team ID on the `tauri > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. To list the available certificates, run `tauri info`.");
exit(1);
}
1 => teams.first().unwrap().id.clone(),
_ => {
log::error!("You must set the code signing certificate development team ID on the `tauri > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. Available certificates: {}", teams.iter().map(|t| format!("{} (ID: {})", t.name, t.id)).collect::<Vec<String>>().join(", "));
exit(1);
}
}
}),
ios_features: ios_options.features.clone(),
bundle_version: config.package.version.clone(),
bundle_version_short: config.package.version.clone(),
..Default::default()
};
let config = AppleConfig::from_raw(app.clone(), Some(raw)).unwrap();
let metadata = AppleMetadata {
supported: true,
ios: ApplePlatform {
cargo_args: Some(ios_options.args),
features: ios_options.features,
..Default::default()
},
macos: Default::default(),
};
(app, config, metadata)
}
fn with_config<T>(
cli_options: Option<CliOptions>,
f: impl FnOnce(&App, &AppleConfig, &AppleMetadata, CliOptions) -> Result<T>,
) -> Result<T> {
let (app, config, metadata, cli_options) = {
let tauri_config = get_tauri_config(None)?;
let tauri_config_guard = tauri_config.lock().unwrap();
let tauri_config_ = tauri_config_guard.as_ref().unwrap();
let cli_options = cli_options.unwrap_or_else(read_options);
let (app, config, metadata) = get_config(None, tauri_config_, &cli_options);
(app, config, metadata, cli_options)
};
f(&app, &config, &metadata, cli_options)
}
fn ios_deploy_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
let device_list = ios_deploy::device_list(env)
.map_err(|cause| anyhow::anyhow!("Failed to detect connected iOS devices: {cause}"))?;
if !device_list.is_empty() {
let device = if let Some(t) = target {
let (device, score) = device_list
.into_iter()
.rev()
.map(|d| {
let score = best_match(t, d.name()).map_or(0, |m| m.score());
(d, score)
})
.max_by_key(|(_, score)| *score)
// we already checked the list is not empty
.unwrap();
if score > MIN_DEVICE_MATCH_SCORE {
device
} else {
anyhow::bail!("Could not find an iOS device matching {t}")
}
} else {
let index = if device_list.len() > 1 {
prompt::list(
concat!("Detected ", "iOS", " devices"),
device_list.iter(),
"device",
None,
"Device",
)
.map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS device: {cause}"))?
} else {
0
};
device_list.into_iter().nth(index).unwrap()
};
println!(
"Detected connected device: {} with target {:?}",
device,
device.target().triple,
);
Ok(device)
} else {
Err(anyhow::anyhow!("No connected iOS devices detected"))
}
}
fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<simctl::Device> {
let simulator_list = simctl::device_list(env).map_err(|cause| {
anyhow::anyhow!("Failed to detect connected iOS Simulator devices: {cause}")
})?;
if !simulator_list.is_empty() {
let device = if let Some(t) = target {
let (device, score) = simulator_list
.into_iter()
.rev()
.map(|d| {
let score = best_match(t, d.name()).map_or(0, |m| m.score());
(d, score)
})
.max_by_key(|(_, score)| *score)
// we already checked the list is not empty
.unwrap();
if score > MIN_DEVICE_MATCH_SCORE {
device
} else {
anyhow::bail!("Could not find an iOS Simulator matching {t}")
}
} else if simulator_list.len() > 1 {
let index = prompt::list(
concat!("Detected ", "iOS", " simulators"),
simulator_list.iter(),
"simulator",
None,
"Simulator",
)
.map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS Simulator device: {cause}"))?;
simulator_list.into_iter().nth(index).unwrap()
} else {
simulator_list.into_iter().next().unwrap()
};
log::info!("Starting simulator {}", device.name());
let handle = device.start(env)?;
spawn(move || {
let _ = handle.wait();
});
Ok(device)
} else {
Err(anyhow::anyhow!("No available iOS Simulator detected"))
}
}
fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
if let Ok(device) = ios_deploy_device_prompt(env, target) {
Ok(device)
} else {
let simulator = simulator_prompt(env, target)?;
let handle = simulator.start(env)?;
spawn(move || {
let _ = handle.wait();
});
Ok(simulator.into())
}
}
fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
device_prompt(env, None).map(|device| device.target()).ok()
}
fn open_and_wait(config: &AppleConfig, env: &Env) -> ! {
log::info!("Opening Xcode");
if let Err(e) = os::open_file_with("Xcode", config.project_dir(), env) {
log::error!("{}", e);
}
loop {
sleep(Duration::from_secs(24 * 60 * 60));
}
}

View File

@@ -0,0 +1,156 @@
use super::{
detect_target_ok, ensure_init, env, init_dot_cargo, log_finished, open_and_wait, with_config,
MobileTarget,
};
use crate::{
helpers::flock,
interface::{AppSettings, Interface, Options as InterfaceOptions},
mobile::{write_options, CliOptions},
Result,
};
use clap::{ArgAction, Parser};
use cargo_mobile::{
apple::{config::Config as AppleConfig, target::Target},
env::Env,
opts::{NoiseLevel, Profile},
target::{call_for_targets_with_fallback, TargetInvalid, TargetTrait},
};
use std::fs;
#[derive(Debug, Clone, Parser)]
#[clap(about = "Android build")]
pub struct Options {
/// Builds with the debug flag
#[clap(short, long)]
pub debug: bool,
/// Which targets to build.
#[clap(
short,
long = "target",
action = ArgAction::Append,
num_args(0..),
default_value = Target::DEFAULT_KEY,
value_parser(clap::builder::PossibleValuesParser::new(Target::name_list()))
)]
pub targets: Vec<String>,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
/// Build number to append to the app version.
#[clap(long)]
pub build_number: Option<u32>,
/// Open Xcode
#[clap(short, long)]
pub open: bool,
}
impl From<Options> for crate::build::Options {
fn from(options: Options) -> Self {
Self {
runner: None,
debug: options.debug,
target: None,
features: options.features,
bundles: None,
config: options.config,
args: Vec::new(),
}
}
}
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
with_config(
Some(Default::default()),
|app, config, _metadata, _cli_options| {
ensure_init(config.project_dir(), MobileTarget::Ios)?;
let mut env = env()?;
init_dot_cargo(app, None)?;
let open = options.open;
run_build(options, config, &mut env, noise_level)?;
if open {
open_and_wait(config, &env);
}
Ok(())
},
)
.map_err(Into::into)
}
fn run_build(
mut options: Options,
config: &AppleConfig,
env: &mut Env,
noise_level: NoiseLevel,
) -> Result<()> {
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
let mut build_options = options.clone().into();
let interface = crate::build::setup(&mut build_options, true)?;
let app_settings = interface.app_settings();
let bin_path = app_settings.app_binary_path(&InterfaceOptions {
debug: build_options.debug,
..Default::default()
})?;
let out_dir = bin_path.parent().unwrap();
let _lock = flock::open_rw(&out_dir.join("lock").with_extension("ios"), "iOS")?;
let cli_options = CliOptions {
features: build_options.features.clone(),
args: build_options.args.clone(),
noise_level,
vars: Default::default(),
};
let _handle = write_options(cli_options, env)?;
options
.features
.get_or_insert(Vec::new())
.push("custom-protocol".into());
let mut out_files = Vec::new();
call_for_targets_with_fallback(
options.targets.iter(),
&detect_target_ok,
env,
|target: &Target| -> Result<()> {
let mut app_version = config.bundle_version().clone();
if let Some(build_number) = options.build_number {
app_version.push_extra(build_number);
}
target.build(config, env, noise_level, profile)?;
target.archive(config, env, noise_level, profile, Some(app_version))?;
target.export(config, env, noise_level)?;
if let Ok(ipa_path) = config.ipa_path() {
let out_dir = config.export_dir().join(target.arch);
fs::create_dir_all(&out_dir)?;
let path = out_dir.join(ipa_path.file_name().unwrap());
fs::rename(&ipa_path, &path)?;
out_files.push(path);
}
Ok(())
},
)
.map_err(|e: TargetInvalid| anyhow::anyhow!(e.to_string()))??;
log_finished(out_files, "IPA");
Ok(())
}

View File

@@ -0,0 +1,22 @@
use crate::Result;
use tauri_bundler::bundle::macos_sign::{delete_keychain, setup_keychain};
pub fn setup() -> Result<()> {
if let (Some(certificate_encoded), Some(certificate_password)) = (
std::env::var_os("APPLE_CERTIFICATE"),
std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
) {
// setup keychain allow you to import your certificate
// for CI build
setup_keychain(certificate_encoded, certificate_password)?;
Ok(())
} else {
Err(anyhow::anyhow!(
"Missing APPLE_CERTIFICATE and APPLE_CERTIFICATE_PASSWORD environment variables"
))
}
}
pub fn delete() {
delete_keychain();
}

View File

@@ -0,0 +1,196 @@
use super::{
device_prompt, ensure_init, env, init_dot_cargo, open_and_wait, with_config, MobileTarget,
APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME,
};
use crate::{
helpers::flock,
interface::{AppSettings, Interface, MobileOptions, Options as InterfaceOptions},
mobile::{write_options, CliOptions, DevChild, DevProcess},
Result,
};
use clap::{ArgAction, Parser};
use cargo_mobile::{
apple::{config::Config as AppleConfig, teams::find_development_teams},
config::app::App,
env::Env,
opts::{NoiseLevel, Profile},
};
use dialoguer::{theme::ColorfulTheme, Select};
use std::env::{set_var, var_os};
#[derive(Debug, Clone, Parser)]
#[clap(about = "iOS dev")]
pub struct Options {
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
/// Run the code in release mode
#[clap(long = "release")]
pub release_mode: bool,
/// Disable the file watcher
#[clap(long)]
pub no_watch: bool,
/// Open Xcode instead of trying to run on a connected device
#[clap(short, long)]
pub open: bool,
/// Runs on the given device name
pub device: Option<String>,
}
impl From<Options> for crate::dev::Options {
fn from(options: Options) -> Self {
Self {
runner: None,
target: None,
features: options.features,
exit_on_panic: options.exit_on_panic,
config: options.config,
release_mode: options.release_mode,
args: Vec::new(),
no_watch: options.no_watch,
}
}
}
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
if var_os(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME).is_none() {
if let Ok(teams) = find_development_teams() {
let index = match teams.len() {
0 => None,
1 => Some(0),
_ => {
let index = Select::with_theme(&ColorfulTheme::default())
.items(
&teams
.iter()
.map(|t| format!("{} (ID: {})", t.name, t.id))
.collect::<Vec<String>>(),
)
.default(0)
.interact()?;
Some(index)
}
};
if let Some(index) = index {
let team = teams.get(index).unwrap();
log::info!(
"Using development team `{}`. To make this permanent, set the `{}` environment variable to `{}`",
team.name,
APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME,
team.id
);
set_var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME, &team.id);
}
}
}
with_config(
Some(Default::default()),
|app, config, _metadata, _cli_options| {
ensure_init(config.project_dir(), MobileTarget::Ios)?;
run_dev(options, app, config, noise_level).map_err(Into::into)
},
)
}
fn run_dev(
options: Options,
app: &App,
config: &AppleConfig,
noise_level: NoiseLevel,
) -> Result<()> {
let mut dev_options = options.clone().into();
let mut interface = crate::dev::setup(&mut dev_options)?;
let app_settings = interface.app_settings();
let bin_path = app_settings.app_binary_path(&InterfaceOptions {
debug: !dev_options.release_mode,
..Default::default()
})?;
let out_dir = bin_path.parent().unwrap();
let _lock = flock::open_rw(&out_dir.join("lock").with_extension("ios"), "iOS")?;
let env = env()?;
init_dot_cargo(app, None)?;
let open = options.open;
let exit_on_panic = options.exit_on_panic;
let no_watch = options.no_watch;
let device = options.device;
interface.mobile_dev(
MobileOptions {
debug: true,
features: options.features,
args: Vec::new(),
config: options.config,
no_watch: options.no_watch,
},
|options| {
let mut env = env.clone();
let cli_options = CliOptions {
features: options.features.clone(),
args: options.args.clone(),
noise_level,
vars: Default::default(),
};
let _handle = write_options(cli_options, &mut env)?;
if open {
open_and_wait(config, &env)
} else {
match run(device.as_deref(), options, config, &env, noise_level) {
Ok(c) => {
crate::dev::wait_dev_process(c.clone(), move |status, reason| {
crate::dev::on_app_exit(status, reason, exit_on_panic, no_watch)
});
Ok(Box::new(c) as Box<dyn DevProcess>)
}
Err(RunError::FailedToPromptForDevice(e)) => {
log::error!("{}", e);
open_and_wait(config, &env)
}
Err(e) => {
crate::dev::kill_before_dev_process();
Err(e.into())
}
}
}
},
)
}
#[derive(Debug, thiserror::Error)]
enum RunError {
#[error("{0}")]
FailedToPromptForDevice(String),
#[error("{0}")]
RunFailed(String),
}
fn run(
device: Option<&str>,
options: MobileOptions,
config: &AppleConfig,
env: &Env,
noise_level: NoiseLevel,
) -> Result<DevChild, RunError> {
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
let non_interactive = true; // ios-deploy --noninteractive (quit when app crashes or exits)
device_prompt(env, device)
.map_err(|e| RunError::FailedToPromptForDevice(e.to_string()))?
.run(config, env, noise_level, non_interactive, profile)
.map(DevChild::new)
.map_err(|e| RunError::RunFailed(e.to_string()))
}

View File

@@ -0,0 +1,14 @@
use super::{ensure_init, env, with_config, MobileTarget};
use crate::Result;
use cargo_mobile::os;
pub fn command() -> Result<()> {
with_config(
Some(Default::default()),
|_root_conf, config, _metadata, _cli_options| {
ensure_init(config.project_dir(), MobileTarget::Ios)?;
let env = env()?;
os::open_file_with("Xcode", config.project_dir(), &env).map_err(Into::into)
},
)
}

View File

@@ -0,0 +1,187 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{helpers::template, Result};
use anyhow::Context;
use cargo_mobile::{
apple::{
config::{Config, Metadata},
deps, rust_version_check,
target::Target,
},
bossy,
config::app::DEFAULT_ASSET_DIR,
target::TargetTrait as _,
util::{self, cli::TextWrapper},
};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use std::{
ffi::{OsStr, OsString},
fs::{create_dir_all, OpenOptions},
path::{Component, PathBuf},
};
const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/mobile/ios");
// unprefixed app_root seems pretty dangerous!!
// TODO: figure out what cargo-mobile meant by that
pub fn gen(
config: &Config,
metadata: &Metadata,
(handlebars, mut map): (Handlebars, template::JsonMap),
wrapper: &TextWrapper,
non_interactive: bool,
reinstall_deps: bool,
) -> Result<()> {
println!("Installing iOS toolchains...");
Target::install_all()?;
rust_version_check(wrapper)?;
deps::install_all(wrapper, non_interactive, true, reinstall_deps)
.with_context(|| "failed to install Apple dependencies")?;
let dest = config.project_dir();
let rel_prefix = util::relativize_path(config.app().root_dir(), &dest);
let source_dirs = vec![rel_prefix.join("src")];
let asset_catalogs = metadata.ios().asset_catalogs().unwrap_or_default();
let ios_pods = metadata.ios().pods().unwrap_or_default();
let macos_pods = metadata.macos().pods().unwrap_or_default();
#[cfg(target_arch = "aarch64")]
let default_archs = ["arm64", "arm64-sim"];
#[cfg(not(target_arch = "aarch64"))]
let default_archs = ["arm64", "x86_64"];
map.insert("file-groups", &source_dirs);
map.insert("ios-frameworks", metadata.ios().frameworks());
map.insert("ios-valid-archs", default_archs);
map.insert("ios-vendor-frameworks", metadata.ios().vendor_frameworks());
map.insert("ios-vendor-sdks", metadata.ios().vendor_sdks());
map.insert("macos-frameworks", metadata.macos().frameworks());
map.insert(
"macos-vendor-frameworks",
metadata.macos().vendor_frameworks(),
);
map.insert("macos-vendor-sdks", metadata.macos().vendor_frameworks());
map.insert("asset-catalogs", asset_catalogs);
map.insert("ios-pods", ios_pods);
map.insert("macos-pods", macos_pods);
map.insert(
"ios-additional-targets",
metadata.ios().additional_targets(),
);
map.insert(
"macos-additional-targets",
metadata.macos().additional_targets(),
);
map.insert("ios-pre-build-scripts", metadata.ios().pre_build_scripts());
map.insert(
"ios-post-compile-scripts",
metadata.ios().post_compile_scripts(),
);
map.insert(
"ios-post-build-scripts",
metadata.ios().post_build_scripts(),
);
map.insert(
"macos-pre-build-scripts",
metadata.macos().pre_build_scripts(),
);
map.insert(
"macos-post-compile-scripts",
metadata.macos().post_compile_scripts(),
);
map.insert(
"macos-post-build-scripts",
metadata.macos().post_build_scripts(),
);
map.insert(
"ios-command-line-arguments",
metadata.ios().command_line_arguments(),
);
map.insert(
"macos-command-line-arguments",
metadata.macos().command_line_arguments(),
);
let mut created_dirs = Vec::new();
template::render_with_generator(
&handlebars,
map.inner(),
&TEMPLATE_DIR,
&dest,
&mut |path| {
let mut components: Vec<_> = path.components().collect();
let mut new_component = None;
for component in &mut components {
if let Component::Normal(c) = component {
let c = c.to_string_lossy();
if c.contains("{{app.name}}") {
new_component.replace(OsString::from(
&c.replace("{{app.name}}", config.app().name()),
));
*component = Component::Normal(new_component.as_ref().unwrap());
break;
}
}
}
let path = dest.join(components.iter().collect::<PathBuf>());
let parent = path.parent().unwrap().to_path_buf();
if !created_dirs.contains(&parent) {
create_dir_all(&parent)?;
created_dirs.push(parent);
}
let mut options = OpenOptions::new();
options.write(true);
if path.file_name().unwrap() == OsStr::new("BuildTask.kt") || !path.exists() {
options.create(true).open(path).map(Some)
} else {
Ok(None)
}
},
)
.with_context(|| "failed to process template")?;
let asset_dir = dest.join(DEFAULT_ASSET_DIR);
if !asset_dir.is_dir() {
create_dir_all(&asset_dir).map_err(|cause| {
anyhow::anyhow!(
"failed to create asset dir {path}: {cause}",
path = asset_dir.display()
)
})?;
}
// Create all asset catalog directories if they don't already exist
for dir in asset_catalogs {
std::fs::create_dir_all(dir).map_err(|cause| {
anyhow::anyhow!(
"failed to create directory at {path}: {cause}",
path = dir.display()
)
})?;
}
// Note that Xcode doesn't always reload the project nicely; reopening is
// often necessary.
println!("Generating Xcode project...");
bossy::Command::impure("xcodegen")
.with_args(["generate", "--spec"])
.with_arg(dest.join("project.yml"))
.run_and_wait()
.with_context(|| "failed to run `xcodegen`")?;
if !ios_pods.is_empty() || !macos_pods.is_empty() {
bossy::Command::impure_parse("pod install")
.with_arg(format!("--project-directory={}", dest.display()))
.run_and_wait()
.with_context(|| "failed to run `pod install`")?;
}
Ok(())
}

View File

@@ -0,0 +1,172 @@
use super::{env, with_config};
use crate::Result;
use clap::Parser;
use cargo_mobile::{apple::target::Target, opts::Profile, util};
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
#[derive(Debug, Parser)]
pub struct Options {
/// Value of `PLATFORM_DISPLAY_NAME` env var
#[clap(long)]
platform: String,
/// Value of `SDKROOT` env var
#[clap(long)]
sdk_root: PathBuf,
/// Value of `FRAMEWORK_SEARCH_PATHS` env var
#[clap(long)]
framework_search_paths: String,
/// Value of `GCC_PREPROCESSOR_DEFINITIONS` env var
#[clap(long)]
gcc_preprocessor_definitions: String,
/// Value of `HEADER_SEARCH_PATHS` env var
#[clap(long)]
header_search_paths: String,
/// Value of `CONFIGURATION` env var
#[clap(long)]
configuration: String,
/// Value of `FORCE_COLOR` env var
#[clap(long)]
force_color: bool,
/// Value of `ARCHS` env var
#[clap(index = 1, required = true)]
arches: Vec<String>,
}
pub fn command(options: Options) -> Result<()> {
fn macos_from_platform(platform: &str) -> bool {
platform == "macOS"
}
fn profile_from_configuration(configuration: &str) -> Profile {
if configuration == "release" {
Profile::Release
} else {
Profile::Debug
}
}
// `xcode-script` is ran from the `gen/apple` folder.
std::env::set_current_dir(
std::env::current_dir()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap(),
)
.unwrap();
let profile = profile_from_configuration(&options.configuration);
let macos = macos_from_platform(&options.platform);
with_config(None, |_root_conf, config, metadata, cli_options| {
let env = env()?;
// The `PATH` env var Xcode gives us is missing any additions
// made by the user's profile, so we'll manually add cargo's
// `PATH`.
let env = env
.explicit_env_vars(cli_options.vars)
.prepend_to_path(util::home_dir()?.join(".cargo/bin"));
if !options.sdk_root.is_dir() {
return Err(anyhow::anyhow!(
"SDK root provided by Xcode was invalid. {} doesn't exist or isn't a directory",
options.sdk_root.display(),
));
}
let include_dir = options.sdk_root.join("usr/include");
if !include_dir.is_dir() {
return Err(anyhow::anyhow!(
"Include dir was invalid. {} doesn't exist or isn't a directory",
include_dir.display()
));
}
// Host flags that are used by build scripts
let macos_isysroot = {
let macos_sdk_root = options
.sdk_root
.join("../../../../MacOSX.platform/Developer/SDKs/MacOSX.sdk");
if !macos_sdk_root.is_dir() {
return Err(anyhow::anyhow!(
"Invalid SDK root {}",
macos_sdk_root.display()
));
}
format!("-isysroot {}", macos_sdk_root.display())
};
let mut host_env = HashMap::<&str, &OsStr>::new();
host_env.insert("RUST_BACKTRACE", "1".as_ref());
host_env.insert("CFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref());
host_env.insert("CXXFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref());
host_env.insert(
"OBJC_INCLUDE_PATH_x86_64_apple_darwin",
include_dir.as_os_str(),
);
host_env.insert(
"FRAMEWORK_SEARCH_PATHS",
options.framework_search_paths.as_ref(),
);
host_env.insert(
"GCC_PREPROCESSOR_DEFINITIONS",
options.gcc_preprocessor_definitions.as_ref(),
);
host_env.insert("HEADER_SEARCH_PATHS", options.header_search_paths.as_ref());
let macos_target = Target::macos();
let isysroot = format!("-isysroot {}", options.sdk_root.display());
for arch in options.arches {
// Set target-specific flags
let triple = match arch.as_str() {
"arm64" => "aarch64_apple_ios",
"arm64-sim" => "aarch64_apple_ios_sim",
"x86_64" => "x86_64_apple_ios",
"Simulator" => continue,
_ => {
return Err(anyhow::anyhow!(
"Arch specified by Xcode was invalid. {} isn't a known arch",
arch
))
}
};
let cflags = format!("CFLAGS_{}", triple);
let cxxflags = format!("CFLAGS_{}", triple);
let objc_include_path = format!("OBJC_INCLUDE_PATH_{}", triple);
let mut target_env = host_env.clone();
target_env.insert(cflags.as_ref(), isysroot.as_ref());
target_env.insert(cxxflags.as_ref(), isysroot.as_ref());
target_env.insert(objc_include_path.as_ref(), include_dir.as_ref());
let target = if macos {
&macos_target
} else {
Target::for_arch(&arch).ok_or_else(|| {
anyhow::anyhow!(
"Arch specified by Xcode was invalid. {} isn't a known arch",
arch
)
})?
};
target.compile_lib(
config,
metadata,
cli_options.noise_level,
true,
profile,
&env,
target_env,
)?;
}
Ok(())
})
.map_err(Into::into)
}

View File

@@ -0,0 +1,269 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
helpers::{app_paths::tauri_dir, config::Config as TauriConfig},
interface::DevProcess,
};
use anyhow::{bail, Result};
use cargo_mobile::{
bossy,
config::app::{App, Raw as RawAppConfig},
env::Error as EnvError,
opts::NoiseLevel,
};
use jsonrpsee::client_transport::ws::WsTransportClientBuilder;
use jsonrpsee::core::client::{Client, ClientBuilder, ClientT};
use jsonrpsee::rpc_params;
use jsonrpsee::server::{RpcModule, ServerBuilder, ServerHandle};
use serde::{Deserialize, Serialize};
use shared_child::SharedChild;
use std::{
collections::HashMap,
env::set_var,
env::var,
ffi::OsString,
fmt::Write,
net::SocketAddr,
path::PathBuf,
process::ExitStatus,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use tokio::runtime::Runtime;
#[cfg(not(windows))]
use cargo_mobile::env::Env;
#[cfg(windows)]
use cargo_mobile::os::Env;
pub mod android;
mod init;
#[cfg(target_os = "macos")]
pub mod ios;
const MIN_DEVICE_MATCH_SCORE: isize = 0;
#[derive(Clone)]
pub struct DevChild {
child: Arc<SharedChild>,
manually_killed_process: Arc<AtomicBool>,
}
impl DevChild {
fn new(handle: bossy::Handle) -> Self {
Self {
child: Arc::new(SharedChild::new(handle.into()).unwrap()),
manually_killed_process: Default::default(),
}
}
}
impl DevProcess for DevChild {
fn kill(&self) -> std::io::Result<()> {
self.child.kill()?;
self.manually_killed_process.store(true, Ordering::Relaxed);
Ok(())
}
fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
self.child.try_wait()
}
fn wait(&self) -> std::io::Result<ExitStatus> {
self.child.wait()
}
fn manually_killed_process(&self) -> bool {
self.manually_killed_process.load(Ordering::Relaxed)
}
fn is_building_app(&self) -> bool {
false
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum Target {
Android,
#[cfg(target_os = "macos")]
Ios,
}
impl Target {
fn ide_name(&self) -> &'static str {
match self {
Self::Android => "Android Studio",
#[cfg(target_os = "macos")]
Self::Ios => "Xcode",
}
}
fn command_name(&self) -> &'static str {
match self {
Self::Android => "android",
#[cfg(target_os = "macos")]
Self::Ios => "ios",
}
}
fn ide_build_script_name(&self) -> &'static str {
match self {
Self::Android => "android-studio-script",
#[cfg(target_os = "macos")]
Self::Ios => "xcode-script",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CliOptions {
pub features: Option<Vec<String>>,
pub args: Vec<String>,
pub noise_level: NoiseLevel,
pub vars: HashMap<String, OsString>,
}
fn env_vars() -> HashMap<String, OsString> {
let mut vars = HashMap::new();
for (k, v) in std::env::vars_os() {
let k = k.to_string_lossy();
if (k.starts_with("TAURI") && k != "TAURI_PRIVATE_KEY" && k != "TAURI_KEY_PASSWORD")
|| k.starts_with("WRY")
|| k == "TMPDIR"
|| k == "PATH"
{
vars.insert(k.into_owned(), v);
}
}
vars
}
fn env() -> Result<Env, EnvError> {
let env = Env::new()?.explicit_env_vars(env_vars());
Ok(env)
}
/// Writes CLI options to be used later on the Xcode and Android Studio build commands
pub fn write_options(
mut options: CliOptions,
env: &mut Env,
) -> crate::Result<(Runtime, ServerHandle)> {
options.vars.extend(env_vars());
let runtime = Runtime::new().unwrap();
let r: anyhow::Result<(ServerHandle, SocketAddr)> = runtime.block_on(async move {
let server = ServerBuilder::default().build("127.0.0.1:0").await?;
let addr = server.local_addr()?;
let mut module = RpcModule::new(());
module.register_method("options", move |_, _| Ok(options.clone()))?;
let handle = server.start(module)?;
Ok((handle, addr))
});
let (handle, addr) = r?;
env.insert_env_var("TAURI_OPTIONS_SERVER_ADDR".into(), addr.to_string().into());
Ok((runtime, handle))
}
fn read_options() -> CliOptions {
let runtime = tokio::runtime::Runtime::new().unwrap();
let options = runtime
.block_on(async move {
let (tx, rx) = WsTransportClientBuilder::default()
.build(
format!(
"ws://{}",
var("TAURI_OPTIONS_SERVER_ADDR").expect("missing addr environment variable")
)
.parse()
.unwrap(),
)
.await?;
let client: Client = ClientBuilder::default().build_with_tokio(tx, rx);
let options: CliOptions = client.request("options", rpc_params![]).await?;
Ok::<CliOptions, anyhow::Error>(options)
})
.expect("failed to read CLI options");
for (k, v) in &options.vars {
set_var(k, v);
}
options
}
fn get_app(config: &TauriConfig) -> App {
let mut s = config.tauri.bundle.identifier.rsplit('.');
let app_name = s.next().unwrap_or("app").to_string();
let mut domain = String::new();
for w in s {
domain.push_str(w);
domain.push('.');
}
domain.pop();
let s = config.tauri.bundle.identifier.split('.');
let last = s.clone().count() - 1;
let mut reverse_domain = String::new();
for (i, w) in s.enumerate() {
if i != last {
reverse_domain.push_str(w);
reverse_domain.push('.');
}
}
reverse_domain.pop();
let manifest_path = tauri_dir().join("Cargo.toml");
let app_name = if let Ok(manifest) = crate::interface::manifest::read_manifest(&manifest_path) {
manifest
.as_table()
.get("package")
.and_then(|p| p.as_table())
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.map(|n| n.to_string())
.unwrap_or(app_name)
} else {
app_name
};
let raw = RawAppConfig {
name: app_name,
stylized_name: config.package.product_name.clone(),
domain,
asset_dir: None,
template_pack: None,
};
App::from_raw(tauri_dir(), raw).unwrap()
}
fn ensure_init(project_dir: PathBuf, target: Target) -> Result<()> {
if !project_dir.exists() {
bail!(
"{} project directory {} doesn't exist. Please run `tauri {} init` and try again.",
target.ide_name(),
project_dir.display(),
target.command_name(),
)
} else {
Ok(())
}
}
fn log_finished(outputs: Vec<PathBuf>, kind: &str) {
if !outputs.is_empty() {
let mut printable_paths = String::new();
for path in &outputs {
writeln!(printable_paths, " {}", path.display()).unwrap();
}
log::info!(action = "Finished"; "{} {}{} at:\n{}", outputs.len(), kind, if outputs.len() == 1 { "" } else { "s" }, printable_paths);
}
}

View File

@@ -5,12 +5,14 @@ description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.59"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = {{{ tauri_build_dep }}}

View File

@@ -0,0 +1,8 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
pub fn main() {
app::AppBuilder::new().run();
}

View File

@@ -0,0 +1,41 @@
use tauri::App;
#[cfg(mobile)]
mod mobile;
#[cfg(mobile)]
pub use mobile::*;
pub type SetupHook = Box<dyn FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send>;
#[derive(Default)]
pub struct AppBuilder {
setup: Option<SetupHook>,
}
impl AppBuilder {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn setup<F>(mut self, setup: F) -> Self
where
F: FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send + 'static,
{
self.setup.replace(Box::new(setup));
self
}
pub fn run(self) {
let setup = self.setup;
tauri::Builder::default()
.setup(move |app| {
if let Some(setup) = setup {
(setup)(app)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
}

View File

@@ -1,10 +1,7 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#[cfg(desktop)]
mod desktop;
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
#[cfg(desktop)]
desktop::main();
}

View File

@@ -0,0 +1,4 @@
#[tauri::mobile_entry_point]
fn main() {
super::AppBuilder::new().run()
}

View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

View File

@@ -0,0 +1,17 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/buildSrc/src/main/{{package-path}}/kotlin/BuildTask.kt
/buildSrc/src/main/{{package-path}}/kotlin/RustPlugin.kt
/captures
.externalNativeBuild
.cxx
local.properties

View File

@@ -0,0 +1 @@
/src/main/{{package-path}}/generated

View File

@@ -0,0 +1,99 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rustPlugin")
{{~#each android-app-plugins}}
id("{{this}}"){{/each}}
}
android {
compileSdk = 33
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "{{reverse-domain app.domain}}.{{snake-case app.name}}"
minSdk = {{android.min-sdk-version}}
targetSdk = 33
versionCode = 1
versionName = "1.0"
}
sourceSets.getByName("main") {
{{#if android.vulkan-validation}}// Vulkan validation layers
val ndkHome = System.getenv("NDK_HOME")
jniLibs.srcDir("${ndkHome}/sources/third_party/vulkan/src/build-android/jniLibs")
{{/if}}
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packagingOptions {
{{~#each targets}}
jniLibs.keepDebugSymbols.add("*/{{this.abi}}/*.so")
{{/each}}
}
}
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
flavorDimensions.add("abi")
productFlavors {
create("universal") {
val abiList = findProperty("abiList") as? String
dimension = "abi"
ndk {
abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf(
{{~#each targets}}
"{{this.abi}}",{{/each}}
)
}
}
{{~#each targets}}
create("{{this.arch}}") {
dimension = "abi"
ndk {
abiFilters += listOf("{{this.abi}}")
}
}
{{/each}}
}
assetPacks += mutableSetOf({{quote-and-join-colon-prefix asset-packs}})
}
rust {
rootDirRel = "{{root-dir-rel}}"
targets = listOf({{quote-and-join target-names}})
arches = listOf({{quote-and-join arches}})
}
dependencies {
{{~#each android-app-dependencies-platform}}
implementation(platform("{{this}}")){{/each}}
{{~#each android-app-dependencies}}
implementation("{{this}}"){{/each}}
implementation("androidx.webkit:webkit:1.4.0")
implementation("androidx.appcompat:appcompat:1.5.0")
implementation("com.google.android.material:material:1.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}
afterEvaluate {
android.applicationVariants.all {
tasks["mergeUniversalReleaseJniLibFolders"].dependsOn(tasks["rustBuildRelease"])
tasks["mergeUniversalDebugJniLibFolders"].dependsOn(tasks["rustBuildDebug"])
productFlavors.filter{ it.name != "universal" }.forEach { _ ->
val archAndBuildType = name.capitalize()
tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"])
}
}
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{reverse-domain app.domain}}.{{snake-case app.name}}">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.{{snake-case app.name}}"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,3 @@
package {{reverse-domain app.domain}}.{{snake-case app.name}}
class MainActivity : TauriActivity()

View File

@@ -0,0 +1,5 @@
package {{reverse-domain app.domain}}.{{snake-case app.name}}
import androidx.appcompat.app.AppCompatActivity
abstract class TauriActivity : AppCompatActivity()

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Some files were not shown because too many files have changed in this diff Show More