// 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"; import "../artifacts.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) { String uriString = "https://storage.googleapis.com/flutter_infra/flutter/$revision/ios/FlutterXcode.zip"; print("Downloading $uriString ..."); return Uri.parse(uriString); } Future<List<int>> _fetchXcodeArchive() async { print("Fetching the Xcode project archive from the cloud..."); HttpClient client = new HttpClient(); HttpClientRequest request = await client.getUrl(_xcodeProjectUri(ArtifactStore.engineRevision)); HttpClientResponse response = await request.close(); if (response.statusCode != 200) throw new Exception(response.reasonPhrase); BytesBuilder bytesBuilder = new BytesBuilder(copy: false); await for (List<int> chunk in response) bytesBuilder.add(chunk); return bytesBuilder.takeBytes(); } Future<bool> _inflateXcodeArchive(String directory, List<int> 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<bool> _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<int> _runInitCommand() async { // Step 1: Fetch the archive from the cloud String xcodeprojPath = path.join(Directory.current.path, "ios"); List<int> 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<int> runInProject() async { if (!Platform.isMacOS) { print("iOS specific commands may only be run on a Mac."); return -1; } if (argResults['init']) return await _runInitCommand(); print("No flags specified..."); return -1; } }