Unverified Commit 9861a1c0 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

flutter build ios-framework generate Flutter.podspec (#47556)

parent 4adad2c6
......@@ -235,6 +235,34 @@ class Cache {
}
String _engineRevision;
String get storageBaseUrl {
final String overrideUrl = platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (overrideUrl == null) {
return 'https://storage.googleapis.com';
}
// verify that this is a valid URI.
try {
Uri.parse(overrideUrl);
} on FormatException catch (err) {
throwToolExit('"FLUTTER_STORAGE_BASE_URL" contains an invalid URI:\n$err');
}
_maybeWarnAboutStorageOverride(overrideUrl);
return overrideUrl;
}
bool _hasWarnedAboutStorageOverride = false;
void _maybeWarnAboutStorageOverride(String overrideUrl) {
if (_hasWarnedAboutStorageOverride) {
return;
}
logger.printStatus(
'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
emphasis: true,
);
_hasWarnedAboutStorageOverride = true;
}
static Cache get instance => context.get<Cache>();
/// Return the top-level directory in the cache; this is `bin/cache`.
......@@ -262,6 +290,9 @@ class Cache {
/// Return the top-level mutable directory in the cache; this is `bin/cache/artifacts`.
Directory getCacheArtifacts() => getCacheDir('artifacts');
/// Location of LICENSE file.
File getLicenseFile() => fs.file(fs.path.join(flutterRoot, 'LICENSE'));
/// Get a named directory from with the cache's artifact directory; for example,
/// `material_fonts` would return `bin/cache/artifacts/material_fonts`.
Directory getArtifactDirectory(String name) {
......@@ -492,23 +523,7 @@ abstract class CachedArtifact extends ArtifactSet {
/// Template method to perform artifact update.
Future<void> updateInner();
@visibleForTesting
String get storageBaseUrl {
final String overrideUrl = platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (overrideUrl == null) {
return 'https://storage.googleapis.com';
}
// verify that this is a valid URI.
try {
Uri.parse(overrideUrl);
} on FormatException catch (err) {
throwToolExit('"FLUTTER_STORAGE_BASE_URL" contains an invalid URI:\n$err');
}
_maybeWarnAboutStorageOverride(overrideUrl);
return overrideUrl;
}
Uri _toStorageUri(String path) => Uri.parse('$storageBaseUrl/$path');
Uri _toStorageUri(String path) => Uri.parse('${cache.storageBaseUrl}/$path');
/// Download an archive from the given [url] and unzip it to [location].
Future<void> _downloadArchive(String message, Uri url, Directory location, bool verifier(File f), void extractor(File f, Directory d)) {
......@@ -549,19 +564,6 @@ abstract class CachedArtifact extends ArtifactSet {
}
}
bool _hasWarnedAboutStorageOverride = false;
void _maybeWarnAboutStorageOverride(String overrideUrl) {
if (_hasWarnedAboutStorageOverride) {
return;
}
logger.printStatus(
'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
emphasis: true,
);
_hasWarnedAboutStorageOverride = true;
}
/// A cached artifact containing fonts used for Material Design.
class MaterialFonts extends CachedArtifact {
MaterialFonts(Cache cache) : super(
......@@ -604,7 +606,7 @@ class FlutterWebSdk extends CachedArtifact {
} else if (platform.isWindows) {
platformName += 'windows-x64';
}
final Uri url = Uri.parse('$storageBaseUrl/flutter_infra/flutter/$version/$platformName.zip');
final Uri url = Uri.parse('${cache.storageBaseUrl}/flutter_infra/flutter/$version/$platformName.zip');
await _downloadZipArchive('Downloading Web SDK...', url, location);
// This is a temporary work-around for not being able to safely download into a shared directory.
for (FileSystemEntity entity in location.listSync(recursive: true)) {
......@@ -669,7 +671,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {
@override
Future<void> updateInner() async {
final String url = '$storageBaseUrl/flutter_infra/flutter/$version/';
final String url = '${cache.storageBaseUrl}/flutter_infra/flutter/$version/';
final Directory pkgDir = cache.getCacheDir('pkg');
for (String pkgName in getPackageDirs()) {
......@@ -695,7 +697,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {
}
}
final File licenseSource = fs.file(fs.path.join(Cache.flutterRoot, 'LICENSE'));
final File licenseSource = cache.getLicenseFile();
for (String licenseDir in getLicenseDirs()) {
final String licenseDestinationPath = fs.path.join(location.path, licenseDir, 'LICENSE');
await licenseSource.copy(licenseDestinationPath);
......@@ -704,7 +706,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {
Future<bool> checkForArtifacts(String engineVersion) async {
engineVersion ??= version;
final String url = '$storageBaseUrl/flutter_infra/flutter/$engineVersion/';
final String url = '${cache.storageBaseUrl}/flutter_infra/flutter/$engineVersion/';
bool exists = false;
for (String pkgName in getPackageDirs()) {
......@@ -1210,7 +1212,7 @@ class IosUsbArtifacts extends CachedArtifact {
}
@visibleForTesting
Uri get archiveUri => Uri.parse('$storageBaseUrl/flutter_infra/ios-usb-dependencies${cache.useUnsignedMacBinaries ? '/unsigned' : ''}/$name/$version/$name.zip');
Uri get archiveUri => Uri.parse('${cache.storageBaseUrl}/flutter_infra/ios-usb-dependencies${cache.useUnsignedMacBinaries ? '/unsigned' : ''}/$name/$version/$name.zip');
}
// Many characters are problematic in filenames, especially on Windows.
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import '../aot.dart';
import '../application_package.dart';
......@@ -25,6 +26,7 @@ import '../macos/xcode.dart';
import '../plugins.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
import '../version.dart';
import 'build.dart';
/// Produces a .framework for integration into a host iOS app. The .framework
......@@ -32,7 +34,7 @@ import 'build.dart';
/// be integrated into plain Xcode projects without using or other package
/// managers.
class BuildIOSFrameworkCommand extends BuildSubCommand {
BuildIOSFrameworkCommand({this.aotBuilder, this.bundleBuilder}) {
BuildIOSFrameworkCommand({this.aotBuilder, this.bundleBuilder, this.flutterVersion, this.cache}) {
usesTargetOption();
usesFlavorOption();
usesPubOption();
......@@ -65,6 +67,9 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
..addFlag('xcframework',
help: 'Produce xcframeworks that include all valid architectures (Xcode 11 or later).',
)
..addFlag('cocoapods',
help: 'Produce a Flutter.podspec instead of an engine Flutter.framework (recomended if host app uses CocoaPods).',
)
..addOption('output',
abbr: 'o',
valueHelp: 'path/to/directory/',
......@@ -74,6 +79,8 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
AotBuilder aotBuilder;
BundleBuilder bundleBuilder;
FlutterVersion flutterVersion;
Cache cache;
@override
final String name = 'ios-framework';
......@@ -150,6 +157,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
aotBuilder ??= AotBuilder();
bundleBuilder ??= BundleBuilder();
cache ??= Cache.instance;
for (BuildMode mode in buildModes) {
printStatus('Building framework for $iosProject in ${getNameForBuildMode(mode)} mode...');
......@@ -162,8 +170,14 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
final Directory iPhoneBuildOutput = modeDirectory.childDirectory('iphoneos');
final Directory simulatorBuildOutput = modeDirectory.childDirectory('iphonesimulator');
// Copy Flutter.framework.
await _produceFlutterFramework(outputDirectory, mode, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory);
if (boolArg('cocoapods')) {
// FlutterVersion.instance kicks off git processing which can sometimes fail, so don't try it until needed.
flutterVersion ??= FlutterVersion.instance;
produceFlutterPodspec(mode, modeDirectory);
} else {
// Copy Flutter.framework.
await _produceFlutterFramework(outputDirectory, mode, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory);
}
// Build aot, create module.framework and copy.
await _produceAppFramework(mode, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory);
......@@ -194,6 +208,64 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
return null;
}
/// Create podspec that will download and unzip remote engine assets so host apps can leverage CocoaPods
/// vendored framework caching.
@visibleForTesting
void produceFlutterPodspec(BuildMode mode, Directory modeDirectory) {
final Status status = logger.startProgress(' ├─Creating Flutter.podspec...', timeout: timeoutConfiguration.fastOperation);
try {
final GitTagVersion gitTagVersion = flutterVersion.gitTagVersion;
if (gitTagVersion.x == null || gitTagVersion.y == null || gitTagVersion.z == null || gitTagVersion.commits != 0) {
throwToolExit(
'--cocoapods is only supported on the dev, beta, or stable channels. Detected version is ${flutterVersion.frameworkVersion}');
}
// Podspecs use semantic versioning, which don't support hotfixes.
// Fake out a semantic version with major.minor.(patch * 100) + hotfix.
// A real increasing version is required to prompt CocoaPods to fetch
// new artifacts when the source URL changes.
final int minorHotfixVersion = gitTagVersion.z * 100 + (gitTagVersion.hotfix ?? 0);
final File license = cache.getLicenseFile();
if (!license.existsSync()) {
throwToolExit('Could not find license at ${license.path}');
}
final String licenseSource = license.readAsStringSync();
final String artifactsMode = mode == BuildMode.debug ? 'ios' : 'ios-${mode.name}';
final String podspecContents = '''
Pod::Spec.new do |s|
s.name = 'Flutter'
s.version = '${gitTagVersion.x}.${gitTagVersion.y}.$minorHotfixVersion' # ${flutterVersion.frameworkVersion}
s.summary = 'Flutter Engine Framework'
s.description = <<-DESC
Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.
This pod vends the iOS Flutter engine framework. It is compatible with application frameworks created with this version of the engine and tools.
The pod version matches Flutter version major.minor.(patch * 100) + hotfix.
DESC
s.homepage = 'https://flutter.dev'
s.license = { :type => 'MIT', :text => <<-LICENSE
$licenseSource
LICENSE
}
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :http => '${cache.storageBaseUrl}/flutter_infra/flutter/${cache.engineRevision}/$artifactsMode/artifacts.zip' }
s.documentation_url = 'https://flutter.dev/docs'
s.platform = :ios, '8.0'
s.vendored_frameworks = 'Flutter.framework'
s.prepare_command = <<-CMD
unzip Flutter.framework -d Flutter.framework
CMD
end
''';
final File podspec = modeDirectory.childFile('Flutter.podspec')..createSync(recursive: true);
podspec.writeAsStringSync(podspecContents);
} finally {
status.stop();
}
}
Future<void> _produceFlutterFramework(Directory outputDirectory, BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async {
final Status status = logger.startProgress(' ├─Populating Flutter.framework...', timeout: timeoutConfiguration.slowOperation);
try {
......@@ -278,7 +350,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
destinationAppFrameworkDirectory.createSync(recursive: true);
if (mode == BuildMode.debug) {
final Status status = logger.startProgress(' ├─Add placeholder App.framework for debug...', timeout: timeoutConfiguration.fastOperation);
final Status status = logger.startProgress(' ├─Adding placeholder App.framework for debug...', timeout: timeoutConfiguration.fastOperation);
try {
await _produceStubAppFrameworkIfNeeded(mode, iPhoneBuildOutput, simulatorBuildOutput, destinationAppFrameworkDirectory);
} finally {
......
......@@ -20,7 +20,8 @@ import 'globals.dart';
class FlutterVersion {
FlutterVersion([this._clock = const SystemClock()]) {
_frameworkRevision = _runGit(gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '));
_frameworkVersion = GitTagVersion.determine().frameworkVersionFor(_frameworkRevision);
_gitTagVersion = GitTagVersion.determine();
_frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
}
final SystemClock _clock;
......@@ -75,6 +76,9 @@ class FlutterVersion {
return _channel;
}
GitTagVersion _gitTagVersion;
GitTagVersion get gitTagVersion => _gitTagVersion;
/// The name of the local branch.
/// Use getBranchName() to read this.
String _branch;
......
// Copyright 2014 The Flutter 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:file/memory.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build_ios_framework.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group('build ios-framework', () {
group('podspec', () {
MemoryFileSystem memoryFileSystem;
MockFlutterVersion mockFlutterVersion;
MockGitTagVersion mockGitTagVersion;
MockCache mockCache;
Directory outputDirectory;
const String storageBaseUrl = 'https://fake.googleapis.com';
const String engineRevision = '0123456789abcdef';
File licenseFile;
setUp(() {
memoryFileSystem = MemoryFileSystem();
mockFlutterVersion = MockFlutterVersion();
mockGitTagVersion = MockGitTagVersion();
mockCache = MockCache();
when(mockFlutterVersion.gitTagVersion).thenReturn(mockGitTagVersion);
outputDirectory = fs.systemTempDirectory
.createTempSync('flutter_build_ios_framework_test_output.')
.childDirectory('Debug')
..createSync();
when(mockCache.storageBaseUrl).thenReturn(storageBaseUrl);
when(mockCache.engineRevision).thenReturn(engineRevision);
licenseFile = memoryFileSystem.file('LICENSE');
when(mockCache.getLicenseFile()).thenReturn(licenseFile);
});
testUsingContext('version unknown', () async {
const String frameworkVersion = '0.0.0-unknown';
when(mockFlutterVersion.frameworkVersion).thenReturn(frameworkVersion);
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
expect(() => command.produceFlutterPodspec(BuildMode.debug, outputDirectory),
throwsToolExit(message: 'Detected version is $frameworkVersion'));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('throws when not on a released version', () async {
const String frameworkVersion = 'v1.13.10+hotfix-pre.2';
when(mockFlutterVersion.frameworkVersion).thenReturn(frameworkVersion);
when(mockGitTagVersion.x).thenReturn(1);
when(mockGitTagVersion.y).thenReturn(13);
when(mockGitTagVersion.z).thenReturn(10);
when(mockGitTagVersion.hotfix).thenReturn(13);
when(mockGitTagVersion.commits).thenReturn(2);
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
expect(() => command.produceFlutterPodspec(BuildMode.debug, outputDirectory),
throwsToolExit(message: 'Detected version is $frameworkVersion'));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('throws when license not found', () async {
when(mockGitTagVersion.x).thenReturn(1);
when(mockGitTagVersion.y).thenReturn(13);
when(mockGitTagVersion.z).thenReturn(10);
when(mockGitTagVersion.hotfix).thenReturn(13);
when(mockGitTagVersion.commits).thenReturn(0);
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
expect(() => command.produceFlutterPodspec(BuildMode.debug, outputDirectory),
throwsToolExit(message: 'Could not find license'));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
group('is created', () {
const String frameworkVersion = 'v1.13.11+hotfix.13';
const String licenseText = 'This is the license!';
setUp(() {
when(mockGitTagVersion.x).thenReturn(1);
when(mockGitTagVersion.y).thenReturn(13);
when(mockGitTagVersion.z).thenReturn(11);
when(mockGitTagVersion.hotfix).thenReturn(13);
when(mockGitTagVersion.commits).thenReturn(0);
when(mockFlutterVersion.frameworkVersion).thenReturn(frameworkVersion);
licenseFile
..createSync(recursive: true)
..writeAsStringSync(licenseText);
});
testUsingContext('contains license and version', () async {
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
command.produceFlutterPodspec(BuildMode.debug, outputDirectory);
final File expectedPodspec = outputDirectory.childFile('Flutter.podspec');
final String podspecContents = expectedPodspec.readAsStringSync();
expect(podspecContents, contains('\'1.13.1113\''));
expect(podspecContents, contains('# $frameworkVersion'));
expect(podspecContents, contains(licenseText));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('debug URL', () async {
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
command.produceFlutterPodspec(BuildMode.debug, outputDirectory);
final File expectedPodspec = outputDirectory.childFile('Flutter.podspec');
final String podspecContents = expectedPodspec.readAsStringSync();
expect(podspecContents, contains('\'$storageBaseUrl/flutter_infra/flutter/$engineRevision/ios/artifacts.zip\''));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('profile URL', () async {
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
command.produceFlutterPodspec(BuildMode.profile, outputDirectory);
final File expectedPodspec = outputDirectory.childFile('Flutter.podspec');
final String podspecContents = expectedPodspec.readAsStringSync();
expect(podspecContents, contains('\'$storageBaseUrl/flutter_infra/flutter/$engineRevision/ios-profile/artifacts.zip\''));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('release URL', () async {
final BuildIOSFrameworkCommand command = BuildIOSFrameworkCommand(
flutterVersion: mockFlutterVersion,
cache: mockCache
);
command.produceFlutterPodspec(BuildMode.release, outputDirectory);
final File expectedPodspec = outputDirectory.childFile('Flutter.podspec');
final String podspecContents = expectedPodspec.readAsStringSync();
expect(podspecContents, contains('\'$storageBaseUrl/flutter_infra/flutter/$engineRevision/ios-release/artifacts.zip\''));
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
});
});
}
class MockFlutterVersion extends Mock implements FlutterVersion {}
class MockGitTagVersion extends Mock implements GitTagVersion {}
class MockCache extends Mock implements Cache {}
......@@ -230,9 +230,7 @@ void main() {
'FLUTTER_STORAGE_BASE_URL': ' http://foo',
});
final Cache cache = Cache();
final CachedArtifact artifact = MaterialFonts(cache);
expect(() => artifact.storageBaseUrl, throwsA(isInstanceOf<ToolExit>()));
expect(() => cache.storageBaseUrl, throwsA(isInstanceOf<ToolExit>()));
}, overrides: <Type, Generator>{
Platform: () => MockPlatform(),
});
......
......@@ -680,6 +680,9 @@ class FakeFlutterVersion implements FlutterVersion {
@override
String get frameworkVersion => null;
@override
GitTagVersion get gitTagVersion => null;
@override
String getBranchName({bool redactUnknownBranches = false}) {
return 'master';
......@@ -829,6 +832,9 @@ class FakeCache implements Cache {
@override
String get dartSdkVersion => null;
@override
String get storageBaseUrl => null;
@override
MapEntry<String, String> get dyLdLibEntry => null;
......@@ -860,6 +866,11 @@ class FakeCache implements Cache {
return fs.currentDirectory;
}
@override
File getLicenseFile() {
return fs.currentDirectory.childFile('LICENSE');
}
@override
File getStampFileFor(String artifactName) {
throw UnsupportedError('Not supported in the fake Cache');
......
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