Skip to content

Commit ceacdd8

Browse files
Make Firebase audit staged umbrella imports configurable (#165)
1 parent 3f8c919 commit ceacdd8

4 files changed

Lines changed: 135 additions & 0 deletions

File tree

scripts/FirebaseBindingAudit.Tests/AuditRunnerTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,45 @@ public void TryResolveSharpieCompanionFrameworkPath_UsesLaterExistingImport()
169169
}
170170
}
171171

172+
[Fact]
173+
public void PrepareStagedXcframeworkForTarget_MaterializesSymlinkBeforeAddingUmbrellaImports()
174+
{
175+
var tempRoot = Path.Combine(Path.GetTempPath(), $"firebase-binding-audit-umbrella-{Guid.NewGuid():N}");
176+
177+
try
178+
{
179+
var sourceXcframeworkPath = Path.Combine(tempRoot, "externals", "FirebaseFunctions.xcframework");
180+
var sourceHeaderPath = Path.Combine(sourceXcframeworkPath, "ios-arm64", "FirebaseFunctions.framework", "Headers", "FirebaseFunctions-umbrella.h");
181+
Directory.CreateDirectory(Path.GetDirectoryName(sourceHeaderPath)!);
182+
File.WriteAllText(sourceHeaderPath, "FOUNDATION_EXPORT double FirebaseFunctionsVersionNumber;\n");
183+
184+
var stagedXcframeworkPath = Path.Combine(tempRoot, "frameworks", "FirebaseFunctions.xcframework");
185+
Directory.CreateDirectory(Path.GetDirectoryName(stagedXcframeworkPath)!);
186+
Directory.CreateSymbolicLink(stagedXcframeworkPath, sourceXcframeworkPath);
187+
188+
AuditRunner.PrepareStagedXcframeworkForTarget(
189+
new AuditTargetDefinition
190+
{
191+
Id = "CloudFunctions",
192+
Xcframework = "FirebaseFunctions",
193+
ObjcUmbrellaHeaderImports = ["FirebaseFunctions/FirebaseFunctions-Swift.h"]
194+
},
195+
stagedXcframeworkPath);
196+
197+
var stagedHeaderPath = Path.Combine(stagedXcframeworkPath, "ios-arm64", "FirebaseFunctions.framework", "Headers", "FirebaseFunctions-umbrella.h");
198+
Assert.Contains("#import <FirebaseFunctions/FirebaseFunctions-Swift.h>", File.ReadAllText(stagedHeaderPath));
199+
Assert.DoesNotContain("#import <FirebaseFunctions/FirebaseFunctions-Swift.h>", File.ReadAllText(sourceHeaderPath));
200+
Assert.False((new DirectoryInfo(stagedXcframeworkPath).Attributes & FileAttributes.ReparsePoint) != 0);
201+
}
202+
finally
203+
{
204+
if (Directory.Exists(tempRoot))
205+
{
206+
Directory.Delete(tempRoot, recursive: true);
207+
}
208+
}
209+
}
210+
172211
private static AuditOptions CreateAuditOptions(bool disableSharpie) =>
173212
new(
174213
RepoRoot: string.Empty,

scripts/FirebaseBindingAudit/AuditRunner.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ private async Task<TargetAuditResult> AuditTargetAsync(
221221

222222
try
223223
{
224+
PrepareStagedXcframeworkForTarget(target, stagedFrameworkPath);
224225
var projectFile = CreateSwiftBindingProject(projectDirectory, projectName, stagedFrameworkPath, options.GeneratorVersion);
225226
var buildResult = await BuildTargetProjectAsync(projectFile, projectDirectory, dotnetEnvironment, cancellationToken);
226227
WriteLogs(logsDirectory, $"{target.Id}-build", buildResult);
@@ -1208,6 +1209,96 @@ private static void StageXcframeworks(string repoRoot, string destinationDirecto
12081209
}
12091210
}
12101211

1212+
internal static void PrepareStagedXcframeworkForTarget(AuditTargetDefinition target, string stagedXcframeworkPath)
1213+
{
1214+
if (target.ObjcUmbrellaHeaderImports.Length == 0)
1215+
{
1216+
return;
1217+
}
1218+
1219+
MaterializeStagedDirectory(stagedXcframeworkPath);
1220+
AddObjcUmbrellaHeaderImports(stagedXcframeworkPath, target.ObjcUmbrellaHeaderImports);
1221+
}
1222+
1223+
private static void MaterializeStagedDirectory(string stagedDirectoryPath)
1224+
{
1225+
var directoryInfo = new DirectoryInfo(stagedDirectoryPath);
1226+
if (!directoryInfo.Exists || (directoryInfo.Attributes & FileAttributes.ReparsePoint) == 0)
1227+
{
1228+
return;
1229+
}
1230+
1231+
var linkTarget = directoryInfo.LinkTarget;
1232+
if (string.IsNullOrWhiteSpace(linkTarget))
1233+
{
1234+
throw new InvalidOperationException($"Unable to resolve symlink target for '{stagedDirectoryPath}'.");
1235+
}
1236+
1237+
var sourcePath = Path.IsPathRooted(linkTarget)
1238+
? linkTarget
1239+
: Path.GetFullPath(Path.Combine(directoryInfo.Parent?.FullName ?? Directory.GetCurrentDirectory(), linkTarget));
1240+
if (!Directory.Exists(sourcePath))
1241+
{
1242+
throw new InvalidOperationException($"Symlink target '{sourcePath}' does not exist for '{stagedDirectoryPath}'.");
1243+
}
1244+
1245+
Directory.Delete(stagedDirectoryPath);
1246+
CopyDirectory(sourcePath, stagedDirectoryPath);
1247+
}
1248+
1249+
private static void AddObjcUmbrellaHeaderImports(string xcframeworkPath, IReadOnlyList<string> imports)
1250+
{
1251+
var importLines = imports
1252+
.Select(NormalizeObjcImportLine)
1253+
.Distinct(StringComparer.Ordinal)
1254+
.ToList();
1255+
if (importLines.Count == 0)
1256+
{
1257+
return;
1258+
}
1259+
1260+
foreach (var umbrellaHeaderPath in Directory.EnumerateFiles(xcframeworkPath, "*-umbrella.h", SearchOption.AllDirectories))
1261+
{
1262+
var content = File.ReadAllText(umbrellaHeaderPath);
1263+
var additions = importLines
1264+
.Where(importLine => !content.Contains(importLine, StringComparison.Ordinal))
1265+
.ToList();
1266+
if (additions.Count == 0)
1267+
{
1268+
continue;
1269+
}
1270+
1271+
var builder = new StringBuilder(content);
1272+
if (builder.Length > 0 && builder[^1] != '\n')
1273+
{
1274+
builder.AppendLine();
1275+
}
1276+
1277+
foreach (var addition in additions)
1278+
{
1279+
builder.AppendLine(addition);
1280+
}
1281+
1282+
File.WriteAllText(umbrellaHeaderPath, builder.ToString());
1283+
}
1284+
}
1285+
1286+
private static string NormalizeObjcImportLine(string import)
1287+
{
1288+
var trimmedImport = import.Trim();
1289+
if (trimmedImport.StartsWith("#import ", StringComparison.Ordinal))
1290+
{
1291+
return trimmedImport;
1292+
}
1293+
1294+
if (trimmedImport.StartsWith("<", StringComparison.Ordinal) || trimmedImport.StartsWith("\"", StringComparison.Ordinal))
1295+
{
1296+
return $"#import {trimmedImport}";
1297+
}
1298+
1299+
return $"#import <{trimmedImport}>";
1300+
}
1301+
12111302
private static void StageSharpieFrameworkSlices(
12121303
string stagedFrameworksDirectory,
12131304
string destinationDirectory,

scripts/FirebaseBindingAudit/Configuration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ internal sealed class AuditTargetDefinition
3434

3535
public string[] HelperFiles { get; set; } = [];
3636

37+
public string[] ObjcUmbrellaHeaderImports { get; set; } = [];
38+
3739
public string? SharpieMode { get; set; }
3840

3941
public bool UseSharpieComparisonFallback { get; set; }

scripts/firebase-binding-audit.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@
118118
"helperFiles": [
119119
"Extension.cs"
120120
],
121+
"objcUmbrellaHeaderImports": [
122+
"FirebaseFunctions/FirebaseFunctions-Swift.h"
123+
],
121124
"sharpieMode": "auto"
122125
},
123126
{

0 commit comments

Comments
 (0)