mirror of
https://github.com/flutter/packages.git
synced 2025-06-04 10:18:12 +08:00

Adds privacy manifests to all iOS plugins. While we only *need* to do the plugins listed [here](https://developer.apple.com/support/third-party-SDK-requirements/) for now, the wording of the page: > The following are commonly used SDKs in apps on the App Store suggests that the list of things for which this is required is just an arbitrary cutoff rather than a conceptual distinction, so it seems safest to just assume the list will grow over time and do all of them. To ensure that, this includes new repo tooling to check that a manifest is specified in the podspec. The large caveat is that we do not currently know if this actually works. This is the method of inclusion that seems to be [the consensus among people using Cocoapods](https://github.com/CocoaPods/CocoaPods/issues/10325), as bundling it directly as a `resource` causes problems for clients who do not use `use_frameworks`. (In theory it seems like a manifest would not actually be *required* in that case since there is no framework, but it has the potential to actually stomp top-level resources.) Hopefully the automated analysis that Apple will eventually roll out will tolerate the file being bundled in a resource bundle in the framework rather than a top-level manifest file. If not, however, it's not clear how Cocoapods can be supported, so we can adopt this common approach for now under the assumption that eventually tooling will adapt to the reality of the ecosystem, and revisit the exact bundling later if necessary. Only `shared_preferences` has a non-empty manifest, as it is our only plugin that uses a required reason API, and none of our plugins themselves collect private data. Ideally for that plugin we would instead use `C56D.1`, which is for wrappers, but as currently written we can't use it since it's exclusively a wrapper. If that changes in the future based on our pending request, we can revisit. For now, however, this reason should suffice since we don't currently allow reading from other app groups. Fixes https://github.com/flutter/flutter/issues/131495 Fixes https://github.com/flutter/flutter/issues/139756 Fixes https://github.com/flutter/flutter/issues/139757 Fixes https://github.com/flutter/flutter/issues/139758 Fixes https://github.com/flutter/flutter/issues/139759 Fixes https://github.com/flutter/flutter/issues/139760 See also https://github.com/flutter/flutter/issues/139761
536 lines
17 KiB
Dart
536 lines
17 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']);
|
|
_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 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)')],
|
|
));
|
|
});
|
|
});
|
|
}
|