[macos] add flavor options to commands in the flutter_tool (#118421)

* Add new macos target configured for flavors

* Rename Free App copy-Info.plist to Free App Info.plist

* Remove bogus entitlements

* Remove Generated.xcconfig

* Audit project.pbxproj

* Remove unused configs

* share one info.plist

* Modify scheme so that paid app works

* Codesign automatic

* Pipe flavor as scheme into xcodebuild

* Ignore incoming flavor string

* pipe flavor for flutter run to work

* Add devicelab tests

* Error if host and target device are same for flutter install desktop

* Avoid bang (!) by promoting a local.

Co-authored-by: Jenn Magder <magder@google.com>

* Add supportsInstall property

* Override  in test classes

* Add install test on macOS

* Refactor application_package and add tests for package directory

Co-authored-by: a-wallen <stephenwallen@google.com>
Co-authored-by: Jenn Magder <magder@google.com>
This commit is contained in:
Alex Wallen 2023-01-19 15:52:16 -08:00 committed by GitHub
parent 09bd0f6613
commit 73096fd96e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 173 additions and 43 deletions

View file

@ -2668,6 +2668,20 @@ targets:
- bin/**
- .ci.yaml
- name: Mac flavors_test_macos
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
dependencies: >-
[
{"dependency": "xcode", "version": "14a5294e"},
{"dependency": "gems", "version": "v3.3.14"}
]
tags: >
["devicelab", "hostonly", "mac"]
task_name: flavors_test_macos
- name: Mac flutter_gallery_macos__compile
presubmit: false
recipe: devicelab/devicelab_drone

View file

@ -220,6 +220,7 @@
/dev/devicelab/bin/tasks/complex_layout_win_desktop__start_up.dart @yaakovschectman @flutter/desktop
/dev/devicelab/bin/tasks/dart_plugin_registry_test.dart @stuartmorgan @flutter/plugin
/dev/devicelab/bin/tasks/entrypoint_dart_registrant.dart @aaclarke @flutter/plugin
/dev/devicelab/bin/tasks/flavors_test_macos.dart @a-wallen @flutter/desktop
/dev/devicelab/bin/tasks/flutter_gallery_macos__compile.dart @a-wallen @flutter/desktop
/dev/devicelab/bin/tasks/flutter_gallery_macos__start_up.dart @a-wallen @flutter/desktop
/dev/devicelab/bin/tasks/flutter_gallery_win_desktop__compile.dart @yaakovschectman @flutter/desktop

View file

@ -0,0 +1,39 @@
// 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_devicelab/framework/devices.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:flutter_devicelab/tasks/integration_tests.dart';
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.macos;
await task(() async {
await createFlavorsTest().call();
await createIntegrationTestFlavorsTest().call();
await inDirectory('${flutterDirectory.path}/dev/integration_tests/flavors', () async {
final StringBuffer stderr = StringBuffer();
await evalFlutter(
'install',
canFail: true,
stderr: stderr,
options: <String>[
'--d', 'macos',
'--flavor', 'free'
],
);
final String stderrString = stderr.toString();
if (!stderrString.contains('Host and target are the same. Nothing to install.')) {
print(stderrString);
return TaskResult.failure('Installing a macOS app on macOS should no-op');
}
});
return TaskResult.success(null);
});
}

View file

@ -651,6 +651,7 @@
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.flavors.free;
PRODUCT_FLAVOR = free;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -673,6 +674,7 @@
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.flavors.free;
PRODUCT_FLAVOR = free;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;

View file

@ -17,6 +17,32 @@ EchoError() {
echo "$@" 1>&2
}
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() {
# Set the working directory to the project root
local project_path="${SOURCE_ROOT}/.."
@ -28,8 +54,10 @@ BuildApp() {
target_path="${FLUTTER_TARGET}"
fi
# Set the build mode
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
# 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="$(ParseFlutterBuildMode)"
if [[ -n "$LOCAL_ENGINE" ]]; then
if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then

View file

@ -50,6 +50,11 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
if (device == null) {
throwToolExit('No target device found');
}
if (!device!.supportsInstall) {
throwToolExit('Host and target are the same. Nothing to install.');
}
if (userIdentifier != null && device is! AndroidDevice) {
throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
}

View file

@ -104,6 +104,9 @@ abstract class DesktopDevice extends Device {
@override
void clearLogs() {}
@override
bool get supportsInstall => false;
@override
Future<LaunchResult> startApp(
ApplicationPackage package, {
@ -123,9 +126,9 @@ abstract class DesktopDevice extends Device {
}
// Ensure that the executable is locatable.
final BuildMode buildMode = debuggingOptions.buildInfo.mode;
final BuildInfo buildInfo = debuggingOptions.buildInfo;
final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
final String? executable = executablePathForDevice(package, buildMode);
final String? executable = executablePathForDevice(package, buildInfo);
if (executable == null) {
_logger.printError('Unable to find executable to run');
return LaunchResult.failed();
@ -161,7 +164,7 @@ abstract class DesktopDevice extends Device {
try {
final Uri? observatoryUri = await observatoryDiscovery.uri;
if (observatoryUri != null) {
onAttached(package, buildMode, process);
onAttached(package, buildInfo, process);
return LaunchResult.succeeded(observatoryUri: observatoryUri);
}
_logger.printError(
@ -203,11 +206,11 @@ abstract class DesktopDevice extends Device {
/// Returns the path to the executable to run for [package] on this device for
/// the given [buildMode].
String? executablePathForDevice(ApplicationPackage package, BuildMode buildMode);
String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo);
/// Called after a process is attached, allowing any device-specific extra
/// steps to be run.
void onAttached(ApplicationPackage package, BuildMode buildMode, Process process) {}
void onAttached(ApplicationPackage package, BuildInfo buildInfo, Process process) {}
/// Computes a set of environment variables used to pass debugging information
/// to the engine without interfering with application level command line

View file

@ -579,6 +579,10 @@ abstract class Device {
/// Whether the device supports the '--fast-start' development mode.
bool get supportsFastStart => false;
/// Whether this device supports the installation of a flutter app via
/// `flutter install`.
bool get supportsInstall => true;
/// Stop an app package on the current device.
///
/// Specify [userIdentifier] to stop app installed to a profile (Android only).

View file

@ -70,8 +70,8 @@ class LinuxDevice extends DesktopDevice {
}
@override
String executablePathForDevice(covariant LinuxApp package, BuildMode buildMode) {
return package.executable(buildMode);
String executablePathForDevice(covariant LinuxApp package, BuildInfo buildInfo) {
return package.executable(buildInfo.mode);
}
}

View file

@ -106,9 +106,9 @@ abstract class MacOSApp extends ApplicationPackage {
@override
String get displayName => id;
String? applicationBundle(BuildMode buildMode);
String? applicationBundle(BuildInfo buildInfo);
String? executable(BuildMode buildMode);
String? executable(BuildInfo buildInfo);
}
class PrebuiltMacOSApp extends MacOSApp implements PrebuiltApplicationPackage {
@ -135,10 +135,10 @@ class PrebuiltMacOSApp extends MacOSApp implements PrebuiltApplicationPackage {
String get name => bundleName;
@override
String? applicationBundle(BuildMode buildMode) => uncompressedBundle.path;
String? applicationBundle(BuildInfo buildInfo) => uncompressedBundle.path;
@override
String? executable(BuildMode buildMode) => _executable;
String? executable(BuildInfo buildInfo) => _executable;
/// A [File] or [Directory] pointing to the application bundle.
///
@ -156,23 +156,30 @@ class BuildableMacOSApp extends MacOSApp {
String get name => 'macOS';
@override
String? applicationBundle(BuildMode buildMode) {
String? applicationBundle(BuildInfo buildInfo) {
final File appBundleNameFile = project.nameFile;
if (!appBundleNameFile.existsSync()) {
globals.printError('Unable to find app name. ${appBundleNameFile.path} does not exist');
return null;
}
return globals.fs.path.join(
getMacOSBuildDirectory(),
'Build',
'Products',
sentenceCase(getNameForBuildMode(buildMode)),
bundleDirectory(buildInfo),
appBundleNameFile.readAsStringSync().trim());
}
String bundleDirectory(BuildInfo buildInfo) {
return sentenceCase(buildInfo.mode.name) + (buildInfo.flavor != null
? ' ${sentenceCase(buildInfo.flavor!)}'
: '');
}
@override
String? executable(BuildMode buildMode) {
final String? directory = applicationBundle(buildMode);
String? executable(BuildInfo buildInfo) {
final String? directory = applicationBundle(buildInfo);
if (directory == null) {
return null;
}

View file

@ -114,7 +114,7 @@ Future<void> buildMacOS({
'xcodebuild',
'-workspace', xcodeWorkspace.path,
'-configuration', configuration,
'-scheme', 'Runner',
'-scheme', scheme,
'-derivedDataPath', flutterBuildDir.absolute.path,
'-destination', 'platform=macOS',
'OBJROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',

View file

@ -78,17 +78,17 @@ class MacOSDevice extends DesktopDevice {
}
@override
String? executablePathForDevice(covariant MacOSApp package, BuildMode buildMode) {
return package.executable(buildMode);
String? executablePathForDevice(covariant MacOSApp package, BuildInfo buildInfo) {
return package.executable(buildInfo);
}
@override
void onAttached(covariant MacOSApp package, BuildMode buildMode, Process process) {
void onAttached(covariant MacOSApp package, BuildInfo buildInfo, Process process) {
// Bring app to foreground. Ideally this would be done post-launch rather
// than post-attach, since this won't run for release builds, but there's
// no general-purpose way of knowing when a process is far enough along in
// the launch process for 'open' to foreground it.
final String? applicationBundle = package.applicationBundle(buildMode);
final String? applicationBundle = package.applicationBundle(buildInfo);
if (applicationBundle == null) {
_logger.printError('Failed to foreground app; application bundle not found');
return;

View file

@ -54,7 +54,7 @@ class MacOSDesignedForIPadDevice extends DesktopDevice {
}
@override
String? executablePathForDevice(ApplicationPackage package, BuildMode buildMode) => null;
String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo) => null;
@override
Future<LaunchResult> startApp(

View file

@ -61,8 +61,8 @@ class WindowsDevice extends DesktopDevice {
}
@override
String executablePathForDevice(covariant WindowsApp package, BuildMode buildMode) {
return package.executable(buildMode);
String executablePathForDevice(covariant WindowsApp package, BuildInfo buildInfo) {
return package.executable(buildInfo.mode);
}
}

View file

@ -157,6 +157,12 @@ class FakeIOSDevice extends Fake implements IOSDevice {
IOSApp app, {
String? userIdentifier,
}) async => true;
@override
String get name => 'iOS';
@override
bool get supportsInstall => true;
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
@ -177,4 +183,10 @@ class FakeAndroidDevice extends Fake implements AndroidDevice {
AndroidApk app, {
String? userIdentifier,
}) async => true;
@override
String get name => 'Android';
@override
bool get supportsInstall => true;
}

View file

@ -86,7 +86,7 @@ void main() {
),
]);
final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager, fileSystem: fileSystem);
final String? executableName = device.executablePathForDevice(FakeApplicationPackage(), BuildMode.debug);
final String? executableName = device.executablePathForDevice(FakeApplicationPackage(), BuildInfo.debug);
fileSystem.file(executableName).writeAsStringSync('\n');
final FakeApplicationPackage package = FakeApplicationPackage();
final LaunchResult result = await device.startApp(
@ -367,11 +367,11 @@ class FakeDesktopDevice extends DesktopDevice {
// Dummy implementation that just returns the build mode name.
@override
String? executablePathForDevice(ApplicationPackage package, BuildMode buildMode) {
String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo) {
if (nullExecutablePathForDevice) {
return null;
}
return buildMode == null ? 'null' : getNameForBuildMode(buildMode);
return buildInfo == null ? 'null' : getNameForBuildMode(buildInfo.mode);
}
}

View file

@ -154,9 +154,9 @@ void main() {
operatingSystemUtils: FakeOperatingSystemUtils(),
);
expect(device.executablePathForDevice(mockApp, BuildMode.debug), 'debug/executable');
expect(device.executablePathForDevice(mockApp, BuildMode.profile), 'profile/executable');
expect(device.executablePathForDevice(mockApp, BuildMode.release), 'release/executable');
expect(device.executablePathForDevice(mockApp, BuildInfo.debug), 'debug/executable');
expect(device.executablePathForDevice(mockApp, BuildInfo.profile), 'profile/executable');
expect(device.executablePathForDevice(mockApp, BuildInfo.release), 'release/executable');
});
}

View file

@ -10,6 +10,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/macos/application_package.dart';
@ -155,6 +156,20 @@ group('PrebuiltMacOSApp', () {
expect(macosApp.id, 'com.example.placeholder');
expect(macosApp.name, 'macOS');
}, overrides: overrides);
testUsingContext('Chooses the correct directory for application.', () {
final MacOSProject project = FlutterProject.fromDirectory(globals.fs.currentDirectory).macos;
final BuildableMacOSApp macosApp = MacOSApp.fromMacOSProject(project) as BuildableMacOSApp;
const BuildInfo vanillaApp = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
String? applicationBundle = macosApp.bundleDirectory(vanillaApp);
expect(applicationBundle, 'Debug');
const BuildInfo flavoredApp = BuildInfo(BuildMode.release, 'flavor', treeShakeIcons: false);
applicationBundle = macosApp.bundleDirectory(flavoredApp);
expect(applicationBundle, 'Release Flavor');
}, overrides: overrides);
});
}

View file

@ -235,9 +235,9 @@ void main() {
const String profilePath = 'profile/executable';
const String releasePath = 'release/executable';
expect(device.executablePathForDevice(package, BuildMode.debug), debugPath);
expect(device.executablePathForDevice(package, BuildMode.profile), profilePath);
expect(device.executablePathForDevice(package, BuildMode.release), releasePath);
expect(device.executablePathForDevice(package, BuildInfo.debug), debugPath);
expect(device.executablePathForDevice(package, BuildInfo.profile), profilePath);
expect(device.executablePathForDevice(package, BuildInfo.release), releasePath);
});
}
@ -251,13 +251,13 @@ FlutterProject setUpFlutterProject(Directory directory) {
class FakeMacOSApp extends Fake implements MacOSApp {
@override
String executable(BuildMode buildMode) {
switch (buildMode) {
case BuildMode.debug:
String executable(BuildInfo buildInfo) {
switch (buildInfo) {
case BuildInfo.debug:
return 'debug/executable';
case BuildMode.profile:
case BuildInfo.profile:
return 'profile/executable';
case BuildMode.release:
case BuildInfo.release:
return 'release/executable';
default:
throw StateError('');

View file

@ -142,7 +142,7 @@ void main() {
throwsA(isA<UnimplementedError>()),
);
await expectLater(() => device.buildForDevice(buildInfo: BuildInfo.debug), throwsA(isA<UnimplementedError>()));
expect(device.executablePathForDevice(FakeIOSApp(), BuildMode.debug), null);
expect(device.executablePathForDevice(FakeIOSApp(), BuildInfo.debug), null);
});
}

View file

@ -101,9 +101,9 @@ void main() {
final WindowsDevice windowsDevice = setUpWindowsDevice();
final FakeWindowsApp fakeApp = FakeWindowsApp();
expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.debug), 'debug/executable');
expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.profile), 'profile/executable');
expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.release), 'release/executable');
expect(windowsDevice.executablePathForDevice(fakeApp, BuildInfo.debug), 'debug/executable');
expect(windowsDevice.executablePathForDevice(fakeApp, BuildInfo.profile), 'profile/executable');
expect(windowsDevice.executablePathForDevice(fakeApp, BuildInfo.release), 'release/executable');
});
}