mirror of
https://github.com/flutter/packages.git
synced 2025-05-28 19:26:50 +08:00

macOS privacy manifest enforcement is rolling out soon, so this brings all macOS plugins into alignment with our iOS policy of always having a manifest, including updating the repo tooling to enforce that. Very few plugins are affected because most share the implementation package with iOS, and we didn't do any target platform switching when adding the manifests for iOS, automatically covering macOS as well. Fixes https://github.com/flutter/flutter/issues/155564
654 lines
20 KiB
Dart
654 lines
20 KiB
Dart
// Copyright 2013 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 'package:args/command_runner.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:file/memory.dart';
|
|
import 'package:flutter_plugin_tools/src/common/core.dart';
|
|
import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
|
|
import 'package:flutter_plugin_tools/src/podspec_check_command.dart';
|
|
import 'package:platform/platform.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'mocks.dart';
|
|
import 'util.dart';
|
|
|
|
/// Adds a fake podspec to [plugin]'s [platform] directory.
|
|
///
|
|
/// If [includeSwiftWorkaround] is set, the xcconfig additions to make Swift
|
|
/// libraries work in apps that have no Swift will be included. If
|
|
/// [scopeSwiftWorkaround] is set, it will be specific to the iOS configuration.
|
|
void _writeFakePodspec(
|
|
RepositoryPackage plugin,
|
|
String platform, {
|
|
bool includeSwiftWorkaround = false,
|
|
bool scopeSwiftWorkaround = false,
|
|
bool includePrivacyManifest = false,
|
|
}) {
|
|
final String pluginName = plugin.directory.basename;
|
|
final File file = plugin.directory
|
|
.childDirectory(platform)
|
|
.childFile('$pluginName.podspec');
|
|
final String swiftWorkaround = includeSwiftWorkaround
|
|
? '''
|
|
s.${scopeSwiftWorkaround ? 'ios.' : ''}xcconfig = {
|
|
'LIBRARY_SEARCH_PATHS' => '\$(TOOLCHAIN_DIR)/usr/lib/swift/\$(PLATFORM_NAME)/ \$(SDKROOT)/usr/lib/swift',
|
|
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
|
|
}
|
|
'''
|
|
: '';
|
|
final String privacyManifest = includePrivacyManifest
|
|
? '''
|
|
s.resource_bundles = {'$pluginName' => ['Resources/PrivacyInfo.xcprivacy']}
|
|
'''
|
|
: '';
|
|
file.createSync(recursive: true);
|
|
file.writeAsStringSync('''
|
|
#
|
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
|
#
|
|
Pod::Spec.new do |s|
|
|
s.name = 'shared_preferences_foundation'
|
|
s.version = '0.0.1'
|
|
s.summary = 'iOS and macOS implementation of the shared_preferences plugin.'
|
|
s.description = <<-DESC
|
|
Wraps NSUserDefaults, providing a persistent store for simple key-value pairs.
|
|
DESC
|
|
s.homepage = 'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_foundation'
|
|
s.license = { :type => 'BSD', :file => '../LICENSE' }
|
|
s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
|
|
s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_foundation' }
|
|
s.source_files = 'Classes/**/*'
|
|
s.ios.dependency 'Flutter'
|
|
s.osx.dependency 'FlutterMacOS'
|
|
s.ios.deployment_target = '9.0'
|
|
s.osx.deployment_target = '10.11'
|
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
|
$swiftWorkaround
|
|
s.swift_version = '5.0'
|
|
$privacyManifest
|
|
|
|
end
|
|
''');
|
|
}
|
|
|
|
void main() {
|
|
group('PodspecCheckCommand', () {
|
|
FileSystem fileSystem;
|
|
late Directory packagesDir;
|
|
late CommandRunner<void> runner;
|
|
late MockPlatform mockPlatform;
|
|
late RecordingProcessRunner processRunner;
|
|
|
|
setUp(() {
|
|
fileSystem = MemoryFileSystem();
|
|
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
|
|
|
|
mockPlatform = MockPlatform(isMacOS: true);
|
|
processRunner = RecordingProcessRunner();
|
|
final PodspecCheckCommand command = PodspecCheckCommand(
|
|
packagesDir,
|
|
processRunner: processRunner,
|
|
platform: mockPlatform,
|
|
);
|
|
|
|
runner =
|
|
CommandRunner<void>('podspec_test', 'Test for $PodspecCheckCommand');
|
|
runner.addCommand(command);
|
|
});
|
|
|
|
test('only runs on macOS', () async {
|
|
createFakePlugin('plugin1', packagesDir,
|
|
extraFiles: <String>['plugin1.podspec']);
|
|
mockPlatform.isMacOS = false;
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
|
|
expect(
|
|
processRunner.recordedCalls,
|
|
equals(<ProcessCall>[]),
|
|
);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[contains('only supported on macOS')],
|
|
));
|
|
});
|
|
|
|
test('runs pod lib lint on a podspec', () async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
extraFiles: <String>[
|
|
'bogus.dart', // Ignore non-podspecs.
|
|
],
|
|
);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
|
|
FakeProcessInfo(MockProcess(stdout: 'Foo', stderr: 'Bar')),
|
|
FakeProcessInfo(MockProcess()),
|
|
];
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
processRunner.recordedCalls,
|
|
orderedEquals(<ProcessCall>[
|
|
ProcessCall('which', const <String>['pod'], packagesDir.path),
|
|
ProcessCall(
|
|
'pod',
|
|
<String>[
|
|
'lib',
|
|
'lint',
|
|
plugin
|
|
.platformDirectory(FlutterPlatform.ios)
|
|
.childFile('plugin1.podspec')
|
|
.path,
|
|
'--configuration=Debug',
|
|
'--skip-tests',
|
|
'--use-modular-headers',
|
|
'--use-libraries'
|
|
],
|
|
packagesDir.path),
|
|
ProcessCall(
|
|
'pod',
|
|
<String>[
|
|
'lib',
|
|
'lint',
|
|
plugin
|
|
.platformDirectory(FlutterPlatform.ios)
|
|
.childFile('plugin1.podspec')
|
|
.path,
|
|
'--configuration=Debug',
|
|
'--skip-tests',
|
|
'--use-modular-headers',
|
|
],
|
|
packagesDir.path),
|
|
]),
|
|
);
|
|
|
|
expect(output, contains('Linting plugin1.podspec'));
|
|
expect(output, contains('Foo'));
|
|
expect(output, contains('Bar'));
|
|
});
|
|
|
|
test('skips shim podspecs for the Flutter framework', () async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
extraFiles: <String>[
|
|
'example/ios/Flutter/Flutter.podspec',
|
|
'example/macos/Flutter/ephemeral/FlutterMacOS.podspec',
|
|
],
|
|
);
|
|
_writeFakePodspec(plugin, 'macos');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(output, isNot(contains('FlutterMacOS.podspec')));
|
|
expect(
|
|
processRunner.recordedCalls,
|
|
orderedEquals(<ProcessCall>[
|
|
ProcessCall('which', const <String>['pod'], packagesDir.path),
|
|
ProcessCall(
|
|
'pod',
|
|
<String>[
|
|
'lib',
|
|
'lint',
|
|
plugin
|
|
.platformDirectory(FlutterPlatform.macos)
|
|
.childFile('plugin1.podspec')
|
|
.path,
|
|
'--configuration=Debug',
|
|
'--skip-tests',
|
|
'--use-modular-headers',
|
|
'--use-libraries'
|
|
],
|
|
packagesDir.path),
|
|
ProcessCall(
|
|
'pod',
|
|
<String>[
|
|
'lib',
|
|
'lint',
|
|
plugin
|
|
.platformDirectory(FlutterPlatform.macos)
|
|
.childFile('plugin1.podspec')
|
|
.path,
|
|
'--configuration=Debug',
|
|
'--skip-tests',
|
|
'--use-modular-headers',
|
|
],
|
|
packagesDir.path),
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('fails if pod is missing', () async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
// Simulate failure from `which pod`.
|
|
processRunner.mockProcessesForExecutable['which'] = <FakeProcessInfo>[
|
|
FakeProcessInfo(MockProcess(exitCode: 1), <String>['pod']),
|
|
];
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Unable to find "pod". Make sure it is in your path.'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('fails if linting as a framework fails', () async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
// Simulate failure from `pod`.
|
|
processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
|
|
FakeProcessInfo(MockProcess(exitCode: 1)),
|
|
];
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('The following packages had errors:'),
|
|
contains('plugin1:\n'
|
|
' plugin1.podspec')
|
|
],
|
|
));
|
|
});
|
|
|
|
test('fails if linting as a static library fails', () async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
// Simulate failure from the second call to `pod`.
|
|
processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
|
|
FakeProcessInfo(MockProcess()),
|
|
FakeProcessInfo(MockProcess(exitCode: 1)),
|
|
];
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('The following packages had errors:'),
|
|
contains('plugin1:\n'
|
|
' plugin1.podspec')
|
|
],
|
|
));
|
|
});
|
|
|
|
test('fails if an iOS Swift plugin is missing the search paths workaround',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
extraFiles: <String>[
|
|
'ios/Classes/SomeSwift.swift',
|
|
'ios/plugin1/Package.swift',
|
|
],
|
|
);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains(r'''
|
|
s.xcconfig = {
|
|
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
|
|
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
|
|
}'''),
|
|
contains('The following packages had errors:'),
|
|
contains('plugin1:\n'
|
|
' plugin1.podspec')
|
|
],
|
|
));
|
|
});
|
|
|
|
test(
|
|
'fails if a shared-source Swift plugin is missing the search paths workaround',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir,
|
|
extraFiles: <String>['darwin/Classes/SomeSwift.swift']);
|
|
_writeFakePodspec(plugin, 'darwin');
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains(r'''
|
|
s.xcconfig = {
|
|
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
|
|
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
|
|
}'''),
|
|
contains('The following packages had errors:'),
|
|
contains('plugin1:\n'
|
|
' plugin1.podspec')
|
|
],
|
|
));
|
|
});
|
|
|
|
test('does not require the search paths workaround for iOS Package.swift',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
extraFiles: <String>['ios/plugin1/Package.swift'],
|
|
);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('does not require the search paths workaround for Swift tests',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
extraFiles: <String>[
|
|
'darwin/Tests/SharedTest.swift',
|
|
'example/ios/RunnerTests/UnitTest.swift',
|
|
'example/ios/RunnerUITests/UITest.swift',
|
|
],
|
|
);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test(
|
|
'does not require the search paths workaround for darwin Package.swift',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
extraFiles: <String>['darwin/plugin1/Package.swift'],
|
|
);
|
|
_writeFakePodspec(plugin, 'darwin');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('does not require the search paths workaround for macOS plugins',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir,
|
|
extraFiles: <String>['macos/Classes/SomeSwift.swift']);
|
|
_writeFakePodspec(plugin, 'macos');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('does not require the search paths workaround for ObjC iOS plugins',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir,
|
|
extraFiles: <String>[
|
|
'ios/Classes/SomeObjC.h',
|
|
'ios/Classes/SomeObjC.m'
|
|
]);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('passes if the search paths workaround is present', () async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir,
|
|
extraFiles: <String>['ios/Classes/SomeSwift.swift']);
|
|
_writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true);
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('passes if the search paths workaround is present for iOS only',
|
|
() async {
|
|
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir,
|
|
extraFiles: <String>['ios/Classes/SomeSwift.swift']);
|
|
_writeFakePodspec(plugin, 'ios',
|
|
includeSwiftWorkaround: true, scopeSwiftWorkaround: true);
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('does not require the search paths workaround for Swift example code',
|
|
() async {
|
|
final RepositoryPackage plugin =
|
|
createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
|
|
'ios/Classes/SomeObjC.h',
|
|
'ios/Classes/SomeObjC.m',
|
|
'example/ios/Runner/AppDelegate.swift',
|
|
]);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[
|
|
contains('Ran for 1 package(s)'),
|
|
],
|
|
));
|
|
});
|
|
|
|
test('skips when there are no podspecs', () async {
|
|
createFakePlugin('plugin1', packagesDir);
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[contains('SKIPPING: No podspecs.')],
|
|
));
|
|
});
|
|
|
|
test('fails when an iOS plugin is missing a privacy manifest', () async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
platformSupport: <String, PlatformDetails>{
|
|
Platform.iOS: const PlatformDetails(PlatformSupport.inline),
|
|
},
|
|
);
|
|
_writeFakePodspec(plugin, 'ios');
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[contains('No PrivacyInfo.xcprivacy file specified.')],
|
|
));
|
|
});
|
|
|
|
test('passes when an iOS plugin has a privacy manifest', () async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
platformSupport: <String, PlatformDetails>{
|
|
Platform.iOS: const PlatformDetails(PlatformSupport.inline),
|
|
},
|
|
);
|
|
_writeFakePodspec(plugin, 'ios', includePrivacyManifest: true);
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[contains('Ran for 1 package(s)')],
|
|
));
|
|
});
|
|
|
|
test('fails when a macOS plugin is missing a privacy manifest', () async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
platformSupport: <String, PlatformDetails>{
|
|
Platform.macOS: const PlatformDetails(PlatformSupport.inline),
|
|
},
|
|
);
|
|
_writeFakePodspec(plugin, 'macos');
|
|
|
|
Error? commandError;
|
|
final List<String> output = await runCapturingPrint(
|
|
runner, <String>['podspec-check'], errorHandler: (Error e) {
|
|
commandError = e;
|
|
});
|
|
|
|
expect(commandError, isA<ToolExit>());
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[contains('No PrivacyInfo.xcprivacy file specified.')],
|
|
));
|
|
});
|
|
|
|
test('passes when a macOS plugin has a privacy manifest', () async {
|
|
final RepositoryPackage plugin = createFakePlugin(
|
|
'plugin1',
|
|
packagesDir,
|
|
platformSupport: <String, PlatformDetails>{
|
|
Platform.macOS: const PlatformDetails(PlatformSupport.inline),
|
|
},
|
|
);
|
|
_writeFakePodspec(plugin, 'macos', includePrivacyManifest: true);
|
|
|
|
final List<String> output =
|
|
await runCapturingPrint(runner, <String>['podspec-check']);
|
|
|
|
expect(
|
|
output,
|
|
containsAllInOrder(
|
|
<Matcher>[contains('Ran for 1 package(s)')],
|
|
));
|
|
});
|
|
});
|
|
}
|