Enable Swift Package Manager by default on master channel (#152049)

Changes:

1. Enables Swift Package Manager by default on the main/master channel
2. Fixes tests that fail if Swift Package Manager is enabled

Corresponding docs change: https://github.com/flutter/website/pull/10938

Addresses https://github.com/flutter/flutter/issues/151567
This commit is contained in:
Loïc Sharma 2024-07-24 09:24:31 -07:00 committed by GitHub
parent 0d504914c7
commit 08d8a7fa81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 215 additions and 85 deletions

View File

@ -151,7 +151,7 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
section('Check debug build has no Dart AOT');
final String aotSymbols = await _dylibSymbols(debugAppFrameworkPath);
final String aotSymbols = await dumpSymbolTable(debugAppFrameworkPath);
if (aotSymbols.contains('architecture') ||
aotSymbols.contains('_kDartVmSnapshot')) {
@ -172,7 +172,7 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
await _checkDylib(appFrameworkPath);
final String aotSymbols = await _dylibSymbols(appFrameworkPath);
final String aotSymbols = await dumpSymbolTable(appFrameworkPath);
if (!aotSymbols.contains('_kDartVmSnapshot')) {
throw TaskResult.failure('$mode App.framework missing Dart AOT');
@ -562,7 +562,7 @@ Future<void> _testBuildMacOSFramework(Directory projectDir) async {
section('Check debug build has no Dart AOT');
final String aotSymbols = await _dylibSymbols(debugAppFrameworkPath);
final String aotSymbols = await dumpSymbolTable(debugAppFrameworkPath);
if (aotSymbols.contains('architecture') ||
aotSymbols.contains('_kDartVmSnapshot')) {
@ -583,7 +583,7 @@ Future<void> _testBuildMacOSFramework(Directory projectDir) async {
await _checkDylib(appFrameworkPath);
final String aotSymbols = await _dylibSymbols(appFrameworkPath);
final String aotSymbols = await dumpSymbolTable(appFrameworkPath);
if (!aotSymbols.contains('_kDartVmSnapshot')) {
throw TaskResult.failure('$mode App.framework missing Dart AOT');
@ -939,15 +939,6 @@ Future<void> _checkStatic(String pathToLibrary) async {
}
}
Future<String> _dylibSymbols(String pathToDylib) {
return eval('nm', <String>[
'-g',
pathToDylib,
'-arch',
'arm64',
]);
}
Future<bool> _linksOnFlutter(String pathToBinary) async {
final String loadCommands = await eval('otool', <String>[
'-l',

View File

@ -703,13 +703,7 @@ Future<bool> _isAppAotBuild(Directory app) async {
'App',
);
final String symbolTable = await eval(
'nm',
<String> [
'-gU',
binary,
],
);
final String symbolTable = await dumpSymbolTable(binary);
return symbolTable.contains('kDartIsolateSnapshotInstructions');
}

View File

@ -6,6 +6,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/ios.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
@ -209,6 +210,7 @@ public class DummyPluginAClass {
final String flutterPluginsDependenciesFileContent = flutterPluginsDependenciesFile.readAsStringSync();
final Map<String, dynamic> jsonContent = json.decode(flutterPluginsDependenciesFileContent) as Map<String, dynamic>;
final bool swiftPackageManagerEnabled = jsonContent['swift_package_manager_enabled'] as bool? ?? false;
// Verify the dependencyGraph object is valid. The rest of the contents of this file are not relevant to the
// dependency graph and are tested by unit tests.
@ -302,28 +304,35 @@ public class DummyPluginAClass {
return TaskResult.failure('Failed to build plugin A example iOS app');
}
checkDirectoryExists(path.join(
appBundle.path,
'Frameworks',
'plugin_a.framework',
));
checkDirectoryExists(path.join(
appBundle.path,
'Frameworks',
'plugin_b.framework',
));
checkDirectoryExists(path.join(
appBundle.path,
'Frameworks',
'plugin_c.framework',
));
if (swiftPackageManagerEnabled) {
// Check plugins are built statically if using SwiftPM.
final String executable = path.join(appBundle.path, 'Runner');
final String symbols = await dumpSymbolTable(executable);
// Plugin D is Android only and should not be embedded.
checkDirectoryNotExists(path.join(
appBundle.path,
'Frameworks',
'plugin_d.framework',
));
final bool foundA = symbols.contains('plugin_a');
final bool foundB = symbols.contains('plugin_b');
final bool foundC = symbols.contains('plugin_c');
final bool foundD = symbols.contains('plugin_d');
if (!foundA || !foundB || !foundC) {
return TaskResult.failure(
'Failed to find plugins_a, plugin_b, or plugin_c symbols in the app'
);
}
if (foundD) {
return TaskResult.failure(
'Found Android plugin_d symbols in iOS app'
);
}
} else {
// Check plugins are built dynamically if using CocoaPods.
checkDirectoryExists(path.join(appBundle.path, 'Frameworks', 'plugin_a.framework'));
checkDirectoryExists(path.join(appBundle.path, 'Frameworks', 'plugin_b.framework'));
checkDirectoryExists(path.join(appBundle.path, 'Frameworks', 'plugin_c.framework'));
checkDirectoryNotExists(path.join(appBundle.path, 'Frameworks', 'plugin_d.framework'));
}
}
return TaskResult.success(null);

View File

@ -308,3 +308,17 @@ File? _createDisabledSandboxEntitlementFile(
return disabledSandboxEntitlementFile;
}
/// Returns global (external) symbol table entries, delimited by new lines.
Future<String> dumpSymbolTable(String filePath) {
return eval(
'nm',
<String>[
'--extern-only',
'--just-symbol-name',
filePath,
'-arch',
'arm64',
],
);
}

View File

@ -77,6 +77,11 @@ class PluginTest {
final _FlutterProject app = await _FlutterProject.create(tempDir, options, buildTarget,
name: 'plugintestapp', template: 'app', environment: appCreateEnvironment);
try {
if (cocoapodsTransitiveFlutterDependency) {
section('Disable Swift Package Manager');
await app.disableSwiftPackageManager();
}
section('Add plugins');
await app.addPlugin('plugintest',
pluginPath: path.join('..', 'plugintest'));
@ -147,6 +152,20 @@ class _FlutterProject {
return _FlutterProject(Directory(path.join(rootPath)), 'example');
}
Future<void> disableSwiftPackageManager() async {
final File pubspec = pubspecFile;
String content = await pubspec.readAsString();
content = content.replaceFirst(
'# The following section is specific to Flutter packages.\n'
'flutter:\n',
'# The following section is specific to Flutter packages.\n'
'flutter:\n'
'\n'
' disable-swift-package-manager: true\n'
);
await pubspec.writeAsString(content, flush: true);
}
Future<void> addPlugin(String plugin, {String? pluginPath}) async {
final File pubspec = pubspecFile;
String content = await pubspec.readAsString();
@ -244,9 +263,14 @@ class $dartPluginClass {
await podspec.writeAsString(podspecContent, flush: true);
// Make PlugintestPlugin.swift compile on iOS and macOS with target conditionals.
// If SwiftPM is disabled, the file will be in `darwin/Classes/`.
// Otherwise, the file will be in `darwin/<plugin>/Sources/<plugin>/`.
final String pluginClass = '${name[0].toUpperCase()}${name.substring(1)}Plugin';
print('pluginClass: $pluginClass');
final File pluginRegister = File(path.join(darwinDir.path, 'Classes', '$pluginClass.swift'));
File pluginRegister = File(path.join(darwinDir.path, 'Classes', '$pluginClass.swift'));
if (!pluginRegister.existsSync()) {
pluginRegister = File(path.join(darwinDir.path, name, 'Sources', name, '$pluginClass.swift'));
}
final String pluginRegisterContent = '''
#if os(macOS)
import FlutterMacOS
@ -494,42 +518,55 @@ s.dependency 'AppAuth', '1.6.0'
}
if (validateNativeBuildProject) {
final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj'));
if (!podsProject.existsSync()) {
throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}');
}
final File generatedSwiftManifest = File(path.join(
rootPath,
target,
'Flutter',
'ephemeral',
'Packages',
'FlutterGeneratedPluginSwiftPackage',
'Package.swift'
));
final bool swiftPackageManagerEnabled = generatedSwiftManifest.existsSync();
final String podsProjectContent = podsProject.readAsStringSync();
if (target == 'ios') {
// Plugins with versions lower than the app version should not have IPHONEOS_DEPLOYMENT_TARGET set.
// The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered
// in _reduceDarwinPluginMinimumVersion to 10, which is below the target version of 11.
if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 10')) {
throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed');
if (!swiftPackageManagerEnabled) {
final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj'));
if (!podsProject.existsSync()) {
throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}');
}
// Transitive dependency AppAuth targeting too-low 8.0 was not fixed.
if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 8')) {
throw TaskResult.failure('Transitive dependency build setting IPHONEOS_DEPLOYMENT_TARGET=8 not removed');
}
if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) {
throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"');
}
} else if (target == 'macos') {
// Same for macOS deployment target, but 10.8.
// The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set.
if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) {
throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed');
}
// Transitive dependency AppAuth targeting too-low 10.9 was not fixed.
if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.9')) {
throw TaskResult.failure('Transitive dependency build setting MACOSX_DEPLOYMENT_TARGET=10.9 not removed');
}
}
if (localEngine != null) {
final RegExp localEngineSearchPath = RegExp('FRAMEWORK_SEARCH_PATHS\\s*=[^;]*${localEngine.path}');
if (!localEngineSearchPath.hasMatch(podsProjectContent)) {
throw TaskResult.failure('FRAMEWORK_SEARCH_PATHS does not contain the --local-engine path');
final String podsProjectContent = podsProject.readAsStringSync();
if (target == 'ios') {
// Plugins with versions lower than the app version should not have IPHONEOS_DEPLOYMENT_TARGET set.
// The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered
// in _reduceDarwinPluginMinimumVersion to 10, which is below the target version of 11.
if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 10')) {
throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed');
}
// Transitive dependency AppAuth targeting too-low 8.0 was not fixed.
if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 8')) {
throw TaskResult.failure('Transitive dependency build setting IPHONEOS_DEPLOYMENT_TARGET=8 not removed');
}
if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) {
throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"');
}
} else if (target == 'macos') {
// Same for macOS deployment target, but 10.8.
// The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set.
if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) {
throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed');
}
// Transitive dependency AppAuth targeting too-low 10.9 was not fixed.
if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.9')) {
throw TaskResult.failure('Transitive dependency build setting MACOSX_DEPLOYMENT_TARGET=10.9 not removed');
}
}
if (localEngine != null) {
final RegExp localEngineSearchPath = RegExp('FRAMEWORK_SEARCH_PATHS\\s*=[^;]*${localEngine.path}');
if (!localEngineSearchPath.hasMatch(podsProjectContent)) {
throw TaskResult.failure('FRAMEWORK_SEARCH_PATHS does not contain the --local-engine path');
}
}
}
}

View File

@ -186,6 +186,7 @@ const Feature swiftPackageManager = Feature(
environmentOverride: 'SWIFT_PACKAGE_MANAGER',
master: FeatureChannelSetting(
available: true,
enabledByDefault: true,
),
);

View File

@ -693,7 +693,7 @@ void main() {
),
});
testUsingContext('kotlin/swift plugin project', () async {
testUsingContext('kotlin/swift plugin project without Swift Package Manager', () async {
return _createProject(
projectDir,
<String>['--no-pub', '--template=plugin', '-a', 'kotlin', '--ios-language', 'swift', '--platforms', 'ios,android'],
@ -718,6 +718,9 @@ void main() {
'ios/Classes/FlutterProjectPlugin.m',
],
);
}, overrides: <Type, Generator>{
// Test flags disable Swift Package Manager.
FeatureFlags: () => TestFeatureFlags(),
});
testUsingContext('swift plugin project with Swift Package Manager', () async {
@ -1944,7 +1947,7 @@ void main() {
);
});
testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org', () async {
testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org, without Swift Package Manager', () async {
await _createProject(
projectDir,
<String>[
@ -1969,6 +1972,7 @@ void main() {
unexpectedPaths: <String>[
'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
'ios/flutter_project/Sources/flutter_project/include/flutter_project/FlutterProjectPlugin.h',
],
);
final FlutterProject project = FlutterProject.fromDirectory(projectDir);
@ -1976,6 +1980,48 @@ void main() {
await project.example.ios.productBundleIdentifier(BuildInfo.debug),
'com.bar.foo.flutterProjectExample',
);
}, overrides: <Type, Generator>{
// Test flags disable Swift Package Manager.
FeatureFlags: () => TestFeatureFlags(),
});
testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org, with Swift Package Manager', () async {
await _createProject(
projectDir,
<String>[
'--no-pub',
'--template=plugin',
'--org', 'com.bar.foo',
'-i', 'objc',
'-a', 'java',
'--platforms', 'ios,android',
],
<String>[],
);
projectDir.childDirectory('example').deleteSync(recursive: true);
projectDir.childDirectory('ios').deleteSync(recursive: true);
await _createProject(
projectDir,
<String>['--no-pub', '--template=plugin', '-i', 'objc', '-a', 'java', '--platforms', 'ios,android'],
<String>[
'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java',
'ios/flutter_project/Sources/flutter_project/include/flutter_project/FlutterProjectPlugin.h',
],
unexpectedPaths: <String>[
'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
'ios/Classes/FlutterProjectPlugin.h',
],
);
final FlutterProject project = FlutterProject.fromDirectory(projectDir);
expect(
await project.example.ios.productBundleIdentifier(BuildInfo.debug),
'com.bar.foo.flutterProjectExample',
);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(
isSwiftPackageManagerEnabled: true,
),
});
testUsingContext('fails to re-gen without specified org when org is ambiguous', () async {

View File

@ -404,7 +404,7 @@ void main() {
});
test('${swiftPackageManager.name} availability and default enabled', () {
expect(swiftPackageManager.master.enabledByDefault, false);
expect(swiftPackageManager.master.enabledByDefault, true);
expect(swiftPackageManager.master.available, true);
expect(swiftPackageManager.beta.enabledByDefault, false);
expect(swiftPackageManager.beta.available, false);

View File

@ -87,6 +87,7 @@ void main() {
late File outputFlutterFrameworkBinary;
late Directory outputAppFramework;
late File outputAppFrameworkBinary;
late File outputRunnerBinary;
late File outputPluginFrameworkBinary;
late Directory buildPath;
late Directory buildAppFrameworkDsym;
@ -122,6 +123,10 @@ void main() {
outputAppFramework = frameworkDirectory.childDirectory('App.framework');
outputAppFrameworkBinary = outputAppFramework.childFile('App');
outputRunnerBinary = outputApp.childFile('Runner');
// Exists only if the plugin is built as a dynamic framework.
// This is is the default for CocoaPods but not Swift Package Manager.
outputPluginFrameworkBinary = frameworkDirectory.childDirectory('hello.framework').childFile('hello');
buildPath = fileSystem.directory(fileSystem.path.join(
@ -141,7 +146,18 @@ void main() {
printOnFailure(buildResult.stderr.toString());
expect(buildResult.exitCode, 0);
expect(outputPluginFrameworkBinary, exists);
// Plugins are built either as a static library (SwiftPM's default)
// or as a dynamic library (CocoaPods's default).
// If built as a dynamic library, the plugin will have a .framework.
// If built as static library, the plugin's symbols will be in the
// Runner binary.
final bool helloDynamic = outputPluginFrameworkBinary.existsSync();
final bool helloStatic = AppleTestUtils
.getExportedSymbols(outputRunnerBinary.path)
.any((String symbol) => symbol.contains('HelloPlugin') && symbol.contains('handle'));
// Plugin is a dynamic xor static framework.
expect(helloDynamic != helloStatic, isTrue);
expect(outputAppFrameworkBinary, exists);
expect(outputAppFramework.childFile('Info.plist'), exists);
@ -300,6 +316,19 @@ void main() {
);
expect(buildSimulator.exitCode, 0);
// Plugins are built either as a static library (SwiftPM's default)
// or as a dynamic library (CocoaPods's default).
// If built as a dynamic library, the plugin will have a .framework.
// If built as static library, the plugin's symbols will be in the
// Runner binary.
final File runnerBinary = fileSystem.file(fileSystem.path.join(
projectRoot,
'build',
'ios',
'iphonesimulator',
'Runner.app',
'Runner',
));
final File pluginFrameworkBinary = fileSystem.file(fileSystem.path.join(
projectRoot,
'build',
@ -310,12 +339,21 @@ void main() {
'hello.framework',
'hello',
));
expect(pluginFrameworkBinary, exists);
final ProcessResult archs = processManager.runSync(
<String>['file', pluginFrameworkBinary.path],
);
expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library x86_64'));
expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library arm64'));
final bool helloDynamic = pluginFrameworkBinary.existsSync();
final bool helloStatic = AppleTestUtils
.getExportedSymbols(runnerBinary.path)
.any((String symbol) => symbol.contains('HelloPlugin') && symbol.contains('handle'));
// Plugin is a dynamic xor static framework.
expect(helloDynamic != helloStatic, isTrue);
if (helloDynamic) {
final ProcessResult archs = processManager.runSync(
<String>['file', pluginFrameworkBinary.path],
);
expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library x86_64'));
expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library arm64'));
}
});
testWithoutContext('build for simulator with all available architectures', () {