Hello services run (#4969)

* making flutter run work with gradle

* locate android studio

* add test for settings

* review comments
This commit is contained in:
Devon Carew 2016-07-19 20:00:02 -07:00 committed by GitHub
parent 932059b901
commit 57b76a050f
13 changed files with 443 additions and 39 deletions

View file

@ -4,7 +4,7 @@
android:versionCode="1"
android:versionName="1.0.0" >
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

View file

@ -5,7 +5,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.flutter.examples.HelloWorld" android:versionCode="1" android:versionName="0.0.1">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="Flutter Hello" android:name="org.domokit.sky.shell.SkyApplication">

View file

@ -0,0 +1,188 @@
// 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 'dart:io';
import 'package:path/path.dart' as path;
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 gradleAppOut = 'android/app/build/outputs/apk/app-debug.apk';
bool isProjectUsingGradle() {
return FileSystemEntity.isFileSync('android/build.gradle');
}
String locateSystemGradle({ bool ensureExecutable: true }) {
String gradle = _locateSystemGradle();
if (ensureExecutable && gradle != null) {
File file = new 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 (FileSystemEntity.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 (FileSystemEntity.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 = new 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 (FileSystemEntity.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 (FileSystemEntity.isFileSync(path)) {
if (ensureExecutable)
os.makeExecutable(new File(path));
return path;
} else {
return null;
}
}
Future<int> buildGradleProject(BuildMode buildMode) async {
// Create android/local.properties.
File localProperties = new File('android/local.properties');
if (!localProperties.existsSync()) {
localProperties.writeAsStringSync(
'sdk.dir=${androidSdk.directory}\n'
'flutter.sdk=${Cache.flutterRoot}\n'
);
}
// Update the local.settings file with the build mode.
// TODO(devoncarew): It would be nicer if we could pass this information in via a cli flag.
SettingsFile settings = new SettingsFile.parseFromFile(localProperties);
settings.values['flutter.buildMode'] = getModeName(buildMode);
settings.writeContents(localProperties);
String gradlew = locateProjectGradlew();
if (gradlew == null) {
String gradle = locateSystemGradle();
if (gradle == null) {
printError(
'Unable to locate gradle. Please configure the path to gradle using \'flutter config --gradle\'.'
);
return 1;
} else {
printTrace('Using gradle from $gradle.');
}
// Stamp the android/app/build.gradle file with the current android sdk and build tools version.
File appGradleFile = new 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'.
Status status = logger.startProgress('Running \'gradle wrapper\'...');
int exitcode = await runCommandAndStreamOutput(
<String>[gradle, 'wrapper'],
workingDirectory: 'android',
allowReentrantFlutter: true
);
status.stop(showElapsedTime: true);
if (exitcode != 0)
return exitcode;
gradlew = locateProjectGradlew();
if (gradlew == null) {
printError('Unable to build android/gradlew.');
return 1;
}
}
// Run 'gradlew build'.
Status status = logger.startProgress('Running \'gradlew build\'...');
int exitcode = await runCommandAndStreamOutput(
<String>[new File('android/gradlew').absolute.path, 'build'],
workingDirectory: 'android',
allowReentrantFlutter: true
);
status.stop(showElapsedTime: true);
if (exitcode == 0) {
File apkFile = new File(gradleAppOut);
printStatus('Built $gradleAppOut (${getSizeAsMB(apkFile.lengthSync())}).');
}
return exitcode;
}
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);
}
}

View file

@ -7,6 +7,7 @@ import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:xml/xml.dart' as xml;
import 'android/gradle.dart';
import 'build_info.dart';
import 'ios/plist_utils.dart';
@ -49,9 +50,20 @@ class AndroidApk extends ApplicationPackage {
/// Creates a new AndroidApk based on the information in the Android manifest.
factory AndroidApk.fromCurrentDirectory() {
String manifestPath = path.join('android', 'AndroidManifest.xml');
String manifestPath;
String apkPath;
if (isProjectUsingGradle()) {
manifestPath = gradleManifestPath;
apkPath = gradleAppOut;
} else {
manifestPath = path.join('android', 'AndroidManifest.xml');
apkPath = path.join('build', 'app.apk');
}
if (!FileSystemEntity.isFileSync(manifestPath))
return null;
String manifestString = new File(manifestPath).readAsStringSync();
xml.XmlDocument document = xml.parse(manifestString);
@ -75,7 +87,7 @@ class AndroidApk extends ApplicationPackage {
return new AndroidApk(
buildDir: 'build',
id: id,
apkPath: path.join('build', 'app.apk'),
apkPath: apkPath,
launchActivity: launchActivity
);
}

View file

@ -0,0 +1,49 @@
// 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:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'context.dart';
class Config {
Config([File configFile]) {
_configFile = configFile ?? new File(path.join(_userHomeDir(), '.flutter_settings'));
if (_configFile.existsSync())
_values = JSON.decode(_configFile.readAsStringSync());
}
static Config get instance => context[Config] ?? (context[Config] = new Config());
File _configFile;
Map<String, dynamic> _values = <String, dynamic>{};
Iterable<String> get keys => _values.keys;
dynamic getValue(String key) => _values[key];
void setValue(String key, String value) {
_values[key] = value;
_flushValues();
}
void removeValue(String key) {
_values.remove(key);
_flushValues();
}
void _flushValues() {
String json = new JsonEncoder.withIndent(' ').convert(_values);
json = '$json\n';
_configFile.writeAsStringSync(json);
}
}
String _userHomeDir() {
String envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
String value = Platform.environment[envKey];
return value == null ? '.' : value;
}

View file

@ -127,3 +127,28 @@ class ItemListNotifier<T> {
_removedController.close();
}
}
class SettingsFile {
SettingsFile.parse(String contents) {
for (String line in contents.split('\n')) {
line = line.trim();
if (line.startsWith('#') || line.isEmpty)
continue;
int index = line.indexOf('=');
if (index != -1)
values[line.substring(0, index)] = line.substring(index + 1);
}
}
factory SettingsFile.parseFromFile(File file) {
return new SettingsFile.parse(file.readAsStringSync());
}
final Map<String, String> values = <String, String>{};
void writeContents(File file) {
file.writeAsStringSync(values.keys.map((String key) {
return '$key=${values[key]}';
}).join('\n'));
}
}

View file

@ -9,6 +9,7 @@ import 'dart:io';
import 'package:path/path.dart' as path;
import '../android/android_sdk.dart';
import '../android/gradle.dart';
import '../base/file_system.dart' show ensureDirectoryExists;
import '../base/logger.dart';
import '../base/os.dart';
@ -202,25 +203,32 @@ class BuildApkCommand extends FlutterCommand {
@override
Future<int> runInProject() async {
// TODO(devoncarew): This command should take an arg for the output type (arm / x64).
return await buildAndroid(
TargetPlatform.android_arm,
getBuildMode(),
force: true,
manifest: argResults['manifest'],
resources: argResults['resources'],
outputFile: argResults['output-file'],
target: argResults['target'],
flxPath: argResults['flx'],
aotPath: argResults['aot-path'],
keystore: (argResults['keystore'] ?? '').isEmpty ? null : new ApkKeystoreInfo(
keystore: argResults['keystore'],
password: argResults['keystore-password'],
keyAlias: argResults['keystore-key-alias'],
keyPassword: argResults['keystore-key-password']
)
);
if (isProjectUsingGradle()) {
return await buildAndroidWithGradle(
TargetPlatform.android_arm,
getBuildMode(),
target: argResults['target']
);
} else {
// TODO(devoncarew): This command should take an arg for the output type (arm / x64).
return await buildAndroid(
TargetPlatform.android_arm,
getBuildMode(),
force: true,
manifest: argResults['manifest'],
resources: argResults['resources'],
outputFile: argResults['output-file'],
target: argResults['target'],
flxPath: argResults['flx'],
aotPath: argResults['aot-path'],
keystore: (argResults['keystore'] ?? '').isEmpty ? null : new ApkKeystoreInfo(
keystore: argResults['keystore'],
password: argResults['keystore-password'],
keyAlias: argResults['keystore-key-alias'],
keyPassword: argResults['keystore-key-password']
)
);
}
}
}
@ -556,24 +564,53 @@ Future<int> buildAndroid(
return result;
}
Future<int> buildAndroidWithGradle(
TargetPlatform platform,
BuildMode buildMode, {
bool force: false,
String target
}) async {
// Validate that we can find an android sdk.
if (androidSdk == null) {
printError('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
return 1;
}
List<String> validationResult = androidSdk.validateSdkWellFormed();
if (validationResult.isNotEmpty) {
validationResult.forEach(printError);
printError('Try re-installing or updating your Android SDK.');
return 1;
}
return buildGradleProject(buildMode);
}
Future<int> buildApk(
TargetPlatform platform, {
String target,
BuildMode buildMode: BuildMode.debug
}) async {
if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) {
printError('Cannot build APK: missing $_kDefaultAndroidManifestPath.');
return 1;
if (isProjectUsingGradle()) {
return await buildAndroidWithGradle(
platform,
buildMode,
force: false,
target: target
);
} else {
if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) {
printError('Cannot build APK: missing $_kDefaultAndroidManifestPath.');
return 1;
}
return await buildAndroid(
platform,
buildMode,
force: false,
target: target
);
}
int result = await buildAndroid(
platform,
buildMode,
force: false,
target: target
);
return result;
}
Map<String, dynamic> _readBuildMeta(String buildDirectoryPath) {

View file

@ -12,6 +12,8 @@ class ConfigCommand extends FlutterCommand {
argParser.addFlag('analytics',
negatable: true,
help: 'Enable or disable reporting anonymously tool usage statistics and crash reports.');
argParser.addOption('gradle-dir', help: 'The gradle install directory.');
argParser.addOption('android-studio-dir', help: 'The Android Studio install directory.');
}
@override
@ -27,7 +29,17 @@ class ConfigCommand extends FlutterCommand {
final List<String> aliases = <String>['configure'];
@override
String get usageFooter => 'Analytics reporting is currently ${flutterUsage.enabled ? 'enabled' : 'disabled'}.';
String get usageFooter {
// List all config settings.
String values = config.keys.map((String key) {
return ' $key: ${config.getValue(key)}';
}).join('\n');
if (values.isNotEmpty)
values = '\nSettings:\n$values\n\n';
return
'$values'
'Analytics reporting is currently ${flutterUsage.enabled ? 'enabled' : 'disabled'}.';
}
@override
bool get requiresProjectRoot => false;
@ -42,10 +54,27 @@ class ConfigCommand extends FlutterCommand {
bool value = argResults['analytics'];
flutterUsage.enabled = value;
printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
} else {
printStatus(usage);
}
if (argResults.wasParsed('gradle-dir'))
_updateConfig('gradle-dir', argResults['gradle-dir']);
if (argResults.wasParsed('android-studio-dir'))
_updateConfig('android-studio-dir', argResults['android-studio-dir']);
if (argResults.arguments.isEmpty)
printStatus(usage);
return 0;
}
void _updateConfig(String keyName, String keyValue) {
if (keyValue.isEmpty) {
config.removeValue(keyName);
printStatus('Removing "$keyName" value.');
} else {
config.setValue(keyName, keyValue);
printStatus('Setting "$keyName" value to "$keyValue".');
}
}
}

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'android/android_sdk.dart';
import 'base/config.dart';
import 'base/context.dart';
import 'base/logger.dart';
import 'cache.dart';
@ -15,6 +16,7 @@ DeviceManager get deviceManager => context[DeviceManager];
Logger get logger => context[Logger];
AndroidSdk get androidSdk => context[AndroidSdk];
Cache get cache => Cache.instance;
Config get config => Config.instance;
Doctor get doctor => context[Doctor];
ToolConfiguration get tools => ToolConfiguration.instance;
Usage get flutterUsage => Usage.instance;

View file

@ -16,6 +16,7 @@ import 'analyze_test.dart' as analyze_test;
import 'android_device_test.dart' as android_device_test;
import 'android_sdk_test.dart' as android_sdk_test;
import 'base_utils_test.dart' as base_utils_test;
import 'config_test.dart' as config_test;
import 'context_test.dart' as context_test;
import 'create_test.dart' as create_test;
import 'daemon_test.dart' as daemon_test;
@ -33,6 +34,7 @@ import 'test_test.dart' as test_test;
import 'toolchain_test.dart' as toolchain_test;
import 'trace_test.dart' as trace_test;
import 'upgrade_test.dart' as upgrade_test;
import 'utils_test.dart' as utils_test;
void main() {
Cache.disableLocking();
@ -43,6 +45,7 @@ void main() {
android_device_test.main();
android_sdk_test.main();
base_utils_test.main();
config_test.main();
context_test.main();
create_test.main();
daemon_test.main();
@ -60,4 +63,5 @@ void main() {
toolchain_test.main();
trace_test.main();
upgrade_test.main();
utils_test.main();
}

View file

@ -0,0 +1,38 @@
// 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:io';
import 'package:flutter_tools/src/base/config.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
void main() {
Config config;
setUp(() {
Directory tempDiretory = Directory.systemTemp.createTempSync('flutter_test');
File file = new File(path.join(tempDiretory.path, '.settings'));
config = new Config(file);
});
group('config', () {
test('get set value', () async {
expect(config.getValue('foo'), null);
config.setValue('foo', 'bar');
expect(config.getValue('foo'), 'bar');
expect(config.keys, contains('foo'));
});
test('removeValue', () async {
expect(config.getValue('foo'), null);
config.setValue('foo', 'bar');
expect(config.getValue('foo'), 'bar');
expect(config.keys, contains('foo'));
config.removeValue('foo');
expect(config.getValue('foo'), null);
expect(config.keys, isNot(contains('foo')));
});
});
}

View file

@ -1,4 +1,3 @@
// 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.

View file

@ -0,0 +1,21 @@
// 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 'package:flutter_tools/src/base/utils.dart';
import 'package:test/test.dart';
void main() {
group('SettingsFile', () {
test('parse', () {
SettingsFile file = new SettingsFile.parse('''
# ignore comment
foo=bar
baz=qux
''');
expect(file.values['foo'], 'bar');
expect(file.values['baz'], 'qux');
expect(file.values, hasLength(2));
});
});
}