// 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/process.dart'; import '../globals.dart'; import '../runner/flutter_command_runner.dart'; 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 updateXcodeLocalProperties(String projectPath) { StringBuffer localsBuffer = new StringBuffer(); localsBuffer.writeln('// This is a generated file; do not edit or check into version control.'); 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(path.join(projectPath, 'ios', '.generated', 'Local.xcconfig')); localsFile.createSync(recursive: true); localsFile.writeAsStringSync(localsBuffer.toString()); } bool xcodeProjectRequiresUpdate() { File revisionFile = new File(path.join(Directory.current.path, 'ios', '.generated', 'REVISION')); // If the revision stamp does not exist, the Xcode project definitely requires // an update if (!revisionFile.existsSync()) { printTrace("A revision stamp does not exist. The Xcode project has never been initialized."); return true; } if (revisionFile.readAsStringSync() != ArtifactStore.engineRevision) { printTrace("The revision stamp and the Flutter engine revision differ. Project needs to be updated."); return true; } printTrace("Xcode project is up to date."); return false; } Future<int> setupXcodeProjectHarness(String flutterProjectPath) async { // Step 1: Fetch the archive from the cloud String iosFilesPath = path.join(flutterProjectPath, '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: Populate the Local.xcconfig with project specific paths updateXcodeLocalProperties(flutterProjectPath); // Step 4: Write the REVISION file File revisionFile = new File(path.join(xcodeprojPath, 'REVISION')); revisionFile.createSync(); revisionFile.writeAsStringSync(ArtifactStore.engineRevision); // Step 5: Tell the user the location of the generated project. printStatus('Xcode project created at $xcodeprojPath/.'); printStatus('User editable settings are in $iosFilesPath/.'); return 0; }