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(