mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Enable R8 (#40453)
This commit is contained in:
parent
e3c4609a5e
commit
2c857b9370
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
@ -159,8 +159,12 @@ class FlutterPlugin implements Plugin<Project> {
|
|||||||
"gradle", "flutter_proguard_rules.pro")
|
"gradle", "flutter_proguard_rules.pro")
|
||||||
project.android.buildTypes {
|
project.android.buildTypes {
|
||||||
release {
|
release {
|
||||||
|
// Enables code shrinking, obfuscation, and optimization for only
|
||||||
|
// your project's release build type.
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
useProguard true
|
// Enables resource shrinking, which is performed by the
|
||||||
|
// Android Gradle plugin.
|
||||||
|
shrinkResources true
|
||||||
// Fallback to `android/app/proguard-rules.pro`.
|
// Fallback to `android/app/proguard-rules.pro`.
|
||||||
// This way, custom Proguard rules can be configured as needed.
|
// This way, custom Proguard rules can be configured as needed.
|
||||||
proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
|
proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
|
||||||
|
@ -306,6 +306,10 @@ Future<String> _initializeGradle(FlutterProject project) async {
|
|||||||
final Status status = logger.startProgress('Initializing gradle...',
|
final Status status = logger.startProgress('Initializing gradle...',
|
||||||
timeout: timeoutConfiguration.slowOperation);
|
timeout: timeoutConfiguration.slowOperation);
|
||||||
|
|
||||||
|
|
||||||
|
// Update the project if needed.
|
||||||
|
// TODO(egarciad): https://github.com/flutter/flutter/issues/40460.
|
||||||
|
migrateToR8(android);
|
||||||
injectGradleWrapperIfNeeded(android);
|
injectGradleWrapperIfNeeded(android);
|
||||||
|
|
||||||
final String gradle = _locateGradlewExecutable(android);
|
final String gradle = _locateGradlewExecutable(android);
|
||||||
@ -335,6 +339,31 @@ Future<String> _initializeGradle(FlutterProject project) async {
|
|||||||
return gradle;
|
return gradle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Migrates the Android's [directory] to R8.
|
||||||
|
/// https://developer.android.com/studio/build/shrink-code
|
||||||
|
@visibleForTesting
|
||||||
|
void migrateToR8(Directory directory) {
|
||||||
|
final File gradleProperties = directory.childFile('gradle.properties');
|
||||||
|
if (!gradleProperties.existsSync()) {
|
||||||
|
throwToolExit('Expected file ${gradleProperties.path}.');
|
||||||
|
}
|
||||||
|
final String propertiesContent = gradleProperties.readAsStringSync();
|
||||||
|
if (propertiesContent.contains('android.enableR8')) {
|
||||||
|
printTrace('gradle.properties already sets `android.enableR8`');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printTrace('set `android.enableR8=true` in gradle.properties');
|
||||||
|
try {
|
||||||
|
gradleProperties
|
||||||
|
.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append);
|
||||||
|
} on FileSystemException {
|
||||||
|
throwToolExit(
|
||||||
|
'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. '
|
||||||
|
'Please update the file manually and try this command again.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Injects the Gradle wrapper files if any of these files don't exist in [directory].
|
/// Injects the Gradle wrapper files if any of these files don't exist in [directory].
|
||||||
void injectGradleWrapperIfNeeded(Directory directory) {
|
void injectGradleWrapperIfNeeded(Directory directory) {
|
||||||
copyDirectorySync(
|
copyDirectorySync(
|
||||||
@ -754,8 +783,8 @@ Future<void> _buildGradleProjectV2(
|
|||||||
if (androidBuildInfo.splitPerAbi) {
|
if (androidBuildInfo.splitPerAbi) {
|
||||||
command.add('-Psplit-per-abi=true');
|
command.add('-Psplit-per-abi=true');
|
||||||
}
|
}
|
||||||
if (androidBuildInfo.proguard) {
|
if (androidBuildInfo.shrink) {
|
||||||
command.add('-Pproguard=true');
|
command.add('-Pshrink=true');
|
||||||
}
|
}
|
||||||
if (androidBuildInfo.targetArchs.isNotEmpty) {
|
if (androidBuildInfo.targetArchs.isNotEmpty) {
|
||||||
final String targetPlatforms = androidBuildInfo.targetArchs
|
final String targetPlatforms = androidBuildInfo.targetArchs
|
||||||
@ -772,7 +801,7 @@ Future<void> _buildGradleProjectV2(
|
|||||||
}
|
}
|
||||||
command.add(assembleTask);
|
command.add(assembleTask);
|
||||||
bool potentialAndroidXFailure = false;
|
bool potentialAndroidXFailure = false;
|
||||||
bool potentialProguardFailure = false;
|
bool potentialR8Failure = false;
|
||||||
final Stopwatch sw = Stopwatch()..start();
|
final Stopwatch sw = Stopwatch()..start();
|
||||||
int exitCode = 1;
|
int exitCode = 1;
|
||||||
try {
|
try {
|
||||||
@ -789,10 +818,10 @@ Future<void> _buildGradleProjectV2(
|
|||||||
if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) {
|
if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) {
|
||||||
potentialAndroidXFailure = true;
|
potentialAndroidXFailure = true;
|
||||||
}
|
}
|
||||||
// Proguard errors include this url.
|
// R8 errors include references to this package.
|
||||||
if (!potentialProguardFailure && androidBuildInfo.proguard &&
|
if (!potentialR8Failure && androidBuildInfo.shrink &&
|
||||||
line.contains('http://proguard.sourceforge.net')) {
|
line.contains('com.android.tools.r8')) {
|
||||||
potentialProguardFailure = true;
|
potentialR8Failure = true;
|
||||||
}
|
}
|
||||||
// Always print the full line in verbose mode.
|
// Always print the full line in verbose mode.
|
||||||
if (logger.isVerbose) {
|
if (logger.isVerbose) {
|
||||||
@ -808,12 +837,12 @@ Future<void> _buildGradleProjectV2(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0) {
|
||||||
if (potentialProguardFailure) {
|
if (potentialR8Failure) {
|
||||||
final String exclamationMark = terminal.color('[!]', TerminalColor.red);
|
final String exclamationMark = terminal.color('[!]', TerminalColor.red);
|
||||||
printStatus('$exclamationMark Proguard may have failed to optimize the Java bytecode.', emphasis: true);
|
printStatus('$exclamationMark The shrinker 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 disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4);
|
||||||
printStatus('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard', indent: 4);
|
printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4);
|
||||||
BuildEvent('proguard-failure').send();
|
BuildEvent('r8-failure').send();
|
||||||
} else if (potentialAndroidXFailure) {
|
} 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,7 +92,7 @@ class AndroidBuildInfo {
|
|||||||
AndroidArch.arm64_v8a,
|
AndroidArch.arm64_v8a,
|
||||||
],
|
],
|
||||||
this.splitPerAbi = false,
|
this.splitPerAbi = false,
|
||||||
this.proguard = false,
|
this.shrink = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The build info containing the mode and flavor.
|
// The build info containing the mode and flavor.
|
||||||
@ -105,8 +105,8 @@ class AndroidBuildInfo {
|
|||||||
/// will be produced.
|
/// will be produced.
|
||||||
final bool splitPerAbi;
|
final bool splitPerAbi;
|
||||||
|
|
||||||
/// Whether to enable Proguard on release mode.
|
/// Whether to enable code shrinking on release mode.
|
||||||
final bool proguard;
|
final bool shrink;
|
||||||
|
|
||||||
/// The target platforms for the build.
|
/// The target platforms for the build.
|
||||||
final Iterable<AndroidArch> targetArchs;
|
final Iterable<AndroidArch> targetArchs;
|
||||||
|
@ -21,6 +21,7 @@ class BuildApkCommand extends BuildSubCommand {
|
|||||||
usesPubOption();
|
usesPubOption();
|
||||||
usesBuildNumberOption();
|
usesBuildNumberOption();
|
||||||
usesBuildNameOption();
|
usesBuildNameOption();
|
||||||
|
addShrinkingFlag();
|
||||||
|
|
||||||
argParser
|
argParser
|
||||||
..addFlag('split-per-abi',
|
..addFlag('split-per-abi',
|
||||||
@ -28,12 +29,6 @@ class BuildApkCommand extends BuildSubCommand {
|
|||||||
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: false,
|
|
||||||
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'],
|
||||||
@ -83,10 +78,11 @@ class BuildApkCommand extends BuildSubCommand {
|
|||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
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'],
|
shrink: argResults['shrink'],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) {
|
if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) {
|
||||||
|
@ -19,15 +19,10 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
|||||||
usesPubOption();
|
usesPubOption();
|
||||||
usesBuildNumberOption();
|
usesBuildNumberOption();
|
||||||
usesBuildNameOption();
|
usesBuildNameOption();
|
||||||
|
addShrinkingFlag();
|
||||||
|
|
||||||
argParser
|
argParser
|
||||||
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
|
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
|
||||||
..addFlag('proguard',
|
|
||||||
negatable: true,
|
|
||||||
defaultsTo: false,
|
|
||||||
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'],
|
||||||
@ -70,7 +65,7 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
|||||||
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'],
|
shrink: argResults['shrink'],
|
||||||
);
|
);
|
||||||
await androidBuilder.buildAab(
|
await androidBuilder.buildAab(
|
||||||
project: FlutterProject.current(),
|
project: FlutterProject.current(),
|
||||||
|
@ -277,6 +277,19 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
help: 'Build a release version of your app${defaultToRelease ? ' (default mode)' : ''}.');
|
help: 'Build a release version of your app${defaultToRelease ? ' (default mode)' : ''}.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addShrinkingFlag() {
|
||||||
|
argParser.addFlag('shrink',
|
||||||
|
negatable: true,
|
||||||
|
defaultsTo: true,
|
||||||
|
help: 'Whether to enable code shrinking on release mode.'
|
||||||
|
'When enabling shrinking, you also benefit from obfuscation, '
|
||||||
|
'which shortens the names of your app’s classes and members, '
|
||||||
|
'and optimization, which applies more aggressive strategies to '
|
||||||
|
'further reduce the size of your app.'
|
||||||
|
'To learn more, see: https://developer.android.com/studio/build/shrink-code'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void usesFuchsiaOptions({ bool hide = false }) {
|
void usesFuchsiaOptions({ bool hide = false }) {
|
||||||
argParser.addOption(
|
argParser.addOption(
|
||||||
'target-model',
|
'target-model',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
{{#androidX}}
|
{{#androidX}}
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
{{#androidX}}
|
{{#androidX}}
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
{{#androidX}}
|
{{#androidX}}
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter_tools/src/android/android_sdk.dart';
|
import 'package:flutter_tools/src/android/android_sdk.dart';
|
||||||
@ -13,6 +12,7 @@ import 'package:flutter_tools/src/base/logger.dart';
|
|||||||
import 'package:flutter_tools/src/artifacts.dart';
|
import 'package:flutter_tools/src/artifacts.dart';
|
||||||
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/os.dart';
|
import 'package:flutter_tools/src/base/os.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.dart';
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
import 'package:flutter_tools/src/build_info.dart';
|
import 'package:flutter_tools/src/build_info.dart';
|
||||||
@ -867,6 +867,13 @@ flutter:
|
|||||||
gradleWrapperDirectory
|
gradleWrapperDirectory
|
||||||
.childFile(gradleBinary)
|
.childFile(gradleBinary)
|
||||||
.writeAsStringSync('irrelevant');
|
.writeAsStringSync('irrelevant');
|
||||||
|
fs.currentDirectory
|
||||||
|
.childDirectory('android')
|
||||||
|
.createSync();
|
||||||
|
fs.currentDirectory
|
||||||
|
.childDirectory('android')
|
||||||
|
.childFile('gradle.properties')
|
||||||
|
.writeAsStringSync('irrelevant');
|
||||||
gradleWrapperDirectory
|
gradleWrapperDirectory
|
||||||
.childDirectory('gradle')
|
.childDirectory('gradle')
|
||||||
.childDirectory('wrapper')
|
.childDirectory('wrapper')
|
||||||
@ -1072,6 +1079,82 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('migrateToR8', () {
|
||||||
|
MemoryFileSystem memoryFileSystem;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
memoryFileSystem = MemoryFileSystem();
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('throws ToolExit if gradle.properties doesn\'t exist', () {
|
||||||
|
final Directory sampleAppAndroid = fs.directory('/sample-app/android');
|
||||||
|
sampleAppAndroid.createSync(recursive: true);
|
||||||
|
|
||||||
|
expect(() {
|
||||||
|
migrateToR8(sampleAppAndroid);
|
||||||
|
}, throwsToolExit(message: 'Expected file ${sampleAppAndroid.path}'));
|
||||||
|
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => memoryFileSystem,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('throws ToolExit if it cannot write gradle.properties', () {
|
||||||
|
final MockDirectory sampleAppAndroid = MockDirectory();
|
||||||
|
final MockFile gradleProperties = MockFile();
|
||||||
|
|
||||||
|
when(gradleProperties.path).thenReturn('foo/gradle.properties');
|
||||||
|
when(gradleProperties.existsSync()).thenReturn(true);
|
||||||
|
when(gradleProperties.readAsStringSync()).thenReturn('');
|
||||||
|
when(gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append))
|
||||||
|
.thenThrow(const FileSystemException());
|
||||||
|
|
||||||
|
when(sampleAppAndroid.childFile('gradle.properties'))
|
||||||
|
.thenReturn(gradleProperties);
|
||||||
|
|
||||||
|
expect(() {
|
||||||
|
migrateToR8(sampleAppAndroid);
|
||||||
|
},
|
||||||
|
throwsToolExit(message:
|
||||||
|
'The tool failed to add `android.enableR8=true` to foo/gradle.properties. '
|
||||||
|
'Please update the file manually and try this command again.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('does not update gradle.properties if it already uses R8', () {
|
||||||
|
final Directory sampleAppAndroid = fs.directory('/sample-app/android');
|
||||||
|
sampleAppAndroid.createSync(recursive: true);
|
||||||
|
sampleAppAndroid.childFile('gradle.properties')
|
||||||
|
.writeAsStringSync('android.enableR8=true');
|
||||||
|
|
||||||
|
migrateToR8(sampleAppAndroid);
|
||||||
|
|
||||||
|
expect(testLogger.traceText,
|
||||||
|
contains('gradle.properties already sets `android.enableR8`'));
|
||||||
|
expect(sampleAppAndroid.childFile('gradle.properties').readAsStringSync(),
|
||||||
|
equals('android.enableR8=true'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => memoryFileSystem,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('sets android.enableR8=true', () {
|
||||||
|
final Directory sampleAppAndroid = fs.directory('/sample-app/android');
|
||||||
|
sampleAppAndroid.createSync(recursive: true);
|
||||||
|
sampleAppAndroid.childFile('gradle.properties')
|
||||||
|
.writeAsStringSync('org.gradle.jvmargs=-Xmx1536M\n');
|
||||||
|
|
||||||
|
migrateToR8(sampleAppAndroid);
|
||||||
|
|
||||||
|
expect(testLogger.traceText, contains('set `android.enableR8=true` in gradle.properties'));
|
||||||
|
expect(sampleAppAndroid.childFile('gradle.properties').readAsStringSync(),
|
||||||
|
equals(
|
||||||
|
'org.gradle.jvmargs=-Xmx1536M\n'
|
||||||
|
'android.enableR8=true\n'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => memoryFileSystem,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('gradle build', () {
|
group('gradle build', () {
|
||||||
MockAndroidSdk mockAndroidSdk;
|
MockAndroidSdk mockAndroidSdk;
|
||||||
MockAndroidStudio mockAndroidStudio;
|
MockAndroidStudio mockAndroidStudio;
|
||||||
@ -1136,6 +1219,9 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
|
|||||||
final File gradlew = fs.file('path/to/project/.android/gradlew');
|
final File gradlew = fs.file('path/to/project/.android/gradlew');
|
||||||
gradlew.createSync(recursive: true);
|
gradlew.createSync(recursive: true);
|
||||||
|
|
||||||
|
fs.file('path/to/project/.android/gradle.properties')
|
||||||
|
.writeAsStringSync('irrelevant');
|
||||||
|
|
||||||
when(mockProcessManager.run(
|
when(mockProcessManager.run(
|
||||||
<String> ['/path/to/project/.android/gradlew', '-v'],
|
<String> ['/path/to/project/.android/gradlew', '-v'],
|
||||||
workingDirectory: anyNamed('workingDirectory'),
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
@ -1198,9 +1284,11 @@ Platform fakePlatform(String name) {
|
|||||||
return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
|
return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockAndroidStudio extends Mock implements AndroidStudio {}
|
||||||
|
class MockDirectory extends Mock implements Directory {}
|
||||||
|
class MockFile extends Mock implements File {}
|
||||||
|
class MockGradleProject extends Mock implements GradleProject {}
|
||||||
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
|
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
|
||||||
class MockProcessManager extends Mock implements ProcessManager {}
|
class MockProcessManager extends Mock implements ProcessManager {}
|
||||||
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
|
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
|
||||||
class MockGradleProject extends Mock implements GradleProject {}
|
|
||||||
class MockitoAndroidSdk extends Mock implements AndroidSdk {}
|
class MockitoAndroidSdk extends Mock implements AndroidSdk {}
|
||||||
class MockAndroidStudio extends Mock implements AndroidStudio {}
|
|
||||||
|
@ -103,6 +103,10 @@ void main() {
|
|||||||
platform.isWindows ? 'gradlew.bat' : 'gradlew',
|
platform.isWindows ? 'gradlew.bat' : 'gradlew',
|
||||||
)..createSync(recursive: true);
|
)..createSync(recursive: true);
|
||||||
|
|
||||||
|
project.android.hostAppGradleRoot
|
||||||
|
.childFile('gradle.properties')
|
||||||
|
.writeAsStringSync('irrelevant');
|
||||||
|
|
||||||
final Directory gradleWrapperDir = fs.systemTempDirectory.createTempSync('gradle_wrapper.');
|
final Directory gradleWrapperDir = fs.systemTempDirectory.createTempSync('gradle_wrapper.');
|
||||||
when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir);
|
when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir);
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ void main() {
|
|||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('proguard is enabled by default on release mode', () async {
|
testUsingContext('shrinking is enabled by default on release mode', () 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']);
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ void main() {
|
|||||||
'-q',
|
'-q',
|
||||||
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
'-Ptrack-widget-creation=false',
|
'-Ptrack-widget-creation=false',
|
||||||
'-Pproguard=true',
|
'-Pshrink=true',
|
||||||
'-Ptarget-platform=android-arm,android-arm64',
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
'assembleRelease',
|
'assembleRelease',
|
||||||
],
|
],
|
||||||
@ -165,17 +165,16 @@ void main() {
|
|||||||
GradleUtils: () => GradleUtils(),
|
GradleUtils: () => GradleUtils(),
|
||||||
ProcessManager: () => mockProcessManager,
|
ProcessManager: () => mockProcessManager,
|
||||||
},
|
},
|
||||||
skip: true,
|
|
||||||
timeout: allowForCreateFlutterProject);
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
testUsingContext('proguard is disabled when --no-proguard is passed', () async {
|
testUsingContext('shrinking is disabled when --no-shrink is passed', () 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']);
|
||||||
|
|
||||||
await expectLater(() async {
|
await expectLater(() async {
|
||||||
await runBuildApkCommand(
|
await runBuildApkCommand(
|
||||||
projectPath,
|
projectPath,
|
||||||
arguments: <String>['--no-proguard'],
|
arguments: <String>['--no-shrink'],
|
||||||
);
|
);
|
||||||
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
||||||
|
|
||||||
@ -198,10 +197,9 @@ void main() {
|
|||||||
GradleUtils: () => GradleUtils(),
|
GradleUtils: () => GradleUtils(),
|
||||||
ProcessManager: () => mockProcessManager,
|
ProcessManager: () => mockProcessManager,
|
||||||
},
|
},
|
||||||
skip: true,
|
|
||||||
timeout: allowForCreateFlutterProject);
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
testUsingContext('guides the user when proguard fails', () async {
|
testUsingContext('guides the user when the shrinker fails', () 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']);
|
||||||
|
|
||||||
@ -211,22 +209,20 @@ void main() {
|
|||||||
'-q',
|
'-q',
|
||||||
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
'-Ptrack-widget-creation=false',
|
'-Ptrack-widget-creation=false',
|
||||||
'-Pproguard=true',
|
'-Pshrink=true',
|
||||||
'-Ptarget-platform=android-arm,android-arm64',
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
'assembleRelease',
|
'assembleRelease',
|
||||||
],
|
],
|
||||||
workingDirectory: anyNamed('workingDirectory'),
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
environment: anyNamed('environment'),
|
environment: anyNamed('environment'),
|
||||||
)).thenAnswer((_) {
|
)).thenAnswer((_) {
|
||||||
const String proguardStdoutWarning =
|
const String r8StdoutWarning =
|
||||||
'Warning: there were 6 unresolved references to program class members.'
|
'Execution failed for task \':app:transformClassesAndResourcesWithR8ForStageInternal\'.'
|
||||||
'Your input classes appear to be inconsistent.'
|
'> com.android.tools.r8.CompilationFailedException: Compilation failed to complete';
|
||||||
'You may need to recompile the code.'
|
|
||||||
'(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)';
|
|
||||||
return Future<Process>.value(
|
return Future<Process>.value(
|
||||||
createMockProcess(
|
createMockProcess(
|
||||||
exitCode: 1,
|
exitCode: 1,
|
||||||
stdout: proguardStdoutWarning,
|
stdout: r8StdoutWarning,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -238,15 +234,15 @@ void main() {
|
|||||||
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
||||||
|
|
||||||
expect(testLogger.statusText,
|
expect(testLogger.statusText,
|
||||||
contains('Proguard may have failed to optimize the Java bytecode.'));
|
contains('The shrinker may have failed to optimize the Java bytecode.'));
|
||||||
expect(testLogger.statusText,
|
expect(testLogger.statusText,
|
||||||
contains('To disable proguard, pass the `--no-proguard` flag to this command.'));
|
contains('To disable the shrinker, pass the `--no-shrink` flag to this command.'));
|
||||||
expect(testLogger.statusText,
|
expect(testLogger.statusText,
|
||||||
contains('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard'));
|
contains('To learn more, see: https://developer.android.com/studio/build/shrink-code'));
|
||||||
|
|
||||||
verify(mockUsage.sendEvent(
|
verify(mockUsage.sendEvent(
|
||||||
'build-apk',
|
'build-apk',
|
||||||
'proguard-failure',
|
'r8-failure',
|
||||||
parameters: anyNamed('parameters'),
|
parameters: anyNamed('parameters'),
|
||||||
)).called(1);
|
)).called(1);
|
||||||
},
|
},
|
||||||
@ -257,7 +253,6 @@ void main() {
|
|||||||
ProcessManager: () => mockProcessManager,
|
ProcessManager: () => mockProcessManager,
|
||||||
Usage: () => mockUsage,
|
Usage: () => mockUsage,
|
||||||
},
|
},
|
||||||
skip: true,
|
|
||||||
timeout: allowForCreateFlutterProject);
|
timeout: allowForCreateFlutterProject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ void main() {
|
|||||||
}, timeout: allowForCreateFlutterProject);
|
}, timeout: allowForCreateFlutterProject);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Flags', () {
|
group('Gradle', () {
|
||||||
Directory tempDir;
|
Directory tempDir;
|
||||||
ProcessManager mockProcessManager;
|
ProcessManager mockProcessManager;
|
||||||
MockAndroidSdk mockAndroidSdk;
|
MockAndroidSdk mockAndroidSdk;
|
||||||
@ -122,7 +122,7 @@ void main() {
|
|||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('proguard is enabled by default on release mode', () async {
|
testUsingContext('shrinking is enabled by default on release mode', () async {
|
||||||
final String projectPath = await createProject(
|
final String projectPath = await createProject(
|
||||||
tempDir,
|
tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app'],
|
arguments: <String>['--no-pub', '--template=app'],
|
||||||
@ -138,7 +138,7 @@ void main() {
|
|||||||
'-q',
|
'-q',
|
||||||
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
'-Ptrack-widget-creation=false',
|
'-Ptrack-widget-creation=false',
|
||||||
'-Pproguard=true',
|
'-Pshrink=true',
|
||||||
'-Ptarget-platform=android-arm,android-arm64',
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
'bundleRelease',
|
'bundleRelease',
|
||||||
],
|
],
|
||||||
@ -152,10 +152,9 @@ void main() {
|
|||||||
GradleUtils: () => GradleUtils(),
|
GradleUtils: () => GradleUtils(),
|
||||||
ProcessManager: () => mockProcessManager,
|
ProcessManager: () => mockProcessManager,
|
||||||
},
|
},
|
||||||
skip: true,
|
|
||||||
timeout: allowForCreateFlutterProject);
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
testUsingContext('proguard is disabled when --no-proguard is passed', () async {
|
testUsingContext('shrinking is disabled when --no-shrink is passed', () async {
|
||||||
final String projectPath = await createProject(
|
final String projectPath = await createProject(
|
||||||
tempDir,
|
tempDir,
|
||||||
arguments: <String>['--no-pub', '--template=app'],
|
arguments: <String>['--no-pub', '--template=app'],
|
||||||
@ -164,7 +163,7 @@ void main() {
|
|||||||
await expectLater(() async {
|
await expectLater(() async {
|
||||||
await runBuildAppBundleCommand(
|
await runBuildAppBundleCommand(
|
||||||
projectPath,
|
projectPath,
|
||||||
arguments: <String>['--no-proguard'],
|
arguments: <String>['--no-shrink'],
|
||||||
);
|
);
|
||||||
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
||||||
|
|
||||||
@ -187,10 +186,9 @@ void main() {
|
|||||||
GradleUtils: () => GradleUtils(),
|
GradleUtils: () => GradleUtils(),
|
||||||
ProcessManager: () => mockProcessManager,
|
ProcessManager: () => mockProcessManager,
|
||||||
},
|
},
|
||||||
skip: true,
|
|
||||||
timeout: allowForCreateFlutterProject);
|
timeout: allowForCreateFlutterProject);
|
||||||
|
|
||||||
testUsingContext('guides the user when proguard fails', () async {
|
testUsingContext('guides the user when the shrinker fails', () 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']);
|
||||||
|
|
||||||
@ -200,22 +198,20 @@ void main() {
|
|||||||
'-q',
|
'-q',
|
||||||
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
'-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
||||||
'-Ptrack-widget-creation=false',
|
'-Ptrack-widget-creation=false',
|
||||||
'-Pproguard=true',
|
'-Pshrink=true',
|
||||||
'-Ptarget-platform=android-arm,android-arm64',
|
'-Ptarget-platform=android-arm,android-arm64',
|
||||||
'bundleRelease',
|
'bundleRelease',
|
||||||
],
|
],
|
||||||
workingDirectory: anyNamed('workingDirectory'),
|
workingDirectory: anyNamed('workingDirectory'),
|
||||||
environment: anyNamed('environment'),
|
environment: anyNamed('environment'),
|
||||||
)).thenAnswer((_) {
|
)).thenAnswer((_) {
|
||||||
const String proguardStdoutWarning =
|
const String r8StdoutWarning =
|
||||||
'Warning: there were 6 unresolved references to program class members.'
|
'Execution failed for task \':app:transformClassesAndResourcesWithR8ForStageInternal\'.'
|
||||||
'Your input classes appear to be inconsistent.'
|
'> com.android.tools.r8.CompilationFailedException: Compilation failed to complete';
|
||||||
'You may need to recompile the code.'
|
|
||||||
'(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)';
|
|
||||||
return Future<Process>.value(
|
return Future<Process>.value(
|
||||||
createMockProcess(
|
createMockProcess(
|
||||||
exitCode: 1,
|
exitCode: 1,
|
||||||
stdout: proguardStdoutWarning,
|
stdout: r8StdoutWarning,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -227,15 +223,15 @@ void main() {
|
|||||||
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
|
||||||
|
|
||||||
expect(testLogger.statusText,
|
expect(testLogger.statusText,
|
||||||
contains('Proguard may have failed to optimize the Java bytecode.'));
|
contains('The shrinker may have failed to optimize the Java bytecode.'));
|
||||||
expect(testLogger.statusText,
|
expect(testLogger.statusText,
|
||||||
contains('To disable proguard, pass the `--no-proguard` flag to this command.'));
|
contains('To disable the shrinker, pass the `--no-shrink` flag to this command.'));
|
||||||
expect(testLogger.statusText,
|
expect(testLogger.statusText,
|
||||||
contains('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard'));
|
contains('To learn more, see: https://developer.android.com/studio/build/shrink-code'));
|
||||||
|
|
||||||
verify(mockUsage.sendEvent(
|
verify(mockUsage.sendEvent(
|
||||||
'build-appbundle',
|
'build-appbundle',
|
||||||
'proguard-failure',
|
'r8-failure',
|
||||||
parameters: anyNamed('parameters'),
|
parameters: anyNamed('parameters'),
|
||||||
)).called(1);
|
)).called(1);
|
||||||
},
|
},
|
||||||
@ -246,7 +242,6 @@ void main() {
|
|||||||
ProcessManager: () => mockProcessManager,
|
ProcessManager: () => mockProcessManager,
|
||||||
Usage: () => mockUsage,
|
Usage: () => mockUsage,
|
||||||
},
|
},
|
||||||
skip: true,
|
|
||||||
timeout: allowForCreateFlutterProject);
|
timeout: allowForCreateFlutterProject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user