Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable sandboxing for macOS tests in CI #6866

Merged
75 changes: 74 additions & 1 deletion packages/pigeon/tool/shared/native_project_runners.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: avoid_print

import 'dart:io';

import 'package:path/path.dart' as path;

import 'flutter_utils.dart';
import 'process_utils.dart';

Expand Down Expand Up @@ -38,11 +44,23 @@ Future<int> runFlutterBuild(
}

Future<int> runXcodeBuild(
String nativeProjectDirectory, {
String nativeProjectDirectory,
String platform, {
String? sdk,
String? destination,
String? configuration,
List<String> extraArguments = const <String>[],
}) {
File? disabledSandboxEntitlementFile;
if (extraArguments.contains('test') &&
platform.toLowerCase() == 'macos' &&
Platform.environment['LUCI_CI'] == 'True') {
vashworth marked this conversation as resolved.
Show resolved Hide resolved
disabledSandboxEntitlementFile = _createDisabledSandboxEntitlementFile(
nativeProjectDirectory,
configuration ?? 'Debug',
);
}

return runProcess(
'xcodebuild',
<String>[
Expand All @@ -52,12 +70,67 @@ Future<int> runXcodeBuild(
'Runner',
if (sdk != null) ...<String>['-sdk', sdk],
if (destination != null) ...<String>['-destination', destination],
if (configuration != null) ...<String>['-configuration', configuration],
...extraArguments,
if (disabledSandboxEntitlementFile != null)
'CODE_SIGN_ENTITLEMENTS=${disabledSandboxEntitlementFile.path}',
],
workingDirectory: nativeProjectDirectory,
);
}

/// Finds and copies macOS entitlements file. In the copy, disables sandboxing.
/// If entitlements file is not found, returns null.
///
/// As of macOS 14, testing a macOS sandbox app may prompt the user to grant
/// access to the app. To workaround this in CI, we create and use a entitlements
/// file with sandboxing disabled. See
/// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox.
File? _createDisabledSandboxEntitlementFile(
String platformDirectory,
String configuration,
) {
String entitlementDefaultFileName;
if (configuration == 'Release') {
entitlementDefaultFileName = 'Release';
} else {
entitlementDefaultFileName = 'DebugProfile';
}

final String entitlementFilePath = path.join(
platformDirectory,
'Runner',
'$entitlementDefaultFileName.entitlements',
);
final File entitlementFile = File(entitlementFilePath);

if (!entitlementFile.existsSync()) {
print('Unable to find entitlements file at ${entitlementFile.path}');
return null;
}

final String originalEntitlementFileContents =
entitlementFile.readAsStringSync();
final String tempEntitlementPath = Directory.systemTemp
.createTempSync('flutter_disable_sandbox_entitlement.')
.path;
final File disabledSandboxEntitlementFile = File(path.join(
tempEntitlementPath,
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements',
));
disabledSandboxEntitlementFile.createSync(recursive: true);
disabledSandboxEntitlementFile.writeAsStringSync(
originalEntitlementFileContents.replaceAll(
RegExp(r'<key>com\.apple\.security\.app-sandbox<\/key>[\S\s]*?<true\/>'),
'''
<key>com.apple.security.app-sandbox</key>
<false/>''',
),
);

return disabledSandboxEntitlementFile;
}
vashworth marked this conversation as resolved.
Show resolved Hide resolved

Future<int> runGradleBuild(String nativeProjectDirectory, [String? command]) {
return runProcess(
'./gradlew',
Expand Down
5 changes: 3 additions & 2 deletions packages/pigeon/tool/shared/test_suites.dart
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,9 @@ Future<int> _runMacOSSwiftUnitTests() async {

return runXcodeBuild(
'$examplePath/macos',
'macos',
configuration: 'Debug',
extraArguments: <String>[
'-configuration',
'Debug',
'test',
],
);
Expand Down Expand Up @@ -301,6 +301,7 @@ Future<int> _runIOSPluginUnitTests(String testPluginPath) async {
await _createSimulator(deviceName, deviceType, deviceRuntime);
return runXcodeBuild(
'$examplePath/ios',
'ios',
sdk: 'iphonesimulator',
destination: 'platform=iOS Simulator,name=$deviceName,OS=$deviceOS',
extraArguments: <String>['test'],
Expand Down
62 changes: 60 additions & 2 deletions script/tool/lib/src/common/xcode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,37 @@ class Xcode {

/// Runs an `xcodebuild` in [directory] with the given parameters.
Future<int> runXcodeBuild(
Directory directory, {
Directory exampleDirectory,
String platform, {
List<String> actions = const <String>['build'],
required String workspace,
required String scheme,
String? configuration,
List<String> extraFlags = const <String>[],
}) {
File? disabledSandboxEntitlementFile;
if (actions.contains('test') && platform.toLowerCase() == 'macos') {
disabledSandboxEntitlementFile = _createDisabledSandboxEntitlementFile(
exampleDirectory.childDirectory(platform.toLowerCase()),
configuration ?? 'Debug',
);
}
final List<String> args = <String>[
_xcodeBuildCommand,
...actions,
...<String>['-workspace', workspace],
...<String>['-scheme', scheme],
if (configuration != null) ...<String>['-configuration', configuration],
...extraFlags,
if (disabledSandboxEntitlementFile != null)
'CODE_SIGN_ENTITLEMENTS=${disabledSandboxEntitlementFile.path}',
];
final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}';
if (log) {
print(completeTestCommand);
}
return processRunner.runAndStream(_xcRunCommand, args,
workingDir: directory);
workingDir: exampleDirectory);
}

/// Returns true if [project], which should be an .xcodeproj directory,
Expand Down Expand Up @@ -156,4 +166,52 @@ class Xcode {
}
return null;
}

/// Finds and copies macOS entitlements file. In the copy, disables sandboxing.
/// If entitlements file is not found, returns null.
///
/// As of macOS 14, testing a macOS sandbox app may prompt the user to grant
/// access to the app. To workaround this in CI, we create and use a entitlements
/// file with sandboxing disabled. See
/// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox.
File? _createDisabledSandboxEntitlementFile(
Directory macosDirectory,
vashworth marked this conversation as resolved.
Show resolved Hide resolved
String configuration,
) {
String entitlementDefaultFileName;
if (configuration == 'Release') {
entitlementDefaultFileName = 'Release';
} else {
entitlementDefaultFileName = 'DebugProfile';
}
vashworth marked this conversation as resolved.
Show resolved Hide resolved

final File entitlementFile = macosDirectory
.childDirectory('Runner')
.childFile('$entitlementDefaultFileName.entitlements');

if (!entitlementFile.existsSync()) {
print('Unable to find entitlements file at ${entitlementFile.path}');
return null;
}

final String originalEntitlementFileContents =
entitlementFile.readAsStringSync();
final File disabledSandboxEntitlementFile = macosDirectory
.fileSystem.systemTempDirectory
.createTempSync('flutter_disable_sandbox_entitlement.')
.childFile(
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements');
disabledSandboxEntitlementFile.createSync(recursive: true);
disabledSandboxEntitlementFile.writeAsStringSync(
originalEntitlementFileContents.replaceAll(
RegExp(
r'<key>com\.apple\.security\.app-sandbox<\/key>[\S\s]*?<true\/>'),
'''
<key>com.apple.security.app-sandbox</key>
<false/>''',
),
);

return disabledSandboxEntitlementFile;
}
}
1 change: 1 addition & 0 deletions script/tool/lib/src/native_test_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ this command.
_printRunningExampleTestsMessage(example, platform);
final int exitCode = await _xcode.runXcodeBuild(
example.directory,
platform,
// Clean before testing to remove cached swiftmodules from previous
// runs, which can cause conflicts.
actions: <String>['clean', 'test'],
Expand Down
1 change: 1 addition & 0 deletions script/tool/lib/src/xcode_analyze_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand {
print('Running $platform tests and analyzer for $examplePath...');
final int exitCode = await _xcode.runXcodeBuild(
example.directory,
platform,
// Clean before analyzing to remove cached swiftmodules from previous
// runs, which can cause conflicts.
actions: <String>['clean', 'analyze'],
Expand Down
41 changes: 40 additions & 1 deletion script/tool/test/common/xcode_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:convert';

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/xcode.dart';
import 'package:test/test.dart';

Expand Down Expand Up @@ -161,6 +162,7 @@ void main() {

final int exitCode = await xcode.runXcodeBuild(
directory,
'ios',
workspace: 'A.xcworkspace',
scheme: 'AScheme',
);
Expand All @@ -186,7 +188,7 @@ void main() {
test('handles all arguments', () async {
final Directory directory = const LocalFileSystem().currentDirectory;

final int exitCode = await xcode.runXcodeBuild(directory,
final int exitCode = await xcode.runXcodeBuild(directory, 'ios',
actions: <String>['action1', 'action2'],
workspace: 'A.xcworkspace',
scheme: 'AScheme',
Expand Down Expand Up @@ -225,6 +227,7 @@ void main() {

final int exitCode = await xcode.runXcodeBuild(
directory,
'ios',
workspace: 'A.xcworkspace',
scheme: 'AScheme',
);
Expand All @@ -246,6 +249,42 @@ void main() {
directory.path),
]));
});

test('sets CODE_SIGN_ENTITLEMENTS for macos tests', () async {
final FileSystem fileSystem = MemoryFileSystem();
final Directory directory = fileSystem.currentDirectory;
directory
.childDirectory('macos')
.childDirectory('Runner')
.childFile('DebugProfile.entitlements')
.createSync(recursive: true);

final int exitCode = await xcode.runXcodeBuild(
directory,
'macos',
workspace: 'A.xcworkspace',
scheme: 'AScheme',
actions: <String>['test'],
);

expect(exitCode, 0);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'test',
'-workspace',
'A.xcworkspace',
'-scheme',
'AScheme',
'CODE_SIGN_ENTITLEMENTS=/.tmp_rand0/flutter_disable_sandbox_entitlement.rand0/DebugProfileWithDisabledSandboxing.entitlements'
],
directory.path),
]));
});
});

group('projectHasTarget', () {
Expand Down