diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index 9154d8095d9..04513af7f0d 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -328,7 +328,7 @@ abstract class IOSApp extends ApplicationPackage { } static Future fromIosProject(IosProject project, BuildInfo buildInfo) { - if (getCurrentHostPlatform() != HostPlatform.darwin_x64) { + if (!globals.platform.isMacOS) { return null; } if (!project.exists) { diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index 8fd9a98ac48..607ac2e777d 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart @@ -7,6 +7,7 @@ import 'package:file/file.dart'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; +import '../build_info.dart'; import '../globals.dart' as globals; import 'common.dart'; import 'file_system.dart'; @@ -29,6 +30,13 @@ abstract class OperatingSystemUtils { platform: platform, processManager: processManager, ); + } else if (platform.isMacOS) { + return _MacOSUtils( + fileSystem: fileSystem, + logger: logger, + platform: platform, + processManager: processManager, + ); } else { return _PosixUtils( fileSystem: fileSystem, @@ -112,6 +120,8 @@ abstract class OperatingSystemUtils { return osNames.containsKey(osName) ? osNames[osName] : osName; } + HostPlatform get hostPlatform; + List _which(String execName, { bool all = false }); /// Returns the separator between items in the PATH environment variable. @@ -246,30 +256,63 @@ class _PosixUtils extends OperatingSystemUtils { return _fileSystem.file(path); } + @override + String get pathVarSeparator => ':'; + + @override + HostPlatform hostPlatform = HostPlatform.linux_x64; +} + +class _MacOSUtils extends _PosixUtils { + _MacOSUtils({ + @required FileSystem fileSystem, + @required Logger logger, + @required Platform platform, + @required ProcessManager processManager, + }) : super( + fileSystem: fileSystem, + logger: logger, + platform: platform, + processManager: processManager, + ); + String _name; @override String get name { if (_name == null) { - if (_platform.isMacOS) { - final List results = [ - _processUtils.runSync(['sw_vers', '-productName']), - _processUtils.runSync(['sw_vers', '-productVersion']), - _processUtils.runSync(['sw_vers', '-buildVersion']), - _processUtils.runSync(['uname', '-m']), - ]; - if (results.every((RunResult result) => result.exitCode == 0)) { - _name = '${results[0].stdout.trim()} ${results[1].stdout - .trim()} ${results[2].stdout.trim()} ${results[3].stdout.trim()}'; - } + final List results = [ + _processUtils.runSync(['sw_vers', '-productName']), + _processUtils.runSync(['sw_vers', '-productVersion']), + _processUtils.runSync(['sw_vers', '-buildVersion']), + ]; + if (results.every((RunResult result) => result.exitCode == 0)) { + _name = + '${results[0].stdout.trim()} ${results[1].stdout.trim()} ${results[2].stdout.trim()} ${getNameForHostPlatform(hostPlatform)}'; } _name ??= super.name; } return _name; } + HostPlatform _hostPlatform; + + // On ARM returns arm64, even when this process is running in Rosetta. @override - String get pathVarSeparator => ':'; + HostPlatform get hostPlatform { + if (_hostPlatform == null) { + final RunResult arm64Check = + _processUtils.runSync(['sysctl', 'hw.optional.arm64']); + // hw.optional.arm64 is unavailable on < macOS 11 and exits with 1, assume x86 on failure. + // On arm64 stdout is "sysctl hw.optional.arm64: 1" + if (arm64Check.exitCode == 0 && arm64Check.stdout.trim().endsWith('1')) { + _hostPlatform = HostPlatform.darwin_arm; + } else { + _hostPlatform = HostPlatform.darwin_x64; + } + } + return _hostPlatform; + } } class _WindowsUtils extends OperatingSystemUtils { @@ -285,6 +328,9 @@ class _WindowsUtils extends OperatingSystemUtils { processManager: processManager, ); + @override + HostPlatform hostPlatform = HostPlatform.windows_x64; + @override void makeExecutable(File file) {} diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index a1bffd4c9bd..17d9b9d8baa 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -398,6 +398,7 @@ bool isEmulatorBuildMode(BuildMode mode) { enum HostPlatform { darwin_x64, + darwin_arm, linux_x64, windows_x64, } @@ -406,6 +407,8 @@ String getNameForHostPlatform(HostPlatform platform) { switch (platform) { case HostPlatform.darwin_x64: return 'darwin-x64'; + case HostPlatform.darwin_arm: + return 'darwin-arm'; case HostPlatform.linux_x64: return 'linux-x64'; case HostPlatform.windows_x64: @@ -418,6 +421,7 @@ String getNameForHostPlatform(HostPlatform platform) { enum TargetPlatform { android, ios, + // darwin_arm64 not yet supported, macOS desktop targets run in Rosetta as x86. darwin_x64, linux_x64, windows_x64, diff --git a/packages/flutter_tools/test/general.shard/base/os_test.dart b/packages/flutter_tools/test/general.shard/base/os_test.dart index d2e77daefee..4ca073ddb86 100644 --- a/packages/flutter_tools/test/general.shard/base/os_test.dart +++ b/packages/flutter_tools/test/general.shard/base/os_test.dart @@ -6,10 +6,10 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; -import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; @@ -22,9 +22,11 @@ const String kPath2 = '/another/bin/$kExecutable'; void main() { MockProcessManager mockProcessManager; + FakeProcessManager fakeProcessManager; setUp(() { mockProcessManager = MockProcessManager(); + fakeProcessManager = FakeProcessManager.list([]); }); OperatingSystemUtils createOSUtils(Platform platform) { @@ -32,28 +34,50 @@ void main() { fileSystem: MemoryFileSystem.test(), logger: BufferLogger.test(), platform: platform, - processManager: mockProcessManager, + processManager: fakeProcessManager, ); } group('which on POSIX', () { testWithoutContext('returns null when executable does not exist', () async { - when(mockProcessManager.runSync(['which', kExecutable])) - .thenReturn(ProcessResult(0, 1, null, null)); + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'which', + kExecutable, + ], + exitCode: 1, + ), + ); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'linux')); expect(utils.which(kExecutable), isNull); }); testWithoutContext('returns exactly one result', () async { - when(mockProcessManager.runSync(['which', 'foo'])) - .thenReturn(ProcessResult(0, 0, kPath1, null)); + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'which', + 'foo', + ], + stdout: kPath1, + ), + ); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'linux')); expect(utils.which(kExecutable).path, kPath1); }); testWithoutContext('returns all results for whichAll', () async { - when(mockProcessManager.runSync(['which', '-a', kExecutable])) - .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null)); + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'which', + '-a', + kExecutable, + ], + stdout: '$kPath1\n$kPath2', + ), + ); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'linux')); final List result = utils.whichAll(kExecutable); expect(result, hasLength(2)); @@ -66,27 +90,55 @@ void main() { testWithoutContext('throws tool exit if where throws an argument error', () async { when(mockProcessManager.runSync(['where', kExecutable])) .thenThrow(ArgumentError('Cannot find executable for where')); - final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); + final OperatingSystemUtils utils = OperatingSystemUtils( + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: mockProcessManager, + ); expect(() => utils.which(kExecutable), throwsA(isA())); }); + testWithoutContext('returns null when executable does not exist', () async { - when(mockProcessManager.runSync(['where', kExecutable])) - .thenReturn(ProcessResult(0, 1, null, null)); + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'where', + kExecutable, + ], + exitCode: 1, + ), + ); + final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); expect(utils.which(kExecutable), isNull); }); testWithoutContext('returns exactly one result', () async { - when(mockProcessManager.runSync(['where', 'foo'])) - .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null)); + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'where', + 'foo', + ], + stdout: '$kPath1\n$kPath2', + ), + ); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); expect(utils.which(kExecutable).path, kPath1); }); testWithoutContext('returns all results for whichAll', () async { - when(mockProcessManager.runSync(['where', kExecutable])) - .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null)); + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'where', + kExecutable, + ], + stdout: '$kPath1\n$kPath2', + ), + ); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); final List result = utils.whichAll(kExecutable); expect(result, hasLength(2)); @@ -95,34 +147,162 @@ void main() { }); }); - testWithoutContext('macos name', () async { - when(mockProcessManager.runSync( - ['sw_vers', '-productName'], - )).thenReturn(ProcessResult(0, 0, 'product', '')); - when(mockProcessManager.runSync( - ['sw_vers', '-productVersion'], - )).thenReturn(ProcessResult(0, 0, 'version', '')); - when(mockProcessManager.runSync( - ['sw_vers', '-buildVersion'], - )).thenReturn(ProcessResult(0, 0, 'build', '')); - when(mockProcessManager.runSync( - ['uname', '-m'], - )).thenReturn(ProcessResult(0, 0, 'arch', '')); - final MockFileSystem fileSystem = MockFileSystem(); - final OperatingSystemUtils utils = OperatingSystemUtils( - fileSystem: fileSystem, - logger: BufferLogger.test(), - platform: FakePlatform(operatingSystem: 'macos'), - processManager: mockProcessManager, - ); - expect(utils.name, 'product version build arch'); + group('host platform', () { + testWithoutContext('unknown defaults to Linux', () async { + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'fuchsia')); + expect(utils.hostPlatform, HostPlatform.linux_x64); + }); + + testWithoutContext('Windows', () async { + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'windows')); + expect(utils.hostPlatform, HostPlatform.windows_x64); + }); + + testWithoutContext('Linux', () async { + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'linux')); + expect(utils.hostPlatform, HostPlatform.linux_x64); + }); + + testWithoutContext('macOS ARM', () async { + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + stdout: 'hw.optional.arm64: 1', + ), + ); + + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'macos')); + expect(utils.hostPlatform, HostPlatform.darwin_arm); + }); + + testWithoutContext('macOS 11 x86', () async { + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + stdout: 'hw.optional.arm64: 0', + ), + ); + + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'macos')); + expect(utils.hostPlatform, HostPlatform.darwin_x64); + }); + + testWithoutContext('macOS 10 x86', () async { + fakeProcessManager.addCommand( + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + exitCode: 1, + ), + ); + + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'macos')); + expect(utils.hostPlatform, HostPlatform.darwin_x64); + }); + + testWithoutContext('macOS ARM name', () async { + fakeProcessManager.addCommands([ + const FakeCommand( + command: [ + 'sw_vers', + '-productName', + ], + stdout: 'product', + ), + const FakeCommand( + command: [ + 'sw_vers', + '-productVersion', + ], + stdout: 'version', + ), + const FakeCommand( + command: [ + 'sw_vers', + '-buildVersion', + ], + stdout: 'build', + ), + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + stdout: 'hw.optional.arm64: 1', + ), + ]); + + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'macos')); + expect(utils.name, 'product version build darwin-arm'); + }); + + testWithoutContext('macOS x86 name', () async { + fakeProcessManager.addCommands([ + const FakeCommand( + command: [ + 'sw_vers', + '-productName', + ], + stdout: 'product', + ), + const FakeCommand( + command: [ + 'sw_vers', + '-productVersion', + ], + stdout: 'version', + ), + const FakeCommand( + command: [ + 'sw_vers', + '-buildVersion', + ], + stdout: 'build', + ), + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + exitCode: 1, + ), + ]); + + final OperatingSystemUtils utils = + createOSUtils(FakePlatform(operatingSystem: 'macos')); + expect(utils.name, 'product version build darwin-x64'); + }); }); testWithoutContext('If unzip fails, include stderr in exception text', () { const String exceptionMessage = 'Something really bad happened.'; - when(mockProcessManager.runSync( - ['unzip', '-o', '-q', null, '-d', null], - )).thenReturn(ProcessResult(0, 1, '', exceptionMessage)); + + fakeProcessManager.addCommand( + const FakeCommand(command: [ + 'unzip', + '-o', + '-q', + null, + '-d', + null, + ], exitCode: 1, stderr: exceptionMessage), + ); + final MockFileSystem fileSystem = MockFileSystem(); final MockFile mockFile = MockFile(); final MockDirectory mockDirectory = MockDirectory(); @@ -134,7 +314,7 @@ void main() { fileSystem: fileSystem, logger: BufferLogger.test(), platform: FakePlatform(operatingSystem: 'linux'), - processManager: mockProcessManager, + processManager: fakeProcessManager, ); expect( diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index a653e7ebf05..deb31efc470 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/template.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/time.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/isolated/mustache_template.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/context_runner.dart'; @@ -294,6 +295,9 @@ class FakeOperatingSystemUtils implements OperatingSystemUtils { @override ProcessResult makeExecutable(File file) => null; + @override + HostPlatform hostPlatform = HostPlatform.linux_x64; + @override void chmod(FileSystemEntity entity, String mode) { }