From b7f5aef11a2c7cc69bd4e34acc875d9756efe7a1 Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Thu, 14 Jan 2021 18:22:08 +0000 Subject: [PATCH] Improve codesign script (#71244) --- dev/bots/codesign.dart | 194 +-------- dev/bots/test.dart | 2 +- dev/tools/bin/conductor.dart | 39 +- dev/tools/lib/codesign.dart | 369 ++++++++++++++++ dev/tools/lib/git.dart | 1 + dev/tools/lib/globals.dart | 62 +++ dev/tools/lib/repository.dart | 271 +++++++----- dev/tools/lib/roll_dev.dart | 22 +- dev/tools/lib/stdio.dart | 14 +- dev/tools/test/codesign_integration_test.dart | 71 +++ dev/tools/test/codesign_test.dart | 412 ++++++++++++++++++ dev/tools/test/common.dart | 1 + dev/tools/test/roll_dev_integration_test.dart | 27 +- dev/tools/test/roll_dev_test.dart | 83 ++-- 14 files changed, 1182 insertions(+), 386 deletions(-) create mode 100644 dev/tools/lib/codesign.dart create mode 100644 dev/tools/test/codesign_integration_test.dart create mode 100644 dev/tools/test/codesign_test.dart diff --git a/dev/bots/codesign.dart b/dev/bots/codesign.dart index feadabad5d3..99c04be1d55 100644 --- a/dev/bots/codesign.dart +++ b/dev/bots/codesign.dart @@ -2,184 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; +import 'dart:io' as io; import 'package:path/path.dart' as path; -String get repoRoot => path.normalize(path.join(path.dirname(Platform.script.toFilePath()), '..', '..')); -String get cacheDirectory => path.normalize(path.join(repoRoot, 'bin', 'cache')); - -/// Check mime-type of file at [filePath] to determine if it is binary -bool isBinary(String filePath) { - final ProcessResult result = Process.runSync( - 'file', - [ - '--mime-type', - '-b', // is binary - filePath, - ], +// TODO(fujino): delete this script once PR #71244 lands on stable. +void main(List args) { + final String scriptPath = io.Platform.script.toFilePath(); + final String scriptDir = path.dirname(scriptPath); + final String repoRoot = path.normalize(path.join(scriptDir, '..', '..')); + final io.ProcessResult result = io.Process.runSync( + path.join(repoRoot, 'dev', 'tools', 'bin', 'conductor'), + ['codesign', '--verify'], ); - return (result.stdout as String).contains('application/x-mach-binary'); -} - -/// Find every binary file in the given [rootDirectory] -List findBinaryPaths([String rootDirectory]) { - rootDirectory ??= cacheDirectory; - final ProcessResult result = Process.runSync( - 'find', - [ - rootDirectory, - '-type', - 'f', - '-perm', - '+111', // is executable - ], - ); - final List allFiles = (result.stdout as String).split('\n').where((String s) => s.isNotEmpty).toList(); - return allFiles.where(isBinary).toList(); -} - -/// Given the path to a stamp file, read the contents. -/// -/// Will throw if the file doesn't exist. -String readStamp(String filePath) { - final File file = File(filePath); - if (!file.existsSync()) { - throw 'Error! Stamp file $filePath does not exist!'; - } - return file.readAsStringSync().trim(); -} - -/// Return whether or not the flutter cache is up to date. -bool checkCacheIsCurrent() { - try { - final String dartSdkStamp = readStamp(path.join(cacheDirectory, 'engine-dart-sdk.stamp')); - final String engineVersion = readStamp(path.join(repoRoot, 'bin', 'internal', 'engine.version')); - return dartSdkStamp == engineVersion; - } catch (e) { - print(e); - return false; - } -} - -List get binariesWithEntitlements => List.unmodifiable([ - 'ideviceinfo', - 'idevicename', - 'idevicescreenshot', - 'idevicesyslog', - 'libimobiledevice.6.dylib', - 'libplist.3.dylib', - 'iproxy', - 'libusbmuxd.4.dylib', - 'libssl.1.0.0.dylib', - 'libcrypto.1.0.0.dylib', - 'libzip.5.0.dylib', - 'libzip.5.dylib', - 'gen_snapshot', - 'dart', - 'flutter_tester', - 'gen_snapshot_arm64', - 'gen_snapshot_armv7', -]); - -List get expectedEntitlements => List.unmodifiable([ - 'com.apple.security.cs.allow-jit', - 'com.apple.security.cs.allow-unsigned-executable-memory', - 'com.apple.security.cs.allow-dyld-environment-variables', - 'com.apple.security.network.client', - 'com.apple.security.network.server', - 'com.apple.security.cs.disable-library-validation', -]); - - -/// Check if the binary has the expected entitlements. -bool hasExpectedEntitlements(String binaryPath) { - try { - final ProcessResult entitlementResult = Process.runSync( - 'codesign', - [ - '--display', - '--entitlements', - ':-', - binaryPath, - ], - ); - - if (entitlementResult.exitCode != 0) { - print('The `codesign --entitlements` command failed with exit code ${entitlementResult.exitCode}:\n' - '${entitlementResult.stderr}\n'); - return false; - } - - bool passes = true; - final String output = entitlementResult.stdout as String; - for (final String entitlement in expectedEntitlements) { - final bool entitlementExpected = binariesWithEntitlements.contains(path.basename(binaryPath)); - if (output.contains(entitlement) != entitlementExpected) { - print('File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} entitlement $entitlement.'); - passes = false; - } - } - return passes; - } catch (e) { - print(e); - return false; - } -} - -void main() { - if (!Platform.isMacOS) { - print('Error! Expected operating system "macos", actual operating system ' - 'is: "${Platform.operatingSystem}"'); - exit(1); - } - - if (!checkCacheIsCurrent()) { - print( - 'Warning! Your cache is either not present or not matching your flutter\n' - 'version. Run a `flutter` command to update your cache, and re-try this\n' - 'test.'); - exit(1); - } - - final List unsignedBinaries = []; - final List wrongEntitlementBinaries = []; - for (final String binaryPath in findBinaryPaths(cacheDirectory)) { - print('Verifying the code signature of $binaryPath'); - final ProcessResult codeSignResult = Process.runSync( - 'codesign', - [ - '-vvv', - binaryPath, - ], - ); - if (codeSignResult.exitCode != 0) { - unsignedBinaries.add(binaryPath); - print('File "$binaryPath" does not appear to be codesigned.\n' - 'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n' - '${codeSignResult.stderr}\n'); - continue; - } else { - print('Verifying entitlements of $binaryPath'); - if (!hasExpectedEntitlements(binaryPath)) { - wrongEntitlementBinaries.add(binaryPath); - } - } - } - - if (unsignedBinaries.isNotEmpty) { - print('Found ${unsignedBinaries.length} unsigned binaries:'); - unsignedBinaries.forEach(print); - } - - if (wrongEntitlementBinaries.isNotEmpty) { - print('Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:'); - wrongEntitlementBinaries.forEach(print); - } - - if (unsignedBinaries.isNotEmpty) { - // TODO(jmagman): Also exit if `wrongEntitlementBinaries.isNotEmpty` after https://github.com/flutter/flutter/issues/46704 is done. - exit(1); - } - - print('Verified that binaries are codesigned and have expected entitlements.'); + if (result.exitCode != 0) { + print('codesign script exited with code $result.exitCode'); + print('stdout:\n${result.stdout}\n'); + print('stderr:\n${result.stderr}\n'); + io.exit(1); + } + print('codesign script succeeded.'); + print('stdout:\n${result.stdout}'); } diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 93ac2038e36..5294072875c 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -694,7 +694,7 @@ Future _runFrameworkTests() async { await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'), tableData: bigqueryApi?.tabledata); await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'), tableData: bigqueryApi?.tabledata); await _pubRunTest(path.join(flutterRoot, 'dev', 'snippets'), tableData: bigqueryApi?.tabledata); - await _pubRunTest(path.join(flutterRoot, 'dev', 'tools'), tableData: bigqueryApi?.tabledata); + await _pubRunTest(path.join(flutterRoot, 'dev', 'tools'), tableData: bigqueryApi?.tabledata, forceSingleCore: true); await _pubRunTest(path.join(flutterRoot, 'dev', 'benchmarks', 'metrics_center'), tableData: bigqueryApi?.tabledata); await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'), tableData: bigqueryApi?.tabledata); await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'), tableData: bigqueryApi?.tabledata); diff --git a/dev/tools/bin/conductor.dart b/dev/tools/bin/conductor.dart index b5a30714e57..03addabc344 100644 --- a/dev/tools/bin/conductor.dart +++ b/dev/tools/bin/conductor.dart @@ -10,15 +10,17 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; +import 'package:dev_tools/codesign.dart'; +import 'package:dev_tools/globals.dart'; +import 'package:dev_tools/roll_dev.dart'; +import 'package:dev_tools/repository.dart'; +import 'package:dev_tools/stdio.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; -import 'package:dev_tools/repository.dart'; -import 'package:dev_tools/roll_dev.dart'; -import 'package:dev_tools/stdio.dart'; -void main(List args) { +Future main(List args) async { const FileSystem fileSystem = LocalFileSystem(); const ProcessManager processManager = LocalProcessManager(); const Platform platform = LocalPlatform(); @@ -29,9 +31,12 @@ void main(List args) { ); final Checkouts checkouts = Checkouts( fileSystem: fileSystem, + parentDirectory: localFlutterRoot.parent, platform: platform, processManager: processManager, + stdio: stdio, ); + final CommandRunner runner = CommandRunner( 'conductor', 'A tool for coordinating Flutter releases.', @@ -39,17 +44,16 @@ void main(List args) { ); >[ - RollDev( + RollDevCommand( + checkouts: checkouts, fileSystem: fileSystem, platform: platform, - repository: checkouts.addRepo( - fileSystem: fileSystem, - platform: platform, - repoType: RepositoryType.framework, - stdio: stdio, - ), stdio: stdio, ), + CodesignCommand( + checkouts: checkouts, + flutterRoot: localFlutterRoot, + ), ].forEach(runner.addCommand); if (!assertsEnabled()) { @@ -58,20 +62,9 @@ void main(List args) { } try { - runner.run(args); + await runner.run(args); } on Exception catch (e) { stdio.printError(e.toString()); io.exit(1); } } - -bool assertsEnabled() { - // Verify asserts enabled - bool assertsEnabled = false; - - assert(() { - assertsEnabled = true; - return true; - }()); - return assertsEnabled; -} diff --git a/dev/tools/lib/codesign.dart b/dev/tools/lib/codesign.dart new file mode 100644 index 00000000000..64e50a49234 --- /dev/null +++ b/dev/tools/lib/codesign.dart @@ -0,0 +1,369 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import './globals.dart'; +import './repository.dart'; +import './stdio.dart'; + +const List expectedEntitlements = [ + 'com.apple.security.cs.allow-jit', + 'com.apple.security.cs.allow-unsigned-executable-memory', + 'com.apple.security.cs.allow-dyld-environment-variables', + 'com.apple.security.network.client', + 'com.apple.security.network.server', + 'com.apple.security.cs.disable-library-validation', +]; + +const String kVerify = 'verify'; +const String kSignatures = 'signatures'; +const String kRevision = 'revision'; +const String kUpstream = 'upstream'; + +/// Command to codesign and verify the signatures of cached binaries. +class CodesignCommand extends Command { + CodesignCommand({ + @required this.checkouts, + @required this.flutterRoot, + }) : assert(flutterRoot != null), + fileSystem = checkouts.fileSystem, + platform = checkouts.platform, + stdio = checkouts.stdio, + processManager = checkouts.processManager { + argParser.addFlag( + kVerify, + help: + 'Only verify expected binaries exist and are codesigned with entitlements.', + ); + argParser.addFlag( + kSignatures, + defaultsTo: true, + help: + 'When off, this command will only verify the existence of binaries, and not their\n' + 'signatures or entitlements. Must be used with --verify flag.', + ); + argParser.addOption( + kUpstream, + defaultsTo: FrameworkRepository.defaultUpstream, + help: 'The git remote URL to use as the Flutter framework\'s upstream.', + ); + argParser.addOption( + kRevision, + help: 'The Flutter framework revision to use.', + ); + } + + final Checkouts checkouts; + final FileSystem fileSystem; + final Platform platform; + final ProcessManager processManager; + final Stdio stdio; + + /// Root directory of the Flutter repository. + final Directory flutterRoot; + + FrameworkRepository _framework; + FrameworkRepository get framework => _framework ??= FrameworkRepository.localRepoAsUpstream( + checkouts, + upstreamPath: flutterRoot.path, + ); + + @visibleForTesting + set framework(FrameworkRepository framework) => _framework = framework; + + @override + String get name => 'codesign'; + + @override + String get description => + 'For codesigning and verifying the signatures of engine binaries.'; + + @override + void run() { + if (!platform.isMacOS) { + throw ConductorException( + 'Error! Expected operating system "macos", actual operating system is: ' + '"${platform.operatingSystem}"'); + } + + if (argResults['verify'] as bool != true) { + throw ConductorException( + 'Sorry, but codesigning is not implemented yet. Please pass the ' + '--$kVerify flag to verify signatures.'); + } + + String revision; + if (argResults.wasParsed(kRevision)) { + stdio.printError('Warning! When providing an arbitrary revision, the contents of the cache may not'); + stdio.printError('match the expected binaries in the conductor tool. It is preferred to check out'); + stdio.printError('the desired revision and run that version of the conductor.\n'); + revision = argResults[kRevision] as String; + } else { + revision = (processManager.runSync( + ['git', 'rev-parse', 'HEAD'], + workingDirectory: framework.checkoutDirectory.path, + ).stdout as String).trim(); + assert(revision.isNotEmpty); + } + + framework.checkout(revision); + + // Ensure artifacts present + framework.runFlutter(['precache', '--ios', '--macos']); + + verifyExist(); + if (argResults[kSignatures] as bool) { + verifySignatures(); + } + } + + /// Binaries that are expected to be codesigned and have entitlements. + /// + /// This list should be kept in sync with the actual contents of Flutter's + /// cache. + List get binariesWithEntitlements { + return [ + 'artifacts/engine/android-arm-profile/darwin-x64/gen_snapshot', + 'artifacts/engine/android-arm-release/darwin-x64/gen_snapshot', + 'artifacts/engine/android-arm64-profile/darwin-x64/gen_snapshot', + 'artifacts/engine/android-arm64-release/darwin-x64/gen_snapshot', + 'artifacts/engine/android-x64-profile/darwin-x64/gen_snapshot', + 'artifacts/engine/android-x64-release/darwin-x64/gen_snapshot', + 'artifacts/engine/darwin-x64-profile/gen_snapshot', + 'artifacts/engine/darwin-x64-release/gen_snapshot', + 'artifacts/engine/darwin-x64/flutter_tester', + 'artifacts/engine/darwin-x64/gen_snapshot', + 'artifacts/engine/ios-profile/gen_snapshot_arm64', + 'artifacts/engine/ios-profile/gen_snapshot_armv7', + 'artifacts/engine/ios-release/gen_snapshot_arm64', + 'artifacts/engine/ios-release/gen_snapshot_armv7', + 'artifacts/engine/ios/gen_snapshot_arm64', + 'artifacts/engine/ios/gen_snapshot_armv7', + 'artifacts/ios-deploy/ios-deploy', + 'artifacts/libimobiledevice/idevicescreenshot', + 'artifacts/libimobiledevice/idevicesyslog', + 'artifacts/libimobiledevice/libimobiledevice-1.0.6.dylib', + 'artifacts/libplist/libplist-2.0.3.dylib', + 'artifacts/openssl/libcrypto.1.1.dylib', + 'artifacts/openssl/libssl.1.1.dylib', + 'artifacts/usbmuxd/iproxy', + 'artifacts/usbmuxd/libusbmuxd-2.0.6.dylib', + 'dart-sdk/bin/dart', + 'dart-sdk/bin/dartaotruntime', + 'dart-sdk/bin/utils/gen_snapshot', + ].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, relativePath)).toList(); + } + + /// Binaries that are only expected to be codesigned. + /// + /// This list should be kept in sync with the actual contents of Flutter's + /// cache. + List get binariesWithoutEntitlements { + return [ + 'artifacts/engine/darwin-x64-profile/FlutterMacOS.framework/Versions/A/FlutterMacOS', + 'artifacts/engine/darwin-x64-release/FlutterMacOS.framework/Versions/A/FlutterMacOS', + 'artifacts/engine/darwin-x64/FlutterMacOS.framework/Versions/A/FlutterMacOS', + 'artifacts/engine/darwin-x64/font-subset', + 'artifacts/engine/ios-profile/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-profile/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter', + ].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, relativePath)).toList(); + } + + /// Verify the existence of all expected binaries in cache. + /// + /// This function ignores code signatures and entitlements, and is intended to + /// be run on every commit. It should throw if either new binaries are added + /// to the cache or expected binaries removed. In either case, this class' + /// [binariesWithEntitlements] or [binariesWithoutEntitlements] lists should + /// be updated accordingly. + @visibleForTesting + void verifyExist() { + final Set foundFiles = {}; + for (final String binaryPath in findBinaryPaths(framework.cacheDirectory)) { + if (binariesWithEntitlements.contains(binaryPath)) { + foundFiles.add(binaryPath); + } else if (binariesWithoutEntitlements.contains(binaryPath)) { + foundFiles.add(binaryPath); + } else { + throw ConductorException('Found unexpected binary in cache: $binaryPath'); + } + } + + final List allExpectedFiles = binariesWithEntitlements + binariesWithoutEntitlements; + if (foundFiles.length < allExpectedFiles.length) { + final List unfoundFiles = allExpectedFiles.where( + (String file) => !foundFiles.contains(file), + ).toList(); + stdio.printError('Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n'); + stdio.printError('If this commit is removing binaries from the cache, this test should be fixed by'); + stdio.printError('removing the relevant entry from either the `binariesWithEntitlements` or'); + stdio.printError('`binariesWithoutEntitlements` getters in dev/tools/lib/codesign.dart.'); + throw ConductorException('Did not find all expected binaries!'); + } + + stdio.printStatus('All expected binaries present.'); + } + + /// Verify code signatures and entitlements of all binaries in the cache. + @visibleForTesting + void verifySignatures() { + final List unsignedBinaries = []; + final List wrongEntitlementBinaries = []; + final List unexpectedBinaries = []; + + for (final String binaryPath in findBinaryPaths(framework.cacheDirectory)) { + bool verifySignature = false; + bool verifyEntitlements = false; + if (binariesWithEntitlements.contains(binaryPath)) { + verifySignature = true; + verifyEntitlements = true; + } + if (binariesWithoutEntitlements.contains(binaryPath)) { + verifySignature = true; + } + if (!verifySignature && !verifyEntitlements) { + unexpectedBinaries.add(binaryPath); + stdio.printError('Unexpected binary $binaryPath found in cache!'); + continue; + } + stdio.printTrace('Verifying the code signature of $binaryPath'); + final io.ProcessResult codeSignResult = processManager.runSync( + [ + 'codesign', + '-vvv', + binaryPath, + ], + ); + if (codeSignResult.exitCode != 0) { + unsignedBinaries.add(binaryPath); + stdio.printError( + 'File "$binaryPath" does not appear to be codesigned.\n' + 'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n' + '${codeSignResult.stderr}\n'); + continue; + } + if (verifyEntitlements) { + stdio.printTrace('Verifying entitlements of $binaryPath'); + if (!hasExpectedEntitlements(binaryPath)) { + wrongEntitlementBinaries.add(binaryPath); + } + } + } + + // First print all deviations from expectations + if (unsignedBinaries.isNotEmpty) { + stdio.printError('Found ${unsignedBinaries.length} unsigned binaries:'); + unsignedBinaries.forEach(print); + } + + if (wrongEntitlementBinaries.isNotEmpty) { + stdio.printError( + 'Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:'); + wrongEntitlementBinaries.forEach(print); + } + + if (unexpectedBinaries.isNotEmpty) { + stdio.printError('Found ${unexpectedBinaries.length} unexpected binaries in the cache:'); + unexpectedBinaries.forEach(print); + } + + // Finally, exit on any invalid state + if (unsignedBinaries.isNotEmpty) { + throw ConductorException('Test failed because unsigned binaries detected.'); + } + + if (wrongEntitlementBinaries.isNotEmpty) { + throw ConductorException( + 'Test failed because files found with the wrong entitlements:\n' + '${wrongEntitlementBinaries.join('\n')}'); + } + + if (unexpectedBinaries.isNotEmpty) { + throw ConductorException('Test failed because unexpected binaries found in the cache.'); + } + + stdio.printStatus( + 'Verified that binaries for commit ${argResults[kRevision] as String} are codesigned and have ' + 'expected entitlements.'); + } + + List _allBinaryPaths; + /// Find every binary file in the given [rootDirectory]. + List findBinaryPaths(String rootDirectory) { + if (_allBinaryPaths != null) { + return _allBinaryPaths; + } + final io.ProcessResult result = processManager.runSync( + [ + 'find', + rootDirectory, + '-type', + 'f', + ], + ); + final List allFiles = (result.stdout as String) + .split('\n') + .where((String s) => s.isNotEmpty) + .toList(); + _allBinaryPaths = allFiles.where(isBinary).toList(); + return _allBinaryPaths; + } + + /// Check mime-type of file at [filePath] to determine if it is binary. + bool isBinary(String filePath) { + final io.ProcessResult result = processManager.runSync( + [ + 'file', + '--mime-type', + '-b', // is binary + filePath, + ], + ); + return (result.stdout as String).contains('application/x-mach-binary'); + } + + /// Check if the binary has the expected entitlements. + bool hasExpectedEntitlements(String binaryPath) { + final io.ProcessResult entitlementResult = processManager.runSync( + [ + 'codesign', + '--display', + '--entitlements', + ':-', + binaryPath, + ], + ); + + if (entitlementResult.exitCode != 0) { + stdio.printError( + 'The `codesign --entitlements` command failed with exit code ${entitlementResult.exitCode}:\n' + '${entitlementResult.stderr}\n'); + return false; + } + + bool passes = true; + final String output = entitlementResult.stdout as String; + for (final String entitlement in expectedEntitlements) { + final bool entitlementExpected = binariesWithEntitlements.contains(binaryPath); + if (output.contains(entitlement) != entitlementExpected) { + stdio.printError( + 'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} ' + 'entitlement $entitlement.'); + passes = false; + } + } + return passes; + } +} diff --git a/dev/tools/lib/git.dart b/dev/tools/lib/git.dart index 468c91d4a7c..a85a2db0ff3 100644 --- a/dev/tools/lib/git.dart +++ b/dev/tools/lib/git.dart @@ -45,6 +45,7 @@ class Git { return processManager.runSync( ['git', ...args], workingDirectory: workingDirectory, + environment: {'GIT_TRACE': '1'}, ); } diff --git a/dev/tools/lib/globals.dart b/dev/tools/lib/globals.dart index 3d9c9207ef9..e6abe0e02b1 100644 --- a/dev/tools/lib/globals.dart +++ b/dev/tools/lib/globals.dart @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:platform/platform.dart'; + const String kIncrement = 'increment'; const String kCommit = 'commit'; const String kRemoteName = 'remote'; @@ -24,3 +28,61 @@ String stdoutToString(dynamic input) { final String str = input as String; return str.trim(); } + +class ConductorException implements Exception { + ConductorException(this.message); + + final String message; + + @override + String toString() => 'Exception: $message'; +} + +Directory _flutterRoot; +Directory get localFlutterRoot { + if (_flutterRoot != null) { + return _flutterRoot; + } + String filePath; + const FileSystem fileSystem = LocalFileSystem(); + const Platform platform = LocalPlatform(); + + // If a test + if (platform.script.scheme == 'data') { + final RegExp pattern = RegExp( + r'(file:\/\/[^"]*[/\\]dev\/tools[/\\][^"]+\.dart)', + multiLine: true, + ); + final Match match = + pattern.firstMatch(Uri.decodeFull(platform.script.path)); + if (match == null) { + throw Exception( + 'Cannot determine path of script!\n${platform.script.path}', + ); + } + filePath = Uri.parse(match.group(1)).path.replaceAll(r'%20', ' '); + } else { + filePath = platform.script.toFilePath(); + } + final String checkoutsDirname = fileSystem.path.normalize( + fileSystem.path.join( + fileSystem.path.dirname(filePath), + '..', // flutter/dev/tools + '..', // flutter/dev + '..', // flutter + ), + ); + _flutterRoot = fileSystem.directory(checkoutsDirname); + return _flutterRoot; +} + +bool assertsEnabled() { + // Verify asserts enabled + bool assertsEnabled = false; + + assert(() { + assertsEnabled = true; + return true; + }()); + return assertsEnabled; +} diff --git a/dev/tools/lib/repository.dart b/dev/tools/lib/repository.dart index f54add1fa13..b1872a49f53 100644 --- a/dev/tools/lib/repository.dart +++ b/dev/tools/lib/repository.dart @@ -16,7 +16,7 @@ import './stdio.dart'; import './version.dart'; /// A source code repository. -class Repository { +abstract class Repository { Repository({ @required this.name, @required this.upstream, @@ -46,22 +46,35 @@ class Repository { Directory _checkoutDirectory; - /// Lazily-loaded directory for the repository checkout. + /// Directory for the repository checkout. /// - /// Cloning a repository is time-consuming, thus the repository is not cloned - /// until this getter is called. + /// Since cloning a repository takes a long time, we do not ensure it is + /// cloned on the filesystem until this getter is accessed. Directory get checkoutDirectory { if (_checkoutDirectory != null) { return _checkoutDirectory; } _checkoutDirectory = parentDirectory.childDirectory(name); - if (checkoutDirectory.existsSync() && !useExistingCheckout) { - deleteDirectory(); - } - if (!checkoutDirectory.existsSync()) { - stdio.printTrace('Cloning $name to ${checkoutDirectory.path}...'); + if (!useExistingCheckout && _checkoutDirectory.existsSync()) { + stdio.printTrace('Deleting $name from ${_checkoutDirectory.path}...'); + _checkoutDirectory.deleteSync(recursive: true); + } else if (useExistingCheckout && _checkoutDirectory.existsSync()) { git.run( - ['clone', '--', upstream, checkoutDirectory.path], + ['checkout', 'master'], + 'Checkout to master branch', + workingDirectory: _checkoutDirectory.path, + ); + git.run( + ['pull', '--ff-only'], + 'Updating $name repo', + workingDirectory: _checkoutDirectory.path, + ); + } + if (!_checkoutDirectory.existsSync()) { + stdio.printTrace( + 'Cloning $name from $upstream to ${_checkoutDirectory.path}...'); + git.run( + ['clone', '--', upstream, _checkoutDirectory.path], 'Cloning $name repo', workingDirectory: parentDirectory.path, ); @@ -72,27 +85,16 @@ class Repository { git.run( ['checkout', channel, '--'], 'check out branch $channel locally', - workingDirectory: checkoutDirectory.path, + workingDirectory: _checkoutDirectory.path, ); } } - } else { - stdio.printTrace( - 'Using existing $name repo at ${checkoutDirectory.path}...', - ); } - return _checkoutDirectory; - } - void deleteDirectory() { - if (!checkoutDirectory.existsSync()) { - stdio.printTrace( - 'Tried to delete ${checkoutDirectory.path} but it does not exist.', - ); - return; - } - stdio.printTrace('Deleting $name from ${checkoutDirectory.path}...'); - checkoutDirectory.deleteSync(recursive: true); + final String revision = reverseParse('HEAD'); + stdio + .printTrace('Repository $name is checked out at revision "$revision".'); + return _checkoutDirectory; } /// The URL of the remote named [remoteName]. @@ -124,6 +126,14 @@ class Repository { ); } + void checkout(String revision) { + git.run( + ['checkout', revision], + 'checkout $revision', + workingDirectory: checkoutDirectory.path, + ); + } + /// Obtain the version tag of the previous dev release. String getFullTag(String remoteName) { const String glob = '*.*.*-*.*.pre'; @@ -142,7 +152,7 @@ class Repository { ['rev-parse', ref], 'look up the commit for the ref $ref', workingDirectory: checkoutDirectory.path, - ); + ).trim(); assert(revisionHash.isNotEmpty); return revisionHash; } @@ -216,24 +226,6 @@ class Repository { ); } - Version flutterVersion() { - // Build tool - processManager.runSync([ - fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'), - 'help', - ]); - // Check version - final io.ProcessResult result = processManager.runSync([ - fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'), - '--version', - '--machine', - ]); - final Map versionJson = jsonDecode( - globals.stdoutToString(result.stdout), - ) as Map; - return Version.fromString(versionJson['frameworkVersion'] as String); - } - /// Create an empty commit and return the revision. @visibleForTesting String authorEmptyCommit([String message = 'An empty commit']) { @@ -261,20 +253,120 @@ class Repository { /// /// This method is for testing purposes. @visibleForTesting + Repository cloneRepository(String cloneName); +} + +class FrameworkRepository extends Repository { + FrameworkRepository( + this.checkouts, { + String name = 'framework', + String upstream = FrameworkRepository.defaultUpstream, + bool localUpstream = false, + bool useExistingCheckout = false, + }) : super( + name: name, + upstream: upstream, + fileSystem: checkouts.fileSystem, + localUpstream: localUpstream, + parentDirectory: checkouts.directory, + platform: checkouts.platform, + processManager: checkouts.processManager, + stdio: checkouts.stdio, + useExistingCheckout: useExistingCheckout, + ); + + /// A [FrameworkRepository] with the host conductor's repo set as upstream. + /// + /// This is useful when testing a commit that has not been merged upstream + /// yet. + factory FrameworkRepository.localRepoAsUpstream( + Checkouts checkouts, { + String name = 'framework', + bool useExistingCheckout = false, + @required String upstreamPath, + }) { + return FrameworkRepository( + checkouts, + name: name, + upstream: 'file://$upstreamPath/', + localUpstream: false, + useExistingCheckout: useExistingCheckout, + ); + } + + final Checkouts checkouts; + static const String defaultUpstream = + 'https://github.com/flutter/flutter.git'; + + String get cacheDirectory => fileSystem.path.join( + checkoutDirectory.path, + 'bin', + 'cache', + ); + + @override Repository cloneRepository(String cloneName) { assert(localUpstream); cloneName ??= 'clone-of-$name'; - return Repository( - fileSystem: fileSystem, + return FrameworkRepository( + checkouts, name: cloneName, - parentDirectory: parentDirectory, - platform: platform, - processManager: processManager, - stdio: stdio, upstream: 'file://${checkoutDirectory.path}/', useExistingCheckout: useExistingCheckout, ); } + + void _ensureToolReady() { + final File toolsStamp = + fileSystem.directory(cacheDirectory).childFile('flutter_tools.stamp'); + if (toolsStamp.existsSync()) { + final String toolsStampHash = toolsStamp.readAsStringSync().trim(); + final String repoHeadHash = reverseParse('HEAD'); + if (toolsStampHash == repoHeadHash) { + return; + } + } + + stdio.printTrace('Building tool...'); + // Build tool + processManager.runSync([ + fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'), + 'help', + ]); + } + + io.ProcessResult runFlutter(List args) { + _ensureToolReady(); + + return processManager.runSync([ + fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'), + ...args, + ]); + } + + @override + void checkout(String revision) { + super.checkout(revision); + // The tool will overwrite old cached artifacts, but not delete unused + // artifacts from a previous version. Thus, delete the entire cache and + // re-populate. + final Directory cache = fileSystem.directory(cacheDirectory); + if (cache.existsSync()) { + stdio.printTrace('Deleting cache...'); + cache.deleteSync(recursive: true); + } + _ensureToolReady(); + } + + Version flutterVersion() { + // Check version + final io.ProcessResult result = + runFlutter(['--version', '--machine']); + final Map versionJson = jsonDecode( + globals.stdoutToString(result.stdout), + ) as Map; + return Version.fromString(versionJson['frameworkVersion'] as String); + } } /// An enum of all the repositories that the Conductor supports. @@ -285,81 +377,22 @@ enum RepositoryType { class Checkouts { Checkouts({ - @required Platform platform, @required this.fileSystem, + @required this.platform, @required this.processManager, - Directory parentDirectory, - String directoryName = 'checkouts', - }) { - if (parentDirectory != null) { - directory = parentDirectory.childDirectory(directoryName); - } else { - String filePath; - // If a test - if (platform.script.scheme == 'data') { - final RegExp pattern = RegExp( - r'(file:\/\/[^"]*[/\\]dev\/tools[/\\][^"]+\.dart)', - multiLine: true, - ); - final Match match = - pattern.firstMatch(Uri.decodeFull(platform.script.path)); - if (match == null) { - throw Exception( - 'Cannot determine path of script!\n${platform.script.path}', - ); - } - filePath = Uri.parse(match.group(1)).path.replaceAll(r'%20', ' '); - } else { - filePath = platform.script.toFilePath(); - } - final String checkoutsDirname = fileSystem.path.normalize( - fileSystem.path.join( - fileSystem.path.dirname(filePath), - '..', - 'checkouts', - ), - ); - directory = fileSystem.directory(checkoutsDirname); - } + @required this.stdio, + @required Directory parentDirectory, + String directoryName = 'flutter_conductor_checkouts', + }) : assert(parentDirectory != null), + directory = parentDirectory.childDirectory(directoryName) { if (!directory.existsSync()) { directory.createSync(recursive: true); } } - Directory directory; + final Directory directory; final FileSystem fileSystem; + final Platform platform; final ProcessManager processManager; - - Repository addRepo({ - @required RepositoryType repoType, - @required Stdio stdio, - @required Platform platform, - FileSystem fileSystem, - String upstream, - String name, - bool localUpstream = false, - bool useExistingCheckout = false, - }) { - switch (repoType) { - case RepositoryType.framework: - name ??= 'framework'; - upstream ??= 'https://github.com/flutter/flutter.git'; - break; - case RepositoryType.engine: - name ??= 'engine'; - upstream ??= 'https://github.com/flutter/engine.git'; - break; - } - return Repository( - name: name, - upstream: upstream, - stdio: stdio, - platform: platform, - fileSystem: fileSystem, - parentDirectory: directory, - processManager: processManager, - localUpstream: localUpstream, - useExistingCheckout: useExistingCheckout, - ); - } + final Stdio stdio; } diff --git a/dev/tools/lib/roll_dev.dart b/dev/tools/lib/roll_dev.dart index 46a16555b23..3fa1da1ff39 100644 --- a/dev/tools/lib/roll_dev.dart +++ b/dev/tools/lib/roll_dev.dart @@ -14,12 +14,12 @@ import './stdio.dart'; import './version.dart'; /// Create a new dev release without cherry picks. -class RollDev extends Command { - RollDev({ - this.fileSystem, - this.platform, - this.repository, - this.stdio, +class RollDevCommand extends Command { + RollDevCommand({ + @required this.checkouts, + @required this.fileSystem, + @required this.platform, + @required this.stdio, }) { argParser.addOption( kIncrement, @@ -60,10 +60,10 @@ class RollDev extends Command { argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.'); } + final Checkouts checkouts; final FileSystem fileSystem; final Platform platform; final Stdio stdio; - final Repository repository; @override String get name => 'roll-dev'; @@ -76,9 +76,7 @@ class RollDev extends Command { void run() { rollDev( argResults: argResults, - fileSystem: fileSystem, - platform: platform, - repository: repository, + repository: FrameworkRepository(checkouts), stdio: stdio, usage: argParser.usage, ); @@ -93,9 +91,7 @@ bool rollDev({ @required String usage, @required ArgResults argResults, @required Stdio stdio, - @required Platform platform, - @required FileSystem fileSystem, - @required Repository repository, + @required FrameworkRepository repository, String remoteName = 'origin', }) { final String level = argResults[kIncrement] as String; diff --git a/dev/tools/lib/stdio.dart b/dev/tools/lib/stdio.dart index 53231de3bda..d5c4477bee5 100644 --- a/dev/tools/lib/stdio.dart +++ b/dev/tools/lib/stdio.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; +import 'dart:io' as io; import 'package:meta/meta.dart'; @@ -31,9 +31,15 @@ class VerboseStdio extends Stdio { @required this.stdin, }) : assert(stdout != null), assert(stderr != null), assert(stdin != null); - final Stdout stdout; - final Stdout stderr; - final Stdin stdin; + factory VerboseStdio.local() => VerboseStdio( + stdout: io.stdout, + stderr: io.stderr, + stdin: io.stdin, + ); + + final io.Stdout stdout; + final io.Stdout stderr; + final io.Stdin stdin; @override void printError(String message) { diff --git a/dev/tools/test/codesign_integration_test.dart b/dev/tools/test/codesign_integration_test.dart new file mode 100644 index 00000000000..6044414781c --- /dev/null +++ b/dev/tools/test/codesign_integration_test.dart @@ -0,0 +1,71 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import 'package:dev_tools/codesign.dart' show CodesignCommand; +import 'package:dev_tools/globals.dart'; +import 'package:dev_tools/repository.dart' show Checkouts; + +import './common.dart'; + +/// Verify all binaries in the Flutter cache are expected by Conductor. +void main() { + test( + 'validate the expected binaries from the conductor codesign command are present in the cache', + () async { + const Platform platform = LocalPlatform(); + const FileSystem fileSystem = LocalFileSystem(); + const ProcessManager processManager = LocalProcessManager(); + final TestStdio stdio = TestStdio(verbose: true); + final Checkouts checkouts = Checkouts( + fileSystem: fileSystem, + parentDirectory: localFlutterRoot.parent, + platform: platform, + processManager: processManager, + stdio: stdio, + ); + + final CommandRunner runner = CommandRunner('codesign-test', '') + ..addCommand( + CodesignCommand(checkouts: checkouts, flutterRoot: localFlutterRoot)); + + try { + await runner.run([ + 'codesign', + '--verify', + // Only verify if the correct binaries are in the cache + '--no-signatures', + ]); + } on ConductorException catch (e) { + print(fixItInstructions); + fail(e.message); + } on Exception { + print('stdout:\n${stdio.stdout}'); + print('stderr:\n${stdio.error}'); + rethrow; + } + }, onPlatform: { + 'windows': const Skip('codesign command is only supported on macos'), + 'linux': const Skip('codesign command is only supported on macos'), + }); +} + +const String fixItInstructions = ''' +Codesign integration test failed. + +This means that the binary files found in the Flutter cache do not match those +expected by the conductor tool (either an expected file was not found in the +cache or an unexpected file was found in the cache). + +This usually happens either during an engine roll or a change to the caching +logic in flutter_tools. If this is a valid change, then the conductor source +code should be updated, specifically either the [binariesWithEntitlements] or +[binariesWithoutEntitlements] lists, depending on if the file should have macOS +entitlements applied during codesigning. +'''; diff --git a/dev/tools/test/codesign_test.dart b/dev/tools/test/codesign_test.dart new file mode 100644 index 00000000000..d77c98161b0 --- /dev/null +++ b/dev/tools/test/codesign_test.dart @@ -0,0 +1,412 @@ +// 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:args/command_runner.dart'; +import 'package:dev_tools/codesign.dart'; +import 'package:dev_tools/globals.dart'; +import 'package:dev_tools/repository.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; + +import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; +import './common.dart'; + +void main() { + group('codesign command', () { + const String flutterRoot = '/flutter'; + const String checkoutsParentDirectory = '$flutterRoot/dev/tools/'; + const String flutterCache = + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache'; + const String flutterBin = + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/flutter'; + const String revision = 'abcd1234'; + CommandRunner runner; + Checkouts checkouts; + MemoryFileSystem fileSystem; + FakePlatform platform; + TestStdio stdio; + FakeProcessManager processManager; + const List binariesWithEntitlements = [ + '$flutterCache/dart-sdk/bin/dart', + '$flutterCache/dart-sdk/bin/dartaotruntime', + ]; + const List binariesWithoutEntitlements = [ + '$flutterCache/engine/darwin-x64/font-subset', + ]; + const List allBinaries = [ + ...binariesWithEntitlements, + ...binariesWithoutEntitlements, + ]; + + void createRunner({ + String operatingSystem = 'macos', + List commands, + }) { + stdio = TestStdio(); + fileSystem = MemoryFileSystem.test(); + platform = FakePlatform(operatingSystem: operatingSystem); + processManager = FakeProcessManager.list(commands ?? []); + checkouts = Checkouts( + fileSystem: fileSystem, + parentDirectory: fileSystem.directory(checkoutsParentDirectory), + platform: platform, + processManager: processManager, + stdio: stdio, + ); + final FakeCodesignCommand command = FakeCodesignCommand( + checkouts: checkouts, + binariesWithEntitlements: binariesWithEntitlements, + binariesWithoutEntitlements: binariesWithoutEntitlements, + flutterRoot: fileSystem.directory(flutterRoot), + ); + runner = CommandRunner('codesign-test', '') + ..addCommand(command); + } + + test('throws exception if not run from macos', () async { + createRunner(operatingSystem: 'linux'); + expect( + () async => await runner.run(['codesign']), + throwsExceptionWith('Error! Expected operating system "macos"'), + ); + }); + + test('throws exception if verify flag is not provided', () async { + createRunner(); + expect( + () async => await runner.run(['codesign']), + throwsExceptionWith( + 'Sorry, but codesigning is not implemented yet. Please pass the --$kVerify flag to verify signatures'), + ); + }); + + test('succeeds if every binary is codesigned and has correct entitlements', () async { + final List codesignCheckCommands = []; + for (final String bin in binariesWithEntitlements) { + codesignCheckCommands.add( + FakeCommand( + command: ['codesign', '-vvv', bin], + ), + ); + codesignCheckCommands.add( + FakeCommand( + command: ['codesign', '--display', '--entitlements', ':-', bin], + stdout: expectedEntitlements.join('\n'), + ), + ); + } + for (final String bin in binariesWithoutEntitlements) { + codesignCheckCommands.add( + FakeCommand( + command: ['codesign', '-vvv', bin], + ), + ); + } + createRunner(commands: [ + const FakeCommand(command: [ + 'git', + 'clone', + '--', + 'file://$flutterRoot/', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', + ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: revision), + const FakeCommand(command: [ + 'git', + 'checkout', + revision, + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'precache', + '--ios', + '--macos', + ]), + FakeCommand( + command: const [ + 'find', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache', + '-type', + 'f', + ], + stdout: allBinaries.join('\n'), + ), + for (String bin in allBinaries) + FakeCommand( + command: ['file', '--mime-type', '-b', bin], + stdout: 'application/x-mach-binary', + ), + ...codesignCheckCommands, + ]); + await runner.run(['codesign', '--$kVerify', '--$kRevision', revision]); + expect(processManager.hasRemainingExpectations, false); + }); + + test('fails if a single binary is not codesigned', () async { + final List codesignCheckCommands = []; + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dart'], + ), + ); + codesignCheckCommands.add( + FakeCommand( + command: const [ + 'codesign', + '--display', + '--entitlements', + ':-', + '$flutterCache/dart-sdk/bin/dart', + ], + stdout: expectedEntitlements.join('\n'), + ) + ); + // Not signed + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dartaotruntime'], + exitCode: 1, + ), + ); + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '-vvv', '$flutterCache/engine/darwin-x64/font-subset'], + ), + ); + + createRunner(commands: [ + const FakeCommand(command: [ + 'git', + 'clone', + '--', + 'file://$flutterRoot/', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', + ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: revision), + const FakeCommand(command: [ + 'git', + 'checkout', + revision, + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'precache', + '--ios', + '--macos', + ]), + FakeCommand( + command: const [ + 'find', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache', + '-type', + 'f', + ], + stdout: allBinaries.join('\n'), + ), + for (String bin in allBinaries) + FakeCommand( + command: ['file', '--mime-type', '-b', bin], + stdout: 'application/x-mach-binary', + ), + ...codesignCheckCommands, + ]); + expect( + () async => await runner.run(['codesign', '--$kVerify', '--$kRevision', revision]), + throwsExceptionWith('Test failed because unsigned binaries detected.'), + ); + expect(processManager.hasRemainingExpectations, false); + }); + + test('fails if a single binary has the wrong entitlements', () async { + final List codesignCheckCommands = []; + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dart'], + ), + ); + codesignCheckCommands.add( + FakeCommand( + command: const ['codesign', '--display', '--entitlements', ':-', '$flutterCache/dart-sdk/bin/dart'], + stdout: expectedEntitlements.join('\n'), + ) + ); + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dartaotruntime'], + ), + ); + // No entitlements + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '--display', '--entitlements', ':-', '$flutterCache/dart-sdk/bin/dartaotruntime'], + ) + ); + codesignCheckCommands.add( + const FakeCommand( + command: ['codesign', '-vvv', '$flutterCache/engine/darwin-x64/font-subset'], + ), + ); + createRunner(commands: [ + const FakeCommand(command: [ + 'git', + 'clone', + '--', + 'file://$flutterRoot/', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', + ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: revision), + const FakeCommand(command: [ + 'git', + 'checkout', + revision, + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'precache', + '--ios', + '--macos', + ]), + FakeCommand( + command: const [ + 'find', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache', + '-type', + 'f', + ], + stdout: allBinaries.join('\n'), + ), + for (String bin in allBinaries) + FakeCommand( + command: ['file', '--mime-type', '-b', bin], + stdout: 'application/x-mach-binary', + ), + ...codesignCheckCommands, + ]); + expect( + () async => await runner.run(['codesign', '--$kVerify', '--$kRevision', revision]), + throwsExceptionWith('Test failed because files found with the wrong entitlements'), + ); + expect(processManager.hasRemainingExpectations, false); + }); + + test('does not check signatures or entitlements if --no-$kSignatures specified', () async { + createRunner(commands: [ + const FakeCommand(command: [ + 'git', + 'clone', + '--', + 'file://$flutterRoot/', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', + ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: revision), + const FakeCommand(command: [ + 'git', + 'checkout', + revision, + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'help', + ]), + const FakeCommand(command: [ + flutterBin, + 'precache', + '--ios', + '--macos', + ]), + FakeCommand( + command: const [ + 'find', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache', + '-type', + 'f', + ], + stdout: allBinaries.join('\n'), + ), + for (String bin in allBinaries) + FakeCommand( + command: ['file', '--mime-type', '-b', bin], + stdout: 'application/x-mach-binary', + ), + ]); + try { + await runner.run([ + 'codesign', + '--$kVerify', + '--no-$kSignatures', + '--$kRevision', + revision, + ]); + } on ConductorException { + //print(stdio.error); + rethrow; + } + expect( + processManager.hasRemainingExpectations, + false, + ); + }); + }); +} + +class FakeCodesignCommand extends CodesignCommand { + FakeCodesignCommand({ + @required Checkouts checkouts, + @required this.binariesWithEntitlements, + @required this.binariesWithoutEntitlements, + @required Directory flutterRoot, + }) : super(checkouts: checkouts, flutterRoot: flutterRoot); + + @override + final List binariesWithEntitlements; + + @override + final List binariesWithoutEntitlements; +} diff --git a/dev/tools/test/common.dart b/dev/tools/test/common.dart index d2e43121d9b..7ebcc099118 100644 --- a/dev/tools/test/common.dart +++ b/dev/tools/test/common.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:file/file.dart'; import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; import 'package:test/test.dart' as test_package show TypeMatcher; diff --git a/dev/tools/test/roll_dev_integration_test.dart b/dev/tools/test/roll_dev_integration_test.dart index 16bb78758db..13e58712678 100644 --- a/dev/tools/test/roll_dev_integration_test.dart +++ b/dev/tools/test/roll_dev_integration_test.dart @@ -7,6 +7,7 @@ import 'package:file/local.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; +import 'package:dev_tools/globals.dart'; import 'package:dev_tools/roll_dev.dart' show rollDev; import 'package:dev_tools/repository.dart'; import 'package:dev_tools/version.dart'; @@ -22,8 +23,8 @@ void main() { const String usageString = 'Usage: flutter conductor.'; Checkouts checkouts; - Repository frameworkUpstream; - Repository framework; + FrameworkRepository frameworkUpstream; + FrameworkRepository framework; setUp(() { platform = const LocalPlatform(); @@ -32,22 +33,20 @@ void main() { stdio = TestStdio(verbose: true); checkouts = Checkouts( fileSystem: fileSystem, + parentDirectory: localFlutterRoot.parent, platform: platform, processManager: processManager, + stdio: stdio, ); - frameworkUpstream = checkouts.addRepo( - repoType: RepositoryType.framework, - name: 'framework-upstream', - stdio: stdio, - platform: platform, - localUpstream: true, - fileSystem: fileSystem, - useExistingCheckout: false, - ); + frameworkUpstream = FrameworkRepository(checkouts, localUpstream: true); // This repository has [frameworkUpstream] set as its push/pull remote. - framework = frameworkUpstream.cloneRepository('test-framework'); + framework = FrameworkRepository( + checkouts, + name: 'test-framework', + upstream: 'file://${frameworkUpstream.checkoutDirectory.path}/', + ); }); test('increment m', () { @@ -68,8 +67,6 @@ void main() { usage: usageString, argResults: fakeArgResults, stdio: stdio, - fileSystem: fileSystem, - platform: platform, repository: framework, ), true, @@ -107,8 +104,6 @@ void main() { usage: usageString, argResults: fakeArgResults, stdio: stdio, - fileSystem: fileSystem, - platform: platform, repository: framework, ), true, diff --git a/dev/tools/test/roll_dev_test.dart b/dev/tools/test/roll_dev_test.dart index 468ded68749..e88cdaff2ea 100644 --- a/dev/tools/test/roll_dev_test.dart +++ b/dev/tools/test/roll_dev_test.dart @@ -24,7 +24,7 @@ void main() { FakeArgResults fakeArgResults; MemoryFileSystem fileSystem; TestStdio stdio; - Repository repo; + FrameworkRepository repo; Checkouts checkouts; FakePlatform platform; FakeProcessManager processManager; @@ -39,12 +39,9 @@ void main() { parentDirectory: fileSystem.directory(checkoutsParentDirectory), platform: platform, processManager: processManager, - ); - repo = checkouts.addRepo( - platform: platform, - repoType: RepositoryType.framework, stdio: stdio, ); + repo = FrameworkRepository(checkouts); }); test('returns false if level not provided', () { @@ -56,8 +53,6 @@ void main() { expect( rollDev( argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, usage: usage, @@ -75,8 +70,6 @@ void main() { expect( rollDev( argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, usage: usage, @@ -92,8 +85,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -115,8 +113,6 @@ void main() { try { rollDev( argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, usage: usage, @@ -137,8 +133,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -187,8 +188,6 @@ void main() { rollDev( usage: usage, argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, ), @@ -206,8 +205,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -267,8 +271,6 @@ void main() { () => rollDev( usage: usage, argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, ), @@ -283,8 +285,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -333,8 +340,6 @@ void main() { () => rollDev( usage: usage, argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, ), @@ -353,8 +358,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -410,8 +420,6 @@ void main() { expect( () => rollDev( argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, usage: usage, @@ -427,8 +435,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -501,8 +514,6 @@ void main() { rollDev( usage: usage, argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, ), @@ -517,8 +528,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -595,8 +611,6 @@ void main() { rollDev( usage: usage, argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, ), @@ -611,8 +625,13 @@ void main() { 'clone', '--', kUpstreamRemote, - '${checkoutsParentDirectory}checkouts/framework', + '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', ]), + const FakeCommand(command: [ + 'git', + 'rev-parse', + 'HEAD', + ], stdout: commit), const FakeCommand(command: [ 'git', 'remote', @@ -684,8 +703,6 @@ void main() { expect( rollDev( argResults: fakeArgResults, - fileSystem: fileSystem, - platform: platform, repository: repo, stdio: stdio, usage: usage,