1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:xml/xml.dart' ; |
6 | import 'package:xml/xpath.dart' ; |
7 | |
8 | import '../base/file_system.dart'; |
9 | import '../base/project_migrator.dart'; |
10 | import '../build_info.dart'; |
11 | import '../ios/xcodeproj.dart'; |
12 | import '../project.dart'; |
13 | |
14 | class LLDBInitMigration extends ProjectMigrator { |
15 | LLDBInitMigration( |
16 | IosProject project, |
17 | BuildInfo buildInfo, |
18 | super.logger, { |
19 | required FileSystem fileSystem, |
20 | required EnvironmentType environmentType, |
21 | String? deviceID, |
22 | }) : _xcodeProject = project, |
23 | _buildInfo = buildInfo, |
24 | _xcodeProjectInfoFile = project.xcodeProjectInfoFile, |
25 | _fileSystem = fileSystem, |
26 | _environmentType = environmentType, |
27 | _deviceID = deviceID; |
28 | |
29 | final IosProject _xcodeProject; |
30 | final BuildInfo _buildInfo; |
31 | final FileSystem _fileSystem; |
32 | final File _xcodeProjectInfoFile; |
33 | final EnvironmentType _environmentType; |
34 | final String? _deviceID; |
35 | |
36 | String get _initPath => |
37 | _xcodeProject.lldbInitFile.path.replaceFirst(_xcodeProject.hostAppRoot.path, r'$(SRCROOT)' ); |
38 | |
39 | static const _launchActionIdentifier = 'LaunchAction' ; |
40 | static const _testActionIdentifier = 'TestAction' ; |
41 | |
42 | @override |
43 | Future<void> migrate() async { |
44 | SchemeInfo? schemeInfo; |
45 | try { |
46 | if (!_xcodeProjectInfoFile.existsSync()) { |
47 | throw Exception('Xcode project not found.' ); |
48 | } |
49 | |
50 | schemeInfo = await _getSchemeInfo(); |
51 | |
52 | final bool isSchemeMigrated = await _isSchemeMigrated(schemeInfo); |
53 | if (isSchemeMigrated) { |
54 | return; |
55 | } |
56 | _migrateScheme(schemeInfo); |
57 | } on Exception catch (e) { |
58 | logger.printError( |
59 | 'An error occurred when adding LLDB Init File:\n' |
60 | ' $e' , |
61 | ); |
62 | } |
63 | } |
64 | |
65 | Future<SchemeInfo> _getSchemeInfo() async { |
66 | final XcodeProjectInfo? projectInfo = await _xcodeProject.projectInfo(); |
67 | if (projectInfo == null) { |
68 | throw Exception('Unable to get Xcode project info.' ); |
69 | } |
70 | if (_xcodeProject.xcodeWorkspace == null) { |
71 | throw Exception('Xcode workspace not found.' ); |
72 | } |
73 | final String? scheme = projectInfo.schemeFor(_buildInfo); |
74 | if (scheme == null) { |
75 | projectInfo.reportFlavorNotFoundAndExit(); |
76 | } |
77 | |
78 | final File schemeFile = _xcodeProject.xcodeProjectSchemeFile(scheme: scheme); |
79 | if (!schemeFile.existsSync()) { |
80 | throw Exception('Unable to get scheme file for $scheme.' ); |
81 | } |
82 | |
83 | final String schemeContent = schemeFile.readAsStringSync(); |
84 | return SchemeInfo(schemeName: scheme, schemeFile: schemeFile, schemeContent: schemeContent); |
85 | } |
86 | |
87 | Future<bool> _isSchemeMigrated(SchemeInfo schemeInfo) async { |
88 | final String? lldbInitFileLaunchPath; |
89 | final String? lldbInitFileTestPath; |
90 | try { |
91 | // Check that both the LaunchAction and TestAction have the customLLDBInitFile set to flutter_lldbinit. |
92 | final document = XmlDocument.parse(schemeInfo.schemeContent); |
93 | |
94 | lldbInitFileLaunchPath = _parseLLDBInitFileFromScheme( |
95 | action: _launchActionIdentifier, |
96 | document: document, |
97 | schemeFile: schemeInfo.schemeFile, |
98 | ); |
99 | lldbInitFileTestPath = _parseLLDBInitFileFromScheme( |
100 | action: _testActionIdentifier, |
101 | document: document, |
102 | schemeFile: schemeInfo.schemeFile, |
103 | ); |
104 | final bool launchActionMigrated = |
105 | lldbInitFileLaunchPath != null && lldbInitFileLaunchPath.contains(_initPath); |
106 | final bool testActionMigrated = |
107 | lldbInitFileTestPath != null && lldbInitFileTestPath.contains(_initPath); |
108 | |
109 | if (launchActionMigrated && testActionMigrated) { |
110 | return true; |
111 | } else if (launchActionMigrated && !testActionMigrated) { |
112 | // If LaunchAction has it set, but TestAction doesn't, give an error |
113 | // with instructions to add it to the TestAction. |
114 | throw _missingActionException('Test' , schemeInfo.schemeName); |
115 | } else if (testActionMigrated && !launchActionMigrated) { |
116 | // If TestAction has it set, but LaunchAction doesn't, give an error |
117 | // with instructions to add it to the LaunchAction. |
118 | throw _missingActionException('Launch' , schemeInfo.schemeName); |
119 | } |
120 | } on XmlException catch (exception) { |
121 | throw Exception( |
122 | 'Failed to parse ${schemeInfo.schemeFile.basename}: Invalid xml: ${schemeInfo.schemeContent}\n $exception' , |
123 | ); |
124 | } |
125 | |
126 | // If the scheme is using a LLDB Init File that is not flutter_lldbinit, |
127 | // attempt to read the file and check if it's importing flutter_lldbinit. |
128 | // If the file name contains a variable, attempt to substitute the variable |
129 | // using the build settings. If it fails to find the file or fails to |
130 | // detect it's using flutter_lldbinit, print a warning to either remove |
131 | // their LLDB Init file or append flutter_lldbinit to their existing one. |
132 | if (schemeInfo.schemeContent.contains('customLLDBInitFile' )) { |
133 | try { |
134 | Map<String, String>? buildSettings; |
135 | if ((lldbInitFileLaunchPath != null && lldbInitFileLaunchPath.contains(r'$' )) || |
136 | (lldbInitFileTestPath != null && lldbInitFileTestPath.contains(r'$' ))) { |
137 | buildSettings = |
138 | await _xcodeProject.buildSettingsForBuildInfo( |
139 | _buildInfo, |
140 | environmentType: _environmentType, |
141 | deviceId: _deviceID, |
142 | ) ?? |
143 | <String, String>{}; |
144 | } |
145 | |
146 | final File? lldbInitFileLaunchFile = _resolveLLDBInitFile( |
147 | lldbInitFileLaunchPath, |
148 | buildSettings, |
149 | ); |
150 | final File? lldbInitFileTestFile = _resolveLLDBInitFile( |
151 | lldbInitFileTestPath, |
152 | buildSettings, |
153 | ); |
154 | |
155 | if (lldbInitFileLaunchFile != null && |
156 | lldbInitFileLaunchFile.existsSync() && |
157 | lldbInitFileLaunchFile.readAsStringSync().contains( |
158 | _xcodeProject.lldbInitFile.basename, |
159 | ) && |
160 | lldbInitFileTestFile != null && |
161 | lldbInitFileTestFile.existsSync() && |
162 | lldbInitFileTestFile.readAsStringSync().contains(_xcodeProject.lldbInitFile.basename)) { |
163 | return true; |
164 | } |
165 | } on XmlException catch (exception) { |
166 | throw Exception( |
167 | 'Failed to parse ${schemeInfo.schemeFile.basename}: Invalid xml: ${schemeInfo.schemeContent}\n $exception' , |
168 | ); |
169 | } |
170 | |
171 | throw Exception( |
172 | 'Running Flutter in debug mode on new iOS versions requires a LLDB ' |
173 | 'Init File, but the scheme already has one set. To ensure debug ' |
174 | 'mode works, please complete one of the following:\n' |
175 | ' * Open Xcode > Product > Scheme > Edit Scheme and remove LLDB Init ' |
176 | 'File for both the Run and Test actions.\n' |
177 | ' * Append the following to your custom LLDB Init File:\n\n' |
178 | ' command source ${_xcodeProject.lldbInitFile.absolute.path}\n' , |
179 | ); |
180 | } |
181 | return false; |
182 | } |
183 | |
184 | /// Add customLLDBInitFile and set to [_initPath] for both LaunchAction and TestAction. |
185 | void _migrateScheme(SchemeInfo schemeInfo) { |
186 | final File schemeFile = schemeInfo.schemeFile; |
187 | final String schemeContent = schemeInfo.schemeContent; |
188 | |
189 | final String newScheme = schemeContent.replaceAll( |
190 | 'selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"' , |
191 | ''' |
192 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
193 | customLLDBInitFile = " $_initPath"''' , |
194 | ); |
195 | try { |
196 | final document = XmlDocument.parse(newScheme); |
197 | _validateSchemeAction( |
198 | action: _launchActionIdentifier, |
199 | document: document, |
200 | schemeFile: schemeFile, |
201 | ); |
202 | _validateSchemeAction( |
203 | action: _testActionIdentifier, |
204 | document: document, |
205 | schemeFile: schemeFile, |
206 | ); |
207 | } on XmlException catch (exception) { |
208 | throw Exception( |
209 | 'Failed to parse ${schemeFile.basename}: Invalid xml: $newScheme\n $exception' , |
210 | ); |
211 | } |
212 | schemeFile.writeAsStringSync(newScheme); |
213 | } |
214 | |
215 | /// Parse the customLLDBInitFile from the XML for the [action] and validate |
216 | /// it contains [_initPath]. |
217 | void _validateSchemeAction({ |
218 | required String action, |
219 | required XmlDocument document, |
220 | required File schemeFile, |
221 | }) { |
222 | final String? lldbInitFile = _parseLLDBInitFileFromScheme( |
223 | action: action, |
224 | document: document, |
225 | schemeFile: schemeFile, |
226 | ); |
227 | if (lldbInitFile == null || !lldbInitFile.contains(_initPath)) { |
228 | throw Exception( |
229 | 'Failed to find correct customLLDBInitFile in $action for the Scheme in ${schemeFile.path}.' , |
230 | ); |
231 | } |
232 | } |
233 | |
234 | /// Parse the customLLDBInitFile from the XML for the [action]. |
235 | String? _parseLLDBInitFileFromScheme({ |
236 | required String action, |
237 | required XmlDocument document, |
238 | required File schemeFile, |
239 | }) { |
240 | final Iterable<XmlNode> nodes = document.xpath('/Scheme/ $action' ); |
241 | if (nodes.isEmpty) { |
242 | throw Exception('Failed to find $action for the Scheme in ${schemeFile.path}.' ); |
243 | } |
244 | final XmlNode actionNode = nodes.first; |
245 | final XmlAttribute? lldbInitFile = actionNode.attributes |
246 | .where((XmlAttribute attribute) => attribute.localName == 'customLLDBInitFile' ) |
247 | .firstOrNull; |
248 | return lldbInitFile?.value; |
249 | } |
250 | |
251 | /// Replace any Xcode variables in [lldbInitFilePath] from [buildSettings]. |
252 | File? _resolveLLDBInitFile(String? lldbInitFilePath, Map<String, String>? buildSettings) { |
253 | if (lldbInitFilePath == null) { |
254 | return null; |
255 | } |
256 | if (lldbInitFilePath.contains(r'$' ) && buildSettings != null) { |
257 | // If the path to the LLDB Init File contains a $, it may contain a |
258 | // variable from build settings. |
259 | final String resolvedInitFilePath = substituteXcodeVariables(lldbInitFilePath, buildSettings); |
260 | return _fileSystem.file(resolvedInitFilePath); |
261 | } |
262 | return _fileSystem.file(lldbInitFilePath); |
263 | } |
264 | |
265 | Exception _missingActionException(String missingAction, String schemeName) { |
266 | return Exception( |
267 | 'Running Flutter in debug mode on new iOS versions requires a LLDB ' |
268 | 'Init File, but the $missingAction action in the $schemeName scheme ' |
269 | 'does not have it set. To ensure debug mode works, please complete ' |
270 | 'the following:\n' |
271 | ' * Open Xcode > Product > Scheme > Edit Scheme and for the ' |
272 | ' $missingAction action, set LLDB Init File to:\n\n' |
273 | ' $_initPath\n' , |
274 | ); |
275 | } |
276 | } |
277 | |
278 | class SchemeInfo { |
279 | SchemeInfo({required this.schemeName, required this.schemeFile, required this.schemeContent}); |
280 | |
281 | final String schemeName; |
282 | final File schemeFile; |
283 | final String schemeContent; |
284 | } |
285 | |