From 08fdf99fea9d950f00d6a1594a2d6ffda097598d Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Thu, 17 Dec 2015 14:22:29 -0800 Subject: [PATCH] Add a 'flutter ios --init' command that fetches the Xcode project from the cloud and configures it for a given flutter project --- packages/flutter_tools/lib/executable.dart | 2 + .../flutter_tools/lib/src/commands/ios.dart | 155 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 packages/flutter_tools/lib/src/commands/ios.dart diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 9a2846649cc..2b9eeb1e731 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -17,6 +17,7 @@ import 'src/commands/cache.dart'; import 'src/commands/daemon.dart'; import 'src/commands/init.dart'; import 'src/commands/install.dart'; +import 'src/commands/ios.dart'; import 'src/commands/list.dart'; import 'src/commands/listen.dart'; import 'src/commands/logs.dart'; @@ -62,6 +63,7 @@ Future main(List args) async { ..addCommand(new DaemonCommand()) ..addCommand(new InitCommand()) ..addCommand(new InstallCommand()) + ..addCommand(new IOSCommand()) ..addCommand(new ListCommand()) ..addCommand(new ListenCommand()) ..addCommand(new LogsCommand()) diff --git a/packages/flutter_tools/lib/src/commands/ios.dart b/packages/flutter_tools/lib/src/commands/ios.dart new file mode 100644 index 00000000000..3943402bae8 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/ios.dart @@ -0,0 +1,155 @@ +// Copyright 2015 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 "../runner/flutter_command_runner.dart"; +import "../runner/flutter_command.dart"; + +class IOSCommand extends FlutterCommand { + final String name = "ios"; + + final String description = "Commands for creating and updating Flutter iOS projects"; + + final bool requiresProjectRoot = true; + + IOSCommand() { + argParser.addFlag('init', help: 'Initialize the Xcode project for building the iOS application'); + } + + static Uri _xcodeProjectUri(String revision) { + return Uri.parse("https://storage.googleapis.com/flutter_infra/flutter/${revision}/ios/FlutterXcode.zip"); + } + + Future> _fetchXcodeArchive() async { + print("Fetching the Xcode project archive from the cloud..."); + + HttpClient client = new HttpClient(); + // TODO(chinmaygarde): Currently, engine releases are not tied to the release of the Xcode project + // archive. So use a "Current" tag. This will need to be replaced once releases + // are in lockstep with the engine + HttpClientRequest request = await client.getUrl(_xcodeProjectUri("Current")); + HttpClientResponse response = await request.close(); + + if (response.statusCode != 200) + throw new Exception(response.reasonPhrase); + + BytesBuilder bytesBuilder = new BytesBuilder(copy: false); + await for (List chunk in response) + bytesBuilder.add(chunk); + + return bytesBuilder.takeBytes(); + } + + Future _inflateXcodeArchive(String directory, List archiveBytes) async { + print("Unzipping Xcode project to local directory..."); + + if (archiveBytes.isEmpty) + return false; + + // We cannot use ArchiveFile because this archive contains file that are exectuable + // and there is currently no provision to modify file permissions during + // or after creation. See https://github.com/dart-lang/sdk/issues/15078 + // So we depend on the platform to unzip the archive for us. + + Directory tempDir = await Directory.systemTemp.create(); + File tempFile = await new File(path.join(tempDir.path, "FlutterXcode.zip")).create(); + IOSink writeSink = tempFile.openWrite(); + writeSink.add(archiveBytes); + await writeSink.close(); + + ProcessResult result = await Process.run('/usr/bin/unzip', [tempFile.path, '-d', directory]); + + // Cleanup the temp directory after unzipping + await Process.run('/bin/rm', ['rf', tempDir.path]); + + if (result.exitCode != 0) + return false; + + Directory flutterDir = new Directory(path.join(directory, 'Flutter')); + bool flutterDirExists = await flutterDir.exists(); + if (!flutterDirExists) + return false; + + // Move contents of the Flutter directory one level up + // There is no dart:io API to do this. See https://github.com/dart-lang/sdk/issues/8148 + + bool moveFailed = false; + for (FileSystemEntity file in flutterDir.listSync()) { + ProcessResult result = await Process.run('/bin/mv', [file.path, directory]); + moveFailed = result.exitCode != 0; + if (moveFailed) + break; + } + + ProcessResult rmResult = await Process.run('/bin/rm', ["-rf", flutterDir.path]); + + return !moveFailed && rmResult.exitCode == 0; + } + + Future _setupXcodeProjXcconfig(String filePath) async { + StringBuffer localsBuffer = new StringBuffer(); + + localsBuffer.writeln("// Generated. Do not edit or check into version control!!"); + localsBuffer.writeln("// Recreate using `flutter ios`"); + + String flutterRoot = path.normalize(Platform.environment[kFlutterRootEnvironmentVariableName]); + localsBuffer.writeln("FLUTTER_ROOT=${flutterRoot}"); + + // This holds because requiresProjectRoot is true for this command + String applicationRoot = path.normalize(Directory.current.path); + localsBuffer.writeln("FLUTTER_APPLICATION_PATH=${applicationRoot}"); + + String dartSDKPath = path.normalize(path.join(Platform.resolvedExecutable, "..", "..")); + localsBuffer.writeln("DART_SDK_PATH=${dartSDKPath}"); + + File localsFile = new File(filePath); + await localsFile.create(recursive: true); + await localsFile.writeAsString(localsBuffer.toString()); + + return true; + } + + Future _runInitCommand() async { + // Step 1: Fetch the archive from the cloud + String xcodeprojPath = path.join(Directory.current.path, "ios"); + List archiveBytes = await _fetchXcodeArchive(); + + // Step 2: Inflate the archive into the user project directory + bool result = await _inflateXcodeArchive(xcodeprojPath, archiveBytes); + if (!result) { + print("Could not fetch the Xcode project from the cloud..."); + return -1; + } + + // Step 3: Populate the Local.xcconfig with project specific paths + result = await _setupXcodeProjXcconfig(path.join(xcodeprojPath, "Local.xcconfig")); + if (!result) { + print("Could not setup local properties file"); + return -1; + } + + // Step 4: Launch Xcode and let the user edit plist, resources, provisioning, etc. + print("Launching project in Xcode..."); + ProcessResult launch = await Process.run("/usr/bin/open", ["ios/FlutterApplication.xcodeproj"]); + return launch.exitCode; + } + + @override + Future runInProject() async { + if (!Platform.isMacOS) { + print("iOS specific commands may only be run on a Mac."); + return -1; + } + + if (argResults['init']) + return _runInitCommand(); + + print("No flags specified..."); + return -1; + } +}