diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index c1a4b84df5..fe430275fa 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)); + 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); } - if (artifact == Artifact.flutterMacOSFramework) { - final String engineDir = _getEngineArtifactsPath(platform, mode)!; - return _getMacOSEngineArtifactPath(engineDir, _fileSystem, _platform); - } - 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 1d0a0a7b68..b1ded0265b 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -51,9 +51,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,6 +83,7 @@ abstract class UnpackMacOS extends Target { if (!frameworkBinary.existsSync()) { throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin'); } + await _thinFramework(environment, frameworkBinaryPath); } @@ -156,10 +158,51 @@ class ReleaseUnpackMacOS extends UnpackMacOS { String get name => 'release_unpack_macos'; @override - List get inputs => [ - ...super.inputs, - const Source.artifact(Artifact.flutterMacOSXcframework, mode: BuildMode.release), + List get outputs => super.outputs + const [ + Source.pattern('{OUTPUT_DIR}/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS'), ]; + + @override + List get inputs => super.inputs + const [ + Source.artifact(Artifact.flutterMacOSXcframework, mode: BuildMode.release), + ]; + + @override + Future build(Environment environment) async { + await super.build(environment); + + // Copy Flutter framework dSYM (debug symbol) bundle, if present. + final String? buildModeEnvironment = environment.defines[kBuildMode]; + if (buildModeEnvironment == null) { + throw MissingDefineException(kBuildMode, 'unpack_macos'); + } + final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); + 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}', + ); + } + } + } } /// Unpack the profile prebuilt engine framework. 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 e172b7b31e..06e1314c31 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,15 @@ void main() { late Artifacts artifacts; late FakeProcessManager processManager; late File binary; + late File frameworkDsym; late BufferLogger logger; late FakeCommand copyFrameworkCommand; + late FakeCommand releaseCopyFrameworkCommand; + 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 +83,51 @@ void main() { ], ); + releaseCopyFrameworkCommand = FakeCommand( + command: [ + 'rsync', + '-av', + '--delete', + '--filter', + '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', + 'Artifact.flutterMacOSFramework.release', + environment.outputDir.path, + ], + ); + + frameworkDsym = fileSystem.directory( + artifacts.getArtifactPath( + Artifact.flutterMacOSFrameworkDsym, + platform: TargetPlatform.darwin, + mode: BuildMode.release, + ), + ) + .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.release', + environment.outputDir.path, + ], + ); + lipoInfoNonFatCommand = FakeCommand(command: [ 'lipo', '-info', @@ -97,6 +146,15 @@ 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', () async { @@ -223,14 +281,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 +289,76 @@ void main() { expect(processManager, hasNoRemainingExpectations); }); + testUsingContext('Copies files to correct cache directory when no dSYM available in xcframework', () async { + binary.createSync(recursive: true); + processManager.addCommands([ + releaseCopyFrameworkCommand, + lipoInfoNonFatCommand, + lipoVerifyX86_64Command, + ]); + + await const ReleaseUnpackMacOS().build(environment..defines[kBuildMode] = 'release'); + + expect(processManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + + testUsingContext('Copies files to correct cache directory when dSYM available in xcframework', () async { + binary.createSync(recursive: true); + frameworkDsym.createSync(recursive: true); + processManager.addCommands([ + releaseCopyFrameworkCommand, + lipoInfoNonFatCommand, + lipoVerifyX86_64Command, + copyFrameworkDsymCommand, + ]); + + await const ReleaseUnpackMacOS().build(environment..defines[kBuildMode] = 'release'); + + expect(processManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + + 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.release', + environment.outputDir.path, + ], exitCode: 1, + ); + processManager.addCommands([ + releaseCopyFrameworkCommand, + lipoInfoFatCommand, + lipoVerifyX86_64Command, + lipoExtractX86_64Command, + failedCopyFrameworkDsymCommand, + ]); + + await expectLater( + const ReleaseUnpackMacOS().build(environment..defines[kBuildMode] = 'release'), + 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( diff --git a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart index e2ae11e127..2e8f8997bc 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart @@ -24,48 +24,68 @@ void main() { ]); }); - test('verify FlutterMacOS.xcframework artifact', () { - final String flutterRoot = getFlutterRoot(); + for (final String buildMode in ['Debug', 'Release']) { + test('verify $buildMode FlutterMacOS.xcframework artifact', () { + final String flutterRoot = getFlutterRoot(); - final Directory xcframeworkArtifact = fileSystem.directory( - fileSystem.path.join( - flutterRoot, - 'bin', - 'cache', - 'artifacts', - 'engine', - 'darwin-x64', - 'FlutterMacOS.xcframework', - ), - ); + final String artifactDir = (buildMode == 'Debug') ? 'darwin-x64' : 'darwin-x64-release'; + final Directory xcframeworkArtifact = fileSystem.directory( + fileSystem.path.join( + flutterRoot, + 'bin', + 'cache', + 'artifacts', + 'engine', + artifactDir, + 'FlutterMacOS.xcframework', + ), + ); - final Directory tempDir = createResolvedTempDirectorySync('macos_content_validation.'); + final Directory tempDir = createResolvedTempDirectorySync('macos_content_validation.'); - // Pre-cache macOS engine FlutterMacOS.xcframework artifacts. - final ProcessResult result = processManager.runSync( - [ - flutterBin, - ...getLocalEngineArguments(), - 'precache', - '--macos', - ], - workingDirectory: tempDir.path, - ); + // Pre-cache macOS engine FlutterMacOS.xcframework artifacts. + final ProcessResult result = processManager.runSync( + [ + flutterBin, + ...getLocalEngineArguments(), + 'precache', + '--macos', + ], + workingDirectory: tempDir.path, + ); - expect(result, const ProcessResultMatcher()); - expect(xcframeworkArtifact.existsSync(), isTrue); + expect(result, const ProcessResultMatcher()); + expect(xcframeworkArtifact.existsSync(), isTrue); - final Directory frameworkArtifact = fileSystem.directory( - fileSystem.path.joinAll([ - xcframeworkArtifact.path, - 'macos-arm64_x86_64', - 'FlutterMacOS.framework', - ]), - ); - // Check read/write permissions are set correctly in the framework engine artifact. - final String artifactStat = frameworkArtifact.statSync().mode.toRadixString(8); - expect(artifactStat, '40755'); - }); + final Directory frameworkArtifact = fileSystem.directory( + fileSystem.path.joinAll([ + xcframeworkArtifact.path, + 'macos-arm64_x86_64', + 'FlutterMacOS.framework', + ]), + ); + // Check read/write permissions are set correctly in the framework engine artifact. + final String artifactStat = frameworkArtifact.statSync().mode.toRadixString(8); + expect(artifactStat, '40755'); + + if (buildMode == 'Release') { + final Directory dsymArtifact = fileSystem.directory( + fileSystem.path.joinAll([ + xcframeworkArtifact.path, + 'macos-arm64_x86_64', + 'dSYMs', + 'FlutterMacOS.framework.dSYM', + ]), + ); + // Verify dSYM is present. + expect(dsymArtifact.existsSync(), isTrue); + + // Check read/write permissions are set correctly in the framework engine artifact. + final String artifactStat = dsymArtifact.statSync().mode.toRadixString(8); + expect(artifactStat, '40755'); + } + }); + } for (final String buildMode in ['Debug', 'Release']) { final String buildModeLower = buildMode.toLowerCase(); @@ -129,6 +149,9 @@ void main() { 'App.framework', )); + final File frameworkDsymBinary = + buildPath.childFile('FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS'); + final File libBinary = outputAppFramework.childFile('App'); final File libDsymBinary = buildPath.childFile('App.framework.dSYM/Contents/Resources/DWARF/App'); @@ -138,10 +161,17 @@ void main() { final List libSymbols = AppleTestUtils.getExportedSymbols(libBinary.path); if (buildMode == 'Debug') { + // Framework dSYM is not copied for debug builds. + expect(frameworkDsymBinary.existsSync(), isFalse); + // dSYM is not created for a debug build. expect(libDsymBinary.existsSync(), isFalse); expect(libSymbols, isEmpty); } else { + // Check framework dSYM file copied. + _checkFatBinary(frameworkDsymBinary, buildModeLower, 'dSYM companion file'); + + // Check extracted dSYM file. _checkFatBinary(libDsymBinary, buildModeLower, 'dSYM companion file'); expect(libSymbols, equals(AppleTestUtils.requiredSymbols)); final List dSymSymbols =