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
|
||||
cd apps/aixue
|
||||
flutter run
|
||||
../../.fvm/flutter_sdk/bin/flutter run
|
||||
```
|
||||
|
||||
也可以替换为:
|
||||
|
||||
```bash
|
||||
```powershell
|
||||
cd apps/test
|
||||
flutter run
|
||||
..\..\.fvm\flutter_sdk\bin\flutter.bat run
|
||||
```
|
||||
|
||||
```bash
|
||||
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
|
||||
dart run tool/generate_app.dart yunxiao
|
||||
请在仓库根目录执行。
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```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/<品牌名>/`
|
||||
|
|
|
|||
|
|
@ -5,15 +5,40 @@ plugins {
|
|||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
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 {
|
||||
|
||||
namespace = "com.yuanxuan.aixue"
|
||||
|
|
@ -41,10 +66,10 @@ android {
|
|||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
keyAlias = releaseKeyAlias
|
||||
keyPassword = releaseKeyPassword
|
||||
storeFile = releaseStoreFile
|
||||
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 |
|
|
@ -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")
|
||||
}
|
||||
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
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 {
|
||||
|
||||
namespace = "com.yuanxuan.test_shell"
|
||||
|
|
@ -41,10 +66,10 @@ android {
|
|||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
keyAlias = releaseKeyAlias
|
||||
keyPassword = releaseKeyPassword
|
||||
storeFile = releaseStoreFile
|
||||
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": [
|
||||
"portraitUp",
|
||||
"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")
|
||||
}
|
||||
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
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 {
|
||||
|
||||
namespace = "com.yuanxuan.yunxiao"
|
||||
|
|
@ -41,10 +66,10 @@ android {
|
|||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
keyAlias = releaseKeyAlias
|
||||
keyPassword = releaseKeyPassword
|
||||
storeFile = releaseStoreFile
|
||||
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")
|
||||
}
|
||||
|
||||
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 {
|
||||
namespace = "com.yuanxuan.webshell.web_android_shell"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
|
|
@ -31,11 +64,18 @@ android {
|
|||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = releaseKeyAlias
|
||||
keyPassword = releaseKeyPassword
|
||||
storeFile = releaseStoreFile
|
||||
storePassword = releaseStorePassword
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// 待补充:为 release 构建配置正式签名。
|
||||
// 当前先使用 debug 签名,确保 `flutter run --release` 可直接运行。
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
'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 {
|
||||
Directory.current = _repoRootDir.path;
|
||||
|
||||
if (args.isEmpty) {
|
||||
print('\x1B[31m用法:dart run tool/generate_app.dart <品牌名>\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[信息] Flutter 命令:$_flutterExecutable\x1B[0m');
|
||||
print('\x1B[34m[信息] Dart 命令:$_dartExecutable\x1B[0m');
|
||||
final yamlString = await configFile.readAsString();
|
||||
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[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) {
|
||||
|
|
@ -123,7 +304,7 @@ Future<void> _createFlutterApp(
|
|||
|
||||
final segments = applicationId.split('.');
|
||||
final org = segments.sublist(0, segments.length - 1).join('.');
|
||||
final result = await Process.run('flutter', <String>[
|
||||
final result = await _runProcess(_flutterExecutable, <String>[
|
||||
'create',
|
||||
'--org',
|
||||
org,
|
||||
|
|
@ -144,7 +325,7 @@ Future<void> _createFlutterApp(
|
|||
|
||||
Future<void> _addCoreDependency(String appDir) async {
|
||||
print('\x1B[34m[信息] 正在添加 web_shell_core 依赖...\x1B[0m');
|
||||
final result = await Process.run('flutter', <String>[
|
||||
final result = await _runProcess(_flutterExecutable, <String>[
|
||||
'pub',
|
||||
'add',
|
||||
'web_shell_core',
|
||||
|
|
@ -316,7 +497,7 @@ Future<void> _generateBrandingAssets(
|
|||
print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m');
|
||||
|
||||
print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m');
|
||||
final addDevDepsResult = await Process.run('flutter', <String>[
|
||||
final addDevDepsResult = await _runProcess(_flutterExecutable, <String>[
|
||||
'pub',
|
||||
'add',
|
||||
'--dev',
|
||||
|
|
@ -328,7 +509,7 @@ Future<void> _generateBrandingAssets(
|
|||
exit(1);
|
||||
}
|
||||
|
||||
await Process.run('flutter', <String>[
|
||||
await _runProcess(_flutterExecutable, <String>[
|
||||
'pub',
|
||||
'get',
|
||||
], workingDirectory: appDir);
|
||||
|
|
@ -360,7 +541,7 @@ flutter_native_splash:
|
|||
await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml);
|
||||
|
||||
print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m');
|
||||
final iconsResult = await Process.run('dart', <String>[
|
||||
final iconsResult = await _runProcess(_dartExecutable, <String>[
|
||||
'run',
|
||||
'flutter_launcher_icons',
|
||||
'-f',
|
||||
|
|
@ -373,7 +554,7 @@ flutter_native_splash:
|
|||
}
|
||||
|
||||
print('\x1B[34m[信息] 正在生成启动页...\x1B[0m');
|
||||
final splashResult = await Process.run('dart', <String>[
|
||||
final splashResult = await _runProcess(_dartExecutable, <String>[
|
||||
'run',
|
||||
'flutter_native_splash:create',
|
||||
'--path=flutter_native_splash.yaml',
|
||||
|
|
@ -385,7 +566,7 @@ flutter_native_splash:
|
|||
}
|
||||
|
||||
print('\x1B[34m[信息] 正在移除资源生成器依赖...\x1B[0m');
|
||||
await Process.run('flutter', <String>[
|
||||
await _runProcess(_flutterExecutable, <String>[
|
||||
'pub',
|
||||
'remove',
|
||||
'flutter_launcher_icons',
|
||||
|
|
@ -428,7 +609,7 @@ Future<void> _configureSigning(String appDir, YamlMap config) async {
|
|||
final keyPassword = signing?['key_password'] as String? ?? '123456';
|
||||
final storePassword = signing?['store_password'] as String? ?? '123456';
|
||||
final storeFile =
|
||||
signing?['store_file'] as String? ?? '../../../../tool/key.jks';
|
||||
signing?['store_file'] as String? ?? '../../../tool/key.jks';
|
||||
|
||||
final keyPropsContent =
|
||||
'''
|
||||
|
|
@ -448,15 +629,40 @@ storeFile=$storeFile
|
|||
var content = await gradleFile.readAsString();
|
||||
if (!content.contains('val keystoreProperties = Properties()')) {
|
||||
content = content.replaceFirst('android {', '''
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
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 {
|
||||
''');
|
||||
}
|
||||
|
|
@ -465,10 +671,10 @@ android {
|
|||
const newBuildTypes = '''
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
keyAlias = releaseKeyAlias
|
||||
keyPassword = releaseKeyPassword
|
||||
storeFile = releaseStoreFile
|
||||
storePassword = releaseStorePassword
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||