Skip to content

Commit 8289802

Browse files
authored
Validate embedded frameworks (swiftlang#324)
Validate embedded frameworks as part of the `ValidateProductTaskAction` to surface issues which would later prevent installation or App Store submission. rdar://124355004
1 parent 15e00e3 commit 8289802

File tree

9 files changed

+231
-11
lines changed

9 files changed

+231
-11
lines changed

Sources/SWBCore/Settings/BuiltinMacros.swift

+2
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ public final class BuiltinMacros {
246246
public static let SEPARATE_SYMBOL_EDIT = BuiltinMacros.declareBooleanMacro("SEPARATE_SYMBOL_EDIT")
247247
public static let SKIP_INSTALL = BuiltinMacros.declareBooleanMacro("SKIP_INSTALL")
248248
public static let SKIP_CLANG_STATIC_ANALYZER = BuiltinMacros.declareBooleanMacro("SKIP_CLANG_STATIC_ANALYZER")
249+
public static let SKIP_EMBEDDED_FRAMEWORKS_VALIDATION = BuiltinMacros.declareBooleanMacro("SKIP_EMBEDDED_FRAMEWORKS_VALIDATION")
249250
public static let STRINGSDATA_DIR = BuiltinMacros.declarePathMacro("STRINGSDATA_DIR")
250251
public static let STRIP_BITCODE_FROM_COPIED_FILES = BuiltinMacros.declareBooleanMacro("STRIP_BITCODE_FROM_COPIED_FILES")
251252
public static let STRIP_INSTALLED_PRODUCT = BuiltinMacros.declareBooleanMacro("STRIP_INSTALLED_PRODUCT")
@@ -2113,6 +2114,7 @@ public final class BuiltinMacros {
21132114
SHARED_SUPPORT_FOLDER_PATH,
21142115
SKIP_INSTALL,
21152116
SKIP_CLANG_STATIC_ANALYZER,
2117+
SKIP_EMBEDDED_FRAMEWORKS_VALIDATION,
21162118
SOURCE_ROOT,
21172119
SPECIALIZATION_SDK_OPTIONS,
21182120
SRCROOT,

Sources/SWBTaskExecution/TaskActions/ValidateProductTaskAction.swift

+102-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ public final class ValidateProductTaskAction: TaskAction {
3434
let storeIssueTreatment: StoreIssueTreatment
3535
let validateForStore: Bool
3636
let validatingArchive: Bool
37+
let validateEmbeddedFrameworks: Bool
3738
let applicationPath: Path
3839
let infoplistSubpath: String
40+
let isShallowBundle: Bool
3941

4042
@_spi(Testing) public init?(_ commandLine: AnySequence<String>, _ outputDelegate: any TaskOutputDelegate) {
4143
let verbose = false
@@ -45,6 +47,8 @@ public final class ValidateProductTaskAction: TaskAction {
4547
var applicationPath: Path! = nil
4648
var infoplistSubpath: String! = nil
4749
var validateExtension = true
50+
var validateEmbeddedFrameworks = true
51+
var isShallowBundle = false
4852

4953
var hadErrors = false
5054
func error(_ message: String) {
@@ -79,7 +83,13 @@ public final class ValidateProductTaskAction: TaskAction {
7983

8084
// The default is yes, so we only need an opt-out for now. This also prevents us from having to pass in `-validate-extension` everywhere, even though that is the default behavior.
8185
case "-no-validate-extension":
82-
validateExtension = false;
86+
validateExtension = false
87+
88+
case "-no-validate-embedded-frameworks":
89+
validateEmbeddedFrameworks = false
90+
91+
case "-shallow-bundle":
92+
isShallowBundle = true
8393

8494
case "-infoplist-subpath":
8595
guard let path = generator.next() else {
@@ -133,8 +143,10 @@ public final class ValidateProductTaskAction: TaskAction {
133143
self.storeIssueTreatment = storeIssueTreatment
134144
self.validateForStore = validateForStore
135145
self.validatingArchive = validatingArchive
146+
self.validateEmbeddedFrameworks = validateEmbeddedFrameworks
136147
self.applicationPath = applicationPath
137148
self.infoplistSubpath = infoplistSubpath
149+
self.isShallowBundle = isShallowBundle
138150
}
139151
}
140152

@@ -174,14 +186,102 @@ public final class ValidateProductTaskAction: TaskAction {
174186
// Validate the iPad multitasking/splitview support in the Info.plist. This never causes the tool to fail, but it may emit warnings.
175187
validateiPadMultiTaskingSplitViewSupport(infoPlist, outputDelegate: outputDelegate)
176188

177-
// Validate that this is actually an App Store category.
178189
if let configuredTarget = task.forTarget, let platform = configuredTarget.parameters.activeRunDestination?.platform {
190+
// Validate that this is actually an App Store category.
179191
validateAppStoreCategory(infoPlist, platform: platform, targetName: configuredTarget.target.name, schemeCommand: executionDelegate.schemeCommand, options: options, outputDelegate: outputDelegate)
192+
193+
// Validate compliance of embedded frameworks w.r.t. installation and App Store submission.
194+
if options.validateEmbeddedFrameworks {
195+
do {
196+
let success = try validateFrameworks(at: applicationPath, isShallow: options.isShallowBundle, outputDelegate: outputDelegate)
197+
return success ? .succeeded : .failed
198+
} catch {
199+
outputDelegate.emitError("Unexpected error while validating embedded frameworks: \(error.localizedDescription)")
200+
return .failed
201+
}
202+
}
180203
}
181204

182205
return .succeeded
183206
}
184207

208+
private static let deepBundleInfoPlistSubpath = "Versions/Current/Resources/Info.plist"
209+
private static let shallowBundleInfoPlistSubpath = "Info.plist"
210+
211+
@_spi(Testing) public func validateFrameworks(at applicationPath: Path, isShallow: Bool, outputDelegate: any TaskOutputDelegate, fs: any FSProxy = localFS) throws -> Bool {
212+
let frameworksSubpath = isShallow ? "Frameworks" : "Contents/Frameworks"
213+
let infoPlistSubpath = isShallow ? Self.shallowBundleInfoPlistSubpath : Self.deepBundleInfoPlistSubpath
214+
215+
let frameworksPath = applicationPath.join(frameworksSubpath)
216+
guard fs.exists(frameworksPath) else {
217+
return true
218+
}
219+
220+
var frameworkPaths = [Path]()
221+
try fs.traverse(frameworksPath) {
222+
if $0.fileExtension == "framework" {
223+
frameworkPaths.append($0)
224+
}
225+
}
226+
227+
var hasErrors = false
228+
for frameworkPath in frameworkPaths {
229+
let infoPlistPath = frameworkPath.join(infoPlistSubpath)
230+
guard fs.exists(infoPlistPath) else {
231+
// It seems reasonably common that developers mix up the formats, so we check that to be more helpful.
232+
if isShallow, fs.exists(frameworkPath.join(Self.deepBundleInfoPlistSubpath)) {
233+
outputDelegate.emitError("Framework \(frameworkPath.str) contains \(Self.deepBundleInfoPlistSubpath), expected \(Self.shallowBundleInfoPlistSubpath) at the root level since the platform uses shallow bundles")
234+
} else if !isShallow, fs.exists(frameworkPath.join(Self.shallowBundleInfoPlistSubpath)) {
235+
outputDelegate.emitError("Framework \(frameworkPath.str) contains \(Self.shallowBundleInfoPlistSubpath), expected \(Self.deepBundleInfoPlistSubpath) since the platform does not use shallow bundles")
236+
} else {
237+
outputDelegate.emitError("Framework \(frameworkPath.str) did not contain an Info.plist")
238+
}
239+
hasErrors = true
240+
continue
241+
}
242+
243+
let infoPlistItem: PropertyListItem
244+
do {
245+
infoPlistItem = try PropertyList.fromPath(infoPlistPath, fs: fs)
246+
}
247+
catch let error {
248+
outputDelegate.emitError("Failed to read Info.plist of framework \(frameworkPath.str): \(error)")
249+
hasErrors = true
250+
continue
251+
}
252+
guard case .plDict(let infoPlist) = infoPlistItem else {
253+
outputDelegate.emitError("Failed to read Info.plist of framework \(frameworkPath.str): Contents of file are not a dictionary")
254+
hasErrors = true
255+
continue
256+
}
257+
258+
// We do a more thorough validation for embedded platforms since installation will otherwise be prevented with more confusing diagnostics.
259+
if !isShallow {
260+
continue
261+
}
262+
263+
// This is being special cased since it appears we always get a dictionary even for invalid plists.
264+
if infoPlist.isEmpty {
265+
outputDelegate.emitError("Info.plist of framework \(frameworkPath.str) was empty")
266+
hasErrors = true
267+
continue
268+
}
269+
270+
guard let rawBundleIdentifier = infoPlist["CFBundleIdentifier"], case .plString(let bundleIdentifier) = rawBundleIdentifier, !bundleIdentifier.isEmpty else {
271+
outputDelegate.emitError("Framework \(frameworkPath.str) did not have a CFBundleIdentifier in its Info.plist")
272+
hasErrors = true
273+
continue
274+
}
275+
276+
if bundleIdentifier != bundleIdentifier.asLegalBundleIdentifier {
277+
outputDelegate.emitError("Framework \(frameworkPath.str) had an invalid CFBundleIdentifier in its Info.plist: \(bundleIdentifier)")
278+
hasErrors = true
279+
}
280+
}
281+
282+
return !hasErrors
283+
}
284+
185285
@_spi(Testing) @discardableResult public func validateiPadMultiTaskingSplitViewSupport(_ infoPlist: [String: PropertyListItem], outputDelegate: any TaskOutputDelegate) -> Bool {
186286
// Note that there is a lot of type validation which *could* be performed here (e.g., emitting an error if some value is not the expected type), but I've only copied the validation being performed in XCWorkQueueCommandBuiltinInvocation_validationUtility, from which this was ported.
187287

Sources/SWBUniversalPlatform/Specs/ProductTypeValidationTool.xcspec

+12
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@
2828
DefaultValue = "$(VALIDATE_PRODUCT)";
2929
CommandLineFlag = "-validate-for-store";
3030
},
31+
{
32+
Name = ShallowBundle;
33+
Type = boolean;
34+
DefaultValue = "$(SHALLOW_BUNDLE)";
35+
CommandLineFlag = "-shallow-bundle";
36+
},
37+
{
38+
Name = SkipEmbeddedFrameworksValidation;
39+
Type = boolean;
40+
DefaultValue = "$(SKIP_EMBEDDED_FRAMEWORKS_VALIDATION)";
41+
CommandLineFlag = "-no-validate-embedded-frameworks";
42+
},
3143
{
3244
Name = __no_validate_extension__;
3345
Type = boolean;

Tests/SWBBuildSystemTests/BuildOperationTests.swift

+4
Original file line numberDiff line numberDiff line change
@@ -2958,6 +2958,9 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
29582958
try await tester.fs.writeFramework(sourceDynamicFrameworkPath, archs: runDestination.platform == "macosx" ? ["arm64", "x86_64"] : ["arm64"], platform: #require(runDestination.buildVersionPlatform(core)), infoLookup: core, static: false) { _, _, headersDir, resourcesDir in
29592959
try await tester.fs.writeFileContents(headersDir.join("ADynamicFwk.h")) { $0 <<< "" }
29602960
try await tester.fs.writePlist(resourcesDir.join("ADynamicResource.plist"), .plDict([:]))
2961+
try await tester.fs.writePlist(resourcesDir.join("Info.plist"), .plDict([
2962+
"CFBundleIdentifier": "com.apple.ADynamicFwk",
2963+
]))
29612964
}
29622965

29632966
let sourceDynamicFrameworkFiles = try tester.fs.traverse(sourceDynamicFrameworkPath, { $0.relativeSubpath(from: sourceDynamicFrameworkPath) }).sorted()
@@ -3006,6 +3009,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
30063009
"LSMinimumSystemVersion": "11.0",
30073010
"MinimumOSVersion": "11.0",
30083011
"CFBundleSupportedPlatforms": bundleSupportedPlatforms,
3012+
"CFBundleIdentifier": "com.apple.AStaticFwk",
30093013
]))
30103014
}
30113015

Tests/SWBBuildSystemTests/MergeableLibrariesBuildOperationTests.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ fileprivate struct MergeableLibrariesBuildOperationTests: CoreBasedTests {
8383
TestBuildConfiguration("Debug",
8484
buildSettings: [
8585
"INSTALL_PATH": "/Applications",
86+
"SKIP_EMBEDDED_FRAMEWORKS_VALIDATION": "YES",
8687
]),
8788
],
8889
buildPhases: [
@@ -553,8 +554,8 @@ fileprivate struct MergeableLibrariesBuildOperationTests: CoreBasedTests {
553554
"INSTALL_GROUP": "",
554555
"INSTALL_MODE_FLAG": "",
555556
"SDK_STAT_CACHE_ENABLE": "NO",
556-
557-
"ASSETCATALOG_COMPILER_SKIP_APP_STORE_DEPLOYMENT": useAppStoreCodelessFrameworksWorkaround ? "NO" : "YES"
557+
"ASSETCATALOG_COMPILER_SKIP_APP_STORE_DEPLOYMENT": useAppStoreCodelessFrameworksWorkaround ? "NO" : "YES",
558+
"SKIP_EMBEDDED_FRAMEWORKS_VALIDATION": "YES",
558559
])
559560
],
560561
targets: [
@@ -1029,6 +1030,7 @@ fileprivate struct MergeableLibrariesBuildOperationTests: CoreBasedTests {
10291030
buildSettings: [
10301031
"INSTALL_PATH": "/Applications",
10311032
"MERGE_LINKED_LIBRARIES": "$(APP_MERGE_LINKED_LIBRARIES)", // Builds below can override APP_MERGE_LINKED_LIBRARIES to NO to disable merging.
1033+
"SKIP_EMBEDDED_FRAMEWORKS_VALIDATION": "YES",
10321034
]),
10331035
],
10341036
buildPhases: [
@@ -1396,6 +1398,7 @@ fileprivate struct MergeableLibrariesBuildOperationTests: CoreBasedTests {
13961398
TestBuildConfiguration(CONFIGURATION,
13971399
buildSettings: [
13981400
"INSTALL_PATH": "/Applications",
1401+
"SKIP_EMBEDDED_FRAMEWORKS_VALIDATION": "YES",
13991402
]),
14001403
],
14011404
buildPhases: [

Tests/SWBBuildSystemTests/XCFrameworkBuildOperationTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ fileprivate struct XCFrameworkBuildOperationTests: CoreBasedTests {
772772
"SKIP_INSTALL": "NO",
773773
"GENERATE_INFOPLIST_FILE": "YES",
774774
"SWIFT_VERSION": swiftVersion,
775+
"SKIP_EMBEDDED_FRAMEWORKS_VALIDATION": "YES",
775776
]
776777
)],
777778
targets: [

Tests/SWBTaskConstructionTests/PlatformTaskConstructionTests.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ fileprivate struct PlatformTaskConstructionTests: CoreBasedTests {
219219
// There should be one product validation task.
220220
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("AppTarget.app")) { task in
221221
task.checkRuleInfo(["Validate", "\(SRCROOT)/build/Debug-iphoneos/AppTarget.app"])
222-
task.checkCommandLine(["builtin-validationUtility", "\(SRCROOT)/build/Debug-iphoneos/AppTarget.app", "-infoplist-subpath", "Info.plist"])
222+
task.checkCommandLine(["builtin-validationUtility", "\(SRCROOT)/build/Debug-iphoneos/AppTarget.app", "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
223223

224224
task.checkInputs([
225225
.path("\(SRCROOT)/build/Debug-iphoneos/AppTarget.app"),
@@ -351,7 +351,7 @@ fileprivate struct PlatformTaskConstructionTests: CoreBasedTests {
351351
// There should be one product validation task.
352352
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("AppTarget.app")) { task in
353353
task.checkRuleInfo(["Validate", "\(SRCROOT)/build/Debug-iphonesimulator/AppTarget.app"])
354-
task.checkCommandLine(["builtin-validationUtility", "\(SRCROOT)/build/Debug-iphonesimulator/AppTarget.app", "-infoplist-subpath", "Info.plist"])
354+
task.checkCommandLine(["builtin-validationUtility", "\(SRCROOT)/build/Debug-iphonesimulator/AppTarget.app", "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
355355

356356
task.checkInputs([
357357
.path("\(SRCROOT)/build/Debug-iphonesimulator/AppTarget.app"),
@@ -439,7 +439,7 @@ fileprivate struct PlatformTaskConstructionTests: CoreBasedTests {
439439
// There should be one product validation task.
440440
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("AppTarget.app")) { task in
441441
task.checkRuleInfo(["Validate", "\(srcRoot.str)/build/Debug-iphoneos/AppTarget.app"])
442-
task.checkCommandLine(["builtin-validationUtility", "\(srcRoot.str)/build/Debug-iphoneos/AppTarget.app", "-infoplist-subpath", "Info.plist"])
442+
task.checkCommandLine(["builtin-validationUtility", "\(srcRoot.str)/build/Debug-iphoneos/AppTarget.app", "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
443443

444444
task.checkOutputs([
445445
.path("\(srcRoot.str)/build/Debug-iphoneos/AppTarget.app"),

Tests/SWBTaskConstructionTests/WatchTaskConstructionTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ fileprivate struct WatchTaskConstructionTests: CoreBasedTests {
392392

393393
// There should be one product validation task.
394394
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("Watchable WatchKit App.app")) { task in
395-
task.checkCommandLine(["builtin-validationUtility", builtWatchAppPath, "-infoplist-subpath", "Info.plist"])
395+
task.checkCommandLine(["builtin-validationUtility", builtWatchAppPath, "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
396396
}
397397

398398
// There should be no other tasks for this target.
@@ -498,7 +498,7 @@ fileprivate struct WatchTaskConstructionTests: CoreBasedTests {
498498

499499
// There should be one product validation task.
500500
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("Watchable.app")) { task in
501-
task.checkCommandLine(["builtin-validationUtility", builtHostIOSAppPath, "-infoplist-subpath", "Info.plist"])
501+
task.checkCommandLine(["builtin-validationUtility", builtHostIOSAppPath, "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
502502
}
503503

504504
// There should be no other tasks for this target.
@@ -681,7 +681,7 @@ fileprivate struct WatchTaskConstructionTests: CoreBasedTests {
681681

682682
// There should be one product validation task.
683683
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("Watchable WatchKit App.app")) { task in
684-
task.checkCommandLine(["builtin-validationUtility", builtWatchAppPath, "-infoplist-subpath", "Info.plist"])
684+
task.checkCommandLine(["builtin-validationUtility", builtWatchAppPath, "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
685685
}
686686

687687
// There should be no other tasks for this target.
@@ -788,7 +788,7 @@ fileprivate struct WatchTaskConstructionTests: CoreBasedTests {
788788

789789
// There should be one product validation task.
790790
results.checkTask(.matchTarget(target), .matchRuleType("Validate"), .matchRuleItemBasename("Watchable.app")) { task in
791-
task.checkCommandLine(["builtin-validationUtility", builtHostIOSAppPath, "-infoplist-subpath", "Info.plist"])
791+
task.checkCommandLine(["builtin-validationUtility", builtHostIOSAppPath, "-shallow-bundle", "-infoplist-subpath", "Info.plist"])
792792
}
793793

794794
// There should be no other tasks for this target.

0 commit comments

Comments
 (0)