diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart index 0192a2e13e1b..fa611ac463a6 100644 --- a/packages/flutter_tools/lib/src/commands/clean.dart +++ b/packages/flutter_tools/lib/src/commands/clean.dart @@ -71,7 +71,8 @@ class CleanCommand extends FlutterCommand { } Future _cleanXcode(XcodeBasedProject xcodeProject) async { - if (!xcodeProject.existsSync()) { + final Directory? xcodeWorkspace = xcodeProject.xcodeWorkspace; + if (xcodeWorkspace == null) { return; } final Status xcodeStatus = globals.logger.startProgress( @@ -79,7 +80,6 @@ class CleanCommand extends FlutterCommand { ); try { final XcodeProjectInterpreter xcodeProjectInterpreter = globals.xcodeProjectInterpreter!; - final Directory xcodeWorkspace = xcodeProject.xcodeWorkspace; final XcodeProjectInfo projectInfo = (await xcodeProjectInterpreter.getInfo(xcodeWorkspace.parent.path))!; for (final String scheme in projectInfo.schemes) { await xcodeProjectInterpreter.cleanWorkspace(xcodeWorkspace.path, scheme, verbose: _verbose); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 67ebedaf464e..b0fdfd7f8ec9 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -250,17 +250,14 @@ Future buildXcodeProject({ buildCommands.add('-allowProvisioningDeviceRegistration'); } - final List contents = app.project.hostAppRoot.listSync(); - for (final FileSystemEntity entity in contents) { - if (globals.fs.path.extension(entity.path) == '.xcworkspace') { - buildCommands.addAll([ - '-workspace', globals.fs.path.basename(entity.path), - '-scheme', scheme, - if (buildAction != XcodeBuildAction.archive) // dSYM files aren't copied to the archive if BUILD_DIR is set. - 'BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}', - ]); - break; - } + final Directory? workspacePath = app.project.xcodeWorkspace; + if (workspacePath != null) { + buildCommands.addAll([ + '-workspace', workspacePath.basename, + '-scheme', scheme, + if (buildAction != XcodeBuildAction.archive) // dSYM files aren't copied to the archive if BUILD_DIR is set. + 'BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}', + ]); } // Check if the project contains a watchOS companion app. diff --git a/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart index 7d200a479237..6a3d90927cbe 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart @@ -15,16 +15,17 @@ class XcodeBuildSystemMigration extends ProjectMigrator { super.logger, ) : _xcodeWorkspaceSharedSettings = project.xcodeWorkspaceSharedSettings; - final File _xcodeWorkspaceSharedSettings; + final File? _xcodeWorkspaceSharedSettings; @override void migrate() { - if (!_xcodeWorkspaceSharedSettings.existsSync()) { + final File? xcodeWorkspaceSharedSettings = _xcodeWorkspaceSharedSettings; + if (xcodeWorkspaceSharedSettings == null || !xcodeWorkspaceSharedSettings.existsSync()) { logger.printTrace('Xcode workspace settings not found, skipping build system migration'); return; } - final String contents = _xcodeWorkspaceSharedSettings.readAsStringSync(); + final String contents = xcodeWorkspaceSharedSettings.readAsStringSync(); // Only delete this file when it is pointing to the legacy build system. const String legacyBuildSettingsWorkspace = ''' @@ -33,8 +34,8 @@ class XcodeBuildSystemMigration extends ProjectMigrator { // contains instead of equals to ignore newline file ending variance. if (contents.contains(legacyBuildSettingsWorkspace)) { - logger.printStatus('Legacy build system detected, removing ${_xcodeWorkspaceSharedSettings.path}'); - _xcodeWorkspaceSharedSettings.deleteSync(); + logger.printStatus('Legacy build system detected, removing ${xcodeWorkspaceSharedSettings.path}'); + xcodeWorkspaceSharedSettings.deleteSync(); } } } diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index f4e8df7c4922..ae05b8a0350b 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -36,7 +36,8 @@ Future buildMacOS({ required bool verboseLogging, SizeAnalyzer? sizeAnalyzer, }) async { - if (!flutterProject.macos.xcodeWorkspace.existsSync()) { + final Directory? xcodeWorkspace = flutterProject.macos.xcodeWorkspace; + if (xcodeWorkspace == null) { throwToolExit('No macOS desktop project configured. ' 'See https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app ' 'to learn about adding macOS support to a project.'); @@ -106,7 +107,7 @@ Future buildMacOS({ '/usr/bin/env', 'xcrun', 'xcodebuild', - '-workspace', flutterProject.macos.xcodeWorkspace.path, + '-workspace', xcodeWorkspace.path, '-configuration', configuration, '-scheme', 'Runner', '-derivedDataPath', flutterBuildDir.absolute.path, diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 2100074ff290..ca08f8960c48 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -47,13 +47,26 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { .childFile('contents.xcworkspacedata'); /// The Xcode workspace (.xcworkspace directory) of the host app. - Directory get xcodeWorkspace => hostAppRoot.childDirectory('$_hostAppProjectName.xcworkspace'); + Directory? get xcodeWorkspace { + if (!hostAppRoot.existsSync()) { + return null; + } + final List contents = hostAppRoot.listSync(); + for (final FileSystemEntity entity in contents) { + // On certain volume types, there is sometimes a stray `._Runner.xcworkspace` file. + // Find the first non-hidden xcworkspace and return the directory. + if (globals.fs.path.extension(entity.path) == '.xcworkspace' && !globals.fs.path.basename(entity.path).startsWith('.')) { + return hostAppRoot.childDirectory(entity.basename); + } + } + return null; + } /// Xcode workspace shared data directory for the host app. - Directory get xcodeWorkspaceSharedData => xcodeWorkspace.childDirectory('xcshareddata'); + Directory? get xcodeWorkspaceSharedData => xcodeWorkspace?.childDirectory('xcshareddata'); /// Xcode workspace shared workspace settings file for the host app. - File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings'); + File? get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData?.childFile('WorkspaceSettings.xcsettings'); /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for /// the Xcode build. diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart index acab399a12f6..291f820d46e3 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart @@ -99,7 +99,7 @@ void main() { '/usr/bin/env', 'xcrun', 'xcodebuild', - '-workspace', flutterProject.macos.xcodeWorkspace.path, + '-workspace', flutterProject.macos.xcodeWorkspace!.path, '-configuration', configuration, '-scheme', 'Runner', '-derivedDataPath', flutterBuildDir.absolute.path, @@ -337,6 +337,7 @@ STDERR STUFF final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); final Directory flutterBuildDir = fileSystem.directory(getMacOSBuildDirectory()); + createMinimalMockProjectFiles(); fakeProcessManager.addCommands([ FakeCommand( @@ -344,7 +345,7 @@ STDERR STUFF '/usr/bin/env', 'xcrun', 'xcodebuild', - '-workspace', flutterProject.macos.xcodeWorkspace.path, + '-workspace', flutterProject.macos.xcodeWorkspace!.path, '-configuration', 'Debug', '-scheme', 'Runner', '-derivedDataPath', flutterBuildDir.absolute.path, @@ -359,7 +360,6 @@ STDERR STUFF ]); final BuildCommand command = BuildCommand(); - createMinimalMockProjectFiles(); await createTestCommandRunner(command).run( const ['build', 'macos', '--debug', '--no-pub'] diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index d703026fc651..43bf911179f2 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -44,7 +44,7 @@ void main() { }); testUsingContext('$CleanCommand removes build and .dart_tool and ephemeral directories, cleans Xcode for iOS and macOS', () async { - final FlutterProject projectUnderTest = setupProjectUnderTest(fs.currentDirectory); + final FlutterProject projectUnderTest = setupProjectUnderTest(fs.currentDirectory, true); // Xcode is installed and version satisfactory. xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); @@ -70,7 +70,7 @@ void main() { expect(projectUnderTest.flutterPluginsDependenciesFile, isNot(exists)); expect(projectUnderTest.packagesFile, isNot(exists)); - expect(xcodeProjectInterpreter.workspaces, const [ + expect(xcodeProjectInterpreter.workspaces, const [ CleanWorkspaceCall('/ios/Runner.xcworkspace', 'Runner', false), CleanWorkspaceCall('/macos/Runner.xcworkspace', 'Runner', false), ]); @@ -81,8 +81,23 @@ void main() { XcodeProjectInterpreter: () => xcodeProjectInterpreter, }); + testUsingContext('$CleanCommand does not run when there is no xcworkspace', () async { + setupProjectUnderTest(fs.currentDirectory, false); + // Xcode is installed and version satisfactory. + xcodeProjectInterpreter.isInstalled = true; + xcodeProjectInterpreter.version = Version(1000, 0, 0); + await CleanCommand().runCommand(); + + expect(xcodeProjectInterpreter.workspaces, const []); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Xcode: () => xcode, + XcodeProjectInterpreter: () => xcodeProjectInterpreter, + }); + testUsingContext('$CleanCommand cleans Xcode verbosely for iOS and macOS', () async { - setupProjectUnderTest(fs.currentDirectory); + setupProjectUnderTest(fs.currentDirectory, true); // Xcode is installed and version satisfactory. xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(1000, 0, 0); @@ -154,12 +169,13 @@ void main() { }); } -FlutterProject setupProjectUnderTest(Directory currentDirectory) { +FlutterProject setupProjectUnderTest(Directory currentDirectory, bool setupXcodeWorkspace) { // This needs to be run within testWithoutContext and not setUp since FlutterProject uses context. final FlutterProject projectUnderTest = FlutterProject.fromDirectory(currentDirectory); - projectUnderTest.ios.xcodeWorkspace.createSync(recursive: true); - projectUnderTest.macos.xcodeWorkspace.createSync(recursive: true); - + if (setupXcodeWorkspace == true) { + projectUnderTest.ios.hostAppRoot.childDirectory('Runner.xcworkspace').createSync(recursive: true); + projectUnderTest.macos.hostAppRoot.childDirectory('Runner.xcworkspace').createSync(recursive: true); + } projectUnderTest.dartTool.createSync(recursive: true); projectUnderTest.packagesFile.createSync(recursive: true); projectUnderTest.android.ephemeralDirectory.createSync(recursive: true); diff --git a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart index 3309015fe146..6326e439d7f8 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart @@ -208,6 +208,20 @@ keep this 2 expect(testLogger.statusText, isEmpty); }); + testWithoutContext('skipped if _xcodeWorkspaceSharedSettings is null', () { + final XcodeBuildSystemMigration iosProjectMigration = XcodeBuildSystemMigration( + project, + testLogger, + ); + project.xcodeWorkspaceSharedSettings = null; + + iosProjectMigration.migrate(); + expect(xcodeWorkspaceSharedSettings.existsSync(), isFalse); + + expect(testLogger.traceText, contains('Xcode workspace settings not found, skipping build system migration')); + expect(testLogger.statusText, isEmpty); + }); + testWithoutContext('skipped if nothing to upgrade', () { const String contents = ''' @@ -995,7 +1009,7 @@ class FakeIosProject extends Fake implements IosProject { File xcodeProjectWorkspaceData = MemoryFileSystem.test().file('xcodeProjectWorkspaceData'); @override - File xcodeWorkspaceSharedSettings = MemoryFileSystem.test().file('xcodeWorkspaceSharedSettings'); + File? xcodeWorkspaceSharedSettings = MemoryFileSystem.test().file('xcodeWorkspaceSharedSettings'); @override File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile'); diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 570db0843af4..485646360980 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -350,6 +350,19 @@ void main() { expect(versionInfo['build_number'],'3'); expect(versionInfo['package_name'],'test'); }); + _testInMemory('gets xcworkspace directory', () async { + final FlutterProject project = await someProject(); + project.ios.xcodeProject.createSync(); + project.ios.hostAppRoot.childFile('._Runner.xcworkspace').createSync(recursive: true); + project.ios.hostAppRoot.childFile('Runner.xcworkspace').createSync(recursive: true); + + expect(project.ios.xcodeWorkspace?.basename, 'Runner.xcworkspace'); + }); + _testInMemory('no xcworkspace directory found', () async { + final FlutterProject project = await someProject(); + project.ios.xcodeProject.createSync(); + expect(project.ios.xcodeWorkspace?.basename, null); + }); }); group('module status', () {