mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Enable Proguard by default on release mode (#39986)
This commit is contained in:
parent
362cde43ff
commit
f098de1fde
@ -132,16 +132,6 @@ class FlutterPlugin implements Plugin<Project> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom build types
|
|
||||||
project.android.buildTypes {
|
|
||||||
profile {
|
|
||||||
initWith debug
|
|
||||||
if (it.hasProperty('matchingFallbacks')) {
|
|
||||||
matchingFallbacks = ['debug', 'release']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)
|
String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)
|
||||||
if (flutterRootPath == null) {
|
if (flutterRootPath == null) {
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
|
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
|
||||||
@ -154,6 +144,30 @@ class FlutterPlugin implements Plugin<Project> {
|
|||||||
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
|
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
|
||||||
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
|
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
|
||||||
|
|
||||||
|
// Add custom build types.
|
||||||
|
project.android.buildTypes {
|
||||||
|
profile {
|
||||||
|
initWith debug
|
||||||
|
if (it.hasProperty("matchingFallbacks")) {
|
||||||
|
matchingFallbacks = ["debug", "release"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useProguard(project)) {
|
||||||
|
String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
|
||||||
|
"gradle", "flutter_proguard_rules.pro")
|
||||||
|
project.android.buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard true
|
||||||
|
// Fallback to `android/app/proguard-rules.pro`.
|
||||||
|
// This way, custom Proguard rules can be configured as needed.
|
||||||
|
proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (useLocalEngine(project)) {
|
if (useLocalEngine(project)) {
|
||||||
String engineOutPath = project.property('localEngineOut')
|
String engineOutPath = project.property('localEngineOut')
|
||||||
File engineOut = project.file(engineOutPath)
|
File engineOut = project.file(engineOutPath)
|
||||||
@ -375,6 +389,14 @@ class FlutterPlugin implements Plugin<Project> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean useProguard(Project project) {
|
||||||
|
if (project.hasProperty('proguard')) {
|
||||||
|
return project.property('proguard').toBoolean()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private static Boolean buildPluginAsAar() {
|
private static Boolean buildPluginAsAar() {
|
||||||
return System.getProperty('build-plugins-as-aars') == 'true'
|
return System.getProperty('build-plugins-as-aars') == 'true'
|
||||||
}
|
}
|
||||||
|
11
packages/flutter_tools/gradle/flutter_proguard_rules.pro
Normal file
11
packages/flutter_tools/gradle/flutter_proguard_rules.pro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Prevents `Fragment and FragmentActivity not found`.
|
||||||
|
# TODO(blasten): Remove once we bring the Maven dependencies.
|
||||||
|
-dontwarn io.flutter.embedding.**
|
||||||
|
|
||||||
|
# Build the ephemeral app in a module project.
|
||||||
|
# Prevents: Warning: library class <plugin-package> depends on program class io.flutter.plugin.**
|
||||||
|
# This is due to plugins (libraries) depending on the embedding (the program jar)
|
||||||
|
-dontwarn io.flutter.plugin.**
|
||||||
|
|
||||||
|
# The android.** package is provided by the OS at runtime.
|
||||||
|
-dontwarn android.**
|
@ -10,6 +10,7 @@ import 'package:meta/meta.dart';
|
|||||||
import '../android/android_sdk.dart';
|
import '../android/android_sdk.dart';
|
||||||
import '../artifacts.dart';
|
import '../artifacts.dart';
|
||||||
import '../base/common.dart';
|
import '../base/common.dart';
|
||||||
|
import '../base/context.dart';
|
||||||
import '../base/file_system.dart';
|
import '../base/file_system.dart';
|
||||||
import '../base/logger.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/os.dart';
|
import '../base/os.dart';
|
||||||
@ -28,11 +29,39 @@ import '../reporting/reporting.dart';
|
|||||||
import 'android_sdk.dart';
|
import 'android_sdk.dart';
|
||||||
import 'android_studio.dart';
|
import 'android_studio.dart';
|
||||||
|
|
||||||
final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
|
/// Gradle utils in the current [AppContext].
|
||||||
|
GradleUtils get gradleUtils => context.get<GradleUtils>();
|
||||||
|
|
||||||
GradleProject _cachedGradleAppProject;
|
/// Provides utilities to run a Gradle task,
|
||||||
GradleProject _cachedGradleLibraryProject;
|
/// such as finding the Gradle executable or constructing a Gradle project.
|
||||||
String _cachedGradleExecutable;
|
class GradleUtils {
|
||||||
|
/// Empty constructor.
|
||||||
|
GradleUtils();
|
||||||
|
|
||||||
|
String _cachedExecutable;
|
||||||
|
/// Gets the Gradle executable path.
|
||||||
|
/// This is the `gradlew` or `gradlew.bat` script in the `android/` directory.
|
||||||
|
Future<String> getExecutable(FlutterProject project) async {
|
||||||
|
_cachedExecutable ??= await _initializeGradle(project);
|
||||||
|
return _cachedExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
GradleProject _cachedAppProject;
|
||||||
|
/// Gets the [GradleProject] for the current [FlutterProject] if built as an app.
|
||||||
|
Future<GradleProject> get appProject async {
|
||||||
|
_cachedAppProject ??= await _readGradleProject(isLibrary: false);
|
||||||
|
return _cachedAppProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
GradleProject _cachedLibraryProject;
|
||||||
|
/// Gets the [GradleProject] for the current [FlutterProject] if built as a library.
|
||||||
|
Future<GradleProject> get libraryProject async {
|
||||||
|
_cachedLibraryProject ??= await _readGradleProject(isLibrary: true);
|
||||||
|
return _cachedLibraryProject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
|
||||||
|
|
||||||
enum FlutterPluginVersion {
|
enum FlutterPluginVersion {
|
||||||
none,
|
none,
|
||||||
@ -103,29 +132,20 @@ Future<File> getGradleAppOut(AndroidProject androidProject) async {
|
|||||||
case FlutterPluginVersion.managed:
|
case FlutterPluginVersion.managed:
|
||||||
// Fall through. The managed plugin matches plugin v2 for now.
|
// Fall through. The managed plugin matches plugin v2 for now.
|
||||||
case FlutterPluginVersion.v2:
|
case FlutterPluginVersion.v2:
|
||||||
return fs.file((await _gradleAppProject()).apkDirectory.childFile('app.apk'));
|
final GradleProject gradleProject = await gradleUtils.appProject;
|
||||||
|
return fs.file(gradleProject.apkDirectory.childFile('app.apk'));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GradleProject> _gradleAppProject() async {
|
|
||||||
_cachedGradleAppProject ??= await _readGradleProject(isLibrary: false);
|
|
||||||
return _cachedGradleAppProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<GradleProject> _gradleLibraryProject() async {
|
|
||||||
_cachedGradleLibraryProject ??= await _readGradleProject(isLibrary: true);
|
|
||||||
return _cachedGradleLibraryProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
|
/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
|
||||||
/// potentially downloaded.
|
/// potentially downloaded.
|
||||||
Future<void> checkGradleDependencies() async {
|
Future<void> checkGradleDependencies() async {
|
||||||
final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation);
|
final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation);
|
||||||
final FlutterProject flutterProject = FlutterProject.current();
|
final FlutterProject flutterProject = FlutterProject.current();
|
||||||
final String gradle = await _ensureGradle(flutterProject);
|
final String gradlew = await gradleUtils.getExecutable(flutterProject);
|
||||||
await runCheckedAsync(
|
await runCheckedAsync(
|
||||||
<String>[gradle, 'dependencies'],
|
<String>[gradlew, 'dependencies'],
|
||||||
workingDirectory: flutterProject.android.hostAppGradleRoot.path,
|
workingDirectory: flutterProject.android.hostAppGradleRoot.path,
|
||||||
environment: _gradleEnv,
|
environment: _gradleEnv,
|
||||||
);
|
);
|
||||||
@ -189,7 +209,8 @@ void createSettingsAarGradle(Directory androidDirectory) {
|
|||||||
// of calculating the app properties using Gradle. This may take minutes.
|
// of calculating the app properties using Gradle. This may take minutes.
|
||||||
Future<GradleProject> _readGradleProject({bool isLibrary = false}) async {
|
Future<GradleProject> _readGradleProject({bool isLibrary = false}) async {
|
||||||
final FlutterProject flutterProject = FlutterProject.current();
|
final FlutterProject flutterProject = FlutterProject.current();
|
||||||
final String gradle = await _ensureGradle(flutterProject);
|
final String gradlew = await gradleUtils.getExecutable(flutterProject);
|
||||||
|
|
||||||
updateLocalProperties(project: flutterProject);
|
updateLocalProperties(project: flutterProject);
|
||||||
|
|
||||||
final FlutterManifest manifest = flutterProject.manifest;
|
final FlutterManifest manifest = flutterProject.manifest;
|
||||||
@ -213,12 +234,12 @@ Future<GradleProject> _readGradleProject({bool isLibrary = false}) async {
|
|||||||
// flavors and build types defined in the project. If gradle fails, then check if the failure is due to t
|
// flavors and build types defined in the project. If gradle fails, then check if the failure is due to t
|
||||||
try {
|
try {
|
||||||
final RunResult propertiesRunResult = await runCheckedAsync(
|
final RunResult propertiesRunResult = await runCheckedAsync(
|
||||||
<String>[gradle, isLibrary ? 'properties' : 'app:properties'],
|
<String>[gradlew, isLibrary ? 'properties' : 'app:properties'],
|
||||||
workingDirectory: hostAppGradleRoot.path,
|
workingDirectory: hostAppGradleRoot.path,
|
||||||
environment: _gradleEnv,
|
environment: _gradleEnv,
|
||||||
);
|
);
|
||||||
final RunResult tasksRunResult = await runCheckedAsync(
|
final RunResult tasksRunResult = await runCheckedAsync(
|
||||||
<String>[gradle, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'],
|
<String>[gradlew, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'],
|
||||||
workingDirectory: hostAppGradleRoot.path,
|
workingDirectory: hostAppGradleRoot.path,
|
||||||
environment: _gradleEnv,
|
environment: _gradleEnv,
|
||||||
);
|
);
|
||||||
@ -274,11 +295,6 @@ String _locateGradlewExecutable(Directory directory) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _ensureGradle(FlutterProject project) async {
|
|
||||||
_cachedGradleExecutable ??= await _initializeGradle(project);
|
|
||||||
return _cachedGradleExecutable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
|
// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
|
||||||
// of validating the Gradle executable. This may take several seconds.
|
// of validating the Gradle executable. This may take several seconds.
|
||||||
Future<String> _initializeGradle(FlutterProject project) async {
|
Future<String> _initializeGradle(FlutterProject project) async {
|
||||||
@ -492,17 +508,15 @@ Future<void> buildGradleProject({
|
|||||||
// from the local.properties file.
|
// from the local.properties file.
|
||||||
updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
|
updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
|
||||||
|
|
||||||
final String gradle = await _ensureGradle(project);
|
|
||||||
|
|
||||||
switch (getFlutterPluginVersion(project.android)) {
|
switch (getFlutterPluginVersion(project.android)) {
|
||||||
case FlutterPluginVersion.none:
|
case FlutterPluginVersion.none:
|
||||||
// Fall through. Pretend it's v1, and just go for it.
|
// Fall through. Pretend it's v1, and just go for it.
|
||||||
case FlutterPluginVersion.v1:
|
case FlutterPluginVersion.v1:
|
||||||
return _buildGradleProjectV1(project, gradle);
|
return _buildGradleProjectV1(project);
|
||||||
case FlutterPluginVersion.managed:
|
case FlutterPluginVersion.managed:
|
||||||
// Fall through. Managed plugin builds the same way as plugin v2.
|
// Fall through. Managed plugin builds the same way as plugin v2.
|
||||||
case FlutterPluginVersion.v2:
|
case FlutterPluginVersion.v2:
|
||||||
return _buildGradleProjectV2(project, gradle, androidBuildInfo, target, isBuildingBundle);
|
return _buildGradleProjectV2(project, androidBuildInfo, target, isBuildingBundle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,9 +530,9 @@ Future<void> buildGradleAar({
|
|||||||
|
|
||||||
GradleProject gradleProject;
|
GradleProject gradleProject;
|
||||||
if (manifest.isModule) {
|
if (manifest.isModule) {
|
||||||
gradleProject = await _gradleAppProject();
|
gradleProject = await gradleUtils.appProject;
|
||||||
} else if (manifest.isPlugin) {
|
} else if (manifest.isPlugin) {
|
||||||
gradleProject = await _gradleLibraryProject();
|
gradleProject = await gradleUtils.libraryProject;
|
||||||
} else {
|
} else {
|
||||||
throwToolExit('AARs can only be built for plugin or module projects.');
|
throwToolExit('AARs can only be built for plugin or module projects.');
|
||||||
}
|
}
|
||||||
@ -538,12 +552,11 @@ Future<void> buildGradleAar({
|
|||||||
multilineOutput: true,
|
multilineOutput: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final String gradle = await _ensureGradle(project);
|
final String gradlew = await gradleUtils.getExecutable(project);
|
||||||
final String gradlePath = fs.file(gradle).absolute.path;
|
|
||||||
final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
|
final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
|
||||||
final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle');
|
final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle');
|
||||||
final List<String> command = <String>[
|
final List<String> command = <String>[
|
||||||
gradlePath,
|
gradlew,
|
||||||
'-I=$initScript',
|
'-I=$initScript',
|
||||||
'-Pflutter-root=$flutterRoot',
|
'-Pflutter-root=$flutterRoot',
|
||||||
'-Poutput-dir=${gradleProject.buildDirectory}',
|
'-Poutput-dir=${gradleProject.buildDirectory}',
|
||||||
@ -601,7 +614,8 @@ Future<void> buildGradleAar({
|
|||||||
printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green);
|
printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
|
Future<void> _buildGradleProjectV1(FlutterProject project) async {
|
||||||
|
final String gradlew = await gradleUtils.getExecutable(project);
|
||||||
// Run 'gradlew build'.
|
// Run 'gradlew build'.
|
||||||
final Status status = logger.startProgress(
|
final Status status = logger.startProgress(
|
||||||
'Running \'gradlew build\'...',
|
'Running \'gradlew build\'...',
|
||||||
@ -610,7 +624,7 @@ Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async
|
|||||||
);
|
);
|
||||||
final Stopwatch sw = Stopwatch()..start();
|
final Stopwatch sw = Stopwatch()..start();
|
||||||
final int exitCode = await runCommandAndStreamOutput(
|
final int exitCode = await runCommandAndStreamOutput(
|
||||||
<String>[fs.file(gradle).absolute.path, 'build'],
|
<String>[fs.file(gradlew).absolute.path, 'build'],
|
||||||
workingDirectory: project.android.hostAppGradleRoot.path,
|
workingDirectory: project.android.hostAppGradleRoot.path,
|
||||||
allowReentrantFlutter: true,
|
allowReentrantFlutter: true,
|
||||||
environment: _gradleEnv,
|
environment: _gradleEnv,
|
||||||
@ -661,12 +675,12 @@ void printUndefinedTask(GradleProject project, BuildInfo buildInfo) {
|
|||||||
|
|
||||||
Future<void> _buildGradleProjectV2(
|
Future<void> _buildGradleProjectV2(
|
||||||
FlutterProject flutterProject,
|
FlutterProject flutterProject,
|
||||||
String gradle,
|
|
||||||
AndroidBuildInfo androidBuildInfo,
|
AndroidBuildInfo androidBuildInfo,
|
||||||
String target,
|
String target,
|
||||||
bool isBuildingBundle,
|
bool isBuildingBundle,
|
||||||
) async {
|
) async {
|
||||||
final GradleProject project = await _gradleAppProject();
|
final String gradlew = await gradleUtils.getExecutable(flutterProject);
|
||||||
|
final GradleProject project = await gradleUtils.appProject;
|
||||||
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
|
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
|
||||||
|
|
||||||
String assembleTask;
|
String assembleTask;
|
||||||
@ -685,8 +699,7 @@ Future<void> _buildGradleProjectV2(
|
|||||||
timeout: timeoutConfiguration.slowOperation,
|
timeout: timeoutConfiguration.slowOperation,
|
||||||
multilineOutput: true,
|
multilineOutput: true,
|
||||||
);
|
);
|
||||||
final String gradlePath = fs.file(gradle).absolute.path;
|
final List<String> command = <String>[gradlew];
|
||||||
final List<String> command = <String>[gradlePath];
|
|
||||||
if (logger.isVerbose) {
|
if (logger.isVerbose) {
|
||||||
command.add('-Pverbose=true');
|
command.add('-Pverbose=true');
|
||||||
} else {
|
} else {
|
||||||
@ -712,6 +725,8 @@ Future<void> _buildGradleProjectV2(
|
|||||||
command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
|
command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
|
||||||
if (androidBuildInfo.splitPerAbi)
|
if (androidBuildInfo.splitPerAbi)
|
||||||
command.add('-Psplit-per-abi=true');
|
command.add('-Psplit-per-abi=true');
|
||||||
|
if (androidBuildInfo.proguard)
|
||||||
|
command.add('-Pproguard=true');
|
||||||
if (androidBuildInfo.targetArchs.isNotEmpty) {
|
if (androidBuildInfo.targetArchs.isNotEmpty) {
|
||||||
final String targetPlatforms = androidBuildInfo.targetArchs
|
final String targetPlatforms = androidBuildInfo.targetArchs
|
||||||
.map(getPlatformNameForAndroidArch).join(',');
|
.map(getPlatformNameForAndroidArch).join(',');
|
||||||
@ -727,6 +742,7 @@ Future<void> _buildGradleProjectV2(
|
|||||||
}
|
}
|
||||||
command.add(assembleTask);
|
command.add(assembleTask);
|
||||||
bool potentialAndroidXFailure = false;
|
bool potentialAndroidXFailure = false;
|
||||||
|
bool potentialProguardFailure = false;
|
||||||
final Stopwatch sw = Stopwatch()..start();
|
final Stopwatch sw = Stopwatch()..start();
|
||||||
int exitCode = 1;
|
int exitCode = 1;
|
||||||
try {
|
try {
|
||||||
@ -743,13 +759,17 @@ Future<void> _buildGradleProjectV2(
|
|||||||
if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) {
|
if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) {
|
||||||
potentialAndroidXFailure = true;
|
potentialAndroidXFailure = true;
|
||||||
}
|
}
|
||||||
|
// Proguard errors include this url.
|
||||||
|
if (!potentialProguardFailure && androidBuildInfo.proguard &&
|
||||||
|
line.contains('http://proguard.sourceforge.net')) {
|
||||||
|
potentialProguardFailure = true;
|
||||||
|
}
|
||||||
// Always print the full line in verbose mode.
|
// Always print the full line in verbose mode.
|
||||||
if (logger.isVerbose) {
|
if (logger.isVerbose) {
|
||||||
return line;
|
return line;
|
||||||
} else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) {
|
} else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return line;
|
return line;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -758,7 +778,13 @@ Future<void> _buildGradleProjectV2(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0) {
|
||||||
if (potentialAndroidXFailure) {
|
if (potentialProguardFailure) {
|
||||||
|
final String exclamationMark = terminal.color('[!]', TerminalColor.red);
|
||||||
|
printStatus('$exclamationMark Proguard may have failed to optimize the Java bytecode.', emphasis: true);
|
||||||
|
printStatus('To disable proguard, pass the `--no-proguard` flag to this command.', indent: 4);
|
||||||
|
printStatus('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard', indent: 4);
|
||||||
|
BuildEvent('proguard-failure').send();
|
||||||
|
} else if (potentialAndroidXFailure) {
|
||||||
printStatus('AndroidX incompatibilities may have caused this build to fail. See https://goo.gl/CP92wY.');
|
printStatus('AndroidX incompatibilities may have caused this build to fail. See https://goo.gl/CP92wY.');
|
||||||
BuildEvent('android-x-failure').send();
|
BuildEvent('android-x-failure').send();
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,7 @@ class AndroidBuildInfo {
|
|||||||
AndroidArch.arm64_v8a,
|
AndroidArch.arm64_v8a,
|
||||||
],
|
],
|
||||||
this.splitPerAbi = false,
|
this.splitPerAbi = false,
|
||||||
|
this.proguard = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The build info containing the mode and flavor.
|
// The build info containing the mode and flavor.
|
||||||
@ -104,6 +105,9 @@ class AndroidBuildInfo {
|
|||||||
/// will be produced.
|
/// will be produced.
|
||||||
final bool splitPerAbi;
|
final bool splitPerAbi;
|
||||||
|
|
||||||
|
/// Whether to enable Proguard on release mode.
|
||||||
|
final bool proguard;
|
||||||
|
|
||||||
/// The target platforms for the build.
|
/// The target platforms for the build.
|
||||||
final Iterable<AndroidArch> targetArchs;
|
final Iterable<AndroidArch> targetArchs;
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,15 @@ class BuildApkCommand extends BuildSubCommand {
|
|||||||
argParser
|
argParser
|
||||||
..addFlag('split-per-abi',
|
..addFlag('split-per-abi',
|
||||||
negatable: false,
|
negatable: false,
|
||||||
help: 'Whether to split the APKs per ABIs.'
|
help: 'Whether to split the APKs per ABIs. '
|
||||||
'To learn more, see: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split',
|
'To learn more, see: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split',
|
||||||
)
|
)
|
||||||
|
..addFlag('proguard',
|
||||||
|
negatable: true,
|
||||||
|
defaultsTo: true,
|
||||||
|
help: 'Whether to enable Proguard on release mode. '
|
||||||
|
'To learn more, see: https://flutter.dev/docs/deployment/android#enabling-proguard',
|
||||||
|
)
|
||||||
..addMultiOption('target-platform',
|
..addMultiOption('target-platform',
|
||||||
splitCommas: true,
|
splitCommas: true,
|
||||||
defaultsTo: <String>['android-arm', 'android-arm64'],
|
defaultsTo: <String>['android-arm', 'android-arm64'],
|
||||||
@ -79,7 +85,8 @@ class BuildApkCommand extends BuildSubCommand {
|
|||||||
final BuildInfo buildInfo = getBuildInfo();
|
final BuildInfo buildInfo = getBuildInfo();
|
||||||
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo,
|
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo,
|
||||||
splitPerAbi: argResults['split-per-abi'],
|
splitPerAbi: argResults['split-per-abi'],
|
||||||
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName)
|
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName),
|
||||||
|
proguard: argResults['proguard'],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) {
|
if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) {
|
||||||
|
@ -22,6 +22,12 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
|||||||
|
|
||||||
argParser
|
argParser
|
||||||
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
|
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
|
||||||
|
..addFlag('proguard',
|
||||||
|
negatable: true,
|
||||||
|
defaultsTo: true,
|
||||||
|
help: 'Whether to enable Proguard on release mode. '
|
||||||
|
'To learn more, see: https://flutter.dev/docs/deployment/android#enabling-proguard',
|
||||||
|
)
|
||||||
..addMultiOption('target-platform',
|
..addMultiOption('target-platform',
|
||||||
splitCommas: true,
|
splitCommas: true,
|
||||||
defaultsTo: <String>['android-arm', 'android-arm64'],
|
defaultsTo: <String>['android-arm', 'android-arm64'],
|
||||||
@ -63,7 +69,8 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
|||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(getBuildInfo(),
|
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(getBuildInfo(),
|
||||||
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName)
|
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName),
|
||||||
|
proguard: argResults['proguard'],
|
||||||
);
|
);
|
||||||
await androidBuilder.buildAab(
|
await androidBuilder.buildAab(
|
||||||
project: FlutterProject.current(),
|
project: FlutterProject.current(),
|
||||||
|
@ -7,6 +7,7 @@ import 'dart:async';
|
|||||||
import 'android/android_sdk.dart';
|
import 'android/android_sdk.dart';
|
||||||
import 'android/android_studio.dart';
|
import 'android/android_studio.dart';
|
||||||
import 'android/android_workflow.dart';
|
import 'android/android_workflow.dart';
|
||||||
|
import 'android/gradle.dart';
|
||||||
import 'application_package.dart';
|
import 'application_package.dart';
|
||||||
import 'artifacts.dart';
|
import 'artifacts.dart';
|
||||||
import 'asset.dart';
|
import 'asset.dart';
|
||||||
@ -89,6 +90,7 @@ Future<T> runInContext<T>(
|
|||||||
FuchsiaSdk: () => FuchsiaSdk(),
|
FuchsiaSdk: () => FuchsiaSdk(),
|
||||||
FuchsiaWorkflow: () => FuchsiaWorkflow(),
|
FuchsiaWorkflow: () => FuchsiaWorkflow(),
|
||||||
GenSnapshot: () => const GenSnapshot(),
|
GenSnapshot: () => const GenSnapshot(),
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
HotRunnerConfig: () => HotRunnerConfig(),
|
HotRunnerConfig: () => HotRunnerConfig(),
|
||||||
IMobileDevice: () => IMobileDevice(),
|
IMobileDevice: () => IMobileDevice(),
|
||||||
IOSDeploy: () => const IOSDeploy(),
|
IOSDeploy: () => const IOSDeploy(),
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io' hide File;
|
import 'dart:io' hide File;
|
||||||
|
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
@ -17,21 +16,7 @@ import 'package:process/process.dart';
|
|||||||
|
|
||||||
import '../src/common.dart';
|
import '../src/common.dart';
|
||||||
import '../src/context.dart';
|
import '../src/context.dart';
|
||||||
|
import '../src/mocks.dart';
|
||||||
Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) {
|
|
||||||
final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
|
|
||||||
utf8.encode(stdout),
|
|
||||||
]);
|
|
||||||
final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
|
|
||||||
utf8.encode(stderr),
|
|
||||||
]);
|
|
||||||
final Process process = MockProcess();
|
|
||||||
|
|
||||||
when(process.stdout).thenAnswer((_) => stdoutStream);
|
|
||||||
when(process.stderr).thenAnswer((_) => stderrStream);
|
|
||||||
when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
|
|
||||||
return process;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('channel', () {
|
group('channel', () {
|
||||||
|
@ -2,15 +2,24 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
import 'package:flutter_tools/src/android/android_builder.dart';
|
import 'package:flutter_tools/src/android/android_builder.dart';
|
||||||
|
import 'package:flutter_tools/src/android/android_sdk.dart';
|
||||||
|
import 'package:flutter_tools/src/android/gradle.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/commands/build_apk.dart';
|
import 'package:flutter_tools/src/commands/build_apk.dart';
|
||||||
|
import 'package:flutter_tools/src/project.dart';
|
||||||
import 'package:flutter_tools/src/reporting/reporting.dart';
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/context.dart';
|
import '../../src/context.dart';
|
||||||
|
import '../../src/mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Cache.disableLocking();
|
Cache.disableLocking();
|
||||||
@ -26,21 +35,10 @@ void main() {
|
|||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<BuildApkCommand> runCommandIn(String target, { List<String> arguments }) async {
|
|
||||||
final BuildApkCommand command = BuildApkCommand();
|
|
||||||
final CommandRunner<void> runner = createTestCommandRunner(command);
|
|
||||||
await runner.run(<String>[
|
|
||||||
'apk',
|
|
||||||
...?arguments,
|
|
||||||
fs.path.join(target, 'lib', 'main.dart'),
|
|
||||||
]);
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
testUsingContext('indicate the default target platforms', () async {
|
testUsingContext('indicate the default target platforms', () async {
|
||||||
final String projectPath = await createProject(tempDir,
|
final String projectPath = await createProject(tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app']);
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
final BuildApkCommand command = await runCommandIn(projectPath);
|
final BuildApkCommand command = await runBuildApkCommand(projectPath);
|
||||||
|
|
||||||
expect(await command.usageValues,
|
expect(await command.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkTargetPlatform, 'android-arm,android-arm64'));
|
containsPair(CustomDimensions.commandBuildApkTargetPlatform, 'android-arm,android-arm64'));
|
||||||
@ -53,12 +51,12 @@ void main() {
|
|||||||
final String projectPath = await createProject(tempDir,
|
final String projectPath = await createProject(tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app']);
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
final BuildApkCommand commandWithFlag = await runCommandIn(projectPath,
|
final BuildApkCommand commandWithFlag = await runBuildApkCommand(projectPath,
|
||||||
arguments: <String>['--split-per-abi']);
|
arguments: <String>['--split-per-abi']);
|
||||||
expect(await commandWithFlag.usageValues,
|
expect(await commandWithFlag.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkSplitPerAbi, 'true'));
|
containsPair(CustomDimensions.commandBuildApkSplitPerAbi, 'true'));
|
||||||
|
|
||||||
final BuildApkCommand commandWithoutFlag = await runCommandIn(projectPath);
|
final BuildApkCommand commandWithoutFlag = await runBuildApkCommand(projectPath);
|
||||||
expect(await commandWithoutFlag.usageValues,
|
expect(await commandWithoutFlag.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkSplitPerAbi, 'false'));
|
containsPair(CustomDimensions.commandBuildApkSplitPerAbi, 'false'));
|
||||||
|
|
||||||
@ -70,21 +68,21 @@ void main() {
|
|||||||
final String projectPath = await createProject(tempDir,
|
final String projectPath = await createProject(tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app']);
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
final BuildApkCommand commandDefault = await runCommandIn(projectPath);
|
final BuildApkCommand commandDefault = await runBuildApkCommand(projectPath);
|
||||||
expect(await commandDefault.usageValues,
|
expect(await commandDefault.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkBuildMode, 'release'));
|
containsPair(CustomDimensions.commandBuildApkBuildMode, 'release'));
|
||||||
|
|
||||||
final BuildApkCommand commandInRelease = await runCommandIn(projectPath,
|
final BuildApkCommand commandInRelease = await runBuildApkCommand(projectPath,
|
||||||
arguments: <String>['--release']);
|
arguments: <String>['--release']);
|
||||||
expect(await commandInRelease.usageValues,
|
expect(await commandInRelease.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkBuildMode, 'release'));
|
containsPair(CustomDimensions.commandBuildApkBuildMode, 'release'));
|
||||||
|
|
||||||
final BuildApkCommand commandInDebug = await runCommandIn(projectPath,
|
final BuildApkCommand commandInDebug = await runBuildApkCommand(projectPath,
|
||||||
arguments: <String>['--debug']);
|
arguments: <String>['--debug']);
|
||||||
expect(await commandInDebug.usageValues,
|
expect(await commandInDebug.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkBuildMode, 'debug'));
|
containsPair(CustomDimensions.commandBuildApkBuildMode, 'debug'));
|
||||||
|
|
||||||
final BuildApkCommand commandInProfile = await runCommandIn(projectPath,
|
final BuildApkCommand commandInProfile = await runBuildApkCommand(projectPath,
|
||||||
arguments: <String>['--profile']);
|
arguments: <String>['--profile']);
|
||||||
expect(await commandInProfile.usageValues,
|
expect(await commandInProfile.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildApkBuildMode, 'profile'));
|
containsPair(CustomDimensions.commandBuildApkBuildMode, 'profile'));
|
||||||
@ -93,4 +91,201 @@ void main() {
|
|||||||
AndroidBuilder: () => FakeAndroidBuilder(),
|
AndroidBuilder: () => FakeAndroidBuilder(),
|
||||||
}, timeout: allowForCreateFlutterProject);
|
}, timeout: allowForCreateFlutterProject);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Gradle', () {
|
||||||
|
Directory tempDir;
|
||||||
|
ProcessManager mockProcessManager;
|
||||||
|
String gradlew;
|
||||||
|
AndroidSdk mockAndroidSdk;
|
||||||
|
Usage mockUsage;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockUsage = MockUsage();
|
||||||
|
when(mockUsage.isFirstRun).thenReturn(true);
|
||||||
|
|
||||||
|
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
|
||||||
|
gradlew = fs.path.join(tempDir.path, 'flutter_project', 'android',
|
||||||
|
platform.isWindows ? 'gradlew.bat' : 'gradlew');
|
||||||
|
|
||||||
|
mockProcessManager = MockProcessManager();
|
||||||
|
when(mockProcessManager.run(<String>[gradlew, '-v'],
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, '', '')));
|
||||||
|
|
||||||
|
when(mockProcessManager.run(<String>[gradlew, 'app:properties'],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'buildDir: irrelevant', '')));
|
||||||
|
|
||||||
|
when(mockProcessManager.run(<String>[gradlew, 'app:tasks', '--all', '--console=auto'],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'assembleRelease', '')));
|
||||||
|
// Fallback with error.
|
||||||
|
final Process process = createMockProcess(exitCode: 1);
|
||||||
|
when(mockProcessManager.start(any,
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<Process>.value(process));
|
||||||
|
when(mockProcessManager.canRun(any)).thenReturn(false);
|
||||||
|
|
||||||
|
mockAndroidSdk = MockAndroidSdk();
|
||||||
|
when(mockAndroidSdk.directory).thenReturn('irrelevant');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
tryToDelete(tempDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('proguard is enabled by default on release mode', () async {
|
||||||
|
final String projectPath = await createProject(tempDir,
|
||||||
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
|
await expectLater(() async {
|
||||||
|
await runBuildApkCommand(projectPath);
|
||||||
|
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
||||||
|
|
||||||
|
verify(mockProcessManager.start(
|
||||||
|
<String>[
|
||||||
|
gradlew,
|
||||||
|
'-q',
|
||||||
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
|
'-Ptrack-widget-creation=false',
|
||||||
|
'-Pproguard=true',
|
||||||
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
|
'assembleRelease',
|
||||||
|
],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment'),
|
||||||
|
)).called(1);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
AndroidSdk: () => mockAndroidSdk,
|
||||||
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
|
ProcessManager: () => mockProcessManager,
|
||||||
|
},
|
||||||
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
|
testUsingContext('proguard is disabled when --no-proguard is passed', () async {
|
||||||
|
final String projectPath = await createProject(tempDir,
|
||||||
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
|
await expectLater(() async {
|
||||||
|
await runBuildApkCommand(
|
||||||
|
projectPath,
|
||||||
|
arguments: <String>['--no-proguard'],
|
||||||
|
);
|
||||||
|
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
||||||
|
|
||||||
|
verify(mockProcessManager.start(
|
||||||
|
<String>[
|
||||||
|
gradlew,
|
||||||
|
'-q',
|
||||||
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
|
'-Ptrack-widget-creation=false',
|
||||||
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
|
'assembleRelease',
|
||||||
|
],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment'),
|
||||||
|
)).called(1);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
AndroidSdk: () => mockAndroidSdk,
|
||||||
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
|
ProcessManager: () => mockProcessManager,
|
||||||
|
},
|
||||||
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
|
testUsingContext('guides the user when proguard fails', () async {
|
||||||
|
final String projectPath = await createProject(tempDir,
|
||||||
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
|
when(mockProcessManager.start(
|
||||||
|
<String>[
|
||||||
|
gradlew,
|
||||||
|
'-q',
|
||||||
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
|
'-Ptrack-widget-creation=false',
|
||||||
|
'-Pproguard=true',
|
||||||
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
|
'assembleRelease',
|
||||||
|
],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment'),
|
||||||
|
)).thenAnswer((_) {
|
||||||
|
const String proguardStdoutWarning =
|
||||||
|
'Warning: there were 6 unresolved references to program class members.'
|
||||||
|
'Your input classes appear to be inconsistent.'
|
||||||
|
'You may need to recompile the code.'
|
||||||
|
'(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)';
|
||||||
|
return Future<Process>.value(
|
||||||
|
createMockProcess(
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: proguardStdoutWarning,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectLater(() async {
|
||||||
|
await runBuildApkCommand(
|
||||||
|
projectPath,
|
||||||
|
);
|
||||||
|
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
||||||
|
|
||||||
|
expect(testLogger.statusText,
|
||||||
|
contains('Proguard may have failed to optimize the Java bytecode.'));
|
||||||
|
expect(testLogger.statusText,
|
||||||
|
contains('To disable proguard, pass the `--no-proguard` flag to this command.'));
|
||||||
|
expect(testLogger.statusText,
|
||||||
|
contains('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard'));
|
||||||
|
|
||||||
|
verify(mockUsage.sendEvent(
|
||||||
|
'build-apk',
|
||||||
|
'proguard-failure',
|
||||||
|
parameters: anyNamed('parameters'),
|
||||||
|
)).called(1);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
AndroidSdk: () => mockAndroidSdk,
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
||||||
|
ProcessManager: () => mockProcessManager,
|
||||||
|
Usage: () => mockUsage,
|
||||||
|
},
|
||||||
|
timeout: allowForCreateFlutterProject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<BuildApkCommand> runBuildApkCommand(
|
||||||
|
String target,
|
||||||
|
{ List<String> arguments }
|
||||||
|
) async {
|
||||||
|
final BuildApkCommand command = BuildApkCommand();
|
||||||
|
final CommandRunner<void> runner = createTestCommandRunner(command);
|
||||||
|
await runner.run(<String>[
|
||||||
|
'apk',
|
||||||
|
...?arguments,
|
||||||
|
fs.path.join(target, 'lib', 'main.dart'),
|
||||||
|
]);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeFlutterProjectFactory extends FlutterProjectFactory {
|
||||||
|
FakeFlutterProjectFactory(this.directoryOverride) :
|
||||||
|
assert(directoryOverride != null);
|
||||||
|
|
||||||
|
final Directory directoryOverride;
|
||||||
|
|
||||||
|
@override
|
||||||
|
FlutterProject fromDirectory(Directory _) {
|
||||||
|
return super.fromDirectory(directoryOverride.childDirectory('flutter_project'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockAndroidSdk extends Mock implements AndroidSdk {}
|
||||||
|
class MockProcessManager extends Mock implements ProcessManager {}
|
||||||
|
class MockProcess extends Mock implements Process {}
|
||||||
|
class MockUsage extends Mock implements Usage {}
|
||||||
|
@ -2,15 +2,24 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
import 'package:flutter_tools/src/android/android_builder.dart';
|
import 'package:flutter_tools/src/android/android_builder.dart';
|
||||||
|
import 'package:flutter_tools/src/android/android_sdk.dart';
|
||||||
|
import 'package:flutter_tools/src/android/gradle.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/commands/build_appbundle.dart';
|
import 'package:flutter_tools/src/commands/build_appbundle.dart';
|
||||||
|
import 'package:flutter_tools/src/project.dart';
|
||||||
import 'package:flutter_tools/src/reporting/reporting.dart';
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/context.dart';
|
import '../../src/context.dart';
|
||||||
|
import '../../src/mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Cache.disableLocking();
|
Cache.disableLocking();
|
||||||
@ -26,21 +35,10 @@ void main() {
|
|||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<BuildAppBundleCommand> runCommandIn(String target, { List<String> arguments }) async {
|
|
||||||
final BuildAppBundleCommand command = BuildAppBundleCommand();
|
|
||||||
final CommandRunner<void> runner = createTestCommandRunner(command);
|
|
||||||
await runner.run(<String>[
|
|
||||||
'appbundle',
|
|
||||||
...?arguments,
|
|
||||||
fs.path.join(target, 'lib', 'main.dart'),
|
|
||||||
]);
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
testUsingContext('indicate the default target platforms', () async {
|
testUsingContext('indicate the default target platforms', () async {
|
||||||
final String projectPath = await createProject(tempDir,
|
final String projectPath = await createProject(tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app']);
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
final BuildAppBundleCommand command = await runCommandIn(projectPath);
|
final BuildAppBundleCommand command = await runBuildAppBundleCommand(projectPath);
|
||||||
|
|
||||||
expect(await command.usageValues,
|
expect(await command.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildAppBundleTargetPlatform, 'android-arm,android-arm64'));
|
containsPair(CustomDimensions.commandBuildAppBundleTargetPlatform, 'android-arm,android-arm64'));
|
||||||
@ -53,21 +51,21 @@ void main() {
|
|||||||
final String projectPath = await createProject(tempDir,
|
final String projectPath = await createProject(tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app']);
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
final BuildAppBundleCommand commandDefault = await runCommandIn(projectPath);
|
final BuildAppBundleCommand commandDefault = await runBuildAppBundleCommand(projectPath);
|
||||||
expect(await commandDefault.usageValues,
|
expect(await commandDefault.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release'));
|
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release'));
|
||||||
|
|
||||||
final BuildAppBundleCommand commandInRelease = await runCommandIn(projectPath,
|
final BuildAppBundleCommand commandInRelease = await runBuildAppBundleCommand(projectPath,
|
||||||
arguments: <String>['--release']);
|
arguments: <String>['--release']);
|
||||||
expect(await commandInRelease.usageValues,
|
expect(await commandInRelease.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release'));
|
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release'));
|
||||||
|
|
||||||
final BuildAppBundleCommand commandInDebug = await runCommandIn(projectPath,
|
final BuildAppBundleCommand commandInDebug = await runBuildAppBundleCommand(projectPath,
|
||||||
arguments: <String>['--debug']);
|
arguments: <String>['--debug']);
|
||||||
expect(await commandInDebug.usageValues,
|
expect(await commandInDebug.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'debug'));
|
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'debug'));
|
||||||
|
|
||||||
final BuildAppBundleCommand commandInProfile = await runCommandIn(projectPath,
|
final BuildAppBundleCommand commandInProfile = await runBuildAppBundleCommand(projectPath,
|
||||||
arguments: <String>['--profile']);
|
arguments: <String>['--profile']);
|
||||||
expect(await commandInProfile.usageValues,
|
expect(await commandInProfile.usageValues,
|
||||||
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'profile'));
|
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'profile'));
|
||||||
@ -76,4 +74,207 @@ void main() {
|
|||||||
AndroidBuilder: () => FakeAndroidBuilder(),
|
AndroidBuilder: () => FakeAndroidBuilder(),
|
||||||
}, timeout: allowForCreateFlutterProject);
|
}, timeout: allowForCreateFlutterProject);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Flags', () {
|
||||||
|
Directory tempDir;
|
||||||
|
ProcessManager mockProcessManager;
|
||||||
|
MockAndroidSdk mockAndroidSdk;
|
||||||
|
String gradlew;
|
||||||
|
Usage mockUsage;
|
||||||
|
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockUsage = MockUsage();
|
||||||
|
when(mockUsage.isFirstRun).thenReturn(true);
|
||||||
|
|
||||||
|
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
|
||||||
|
gradlew = fs.path.join(tempDir.path, 'flutter_project', 'android',
|
||||||
|
platform.isWindows ? 'gradlew.bat' : 'gradlew');
|
||||||
|
|
||||||
|
mockProcessManager = MockProcessManager();
|
||||||
|
when(mockProcessManager.run(<String>[gradlew, '-v'],
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, '', '')));
|
||||||
|
|
||||||
|
when(mockProcessManager.run(<String>[gradlew, 'app:properties'],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'buildDir: irrelevant', '')));
|
||||||
|
|
||||||
|
when(mockProcessManager.run(<String>[gradlew, 'app:tasks', '--all', '--console=auto'],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'assembleRelease', '')));
|
||||||
|
// Fallback with error.
|
||||||
|
final Process process = createMockProcess(exitCode: 1);
|
||||||
|
when(mockProcessManager.start(any,
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment')))
|
||||||
|
.thenAnswer((_) => Future<Process>.value(process));
|
||||||
|
when(mockProcessManager.canRun(any)).thenReturn(false);
|
||||||
|
|
||||||
|
mockAndroidSdk = MockAndroidSdk();
|
||||||
|
when(mockAndroidSdk.validateSdkWellFormed()).thenReturn(const <String>[]);
|
||||||
|
when(mockAndroidSdk.directory).thenReturn('irrelevant');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
tryToDelete(tempDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('proguard is enabled by default on release mode', () async {
|
||||||
|
final String projectPath = await createProject(
|
||||||
|
tempDir,
|
||||||
|
arguments: <String>['--no-pub', '--template=app'],
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(() async {
|
||||||
|
await runBuildAppBundleCommand(projectPath);
|
||||||
|
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
||||||
|
|
||||||
|
verify(mockProcessManager.start(
|
||||||
|
<String>[
|
||||||
|
gradlew,
|
||||||
|
'-q',
|
||||||
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
|
'-Ptrack-widget-creation=false',
|
||||||
|
'-Pproguard=true',
|
||||||
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
|
'bundleRelease',
|
||||||
|
],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment'),
|
||||||
|
)).called(1);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
AndroidSdk: () => mockAndroidSdk,
|
||||||
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
|
ProcessManager: () => mockProcessManager,
|
||||||
|
},
|
||||||
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
|
testUsingContext('proguard is disabled when --no-proguard is passed', () async {
|
||||||
|
final String projectPath = await createProject(
|
||||||
|
tempDir,
|
||||||
|
arguments: <String>['--no-pub', '--template=app'],
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(() async {
|
||||||
|
await runBuildAppBundleCommand(
|
||||||
|
projectPath,
|
||||||
|
arguments: <String>['--no-proguard'],
|
||||||
|
);
|
||||||
|
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
||||||
|
|
||||||
|
verify(mockProcessManager.start(
|
||||||
|
<String>[
|
||||||
|
gradlew,
|
||||||
|
'-q',
|
||||||
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
|
'-Ptrack-widget-creation=false',
|
||||||
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
|
'bundleRelease',
|
||||||
|
],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment'),
|
||||||
|
)).called(1);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
AndroidSdk: () => mockAndroidSdk,
|
||||||
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
|
ProcessManager: () => mockProcessManager,
|
||||||
|
},
|
||||||
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
|
testUsingContext('guides the user when proguard fails', () async {
|
||||||
|
final String projectPath = await createProject(tempDir,
|
||||||
|
arguments: <String>['--no-pub', '--template=app']);
|
||||||
|
|
||||||
|
when(mockProcessManager.start(
|
||||||
|
<String>[
|
||||||
|
gradlew,
|
||||||
|
'-q',
|
||||||
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
|
'-Ptrack-widget-creation=false',
|
||||||
|
'-Pproguard=true',
|
||||||
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
|
'bundleRelease',
|
||||||
|
],
|
||||||
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
|
environment: anyNamed('environment'),
|
||||||
|
)).thenAnswer((_) {
|
||||||
|
const String proguardStdoutWarning =
|
||||||
|
'Warning: there were 6 unresolved references to program class members.'
|
||||||
|
'Your input classes appear to be inconsistent.'
|
||||||
|
'You may need to recompile the code.'
|
||||||
|
'(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)';
|
||||||
|
return Future<Process>.value(
|
||||||
|
createMockProcess(
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: proguardStdoutWarning,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectLater(() async {
|
||||||
|
await runBuildAppBundleCommand(
|
||||||
|
projectPath,
|
||||||
|
);
|
||||||
|
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
||||||
|
|
||||||
|
expect(testLogger.statusText,
|
||||||
|
contains('Proguard may have failed to optimize the Java bytecode.'));
|
||||||
|
expect(testLogger.statusText,
|
||||||
|
contains('To disable proguard, pass the `--no-proguard` flag to this command.'));
|
||||||
|
expect(testLogger.statusText,
|
||||||
|
contains('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard'));
|
||||||
|
|
||||||
|
verify(mockUsage.sendEvent(
|
||||||
|
'build-appbundle',
|
||||||
|
'proguard-failure',
|
||||||
|
parameters: anyNamed('parameters'),
|
||||||
|
)).called(1);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
AndroidSdk: () => mockAndroidSdk,
|
||||||
|
GradleUtils: () => GradleUtils(),
|
||||||
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
||||||
|
ProcessManager: () => mockProcessManager,
|
||||||
|
Usage: () => mockUsage,
|
||||||
|
},
|
||||||
|
timeout: allowForCreateFlutterProject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<BuildAppBundleCommand> runBuildAppBundleCommand(
|
||||||
|
String target,
|
||||||
|
{ List<String> arguments }
|
||||||
|
) async {
|
||||||
|
final BuildAppBundleCommand command = BuildAppBundleCommand();
|
||||||
|
final CommandRunner<void> runner = createTestCommandRunner(command);
|
||||||
|
await runner.run(<String>[
|
||||||
|
'appbundle',
|
||||||
|
...?arguments,
|
||||||
|
fs.path.join(target, 'lib', 'main.dart'),
|
||||||
|
]);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeFlutterProjectFactory extends FlutterProjectFactory {
|
||||||
|
FakeFlutterProjectFactory(this._directoryOverride) :
|
||||||
|
assert(_directoryOverride != null);
|
||||||
|
|
||||||
|
final Directory _directoryOverride;
|
||||||
|
|
||||||
|
@override
|
||||||
|
FlutterProject fromDirectory(Directory _) {
|
||||||
|
return super.fromDirectory(_directoryOverride.childDirectory('flutter_project'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockAndroidSdk extends Mock implements AndroidSdk {}
|
||||||
|
class MockProcessManager extends Mock implements ProcessManager {}
|
||||||
|
class MockProcess extends Mock implements Process {}
|
||||||
|
class MockUsage extends Mock implements Usage {}
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter_tools/src/base/common.dart';
|
import 'package:flutter_tools/src/base/common.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart';
|
import 'package:flutter_tools/src/base/io.dart';
|
||||||
@ -17,21 +15,7 @@ import 'package:process/process.dart';
|
|||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/context.dart';
|
import '../../src/context.dart';
|
||||||
|
import '../../src/mocks.dart';
|
||||||
Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) {
|
|
||||||
final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
|
|
||||||
utf8.encode(stdout),
|
|
||||||
]);
|
|
||||||
final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
|
|
||||||
utf8.encode(stderr),
|
|
||||||
]);
|
|
||||||
final Process process = MockProcess();
|
|
||||||
|
|
||||||
when(process.stdout).thenAnswer((_) => stdoutStream);
|
|
||||||
when(process.stderr).thenAnswer((_) => stderrStream);
|
|
||||||
when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
|
|
||||||
return process;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('UpgradeCommandRunner', () {
|
group('UpgradeCommandRunner', () {
|
||||||
|
@ -116,6 +116,8 @@ Future<String> createProject(Directory temp, { List<String> arguments }) async {
|
|||||||
final CreateCommand command = CreateCommand();
|
final CreateCommand command = CreateCommand();
|
||||||
final CommandRunner<void> runner = createTestCommandRunner(command);
|
final CommandRunner<void> runner = createTestCommandRunner(command);
|
||||||
await runner.run(<String>['create', ...arguments, projectPath]);
|
await runner.run(<String>['create', ...arguments, projectPath]);
|
||||||
|
// Created `.packages` since it's not created when the flag `--no-pub` is passed.
|
||||||
|
fs.file(fs.path.join(projectPath, '.packages')).createSync();
|
||||||
return projectPath;
|
return projectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,6 +221,24 @@ ProcessFactory flakyProcessFactory({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a mock process that returns with the given [exitCode], [stdout] and [stderr].
|
||||||
|
Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) {
|
||||||
|
final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
|
||||||
|
utf8.encode(stdout),
|
||||||
|
]);
|
||||||
|
final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
|
||||||
|
utf8.encode(stderr),
|
||||||
|
]);
|
||||||
|
final Process process = MockBasicProcess();
|
||||||
|
|
||||||
|
when(process.stdout).thenAnswer((_) => stdoutStream);
|
||||||
|
when(process.stderr).thenAnswer((_) => stderrStream);
|
||||||
|
when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockBasicProcess extends Mock implements Process {}
|
||||||
|
|
||||||
/// A process that exits successfully with no output and ignores all input.
|
/// A process that exits successfully with no output and ignores all input.
|
||||||
class MockProcess extends Mock implements Process {
|
class MockProcess extends Mock implements Process {
|
||||||
MockProcess({
|
MockProcess({
|
||||||
|
Loading…
Reference in New Issue
Block a user