From 74cc30330a5b4eba08dea977e4b9fffc690f1867 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 22 Aug 2024 10:18:48 -0700 Subject: [PATCH] macOS: Copy macOS framwork dSYM into build outputs As of Xcode 16, App Store validation now requires that apps uploaded to the App store bundle dSYM debug information bundles for each Framework they embed. dSYM bundles are packaged in the FlutterMacOS.xcframework shipped in the `darwin-x64-release` tools archive as of engine patches: * https://github.com/flutter/engine/pull/54696 This copies the FlutterMacOS.framework.dSYM bundle from the tools cache to the build outputs produced by `flutter build macos`. Issue: https://github.com/flutter/flutter/issue/153879 --- packages/flutter_tools/lib/src/artifacts.dart | 106 ++++++++++++++--- .../lib/src/build_system/targets/macos.dart | 33 +++++- .../build_system/targets/macos_test.dart | 109 ++++++++++++++++-- 3 files changed, 222 insertions(+), 26 deletions(-) diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index c1a4b84df5b3f8..fe430275faba26 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -26,6 +26,7 @@ enum Artifact { flutterXcframework, /// The framework directory of the macOS desktop. flutterMacOSFramework, + flutterMacOSFrameworkDsym, flutterMacOSXcframework, vmSnapshotData, isolateSnapshotData, @@ -182,12 +183,14 @@ String? _artifactToFileName(Artifact artifact, Platform hostPlatform, [ BuildMod return 'flutter_tester$exe'; case Artifact.flutterFramework: return 'Flutter.framework'; - case Artifact.flutterFrameworkDsym: - return 'Flutter.framework.dSYM'; + case Artifact.flutterFrameworkDsym: + return 'Flutter.framework.dSYM'; case Artifact.flutterXcframework: return 'Flutter.xcframework'; case Artifact.flutterMacOSFramework: return 'FlutterMacOS.framework'; + case Artifact.flutterMacOSFrameworkDsym: + return 'FlutterMacOS.framework.dSYM'; case Artifact.flutterMacOSXcframework: return 'FlutterMacOS.xcframework'; case Artifact.vmSnapshotData: @@ -600,15 +603,44 @@ class CachedArtifacts implements Artifacts { String _getDesktopArtifactPath(Artifact artifact, TargetPlatform platform, BuildMode? mode) { // When platform is null, a generic host platform artifact is being requested // and not the gen_snapshot for darwin as a target platform. - if (artifact == Artifact.genSnapshot) { - final String engineDir = _getEngineArtifactsPath(platform, mode)!; - return _fileSystem.path.join(engineDir, _artifactToFileName(artifact, _platform)); - } - if (artifact == Artifact.flutterMacOSFramework) { - final String engineDir = _getEngineArtifactsPath(platform, mode)!; - return _getMacOSEngineArtifactPath(engineDir, _fileSystem, _platform); + final String engineDir = _getEngineArtifactsPath(platform, mode)!; + switch (artifact) { + case Artifact.genSnapshot: + return _fileSystem.path.join(engineDir, _artifactToFileName(artifact, _platform)); + case Artifact.engineDartSdkPath: + case Artifact.engineDartBinary: + case Artifact.engineDartAotRuntime: + case Artifact.dart2jsSnapshot: + case Artifact.dart2wasmSnapshot: + case Artifact.frontendServerSnapshotForEngineDartSdk: + case Artifact.constFinder: + case Artifact.flutterFramework: + case Artifact.flutterFrameworkDsym: + case Artifact.flutterMacOSFramework: + return _getMacOSFrameworkPath(engineDir, _fileSystem, _platform); + case Artifact.flutterMacOSFrameworkDsym: + return _getMacOSFrameworkDsymPath(engineDir, _fileSystem, _platform); + case Artifact.flutterMacOSXcframework: + case Artifact.flutterPatchedSdkPath: + case Artifact.flutterTester: + case Artifact.flutterXcframework: + case Artifact.fontSubset: + case Artifact.fuchsiaFlutterRunner: + case Artifact.fuchsiaKernelCompiler: + case Artifact.icuData: + case Artifact.isolateSnapshotData: + case Artifact.linuxDesktopPath: + case Artifact.linuxHeaders: + case Artifact.platformKernelDill: + case Artifact.platformLibrariesJson: + case Artifact.skyEnginePath: + case Artifact.vmSnapshotData: + case Artifact.windowsCppClientWrapper: + case Artifact.windowsDesktopPath: + case Artifact.flutterToolsFileGenerators: + case Artifact.flutterPreviewDevice: + return _getHostArtifactPath(artifact, platform, mode); } - return _getHostArtifactPath(artifact, platform, mode); } String _getAndroidArtifactPath(Artifact artifact, TargetPlatform platform, BuildMode mode) { @@ -628,6 +660,7 @@ class CachedArtifacts implements Artifacts { case Artifact.flutterFramework: case Artifact.flutterFrameworkDsym: case Artifact.flutterMacOSFramework: + case Artifact.flutterMacOSFrameworkDsym: case Artifact.flutterMacOSXcframework: case Artifact.flutterPatchedSdkPath: case Artifact.flutterTester: @@ -672,6 +705,7 @@ class CachedArtifacts implements Artifacts { case Artifact.frontendServerSnapshotForEngineDartSdk: case Artifact.constFinder: case Artifact.flutterMacOSFramework: + case Artifact.flutterMacOSFrameworkDsym: case Artifact.flutterMacOSXcframework: case Artifact.flutterPatchedSdkPath: case Artifact.flutterTester: @@ -722,6 +756,7 @@ class CachedArtifacts implements Artifacts { case Artifact.flutterFramework: case Artifact.flutterFrameworkDsym: case Artifact.flutterMacOSFramework: + case Artifact.flutterMacOSFrameworkDsym: case Artifact.flutterMacOSXcframework: case Artifact.flutterTester: case Artifact.flutterXcframework: @@ -794,7 +829,14 @@ class CachedArtifacts implements Artifacts { platformDirName = '$platformDirName-${mode!.cliName}'; } final String engineArtifactsPath = _cache.getArtifactDirectory('engine').path; - return _getMacOSEngineArtifactPath(_fileSystem.path.join(engineArtifactsPath, platformDirName), _fileSystem, _platform); + return _getMacOSFrameworkPath(_fileSystem.path.join(engineArtifactsPath, platformDirName), _fileSystem, _platform); + case Artifact.flutterMacOSFrameworkDsym: + String platformDirName = _enginePlatformDirectoryName(platform); + if (mode == BuildMode.profile || mode == BuildMode.release) { + platformDirName = '$platformDirName-${mode!.cliName}'; + } + final String engineArtifactsPath = _cache.getArtifactDirectory('engine').path; + return _getMacOSFrameworkDsymPath(_fileSystem.path.join(engineArtifactsPath, platformDirName), _fileSystem, _platform); case Artifact.flutterMacOSXcframework: case Artifact.linuxDesktopPath: case Artifact.windowsDesktopPath: @@ -957,7 +999,13 @@ String _getIosFrameworkDsymPath( .path; } -String _getMacOSEngineArtifactPath( +/// Returns the Flutter.xcframework platform directory for the specified environment type. +/// +/// `FlutterMacOS.xcframework` contains target environment/architecture-specific +/// subdirectories containing the appropriate `FlutterMacOS.framework` and +/// `FlutterMacOS.framework.dSYM` bundles for that target architecture. At present, +/// there is only one such directory: `macos-arm64_x86_64`. +Directory _getMacOSFrameworkPlatformDirectory( String engineDirectory, FileSystem fileSystem, Platform hostPlatform, @@ -969,21 +1017,43 @@ String _getMacOSEngineArtifactPath( if (!xcframeworkDirectory.existsSync()) { throwToolExit('No xcframework found at ${xcframeworkDirectory.path}. Try running "flutter precache --macos".'); } - final Directory? flutterFrameworkSource = xcframeworkDirectory + final Directory? platformDirectory = xcframeworkDirectory .listSync() .whereType() .where((Directory platformDirectory) => platformDirectory.basename.startsWith('macos-')) .firstOrNull; - if (flutterFrameworkSource == null) { + if (platformDirectory == null) { throwToolExit('No macOS frameworks found in ${xcframeworkDirectory.path}'); } + return platformDirectory; +} - return flutterFrameworkSource +/// Returns the path to `FlutterMacOS.framework`. +String _getMacOSFrameworkPath( + String engineDirectory, + FileSystem fileSystem, + Platform hostPlatform, +) { + final Directory platformDirectory = _getMacOSFrameworkPlatformDirectory(engineDirectory, fileSystem, hostPlatform); + return platformDirectory .childDirectory(_artifactToFileName(Artifact.flutterMacOSFramework, hostPlatform)!) .path; } +/// Returns the path to `FlutterMacOS.framework`. +String _getMacOSFrameworkDsymPath( + String engineDirectory, + FileSystem fileSystem, + Platform hostPlatform, +) { + final Directory platformDirectory = _getMacOSFrameworkPlatformDirectory(engineDirectory, fileSystem, hostPlatform); + return platformDirectory + .childDirectory('dSYMs') + .childDirectory(_artifactToFileName(Artifact.flutterMacOSFrameworkDsym, hostPlatform)!) + .path; +} + /// Manages the artifacts of a locally built engine. class CachedLocalEngineArtifacts implements Artifacts { CachedLocalEngineArtifacts( @@ -1158,7 +1228,10 @@ class CachedLocalEngineArtifacts implements Artifacts { return _getIosFrameworkDsymPath( localEngineInfo.targetOutPath, environmentType, _fileSystem, _platform); case Artifact.flutterMacOSFramework: - return _getMacOSEngineArtifactPath( + return _getMacOSFrameworkPath( + localEngineInfo.targetOutPath, _fileSystem, _platform); + case Artifact.flutterMacOSFrameworkDsym: + return _getMacOSFrameworkDsymPath( localEngineInfo.targetOutPath, _fileSystem, _platform); case Artifact.flutterPatchedSdkPath: // When using local engine always use [BuildMode.debug] regardless of @@ -1341,6 +1414,7 @@ class CachedLocalWebSdkArtifacts implements Artifacts { case Artifact.flutterFrameworkDsym: case Artifact.flutterXcframework: case Artifact.flutterMacOSFramework: + case Artifact.flutterMacOSFrameworkDsym: case Artifact.flutterMacOSXcframework: case Artifact.vmSnapshotData: case Artifact.isolateSnapshotData: diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 1d0a0a7b68673d..aa70d8e521d2ff 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -40,6 +40,7 @@ abstract class UnpackMacOS extends Target { @override List get outputs => const [ Source.pattern('{OUTPUT_DIR}/FlutterMacOS.framework/Versions/A/FlutterMacOS'), + Source.pattern('{OUTPUT_DIR}/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS'), ]; @override @@ -51,9 +52,10 @@ abstract class UnpackMacOS extends Target { if (buildModeEnvironment == null) { throw MissingDefineException(kBuildMode, 'unpack_macos'); } + + // Copy Flutter framework. final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final String basePath = environment.artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: buildMode); - final ProcessResult result = environment.processManager.runSync([ 'rsync', '-av', @@ -82,7 +84,35 @@ abstract class UnpackMacOS extends Target { if (!frameworkBinary.existsSync()) { throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin'); } + await _thinFramework(environment, frameworkBinaryPath); + + // Copy Flutter framework dSYM (debug symbol) bundle, if present. + final Directory frameworkDsym = environment.fileSystem.directory( + environment.artifacts.getArtifactPath( + Artifact.flutterMacOSFrameworkDsym, + platform: TargetPlatform.darwin, + mode: buildMode, + ) + ); + if (frameworkDsym.existsSync()) { + final ProcessResult result = await environment.processManager.run([ + 'rsync', + '-av', + '--delete', + '--filter', + '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', + frameworkDsym.path, + environment.outputDir.path, + ]); + if (result.exitCode != 0) { + throw Exception( + 'Failed to copy framework dSYM (exit ${result.exitCode}:\n' + '${result.stdout}\n---\n${result.stderr}', + ); + } + } } static const List _copyDenylist = ['entitlements.txt', 'without_entitlements.txt']; @@ -159,6 +189,7 @@ class ReleaseUnpackMacOS extends UnpackMacOS { List get inputs => [ ...super.inputs, const Source.artifact(Artifact.flutterMacOSXcframework, mode: BuildMode.release), + const Source.artifact(Artifact.flutterMacOSXcframework, mode: BuildMode.release), ]; } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index e172b7b31eefba..a1c33c0932e6da 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -25,11 +25,14 @@ void main() { late Artifacts artifacts; late FakeProcessManager processManager; late File binary; + late File frameworkDsym; late BufferLogger logger; late FakeCommand copyFrameworkCommand; + late FakeCommand copyFrameworkDsymCommand; late FakeCommand lipoInfoNonFatCommand; late FakeCommand lipoInfoFatCommand; late FakeCommand lipoVerifyX86_64Command; + late FakeCommand lipoExtractX86_64Command; late TestUsage usage; late FakeAnalytics fakeAnalytics; @@ -79,6 +82,38 @@ void main() { ], ); + frameworkDsym = fileSystem.directory( + artifacts.getArtifactPath( + Artifact.flutterMacOSFrameworkDsym, + platform: TargetPlatform.darwin, + mode: BuildMode.debug, + ), + ) + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('FlutterMacOS'); + + environment.outputDir + .childDirectory('FlutterMacOS.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('FlutterMacOS'); + + copyFrameworkDsymCommand = FakeCommand( + command: [ + 'rsync', + '-av', + '--delete', + '--filter', + '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', + 'Artifact.flutterMacOSFrameworkDsym.TargetPlatform.darwin.debug', + environment.outputDir.path, + ], + ); + lipoInfoNonFatCommand = FakeCommand(command: [ 'lipo', '-info', @@ -97,14 +132,41 @@ void main() { '-verify_arch', 'x86_64', ]); + + lipoExtractX86_64Command = FakeCommand(command: [ + 'lipo', + '-output', + binary.path, + '-extract', + 'x86_64', + binary.path, + ]); + }); + + testUsingContext('Copies files to correct cache directory when no dSYM available in xcframework', () async { + binary.createSync(recursive: true); + processManager.addCommands([ + copyFrameworkCommand, + lipoInfoNonFatCommand, + lipoVerifyX86_64Command, + ]); + + await const DebugUnpackMacOS().build(environment); + + expect(processManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, }); - testUsingContext('Copies files to correct cache directory', () async { + testUsingContext('Copies files to correct cache directory when dSYM available in xcframework', () async { binary.createSync(recursive: true); + frameworkDsym.createSync(recursive: true); processManager.addCommands([ copyFrameworkCommand, lipoInfoNonFatCommand, lipoVerifyX86_64Command, + copyFrameworkDsymCommand, ]); await const DebugUnpackMacOS().build(environment); @@ -223,14 +285,7 @@ void main() { copyFrameworkCommand, lipoInfoFatCommand, lipoVerifyX86_64Command, - FakeCommand(command: [ - 'lipo', - '-output', - binary.path, - '-extract', - 'x86_64', - binary.path, - ]), + lipoExtractX86_64Command, ]); await const DebugUnpackMacOS().build(environment); @@ -238,6 +293,42 @@ void main() { expect(processManager, hasNoRemainingExpectations); }); + testUsingContext('Fails if framework dSYM found within framework but copy fails', () async { + binary.createSync(recursive: true); + frameworkDsym.createSync(recursive: true); + final FakeCommand failedCopyFrameworkDsymCommand = FakeCommand( + command: [ + 'rsync', + '-av', + '--delete', + '--filter', + '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', + 'Artifact.flutterMacOSFrameworkDsym.TargetPlatform.darwin.debug', + environment.outputDir.path, + ], exitCode: 1, + ); + processManager.addCommands([ + copyFrameworkCommand, + lipoInfoFatCommand, + lipoVerifyX86_64Command, + lipoExtractX86_64Command, + failedCopyFrameworkDsymCommand, + ]); + + await expectLater( + const DebugUnpackMacOS().build(environment), + throwsA(isException.having( + (Exception exception) => exception.toString(), + 'description', + contains('Failed to copy framework dSYM'), + )), + ); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + testUsingContext('debug macOS application fails if App.framework missing', () async { fileSystem.directory( artifacts.getArtifactPath(