Unverified Commit 8aad1190 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Check for watch companion in build settings (#114078)

parent cece4a5d
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/ios.dart';
import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -28,54 +27,19 @@ Future<void> main() async { ...@@ -28,54 +27,19 @@ Future<void> main() async {
section('Create release build'); section('Create release build');
// This only builds the iOS app, not the companion watchOS app. The watchOS app // This attempts to build the companion watchOS app. However, the watchOS
// has been removed as a build dependency and is not embedded in the app to avoid // SDK is not available in CI and therefore the build will fail.
// requiring the watchOS being available in CI. // Check to make sure that the tool attempts to build the companion watchOS app.
// Instead, validate the tool detects that there is a watch companion, and omits
// the "-sdk iphoneos" option, which fails to build the watchOS app.
// See https://github.com/flutter/flutter/pull/94190. // See https://github.com/flutter/flutter/pull/94190.
await inDirectory(projectDir, () async { await inDirectory(projectDir, () async {
final String buildOutput = await evalFlutter( final String buildOutput = await evalFlutter(
'build', 'build',
options: <String>['ios', '--no-codesign', '--release', '--verbose'], options: <String>['ios', '--no-codesign', '--release', '--verbose'],
); );
if (!buildOutput.contains('Watch companion app found')) { if (!buildOutput.contains('-destination generic/platform=watchOS')) {
throw TaskResult.failure('Did not detect watch companion'); print(buildOutput);
throw TaskResult.failure('Did not try to get watch build settings');
} }
if (buildOutput.contains('-sdk iphoneos -destination')) {
throw TaskResult.failure('-sdk must be omitted for app with watch companion');
}
});
final String appBundle = Directory(path.join(
projectDir.path,
'build',
'ios',
'iphoneos',
'Runner.app',
)).path;
final String appFrameworkPath = path.join(
appBundle,
'Frameworks',
'App.framework',
'App',
);
final String flutterFrameworkPath = path.join(
appBundle,
'Frameworks',
'Flutter.framework',
'Flutter',
);
checkDirectoryExists(appBundle);
await _checkFlutterFrameworkArchs(appFrameworkPath);
await _checkFlutterFrameworkArchs(flutterFrameworkPath);
section('Clean build');
await inDirectory(projectDir, () async {
await flutter('clean');
}); });
return TaskResult.success(null); return TaskResult.success(null);
...@@ -86,16 +50,3 @@ Future<void> main() async { ...@@ -86,16 +50,3 @@ Future<void> main() async {
} }
}); });
} }
Future<void> _checkFlutterFrameworkArchs(String frameworkPath) async {
checkFileExists(frameworkPath);
final String archs = await fileType(frameworkPath);
if (!archs.contains('arm64')) {
throw TaskResult.failure('$frameworkPath arm64 architecture missing');
}
if (archs.contains('x86_64')) {
throw TaskResult.failure('$frameworkPath x86_64 architecture unexpectedly present');
}
}
...@@ -727,6 +727,7 @@ ...@@ -727,6 +727,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = watch_Extension; IBSC_MODULE = watch_Extension;
INFOPLIST_FILE = watch/Info.plist; INFOPLIST_FILE = watch/Info.plist;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.flutter.extensionTest;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
...@@ -757,6 +758,7 @@ ...@@ -757,6 +758,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = watch_Extension; IBSC_MODULE = watch_Extension;
INFOPLIST_FILE = watch/Info.plist; INFOPLIST_FILE = watch/Info.plist;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.flutter.extensionTest;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_LDFLAGS = ""; OTHER_LDFLAGS = "";
...@@ -785,6 +787,7 @@ ...@@ -785,6 +787,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = watch_Extension; IBSC_MODULE = watch_Extension;
INFOPLIST_FILE = watch/Info.plist; INFOPLIST_FILE = watch/Info.plist;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.flutter.extensionTest;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_LDFLAGS = ""; OTHER_LDFLAGS = "";
......
...@@ -25,8 +25,6 @@ ...@@ -25,8 +25,6 @@
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> </array>
<key>WKCompanionAppBundleIdentifier</key>
<string>$(APP_BUNDLE_IDENTIFIER)</string>
<key>WKWatchKitApp</key> <key>WKWatchKitApp</key>
<true/> <true/>
</dict> </dict>
......
...@@ -265,9 +265,9 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -265,9 +265,9 @@ Future<XcodeBuildResult> buildXcodeProject({
// Check if the project contains a watchOS companion app. // Check if the project contains a watchOS companion app.
final bool hasWatchCompanion = await app.project.containsWatchCompanion( final bool hasWatchCompanion = await app.project.containsWatchCompanion(
projectInfo.targets, projectInfo: projectInfo,
buildInfo, buildInfo: buildInfo,
deviceID, deviceId: deviceID,
); );
if (hasWatchCompanion) { if (hasWatchCompanion) {
// The -sdk argument has to be omitted if a watchOS companion app exists. // The -sdk argument has to be omitted if a watchOS companion app exists.
......
...@@ -198,7 +198,11 @@ class XcodeProjectInterpreter { ...@@ -198,7 +198,11 @@ class XcodeProjectInterpreter {
if (buildContext.environmentType == EnvironmentType.simulator) if (buildContext.environmentType == EnvironmentType.simulator)
...<String>['-sdk', 'iphonesimulator'], ...<String>['-sdk', 'iphonesimulator'],
'-destination', '-destination',
if (deviceId != null) if (buildContext.isWatch == true && buildContext.environmentType == EnvironmentType.physical)
'generic/platform=watchOS'
else if (buildContext.isWatch == true)
'generic/platform=watchOS Simulator'
else if (deviceId != null)
'id=$deviceId' 'id=$deviceId'
else if (buildContext.environmentType == EnvironmentType.physical) else if (buildContext.environmentType == EnvironmentType.physical)
'generic/platform=iOS' 'generic/platform=iOS'
...@@ -376,12 +380,14 @@ class XcodeProjectBuildContext { ...@@ -376,12 +380,14 @@ class XcodeProjectBuildContext {
this.configuration, this.configuration,
this.environmentType = EnvironmentType.physical, this.environmentType = EnvironmentType.physical,
this.deviceId, this.deviceId,
this.isWatch = false,
}); });
final String? scheme; final String? scheme;
final String? configuration; final String? configuration;
final EnvironmentType environmentType; final EnvironmentType environmentType;
final String? deviceId; final String? deviceId;
final bool isWatch;
@override @override
int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId); int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId);
...@@ -395,7 +401,8 @@ class XcodeProjectBuildContext { ...@@ -395,7 +401,8 @@ class XcodeProjectBuildContext {
other.scheme == scheme && other.scheme == scheme &&
other.configuration == configuration && other.configuration == configuration &&
other.deviceId == deviceId && other.deviceId == deviceId &&
other.environmentType == environmentType; other.environmentType == environmentType &&
other.isWatch == isWatch;
} }
} }
......
...@@ -256,6 +256,8 @@ class IosProject extends XcodeBasedProject { ...@@ -256,6 +256,8 @@ class IosProject extends XcodeBasedProject {
BuildInfo? buildInfo, { BuildInfo? buildInfo, {
EnvironmentType environmentType = EnvironmentType.physical, EnvironmentType environmentType = EnvironmentType.physical,
String? deviceId, String? deviceId,
String? scheme,
bool isWatch = false,
}) async { }) async {
if (!existsSync()) { if (!existsSync()) {
return null; return null;
...@@ -265,9 +267,11 @@ class IosProject extends XcodeBasedProject { ...@@ -265,9 +267,11 @@ class IosProject extends XcodeBasedProject {
return null; return null;
} }
final String? scheme = info.schemeFor(buildInfo);
if (scheme == null) { if (scheme == null) {
info.reportFlavorNotFoundAndExit(); scheme = info.schemeFor(buildInfo);
if (scheme == null) {
info.reportFlavorNotFoundAndExit();
}
} }
final String? configuration = (await projectInfo())?.buildConfigurationFor( final String? configuration = (await projectInfo())?.buildConfigurationFor(
...@@ -279,6 +283,7 @@ class IosProject extends XcodeBasedProject { ...@@ -279,6 +283,7 @@ class IosProject extends XcodeBasedProject {
scheme: scheme, scheme: scheme,
configuration: configuration, configuration: configuration,
deviceId: deviceId, deviceId: deviceId,
isWatch: isWatch,
); );
final Map<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext]; final Map<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
if (currentBuildSettings == null) { if (currentBuildSettings == null) {
...@@ -327,17 +332,21 @@ class IosProject extends XcodeBasedProject { ...@@ -327,17 +332,21 @@ class IosProject extends XcodeBasedProject {
} }
/// Check if one the [targets] of the project is a watchOS companion app target. /// Check if one the [targets] of the project is a watchOS companion app target.
Future<bool> containsWatchCompanion(List<String> targets, BuildInfo buildInfo, String? deviceId) async { Future<bool> containsWatchCompanion({
required XcodeProjectInfo projectInfo,
required BuildInfo buildInfo,
String? deviceId,
}) async {
final String? bundleIdentifier = await productBundleIdentifier(buildInfo); final String? bundleIdentifier = await productBundleIdentifier(buildInfo);
// A bundle identifier is required for a companion app. // A bundle identifier is required for a companion app.
if (bundleIdentifier == null) { if (bundleIdentifier == null) {
return false; return false;
} }
for (final String target in targets) { for (final String target in projectInfo.targets) {
// Create Info.plist file of the target. // Create Info.plist file of the target.
final File infoFile = hostAppRoot.childDirectory(target).childFile('Info.plist'); final File infoFile = hostAppRoot.childDirectory(target).childFile('Info.plist');
// The Info.plist file of a target contains the key WKCompanionAppBundleIdentifier, // In older versions of Xcode, if the target was a watchOS companion app,
// if it is a watchOS companion app. // the Info.plist file of the target contained the key WKCompanionAppBundleIdentifier.
if (infoFile.existsSync()) { if (infoFile.existsSync()) {
final String? fromPlist = globals.plistParser.getStringValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier'); final String? fromPlist = globals.plistParser.getStringValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier');
if (bundleIdentifier == fromPlist) { if (bundleIdentifier == fromPlist) {
...@@ -357,6 +366,43 @@ class IosProject extends XcodeBasedProject { ...@@ -357,6 +366,43 @@ class IosProject extends XcodeBasedProject {
} }
} }
} }
// If key not found in Info.plist above, do more expensive check of build settings.
// In newer versions of Xcode, the build settings of the watchOS companion
// app's scheme should contain the key INFOPLIST_KEY_WKCompanionAppBundleIdentifier.
final bool watchIdentifierFound = xcodeProjectInfoFile.readAsStringSync().contains('WKCompanionAppBundleIdentifier');
if (watchIdentifierFound == false) {
return false;
}
final String? defaultScheme = projectInfo.schemeFor(buildInfo);
if (defaultScheme == null) {
projectInfo.reportFlavorNotFoundAndExit();
}
for (final String scheme in projectInfo.schemes) {
// the default scheme should not be a watch scheme, so skip it
if (scheme == defaultScheme) {
continue;
}
final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(
buildInfo,
deviceId: deviceId,
scheme: scheme,
isWatch: true,
);
if (allBuildSettings != null) {
final String? fromBuild = allBuildSettings['INFOPLIST_KEY_WKCompanionAppBundleIdentifier'];
if (bundleIdentifier == fromBuild) {
return true;
}
if (fromBuild != null && fromBuild.contains(r'$')) {
final String substitutedVariable = substituteXcodeVariables(fromBuild, allBuildSettings);
if (substitutedVariable == bundleIdentifier) {
return true;
}
}
}
}
return false; return false;
} }
......
...@@ -395,6 +395,76 @@ void main() { ...@@ -395,6 +395,76 @@ void main() {
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('build settings uses watch destination if isWatch is true', () async {
platform.environment = const <String, String>{};
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
kx64CheckCommand,
FakeCommand(
command: <String>[
'xcrun',
'xcodebuild',
'-project',
'/',
'-destination',
'generic/platform=watchOS',
'-showBuildSettings',
'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
],
exitCode: 1,
),
]);
expect(
await xcodeProjectInterpreter.getBuildSettings(
'',
buildContext: const XcodeProjectBuildContext(isWatch: true),
),
const <String, String>{},
);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('build settings uses watch simulator destination if isWatch is true and environment type is simulator', () async {
platform.environment = const <String, String>{};
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
kx64CheckCommand,
FakeCommand(
command: <String>[
'xcrun',
'xcodebuild',
'-project',
'/',
'-sdk',
'iphonesimulator',
'-destination',
'generic/platform=watchOS Simulator',
'-showBuildSettings',
'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
],
exitCode: 1,
),
]);
expect(
await xcodeProjectInterpreter.getBuildSettings(
'',
buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator, isWatch: true),
),
const <String, String>{},
);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async { testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
platform.environment = const <String, String>{ platform.environment = const <String, String>{
'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
......
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