Skip to content

Commit 769a148

Browse files
fix: ensure that bundle script is placed first for Apple build phases (#420)
1 parent 3edb907 commit 769a148

5 files changed

Lines changed: 223 additions & 29 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ test_generation/
2727
test_generation_dev/
2828

2929
# melos
30-
pubspec_overrides.yaml
30+
pubspec_overrides.yaml
31+
32+
context/

packages/flutterfire_cli/lib/src/firebase.dart

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Future<String?> getDefaultFirebaseProjectId() async {
5050
final fileContents = firebaseRcFile.readAsStringSync();
5151
try {
5252
final jsonMap =
53-
const JsonDecoder().convert(fileContents) as Map<String, dynamic>;
53+
const JsonDecoder().convert(fileContents) as Map<String, dynamic>;
5454
if (jsonMap['projects'] != null &&
5555
(jsonMap['projects'] as Map)['default'] != null) {
5656
return (jsonMap['projects'] as Map)['default'] as String;
@@ -67,11 +67,11 @@ Future<String?> getDefaultFirebaseProjectId() async {
6767
/// final result = await runFirebaseCommand(['projects:list']);
6868
/// print(result);
6969
Future<Map<String, dynamic>> runFirebaseCommand(
70-
List<String> commandAndArgs, {
71-
String? project,
72-
String? account,
73-
String? serviceAccount,
74-
}) async {
70+
List<String> commandAndArgs, {
71+
String? project,
72+
String? account,
73+
String? serviceAccount,
74+
}) async {
7575
final cliExists = await exists();
7676
if (!cliExists) {
7777
throw FirebaseCommandException(
@@ -116,7 +116,7 @@ Future<Map<String, dynamic>> runFirebaseCommand(
116116
if (jsonString.length > characterLimit) {
117117
// If the JSON string is large, write it to a temporary file
118118
final tempFile =
119-
File('${Directory.systemTemp.path}/firebase_output.json');
119+
File('${Directory.systemTemp.path}/firebase_output.json');
120120
await tempFile.writeAsString(jsonString);
121121

122122
// Read from the temporary file to create a Dart object
@@ -171,8 +171,8 @@ Future<List<FirebaseProject>> getProjects({
171171
return result
172172
.map<FirebaseProject>(
173173
(Map<String, dynamic> e) =>
174-
FirebaseProject.fromJson(Map<String, dynamic>.from(e)),
175-
)
174+
FirebaseProject.fromJson(Map<String, dynamic>.from(e)),
175+
)
176176
.where((project) => project.state == 'ACTIVE')
177177
.toList();
178178
} catch (e) {
@@ -231,8 +231,8 @@ Future<List<FirebaseApp>> getApps({
231231
return result
232232
.map<FirebaseApp>(
233233
(Map<String, dynamic> e) =>
234-
FirebaseApp.fromJson(Map<String, dynamic>.from(e)),
235-
)
234+
FirebaseApp.fromJson(Map<String, dynamic>.from(e)),
235+
)
236236
.toList();
237237
}
238238

@@ -300,7 +300,7 @@ Future<FirebaseApp> findOrCreateFirebaseApp({
300300

301301
_assertFirebaseSupportedPlatform(platformFirebase);
302302
final fetchingAppsSpinner = spinner(
303-
(done) {
303+
(done) {
304304
final loggingAppName =
305305
packageNameOrBundleIdentifier ?? webAppId ?? displayNameWithPlatform;
306306
if (!done) {
@@ -333,7 +333,7 @@ Future<FirebaseApp> findOrCreateFirebaseApp({
333333
final flagOption = platform == kWeb ? kWebAppIdFlag : kWindowsAppIdFlag;
334334
// Find provided web app id for web and windows, otherwise, throw Exception that it doesn't exist
335335
final webApp = unfilteredFirebaseApps.firstWhere(
336-
(firebaseApp) => firebaseApp.appId == webAppId,
336+
(firebaseApp) => firebaseApp.appId == webAppId,
337337
orElse: () {
338338
fetchingAppsSpinner.done();
339339
throw Exception(
@@ -347,7 +347,7 @@ Future<FirebaseApp> findOrCreateFirebaseApp({
347347
}
348348
// Find web app for web and windows using display name with this signature: "flutter_app_name (platform)
349349
filteredFirebaseApps = unfilteredFirebaseApps.where(
350-
(firebaseApp) {
350+
(firebaseApp) {
351351
if (firebaseApp.displayName == displayNameWithPlatform) {
352352
return true;
353353
}
@@ -357,17 +357,17 @@ Future<FirebaseApp> findOrCreateFirebaseApp({
357357
// Find any for that platform if no web app found with display name
358358
if (filteredFirebaseApps.isEmpty) {
359359
filteredFirebaseApps = unfilteredFirebaseApps.where(
360-
(firebaseApp) {
360+
(firebaseApp) {
361361
return firebaseApp.platform == platform;
362362
},
363363
);
364364
}
365365
} else {
366366
filteredFirebaseApps = unfilteredFirebaseApps.where(
367-
(firebaseApp) {
367+
(firebaseApp) {
368368
if (packageNameOrBundleIdentifier != null) {
369369
return firebaseApp.packageNameOrBundleIdentifier ==
370-
packageNameOrBundleIdentifier &&
370+
packageNameOrBundleIdentifier &&
371371
firebaseApp.platform == platformFirebase;
372372
}
373373
return false;
@@ -405,7 +405,7 @@ Future<FirebaseApp> findOrCreateFirebaseApp({
405405
);
406406
break;
407407
case kWeb:
408-
// This is used to also create windows app, Firebase has no concept of a windows app
408+
// This is used to also create windows app, Firebase has no concept of a windows app
409409
createFirebaseAppFuture = createWebApp(
410410
project: project,
411411
displayName: displayNameWithPlatform,
@@ -419,7 +419,7 @@ Future<FirebaseApp> findOrCreateFirebaseApp({
419419
}
420420

421421
final creatingAppSpinner = spinner(
422-
(done) {
422+
(done) {
423423
if (!done) {
424424
return AnsiStyles.bold(
425425
'Registering new Firebase ${AnsiStyles.cyan(platform)} app on Firebase project ${AnsiStyles.cyan(project)}.',
@@ -519,7 +519,7 @@ Future<String> getAccessToken() async {
519519
: Platform.environment['HOME']!;
520520
// Path to 'firebase-tools.json'
521521
final configPath =
522-
path.join(homeDir, '.config', 'configstore', 'firebase-tools.json');
522+
path.join(homeDir, '.config', 'configstore', 'firebase-tools.json');
523523
final configFile = File(configPath);
524524
if (!configFile.existsSync()) {
525525
throw Exception(
@@ -536,7 +536,7 @@ Future<String> getAccessToken() async {
536536
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
537537
// Values for obtaining the access token are taken from the Firebase CLI source code: https://github.com/firebase/firebase-tools/blob/b14b5f38fe23da6543778a588811b0e2391427c0/src/api.ts#L18
538538
body:
539-
'grant_type=refresh_token&client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&client_secret=j9iVZfS8kkCEFUPaAeJV0sAi&refresh_token=$refreshToken',
539+
'grant_type=refresh_token&client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&client_secret=j9iVZfS8kkCEFUPaAeJV0sAi&refresh_token=$refreshToken',
540540
);
541541

542542
if (response.statusCode == 200) {
@@ -551,19 +551,19 @@ Future<String> getAccessToken() async {
551551

552552
// Return string value of "GoogleService-Info.plist" or "google-services.json" file for relevant platform
553553
Future<String> getServiceFileContent(
554-
String projectId,
555-
String appId,
556-
String accessToken,
557-
String platform,
558-
) async {
554+
String projectId,
555+
String appId,
556+
String accessToken,
557+
String platform,
558+
) async {
559559
String? uri;
560560

561561
if (platform == kIos || platform == kMacos) {
562562
uri =
563-
'https://firebase.googleapis.com/v1beta1/projects/$projectId/iosApps/$appId/config';
563+
'https://firebase.googleapis.com/v1beta1/projects/$projectId/iosApps/$appId/config';
564564
} else if (platform == kAndroid) {
565565
uri =
566-
'https://firebase.googleapis.com/v1beta1/projects/$projectId/androidApps/$appId/config';
566+
'https://firebase.googleapis.com/v1beta1/projects/$projectId/androidApps/$appId/config';
567567
} else {
568568
throw ServiceFileException(
569569
platform,

packages/flutterfire_cli/lib/src/firebase/firebase_apple_writes.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ String _debugSymbolsScript(
390390
require 'xcodeproj'
391391
xcodeFile='${getXcodeProjectPath(platform)}'
392392
runScriptName='$debugSymbolScriptName'
393+
bundleScriptName='$bundleServiceScriptName'
393394
project = Xcodeproj::Project.open(xcodeFile)
394395
395396
@@ -410,21 +411,74 @@ fi
410411
${isDevDependency ? 'dart run flutterfire_cli:flutterfire' : 'flutterfire'} upload-crashlytics-symbols --upload-symbols-script-path="\$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT" --platform=$platform --apple-project-path="\${SRCROOT}" --env-platform-name="\${PLATFORM_NAME}" --env-configuration="\${CONFIGURATION}" --env-project-dir="\${PROJECT_DIR}" --env-built-products-dir="\${BUILT_PRODUCTS_DIR}" --env-dwarf-dsym-folder-path="\${DWARF_DSYM_FOLDER_PATH}" --env-dwarf-dsym-file-name="\${DWARF_DSYM_FILE_NAME}" --env-infoplist-path="\${INFOPLIST_PATH}" $projectType
411412
)
412413
414+
def ensure_phase_is_after(target, phase, preceding_phase)
415+
return if preceding_phase.nil?
416+
417+
allPhases = target.build_phases
418+
precedingIndex = allPhases.index(preceding_phase)
419+
phaseIndex = allPhases.index(phase)
420+
421+
return if precedingIndex.nil? || phaseIndex.nil?
422+
return if phaseIndex == precedingIndex + 1
423+
424+
target.build_phases.delete(phase)
425+
426+
insertionIndex =
427+
if phaseIndex < precedingIndex
428+
precedingIndex
429+
else
430+
precedingIndex + 1
431+
end
432+
433+
target.build_phases.insert(insertionIndex, phase)
434+
end
435+
413436
for target in project.targets
414437
if (target.name == '$target')
438+
# Find existing debug symbols phase
415439
phase = target.shell_script_build_phases().find do |item|
416440
if defined? item && item.name
417441
item.name == runScriptName
418442
end
419443
end
444+
445+
# Find bundle-service-file phase to determine insertion position
446+
bundlePhase = target.shell_script_build_phases().find do |item|
447+
if defined? item && item.name
448+
item.name == bundleScriptName
449+
end
450+
end
420451
421452
if phase.nil?
453+
# Create new phase
422454
phase = target.new_shell_script_build_phase(runScriptName)
423455
phase.shell_script = bashScript
456+
457+
# If bundle-service-file exists, ensure debug symbols is placed right after it
458+
if (!bundlePhase.nil?)
459+
ensure_phase_is_after(target, phase, bundlePhase)
460+
end
461+
424462
project.save()
425463
elsif phase.shell_script != bashScript
464+
# Update existing phase
426465
phase.shell_script = bashScript
466+
467+
# Ensure correct ordering: debug symbols should be right after bundle-service-file
468+
if (!bundlePhase.nil?)
469+
ensure_phase_is_after(target, phase, bundlePhase)
470+
end
471+
427472
project.save()
473+
else
474+
# Script exists and content is correct, but check ordering
475+
if (!bundlePhase.nil?)
476+
currentOrdering = target.build_phases.dup
477+
ensure_phase_is_after(target, phase, bundlePhase)
478+
if (target.build_phases != currentOrdering)
479+
project.save()
480+
end
481+
end
428482
end
429483
end
430484
end

packages/flutterfire_cli/test/configure_test.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,85 @@ void main() {
338338
),
339339
);
340340

341+
test(
342+
'flutterfire configure: build configuration - verify script ordering after second configure',
343+
() async {
344+
// Add crashlytics dependency so debug symbols script gets added
345+
final addCrashlyticsResult = Process.runSync(
346+
'flutter',
347+
['pub', 'add', 'firebase_crashlytics'],
348+
workingDirectory: projectPath,
349+
);
350+
351+
if (addCrashlyticsResult.exitCode != 0) {
352+
fail(addCrashlyticsResult.stderr as String);
353+
}
354+
355+
// First configure run
356+
final result = Process.runSync(
357+
'flutterfire',
358+
[
359+
'configure',
360+
'--yes',
361+
'--project=$firebaseProjectId',
362+
'--platforms=macos',
363+
'--macos-out=macos/$buildType',
364+
'--macos-build-config=$appleBuildConfiguration',
365+
],
366+
workingDirectory: projectPath,
367+
runInShell: true,
368+
);
369+
370+
if (result.exitCode != 0) {
371+
fail(result.stderr as String);
372+
}
373+
374+
// Second configure run - this should trigger reordering if needed
375+
final result2 = Process.runSync(
376+
'flutterfire',
377+
[
378+
'configure',
379+
'--yes',
380+
'--project=$firebaseProjectId',
381+
'--platforms=macos',
382+
'--macos-out=macos/$buildType',
383+
'--macos-build-config=$appleBuildConfiguration',
384+
],
385+
workingDirectory: projectPath,
386+
runInShell: true,
387+
);
388+
389+
if (result2.exitCode != 0) {
390+
fail(result2.stderr as String);
391+
}
392+
393+
// Verify script ordering for macOS
394+
final scriptOrderCheckMacos = rubyScriptForCheckingScriptOrdering(
395+
projectPath!,
396+
kMacos,
397+
);
398+
399+
final macosOrderResult = Process.runSync(
400+
'ruby',
401+
[
402+
'-e',
403+
scriptOrderCheckMacos,
404+
],
405+
runInShell: true,
406+
);
407+
408+
if (macosOrderResult.exitCode != 0) {
409+
fail(macosOrderResult.stderr as String);
410+
}
411+
412+
expect(macosOrderResult.stdout, 'success');
413+
},
414+
skip: !Platform.isMacOS,
415+
timeout: const Timeout(
416+
Duration(minutes: 2),
417+
),
418+
);
419+
341420
test(
342421
'flutterfire configure: android - "default" Apple - "target"',
343422
() async {

0 commit comments

Comments
 (0)