// 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 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 command = logger.isVerbose ? [gradle, 'wrapper'] : [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 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 buildGradleProjectV1(String gradlew) async { // Run 'gradlew build'. Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true); int exitcode = await runCommandAndStreamOutput( [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 buildGradleProjectV2(String gradlew, String buildModeName) async { String assembleTask = "assemble${toTitleCase(buildModeName)}"; // Run 'gradlew assemble'. Status status = logger.startProgress('Running \'gradlew $assembleTask\'...', expectSlowOperation: true); String gradlewPath = fs.file(gradlew).absolute.path; List command = logger.isVerbose ? [gradlewPath, assembleTask] : [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); } }