Unverified Commit ace54425 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

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

parent 020215bb
...@@ -14,8 +14,6 @@ import 'package:path/path.dart' as path; ...@@ -14,8 +14,6 @@ import 'package:path/path.dart' as path;
Future<void> main() async { Future<void> main() async {
await task(() async { await task(() async {
try { try {
bool foundProjectName = false;
bool bitcode = false;
await runProjectTest((FlutterProject flutterProject) async { await runProjectTest((FlutterProject flutterProject) async {
section('Build app with with --obfuscate'); section('Build app with with --obfuscate');
await inDirectory(flutterProject.rootPath, () async { await inDirectory(flutterProject.rootPath, () async {
...@@ -52,6 +50,13 @@ Future<void> main() async { ...@@ -52,6 +50,13 @@ Future<void> main() async {
fail('Failed to produce expected output at ${outputAppFrameworkBinary.path}'); 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'); section('Validate obfuscation');
// Verify that an identifier from the Dart project code is not present // Verify that an identifier from the Dart project code is not present
...@@ -63,11 +68,11 @@ Future<void> main() async { ...@@ -63,11 +68,11 @@ Future<void> main() async {
canFail: true, canFail: true,
); );
if (response.trim().contains('matches')) { 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( final Directory outputFlutterFramework = Directory(path.join(
flutterProject.rootPath, flutterProject.rootPath,
...@@ -83,7 +88,13 @@ Future<void> main() async { ...@@ -83,7 +88,13 @@ Future<void> main() async {
if (!outputFlutterFrameworkBinary.existsSync()) { if (!outputFlutterFrameworkBinary.existsSync()) {
fail('Failed to produce expected output at ${outputFlutterFrameworkBinary.path}'); 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'); section('Xcode backend script');
...@@ -101,7 +112,7 @@ Future<void> main() async { ...@@ -101,7 +112,7 @@ Future<void> main() async {
'xcode_backend.sh' 'xcode_backend.sh'
); );
// Simulate a commonly Xcode build setting misconfiguration // Simulate a common Xcode build setting misconfiguration
// where FLUTTER_APPLICATION_PATH is missing // where FLUTTER_APPLICATION_PATH is missing
final int result = await exec( final int result = await exec(
xcodeBackendPath, xcodeBackendPath,
...@@ -111,6 +122,7 @@ Future<void> main() async { ...@@ -111,6 +122,7 @@ Future<void> main() async {
'TARGET_BUILD_DIR': buildPath, 'TARGET_BUILD_DIR': buildPath,
'FRAMEWORKS_FOLDER_PATH': 'Runner.app/Frameworks', 'FRAMEWORKS_FOLDER_PATH': 'Runner.app/Frameworks',
'VERBOSE_SCRIPT_LOGGING': '1', 'VERBOSE_SCRIPT_LOGGING': '1',
'FLUTTER_BUILD_MODE': 'release',
'ACTION': 'install', // Skip bitcode stripping since we just checked that above. 'ACTION': 'install', // Skip bitcode stripping since we just checked that above.
}, },
); );
...@@ -126,17 +138,35 @@ Future<void> main() async { ...@@ -126,17 +138,35 @@ Future<void> main() async {
if (!outputAppFrameworkBinary.existsSync()) { if (!outputAppFrameworkBinary.existsSync()) {
fail('Failed to re-embed ${outputAppFrameworkBinary.path}'); fail('Failed to re-embed ${outputAppFrameworkBinary.path}');
} }
section('Clean build');
await inDirectory(flutterProject.rootPath, () async {
await flutter('clean');
}); });
if (foundProjectName) { section('Validate debug contents');
return TaskResult.failure('Found project name in obfuscated dart library');
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');
} }
// Archiving should contain a bitcode blob, but not building in release.
// This mimics Xcode behavior and present a developer from having to install a if (!await dartObservatoryBonjourServiceFound(outputAppPath)) {
// 300+MB app to test devices. throw TaskResult.failure('Debug bundle is missing NSBonjourServices');
if (bitcode) { }
return TaskResult.failure('Bitcode present in Flutter.framework'); if (!await localNetworkUsageFound(outputAppPath)) {
throw TaskResult.failure('Debug bundle is missing NSLocalNetworkUsageDescription');
} }
});
return TaskResult.success(null); return TaskResult.success(null);
} on TaskResult catch (taskResult) { } on TaskResult catch (taskResult) {
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:path/path.dart' as path;
import 'utils.dart'; import 'utils.dart';
typedef SimulatorFunction = Future<void> Function(String deviceId); typedef SimulatorFunction = Future<void> Function(String deviceId);
...@@ -102,6 +104,40 @@ Future<bool> containsBitcode(String pathToBinary) async { ...@@ -102,6 +104,40 @@ Future<bool> containsBitcode(String pathToBinary) async {
return !emptyBitcodeMarkerFound; 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 /// Creates and boots a new simulator, passes the new simulator's identifier to
/// `testFunction`. /// `testFunction`.
/// ///
......
...@@ -38,6 +38,32 @@ AssertExists() { ...@@ -38,6 +38,32 @@ AssertExists() {
return 0 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() { BuildApp() {
local project_path="${SOURCE_ROOT}/.." local project_path="${SOURCE_ROOT}/.."
if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then
...@@ -72,24 +98,12 @@ BuildApp() { ...@@ -72,24 +98,12 @@ BuildApp() {
# Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name # 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, # 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. # 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" local artifact_variant="unknown"
case "$build_mode" in case "$build_mode" in
*release*) build_mode="release"; artifact_variant="ios-release";; release ) artifact_variant="ios-release";;
*profile*) build_mode="profile"; artifact_variant="ios-profile";; profile ) artifact_variant="ios-profile";;
*debug*) build_mode="debug"; artifact_variant="ios";; 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;;
esac esac
# Warn the user if not archiving (ACTION=install) in release mode. # 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 ...@@ -127,7 +141,7 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr
fi fi
local bitcode_flag="" local bitcode_flag=""
if [[ $ENABLE_BITCODE == "YES" ]]; then if [[ "$ENABLE_BITCODE" == "YES" ]]; then
bitcode_flag="true" bitcode_flag="true"
fi fi
...@@ -306,6 +320,36 @@ EmbedFlutterFrameworks() { ...@@ -306,6 +320,36 @@ 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}/App.framework/App"
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter" RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi 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 [[ ! -f "${built_products_plist}" ]]; then
EchoError "error: ${INFOPLIST_PATH} does not exist. The Flutter \"Thin Binary\" build phase must run after \"Copy Bundle Resources\"."
exit -1
fi
# 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() { EmbedAndThinFrameworks() {
...@@ -328,5 +372,8 @@ else ...@@ -328,5 +372,8 @@ else
EmbedFlutterFrameworks ;; EmbedFlutterFrameworks ;;
"embed_and_thin") "embed_and_thin")
EmbedAndThinFrameworks ;; EmbedAndThinFrameworks ;;
"test_observatory_bonjour_service")
# Exposed for integration testing only.
AddObservatoryBonjourService ;;
esac esac
fi fi
// Copyright 2014 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:flutter_tools/src/base/io.dart';
import '../../src/common.dart';
const String xcodeBackendPath = 'bin/xcode_backend.sh';
const String xcodeBackendErrorHeader = '========================================================================';
// Acceptable $CONFIGURATION/$FLUTTER_BUILD_MODE values should be debug, profile, or release
const Map<String, String> unknownConfiguration = <String, String>{
'CONFIGURATION': 'Custom',
};
// $FLUTTER_BUILD_MODE will override $CONFIGURATION
const Map<String, String> unknownFlutterBuildMode = <String, String>{
'FLUTTER_BUILD_MODE': 'Custom',
'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': '../../..',
'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': '../../..',
'LOCAL_ENGINE': '/engine/src/out/ios_profile',
'CONFIGURATION': 'Debug',
'FLUTTER_BUILD_MODE': 'Debug',
};
void main() {
Future<void> expectXcodeBackendFails(Map<String, String> environment) async {
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>['build'],
environment: environment,
);
expect(result.stderr, startsWith(xcodeBackendErrorHeader));
expect(result.exitCode, isNot(0));
}
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)
}
// Copyright 2014 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 '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';
const String xcodeBackendPath = 'bin/xcode_backend.sh';
const String xcodeBackendErrorHeader = '========================================================================';
// Acceptable $CONFIGURATION/$FLUTTER_BUILD_MODE values should be debug, profile, or release
const Map<String, String> unknownConfiguration = <String, String>{
'CONFIGURATION': 'Custom',
};
// $FLUTTER_BUILD_MODE will override $CONFIGURATION
const Map<String, String> unknownFlutterBuildMode = <String, String>{
'FLUTTER_BUILD_MODE': 'Custom',
'CONFIGURATION': 'Debug',
};
// 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': '../..',
'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': '../..',
'LOCAL_ENGINE': '/engine/src/out/ios_profile',
'CONFIGURATION': 'Debug',
'FLUTTER_BUILD_MODE': 'Debug',
};
void main() {
Future<void> expectXcodeBackendFails(Map<String, String> environment) async {
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>['build'],
environment: environment,
);
expect(result.stderr, startsWith(xcodeBackendErrorHeader));
expect(result.exitCode, isNot(0));
}
test('Xcode backend fails with no arguments', () async {
final ProcessResult result = await Process.run(
xcodeBackendPath,
<String>[],
environment: <String, String>{
'SOURCE_ROOT': '../examples/hello_world',
'FLUTTER_ROOT': '../..',
},
);
expect(result.stderr, startsWith('error: Your Xcode project is incompatible with this version of Flutter.'));
expect(result.exitCode, isNot(0));
}, skip: !io.Platform.isMacOS);
test('Xcode backend fails for on unsupported configuration combinations', () async {
await expectXcodeBackendFails(unknownConfiguration);
await expectXcodeBackendFails(unknownFlutterBuildMode);
await expectXcodeBackendFails(localEngineDebugBuildModeRelease);
await expectXcodeBackendFails(localEngineProfileBuildeModeRelease);
}, 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');
});
test('fails when the Info.plist is missing', () async {
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(result.stderr, startsWith('error: Info.plist does not exist.'));
expect(result.exitCode, isNot(0));
});
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',
},
);
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',
},
);
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