blob: 4e2d94192934887d3b7ddee3bc3047ac40d70342 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:path/path.dart' as path;
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart';
import 'android_sdk.dart';
const String gradleManifestPath = 'android/app/src/main/AndroidManifest.xml';
const String gradleAppOutV1 = 'android/app/build/outputs/apk/app-debug.apk';
const String gradleAppOutV2 = 'android/app/build/outputs/apk/app.apk';
const String gradleAppOutDir = 'android/app/build/outputs/apk';
enum FlutterPluginVersion {
none,
v1,
v2,
managed,
}
bool isProjectUsingGradle() {
return fs.isFileSync('android/build.gradle');
}
FlutterPluginVersion get flutterPluginVersion {
File plugin = fs.file('android/buildSrc/src/main/groovy/FlutterPlugin.groovy');
if (plugin.existsSync()) {
String packageLine = plugin.readAsLinesSync().skip(4).first;
if (packageLine == "package io.flutter.gradle") {
return FlutterPluginVersion.v2;
}
return FlutterPluginVersion.v1;
}
File appGradle = fs.file('android/app/build.gradle');
if (appGradle.existsSync()) {
for (String line in appGradle.readAsLinesSync()) {
if (line.contains(new RegExp(r"apply from: .*/flutter.gradle"))) {
return FlutterPluginVersion.managed;
}
}
}
return FlutterPluginVersion.none;
}
String get gradleAppOut {
switch (flutterPluginVersion) {
case FlutterPluginVersion.none:
// Fall through. Pretend we're v1, and just go with it.
case FlutterPluginVersion.v1:
return gradleAppOutV1;
case FlutterPluginVersion.managed:
// Fall through. The managed plugin matches plugin v2 for now.
case FlutterPluginVersion.v2:
return gradleAppOutV2;
}
return null;
}
String locateSystemGradle({ bool ensureExecutable: true }) {
String gradle = _locateSystemGradle();
if (ensureExecutable && gradle != null) {
File file = fs.file(gradle);
if (file.existsSync())
os.makeExecutable(file);
}
return gradle;
}
String _locateSystemGradle() {
// See if the user has explicitly configured gradle-dir.
String gradleDir = config.getValue('gradle-dir');
if (gradleDir != null) {
if (fs.isFileSync(gradleDir))
return gradleDir;
return path.join(gradleDir, 'bin', 'gradle');
}
// Look relative to Android Studio.
String studioPath = config.getValue('android-studio-dir');
if (studioPath == null && os.isMacOS) {
final String kDefaultMacPath = '/Applications/Android Studio.app';
if (fs.isDirectorySync(kDefaultMacPath))
studioPath = kDefaultMacPath;
}
if (studioPath != null) {
// '/Applications/Android Studio.app/Contents/gradle/gradle-2.10/bin/gradle'
if (os.isMacOS && !studioPath.endsWith('Contents'))
studioPath = path.join(studioPath, 'Contents');
Directory dir = fs.directory(path.join(studioPath, 'gradle'));
if (dir.existsSync()) {
// We find the first valid gradle directory.
for (FileSystemEntity entity in dir.listSync()) {
if (entity is Directory && path.basename(entity.path).startsWith('gradle-')) {
String executable = path.join(entity.path, 'bin', 'gradle');
if (fs.isFileSync(executable))
return executable;
}
}
}
}
// Use 'which'.
File file = os.which('gradle');
if (file != null)
return file.path;
// We couldn't locate gradle.
return null;
}
String locateProjectGradlew({ bool ensureExecutable: true }) {
final String path = 'android/gradlew';
if (fs.isFileSync(path)) {
if (ensureExecutable)
os.makeExecutable(fs.file(path));
return path;
} else {
return null;
}
}
Future<String> ensureGradlew() async {
String gradlew = locateProjectGradlew();
if (gradlew == null) {
String gradle = locateSystemGradle();
if (gradle == null) {
throwToolExit(
'Unable to locate gradle. Please configure the path to gradle using \'flutter config --gradle-dir\'.'
);
} else {
printTrace('Using gradle from $gradle.');
}
// Stamp the android/app/build.gradle file with the current android sdk and build tools version.
File appGradleFile = fs.file('android/app/build.gradle');
if (appGradleFile.existsSync()) {
_GradleFile gradleFile = new _GradleFile.parse(appGradleFile);
AndroidSdkVersion sdkVersion = androidSdk.latestVersion;
gradleFile.replace('compileSdkVersion', "${sdkVersion.sdkLevel}");
gradleFile.replace('buildToolsVersion', "'${sdkVersion.buildToolsVersionName}'");
gradleFile.writeContents(appGradleFile);
}
// Run 'gradle wrapper'.
List<String> command = logger.isVerbose
? <String>[gradle, 'wrapper']
: <String>[gradle, '-q', 'wrapper'];
try {
Status status = logger.startProgress('Running \'gradle wrapper\'...', expectSlowOperation: true);
int exitcode = await runCommandAndStreamOutput(
command,
workingDirectory: 'android',
allowReentrantFlutter: true
);
status.stop();
if (exitcode != 0)
throwToolExit('Gradle failed: $exitcode', exitCode: exitcode);
} catch (error) {
throwToolExit('$error');
}
gradlew = locateProjectGradlew();
if (gradlew == null)
throwToolExit('Unable to build android/gradlew.');
}
return gradlew;
}
Future<Null> buildGradleProject(BuildMode buildMode) async {
// Create android/local.properties.
File localProperties = fs.file('android/local.properties');
if (!localProperties.existsSync()) {
localProperties.writeAsStringSync(
'sdk.dir=${androidSdk.directory}\n'
'flutter.sdk=${Cache.flutterRoot}\n'
);
}
// Update the local.properties file with the build mode.
// FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
// uses the standard Android way to determine what to build, but we still
// update local.properties, in case we want to use it in the future.
String buildModeName = getModeName(buildMode);
SettingsFile settings = new SettingsFile.parseFromFile(localProperties);
settings.values['flutter.buildMode'] = buildModeName;
settings.writeContents(localProperties);
String gradlew = await ensureGradlew();
switch (flutterPluginVersion) {
case FlutterPluginVersion.none:
// Fall through. Pretend it's v1, and just go for it.
case FlutterPluginVersion.v1:
return buildGradleProjectV1(gradlew);
case FlutterPluginVersion.managed:
// Fall through. Managed plugin builds the same way as plugin v2.
case FlutterPluginVersion.v2:
return buildGradleProjectV2(gradlew, buildModeName);
}
}
Future<Null> buildGradleProjectV1(String gradlew) async {
// Run 'gradlew build'.
Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true);
int exitcode = await runCommandAndStreamOutput(
<String>[fs.file(gradlew).absolute.path, 'build'],
workingDirectory: 'android',
allowReentrantFlutter: true
);
status.stop();
if (exitcode != 0)
throwToolExit('Gradlew failed: $exitcode', exitCode: exitcode);
File apkFile = fs.file(gradleAppOutV1);
printStatus('Built $gradleAppOutV1 (${getSizeAsMB(apkFile.lengthSync())}).');
}
Future<Null> buildGradleProjectV2(String gradlew, String buildModeName) async {
String assembleTask = "assemble${toTitleCase(buildModeName)}";
// Run 'gradlew assemble<BuildMode>'.
Status status = logger.startProgress('Running \'gradlew $assembleTask\'...', expectSlowOperation: true);
String gradlewPath = fs.file(gradlew).absolute.path;
List<String> command = logger.isVerbose
? <String>[gradlewPath, assembleTask]
: <String>[gradlewPath, '-q', assembleTask];
int exitcode = await runCommandAndStreamOutput(
command,
workingDirectory: 'android',
allowReentrantFlutter: true
);
status.stop();
if (exitcode != 0)
throwToolExit('Gradlew failed: $exitcode', exitCode: exitcode);
String apkFilename = 'app-$buildModeName.apk';
File apkFile = fs.file('$gradleAppOutDir/$apkFilename');
// Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
apkFile.copySync('$gradleAppOutDir/app.apk');
printStatus('Built $apkFilename (${getSizeAsMB(apkFile.lengthSync())}).');
}
class _GradleFile {
_GradleFile.parse(File file) {
contents = file.readAsStringSync();
}
String contents;
void replace(String key, String newValue) {
// Replace 'ws key ws value' with the new value.
final RegExp regex = new RegExp('\\s+$key\\s+(\\S+)', multiLine: true);
Match match = regex.firstMatch(contents);
if (match != null) {
String oldValue = match.group(1);
int offset = match.end - oldValue.length;
contents = contents.substring(0, offset) + newValue + contents.substring(match.end);
}
}
void writeContents(File file) {
file.writeAsStringSync(contents);
}
}