feat:1.generate_app 兼容windows;2.支持windows 打包apk。
41
README.md
|
|
@ -45,31 +45,56 @@ web_android_shell/
|
||||||
|
|
||||||
### 运行已有品牌
|
### 运行已有品牌
|
||||||
|
|
||||||
|
建议优先使用仓库内 `.fvm` 的 Flutter / Dart,避免全局版本不一致。
|
||||||
|
|
||||||
|
Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd apps/aixue
|
||||||
|
..\..\.fvm\flutter_sdk\bin\flutter.bat run
|
||||||
|
```
|
||||||
|
|
||||||
|
macOS / zsh:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/aixue
|
cd apps/aixue
|
||||||
flutter run
|
../../.fvm/flutter_sdk/bin/flutter run
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以替换为:
|
也可以替换为:
|
||||||
|
|
||||||
```bash
|
```powershell
|
||||||
cd apps/test
|
cd apps/test
|
||||||
flutter run
|
..\..\.fvm\flutter_sdk\bin\flutter.bat run
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/yunxiao
|
cd apps/yunxiao
|
||||||
flutter run
|
../../.fvm/flutter_sdk/bin/flutter run
|
||||||
```
|
```
|
||||||
|
|
||||||
### 生成或重建品牌应用
|
### 生成或重建品牌应用
|
||||||
|
|
||||||
```bash
|
请在仓库根目录执行。
|
||||||
dart run tool/generate_app.dart aixue
|
|
||||||
dart run tool/generate_app.dart test
|
Windows PowerShell:
|
||||||
dart run tool/generate_app.dart yunxiao
|
|
||||||
|
```powershell
|
||||||
|
.\.fvm\flutter_sdk\bin\dart.bat run tool\generate_app.dart aixue
|
||||||
|
.\.fvm\flutter_sdk\bin\dart.bat run tool\generate_app.dart test
|
||||||
|
.\.fvm\flutter_sdk\bin\dart.bat run tool\generate_app.dart yunxiao
|
||||||
```
|
```
|
||||||
|
|
||||||
|
macOS / zsh:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.fvm/flutter_sdk/bin/dart run tool/generate_app.dart aixue
|
||||||
|
./.fvm/flutter_sdk/bin/dart run tool/generate_app.dart test
|
||||||
|
./.fvm/flutter_sdk/bin/dart run tool/generate_app.dart yunxiao
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:生成脚本会删除并重建已有的 `apps/<品牌名>/` 目录。
|
||||||
|
|
||||||
生成脚本会自动完成:
|
生成脚本会自动完成:
|
||||||
|
|
||||||
- 创建 Flutter Android 应用到 `apps/<品牌名>/`
|
- 创建 Flutter Android 应用到 `apps/<品牌名>/`
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,40 @@ plugins {
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
val keystoreProperties = Properties()
|
val keystoreProperties = Properties()
|
||||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks")
|
||||||
|
fun resolveKeystoreFile(rawPath: String?): File {
|
||||||
|
val candidate = rawPath?.trim().orEmpty()
|
||||||
|
if (candidate.isEmpty()) {
|
||||||
|
return defaultReleaseStoreFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val expandedHome = if (candidate.startsWith("~/") || candidate == "~") {
|
||||||
|
candidate.replaceFirst("~", System.getProperty("user.home"))
|
||||||
|
} else {
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
|
||||||
|
val storeFile = File(normalized)
|
||||||
|
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile =
|
||||||
|
resolveKeystoreFile(keystoreProperties["storeFile"] as String?)
|
||||||
|
val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias"
|
||||||
|
val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456"
|
||||||
|
val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
namespace = "com.yuanxuan.aixue"
|
namespace = "com.yuanxuan.aixue"
|
||||||
|
|
@ -41,10 +66,10 @@ android {
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
keyAlias = releaseKeyAlias
|
||||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
keyPassword = releaseKeyPassword
|
||||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
storeFile = releaseStoreFile
|
||||||
storePassword = keystoreProperties["storePassword"] as String?
|
storePassword = releaseStorePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 490 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 490 KiB |
|
|
@ -4,4 +4,4 @@
|
||||||
"portraitUp",
|
"portraitUp",
|
||||||
"portraitDown"
|
"portraitDown"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
|
@ -5,15 +5,40 @@ plugins {
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
val keystoreProperties = Properties()
|
val keystoreProperties = Properties()
|
||||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks")
|
||||||
|
fun resolveKeystoreFile(rawPath: String?): File {
|
||||||
|
val candidate = rawPath?.trim().orEmpty()
|
||||||
|
if (candidate.isEmpty()) {
|
||||||
|
return defaultReleaseStoreFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val expandedHome = if (candidate.startsWith("~/") || candidate == "~") {
|
||||||
|
candidate.replaceFirst("~", System.getProperty("user.home"))
|
||||||
|
} else {
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
|
||||||
|
val storeFile = File(normalized)
|
||||||
|
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile =
|
||||||
|
resolveKeystoreFile(keystoreProperties["storeFile"] as String?)
|
||||||
|
val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias"
|
||||||
|
val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456"
|
||||||
|
val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
namespace = "com.yuanxuan.test_shell"
|
namespace = "com.yuanxuan.test_shell"
|
||||||
|
|
@ -41,10 +66,10 @@ android {
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
keyAlias = releaseKeyAlias
|
||||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
keyPassword = releaseKeyPassword
|
||||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
storeFile = releaseStoreFile
|
||||||
storePassword = keystoreProperties["storePassword"] as String?
|
storePassword = releaseStorePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"initialUrl": "http://192.168.2.57:8080/test_bridge.html",
|
"initialUrl": "http://192.168.2.54:8080/test_bridge.html",
|
||||||
"preferredOrientations": [
|
"preferredOrientations": [
|
||||||
"portraitUp",
|
"portraitUp",
|
||||||
"portraitDown"
|
"portraitDown"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
|
@ -5,15 +5,40 @@ plugins {
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
val keystoreProperties = Properties()
|
val keystoreProperties = Properties()
|
||||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks")
|
||||||
|
fun resolveKeystoreFile(rawPath: String?): File {
|
||||||
|
val candidate = rawPath?.trim().orEmpty()
|
||||||
|
if (candidate.isEmpty()) {
|
||||||
|
return defaultReleaseStoreFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val expandedHome = if (candidate.startsWith("~/") || candidate == "~") {
|
||||||
|
candidate.replaceFirst("~", System.getProperty("user.home"))
|
||||||
|
} else {
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
|
||||||
|
val storeFile = File(normalized)
|
||||||
|
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile =
|
||||||
|
resolveKeystoreFile(keystoreProperties["storeFile"] as String?)
|
||||||
|
val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias"
|
||||||
|
val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456"
|
||||||
|
val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
namespace = "com.yuanxuan.yunxiao"
|
namespace = "com.yuanxuan.yunxiao"
|
||||||
|
|
@ -41,10 +66,10 @@ android {
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
keyAlias = releaseKeyAlias
|
||||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
keyPassword = releaseKeyPassword
|
||||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
storeFile = releaseStoreFile
|
||||||
storePassword = keystoreProperties["storePassword"] as String?
|
storePassword = releaseStorePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 490 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 490 KiB |
|
|
@ -5,6 +5,39 @@ plugins {
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks")
|
||||||
|
fun resolveKeystoreFile(rawPath: String?): File {
|
||||||
|
val candidate = rawPath?.trim().orEmpty()
|
||||||
|
if (candidate.isEmpty()) {
|
||||||
|
return defaultReleaseStoreFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val expandedHome = if (candidate.startsWith("~/") || candidate == "~") {
|
||||||
|
candidate.replaceFirst("~", System.getProperty("user.home"))
|
||||||
|
} else {
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
|
||||||
|
val storeFile = File(normalized)
|
||||||
|
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile = resolveKeystoreFile(keystoreProperties["storeFile"] as String?)
|
||||||
|
val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias"
|
||||||
|
val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456"
|
||||||
|
val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.yuanxuan.webshell.web_android_shell"
|
namespace = "com.yuanxuan.webshell.web_android_shell"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
|
@ -31,11 +64,18 @@ android {
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
keyAlias = releaseKeyAlias
|
||||||
|
keyPassword = releaseKeyPassword
|
||||||
|
storeFile = releaseStoreFile
|
||||||
|
storePassword = releaseStorePassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// 待补充:为 release 构建配置正式签名。
|
signingConfig = signingConfigs.getByName("release")
|
||||||
// 当前先使用 debug 签名,确保 `flutter run --release` 可直接运行。
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
|
@ -8,8 +8,14 @@ const List<String> _defaultPreferredOrientations = <String>[
|
||||||
'portraitUp',
|
'portraitUp',
|
||||||
'portraitDown',
|
'portraitDown',
|
||||||
];
|
];
|
||||||
|
final Directory _repoRootDir = File.fromUri(Platform.script).parent.parent;
|
||||||
|
final String _currentDartExecutable = Platform.resolvedExecutable;
|
||||||
|
final String _flutterExecutable = _resolveFlutterExecutable();
|
||||||
|
final String _dartExecutable = _resolveDartExecutable();
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
|
Directory.current = _repoRootDir.path;
|
||||||
|
|
||||||
if (args.isEmpty) {
|
if (args.isEmpty) {
|
||||||
print('\x1B[31m用法:dart run tool/generate_app.dart <品牌名>\x1B[0m');
|
print('\x1B[31m用法:dart run tool/generate_app.dart <品牌名>\x1B[0m');
|
||||||
print('\x1B[33m示例:dart run tool/generate_app.dart aixue\x1B[0m');
|
print('\x1B[33m示例:dart run tool/generate_app.dart aixue\x1B[0m');
|
||||||
|
|
@ -29,6 +35,8 @@ Future<void> main(List<String> args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\x1B[34m[信息] 正在为品牌生成应用:$brand...\x1B[0m');
|
print('\x1B[34m[信息] 正在为品牌生成应用:$brand...\x1B[0m');
|
||||||
|
print('\x1B[34m[信息] Flutter 命令:$_flutterExecutable\x1B[0m');
|
||||||
|
print('\x1B[34m[信息] Dart 命令:$_dartExecutable\x1B[0m');
|
||||||
final yamlString = await configFile.readAsString();
|
final yamlString = await configFile.readAsString();
|
||||||
final config = loadYaml(yamlString) as YamlMap;
|
final config = loadYaml(yamlString) as YamlMap;
|
||||||
|
|
||||||
|
|
@ -77,7 +85,180 @@ Future<void> main(List<String> args) async {
|
||||||
|
|
||||||
print('\x1B[32m✔ 应用 $brand 已生成到 $appDir!\x1B[0m');
|
print('\x1B[32m✔ 应用 $brand 已生成到 $appDir!\x1B[0m');
|
||||||
print('\x1B[34m构建应用请执行:\x1B[0m');
|
print('\x1B[34m构建应用请执行:\x1B[0m');
|
||||||
print(' cd $appDir && flutter build apk');
|
print(' cd $appDir');
|
||||||
|
print(' ${_buildApkCommandForCurrentPlatform()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveFlutterExecutable() {
|
||||||
|
final candidates = <String>[
|
||||||
|
..._toolCandidatesFromFlutterRoot(_flutterToolRelativePaths),
|
||||||
|
..._toolCandidatesFromRepo(_flutterToolRelativePaths),
|
||||||
|
..._flutterCandidatesFromCurrentDart(),
|
||||||
|
'flutter',
|
||||||
|
];
|
||||||
|
|
||||||
|
return _firstAvailableExecutable(candidates, fallback: 'flutter');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveDartExecutable() {
|
||||||
|
final candidates = <String>[
|
||||||
|
..._toolCandidatesFromFlutterRoot(_dartToolRelativePaths),
|
||||||
|
..._toolCandidatesFromRepo(_dartToolRelativePaths),
|
||||||
|
_currentDartExecutable,
|
||||||
|
'dart',
|
||||||
|
];
|
||||||
|
|
||||||
|
return _firstAvailableExecutable(candidates, fallback: 'dart');
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _flutterCandidatesFromCurrentDart() {
|
||||||
|
final dartExecutable = File(_currentDartExecutable);
|
||||||
|
final dartBinDir = dartExecutable.parent;
|
||||||
|
final dartSdkDir = dartBinDir.parent;
|
||||||
|
final cacheDir = dartSdkDir.parent;
|
||||||
|
final flutterBinDir = cacheDir.parent;
|
||||||
|
|
||||||
|
if (_basename(dartSdkDir.path) != 'dart-sdk' ||
|
||||||
|
_basename(cacheDir.path) != 'cache' ||
|
||||||
|
!_isSameDirectoryName(flutterBinDir.path, 'bin')) {
|
||||||
|
return <String>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final flutterBat = File.fromUri(flutterBinDir.uri.resolve('flutter.bat'));
|
||||||
|
final flutterShell = File.fromUri(flutterBinDir.uri.resolve('flutter'));
|
||||||
|
|
||||||
|
return <String>[
|
||||||
|
if (flutterBat.existsSync()) flutterBat.path,
|
||||||
|
if (flutterShell.existsSync()) flutterShell.path,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get _flutterToolRelativePaths => Platform.isWindows
|
||||||
|
? <String>['bin/flutter.bat', 'bin/flutter']
|
||||||
|
: <String>['bin/flutter', 'bin/flutter.bat'];
|
||||||
|
|
||||||
|
List<String> get _dartToolRelativePaths => Platform.isWindows
|
||||||
|
? <String>[
|
||||||
|
'bin/dart.bat',
|
||||||
|
'bin/cache/dart-sdk/bin/dart.exe',
|
||||||
|
'bin/dart',
|
||||||
|
'bin/cache/dart-sdk/bin/dart',
|
||||||
|
]
|
||||||
|
: <String>['bin/dart', 'bin/cache/dart-sdk/bin/dart'];
|
||||||
|
|
||||||
|
List<String> _toolCandidatesFromFlutterRoot(List<String> relativePaths) {
|
||||||
|
final flutterRoot = Platform.environment['FLUTTER_ROOT'];
|
||||||
|
if (flutterRoot == null || flutterRoot.isEmpty) {
|
||||||
|
return <String>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <String>[
|
||||||
|
for (final relativePath in relativePaths)
|
||||||
|
if (_fileExists(flutterRoot, relativePath))
|
||||||
|
_joinPath(flutterRoot, relativePath),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _toolCandidatesFromRepo(List<String> relativePaths) {
|
||||||
|
return <String>[
|
||||||
|
for (final relativePath in relativePaths)
|
||||||
|
if (_repoFile(relativePath).existsSync()) _repoFile(relativePath).path,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstAvailableExecutable(
|
||||||
|
List<String> candidates, {
|
||||||
|
required String fallback,
|
||||||
|
}) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate == fallback || File(candidate).existsSync()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _fileExists(String? basePath, String childPath) {
|
||||||
|
if (basePath == null || basePath.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return File(_joinPath(basePath, childPath)).existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _joinPath(String basePath, String childPath) {
|
||||||
|
final normalizedBase = basePath.replaceAll('\\', '/');
|
||||||
|
final normalizedChild = childPath.replaceAll('\\', '/');
|
||||||
|
return File.fromUri(
|
||||||
|
Directory(normalizedBase).uri.resolve(normalizedChild),
|
||||||
|
).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
File _repoFile(String relativePath) {
|
||||||
|
return File.fromUri(_repoRootDir.uri.resolve(relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
String _basename(String path) {
|
||||||
|
final normalized = path
|
||||||
|
.replaceAll('\\', '/')
|
||||||
|
.replaceFirst(RegExp(r'/+$'), '');
|
||||||
|
final segments = normalized.split('/');
|
||||||
|
return segments.isEmpty ? normalized : segments.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSameDirectoryName(String path, String name) {
|
||||||
|
return _basename(path).toLowerCase() == name.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildApkCommandForCurrentPlatform() {
|
||||||
|
final fvmFlutter = _repoFile(
|
||||||
|
Platform.isWindows
|
||||||
|
? '.fvm/flutter_sdk/bin/flutter.bat'
|
||||||
|
: '.fvm/flutter_sdk/bin/flutter',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fvmFlutter.existsSync()) {
|
||||||
|
final commandPath = Platform.isWindows
|
||||||
|
? '../../.fvm/flutter_sdk/bin/flutter.bat'
|
||||||
|
: '../../.fvm/flutter_sdk/bin/flutter';
|
||||||
|
return '$commandPath build apk';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'flutter build apk';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ProcessResult> _runProcess(
|
||||||
|
String executable,
|
||||||
|
List<String> arguments, {
|
||||||
|
String? workingDirectory,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await Process.run(
|
||||||
|
executable,
|
||||||
|
arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
runInShell:
|
||||||
|
Platform.isWindows && executable.toLowerCase().endsWith('.bat'),
|
||||||
|
);
|
||||||
|
} on ProcessException catch (error) {
|
||||||
|
print('\x1B[31m[错误] 外部命令启动失败:$executable\x1B[0m');
|
||||||
|
print('\x1B[33m命令参数:${arguments.join(' ')}\x1B[0m');
|
||||||
|
print('\x1B[33m系统信息:${error.message}\x1B[0m');
|
||||||
|
|
||||||
|
if (executable == _flutterExecutable || executable == 'flutter') {
|
||||||
|
print('\x1B[33m建议先执行:\x1B[0m');
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
print(
|
||||||
|
' \$env:PATH = "${_repoFile('.fvm/flutter_sdk/bin').path};\$env:PATH"',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
' export PATH="${_repoFile('.fvm/flutter_sdk/bin').path}:\$PATH"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _readPreferredOrientations(Object? raw) {
|
List<String> _readPreferredOrientations(Object? raw) {
|
||||||
|
|
@ -123,7 +304,7 @@ Future<void> _createFlutterApp(
|
||||||
|
|
||||||
final segments = applicationId.split('.');
|
final segments = applicationId.split('.');
|
||||||
final org = segments.sublist(0, segments.length - 1).join('.');
|
final org = segments.sublist(0, segments.length - 1).join('.');
|
||||||
final result = await Process.run('flutter', <String>[
|
final result = await _runProcess(_flutterExecutable, <String>[
|
||||||
'create',
|
'create',
|
||||||
'--org',
|
'--org',
|
||||||
org,
|
org,
|
||||||
|
|
@ -144,7 +325,7 @@ Future<void> _createFlutterApp(
|
||||||
|
|
||||||
Future<void> _addCoreDependency(String appDir) async {
|
Future<void> _addCoreDependency(String appDir) async {
|
||||||
print('\x1B[34m[信息] 正在添加 web_shell_core 依赖...\x1B[0m');
|
print('\x1B[34m[信息] 正在添加 web_shell_core 依赖...\x1B[0m');
|
||||||
final result = await Process.run('flutter', <String>[
|
final result = await _runProcess(_flutterExecutable, <String>[
|
||||||
'pub',
|
'pub',
|
||||||
'add',
|
'add',
|
||||||
'web_shell_core',
|
'web_shell_core',
|
||||||
|
|
@ -316,7 +497,7 @@ Future<void> _generateBrandingAssets(
|
||||||
print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m');
|
print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m');
|
||||||
|
|
||||||
print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m');
|
print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m');
|
||||||
final addDevDepsResult = await Process.run('flutter', <String>[
|
final addDevDepsResult = await _runProcess(_flutterExecutable, <String>[
|
||||||
'pub',
|
'pub',
|
||||||
'add',
|
'add',
|
||||||
'--dev',
|
'--dev',
|
||||||
|
|
@ -328,7 +509,7 @@ Future<void> _generateBrandingAssets(
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Process.run('flutter', <String>[
|
await _runProcess(_flutterExecutable, <String>[
|
||||||
'pub',
|
'pub',
|
||||||
'get',
|
'get',
|
||||||
], workingDirectory: appDir);
|
], workingDirectory: appDir);
|
||||||
|
|
@ -360,7 +541,7 @@ flutter_native_splash:
|
||||||
await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml);
|
await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml);
|
||||||
|
|
||||||
print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m');
|
print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m');
|
||||||
final iconsResult = await Process.run('dart', <String>[
|
final iconsResult = await _runProcess(_dartExecutable, <String>[
|
||||||
'run',
|
'run',
|
||||||
'flutter_launcher_icons',
|
'flutter_launcher_icons',
|
||||||
'-f',
|
'-f',
|
||||||
|
|
@ -373,7 +554,7 @@ flutter_native_splash:
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\x1B[34m[信息] 正在生成启动页...\x1B[0m');
|
print('\x1B[34m[信息] 正在生成启动页...\x1B[0m');
|
||||||
final splashResult = await Process.run('dart', <String>[
|
final splashResult = await _runProcess(_dartExecutable, <String>[
|
||||||
'run',
|
'run',
|
||||||
'flutter_native_splash:create',
|
'flutter_native_splash:create',
|
||||||
'--path=flutter_native_splash.yaml',
|
'--path=flutter_native_splash.yaml',
|
||||||
|
|
@ -385,7 +566,7 @@ flutter_native_splash:
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\x1B[34m[信息] 正在移除资源生成器依赖...\x1B[0m');
|
print('\x1B[34m[信息] 正在移除资源生成器依赖...\x1B[0m');
|
||||||
await Process.run('flutter', <String>[
|
await _runProcess(_flutterExecutable, <String>[
|
||||||
'pub',
|
'pub',
|
||||||
'remove',
|
'remove',
|
||||||
'flutter_launcher_icons',
|
'flutter_launcher_icons',
|
||||||
|
|
@ -428,7 +609,7 @@ Future<void> _configureSigning(String appDir, YamlMap config) async {
|
||||||
final keyPassword = signing?['key_password'] as String? ?? '123456';
|
final keyPassword = signing?['key_password'] as String? ?? '123456';
|
||||||
final storePassword = signing?['store_password'] as String? ?? '123456';
|
final storePassword = signing?['store_password'] as String? ?? '123456';
|
||||||
final storeFile =
|
final storeFile =
|
||||||
signing?['store_file'] as String? ?? '../../../../tool/key.jks';
|
signing?['store_file'] as String? ?? '../../../tool/key.jks';
|
||||||
|
|
||||||
final keyPropsContent =
|
final keyPropsContent =
|
||||||
'''
|
'''
|
||||||
|
|
@ -448,15 +629,40 @@ storeFile=$storeFile
|
||||||
var content = await gradleFile.readAsString();
|
var content = await gradleFile.readAsString();
|
||||||
if (!content.contains('val keystoreProperties = Properties()')) {
|
if (!content.contains('val keystoreProperties = Properties()')) {
|
||||||
content = content.replaceFirst('android {', '''
|
content = content.replaceFirst('android {', '''
|
||||||
|
import java.io.File
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
val keystoreProperties = Properties()
|
val keystoreProperties = Properties()
|
||||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks")
|
||||||
|
fun resolveKeystoreFile(rawPath: String?): File {
|
||||||
|
val candidate = rawPath?.trim().orEmpty()
|
||||||
|
if (candidate.isEmpty()) {
|
||||||
|
return defaultReleaseStoreFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val expandedHome = if (candidate.startsWith("~/") || candidate == "~") {
|
||||||
|
candidate.replaceFirst("~", System.getProperty("user.home"))
|
||||||
|
} else {
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
|
||||||
|
val storeFile = File(normalized)
|
||||||
|
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile =
|
||||||
|
resolveKeystoreFile(keystoreProperties["storeFile"] as String?)
|
||||||
|
val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias"
|
||||||
|
val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456"
|
||||||
|
val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
''');
|
''');
|
||||||
}
|
}
|
||||||
|
|
@ -465,10 +671,10 @@ android {
|
||||||
const newBuildTypes = '''
|
const newBuildTypes = '''
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
keyAlias = releaseKeyAlias
|
||||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
keyPassword = releaseKeyPassword
|
||||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
storeFile = releaseStoreFile
|
||||||
storePassword = keystoreProperties["storePassword"] as String?
|
storePassword = releaseStorePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||