Commit 5cebf70d authored by Chinmay Garde's avatar Chinmay Garde

`flutter start` initializes the Xcode project if the user has not already done so.

parent d38bfee6
......@@ -13,7 +13,7 @@ import '../android/android.dart' as android;
import '../artifacts.dart';
import '../base/globals.dart';
import '../dart/pub.dart';
import 'ios.dart';
import '../ios/initialize_xcode.dart';
class CreateCommand extends Command {
final String name = 'create';
......
......@@ -5,20 +5,9 @@
import "dart:async";
import "dart:io";
import "package:path/path.dart" as path;
import "../artifacts.dart";
import "../base/globals.dart";
import "../base/process.dart";
import "../runner/flutter_command.dart";
import "../runner/flutter_command_runner.dart";
/// A map from file path to file contents.
final Map<String, String> iosTemplateFiles = <String, String>{
'ios/Info.plist': _infoPlistInitialContents,
'ios/LaunchScreen.storyboard': _launchScreenInitialContents,
'ios/Assets.xcassets/AppIcon.appiconset/Contents.json': _iconAssetInitialContents
};
import "../ios/initialize_xcode.dart";
class IOSCommand extends FlutterCommand {
final String name = "ios";
......@@ -28,148 +17,6 @@ class IOSCommand extends FlutterCommand {
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";
return Uri.parse(uriString);
}
Future<List<int>> _fetchXcodeArchive() async {
printStatus("Fetching the Xcode project archive from the cloud...");
HttpClient client = new HttpClient();
Uri xcodeProjectUri = _xcodeProjectUri(ArtifactStore.engineRevision);
printStatus("Downloading $xcodeProjectUri...");
HttpClientRequest request = await client.getUrl(xcodeProjectUri);
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 {
printStatus("Unzipping Xcode project to local directory...");
// We cannot use ArchiveFile because this archive contains files 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 = new File(path.join(tempDir.path, "FlutterXcode.zip"))..createSync();
tempFile.writeAsBytesSync(archiveBytes);
try {
// Remove the old generated project if one is present
runCheckedSync(['/bin/rm', '-rf', directory]);
// Create the directory so unzip can write to it
runCheckedSync(['/bin/mkdir', '-p', directory]);
// Unzip the Xcode project into the new empty directory
runCheckedSync(['/usr/bin/unzip', tempFile.path, '-d', directory]);
} catch (error) {
return false;
}
// Cleanup the temp directory after unzipping
runSync(['/bin/rm', '-rf', tempDir.path]);
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
for (FileSystemEntity file in flutterDir.listSync()) {
try {
runCheckedSync(['/bin/mv', file.path, directory]);
} catch (error) {
return false;
}
}
runSync(['/bin/rm', '-rf', flutterDir.path]);
return true;
}
void _writeUserEditableFilesIfNecessary(String directory) {
iosTemplateFiles.forEach((String filePath, String contents) {
File file = new File(filePath);
if (!file.existsSync()) {
file.parent.createSync(recursive: true);
file.writeAsStringSync(contents);
printStatus('Created $filePath.');
}
});
}
void _setupXcodeProjXcconfig(String filePath) {
StringBuffer localsBuffer = new StringBuffer();
localsBuffer.writeln("// This is a generated file; do not edit or check into version control.");
localsBuffer.writeln("// Recreate using `flutter ios --init`.");
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);
localsFile.createSync(recursive: true);
localsFile.writeAsStringSync(localsBuffer.toString());
}
Future<int> _runInitCommand() async {
// Step 1: Fetch the archive from the cloud
String iosFilesPath = path.join(Directory.current.path, "ios");
String xcodeprojPath = path.join(iosFilesPath, ".generated");
List<int> archiveBytes = await _fetchXcodeArchive();
if (archiveBytes.isEmpty) {
printError("Error: No archive bytes received.");
return 1;
}
// Step 2: Inflate the archive into the user project directory
bool result = await _inflateXcodeArchive(xcodeprojPath, archiveBytes);
if (!result) {
printError("Could not inflate the Xcode project archive.");
return 1;
}
// Step 3: Setup default user editable files if this is the first run of
// the init command.
_writeUserEditableFilesIfNecessary(iosFilesPath);
// Step 4: Populate the Local.xcconfig with project specific paths
_setupXcodeProjXcconfig(path.join(xcodeprojPath, "Local.xcconfig"));
// Step 5: Write the REVISION file
File revisionFile = new File(path.join(xcodeprojPath, "REVISION"));
revisionFile.createSync();
revisionFile.writeAsStringSync(ArtifactStore.engineRevision);
// Step 6: Tell the user the location of the generated project.
printStatus("Xcode project created at $xcodeprojPath/.");
printStatus("User editable settings are in $iosFilesPath/.");
return 0;
}
@override
Future<int> runInProject() async {
if (!Platform.isMacOS) {
......@@ -178,165 +25,9 @@ class IOSCommand extends FlutterCommand {
}
if (argResults['init'])
return await _runInitCommand();
return await initializeXcodeProjectHarness();
printError("No flags specified.");
return 1;
}
}
final String _infoPlistInitialContents = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>Runner</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.runner.Runner</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Flutter</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
''';
final String _launchScreenInitialContents = '''
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
''';
final String _iconAssetInitialContents = '''
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
''';
......@@ -17,6 +17,7 @@ import '../build_configuration.dart';
import '../device.dart';
import '../services.dart';
import '../toolchain.dart';
import '../ios/initialize_xcode.dart';
import 'simulator.dart';
const String _ideviceinstallerInstructions =
......@@ -560,9 +561,12 @@ String _getIOSEngineRevision(ApplicationPackage app) {
Future<bool> _buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async {
if (!FileSystemEntity.isDirectorySync(app.localPath)) {
printError('Path "${path.absolute(app.localPath)}" does not exist.\nDid you run `flutter ios --init`?');
printTrace('Path "${path.absolute(app.localPath)}" does not exist. Initializing the Xcode project.');
if ((await initializeXcodeProjectHarness()) != 0) {
printError('Could not initialize the Xcode project.');
return false;
}
}
if (!_validateEngineRevision(app))
return false;
......
// 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 '../artifacts.dart';
import '../base/globals.dart';
import '../base/process.dart';
import '../runner/flutter_command_runner.dart';
/// A map from file path to file contents.
final Map<String, String> iosTemplateFiles = <String, String>{
'ios/Info.plist': _infoPlistInitialContents,
'ios/LaunchScreen.storyboard': _launchScreenInitialContents,
'ios/Assets.xcassets/AppIcon.appiconset/Contents.json': _iconAssetInitialContents
};
Uri _xcodeProjectUri(String revision) {
String uriString = 'https://storage.googleapis.com/flutter_infra/flutter/$revision/ios/FlutterXcode.zip';
return Uri.parse(uriString);
}
Future<List<int>> _fetchXcodeArchive() async {
printStatus('Fetching the Xcode project archive from the cloud...');
HttpClient client = new HttpClient();
Uri xcodeProjectUri = _xcodeProjectUri(ArtifactStore.engineRevision);
printStatus('Downloading $xcodeProjectUri...');
HttpClientRequest request = await client.getUrl(xcodeProjectUri);
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 {
printStatus('Unzipping Xcode project to local directory...');
// We cannot use ArchiveFile because this archive contains files 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 = new File(path.join(tempDir.path, 'FlutterXcode.zip'))..createSync();
tempFile.writeAsBytesSync(archiveBytes);
try {
// Remove the old generated project if one is present
runCheckedSync(['/bin/rm', '-rf', directory]);
// Create the directory so unzip can write to it
runCheckedSync(['/bin/mkdir', '-p', directory]);
// Unzip the Xcode project into the new empty directory
runCheckedSync(['/usr/bin/unzip', tempFile.path, '-d', directory]);
} catch (error) {
return false;
}
// Cleanup the temp directory after unzipping
runSync(['/bin/rm', '-rf', tempDir.path]);
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
for (FileSystemEntity file in flutterDir.listSync()) {
try {
runCheckedSync(['/bin/mv', file.path, directory]);
} catch (error) {
return false;
}
}
runSync(['/bin/rm', '-rf', flutterDir.path]);
return true;
}
void _writeUserEditableFilesIfNecessary(String directory) {
iosTemplateFiles.forEach((String filePath, String contents) {
File file = new File(filePath);
if (!file.existsSync()) {
file.parent.createSync(recursive: true);
file.writeAsStringSync(contents);
printStatus('Created $filePath.');
}
});
}
void _setupXcodeProjXcconfig(String filePath) {
StringBuffer localsBuffer = new StringBuffer();
localsBuffer.writeln('// This is a generated file; do not edit or check into version control.');
localsBuffer.writeln('// Recreate using `flutter ios --init`.');
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);
localsFile.createSync(recursive: true);
localsFile.writeAsStringSync(localsBuffer.toString());
}
Future<int> initializeXcodeProjectHarness() async {
// Step 1: Fetch the archive from the cloud
String iosFilesPath = path.join(Directory.current.path, 'ios');
String xcodeprojPath = path.join(iosFilesPath, '.generated');
List<int> archiveBytes = await _fetchXcodeArchive();
if (archiveBytes.isEmpty) {
printError('Error: No archive bytes received.');
return 1;
}
// Step 2: Inflate the archive into the user project directory
bool result = await _inflateXcodeArchive(xcodeprojPath, archiveBytes);
if (!result) {
printError('Could not inflate the Xcode project archive.');
return 1;
}
// Step 3: Setup default user editable files if this is the first run of
// the init command.
_writeUserEditableFilesIfNecessary(iosFilesPath);
// Step 4: Populate the Local.xcconfig with project specific paths
_setupXcodeProjXcconfig(path.join(xcodeprojPath, 'Local.xcconfig'));
// Step 5: Write the REVISION file
File revisionFile = new File(path.join(xcodeprojPath, 'REVISION'));
revisionFile.createSync();
revisionFile.writeAsStringSync(ArtifactStore.engineRevision);
// Step 6: Tell the user the location of the generated project.
printStatus('Xcode project created at $xcodeprojPath/.');
printStatus('User editable settings are in $iosFilesPath/.');
return 0;
}
final String _infoPlistInitialContents = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>Runner</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.runner.Runner</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Flutter</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
''';
final String _launchScreenInitialContents = '''
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
''';
final String _iconAssetInitialContents = '''
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
''';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment