Unverified Commit 4fde217d authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Add observatory Bonjour service to built iOS Info.plist bundle (#64988)

parent a8281e31
......@@ -14,8 +14,6 @@ import 'package:path/path.dart' as path;
Future<void> main() async {
await task(() async {
try {
bool foundProjectName = false;
bool bitcode = false;
await runProjectTest((FlutterProject flutterProject) async {
section('Build app with with --obfuscate');
await inDirectory(flutterProject.rootPath, () async {
......@@ -52,6 +50,13 @@ Future<void> main() async {
fail('Failed to produce expected output at ${outputAppFrameworkBinary.path}');
}
if (await dartObservatoryBonjourServiceFound(outputAppPath)) {
throw TaskResult.failure('Release bundle has unexpected NSBonjourServices');
}
if (await localNetworkUsageFound(outputAppPath)) {
throw TaskResult.failure('Release bundle has unexpected NSLocalNetworkUsageDescription');
}
section('Validate obfuscation');
// Verify that an identifier from the Dart project code is not present
......@@ -63,11 +68,11 @@ Future<void> main() async {
canFail: true,
);
if (response.trim().contains('matches')) {
foundProjectName = true;
throw TaskResult.failure('Found project name in obfuscated dart library');
}
});
section('Validate bitcode');
section('Validate release contents');
final Directory outputFlutterFramework = Directory(path.join(
flutterProject.rootPath,
......@@ -83,7 +88,13 @@ Future<void> main() async {
if (!outputFlutterFrameworkBinary.existsSync()) {
fail('Failed to produce expected output at ${outputFlutterFrameworkBinary.path}');
}
bitcode = await containsBitcode(outputFlutterFrameworkBinary.path);
// Archiving should contain a bitcode blob, but not building in release.
// This mimics Xcode behavior and present a developer from having to install a
// 300+MB app to test devices.
if (await containsBitcode(outputFlutterFrameworkBinary.path)) {
throw TaskResult.failure('Bitcode present in Flutter.framework');
}
section('Xcode backend script');
......@@ -101,7 +112,7 @@ Future<void> main() async {
'xcode_backend.sh'
);
// Simulate a commonly Xcode build setting misconfiguration
// Simulate a common Xcode build setting misconfiguration
// where FLUTTER_APPLICATION_PATH is missing
final int result = await exec(
xcodeBackendPath,
......@@ -111,6 +122,7 @@ Future<void> main() async {
'TARGET_BUILD_DIR': buildPath,
'FRAMEWORKS_FOLDER_PATH': 'Runner.app/Frameworks',
'VERBOSE_SCRIPT_LOGGING': '1',
'FLUTTER_BUILD_MODE': 'release',
'ACTION': 'install', // Skip bitcode stripping since we just checked that above.
},
);
......@@ -126,17 +138,35 @@ Future<void> main() async {
if (!outputAppFrameworkBinary.existsSync()) {
fail('Failed to re-embed ${outputAppFrameworkBinary.path}');
}
});
if (foundProjectName) {
return TaskResult.failure('Found project name in obfuscated dart library');
}
// Archiving should contain a bitcode blob, but not building in release.
// This mimics Xcode behavior and present a developer from having to install a
// 300+MB app to test devices.
if (bitcode) {
return TaskResult.failure('Bitcode present in Flutter.framework');
}
section('Clean build');
await inDirectory(flutterProject.rootPath, () async {
await flutter('clean');
});
section('Validate debug contents');
await inDirectory(flutterProject.rootPath, () async {
await flutter('build', options: <String>[
'ios',
'--debug',
'--no-codesign',
]);
});
// Debug should also not contain bitcode.
if (await containsBitcode(outputFlutterFrameworkBinary.path)) {
throw TaskResult.failure('Bitcode present in Flutter.framework');
}
if (!await dartObservatoryBonjourServiceFound(outputAppPath)) {
throw TaskResult.failure('Debug bundle is missing NSBonjourServices');
}
if (!await localNetworkUsageFound(outputAppPath)) {
throw TaskResult.failure('Debug bundle is missing NSLocalNetworkUsageDescription');
}
});
return TaskResult.success(null);
} on TaskResult catch (taskResult) {
......
......@@ -5,6 +5,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:path/path.dart' as path;
import 'utils.dart';
typedef SimulatorFunction = Future<void> Function(String deviceId);
......@@ -102,6 +104,40 @@ Future<bool> containsBitcode(String pathToBinary) async {
return !emptyBitcodeMarkerFound;
}
Future<bool> dartObservatoryBonjourServiceFound(String appBundlePath) async =>
(await eval(
'plutil',
<String>[
'-extract',
'NSBonjourServices',
'xml1',
'-o',
'-',
path.join(
appBundlePath,
'Info.plist',
),
],
canFail: true,
)).contains('_dartobservatory._tcp');
Future<bool> localNetworkUsageFound(String appBundlePath) async =>
await exec(
'plutil',
<String>[
'-extract',
'NSLocalNetworkUsageDescription',
'xml1',
'-o',
'-',
path.join(
appBundlePath,
'Info.plist',
),
],
canFail: true,
) == 0;
/// Creates and boots a new simulator, passes the new simulator's identifier to
/// `testFunction`.
///
......
......@@ -38,6 +38,32 @@ AssertExists() {
return 0
}
ParseFlutterBuildMode() {
# Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
# This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
# they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
case "$build_mode" in
*release*) build_mode="release";;
*profile*) build_mode="profile";;
*debug*) build_mode="debug";;
*)
EchoError "========================================================================"
EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}."
EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."
EchoError "This is controlled by the FLUTTER_BUILD_MODE environment variable."
EchoError "If that is not set, the CONFIGURATION environment variable is used."
EchoError ""
EchoError "You can fix this by either adding an appropriately named build"
EchoError "configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the"
EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})."
EchoError "========================================================================"
exit -1;;
esac
echo "${build_mode}"
}
BuildApp() {
local project_path="${SOURCE_ROOT}/.."
if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then
......@@ -72,24 +98,12 @@ BuildApp() {
# Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
# This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
# they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
local build_mode="$(ParseFlutterBuildMode)"
local artifact_variant="unknown"
case "$build_mode" in
*release*) build_mode="release"; artifact_variant="ios-release";;
*profile*) build_mode="profile"; artifact_variant="ios-profile";;
*debug*) build_mode="debug"; artifact_variant="ios";;
*)
EchoError "========================================================================"
EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}."
EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."
EchoError "This is controlled by the FLUTTER_BUILD_MODE environment variable."
EchoError "If that is not set, the CONFIGURATION environment variable is used."
EchoError ""
EchoError "You can fix this by either adding an appropriately named build"
EchoError "configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the"
EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})."
EchoError "========================================================================"
exit -1;;
release ) artifact_variant="ios-release";;
profile ) artifact_variant="ios-profile";;
debug ) artifact_variant="ios";;
esac
# Warn the user if not archiving (ACTION=install) in release mode.
......@@ -127,7 +141,7 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr
fi
local bitcode_flag=""
if [[ $ENABLE_BITCODE == "YES" ]]; then
if [[ "$ENABLE_BITCODE" == "YES" ]]; then
bitcode_flag="true"
fi
......@@ -306,6 +320,32 @@ EmbedFlutterFrameworks() {
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/App.framework/App"
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi
AddObservatoryBonjourService
}
# Add the observatory publisher Bonjour service to the produced app bundle Info.plist.
AddObservatoryBonjourService() {
local build_mode="$(ParseFlutterBuildMode)"
# Debug and profile only.
if [[ "${build_mode}" == "release" ]]; then
return
fi
local built_products_plist="${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}"
# If there are already NSBonjourServices specified by the app (uncommon), insert the observatory service name to the existing list.
if plutil -extract NSBonjourServices xml1 -o - "${built_products_plist}"; then
RunCommand plutil -insert NSBonjourServices.0 -string "_dartobservatory._tcp" "${built_products_plist}"
else
# Otherwise, add the NSBonjourServices key and observatory service name.
RunCommand plutil -insert NSBonjourServices -json "[\"_dartobservatory._tcp\"]" "${built_products_plist}"
fi
# Don't override the local network description the Flutter app developer specified (uncommon).
# This text will appear below the "Your app would like to find and connect to devices on your local network" permissions popup.
if ! plutil -extract NSLocalNetworkUsageDescription xml1 -o - "${built_products_plist}"; then
RunCommand plutil -insert NSLocalNetworkUsageDescription -string "Allow Flutter tools on your computer to connect and debug your application. This prompt will not appear on release builds." "${built_products_plist}"
fi
}
EmbedAndThinFrameworks() {
......@@ -328,5 +368,8 @@ else
EmbedFlutterFrameworks ;;
"embed_and_thin")
EmbedAndThinFrameworks ;;
"test_observatory_bonjour_service")
# Exposed for integration testing only.
AddObservatoryBonjourService ;;
esac
fi
......@@ -2,9 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart';
import '../src/common.dart';
const String xcodeBackendPath = 'bin/xcode_backend.sh';
const String xcodeBackendErrorHeader = '========================================================================';
......@@ -20,24 +24,18 @@ const Map<String, String> unknownFlutterBuildMode = <String, String>{
'CONFIGURATION': 'Debug',
};
// Can't archive a non-release build.
const Map<String, String> installWithoutRelease = <String, String>{
'CONFIGURATION': 'Debug',
'ACTION': 'install',
};
// Can't use a debug engine build with a release build.
const Map<String, String> localEngineDebugBuildModeRelease = <String, String>{
'SOURCE_ROOT': '../../../examples/hello_world',
'FLUTTER_ROOT': '../../..',
'SOURCE_ROOT': '../examples/hello_world',
'FLUTTER_ROOT': '../..',
'LOCAL_ENGINE': '/engine/src/out/ios_debug_unopt',
'CONFIGURATION': 'Release',
};
// Can't use a debug build with a profile engine.
const Map<String, String> localEngineProfileBuildeModeRelease = <String, String>{
'SOURCE_ROOT': '../../../examples/hello_world',
'FLUTTER_ROOT': '../../..',
'SOURCE_ROOT': '../examples/hello_world',
'FLUTTER_ROOT': '../..',
'LOCAL_ENGINE': '/engine/src/out/ios_profile',
'CONFIGURATION': 'Debug',
'FLUTTER_BUILD_MODE': 'Debug',
......@@ -57,8 +55,127 @@ void main() {
test('Xcode backend fails for on unsupported configuration combinations', () async {
await expectXcodeBackendFails(unknownConfiguration);
await expectXcodeBackendFails(unknownFlutterBuildMode);
await expectXcodeBackendFails(installWithoutRelease);
await expectXcodeBackendFails(localEngineDebugBuildModeRelease);
await expectXcodeBackendFails(localEngineProfileBuildeModeRelease);
}, skip: true); // https://github.com/flutter/flutter/issues/35707 (non-hermetic test requires precache to have run)
}, skip: !io.Platform.isMacOS);
test('Xcode backend warns archiving a non-release build.', () async {
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>['build'],
environment: <String, String>{
'CONFIGURATION': 'Debug',
'ACTION': 'install',
},
);
expect(result.stdout, contains('warning: Flutter archive not built in Release mode.'));
expect(result.exitCode, isNot(0));
}, skip: !io.Platform.isMacOS);
group('observatory Bonjour service keys', () {
Directory buildDirectory;
File infoPlist;
setUp(() {
buildDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_tools_xcode_backend_test.');
infoPlist = buildDirectory.childFile('Info.plist');
});
const String emptyPlist = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>''';
test('does not add keys in Release', () async {
infoPlist.writeAsStringSync(emptyPlist);
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>['test_observatory_bonjour_service'],
environment: <String, String>{
'CONFIGURATION': 'Release',
'BUILT_PRODUCTS_DIR': buildDirectory.path,
'INFOPLIST_PATH': 'Info.plist',
},
);
print(result.stderr);
final String actualInfoPlist = infoPlist.readAsStringSync();
expect(actualInfoPlist, isNot(contains('NSBonjourServices')));
expect(actualInfoPlist, isNot(contains('dartobservatory')));
expect(actualInfoPlist, isNot(contains('NSLocalNetworkUsageDescription')));
expect(result.exitCode, 0);
});
for (final String buildConfiguration in <String>['Debug', 'Profile']) {
test('add keys in $buildConfiguration', () async {
infoPlist.writeAsStringSync(emptyPlist);
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>['test_observatory_bonjour_service'],
environment: <String, String>{
'CONFIGURATION': buildConfiguration,
'BUILT_PRODUCTS_DIR': buildDirectory.path,
'INFOPLIST_PATH': 'Info.plist',
},
);
print(result.stderr);
final String actualInfoPlist = infoPlist.readAsStringSync();
expect(actualInfoPlist, contains('NSBonjourServices'));
expect(actualInfoPlist, contains('dartobservatory'));
expect(actualInfoPlist, contains('NSLocalNetworkUsageDescription'));
expect(result.exitCode, 0);
});
}
test('adds to existing Bonjour services, does not override network usage description', () async {
infoPlist.writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBonjourServices</key>
<array>
<string>_bogus._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Don't override this</string>
</dict>
</plist>''');
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>['test_observatory_bonjour_service'],
environment: <String, String>{
'CONFIGURATION': 'Debug',
'BUILT_PRODUCTS_DIR': buildDirectory.path,
'INFOPLIST_PATH': 'Info.plist',
},
);
expect(infoPlist.readAsStringSync(), '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBonjourServices</key>
<array>
<string>_dartobservatory._tcp</string>
<string>_bogus._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Don't override this</string>
</dict>
</plist>
''');
expect(result.exitCode, 0);
});
}, skip: !io.Platform.isMacOS);
}
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